[Performance] Add opt-in fast_stack for TensorDict - #1694
Merged
Conversation
Initial strict baseline: lockstep zip traversal of N TensorDicts that share an identical structure. Skips per-leaf string lookups and Python shape validation; falls back via RuntimeError when any precondition fails. Speedups of 4-9x on homogeneous collector-shaped inputs. Strict preconditions (intentionally narrow for first cut, will be loosened): plain TensorDict only (no lazy/persistent/tensorclass), identical key set, key insertion order, batch_size and device, only regular torch.Tensor leaves.
When inputs have the same key set but were built in different insertion orders, drive iteration from td0 and look up by key in the others mid-traversal. Lockstep zip remains the fast path when orders match. ~3-6% perf regression on the lockstep-matching benchmark (an extra Python frame from factoring out _stack_leaf), in exchange for handling a real-world case (TDs built independently often share keys but not order).
Relaxes the leaf check from 'type(v) is Tensor' to 'isinstance(v, Tensor)' plus dedicated branches for UninitializedTensorMixin (-> _stack_uninit_params) and UnbatchedTensor / other _pass_through tensors (-> _stack_non_tensor). The hot 'type is Tensor' branch is unchanged; the dispatch only fires for subclasses, so the homogeneous-Tensor benchmark is flat vs prior commit. Now handles: nn.Parameter, MemoryMappedTensor, UninitializedParameter, UninitializedBuffer, UnbatchedTensor.
Adds dispatch for: - tensorclass at root: unwrap _tensordict, recurse, re-wrap with _from_tensordict. - tensorclass at nested leaf positions: same treatment. - _pass_through types (NonTensorData, NonTensorStack, MetaData) at root and at leaves: route to _stack_non_tensor. Mirrors the corresponding branches in the slow _stack path. Plain TensorDict / Tensor hot path is untouched, benchmark flat vs prior. Tensorclass instances whose _tensordict is not a plain TensorDict (e.g. lazy-stacked tensorclass) bail to the slow path — handling them is part of the upcoming lazy-stack work.
…ested Adds _stack_homogeneous_lazy that mirrors the lazy branch in _stack (of _torch_func.py): all inputs must be LazyStackedTensorDict with matching stack_dim and inner TD count. The new lazy stack_dim shifts +1 if the new dim lands before the existing lazy dim, otherwise the inner stack dim shifts -1. Tests cover lazy at root, lazy nested at a leaf, and a stack_dim mismatch fallback. Hot path benchmark flat vs prior commit. NonTensorData / NonTensorStack support landed in the previous commit (via _pass_through_cls) so all five user-listed cases are covered: key ordering, tensorclass, lazy stack, non-tensor data, tensor subclasses.
Skips a Python frame per leaf when the value is a regular Tensor. ~3% speedup on the (8 TDs, 20 leaves) hot benchmark; flat on larger-N cases dominated by the torch.stack call itself.
…sorDict Replaces the strict 'inner is plain TensorDict' check on the tensorclass branches with a recursive _stack_homogeneous call so the inner _tensordict can be a TensorDict, a LazyStackedTensorDict, or another tensorclass.
The previous error message dated to the strict baseline and falsely claimed inputs must be plain TensorDict with regular Tensor leaves. Update it to reflect what fast_stack actually requires now and to list common reasons it would bail (mixed root types, mismatched batch_size/device/key set, lazy stack_dim mismatch, unsupported container types). Add explicit negative tests for cases that were previously only implicit: mixed root types (TD + LazyStack), PersistentTensorDict, and TD-level device mismatch.
- Add fast_stack classmethod entry to tensorclass.pyi (test_tensorclass_stub_methods CI check requires every public classmethod on TensorDictBase to be present here). - Reformat _stack_homogeneous_inner docstring: split into a one-line summary plus body to satisfy pydocstyle D205/D415. - Drop empty f-string prefix on a NonTensorData literal (flake8 F541). - ufmt reformat in test_tensordict.py.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
tensordict.fast_stack(andTensorDictBase.fast_stackclassmethod), a strict, opt-in variant ofstackthat walks all input TensorDicts in lockstep instead of doing per-leaf string lookups across each input. Same shape as theupdate_at_→copy_at_split: both functions remain available, andfast_stackraisesRuntimeErrorwhen its preconditions aren't met so callers can decide between the two explicitly.tensordict.stackis unchanged. The fast path is purely additive.Speedup
Benchmark: stacking N homogeneous TensorDicts with M leaves at depth levels. Mean per call (lower is better):
The win comes from skipping the per-leaf `_get_str` calls and per-leaf Python shape validation in `_stack`. Speedup grows with N (number of TDs) and depth (nesting).
What fast_stack accepts
What fast_stack raises on (use stack instead)
Commit history
The branch is split into 8 logical commits for review. The first lands a strict baseline; the next six progressively loosen support (key ordering, tensor subclasses, tensorclass + pass-through, lazy stack, hot-path inlining, tensorclass-wrapping-lazy); the last cleans up the error message and adds negative tests.
Test plan
Stacked on
This PR is stacked on #1692. Once #1692 merges this should auto-rebase onto main.
🤖 Generated with Claude Code