Skip to content

[Performance] Add opt-in fast_stack for TensorDict - #1694

Merged
vmoens merged 9 commits into
pytorch:prepare-copy-at-writerfrom
vmoens:add-fast-stack
May 8, 2026
Merged

[Performance] Add opt-in fast_stack for TensorDict#1694
vmoens merged 9 commits into
pytorch:prepare-copy-at-writerfrom
vmoens:add-fast-stack

Conversation

@vmoens

@vmoens vmoens commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds tensordict.fast_stack (and TensorDictBase.fast_stack classmethod), a strict, opt-in variant of stack that walks all input TensorDicts in lockstep instead of doing per-leaf string lookups across each input. Same shape as the update_at_copy_at_ split: both functions remain available, and fast_stack raises RuntimeError when its preconditions aren't met so callers can decide between the two explicitly.

tensordict.stack is 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):

Shape (N, M, depth) stack fast_stack Speedup
(8, 20, 0) 127 µs 32 µs 4.0x
(64, 20, 0) 869 µs 151 µs 5.7x
(8, 10, 2) 207 µs 34 µs 6.1x
(64, 10, 2) 1,340 µs 151 µs 8.9x

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

  • Plain TensorDict at root, with arbitrary nesting.
  • Tensorclass at root or at any nested leaf (including tensorclass-wrapping-LazyStackedTensorDict).
  • LazyStackedTensorDict at root or nested, when all inputs agree on stack_dim and inner-TD count.
  • NonTensorData / NonTensorStack / MetaData (via _pass_through_cls).
  • nn.Parameter, MemoryMappedTensor, UninitializedParameter, UninitializedBuffer, UnbatchedTensor leaves.
  • Differing key insertion orders across inputs (lockstep zip when orders match, td0-driven dict lookup mid-traversal on mismatch).

What fast_stack raises on (use stack instead)

  • Mixed root types (e.g. TensorDict + LazyStackedTensorDict).
  • Mismatched batch_size, device, or key set.
  • LazyStackedTensorDict with mismatched stack_dim or inner-TD count.
  • PersistentTensorDict, _SubTensorDict, TensorDictParams, TypedTensorDict, or any other TensorDict subclass.
  • API kwargs not implemented: out=, device=, strict, contiguous, maybe_dense_stack.
  • lazy_legacy=True global mode is ignored — fast_stack always returns dense.

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

  • pytest test/test_tensordict.py -k fast_stack — 30 new tests pass (1 h5py-skipped). Covers equivalence vs torch.stack on flat/nested TDs, tensorclass at root and nested, tensorclass-wrapping-lazy, lazy stack at root and nested, NonTensorData at root and nested, nn.Parameter / MemoryMappedTensor / UninitializedParameter / UnbatchedTensor leaves, key-order mismatch (positive), and rejections for mixed root types / PersistentTensorDict / device mismatch / batch_size mismatch / key set mismatch / lazy stack_dim mismatch / dim out of range / empty input.
  • pytest test/test_tensordict.py -k "stack or Stack" — 1684 stack-related tests pass total; existing stack paths unchanged.
  • torch.compile(fullgraph=True) works on fast_stack over nested TDs.
  • Benchmark via benchmarks/common/common_ops_test.py::test_fast_stack_homogeneous parametrized over (num_tds, num_keys, depth).

Stacked on

This PR is stacked on #1692. Once #1692 merges this should auto-rebase onto main.

🤖 Generated with Claude Code

vmoens added 8 commits May 7, 2026 16:34
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.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 7, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation Benchmarks Test Performance labels May 7, 2026
- 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.
@vmoens
vmoens merged commit 158ea5d into pytorch:prepare-copy-at-writer May 8, 2026
69 checks passed
@vmoens
vmoens deleted the add-fast-stack branch May 8, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Benchmarks CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. documentation Improvements or additions to documentation Performance tensorclass Test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant