在深度学习的训练场景中,有时候会遇到一开始不会报显存错误,但是随着轮次的提升,显存莫名其妙就爆了,GPU 占用缓慢递增直至
CUDA out of memory
;每个 step 显存都不大,却越跑越高。在本章节中,讨论为什么会出现这种现象以及如何解决。
no_grad()
时上涨更快。torch.cuda.memory_summary()
看到活跃块在增长,但无明显大对象分配。import torch, torch.nn as nn, torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
x = torch.randn(2000, 3, 224, 224)
y = (torch.rand(2000, 10) > 0.5).float()
loader = DataLoader(TensorDataset(x, y), batch_size=32, shuffle=True)
net = nn.Sequential(nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(),
nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(16, 10)).cuda()
opt = torch.optim.AdamW(net.parameters(), lr=1e-3)
logits_buf, labels_buf, loss_hist = [], [], [] # ❌ 跨 step 的“粘性”列表
for step, (bx, by) in enumerate(loader, 1):
bx, by = bx.cuda(), by.cuda()
opt.zero_grad(set_to_none=True)
logits = net(bx) # [B, 10], requires_grad=True
loss = F.binary_cross_entropy_with_logits(logits, by)
loss.backward()
opt.step()
# ❌ 直接把带梯度的张量放进列表,持有整条计算图
logits_buf.append(logits) # ← 泄漏点 1
labels_buf.append(by) # ← 泄漏点 2
loss_hist.append(loss) # ← 泄漏点 3:loss 张量也持图
if step % 50 == 0:
alloc = torch.cuda.memory_allocated() / 1024**2
print(f"step {step}, mem={alloc:.1f} MB")
# 若后面还想做 epoch F1/PR 曲线,这些列表会继续增长并持有图,直至 OOM
可能触发的原因:
logits
、by
、loss
都在计算图链条上(requires_grad=True
);放入 Python 容器会让 Autograd 图无法释放,跨 step 积累。loss_hist.append(loss)
——很多人以为“只存个标量”,但张量不是标量,必须 .item()
。1️⃣ Step 1:量化现象(Codex 生成显存探针)
def gpu_mb():
return torch.cuda.memory_allocated() / 1024**2
# 在训练 loop 打点:
print(f"[dbg] before step={step}, mem={gpu_mb():.1f}MB")
...
print(f"[dbg] after step={step}, mem={gpu_mb():.1f}MB")
2️⃣ Step 2:二分法剥离(ChatGPT 提示)
loss_hist
/ logits_buf
/ labels_buf
,锁定任一恢复即复现。3️⃣ Step 3:Cursor 语义 Review(上下文提示)
requires_grad=True
的张量被加入跨 step 复用的列表”,建议统一 .detach().cpu()
或 .item()
。torch.no_grad()
,加剧增长。4️⃣ Step 4:CodeBuddy PR 建议
retain_graph=True
(历史遗留);import torch, torch.nn as nn, torch.nn.functional as F
logits_buf, labels_buf, loss_hist = [], [], []
for step, (bx, by) in enumerate(loader, 1):
bx, by = bx.cuda(non_blocking=True), by.cuda(non_blocking=True)
opt.zero_grad(set_to_none=True)
with torch.cuda.amp.autocast(False): # 可选:若用 AMP,保持默认策略即可
logits = net(bx)
loss = F.binary_cross_entropy_with_logits(logits, by)
loss.backward()
# 不要随意 retain_graph=True;若确需多次 backward,请定位到子图而非整图
torch.nn.utils.clip_grad_norm_(net.parameters(), 1.0)
opt.step()
# ✅ 仅保存“与训练解耦”的副本
logits_buf.append(logits.detach().cpu()) # 切断计算图,移到 CPU
labels_buf.append(by.detach().cpu())
loss_hist.append(loss.item()) # 标量化
if step % 50 == 0:
print(f"step {step}, mem={torch.cuda.memory_allocated()/1024**2:.1f} MB")
# ✅ 指标计算放到 epoch 尾,并尽快释放 GPU 中间态
import torchmetrics
pred = torch.sigmoid(torch.cat(logits_buf)) > 0.5
tgt = torch.cat(labels_buf).bool()
# ... 计算 F1/PR 等 ...
logits_buf.clear(); labels_buf.clear() # 释放 CPU 内存引用
torch.cuda.empty_cache() # 可选:释放可缓存块(碎片化时有用)
# ✅ 验证阶段务必 no_grad
net.eval()
with torch.inference_mode():
for bx, by in val_loader:
# 验证不会增长显存
_ = net(bx.cuda())
net.train()
备注:
torch.cuda.empty_cache()
只把缓存还给 CUDA 驱动,不是“强制释放”,真正的泄漏关键还是引用断开。
memory_allocated
在训练中稳定震荡(随前向/反向分配与释放),无单调上涨;以上就是这次“显存越跑越涨直到 OOM”的完整排查与修复。把这篇作为“AI 协作 debug 日志”投稿,既能展示真实问题和可复用修复策略,也能量化 AI 带来的效率提升:定位时间从数小时降到 20 分钟内。需最终定位是 把带梯度的张量(logits、loss)存进 Python 列表做 epoch 级指标/可视化,无 detach()
/ .item()
,导致 计算图被跨 step 持有。本文按你的“基本要求”完整记录与 Cursor / Codex / CodeBuddy / ChatGPT 协作排查的真实过程。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。