Tensor¶
The danling.tensors module provides utilities for handling tensors with variable lengths in batched operations.
The core feature is the NestedTensor class which allows efficient representation of sequences of different lengths without excessive padding.
Overview¶
In many deep learning tasks, especially those involving sequences (text, time series, etc.), each example in a batch may have a different length. Traditional approaches include:
- Padding: Adding placeholder values to make all examples the same length (wastes computation)
- Bucketing: Grouping similar-length examples (complicates training)
- Processing one sample at a time: Slow and inefficient
The NestedTensor solves these problems by providing:
- A way to store variable-length tensors in a single object
- Automatic padding and mask generation for efficient computation
- Transparent access to the original tensors or padded representations
- Support for 615+ PyTorch operations via a multi-level dispatch system
Key Components¶
NestedTensor: Main class for handling variable-length tensors in a batch.PNTensor: A tensor wrapper that can be converted to NestedTensor by PyTorch DataLoader.tensor(): Function to create aPNTensorobject (similar totorch.tensor()).NestedTensorFuncRegistry: Registry fortorch.*andF.*dispatch handlers.NestedTensorAtenRegistry: Registry foratendispatch handlers.
Quick Start¶
Creating a NestedTensor¶
| Python | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Creating from Non-Tensor Data¶
| Python | |
|---|---|
1 2 3 4 5 6 | |
Converting to torch.nested_tensor¶
| Python | |
|---|---|
1 2 3 4 5 6 7 8 | |
Working with NestedTensor¶
Operations¶
NestedTensor supports many PyTorch operations:
| Python | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Unpacking¶
You can easily convert back to original tensors:
| Python | |
|---|---|
1 2 3 4 5 6 7 8 | |
Architecture¶
NestedTensor uses a packed representation that stores all variable-length elements concatenated into a single contiguous tensor, tracked by offset metadata:
_values: All element tensors concatenated along dim 0 (e.g., shape[total_elements, *])_offsets: Cumulative element counts, shape(B+1,), marking where each element starts/ends_physical_shape: Per-element shapes, shape(B, ndim), recording each element’s original dimensions
This avoids the waste of padding in the internal representation while allowing efficient batch operations.
Dispatch System¶
Operations on NestedTensor are handled by a three-tier dispatch system, ordered from fastest to most flexible:
Level 1 — Aten dispatch (aten_functions.py, 285 ops): Operates directly on the packed _values tensor via __torch_dispatch__. This is the fastest path — no Python loops, no unpacking. Used for elementwise ops (add, mul, sin, exp, …), reductions, softmax, layer_norm, etc.
Level 2 — Torch function dispatch (torch_functions.py, 217 ops): Intercepts torch.* calls via __torch_function__. Handles ops that need dimension translation (e.g., torch.flatten, torch.softmax with non-default dim), multi-operand dispatch (e.g., torch.einsum), per-element matrix ops (e.g., torch.det, torch.linalg.svd), and fused attention (torch._native_multi_head_attention, torch._transformer_encoder_layer_fwd).
Level 3 — NN function dispatch (nn_functions.py, 113 ops): Also via __torch_function__, handles torch.nn.functional.* ops including convolutions, pooling, normalization, attention, embedding, activations (F.relu, F.gelu, F.silu, …), and loss functions. Transformer-hot ops use packed fast paths; activation handlers strip inplace flags to preserve autograd on the wrapper subclass.
Fallback: Any aten op without an explicit handler falls back to per_element_fallback, which unpacks to individual tensors, applies the op element-by-element, and repacks. Under torch.compile, DanLing prefers explicit failure over silently entering those eager-only fallbacks.
| Text Only | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Key Internal Helpers¶
_from_packed(values, offsets, shape_tensor, ...): Direct constructor from packed representation. Used by all aten handlers to build results without function call overhead._map_storage_serial(input, fn): Per-element slow path — appliesfnto each element via_unpack(). Used when ops need individual element dimensionality._translate_non_batch_dim(nt, dim): Converts a NestedTensor dim index to the corresponding element-level dim (skipping the batch dimension).
Integration with PyTorch DataLoader¶
The PNTensor class makes it easy to use NestedTensor with PyTorch’s DataLoader:
| Python | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
Advanced Usage¶
Custom Collation¶
If you need more control over collation:
| Python | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
Working with PyTorch Models¶
NestedTensor works natively with PyTorch’s built-in transformer and vision models — no padding or masks needed:
| Python | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
For models that require padded input (e.g., HuggingFace transformers), materialize with .tensor and .mask:
| Python | |
|---|---|
1 2 3 4 | |
Extending with New Operations¶
You can register new torch.* functions to work with NestedTensor:
| Python | |
|---|---|
1 2 3 4 5 6 7 8 | |
For ops that are purely elementwise on the packed data, register at the aten level instead:
| Python | |
|---|---|
1 2 3 4 5 6 7 8 | |
packed_like requires the packed output shape to remain unchanged. Operations
that retain the ragged lengths but replace or add a static feature tail can use
packed_with_static_tail:
| Python | |
|---|---|
1 2 | |
For a canonical reference, the ragged dimensions are a leading logical prefix,
packed_dim_order is the identity, and the packed-value tail may have any
rank. The packed leading dimension is unchanged, while
atom_values.shape[1:] becomes the new static tail.
An explicit tensor-backed layout with one non-leading logical ragged dimension
may also replace its static dimensions, provided their packed rank does not
change. Tail sizes follow packed_dim_order and return to their original
logical positions. For example, sampled single representations remain
sample-major logically while token-major in packed storage:
| Python | |
|---|---|
1 2 3 | |
Explicit ragged layouts retain tensor-backed row splits whenever
packed_dim_order begins with ragged_dims. This includes an explicit leading
single-ragged layout such as ragged_dims=(0,); the ragged dimensions also need
not be a leading logical prefix. For example, elements shaped (1, H, N_i, N_i)
with ragged_dims=(2, 3) pack in (2, 3, 0, 1) order and remain tensor-backed.
These row splits are available through
ragged_level_offsets(level) and travel with packed_like, shape-preserving
static-tail operations, autograd transforms, and serialization. Because
per-sample lengths are tensor inputs rather than Python flatten metadata,
fixed-rank layouts such as (N_i, N_i, C) and (S, N_i, C) can reuse one
torch.compile(dynamic=True) graph across different N_i. This does not
expose the private child names or add a general packed constructor. Inferred
list construction without an explicit ragged_dims declaration keeps its
existing Python-metadata contract.
When an operator produces a new one-dimensional ragged topology, use a concrete CPU integer lengths tensor:
| Python | |
|---|---|
1 2 | |
packed_with_lengths requires one non-negative length per batch element and
token_lengths.sum() == token_values.shape[0]. It constructs a canonical
leading ragged dimension and uses the remaining packed-value dimensions as the
static tail.
Square pair operators can rebuild two canonical ragged dimensions from the same lengths without materializing Python element shapes:
| Python | |
|---|---|
1 2 | |
Here pair_values.shape[0] must equal token_lengths.square().sum(). The
result persistently carries both CSR row-split levels as tensor metadata, so a
fixed batch size can reuse one dynamic compiled graph across different square
layouts. Rectangular pair operators use two independent length vectors:
| Python | |
|---|---|
1 2 | |
Here the packed leading length is (query_lengths * key_lengths).sum() and
both ragged maxima remain tensor-backed graph inputs. All three length-based
reconstruction methods are zero-copy with respect
to their packed values and preserve dtype, device, strides, pinning, subclass,
runtime configuration, and autograd history. Reconstruction stores dynamic
lengths in tensor-backed offsets and shape metadata rather than Python tuples,
so its tracing cost does not grow with metadata rank. The current compiled
contract covers structural index consumers, runtime-validated same-layout
elementwise operations, static-tail broadcasting, and static-tail
normalization. Tensor-backed view/index remapping, padding,
broadcast_tensors, global-query einsum, and ragged-dimension softmax remain
staged; these paths raise an explicit compile error instead of materializing
Python metadata or silently assuming a layout.
For pairwise distances between explicit canonical (P_i, M) and (R_i, M)
elements, use left.cdist(right) in compiled code. A single-sample batch calls
native cdist directly; larger compiled batches use a registered segmented
operation that runs native cdist independently over the packed samples.
Eager mode issues the same native calls directly to avoid custom-op dispatcher
overhead. The result is
reconstructed as (P_i, R_i) through the rectangular metadata above. It does not
materialize padding, a cross-sample matrix, pair index tensors, or
sum(P_i * R_i) x M gathered operands. Eager torch.cdist(left, right) remains
supported, but the method form is the AOT/Inductor entry point because PyTorch
treats the built-in function as an opaque graph leaf.
For cumulative products along a canonical ragged dimension, use
input.cumprod(dim) in compiled training code. The segmented operator invokes
the current device’s native cumprod and backward kernel independently for
each packed sample. It therefore preserves native rounding, underflow, zero,
and non-finite behavior without padding or reassociating the product. Eager
torch.cumprod(input, dim) remains supported.
The result can remain a NestedTensor throughout a compiled model or cross a
compiled/eager training boundary as a wrapper-only output. DanLing preserves
the outer wrapper’s AOTAutograd edge and projects it back to result.concat
without padding or copying, so both wrapper outputs and direct packed outputs
remain differentiable.
They intentionally do not expose the general private packed constructor.
packed_offsets() returns the boundaries of complete logical batch elements
in the flattened leading dimension of concat. It is the public operator
metadata interface for segmented kernels: for (N_i, N_i, C) elements it
returns cumulative N_i * N_i cell counts, whereas
ragged_level_offsets(level) returns row splits within the ragged hierarchy.
The same contract applies to single-ragged and non-leading-ragged layouts.
The canonical CPU integer tensor is returned without a copy; optional device
and dtype conversions are cached on the NestedTensor instance. An explicit
accelerator index is required for device caching; index-less device requests
retain PyTorch’s current-device semantics and are converted on every call.
element_sizes() returns the exact logical shape of every batch element as a
CPU torch.int64 tensor with shape (batch_size, element_rank). Columns remain
in logical element-dimension order regardless of batch_first or
packed_dim_order, so zero-volume shapes such as (0, 3) and (0, 7) remain
distinguishable even when their packed offsets coincide. The method returns the
canonical tensor-backed metadata without a copy. It intentionally has no
device or dtype conversion arguments; consumers that need another placement or
integer width can apply .to(...) explicitly.
torch.repeat_interleave(input, repeats, dim=batch_dim) and
input.repeat_interleave(repeats, dim=batch_dim) accept a non-negative integer
repeats and duplicate complete logical batch elements in their original
order. They repeat packed sample segments and every tensor-backed ragged
row-split level directly, without padding or per-element Python dispatch.
input.repeat_batch(repeats) is the equivalent explicit batch operation and
the canonical AOTAutograd-safe entry for compiled model code. Explicit single-
and multi-ragged layouts can reuse one
torch.compile(fullgraph=True, dynamic=True) graph across different ragged
lengths, and gradients from repeated samples accumulate into the original
packed values. Tensor-valued batch repeat counts are intentionally unsupported;
non-batch dimensions retain ordinary per-element torch.repeat_interleave
semantics.
packed_dim_order exposes the read-only mapping from logical element dimensions
to physical packed-storage order when an operator needs to validate its layout.
Critical packed paths can use nested_execution_guard from danling.tensors in
tests or diagnostics to reject iteration, per-element fallback, padded
materialization, or dense repacking instead of silently accepting a slow path.
Benchmarks¶
Benchmarked on a single NVIDIA B200 180GB GPU with PyTorch 2.11, bfloat16.
Run with: python scripts/benchmark_nested_tensor.py
IMDB Training¶
Real workload benchmark from examples/tensors/imdb.py, using a BERT-large-shaped torch.nn.TransformerEncoder on IMDB with long variable-length sequences.
Config: bert-large-uncased, 2 epochs, batch size 32, max length 8192, d_model=1024, nhead=16, num_layers=24
| Metric | NestedTensor | Padded | Result |
|---|---|---|---|
| Training step compute (forward + backward, all epochs) | 154819.4 ms |
306926.7 ms |
1.98x faster |
| Peak extra CUDA memory per training step | 12.68 GiB |
74.67 GiB |
83% lower |
This run measured nearly 2x faster model compute and an 83% reduction in peak extra CUDA memory for the NestedTensor path.
Note: This benchmark compares native PyTorch
nn.TransformerEncoderexecution on NestedTensor vs padded input. The timing is model forward+backward compute, not full end-to-end wall clock including tokenization, data loading, or validation.
Models¶
Synthetic model benchmarks covering TransformerEncoder, TransformerDecoder, Transformer, and ResNet-50 across varying occupancy levels on a single NVIDIA B200 180GB GPU.
| Model | Mode | Occ. | Padded (eager) | Padded (compiled) | DanLing (eager) | DanLing (compiled) | DL vs Padded | DL vs Compiled |
|---|---|---|---|---|---|---|---|---|
| TransformerEncoder | Infer | 20% | 2.70 ms | 39.98 ms | 5.00 ms | 1.10 ms | 0.54x | 7.99x |
| TransformerEncoder | Train | 20% | 26.95 ms | 19.39 ms | 8.60 ms | ERR ms | 3.13x | 2.25x |
| TransformerEncoder | Infer | 35% | 3.68 ms | 38.93 ms | 5.20 ms | 1.79 ms | 0.71x | 7.48x |
| TransformerEncoder | Train | 35% | 27.05 ms | 19.46 ms | 11.55 ms | ERR ms | 2.34x | 1.68x |
| TransformerEncoder | Infer | 77% | 6.95 ms | 36.39 ms | 5.52 ms | 4.45 ms | 1.26x | 6.59x |
| TransformerEncoder | Train | 77% | 27.15 ms | 19.71 ms | 21.37 ms | ERR ms | 1.27x | 0.92x |
| TransformerDecoder | Infer | 20% | 47.38 ms | 10.86 ms | 9.27 ms | 1.79 ms | 5.11x | 1.17x |
| TransformerDecoder | Train | 20% | 45.86 ms | 33.16 ms | 15.00 ms | ERR ms | 3.06x | 2.21x |
| TransformerDecoder | Infer | 35% | 46.33 ms | 10.91 ms | 9.19 ms | 2.91 ms | 5.04x | 1.19x |
| TransformerDecoder | Train | 35% | 45.98 ms | 33.30 ms | 19.67 ms | ERR ms | 2.34x | 1.69x |
| TransformerDecoder | Infer | 77% | 43.79 ms | 11.02 ms | 9.41 ms | 7.47 ms | 4.65x | 1.17x |
| TransformerDecoder | Train | 77% | 46.18 ms | 33.50 ms | 36.00 ms | ERR ms | 1.28x | 0.93x |
| Transformer | Infer | 21% | 47.99 ms | 48.51 ms | 14.81 ms | 2.93 ms | 3.24x | 3.27x |
| Transformer | Train | 21% | 68.79 ms | 47.25 ms | 24.53 ms | ERR ms | 2.80x | 1.93x |
| Transformer | Infer | 40% | 47.54 ms | 47.52 ms | 14.26 ms | 5.39 ms | 3.34x | 3.33x |
| Transformer | Train | 40% | 69.03 ms | 47.49 ms | 32.72 ms | ERR ms | 2.11x | 1.45x |
| Transformer | Infer | 84% | 48.37 ms | 45.12 ms | 15.76 ms | 12.51 ms | 3.07x | 2.86x |
| Transformer | Train | 84% | 69.52 ms | 48.81 ms | 59.88 ms | ERR ms | 1.16x | 0.82x |
| ResNet-50 | Infer | 41% | 42.42 ms | ERR ms | 219.49 ms | ERR ms | 0.19x | N/A |
| ResNet-50 | Train | 41% | 221.80 ms | ERR ms | 513.08 ms | ERR ms | 0.43x | N/A |
| ResNet-50 | Infer | 52% | 42.56 ms | ERR ms | 246.27 ms | ERR ms | 0.17x | N/A |
| ResNet-50 | Train | 52% | 221.37 ms | ERR ms | 551.29 ms | ERR ms | 0.40x | N/A |
| ResNet-50 | Infer | 81% | 42.60 ms | ERR ms | 318.03 ms | ERR ms | 0.13x | N/A |
| ResNet-50 | Train | 81% | 229.88 ms | ERR ms | 709.83 ms | ERR ms | 0.32x | N/A |
Note: ResNet-50 uses per-element dispatch (each image processed individually through conv/pool/BN layers). Inference is slower than padded due to per-element repacking overhead. BatchNorm statistics are computed correctly across all elements via concatenated storage.
Operators¶
Synthetic operator benchmarks covering common transformer-hot ops and tensor primitives across padded tensors, DanLing NestedTensor, and torch.nested.
| Operator | Occ. | Padded (eager) | Padded (compiled) | DanLing (eager) | DanLing (compiled) | torch.nested (eager) | torch.nested (compiled) | DL vs Padded | DL vs torch.nested |
|---|---|---|---|---|---|---|---|---|---|
| F.linear | 35% | 0.05 ms | 0.05 ms | 0.13 ms | 0.17 ms | 0.13 ms | 0.39 ms | 0.27x | 2.29x |
| F.layer_norm | 35% | 0.17 ms | 0.05 ms | 0.10 ms | 0.16 ms | 0.20 ms | 0.38 ms | 0.28x | 2.36x |
| F.relu | 35% | 0.05 ms | 0.05 ms | 0.09 ms | 0.16 ms | 0.10 ms | 0.37 ms | 0.29x | 2.38x |
| F.gelu | 35% | 0.08 ms | 0.08 ms | 0.08 ms | 0.16 ms | 0.10 ms | 0.36 ms | 0.51x | 2.33x |
| F.softmax | 35% | 0.12 ms | 0.06 ms | 0.11 ms | 0.16 ms | 0.15 ms | 0.37 ms | 0.36x | 2.31x |
| F.embedding | 35% | 0.04 ms | 0.04 ms | 0.15 ms | 0.16 ms | 0.14 ms | 0.36 ms | 0.25x | 2.27x |
| torch.matmul | 35% | 0.04 ms | 0.05 ms | 0.17 ms | 0.17 ms | 0.15 ms | 0.39 ms | 0.28x | 2.31x |
| torch.add | 35% | 0.05 ms | 0.05 ms | 0.12 ms | 0.15 ms | 0.11 ms | 0.36 ms | 0.29x | 2.36x |
Tensor API¶
- NestedTensor: variable-length tensor batches and their operations.
- PNTensor and tensor: tensor markers for collation.
- Dispatch functions and registries: supported dispatch layers and extension interfaces.