TensorDict#
A dict[str, torch.Tensor] that behaves like one tensor.
PyTorch code that handles groups of tensors, such as a training batch, a particle system, or a batch of graphs, repeats every operation for every tensor:
def shuffle_and_split(images, labels, masks, weights):
perm = torch.randperm(len(images))
images, labels, masks, weights = images[perm], labels[perm], masks[perm], weights[perm]
half = len(images) // 2
first = images[:half], labels[:half], masks[:half], weights[:half]
second = images[half:], labels[half:], masks[half:], weights[half:]
return first, second
With a TensorDict, the group is one object, and the
function body is two lines:
>>> import torch
>>> from tensordict import TensorDict
>>> def shuffle_and_split(batch):
... batch = batch[torch.randperm(len(batch))]
... return batch.split(len(batch) // 2)
Build a TensorDict from the four tensors, then call the function:
>>> batch = TensorDict(
... {
... "images": torch.randn(32, 3, 64, 64),
... "labels": torch.randint(10, (32,)),
... "masks": torch.ones(32, 64, 64, dtype=torch.bool),
... "weights": torch.rand(32),
... },
... batch_size=[32], # the leading dimension that all tensors share
... )
>>> first, second = shuffle_and_split(batch)
>>> first["images"].shape
torch.Size([16, 3, 64, 64])
The new function names no tensor, so it does not change when you add one.
TensorClass: fields and methods, like a dataclass#
TensorClass is the class form of the same idea. You
declare typed fields and write methods, as for a dataclass. A method that you
write once runs on one item or on a batch of any shape, and it compiles with
fullgraph=True:
>>> from tensordict import TensorClass
>>> class Particles(TensorClass):
... pos: torch.Tensor # [*batch, 3]
... vel: torch.Tensor # [*batch, 3]
... mass: torch.Tensor # [*batch]
...
... def step(self, force: torch.Tensor, dt: float) -> "Particles":
... vel = self.vel + dt * force / self.mass.unsqueeze(-1)
... pos = self.pos + dt * vel
... return Particles(pos=pos, vel=vel, mass=self.mass, batch_size=self.batch_size)
>>> systems = Particles( # 64 systems of 1,000 particles each
... pos=torch.randn(64, 1000, 3),
... vel=torch.zeros(64, 1000, 3),
... mass=torch.ones(64, 1000),
... batch_size=[64, 1000],
... )
>>> systems = systems.step(torch.randn(64, 1000, 3), dt=0.01) # all 64 systems
>>> one = systems[0].step(torch.randn(1000, 3), dt=0.01) # one system
>>> type(one).__name__, one.batch_size
('Particles', torch.Size([1000]))
>>> torch.stack([systems, systems]).batch_size
torch.Size([2, 64, 1000])
>>> step = torch.compile(Particles.step, fullgraph=True)
>>> step(systems, torch.randn(64, 1000, 3), 0.01).batch_size
torch.Size([64, 1000])
>>> systems = systems.to("cuda" if torch.cuda.is_available() else "cpu")
The fields are class attributes, so your editor completes systems.pos.
Saving works as for a TensorDict.
TensorClass: a dataclass of tensors explains TensorClass, and
Which container should I use? helps you choose between the two forms.
TensorDict: keys at run time, like a dict#
You add and read keys at run time, as in a dict. A dict value becomes
a nested TensorDict, and a tuple of keys reads its entries. The tensors share
the batch size, so indexing, reshaping, stacking, and .to(device) apply to
every tensor at once, nested ones too:
>>> batch["meta"] = {"id": torch.arange(32)}
>>> batch[:8]["meta", "id"].shape
torch.Size([8])
>>> batch.reshape(4, 8)["images"].shape
torch.Size([4, 8, 3, 64, 64])
>>> torch.stack([batch, batch]).batch_size
torch.Size([2, 32])
>>> batch = batch.to("cuda" if torch.cuda.is_available() else "cpu")
Every tensor that you add must have the batch dimensions, so the tensors of a group stay aligned:
>>> batch["scores"] = torch.ones(31)
Traceback (most recent call last):
...
RuntimeError: batch dimension mismatch, got self.batch_size=torch.Size([32]) and value.shape=torch.Size([31]) for key 'scores'. The leading dimensions of a value must match the batch size.
You can also save it to disk, pass it to modules, and compile functions that take it.
Install#
pip install tensordict
tensordict needs Python 3.10+ and PyTorch 2.11+. Installation covers nightly builds, the optional dependencies, conda-forge, and source installs.
Where to go#
Getting started: a one-hour tour, with examples that you can run. Start here.
Coming from JAX: JAX pytree idioms in tensordict.
Coming from a hand-written dataclass: what replaces a hand-written
to,pin_memory,__getitem__andcollate_fn.User guide: one page for each concept, from the batch size to TensorClass and
tensordict.nn.How-to guides: one page for each task, such as saving, large datasets, data loading, distributed training, and
torch.compile().Tutorials: scripts that you can run or download as notebooks.
API reference: every public class, function, and method. The Index lists them from A to Z.