Containers on storage backends#
A TensorClass or a TypedTensorDict
can wrap a tensordict on any storage backend without a copy. The backends are
an in-memory TensorDict, an HDF5 or zarr file
(PersistentTensorDict), a Redis server
(TensorDictStore), a lazy stack
(LazyStackedTensorDict), or a memory-mapped
TensorDict. Code that uses the typed fields then reads and writes the file
or the server directly. A few operations behave differently on some backends:
see Operations on each backend.
TypedTensorDict is experimental: its API can change in a minor release
without a deprecation cycle (see TypedTensorDict). To choose
between TensorClass and TypedTensorDict, see
Which container should I use?. For the backends themselves, see Large datasets and storage backends
and Save and load tensordicts.
How the classes relate#
TensorDictBase
├── TensorDict (in memory)
├── TypedTensorDict (typed fields, wraps any TensorDictBase)
├── PersistentTensorDict (HDF5 or zarr file)
├── TensorDictStore (Redis, Dragonfly or KeyDB server)
└── LazyStackedTensorDict (lazy stack of tensordicts)
TensorClass (typed fields, has a TensorDictBase)
A TypedTensorDict is a TensorDictBase. A
TensorClass is not: it keeps its fields in a tensordict that it holds.
Both have a from_tensordict(td) class method that wraps td. Reads and
writes through the wrapper go to the storage of td. If you call the
constructor instead, the storage is a new in-memory TensorDict.
Operations on each backend#
The following operations work on each backend, directly or through a
TensorClass or a TypedTensorDict, with the exceptions in the next
table:
Build:
MyTC.from_tensordict(td).Read a field:
w.a.Write a field:
w.a = value.In-place write:
w.set_("a", value)andw.update(d, inplace=True).Update:
w.update(d).Index:
w[0]returns the wrapper class, andw[0] = otherwrites into the backend.Clone:
w.clone().Stack:
torch.stack([w, w]).Iterate:
list(w).
Backend |
Operation |
What happens, and what to do |
|---|---|---|
Memory-mapped |
Write a field, |
|
|
Write a field, |
Each write replaces the dataset in the file and warns |
|
|
Without a destination, it warns |
|
In-place methods such as |
Write nothing, and warn |
|
In-place operation on a field, such as |
Reading an entry returns a new tensor with a copy of the data, so the
operation does not change the storage. Write the result back with
|
Nested tensorclass fields#
A field can have another tensorclass as its type (a: Pose or
a: Pose | None). If the input is a TensorDict and its entry for that
field is a tensordict, from_tensordict builds a Pose from that entry,
recursively. The Pose goes into a shallow copy of the input. The leaves
are shared, but the input keeps its tensordict entry, and it does not see the
entries set later on the tensorclass. An entry with keys that Pose does
not declare stays a tensordict. On the other backends, from_tensordict
wraps the input as it is, and the nested entry stays a tensordict, so that
the wrapper writes nothing to their storage.
A TensorClass on each backend#
The examples write their files to a temporary directory, and use this class:
>>> import tempfile
>>> tmpdir = tempfile.TemporaryDirectory()
>>> path = tmpdir.name
>>> import torch
>>> from torch import Tensor
>>> from tensordict import TensorDict, TensorClass
>>> class MyTC(TensorClass):
... a: Tensor
... b: Tensor
In-memory TensorDict#
>>> td = TensorDict({"a": torch.randn(4, 3), "b": torch.randn(4, 5)}, batch_size=[4])
>>> tc = MyTC.from_tensordict(td)
>>> tc.a.shape
torch.Size([4, 3])
HDF5 (PersistentTensorDict)#
>>> import os
>>> from tensordict import PersistentTensorDict
>>> h5 = PersistentTensorDict.from_dict(td, filename=os.path.join(path, "data.h5"))
>>> tc_h5 = MyTC.from_tensordict(h5)
>>> tc_h5.a.shape # reads from the HDF5 file
torch.Size([4, 3])
Redis (TensorDictStore)#
These examples need a Redis server on localhost, so the docs build does
not run them. To start a server for a test, run
docker run --rm -d -p 6379:6379 redis:7-alpine.
>>> from tensordict.store import TensorDictStore
>>> store = TensorDictStore.from_tensordict(td, host="localhost")
>>> tc_redis = MyTC.from_tensordict(store)
>>> tc_redis.a.shape # fetched from Redis
torch.Size([4, 3])
The data stays on the server after the process ends. To delete it, call
store.clear_redis(). To close the connection, call store.close():
>>> store.clear_redis()
>>> store.close()
Lazy stack#
>>> from tensordict import lazy_stack
>>> tds = [TensorDict({"a": torch.randn(3), "b": torch.randn(5)}) for _ in range(4)]
>>> ls = lazy_stack(tds, dim=0)
>>> tc_lazy = MyTC.from_tensordict(ls)
>>> tc_lazy[0].a.shape
torch.Size([3])
Memory-mapped TensorDict#
memmap_() locks the tensordict, so write with set_():
>>> td_mmap = td.clone().memmap_(os.path.join(path, "tc_memmap"))
>>> tc_mmap = MyTC.from_tensordict(td_mmap)
>>> tc_mmap.a.shape
torch.Size([4, 3])
>>> _ = tc_mmap.set_("a", torch.ones(4, 3))
>>> bool((td_mmap["a"] == 1).all())
True
TypedTensorDict#
A TypedTensorDict can be the backend of a TensorClass. Both declare
fields, and the two declarations do not conflict:
>>> from tensordict import TypedTensorDict
>>> class MyTTD(TypedTensorDict):
... a: Tensor
... b: Tensor
>>> ttd = MyTTD(a=torch.randn(4, 3), b=torch.randn(4, 5), batch_size=[4])
>>> tc_typed = MyTC.from_tensordict(ttd)
>>> tc_typed.a.shape
torch.Size([4, 3])
The other direction is not supported: TypedTensorDict.from_tensordict
accepts a tensorclass, but an attribute write through it raises
ValueError.
A TypedTensorDict on each backend#
The examples use this class:
>>> class State(TypedTensorDict):
... x: Tensor
... y: Tensor
In-memory TensorDict#
The constructor creates an in-memory TensorDict:
>>> state = State(x=torch.randn(4, 3), y=torch.randn(4, 5), batch_size=[4])
>>> state.x.shape
torch.Size([4, 3])
from_tensordict wraps an existing TensorDict without a copy:
>>> td = TensorDict({"x": torch.randn(4, 3), "y": torch.randn(4, 5)}, batch_size=[4])
>>> state = State.from_tensordict(td)
>>> state.x.shape # reads from td
torch.Size([4, 3])
>>> state.x = torch.ones(4, 3) # writes to td
>>> (td["x"] == 1).all()
tensor(True)
HDF5 (PersistentTensorDict)#
>>> h5_state = PersistentTensorDict.from_dict(td, filename=os.path.join(path, "state.h5"))
>>> state = State.from_tensordict(h5_state)
>>> state.x.shape # reads from the HDF5 file
torch.Size([4, 3])
Redis (TensorDictStore)#
As above, these examples need a Redis server on localhost:
>>> store = TensorDictStore.from_tensordict(td, host="localhost")
>>> state = State.from_tensordict(store)
>>> state.x.shape # fetched from Redis
torch.Size([4, 3])
>>> store.clear_redis()
>>> store.close()
Lazy stack#
>>> tds = [TensorDict({"x": torch.randn(3), "y": torch.randn(5)}) for _ in range(4)]
>>> ls = lazy_stack(tds, dim=0)
>>> state = State.from_tensordict(ls)
>>> state[0].x.shape
torch.Size([3])
Memory-mapped TensorDict#
>>> td_mmap = td.clone().memmap_(os.path.join(path, "state_memmap"))
>>> state = State.from_tensordict(td_mmap)
>>> state.x.shape
torch.Size([4, 3])
>>> _ = state.set_("x", torch.zeros(4, 3))
>>> bool((td_mmap["x"] == 0).all())
True
Stacking TypedTensorDicts#
torch.stack returns an instance of the TypedTensorDict subclass:
>>> s1 = State(x=torch.randn(3), y=torch.randn(3), batch_size=[3])
>>> s2 = State(x=torch.randn(3), y=torch.randn(3), batch_size=[3])
>>> stacked = torch.stack([s1, s2], dim=0)
>>> type(stacked).__name__, stacked.batch_size
('State', torch.Size([2, 3]))
A lazy stack also works. An index of a LazyStackedTensorDict of
TypedTensorDict instances returns the subclass:
>>> from tensordict import LazyStackedTensorDict
>>> ls = LazyStackedTensorDict(s1, s2, stack_dim=0)
>>> isinstance(ls[0], State)
True
When you no longer need the files, close the HDF5 files and delete the directory:
>>> h5.close()
>>> h5_state.close()
>>> tmpdir.cleanup()
To allocate storage on a Redis server first and fill it one sample at a time, see pre-allocating on Redis.
See also#
TensorClass: a dataclass of tensors and TypedTensorDict: the two typed containers.
Which container should I use?: which container to use, and the
TensorClassandTypedTensorDictcomparison.Large datasets and storage backends: HDF5, zarr, Redis, and the other storage backends.
Save and load tensordicts: memory-mapped tensordicts and the other formats on disk.