Common surprises#
A TensorDict has the methods of a dict and the operations of a tensor.
Where the two models differ, it follows the tensor, and new users then get a
result that they did not expect. This page lists the cases that users
report most often. Each one has a short example and a link to the page that
explains it.
Run the examples in order in one Python session:
>>> import dataclasses
>>> import torch
>>> from tensordict import NonTensorData, TensorClass, TensorDict, is_floating
len, bool, and == do not look at the keys#
len(td) is the size of the first batch dimension, and 0 for a tensordict
without batch dimensions, whatever its keys. len(td.keys()) counts the
keys. bool(td) raises. td == other compares the entries and returns a
tensordict of booleans: call .all() on it to get a Python bool:
>>> td = TensorDict({"a": torch.zeros(3, 2)}, batch_size=[3])
>>> len(td), len(TensorDict({"a": torch.zeros(3, 2)})), len(td.keys())
(3, 0, 1)
>>> bool(td)
Traceback (most recent call last):
...
RuntimeError: Converting a tensordict to boolean value is not permitted. ...
>>> (td == td.clone()).batch_size
torch.Size([3])
>>> (td == td.clone()).all()
True
A TensorDict is not exactly a dict lists the other operations that differ from a
dict.
The batch size is not inferred#
If you do not give a batch size, it is torch.Size([]): TensorDict does
not infer it from the entries.
auto_batch_size_() infers it:
>>> td = TensorDict({"x": torch.zeros(3, 4), "name": "run-1"})
>>> td.batch_size
torch.Size([])
>>> td.auto_batch_size_().batch_size
torch.Size([3, 4])
A Python int, float, or bool becomes a tensor with no
dimensions, and a string becomes a NonTensorData. A
tensor with no dimensions has no batch dimensions to share, so it keeps
auto_batch_size_ from inferring any:
>>> td = TensorDict({"x": torch.zeros(3, 4), "scale": 2.0, "name": "run-1"})
>>> type(td.get("scale")).__name__, type(td.get("name")).__name__
('Tensor', 'NonTensorData')
>>> td.auto_batch_size_().batch_size
torch.Size([])
For the same reason, a tensordict with batch dimensions raises for a Python
number. To store one value for the whole batch, wrap it in a
NonTensorData:
>>> td = TensorDict({"x": torch.zeros(3, 4)}, batch_size=[3])
>>> td["scale"] = 2.0
Traceback (most recent call last):
...
RuntimeError: batch dimension mismatch, got self.batch_size=torch.Size([3]) and value.shape=torch.Size([]) for key 'scale'. ...
>>> td["scale"] = NonTensorData(2.0)
>>> td["scale"]
2.0
Batch size, shapes, and indexing explains the batch size, and Non-tensor data and unbatched entries explains the values that are not tensors.
Broadcasting aligns with the batch dimensions#
A tensor operand aligns with the batch dimensions of the tensordict, not
with the last dimensions of each leaf. Here the tensor of shape (3,)
multiplies the rows of a leaf of shape (3, 2):
>>> td = TensorDict({"a": torch.ones(3, 2)}, batch_size=[3])
>>> (td * torch.tensor([0.0, 1.0, 2.0]))["a"]
tensor([[0., 0.],
[1., 1.],
[2., 2.]])
To scale the last dimensions of each leaf, use
td.apply(lambda x: x * w). If two tensordicts have batch sizes with
different numbers of dimensions, the result can depend on the order of the
operands:
>>> td0 = TensorDict({"x": torch.ones(3, 2)}, batch_size=[])
>>> td1 = TensorDict({"x": torch.ones(3, 2)}, batch_size=[3])
>>> (td1 + td0)["x"].shape, (td0 + td1)["x"].shape
(torch.Size([3, 2]), torch.Size([3, 3, 2]))
To get the same result in both orders, give the two tensordicts the same
batch size first, for example with td0.expand(3). Arithmetic, apply, reductions, and gradients
gives the broadcasting rules.
td * t breaks the graph under torch.compile#
Inside torch.compile(), an operator between a tensordict and a tensor,
such as td * t or td + t, breaks the graph. With fullgraph=True,
it raises. Call the method instead, such as
mul() or add().
An operator with a Python number or with another tensordict compiles:
>>> t = torch.tensor([0.0, 1.0, 2.0])
>>> torch.compile(lambda td, t: td.mul(t), fullgraph=True)(td, t)["a"]
tensor([[0., 0.],
[1., 1.],
[2., 2.]])
>>> torch.compile(lambda td, t: td * t, fullgraph=True)(td, t)
Traceback (most recent call last):
...
torch._dynamo.exc.Unsupported: ...
torch.compile, torch.func, torch.export, and CUDA graphs lists the other patterns that break the graph.
set, set_, update_, and locks#
td[key] = value and set() replace the
tensor under the key. set_() and
update_() copy the values into the existing
tensor, and raise a KeyError for a missing key:
>>> td = TensorDict({"pos": torch.zeros(3)}, batch_size=[3])
>>> old = td["pos"]
>>> td["pos"] = torch.ones(3)
>>> old # replaced: the old tensor did not change
tensor([0., 0., 0.])
>>> old = td["pos"]
>>> _ = td.set_("pos", torch.full((3,), 2.0))
>>> old # copied into the same tensor
tensor([2., 2., 2.])
>>> td.update_({"vel": torch.zeros(3)})
Traceback (most recent call last):
...
KeyError: 'The key vel was not found in the dest tensordict.'
A locked tensordict raises for a new key, a replaced entry, and a removed
one. It accepts set_, update_, and in-place operations, which write into
the existing tensors. memmap and consolidate return a locked
tensordict:
>>> _ = td.lock_()
>>> td["pos"] = torch.ones(3)
Traceback (most recent call last):
...
RuntimeError: Cannot modify locked TensorDict. ...
>>> _ = td.set_("pos", torch.ones(3))
>>> td["pos"].add_(1)
tensor([2., 2., 2.])
td["pos"] += 1 raises on a locked tensordict, but only after it changed
the tensor: Python runs it as td["pos"] = td["pos"].__iadd__(1). Use
td["pos"].add_(1) instead. Write an entry compares the ways to write
an entry, and Locking and temporary changes lists what a lock blocks.
to(dtype) casts every leaf#
td.to(dtype) also casts the integer and boolean leaves, such as labels
and masks. cast_filter selects the leaves to cast:
is_floating() selects the floating-point leaves only:
>>> td = TensorDict(
... {"x": torch.zeros(2), "label": torch.arange(2), "mask": torch.ones(2, dtype=torch.bool)},
... batch_size=[2],
... )
>>> {k: v.dtype for k, v in td.to(torch.float16).items()}
{'x': torch.float16, 'label': torch.float16, 'mask': torch.float16}
>>> {k: v.dtype for k, v in td.to(torch.float16, cast_filter=is_floating).items()}
{'x': torch.float16, 'label': torch.int64, 'mask': torch.bool}
The dtype methods, such as td.half(), take cast_filter too.
Devices, dtypes, and memory explains devices and dtypes.