PyTorch Tensor底层解密:内存布局、梯度机制与性能优化
1. 项目概述:为什么 PyTorch Tensor 是你绕不开的“肌肉记忆”
如果你刚接触深度学习,大概率会被torch.tensor([1, 2, 3])这行代码轻轻带进门;但真正开始写模型、调数据、查梯度时,你会发现——自己不是在操作模型,而是在和 Tensor 打交道。它不像 NumPy array 那样安静,也不像 Python list 那样随意;它自带设备感知、自动微分、内存共享、视图机制,甚至能决定你的训练是快是慢、显存是否爆掉、梯度是否消失。我带过十几期从零起步的 PyTorch 实战训练营,90% 的学员卡点不在反向传播原理,而在.view()和.reshape()的区别上,在.detach()和.clone().detach()的语义差异里,在requires_grad=True没设对导致整个 loss 不更新的深夜 debug 中。这不是语法细节问题,而是你对 PyTorch 底层数据载体的理解断层。Tensor 不是容器,它是计算图的节点、是 GPU 内存的映射、是张量代数的可执行实体。本篇不讲“如何创建 tensor”,而是带你亲手拆开它的内存布局、跟踪它的梯度血缘、验证它的视图共享逻辑、实测它的设备迁移开销——所有内容均基于 PyTorch 2.3(2024 年最新稳定版)源码行为与真实运行日志,每一步都附带print(tensor.__dict__)级别的底层观察。适合已写过nn.Module但常被RuntimeError: Trying to backward through the graph a second time报错打断思路的中级实践者,也适合想跳过“抄 demo”阶段、直击框架设计内核的进阶学习者。
2. Tensor 的本质解构:不只是多维数组,而是“可计算的内存块”
2.1 从 C++ 后端看 Tensor 的三重身份
PyTorch 的 Tensor 在 C++ 层由三个核心对象协同构成:c10::TensorImpl(张量元数据与状态)、c10::Storage(连续内存块)和c10::DataPtr(指向实际内存的智能指针)。这三者关系,就像“身份证+房产证+钥匙”的组合:
c10::Storage是真正的“房子”——一块连续分配的内存(CPU 上是std::vector或malloc分配区,CUDA 上是cudaMalloc分配的显存),它不关心维度、形状,只管“我有多大、里面存了什么类型的数据”。c10::TensorImpl是“房产证+户口本”——它记录了 shape(尺寸)、stride(步长)、dtype(数据类型)、device(设备)、requires_grad(是否参与求导)、is_leaf(是否为计算图叶子节点)等全部元信息。关键在于:多个 Tensor 可以共享同一个 Storage,只要它们的 stride 和 offset 设置得当。c10::DataPtr是“钥匙”——它持有对 Storage 内存的引用计数指针,确保内存不会在还有 Tensor 使用时被释放。
这个设计直接解释了为什么x.view(-1)不拷贝数据,而x.clone().view(-1)会新建 Storage:前者只是给同一块内存换了一张“房产证”(修改 TensorImpl 的 shape/stride),后者则是先造了一栋新房子(clone 创建新 Storage),再发新房产证。
提示:你可以用
tensor.storage().data_ptr()获取底层内存地址,用tensor.storage().size()查看 Storage 容量(单位:元素个数),这两个值往往与tensor.numel()相同,但tensor.shape可能完全不同。例如torch.arange(12).view(3, 4)和torch.arange(12).view(2, 6)共享同一 Storage,但 shape 和 stride 截然不同。
2.2 stride 机制:决定“怎么看”那块内存的关键参数
Stride 是理解 Tensor 视图操作的核心。它是一个元组,表示在每个维度上移动一个单位索引时,需要跨多少个内存元素。例如:
x = torch.arange(12).view(3, 4) # shape=(3,4), storage=[0,1,2,...,11] print(x.stride()) # 输出 (4, 1)这意味着:访问x[i, j]时,内存偏移量 =i * 4 + j * 1。第一维 stride=4,因为每向下走一行,内存要跳过整行 4 个元素;第二维 stride=1,因为每向右一列,内存只进一位。
再看转置:
y = x.t() # shape=(4,3),但 storage 仍是 [0,1,...,11] print(y.stride()) # 输出 (1, 4)此时y[i, j]的内存偏移 =i * 1 + j * 4。形状变了,stride 反了,但 Storage 没动——这就是t()零拷贝的原理。
注意:并非所有 shape 变换都能通过调整 stride 实现。比如
x.view(2, 2, 3)要求原始内存能被重新解释为三维连续布局,而x.view(5, 5)则直接报错RuntimeError: shape '[5, 5]' is invalid for input of size 12,因为 5×5≠12。但x.reshape(5, 5)会尝试隐式拷贝(如果可能),这是view和reshape的根本区别:view强制要求 stride 可重构,reshape更宽容,内部会判断是否需拷贝。
2.3 requires_grad 与计算图:Tensor 的“责任链”属性
requires_grad不是布尔开关,而是一条责任声明。当你设置x.requires_grad = True,PyTorch 就在x的TensorImpl中标记“此节点需参与反向传播”,并为其创建一个grad_fn(梯度函数节点)。这个grad_fn记录了x是如何被算出来的——比如x = a + b,则x.grad_fn指向一个AddBackward0对象,它知道反向时该把上游梯度分别加给a和b。
更关键的是:只有 leaf tensor(叶子张量)才默认有.grad属性。所谓叶子张量,是指用户直接创建(如torch.tensor(..., requires_grad=True))或torch.nn.Parameter创建的 tensor,它们不是任何运算的输出。中间变量(如z = x @ y)即使requires_grad=True,其.grad也为None,因为梯度会自动累加到其输入(x,y)的.grad上。
你可以用x.is_leaf查看是否为叶子,用x.grad_fn查看其前驱节点,用x.grad查看累积梯度。一旦调用x.backward(),整个计算图就会从x开始反向遍历,触发每个grad_fn的apply()方法,最终将梯度填入所有叶子节点的.grad。
实操心得:我曾调试一个模型,loss 不下降,
print(loss.grad_fn)发现是None,立刻意识到loss是从 numpy 转过来的(torch.from_numpy().float()默认requires_grad=False),漏掉了.requires_grad_(True)。这种问题无法靠 print 值发现,必须检查grad_fn链。
3. 核心操作深度解析:从创建、变换到设备协同的全链路
3.1 创建阶段:构造器选择背后的内存与性能权衡
PyTorch 提供至少 7 种创建 tensor 的方式,它们在内存分配策略、默认requires_grad、设备绑定上存在显著差异:
| 创建方式 | 示例 | 默认 requires_grad | 是否共享内存 | 设备继承 | 典型用途 |
|---|---|---|---|---|---|
torch.tensor() | torch.tensor([1,2,3]) | False | 否(深拷贝) | 继承输入(list→CPU) | 从 Python 数据结构安全导入 |
torch.Tensor() | torch.Tensor([1,2,3]) | False | 否(深拷贝) | CPU only | 不推荐,无 dtype 控制,易出错 |
torch.zeros()/ones()/empty() | torch.zeros(3,4,dtype=torch.float32) | False | 否 | 可指定device='cuda' | 预分配固定形状缓冲区 |
torch.arange()/linspace() | torch.arange(0,10,0.5) | False | 否 | CPU only | 生成序列,注意浮点精度误差 |
torch.randn()/rand() | torch.randn(1000,1000) | False | 否 | 可指定device | 初始化权重,注意randn是标准正态分布 |
torch.as_tensor() | torch.as_tensor(np_array) | False | 是(零拷贝) | 继承输入(np→CPU) | Numpy 互操作首选,避免冗余拷贝 |
torch.from_numpy() | torch.from_numpy(np_array) | False | 是(零拷贝) | CPU only | 与as_tensor行为一致,语义更明确 |
重点看as_tensor和from_numpy:它们不复制数据,而是直接包装 numpy array 的__array_interface__,共享同一块内存。这意味着:
import numpy as np np_arr = np.array([1,2,3]) pt_tensor = torch.as_tensor(np_arr) np_arr[0] = 999 print(pt_tensor) # tensor([999, 2, 3]) —— 值同步变化!但这也带来风险:若 numpy array 被释放(如函数返回后局部变量销毁),pt_tensor将指向野指针。因此,生产环境务必用torch.tensor()或torch.clone()做一次深拷贝,除非你明确控制生命周期。
注意:
torch.tensor()对 list 是深拷贝,对 numpy array 也是深拷贝(与as_tensor本质不同),这是初学者最易混淆的点。torch.tensor(np_arr)总是安全的,torch.as_tensor(np_arr)快但需谨慎。
3.2 形状变换:view / reshape / transpose / permute 的语义边界
形状操作是 Tensor 最高频操作,但每个方法的约束和副作用截然不同:
view(*shape):严格要求新 shape 与原 shape 元素总数相等,且能通过 stride 重构。失败则抛异常。适用于确定内存连续、无需拷贝的场景。reshape(*shape):行为类似view,但当 stride 无法满足时,会自动调用contiguous()+view,即可能触发内存拷贝。更鲁棒,适合不确定输入连续性的通用代码。transpose(dim0, dim1):仅交换两个维度,返回视图(view)。x.transpose(0,1)等价于x.t()(对 2D)。permute(*dims):按任意顺序重排维度,返回视图。x.permute(2,0,1)将 shape(B,C,H,W)→(H,B,C,W)。narrow(dimension, start, length):沿某维切片,返回视图。x.narrow(0, 2, 3)取第 0 维索引 2~4 的 3 行。expand(*shape):广播扩展,不分配新内存,仅修改 stride 使逻辑 shape 变大。x.expand(2, -1)将(1,5)→(2,5),但x.data_ptr()不变。repeat(*reps):分配新内存,物理复制数据。x.repeat(2,1)将(1,5)→(2,5),但内存翻倍。
关键鉴别点:是否改变tensor.data_ptr()?是否改变tensor.storage().size()?用这两条可 100% 判断是否发生拷贝。
实操心得:我在优化一个图像预处理 pipeline 时,发现
batch.permute(0,3,1,2)(NHWC→NCHW)后接batch.view(-1, 3, H, W)极慢。print(batch.is_contiguous())返回False,原来permute返回非连续视图,view内部被迫拷贝。解决方案:batch = batch.permute(0,3,1,2).contiguous().view(-1, 3, H, W),速度提升 3.2 倍。记住:任何非连续 tensor 后接view或某些 CUDA kernel,都会触发隐式拷贝。
3.3 设备协同:CPU/GPU 数据迁移的隐藏成本与最佳实践
tensor.to(device)看似简单,实则暗藏三重开销:
- 内存分配开销:GPU 显存分配比 CPU 内存分配慢 10~100 倍(尤其首次分配);
- PCIe 带宽瓶颈:CPU↔GPU 数据传输速率通常仅 10~20 GB/s,远低于 GPU 显存带宽(>800 GB/s);
- 同步阻塞:
.to('cuda')默认同步,会等待所有先前 CUDA 操作完成,造成 CPU 空等。
因此,高效迁移的核心原则是:批量、异步、预热。
批量:避免单个 tensor 频繁迁移。应将整个 batch、整个模型参数一次性移到 GPU。例如:
# ❌ 低效:逐个迁移 x = x.to('cuda') y = y.to('cuda') z = z.to('cuda') # ✅ 高效:批量迁移 x, y, z = x.to('cuda'), y.to('cuda'), z.to('cuda') # 或使用 device 参数初始化 x = torch.randn(1000, 1000, device='cuda')异步:对大 tensor,使用
non_blocking=True(需确保源 tensor 在 pinned memory 中):# 先将 DataLoader 的 num_workers > 0 且 pin_memory=True dataloader = DataLoader(dataset, pin_memory=True, num_workers=4) # 然后在训练循环中 for x, y in dataloader: x = x.to('cuda', non_blocking=True) # 异步传输 y = y.to('cuda', non_blocking=True) # 此时 CPU 可继续准备下一个 batch,GPU 并行传输预热:首次
to('cuda')会触发 CUDA 上下文初始化,耗时可达 100ms+。应在训练前用 dummy tensor 预热:_ = torch.zeros(1, device='cuda') # 触发上下文创建
提示:
tensor.device返回device(type='cuda', index=0),tensor.is_cuda是布尔快捷方式。但判断设备类型应优先用tensor.device.type == 'cuda',因is_cuda在多 GPU 下不够精确。
4. 实操全流程:构建一个可调试的张量操作沙盒
4.1 沙盒环境搭建:可视化内存与计算图
我们构建一个轻量级沙盒,用于实时观察 tensor 的底层状态。核心是封装一个TensorInspector类:
class TensorInspector: def __init__(self, name="tensor"): self.name = name def inspect(self, t): print(f"\n=== {self.name} inspection ===") print(f"Shape: {t.shape}") print(f"Stride: {t.stride()}") print(f"Storage size: {t.storage().size()}") print(f"Data ptr: {t.storage().data_ptr():x}") print(f"Is contiguous: {t.is_contiguous()}") print(f"Device: {t.device}") print(f"Requires grad: {t.requires_grad}") print(f"Is leaf: {t.is_leaf}") if t.grad_fn: print(f"Grad fn: {t.grad_fn}") if t.grad is not None: print(f"Grad shape: {t.grad.shape}") print("=" * 30)用它追踪一个典型训练步骤:
# 初始化 x = torch.randn(2, 3, requires_grad=True) w = torch.randn(3, 4, requires_grad=True) b = torch.randn(4, requires_grad=True) insp = TensorInspector("x") # 前向 insp.name = "x"; insp.inspect(x) # leaf, grad=False, contiguous=True insp.name = "w"; insp.inspect(w) # leaf, grad=False, contiguous=True y = x @ w + b # 矩阵乘 + 广播加法 insp.name = "y"; insp.inspect(y) # non-leaf, grad_fn=AddBackward0, contiguous=True loss = y.sum() insp.name = "loss"; insp.inspect(loss) # non-leaf, grad_fn=SumBackward0 # 反向 loss.backward() insp.name = "x_grad"; insp.inspect(x) # .grad now exists, shape=(2,3)运行结果清晰显示:x和w是叶子节点,y和loss是中间节点;y的grad_fn指向AddBackward0,loss指向SumBackward0;反向后x.grad被正确填充。这种可视化是 debug 梯度流的黄金标准。
4.2 梯度调试实战:定位“梯度消失”与“梯度爆炸”的源头
梯度异常是模型不收敛的主因。沙盒可快速定位:
- 梯度消失:
x.grad全为 0 或极小值(如1e-12) - 梯度爆炸:
x.grad出现inf或极大值(如1e8)
常见原因及检测:
| 现象 | 检测命令 | 根本原因 | 解决方案 |
|---|---|---|---|
x.grad为None | print(x.grad is None) | x不是叶子节点,或requires_grad=False | 检查x.requires_grad,确认x是torch.tensor(..., requires_grad=True)创建 |
x.grad全 0 | print(x.grad.abs().max()) | 激活函数饱和(如 sigmoid 输入过大)、权重初始化不当、loss 未正确连接 | 用torch.nn.init.xavier_normal_(w)初始化;检查 loss 计算是否包含x |
x.grad含inf | print(torch.isinf(x.grad).any()) | 除零(如1/x中x=0)、log(0)、数值溢出 | 在log前加clamp(min=1e-8);用torch.finfo(dtype).tiny替代硬编码小值 |
x.grad值域异常 | print(x.grad.mean(), x.grad.std()) | 学习率过大、梯度未归一化、batch size 过小 | 添加torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) |
我曾调试一个 RNN 模型,h.grad在第 3 层就衰减到1e-15。用inspect发现h的requires_grad=True,但h.grad_fn是None。追查发现h = h.detach()被误放在循环内,切断了计算图。删掉detach()后梯度恢复正常。
注意:
detach()创建新 tensor,不共享梯度历史;clone().detach()先拷贝再切断;with torch.no_grad():是上下文管理器,临时禁用梯度。三者适用场景不同,混用是常见 bug 源。
4.3 性能剖析:用torch.autograd.profiler定位 tensor 操作瓶颈
PyTorch 内置 profiler 可精确测量每个 tensor 操作的耗时与内存:
with torch.autograd.profiler.profile(use_cuda=True) as prof: for _ in range(10): x = torch.randn(1000, 1000, device='cuda') y = torch.randn(1000, 1000, device='cuda') z = x @ y z.sum().backward() print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))输出关键行示例:
Name Self CPU total % Self CUDA total % aten::mm 0.2% 85.7% aten::sum 0.1% 8.2% aten::add 0.0% 3.1%这表明矩阵乘mm占据 85.7% 的 CUDA 时间,是绝对瓶颈。若发现aten::copy或aten::contiguous占比较高,则说明存在大量隐式拷贝,需检查view/permute后是否调用了contiguous()。
实操心得:在优化一个 Transformer 推理服务时,profiler 显示
aten::narrow占 40% 时间。深入发现是kv_cache的narrow操作在每次 decode step 都执行。改为预分配kv_cache并用索引赋值cache[:, step] = new_kv,性能提升 2.8 倍。Profiler 不是锦上添花,而是性能优化的起点和终点。
5. 常见问题与排查技巧实录:来自真实项目的 12 个高频陷阱
5.1 “RuntimeError: Trying to backward through the graph a second time” —— 计算图复用陷阱
现象:第一次loss.backward()正常,第二次报错。
原因:PyTorch 默认在反向传播后释放计算图(retain_graph=False),第二次反向时图已销毁。
排查:
print(loss.grad_fn) # 第一次后为 None,证明图已释放解决方案:
- 若需多次反向(如 GAN 的 generator/discriminator 分步更新),加
retain_graph=True:loss_g.backward(retain_graph=True) # generator loss loss_d.backward() # discriminator loss - 更优方案:用
torch.no_grad()包裹不需要梯度的部分,或显式清空:optimizer_g.zero_grad() loss_g.backward() optimizer_g.step() optimizer_d.zero_grad() loss_d.backward() optimizer_d.step()
注意:
retain_graph=True会增加内存占用,因计算图节点不能被垃圾回收。仅在必要时使用。
5.2 “RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation” —— 原地操作破坏图
现象:x += y或x.sigmoid_()后调用backward()报错。
原因:原地操作(inplace)直接修改 tensor 内存,破坏了计算图中保存的原始值,反向时无法计算梯度。
排查:
- 检查所有
+=,-=,*=,/=,sigmoid_(),relu_()等带_的方法; - 用
torch.autograd.set_detect_anomaly(True)启用异常检测,报错时会打印具体操作栈。
解决方案:
- 改用非原地操作:
x = x + y替代x += y; - 若必须原地(如节省内存),确保该 tensor 不参与反向:
x = x.detach().sigmoid_()。
5.3 “CUDA out of memory” —— 显存泄漏的隐形杀手
现象:训练几轮后显存持续增长直至 OOM。
原因:tensor 未被及时释放,常见于:
- 在循环中不断创建新 tensor 但未
del; torch.no_grad()外部保留了中间变量引用;- 使用
@property返回 tensor 但未缓存,每次调用都新建。
排查:
# 查看当前显存占用 print(torch.cuda.memory_allocated() / 1024**3, "GB") # 已分配 print(torch.cuda.memory_reserved() / 1024**3, "GB") # 已预留(含缓存) # 强制清理缓存 torch.cuda.empty_cache()解决方案:
- 循环内用
del tensor并gc.collect(); - 使用
with torch.no_grad():包裹推理代码; - 对大型中间结果,用
.cpu().detach().numpy()立即转出 GPU。
5.4 “ValueError: Expected all tensors to be on the same device” —— 设备不一致的静默错误
现象:x + y报错,但x.device和y.device看起来一样。
原因:x.device可能是cuda:0,y.device是cuda:1,或x在 CPU 而y在 CUDA。
排查:
print(f"x device: {x.device}, y device: {y.device}") print(f"x type: {x.device.type}, y type: {y.device.type}") print(f"x index: {x.device.index}, y index: {y.device.index}")解决方案:
- 统一用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'); - 所有 tensor 创建时指定
device=device; - 模型
.to(device)后,输入数据也.to(device)。
5.5 “RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn” —— loss 未连接到参数
现象:loss.backward()后所有param.grad为None。
原因:loss 计算过程中,某个环节断开了梯度流,如:
- 用
tensor.item()提取标量,返回 Python float,无梯度; - 用
tensor.numpy()转出,返回 numpy array,无梯度; torch.no_grad()范围过大。
排查:
print(loss.requires_grad) # 应为 True print(loss.grad_fn) # 应不为 None # 逐层向上检查:loss 的输入、输入的输入...解决方案:
- 避免在 loss 计算中调用
.item()或.numpy(); - 用
torch.mean()替代np.mean(); - 确保
loss是 tensor,而非 Python 数字。
5.6 其他高频陷阱速查表
| 问题现象 | 根本原因 | 快速修复 |
|---|---|---|
x.view(-1)报错invalid shape | x不连续(如经permute后) | 改用x.contiguous().view(-1) |
torch.cat([x,y], dim=0)报错mismatched dtypes | x.dtype=torch.float32,y.dtype=torch.float64 | 统一x = x.float(),y = y.float() |
x[0] = 1报错assignment destination is read-only | x来自torch.from_numpy()且 numpy array 是只读 | 创建时加writeable=True或用x.clone() |
torch.where(cond, x, y)返回全x或全y | cond是标量 bool,非 tensor | 用torch.tensor(cond)包装 |
x.mean()返回nan | x含inf或nan | 用x = x.nan_to_num()清洗 |
torch.nn.functional.cross_entropy报错target not in [0,C) | target是 one-hot 编码,非 class index | 用target.argmax(dim=1)转换 |
DataLoader返回 tensor 在 CPU,但模型在 GPU | DataLoader未启用pin_memory=True | 加pin_memory=True并在.to('cuda', non_blocking=True) |
torch.save(model, path)保存巨大文件 | 保存了整个计算图(含grad_fn) | 改用torch.save(model.state_dict(), path) |
x.grad在多次backward()后累加而非覆盖 | zero_grad()未调用 | 每次backward()前加optimizer.zero_grad() |
torch.distributed报错default process group is not initialized | 未调用torch.distributed.init_process_group() | 在多卡启动前初始化,指定backend='nccl' |
实操心得:我维护的一个开源模型库,曾因
torch.save(model, path)导致 checkpoint 文件达 2GB(含冗余梯度历史)。改为state_dict后压缩至 300MB,加载速度提升 5 倍。永远用state_dict保存/加载模型,这是铁律。
6. 进阶延伸:Tensor 与系统底层的深度耦合
6.1 内存池与碎片:理解torch.cuda.memory_allocated()与reserved
PyTorch CUDA 内存管理采用两级池化:
- Allocated(已分配):当前被 tensor 占用的显存,
tensor.data_ptr()指向此处; - Reserved(已预留):PyTorch 向 CUDA driver 申请的总显存块,包含 allocated + 缓存(cache)。
torch.cuda.empty_cache()仅释放 cache,不释放 allocated;del tensor后 allocated 内存可能仍被 reserved,直到 Python GC 触发__del__。因此,显存“不释放”不等于泄漏,可能是 PyTorch 的缓存策略。
验证方法:
# 创建大 tensor x = torch.randn(10000, 10000, device='cuda') print(torch.cuda.memory_allocated() / 1024**3) # ~0.75 GB print(torch.cuda.memory_reserved() / 1024**3) # ~0.75 GB # 删除 del x torch.cuda.empty_cache() print(torch.cuda.memory_allocated() / 1024**3) # 0 print(torch.cuda.memory_reserved() / 1024**3) # 可能仍 >0,因 driver 缓存6.2 自定义 autograd Function:突破内置 op 的限制
当需要实现不可导或需定制梯度的 tensor 操作时,继承torch.autograd.Function:
class CustomReLU(torch.autograd.Function): @staticmethod def forward(ctx, input): ctx.save_for_backward(input) # 保存输入供 backward 使用 return input.clamp(min=0) @staticmethod def backward(ctx, grad_output): input, = ctx.saved_tensors grad_input = grad_output.clone() grad_input[input < 0] = 0 # ReLU 梯度:输入>0时为1,否则为0 return grad_input # 使用 x = torch.randn(3, 4, requires_grad=True) y = CustomReLU.apply(x) # 注意是 apply,非实例化 y.sum().backward()ctx是上下文对象,save_for_backward安全保存 tensor,saved_tensors在 backward 中恢复。这让你完全掌控前向/反向逻辑,是实现 custom op 的基石。
6.3 Tensor 的序列化与跨进程共享
PyTorch 提供torch.save/torch.load,但底层是 pickle + tensor 内存映射。对于超大 tensor,推荐:
- 内存映射文件(Memory-mapped file):
torch.load(path, map_location='cpu')可延迟加载; - 共享内存(Shared memory):多进程训练中,用
torch.multiprocessing的share_memory_():x = torch.randn(10000, 10000) x.share_memory_() # 在所有子进程中共享同一块内存
这避免了进程间 tensor 复制,是分布式训练的底层支撑。
最后分享一个小技巧:在 Jupyter 中调试 tensor,别只用
print(x),试试%debug进入报错现场,然后pp x.__dict__查看所有属性;或用!nvidia-smi实时监控显存。Tensor 不是黑箱,它是你亲手操控的、有血有肉的计算实体——理解它,就是理解 PyTorch 的全部灵魂。
