DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

训练越跑越占显存,running_loss += loss 会留下什么

区分日志累加、待反传损失与 retain_graph;用数值保存训练日志,并按相同步骤观察已分配显存,避免把缓存或优化器状态误判为泄漏。

训练开始能正常运行,几个 epoch 后才报 CUDA out of memory。查看代码时发现用于打印平均值的 running_loss += loss 一直在累加,而且 loss 是需要梯度的 Tensor。这值得检查,但还不能据此认定所有前向激活都被永久留住。

本文按 PyTorch 2.8 的自动求导与 CUDA 内存行为说明。重点是用于统计的损失对象活了多久,以及反向传播是否已经释放保存的中间量。

同一行累加,可能有不同的用途

下面是日志对象意外连接计算图的示例,loss 来自需要训练的模型:

running_loss = 0.0
for inputs, targets in loader:
    outputs = model(inputs)
    loss = loss_fn(outputs, targets)
    running_loss += loss

running_loss 从 Python 数值变成 Tensor,并沿加法连接各次损失的计算图。如果尚未执行反向传播,相关图和保存的中间量仍可能被保留;再配上 retain_graph=True,显存压力会更明显。

如果每一步已经执行普通的 loss.backward(),默认不保留反传图,保存的反向中间量通常已释放。日志累加仍可能留下图节点,但不能写成“一定把每一步的全部激活留到最后”。还要检查 create_graph=True、输出列表、模块属性或闭包是否保存了其他需要梯度的结果。

另一个场景是有意先加总多次前向损失,再调用一次 backward()。那本来就在等待对总损失求导,不能把 loss 全换成 .item():数值累加不可微,会破坏训练。是否改成逐个微批反传、如何归一化、何时更新优化器,需要按原训练设计处理;只改日志变量解决不了这类显存需求。

日志只保存数值,训练仍使用原损失

下面是普通单步训练中的局部示例。model、optimizer、loss_fn、loader 已创建,输入和目标由既有流程放在正确设备。这里假定每个批次的 loss 是一个标量:

running_loss = 0.0
batch_count = 0

for inputs, targets in loader:
    optimizer.zero_grad(set_to_none=True)
    outputs = model(inputs)
    loss = loss_fn(outputs, targets)
    loss.backward()
    optimizer.step()

    running_loss += loss.item()
    batch_count += 1

if batch_count:
    print(running_loss / batch_count)

这打印的是批次损失的平均值。最后一个批次更小,或损失按 token、权重等方式归一化时,未必等于每个样本或每个有效 token 的平均损失;统计分母应服从业务定义。

.item() 只接受单元素 Tensor,并取得 Python 数值。它适合普通标量日志,但 CUDA 数值读回会涉及同步,不必为了每个中间值都立刻打印而大量调用。

若确实需要保存张量结果,loss.detach() 可以断开求导关系,但仍位于原设备、共享存储。把大量分离后的 GPU 输出放进列表,张量数据本身仍会占显存。只用于后续离线统计时,应按需要保留有界的 CPU 数据;多元素导出与共享存储的细节可对照 Tensor 转 NumPy 的处理方式。

不要用 .data 绕过自动求导检查,也不要把整个训练循环放进 no_grad() 来消除日志对象的图。

看固定阶段的已分配量

在相同输入尺寸、相同训练阶段记录 torch.cuda.memory_allocated(),比只看任务管理器或 nvidia-smi 总占用更容易判断张量是否持续增加。下面是诊断片段,不会清空缓存:

print("allocated:", torch.cuda.memory_allocated())
print("reserved:", torch.cuda.memory_reserved())

reserved 包含缓存分配器保留的空间,不等于全被活跃 Tensor 使用。empty_cache() 只能释放未使用的缓存块,不能释放仍被引用的计算图或张量。Adam 等优化器还可能在第一次更新时创建状态,随后趋于稳定;第一次跳升与每轮持续增长是不同证据。

修改日志后,在相同位置对照几轮占用,并检查损失与梯度仍按原设计产生。若仍持续增长,继续追踪输出缓存、仍被引用的旧模型与优化器、跨批次状态,而不是继续添加 empty_cache()。涉及截断循环网络的历史梯度时,应另行确认截断边界;不能因为看到 LSTM 就一律保留整张图。