概览¶
TensorDict 使得组织数据和编写可重用、通用的 PyTorch 代码变得简单。最初为 TorchRL 开发,我们已将其分离为一个独立的库。
TensorDict 主要是一个字典,但也是一个类似张量的类:它支持多种主要与形状和存储相关的张量操作。它设计用于高效地在节点之间或进程之间进行序列化或传输。最后,它附带了自己的 tensordict.nn 模块,该模块与 functorch 兼容,并旨在使模型集成和参数操作更加容易。
在这一页中,我们将介绍TensorDict并给出一些它可以做的事情的例子。
动机¶
TensorDict 允许你编写通用的代码模块,这些模块可以在不同范式之间重用。例如,以下循环可以在大多数 SL、SSL、UL 和 RL 任务中重用。
>>> for i, tensordict in enumerate(dataset):
... # the model reads and writes tensordicts
... tensordict = model(tensordict)
... loss = loss_module(tensordict)
... loss.backward()
... optimizer.step()
... optimizer.zero_grad()
通过其tensordict.nn模块,该包提供了许多工具,可以在代码库中轻松使用TensorDict。
在多进程或分布式环境中,tensordict 允许您无缝地将数据分发给每个工作进程:
>>> # creates batches of 10 datapoints
>>> splits = torch.arange(tensordict.shape[0]).split(10)
>>> for worker in range(workers):
... idx = splits[worker]
... pipe[worker].send(tensordict[idx])
TensorDict 提供的一些操作也可以通过 tree_map 来完成,但复杂度更高:
>>> td = TensorDict(
... {"a": torch.randn(3, 11), "b": torch.randn(3, 3)}, batch_size=3
... )
>>> regular_dict = {"a": td["a"], "b": td["b"]}
>>> td0, td1, td2 = td.unbind(0)
>>> # similar structure with pytree
>>> regular_dicts = tree_map(lambda x: x.unbind(0))
>>> regular_dict1, regular_dict2, regular_dict3 = [
... {"a": regular_dicts["a"][i], "b": regular_dicts["b"][i]}
... for i in range(3)]
嵌套的情况更加令人信服:
>>> td = TensorDict(
... {"a": {"c": torch.randn(3, 11)}, "b": torch.randn(3, 3)}, batch_size=3
... )
>>> regular_dict = {"a": {"c": td["a", "c"]}, "b": td["b"]}
>>> td0, td1, td2 = td.unbind(0)
>>> # similar structure with pytree
>>> regular_dicts = tree_map(lambda x: x.unbind(0))
>>> regular_dict1, regular_dict2, regular_dict3 = [
... {"a": {"c": regular_dicts["a"]["c"][i]}, "b": regular_dicts["b"][i]}
... for i in range(3)
在应用 unbind 操作后,将输出字典分解为三个结构相似的字典会迅速变得非常繁琐,尤其是在使用 pytree 时。通过 tensordict,我们为用户提供了一个简单的 API,用于解绑或拆分嵌套结构,而不是计算嵌套拆分 / 解绑嵌套结构。
特性¶
一个 TensorDict 是一个类似于字典的张量容器。要实例化一个 TensorDict,你必须指定键值对以及批量大小。TensorDict 中任何值的前几维必须与批量大小兼容。
>>> import torch
>>> from tensordict import TensorDict
>>> tensordict = TensorDict(
... {"zeros": torch.zeros(2, 3, 4), "ones": torch.ones(2, 3, 4, 5)},
... batch_size=[2, 3],
... )
设置或检索值的语法与常规字典非常相似。
>>> zeros = tensordict["zeros"]
>>> tensordict["twos"] = 2 * torch.ones(2, 3)
还可以根据 tensordict 的 batch_size 进行索引,从而仅用几个字符即可获取数据的一致切片(注意:若使用 tree_map 并配合省略号(...)对第 n 个前导维度进行索引,则需要编写稍多一些的代码):
>>> sub_tensordict = tensordict[..., :2]
也可以使用 set 方法与 inplace=True 或 set_ 方法来进行内容的原地更新。
前者是后者的容错版本:如果未找到匹配的键,它将写入一个新的键。
现在可以统一操作 TensorDict 的内容。 例如,要将所有内容放置到特定设备上,只需执行以下操作:
>>> tensordict = tensordict.to("cuda:0")
要重塑批次维度,可以这样做
>>> tensordict = tensordict.reshape(6)
该类支持许多其他操作,包括 squeeze、unsqueeze、view、permute、unbind、stack、cat 等等。如果某个操作不存在,TensorDict.apply 方法通常会提供所需的解决方案。
命名维度¶
TensorDict 及相关类也支持维度名称。 名称可在构建时指定,也可在后续进行细化。其语义 与 torch.Tensor 的维度名称功能类似:
>>> tensordict = TensorDict({}, batch_size=[3, 4], names=["a", None])
>>> tensordict.refine_names(..., "b")
>>> tensordict.names = ["z", "y"]
>>> tensordict.rename("m", "n")
>>> tensordict.rename(m="h")
嵌套的TensorDicts¶
TensorDict 中的值本身可以是 TensorDicts(下面示例中的嵌套字典将被转换为嵌套 TensorDicts)。
>>> tensordict = TensorDict(
... {
... "inputs": {
... "image": torch.rand(100, 28, 28),
... "mask": torch.randint(2, (100, 28, 28), dtype=torch.uint8)
... },
... "outputs": {"logits": torch.randn(100, 10)},
... },
... batch_size=[100],
... )
访问或设置嵌套键可以通过字符串元组来完成
>>> image = tensordict["inputs", "image"]
>>> logits = tensordict.get(("outputs", "logits")) # alternative way to access
>>> tensordict["outputs", "probabilities"] = torch.sigmoid(logits)
延迟评估¶
某些操作在 TensorDict 上延迟执行,直到访问项目。例如堆叠、挤压、取消挤压、交换批量维度和创建视图等操作不会立即对 TensorDict 中的所有内容执行。相反,它们会在访问 TensorDict 中的值时懒惰地执行。如果 TensorDict 包含许多值,这可以节省大量不必要的计算。
>>> tensordicts = [TensorDict({
... "a": torch.rand(10),
... "b": torch.rand(10, 1000, 1000)}, [10])
... for _ in range(3)]
>>> stacked = torch.stack(tensordicts, 0) # no stacking happens here
>>> stacked_a = stacked["a"] # we stack the a values, b values are not stacked
它还有一个优点,即我们可以在堆栈中操作原始的 tensordicts:
>>> stacked["a"] = torch.zeros_like(stacked["a"])
>>> assert (tensordicts[0]["a"] == 0).all()
需要注意的是,get 方法现在已成为一个昂贵的操作,如果多次重复调用,可能会导致一些开销。可以通过在执行 stack 操作后简单地调用 tensordict.contiguous() 来避免这种情况。为了进一步缓解这个问题,TensorDict 自带了一个元数据类(MetaTensor),它可以跟踪字典中每个条目的类型、形状、数据类型和设备,而无需执行昂贵的操作。
延迟预分配¶
假设我们有一个函数 foo() -> TensorDict,并且我们做了如下操作:
>>> tensordict = TensorDict({}, batch_size=[N])
>>> for i in range(N):
... tensordict[i] = foo()
当 i == 0 空的 TensorDict 将会自动填充为具有批量大小 N 的空张量。在循环的后续迭代中,所有更新都将原地写入。
TensorDictModule¶
为了方便在代码库中集成TensorDict,我们提供了一个tensordict.nn包,允许用户将TensorDict实例传递给nn.Module对象。
TensorDictModule 包含 nn.Module 并接受一个 TensorDict 作为输入。您可以指定底层模块应从何处获取其输入,并在何处写入其输出。这是我们能够编写可重用、通用的高级代码(如动机部分中的训练循环)的关键原因之一。
>>> from tensordict.nn import TensorDictModule
>>> class Net(nn.Module):
... def __init__(self):
... super().__init__()
... self.linear = nn.LazyLinear(1)
...
... def forward(self, x):
... logits = self.linear(x)
... return logits, torch.sigmoid(logits)
>>> module = TensorDictModule(
... Net(),
... in_keys=["input"],
... out_keys=[("outputs", "logits"), ("outputs", "probabilities")],
... )
>>> tensordict = TensorDict({"input": torch.randn(32, 100)}, [32])
>>> tensordict = module(tensordict)
>>> # outputs can now be retrieved from the tensordict
>>> logits = tensordict["outputs", "logits"]
>>> probabilities = tensordict.get(("outputs", "probabilities"))
为了便于采用此类,还可以将张量作为关键字参数传递:
>>> tensordict = module(input=torch.randn(32, 100))
这将返回一个 TensorDict,与前一个代码框中的相同。
多个PyTorch用户面临的一个关键痛点是nn.Sequential无法处理具有多个输入的模块。通过使用基于键的图可以轻松解决这个问题,因为序列中的每个节点都知道需要读取哪些数据以及将数据写入何处。
为此目的,我们提供了TensorDictSequential类,该类通过一系列TensorDictModules传递数据。序列中的每个模块从原始TensorDict获取输入并将其输出写回原始TensorDict,这意味着序列中的模块可以忽略其前驱的输出,或者根据需要从tensordict中获取额外的输入。以下是一个示例。
>>> class Net(nn.Module):
... def __init__(self, input_size=100, hidden_size=50, output_size=10):
... super().__init__()
... self.fc1 = nn.Linear(input_size, hidden_size)
... self.fc2 = nn.Linear(hidden_size, output_size)
...
... def forward(self, x):
... x = torch.relu(self.fc1(x))
... return self.fc2(x)
...
... class Masker(nn.Module):
... def forward(self, x, mask):
... return torch.softmax(x * mask, dim=1)
>>> net = TensorDictModule(
... Net(), in_keys=[("input", "x")], out_keys=[("intermediate", "x")]
... )
>>> masker = TensorDictModule(
... Masker(),
... in_keys=[("intermediate", "x"), ("input", "mask")],
... out_keys=[("output", "probabilities")],
... )
>>> module = TensorDictSequential(net, masker)
>>> tensordict = TensorDict(
... {
... "input": TensorDict(
... {"x": torch.rand(32, 100), "mask": torch.randint(2, size=(32, 10))},
... batch_size=[32],
... )
... },
... batch_size=[32],
... )
>>> tensordict = module(tensordict)
>>> intermediate_x = tensordict["intermediate", "x"]
>>> probabilities = tensordict["output", "probabilities"]
在此示例中,第二个模块将第一个模块的输出与存储在 (“inputs”, “mask”) 下的掩码相结合,该掩码位于 TensorDict 中。
TensorDictSequential 提供了许多其他功能:可以通过查询 in_keys 和 out_keys 属性来访问输入和输出键的列表。还可以通过查询 select_subsequence() 来请求子图,其中包含所需的输入和输出键集合。这将返回另一个 TensorDictSequential,其中仅包含满足这些要求所必需的模块。TensorDictModule 还与 vmap 以及其他 functorch 功能兼容。
函数式编程¶
我们提供了一个API来使用TensorDict与functorch结合使用。例如,TensorDict使得将模型权重拼接以进行模型集成变得简单:
>>> from torch import nn
>>> from tensordict import TensorDict
>>> from tensordict.nn import make_functional
>>> import torch
>>> from torch import vmap
>>> layer1 = nn.Linear(3, 4)
>>> layer2 = nn.Linear(4, 4)
>>> model = nn.Sequential(layer1, layer2)
>>> # we represent the weights hierarchically
>>> weights1 = TensorDict(layer1.state_dict(), []).unflatten_keys(separator=".")
>>> weights2 = TensorDict(layer2.state_dict(), []).unflatten_keys(separator=".")
>>> params = make_functional(model)
>>> # params provided by make_functional match state_dict:
>>> assert (params == TensorDict({"0": weights1, "1": weights2}, [])).all()
>>> # Let's use our functional module
>>> x = torch.randn(10, 3)
>>> out = model(x, params=params) # params is the last arg (or kwarg)
>>> # an ensemble of models: we stack params along the first dimension...
>>> params_stack = torch.stack([params, params], 0)
>>> # ... and use it as an input we'd like to pass through the model
>>> y = vmap(model, (None, 0))(x, params_stack)
>>> print(y.shape)
torch.Size([2, 10, 4])
函数式 API 的性能与当前在 FunctionalModule 中实现的 functorch 相当,甚至更快。