flowchart TD
L[local_tensor] --> F[DTensor.from_local]
F --> B[build or reuse Layout]
F --> N[DTensorBase.__new__]
N --> M[Tensor._make_subclass]
M --> D[DTensor Python object and TensorImpl]
D --> I[DTensor.__init_data__]
I --> R[store local_tensor as _local_tensor]
I --> E[store mesh layout placements]
6.1 _make_subclass() 与 local tensor 的关系
核心调用:
t = Tensor._make_subclass(
cls,
local_tensor,
local_tensor.requires_grad,
)
flowchart LR
D[DTensor] --> X[DTensor.detach]
X --> LD[detached local tensor]
LD --> C[DTensor constructor]
C --> MS[Tensor._make_subclass]
MS --> P[DTensor marked as Parameter]
P --> SP[sharded_param TensorImpl]
P --> LP[_local_tensor TensorImpl]
SP --> S[shared sharded Storage]
LP --> S
9. Deferred init 为什么会打破 Storage 一致性
FSDP meta-init 场景中,初始 sharded DTensor 可能位于 meta device:
sharded_param 本体 ──> TensorImpl P ──┐
├──> Meta Storage
sharded_param._local_tensor ──> TensorImpl L ──┘
module.to_empty(device="npu") 物化模块参数后,模块上的参数可能已成为实体设备上的普通 Tensor/Parameter,而 HSDP state 仍保存原来的 sharded DTensor 对象身份。
修复前暂态:
sharded_param 本体 ──> TensorImpl P ──> Meta Storage
sharded_param.data ──> TensorImpl A ──> Meta Storage
sharded_param._local_tensor ──> TensorImpl L ──> NPU Storage X
sharded_param ──> TensorImpl P(meta) ──> Meta Storage
_local_tensor ──> TensorImpl L ──> NPU Storage X
local_tensor_data alias ──> TensorImpl A ──> NPU Storage X
10.2 只交换 TensorImpl 后
sharded_param ──> TensorImpl A ──> NPU Storage X
_local_tensor ──> TensorImpl L ──> NPU Storage X
local_tensor_data alias ──> TensorImpl P(meta)──> Meta Storage
临时 alias 在函数结束后释放,旧 meta TensorImpl 随之失去该临时引用。_local_tensor 从未参与 swap,所以不会变成 meta。
flowchart TB
subgraph Before[Before repair]
BP[sharded_param] --> BI[Meta TensorImpl]
BL[_local_tensor] --> BLI[Local TensorImpl]
BA[local_tensor.data alias] --> BAI[Alias TensorImpl]
BI --> BM[Meta Storage]
BLI --> BN[NPU Storage X]
BAI --> BN
end
subgraph After[After TensorImpl swap]
AP[sharded_param] --> AAI[Alias TensorImpl]
AL[_local_tensor] --> ALI[Local TensorImpl]
AA[temporary alias] --> AMI[Old Meta TensorImpl]
AAI --> AN[NPU Storage X]
ALI --> AN
AMI --> AM[Meta Storage]
end
PyTorch TensorImpl、Storage 与 HyperParallel DTensor 创建机制笔记
1. 这份笔记回答什么
本文记录以下几个容易混淆的问题:
Tensor对象、C++ 层的TensorImpl和实际数据Storage分别是什么;tensor.data为什么与tensor的 Pythonid、TensorImpl 不同,但通常共享 Storage;DTensor如何通过Tensor._make_subclass()创建;DTensor本体、DTensor._local_tensor和DTensor.data之间是什么关系;nn.Parameter(DTensor)为什么又会产生一层新的 TensorImpl;to_empty()为什么可能让 DTensor 本体与_local_tensor的 Storage 暂时失配;_local_tensor本体。本文是机制备忘录,不定义新的用户 API,也不讨论完整的 DTensor 分布式语义。
2. PyTorch Tensor 的三层模型
可以把一个 PyTorch Tensor 简化成三层:
2.1 Python Tensor 对象
这是 Python 代码中看到的对象:
tensor = torch.ones(4)id(tensor)标识的是 Python 对象身份。模块参数注册、优化器保存的参数引用以及用户持有的变量,首先依赖这一层的对象身份。两个不同的 Python Tensor 对象可以持有不同的 TensorImpl,也可以通过这些 TensorImpl 共享同一个 Storage。
2.2 TensorImpl
TensorImpl是 Tensor 的核心 C++ 元数据对象。简化来看,它记录:storage_offset;因此,整体替换 TensorImpl 会同时改变当前 Tensor 看到的 Storage、设备、dtype、shape、stride 等信息。
TensorImpl 自身不是实际数据内存。多个 TensorImpl 可以引用同一个 Storage,并用不同的 offset、sizes 和 strides 表达不同 view。
2.3 Storage / StorageImpl
Storage 管理实际内存区域。简化来看,它包含:
常用观测方式:
storage = tensor.untyped_storage() storage_id = storage._cdata # StorageImpl 标识,仅用于调试 memory_ptr = storage.data_ptr() # 实际内存地址;meta/空 tensor 可能为 0判断“是否为同一份底层存储”时,
StorageImpl标识比只比较data_ptr()更严格。两个不同 view 可以共享同一个 StorageImpl,但tensor.data_ptr()还会受到storage_offset影响。3. 多个 TensorImpl 共享一个 Storage
典型 view 关系如下:
两个 Tensor:
对应 Mermaid:
flowchart LR A[Python Tensor A] --> IA[TensorImpl A] B[Python Tensor B] --> IB[TensorImpl B] IA --> S[StorageImpl X] IB --> S S --> M[Device Memory]4.
tensor.data的读取机制读取:
不是读取一个长期保存在
tensor.__dict__中的普通属性。Tensor.data是 descriptor;getter 会返回一个 detached alias。通常关系为:
所以一般有:
id(tensor) != id(tensor.data) tensor._cdata != tensor.data._cdata tensor.untyped_storage()._cdata == tensor.data.untyped_storage()._cdata每次读取
.data都可能产生新的临时 Tensor 对象和 TensorImpl:data1 = tensor.data data2 = tensor.data assert id(data1) != id(data2) assert data1._cdata != data2._cdata assert data1.untyped_storage()._cdata == data2.untyped_storage()._cdata.data会绕开正常 autograd 语义,生产逻辑中应谨慎使用;这里主要用它解释 alias 和底层存储关系。5.
tensor.data = another_tensor与普通 Python 赋值不同赋值:
会调用
Tensor.datadescriptor 的 setter,而不是让一个名为data的普通 Python 属性指向another_tensor。简化后的效果是:保留
tensor的 Python 对象身份,并修改其底层数据相关状态,使其引用another_tensor的数据:实际 setter 还会做 Tensor 类型、设备和 autograd 兼容性检查。对于 Tensor subclass,普通 Tensor 与 subclass 之间可能被判定为 incompatible tensor type;meta 与实体设备之间也不能简单依赖
set_()跨设备重绑 Storage。这与交换两个
.data临时 alias 不同:这里只改变 getter 返回的两个临时 Python Tensor 对象,不会回写
tensor本体所持有的 TensorImpl。6. HyperParallel Torch DTensor 的创建链路
关键代码位置:
hyper_parallel/core/dtensor/dtensor.py::DTensor.from_local();hyper_parallel/platform/torch/dtensor.py::DTensorBase.__new__();hyper_parallel/core/dtensor/dtensor.py::DTensor.__init_data__()。调用链:
Mermaid 表示:
flowchart TD L[local_tensor] --> F[DTensor.from_local] F --> B[build or reuse Layout] F --> N[DTensorBase.__new__] N --> M[Tensor._make_subclass] M --> D[DTensor Python object and TensorImpl] D --> I[DTensor.__init_data__] I --> R[store local_tensor as _local_tensor] I --> E[store mesh layout placements]6.1
_make_subclass()与 local tensor 的关系核心调用:
它创建一个 Tensor subclass 对象及其 TensorImpl。该 TensorImpl 与传入的
local_tensorTensorImpl 不同,但两者共享 local tensor 的 StorageImpl。随后:
又将传入的 Python local tensor 对象保存到
t._local_tensor。裸 DTensor 创建后的长期关系为:
其中 DTensor 本体和
_local_tensor是不同的 Python 对象、不同的 TensorImpl,但共享同一个 StorageImpl。访问
DTensor.data后,还会多出一个临时 alias:7. HyperParallel 的
DTensor.dataoverrideTorch 后端在
DTensorBase中覆盖了.data。getter 的目标是绕过
__torch_function__,直接读取 DTensor 本体对应的 Tensor data alias:with torch._C.DisableTorchFunctionSubclass(): return Tensor.data.__get__(self, type(self))这里的
DisableTorchFunctionSubclass很重要。如果不绕过 subclass dispatch,观测结果可能被 HyperParallel DTensor 分发逻辑改写为_local_tensor路径,无法准确看到 DTensor 本体所持有的 TensorImpl/Storage。setter 的意图是同时更新两条长期引用:
Tensor.data.__set__(self, local_value) Tensor.data.__set__(self._local_tensor, local_value)即保持:
但底层 PyTorch 对 Tensor subclass、跨设备及 meta materialization 有兼容性限制,不能假定任意场景都能通过
.data = ...完成修复。8.
nn.Parameter(DTensor)的创建机制PyTorch 对 custom Tensor subclass 采用 Parameter 的特殊路径:
t = data.detach().requires_grad_(requires_grad) t._is_param = True return tHyperParallel
DTensor.detach()会:detached_local = self._local_tensor.detach() return self.__class__( detached_local, device_mesh=self._device_mesh, placements=self._alias_placements(), )因此
nn.Parameter(DTensor)并不是简单给原 DTensor Python 对象加一个标签。它会经过:最终 Parameter DTensor 仍然满足:
访问
sharded_param.data时,再创建临时 TensorImpl A,同样共享 Storage X。flowchart LR D[DTensor] --> X[DTensor.detach] X --> LD[detached local tensor] LD --> C[DTensor constructor] C --> MS[Tensor._make_subclass] MS --> P[DTensor marked as Parameter] P --> SP[sharded_param TensorImpl] P --> LP[_local_tensor TensorImpl] SP --> S[shared sharded Storage] LP --> S9. Deferred init 为什么会打破 Storage 一致性
FSDP meta-init 场景中,初始 sharded DTensor 可能位于 meta device:
module.to_empty(device="npu")物化模块参数后,模块上的参数可能已成为实体设备上的普通 Tensor/Parameter,而 HSDP state 仍保存原来的 sharded DTensor 对象身份。reset_sharded_param()会根据物化参数构建新的 local view,并执行:self.sharded_param._local_tensor = local_view此时只替换了 Python 属性
_local_tensor指向的对象,没有更新 sharded DTensor 本体持有的 TensorImpl:这里是三份 TensorImpl、两份 StorageImpl。DTensor 的部分 Python property 会从
_local_tensor返回 device/dtype,看起来已经在 NPU;但直接使用 DTensor 本体 TensorImpl 的底层路径仍可能看到 meta Storage。这种“表层元数据已物化、本体 TensorImpl 未物化”的状态必须修复。10. TensorImpl 修复的实际机制
当前讨论的精简修复思路为:
sharded_param_data = self.sharded_param.data local_tensor_data = self.sharded_param._local_tensor.data if storage_mismatch: local_tensor_data.requires_grad_(self.sharded_param.requires_grad) torch._C._swap_tensor_impl(self.sharded_param, local_tensor_data)关键点:参与交换的是
_local_tensor.data临时 alias,不是_local_tensor本体。10.1 swap 前
10.2 只交换 TensorImpl 后
临时 alias 在函数结束后释放,旧 meta TensorImpl 随之失去该临时引用。
_local_tensor从未参与 swap,所以不会变成 meta。flowchart TB subgraph Before[Before repair] BP[sharded_param] --> BI[Meta TensorImpl] BL[_local_tensor] --> BLI[Local TensorImpl] BA[local_tensor.data alias] --> BAI[Alias TensorImpl] BI --> BM[Meta Storage] BLI --> BN[NPU Storage X] BAI --> BN end subgraph After[After TensorImpl swap] AP[sharded_param] --> AAI[Alias TensorImpl] AL[_local_tensor] --> ALI[Local TensorImpl] AA[temporary alias] --> AMI[Old Meta TensorImpl] AAI --> AN[NPU Storage X] ALI --> AN AMI --> AM[Meta Storage] end11. 为什么不能交换两个
.dataalias下面的代码无效:
torch.utils.swap_tensors( self.sharded_param.data, self.sharded_param._local_tensor.data, )因为两个参数都是 getter 返回的临时 Python Tensor 对象。交换只影响这两个临时对象,不会改变
self.sharded_param本体持有的 TensorImpl。12. 为什么不能直接 swap
_local_tensor本体下面的逻辑会污染
_local_tensor:swap_impl(self.sharded_param, self.sharded_param._local_tensor)交换后
_local_tensor会拿到旧 meta TensorImpl:所以必须使用
_local_tensor.data生成的临时 alias 作为 donor。alias 与_local_tensor共享 NPU Storage,但拥有独立 TensorImpl;旧 meta TensorImpl 最终落在 alias 上,而不是_local_tensor上。13.
torch.utils.swap_tensors()与_swap_tensor_impl()的差异公开接口
torch.utils.swap_tensors(t1, t2)不只交换 TensorImpl,还会交换:__class__;__dict__;因此不能直接用普通 Tensor alias 与 DTensor Parameter 本体调用:
否则
sharded_param的 DTensor/Parameter Python 壳和 mesh/layout 等动态属性也会被交换给临时 alias。如果只希望保留原 Python 对象、Parameter 身份和 DTensor 元数据,仅替换其 TensorImpl,就需要更窄的 TensorImpl 级操作。但
_swap_tensor_impl()是 PyTorch 私有接口,存在版本兼容风险;使用时应有定向测试覆盖,或者使用同类型、同 slots、完整元数据的 DTensor donor 配合公开swap_tensors()。14. 调试与验证方法
14.1 建议观测四类标识
def inspect_tensor(tensor): with torch._C.DisableTorchFunctionSubclass(): storage = torch.Tensor.untyped_storage(tensor) tensor_impl = torch.Tensor._cdata.__get__(tensor, torch.Tensor) return { "python_id": id(tensor), "tensor_impl": tensor_impl, "storage_impl": storage._cdata, "data_ptr": storage.data_ptr(), "storage_device": storage.device, "offset": tensor.storage_offset(), "size": tuple(tensor.size()), "stride": tuple(tensor.stride()), }对 DTensor 调试时需要绕过
__torch_function__。HyperParallel 的device、dtype等 property 可能主动返回_local_tensor的信息,仅打印dtensor.device不足以证明 DTensor 本体 TensorImpl 已经物化。14.2 验证同 Storage 时不要只看
data_ptr()建议同时比较:
原因:
data_ptr()都可能为 0;tensor.data_ptr()可能因 offset 不同而不同;15. 本次实测结论
在 HyperParallel Torch DTensor 上观测到:
_local_tensor是不同 Python 对象、不同 TensorImpl,共享同一个 StorageImpl;DTensor.data会得到新的临时 Python Tensor/TensorImpl,并继续共享 DTensor 本体当时的 Storage;nn.Parameter(DTensor)会通过 DTensor 的detach()再构造一个 DTensor,并设置_is_param;_local_tensor以及当次读取的.dataalias 共享同一个 StorageImpl;_local_tensor时,会暂时形成 DTensor 本体为 meta Storage、_local_tensor为 NPU Storage 的失配;_local_tensor.data临时 alias 作为 TensorImpl donor,可以在不污染_local_tensor的前提下让 DTensor 本体重新引用 NPU Storage;.dataalias 不会修改父 Tensor;直接交换_local_tensor本体则会把旧 meta TensorImpl 交换给_local_tensor。16. 相关代码位置
hyper_parallel/platform/torch/dtensor.pyDTensorBase.__new__()DTensorBase.detach()DTensorBase.datahyper_parallel/core/dtensor/dtensor.pyDTensor.from_local()DTensor.__init_data__()hyper_parallel/platform/torch/fully_shard/param.pyTorchHSDPParamV2._resolve_reset_param()TorchHSDPParamV2.reset_sharded_param()TorchHSDPParamV2._update_shardedparam_storage_forcely()17. 一句话记忆