☰
PyTorch 深度实战:从张量、自动微分到多卡训练与部署的完整链路
2026/10/15 7:05:54 网站建设 项目流程

1. 从一行 import 说起:为什么 PyTorch 值得花时间啃透

如果你写过深度学习代码,大概率第一行就是import torch。但很多人用了两三年,对它的理解还停留在“能跑就行”的层面——模型能训起来就万事大吉,遇到显存爆炸、梯度消失、训练速度上不去,就靠玄学调参硬扛。我见过太多项目,代码里堆满了torch.cuda.empty_cache()和with torch.no_grad(),问为什么这么写,答“网上抄的”。

PyTorch 的核心价值不在于它提供了多少 API,而在于它把动态计算图、自动微分、GPU 加速这三件事做得足够透明。透明意味着你可以精确控制每一步张量运算,也意味着你必须理解每一步在干什么。这篇内容不打算复述官方文档,而是从一个实际使用者的角度,把 PyTorch 从张量操作到模型部署的完整链路拆开,重点讲那些文档里不会写、但实际项目中一定会遇到的细节。

适合谁看?如果你已经能跑通 MNIST 分类,但说不清楚requires_grad和detach()的区别,或者模型在单卡上跑得好好的、一上多卡就报错,那这篇内容就是为你准备的。我会尽量用“为什么这么设计”的视角来解释每个关键机制,而不是只给一堆代码让你复制。

2. 张量不只是数组:理解 PyTorch 的数据底座

2.1 张量的内存布局与视图机制

很多人把 PyTorch 张量当成 NumPy 数组的 GPU 版本,这个理解不算错,但漏掉了最关键的一点:张量的内存布局决定了操作效率。PyTorch 张量在底层是一块连续的内存区域加上一组元数据(形状、步长、偏移量)。步长(stride)这个概念是理解视图操作的关键。

举个例子,一个形状为(3, 4)的张量,默认步长是(4, 1),意味着第 0 维移动一个位置需要跳过 4 个元素,第 1 维移动一个位置跳过 1 个元素。当你调用.t()转置时,PyTorch 并不会真的搬运数据,而是把步长改成(1, 4),形状改成(4, 3)。这就是为什么转置操作几乎不耗时——它只是改了元数据。

但这里有个坑:转置后的张量在内存中不再连续。如果你接着调用.view(),会直接报错,因为view()要求内存连续。正确做法是用.reshape(),它会自动判断是否需要拷贝一份连续内存。我实测过一个场景,在数据加载管道里对图像做转置后直接view,训练脚本跑了半小时才在某个 batch 上报错,排查起来非常浪费时间。

提示:凡是涉及维度变换的操作,先想清楚是“改步长”还是“搬数据”。.view()只改步长,.reshape()可能搬数据,.contiguous()强制搬数据。

2.2 自动微分不是魔法:计算图的构建与释放

PyTorch 的自动微分靠的是动态计算图。每次前向传播时,框架会记录所有涉及requires_grad=True的张量操作,构建一张有向无环图。反向传播时沿着这张图回传梯度。关键点在于:这张图在前向结束后并不会自动消失,它占用的显存会一直保留到你不需要它为止。

这就是为什么训练循环里必须写optimizer.zero_grad()。不清零的话,梯度会在多个 batch 之间累加,相当于变相增大了 batch size,训练动态完全乱套。但很多人不知道的是,zero_grad()默认只把梯度置零,不会释放计算图。真正释放计算图的是反向传播本身——loss.backward()执行完后,中间激活值占用的显存会被释放(除非你设置了retain_graph=True)。

另一个常见误区是torch.no_grad()的使用场景。推理阶段一定要用,因为它告诉 PyTorch 不要构建计算图,能省下大量显存。但训练阶段如果误用了,梯度就传不回去了。我见过有人在验证循环里忘了加no_grad,结果显存占用比训练还高,就是因为验证时也在建图。

2.3 设备管理:CPU 与 GPU 之间的数据搬运

PyTorch 不会自动帮你把数据在 CPU 和 GPU 之间搬来搬去。每个张量都有一个device属性,运算要求所有参与张量在同一设备上。新手最常犯的错误是:模型已经.to('cuda')了,但输入数据还在 CPU 上,一跑就报设备不匹配。

更隐蔽的问题是隐式同步。当你执行loss.item()或print(tensor)时,PyTorch 会强制等待 GPU 完成所有排队操作,把结果拷回 CPU。这个等待在训练循环里如果频繁出现,会严重拖慢速度。正确做法是累积多个 batch 的 loss 后再统一取平均值,而不是每个 batch 都.item()。

还有一个经验:尽量在 GPU 上创建张量。比如torch.zeros(1000, 1000, device='cuda')比先创建再.to('cuda')快得多,因为省去了一次主机到设备的拷贝。数据加载器那边也是同理,用pin_memory=True配合non_blocking=True能让拷贝和计算重叠起来。

3. 模型构建的取舍:从 nn.Module 到参数初始化

3.1 nn.Module 的注册机制与参数管理

nn.Module是 PyTorch 模型构建的基石,但它的核心机制很多人没搞明白:只有被正确注册的子模块和参数才会出现在parameters()里。什么叫正确注册?就是把nn.Linear、nn.Conv2d这类模块赋值给self的属性,或者用nn.Parameter包装张量。

如果你把参数存在一个普通 Python 列表里,比如self.weights = [nn.Parameter(...), ...],这些参数不会出现在model.parameters()中,优化器就更新不到它们。正确做法是用nn.ParameterList或nn.ModuleList。这个坑我在早期做多分支网络时踩过,模型训练 loss 完全不降,排查了一整天才发现是参数没注册。

另一个细节是train()和eval()的切换。这两个方法会递归地改变所有子模块的模式,影响 Dropout 和 BatchNorm 的行为。但如果你自定义了模块,需要自己实现对应的逻辑。我建议在自定义模块里显式检查self.training来决定行为,而不是依赖外部调用。

3.2 参数初始化的实际影响

PyTorch 对nn.Linear和nn.Conv2d有默认初始化,用的是 Kaiming 均匀分布。这个默认值在大多数情况下够用,但在深层网络里可能不够。我做过一个对比实验:同样的 20 层全连接网络,用默认初始化时训练到第 5 个 epoch 梯度就接近零了,换成显式的 Kaiming 正态初始化后能稳定训练到收敛。

初始化的核心原则是保持前向传播时各层激活值的方差稳定。如果每层输出方差逐渐缩小,信号传到后面就没了;如果逐渐放大,就会爆炸。Kaiming 初始化通过考虑 fan_in(输入维度)来缩放初始权重,让方差保持大致不变。具体公式是标准差等于sqrt(2 / fan_in),适用于 ReLU 激活函数。

实际操作中,我通常会在模型定义完后遍历所有模块,对卷积和全连接层做统一初始化:

import torch.nn as nn import math def init_weights(m): if isinstance(m, nn.Linear) or isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu') if m.bias is not None: nn.init.zeros_(m.bias) model.apply(init_weights)

注意apply会递归遍历所有子模块,所以不用担心遗漏。偏置项初始化为零是常见做法,因为权重已经提供了足够的随机性。

3.3 自定义层的设计模式

当内置层不够用时,就需要自定义。自定义层的关键是继承nn.Module,在__init__里注册所有需要训练的参数和子模块,在forward里定义计算逻辑。这里有个容易忽略的点:forward 里不要创建新的参数。每次调用 forward 都创建nn.Parameter会导致参数不断累积,显存越用越多。

如果确实需要动态生成权重(比如某些元学习场景),应该用torch.nn.functional里的无状态函数,传入外部管理的参数。我见过一个实现注意力机制的代码,在 forward 里创建了一个nn.Linear来做投影,结果每个 batch 都新增一组参数,跑了几百步就 OOM 了。

4. 训练循环里的魔鬼细节:优化器、学习率与梯度控制

4.1 优化器选择的实际考量

PyTorch 提供了 SGD、Adam、AdamW、RMSprop 等优化器。新手最常问的是“选哪个”。我的经验是:如果不知道选什么,先用 AdamW。它在 Adam 的基础上修正了权重衰减的实现,把 L2 正则和自适应学习率解耦,在大多数任务上表现稳定。

但 Adam 系列有个问题:它维护了每个参数的一阶和二阶动量,显存占用是参数量的两倍。对于大模型,这可能是压垮显存的最后一根稻草。这时候 SGD 加动量反而是更实际的选择,虽然收敛慢一些,但显存占用小。我做过一个 1 亿参数量的模型,用 Adam 时单卡只能放 batch size 8,换成 SGD 后能放到 32。

另一个细节是weight_decay的设置。很多人直接照搬论文里的值,但忽略了 Adam 和 SGD 对权重衰减的敏感度不同。AdamW 的权重衰减通常设在 0.01 到 0.1 之间,而 SGD 的权重衰减可以设得更小。我一般会先用默认值跑一轮,观察训练和验证 loss 的差距,如果验证 loss 明显高于训练 loss,再适当增大权重衰减。

4.2 学习率调度:不是越复杂越好

学习率调度器的作用是在训练过程中动态调整学习率。PyTorch 提供了StepLR、CosineAnnealingLR、ReduceLROnPlateau等多种调度器。我的建议是:优先用余弦退火,它在大多数任务上表现稳定,不需要手动设置衰减节点。

ReduceLROnPlateau看起来很美——验证 loss 不降就降学习率。但它有个问题:需要你手动传入验证指标,而且 patience 参数很难调。patience 设小了学习率降得太频繁,设大了又起不到作用。我实测下来,余弦退火配合 warmup(前几个 epoch 线性增加学习率)的组合最省心。

warmup 的作用是避免训练初期梯度过大导致参数跑飞。实现方式很简单,用一个线性函数在前 N 个 step 里把学习率从 0 升到目标值。很多预训练模型都用了这个技巧,但在小数据集上可能不需要。

4.3 梯度裁剪与梯度累积

梯度裁剪是防止梯度爆炸的常用手段。torch.nn.utils.clip_grad_norm_会把所有参数的梯度范数限制在一个阈值内。这个操作应该在loss.backward()之后、optimizer.step()之前执行。阈值设多少?我一般从 1.0 开始试,如果训练不稳定就降到 0.5,如果模型收敛太慢就升到 5.0。

梯度累积是另一个实用技巧:当显存不够放下大 batch 时,可以分多次前向反向,累积梯度后再更新参数。实现上就是在每次loss.backward()后不立即step(),而是等累积够 N 次后再step()并清零梯度。注意 loss 要除以累积步数,否则等效学习率会变大。

accumulation_steps = 4 for i, (inputs, labels) in enumerate(dataloader): outputs = model(inputs) loss = criterion(outputs, labels) / accumulation_steps loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()

这个模式在显存受限时非常有用,等效于把 batch size 扩大了 4 倍,但显存占用不变。

5. 数据管道:从 Dataset 到 DataLoader 的性能调优

5.1 Dataset 设计的常见陷阱

自定义Dataset需要实现__len__和__getitem__。看起来简单,但有几个坑。首先,__getitem__里不要做太重的计算,比如实时解码高分辨率图像或做复杂增强。这些操作应该尽量放在 DataLoader 的 worker 进程里并行执行,而不是在主进程串行执行。

其次,注意数据类型的转换。__getitem__返回的张量默认是 CPU 上的,DataLoader 会负责把它们拼成 batch。如果返回的是 Python 列表或字典,PyTorch 有默认的 collate 函数来处理,但自定义结构可能需要自己写collate_fn。我见过有人在__getitem__里返回一个包含可变长度序列的列表,结果默认 collate 直接报错,因为不同样本长度不一致。

还有一个经验:尽量返回张量而不是 PIL 图像。PIL 图像在 collate 时需要额外转换,而且不支持某些增强操作。如果数据源是图像文件,在__getitem__里就用torchvision.transforms转成张量再返回。

5.2 DataLoader 的 num_workers 与内存平衡

num_workers控制有多少个子进程并行加载数据。设得太小,GPU 等数据;设得太大,内存爆炸。我的经验值是:num_workers 设为 CPU 核心数的 1/4 到 1/2。比如 8 核机器设 2 到 4 比较合适。但这不是绝对的,如果数据增强很重,可以适当增加;如果数据集很小,设 0 反而更快,因为省去了进程间通信开销。

pin_memory=True在 GPU 训练时几乎总是应该开启。它会把数据加载到锁页内存,使得主机到设备的拷贝可以用 DMA 异步完成。配合non_blocking=True在.to(device)时使用,能让数据拷贝和 GPU 计算重叠。我实测过一个图像分类任务,开启这两个选项后每个 epoch 快了将近 15%。

但要注意,pin_memory会占用更多主机内存。如果数据集很大且内存紧张,可能需要权衡。另外,persistent_workers=True可以避免每个 epoch 重新创建 worker 进程,在 epoch 数很多时能省下不少启动开销。

5.3 数据增强的时机与位置

数据增强应该在__getitem__里做,而不是在训练循环里做。原因很简单:__getitem__在 worker 进程里执行,可以并行;训练循环在主进程里执行,串行。把增强放在训练循环里,GPU 利用率会明显下降。

另一个问题是增强的随机性。如果用了random模块,要注意每个 worker 的随机种子可能相同,导致不同 worker 产生相同的增强结果。PyTorch 的 DataLoader 会自动为每个 worker 设置不同的种子,但如果你用了自定义的随机数生成器,需要自己处理。我一般会在__getitem__里用torch.rand而不是 Python 的random,因为 PyTorch 的随机数生成器对多进程更友好。

6. 显存优化:从 OOM 到稳定训练

6.1 显存都去哪了:拆解占用来源

显存占用主要来自四部分:模型参数、梯度、优化器状态、中间激活值。前三者相对固定,中间激活值随 batch size 线性增长。一个常见的误解是“模型参数量决定了显存占用”,实际上在训练阶段,中间激活值往往是大头。

举个例子,一个 1000 万参数量的模型,参数占 40MB(float32),梯度再占 40MB,Adam 优化器状态占 80MB,加起来 160MB。但如果 batch size 是 64,输入是 224x224 的图像,中间激活值可能占到几个 GB。这就是为什么减小 batch size 能有效缓解 OOM。

6.2 混合精度训练:省显存还能提速

混合精度训练用 float16 做前向和反向,用 float32 维护模型参数副本。这样中间激活值减半,显存占用大幅下降,同时 GPU 的 float16 计算单元吞吐更高,训练速度也能提升。PyTorch 通过torch.cuda.amp提供了自动混合精度支持。

使用方式很简单:用autocast上下文管理器包裹前向传播,用GradScaler缩放 loss 再反向传播。缩放是为了防止 float16 下梯度下溢。我实测过一个 ResNet50 的训练,开启混合精度后显存从 10GB 降到 6GB,每个 epoch 时间从 120 秒降到 85 秒。

但混合精度不是万能的。某些操作在 float16 下数值不稳定,比如 softmax 和 layer norm。autocast会自动把这些操作保持在 float32,但如果你自定义了层,可能需要手动指定。另外,GradScaler需要和优化器配合,在step()前调用scaler.step(optimizer),在zero_grad()前调用scaler.update()。

6.3 梯度检查点:用时间换空间

梯度检查点(gradient checkpointing)是一种用计算时间换显存的技术。它在前向传播时不保存中间激活值,反向传播时重新计算。这样显存占用从 O(n) 降到 O(sqrt(n)),代价是多了大约 30% 的计算时间。

PyTorch 通过torch.utils.checkpoint提供支持。用法是把需要检查点的模块包在checkpoint.checkpoint()里。但要注意,不是所有模块都适合做检查点。如果模块本身计算量很小,重新计算的收益抵不上开销。我一般只对 Transformer 的注意力层或 ResNet 的 bottleneck 块做检查点。

还有一个坑:检查点要求输入张量的requires_grad为 True,否则不会保存计算图。如果输入是嵌入层的输出,需要确保嵌入层的参数是可训练的。

7. 模型保存与加载:格式选择与兼容性

7.1 state_dict 与整个模型的取舍

PyTorch 保存模型有两种方式:保存state_dict(推荐)和保存整个模型。保存整个模型用torch.save(model, path),加载时用torch.load(path)。这种方式看起来方便,但有几个致命问题:它依赖模型类的定义,如果代码结构变了,加载就会失败;它把模型结构和参数绑在一起,不利于迁移学习。

保存state_dict是更好的选择。它只保存参数张量,加载时需要先实例化模型,再调用load_state_dict。这样模型结构可以独立演化,只要参数名匹配就能加载。我一般会在保存时同时存下优化器状态和当前 epoch,方便断点续训。

torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': loss, }, 'checkpoint.pth')

加载时用torch.load读回来,分别恢复模型和优化器。注意torch.load默认会把张量加载到保存时的设备上,如果保存时在 GPU 而加载时想用 CPU,需要指定map_location。

7.2 跨设备加载与版本兼容

跨设备加载是常见需求。比如在 GPU 上训练,在 CPU 上推理。用map_location='cpu'就能把张量映射到 CPU。反过来,从 CPU 加载到 GPU 也可以,但要注意torch.load的map_location参数可以是一个函数,用来精细控制每个张量的位置。

版本兼容是另一个问题。PyTorch 不同版本之间的state_dict格式可能有细微差异。我遇到过用 1.7 保存的模型在 1.10 上加载时参数名对不上的情况。解决办法是加载时用strict=False,让 PyTorch 忽略不匹配的键,然后手动检查哪些参数没加载上。但这只是权宜之计,长期来看还是应该统一环境版本。

8. 多卡训练:DataParallel 与 DistributedDataParallel 的选择

8.1 DataParallel 的局限

nn.DataParallel是最简单的多卡方案,一行代码就能把模型放到多张卡上。但它的实现方式是在每次前向传播时把输入切分到各卡,在卡 0 上汇总输出计算 loss,再反向传播。这导致卡 0 的显存占用和计算量都远高于其他卡,负载严重不均衡。

更严重的是,DataParallel在每个 batch 都要做一次卡间通信,通信开销随卡数增加而增加。我实测过 4 卡 DataParallel,加速比只有 2.5 倍左右,而且卡 0 经常 OOM。所以我的建议是:能用 DistributedDataParallel 就别用 DataParallel。

8.2 DistributedDataParallel 的启动与配置

DistributedDataParallel(DDP)采用多进程架构,每个进程控制一张卡,各自独立前向反向,只在梯度同步时通信。这样负载均衡,通信效率也更高。但配置起来复杂一些,需要设置进程组、初始化环境、用DistributedSampler切分数据。

启动方式有两种:torch.multiprocessing.spawn和torchrun。后者是官方推荐的方式,支持多机多卡,而且能自动处理一些环境变量。我一般用torchrun --nproc_per_node=4 train.py来启动 4 卡训练。

DDP 的一个关键点是DistributedSampler。它确保每个进程看到不同的数据子集,避免重复计算。每个 epoch 开始前需要调用sampler.set_epoch(epoch),否则随机种子不变,数据切分方式每个 epoch 都一样。

8.3 多卡训练的常见报错与排查

多卡训练最容易出的问题是进程间通信超时。常见原因包括:某张卡上的 batch 数据量不一致导致等待、某个进程崩溃导致其他进程卡住、网络配置问题导致 NCCL 通信失败。排查时可以先看日志里哪个进程先报错,然后单独跑那个进程的代码。

另一个坑是 BatchNorm 在多卡下的行为。DDP 默认每张卡独立计算 BatchNorm 统计量,这在小 batch size 下会导致统计不准确。解决办法是用SyncBatchNorm,它在所有卡上同步统计量。但SyncBatchNorm会增加通信开销,而且不是所有模型都支持。

9. 从训练到部署:TorchScript 与 ONNX 导出

9.1 TorchScript 的追踪与脚本化

TorchScript 是 PyTorch 的中间表示,可以把模型从 Python 代码转成独立的计算图,脱离 Python 环境运行。转换方式有两种:torch.jit.trace和torch.jit.script。trace通过运行一次前向传播来记录操作,简单但不支持动态控制流;script直接解析 Python 代码,支持控制流但要求代码符合 TorchScript 语法。

我一般先用trace试,如果模型里有 if/for 依赖输入数据,就改用script。trace的一个坑是它会固化输入形状,如果推理时输入尺寸变了,需要重新 trace。script则更灵活,但需要把模型里的 Python 特性(如动态类型、闭包)改写成 TorchScript 支持的写法。

9.2 ONNX 导出的注意事项

ONNX 是跨框架的模型格式,导出后可以在 TensorRT、OpenVINO 等推理引擎上运行。PyTorch 通过torch.onnx.export支持导出。关键参数是input_names、output_names和dynamic_axes。dynamic_axes用来指定哪些维度是动态的,比如 batch size 和序列长度。

导出时最常见的报错是“不支持的操作”。PyTorch 的某些操作在 ONNX 里没有对应实现,比如自定义的 CUDA 核函数。解决办法是用 ONNX 支持的操作重写,或者用torch.onnx.register_custom_op_symbolic注册自定义符号。我建议导出前先用torch.onnx.export的verbose=True看看哪些操作被转换了,哪些被忽略了。

导出后一定要用 ONNX Runtime 验证一遍,确保输出和 PyTorch 一致。数值误差在 1e-5 以内算正常,如果差太多说明转换有问题。

10. 那些年我踩过的 PyTorch 坑:真实排查记录

10.1 loss 不降反升:从数据管道找到元凶

有一次训练一个图像分类模型,loss 在前几个 batch 正常下降,然后突然飙升到 NaN。检查了学习率、梯度裁剪、初始化,都没问题。最后发现是数据管道里的归一化参数写错了——均值写成了 0.5 但实际数据均值是 0.2,导致输入分布偏移。这个坑的教训是:数据管道的正确性要用可视化验证,不能只看代码逻辑。

具体做法是:从 DataLoader 里取一个 batch,把图像反归一化后显示出来,确认看起来正常。同时打印每个通道的均值和方差,和预期对比。这个检查花不了五分钟,但能省下几小时的排查时间。

10.2 多卡训练速度反而变慢:通信瓶颈定位

用 DDP 跑 4 卡训练,发现比单卡还慢。排查后发现是模型太小,通信开销超过了计算收益。DDP 在每个 batch 后都要做 all-reduce 同步梯度,如果模型参数量少、计算量小,通信就成了瓶颈。解决办法是增大 batch size 或换用梯度累积,让每次通信对应更多的计算。

另一个原因是 NCCL 的默认配置不适合当前网络环境。可以通过设置环境变量NCCL_IB_DISABLE=1或NCCL_SOCKET_IFNAME来调整通信接口。这个需要根据实际硬件环境来调,没有通用方案。

10.3 显存泄漏:一个没有释放的计算图

有一次在验证循环里忘了加torch.no_grad(),导致每个 batch 都构建计算图,显存越用越多,跑了几百个 batch 后 OOM。这个问题的隐蔽之处在于,它不会立即报错,而是随着验证集增大逐渐暴露。解决办法很简单:验证和推理阶段一律加with torch.no_grad():。

还有一个更隐蔽的显存泄漏:在训练循环里把 loss 存进了一个列表,比如losses.append(loss)。这个 loss 是带计算图的张量,存下来就相当于保留了整个计算图。正确做法是存loss.item()或loss.detach()。

11. 版本演进与生态:PyTorch 2.x 带来了什么

PyTorch 2.0 引入了torch.compile,通过图编译技术自动优化模型性能。我实测过一个 Transformer 模型,开启torch.compile后训练速度提升了 20% 到 30%。用法很简单:model = torch.compile(model)。但要注意,编译需要时间,第一次运行会慢一些,适合长时间训练的场景。

另一个重要变化是torch.func模块的引入,提供了函数式的自动微分 API。这对于需要高阶梯度或复杂微分操作的研究场景很有用。不过对于大多数应用开发者来说,传统的autograd接口已经够用了。

生态方面,torchvision、torchaudio、torchtext这些领域库也在持续更新。我建议保持这些库和 PyTorch 主版本同步升级,避免版本不匹配导致的兼容性问题。升级前先在测试环境跑一遍完整训练流程,确认没有 API 变更影响。

12. 我个人的 PyTorch 使用习惯

写了这么多,最后分享几个我日常使用中形成的习惯。第一,每个项目固定一个随机种子,在训练脚本开头设置torch.manual_seed、torch.cuda.manual_seed_all和numpy.random.seed,保证实验可复现。第二,训练前先跑一个 batch 的过拟合测试,用少量数据训练几十步,确认模型能记住这些数据。如果连过拟合都做不到,说明模型结构或训练逻辑有问题。

第三,日志里记录学习率、梯度范数和显存占用。这三个指标能提前预警大部分训练问题。梯度范数突然增大说明可能要梯度爆炸,显存占用持续上升说明可能有泄漏。第四,保存 checkpoint 时同时保存优化器状态和 epoch,断点续训时能无缝恢复。我一般每 5 个 epoch 存一次,同时保留最近三个 checkpoint,防止某个 checkpoint 损坏。

这些习惯看起来琐碎,但每一个都是踩坑之后总结出来的。PyTorch 的灵活性是把双刃剑,它给了你完全的控制权,也要求你对每个细节负责。希望这篇内容能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询