☰
PyTorch显存泄漏排查指南:从计算图机制到实战修复
2026/10/31 13:16:30 网站建设 项目流程

1. 显存泄漏到底是个什么问题

训练到一半突然蹦出CUDA Out of Memory,重启之后又能跑一阵,跑着跑着显存又满了——这个场景但凡做过深度学习训练的人都不陌生。很多人第一反应是“模型太大”或者“batch size 开太大了”,于是把 batch 调小、把图片尺寸降下来,结果发现显存该涨还是涨,只是涨得慢了一点。这种情况,大概率不是模型本身的问题,而是显存泄漏。

显存泄漏和普通的显存不够是两码事。显存不够是你在某个时刻需要的显存超过了显卡容量,比如模型参数加中间激活值加优化器状态一共需要 20GB,但你只有 16GB 的卡,那不管怎么跑都会 OOM。而显存泄漏是随着训练步数增加,显存占用持续上升,哪怕每一步的计算量完全一样。典型的症状就是:前 100 个 step 显存占用 8GB,到 500 个 step 变成 12GB,到 1000 个 step 直接 OOM。你去看nvidia-smi,发现显存就是被你的进程一点点吃掉的,但模型大小、batch size 都没变。

这个问题在 PyTorch 训练里特别常见,尤其是自己写训练循环、自定义 loss、做多任务学习或者搞强化学习的时候。因为 PyTorch 是动态图框架,计算图在每次 forward 的时候动态构建,如果你不小心把某些中间变量保留下来了,计算图就不会被释放,显存自然越积越多。更麻烦的是,这种泄漏往往不会立刻报错,而是等到你跑了几百上千步之后才炸,排查起来非常头疼。

这篇文章就是写给那些正在被显存泄漏折磨的人。不管你是刚入门 PyTorch 的新手,还是已经能熟练搭建训练流程的老手,只要你的训练任务出现了“越跑显存越高”的现象,这里面的排查思路和修复方法都能直接拿来用。我会从计算图的机制讲起,把常见的泄漏场景一个个拆开,给出具体的定位手段和修复代码,最后再分享一些我在实际项目里踩过的坑和总结出来的经验。

2. 计算图与显存管理机制拆解

2.1 动态图到底是怎么吃显存的

要理解显存泄漏,得先搞清楚 PyTorch 的动态计算图是怎么工作的。当你执行loss = model(input)的时候,PyTorch 会记录下所有涉及张量操作的运算,构建一张从输入到 loss 的计算图。这张图里的每个节点都保存了前向传播时的中间结果,因为反向传播求梯度的时候需要用到这些中间值。

举个例子,假设你有一个简单的线性层y = Wx + b,反向传播求 W 的梯度需要用到 x 的值,求 b 的梯度需要用到上游传回来的梯度。所以在前向传播的时候,PyTorch 会把 x 和 y 都存下来,挂在计算图上。这些中间张量占用的显存,就是计算图的显存开销。

正常情况下,当你调用loss.backward()之后,计算图上的中间张量会被释放,因为梯度已经算完了,不再需要它们了。然后你调用optimizer.step()更新参数,再调用optimizer.zero_grad()清空梯度,进入下一个 iteration。整个过程显存占用应该是稳定的,不会持续增长。

但如果你在某个地方不小心把计算图的一部分“留住”了,比如把某个中间张量赋值给了一个全局变量,或者存进了一个列表里,那这个张量就不会被释放,它所在的计算图分支也会一直保留。每跑一个 step,就多留一份,显存自然就涨上去了。

2.2 哪些操作会让计算图无法释放

常见的导致计算图无法释放的操作有这么几类:

第一类是把带梯度的张量存进了列表或字典。比如你想记录每个 step 的 loss 值,写了个losses.append(loss),看起来没问题,但loss是一个带计算图的张量,你把它存进列表之后,这个计算图就不会被释放了。正确的做法是losses.append(loss.item()),用.item()取出标量值,切断计算图。

第二类是在训练循环里做了需要梯度的运算但没有 detach。比如你想监控某个中间层的输出,写了activations.append(feature_map),这个feature_map是带梯度的,存下来就泄漏了。应该写成activations.append(feature_map.detach().cpu()),先切断梯度再搬到 CPU 上。

第三类是在验证或测试阶段没有用torch.no_grad()。验证的时候你不需要计算梯度,但如果不加torch.no_grad(),PyTorch 还是会构建计算图,白白浪费显存。虽然验证完一个 batch 之后计算图会被释放,但如果验证集很大,或者你在验证的时候也做了类似“存中间结果”的操作,同样会导致泄漏。

第四类是在自定义 loss 或自定义层里保留了不必要的引用。比如你在 loss 函数里把某个中间变量存到了self上,或者用了闭包捕获了带梯度的张量,都会导致计算图无法释放。

第五类是在多卡训练或分布式训练里,梯度没有正确同步或释放。这种情况相对复杂,但核心问题是一样的:某些该释放的张量没有被释放。

2.3 显存泄漏和显存碎片化的区别

还有一个容易混淆的概念是显存碎片化。显存碎片化不是泄漏,但表现和泄漏很像:显存占用看起来在涨,但实际上是因为频繁申请和释放不同大小的显存块,导致显存池里出现了很多小的空洞,无法被大块分配利用。

区分两者的方法很简单:如果是泄漏,torch.cuda.memory_allocated()会持续上升;如果是碎片化,torch.cuda.memory_allocated()可能比较稳定,但torch.cuda.memory_reserved()会涨得比较快。memory_allocated是实际被张量占用的显存,memory_reserved是 PyTorch 向 CUDA 申请的总显存(包括缓存池里空闲的部分)。

碎片化一般可以通过设置PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True来缓解,或者调整 batch size 让每次申请的显存块大小更一致。但泄漏只能通过找到并切断那个“留住计算图”的引用来解决。

3. 定位显存泄漏的实操手段

3.1 用 memory_allocated 和 memory_reserved 做初步判断

定位泄漏的第一步是确认它确实是泄漏,而不是碎片化或者正常的显存波动。最直接的方法是在训练循环里每隔一定步数打印一次显存统计:

import torch for step, (inputs, labels) in enumerate(dataloader): inputs = inputs.cuda() labels = labels.cuda() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() optimizer.zero_grad() if step % 100 == 0: allocated = torch.cuda.memory_allocated() / 1024**2 reserved = torch.cuda.memory_reserved() / 1024**2 max_allocated = torch.cuda.max_memory_allocated() / 1024**2 print(f"Step {step}: allocated={allocated:.1f}MB, " f"reserved={reserved:.1f}MB, max_allocated={max_allocated:.1f}MB")

如果allocated随着 step 持续上升,那基本可以确定是泄漏。如果allocated稳定但reserved在涨,那可能是碎片化。如果两者都稳定,那说明显存管理没问题,OOM 可能是别的原因导致的,比如某个 batch 的数据特别大。

这里有个细节:torch.cuda.max_memory_allocated()返回的是从程序开始到现在的峰值显存占用,它只会增不会减。所以如果你看到max_allocated在涨,不一定代表当前有泄漏,可能只是之前某个时刻用得多。真正要关注的是allocated的实时值。

3.2 用 torch.cuda.memory_summary 看详细分布

torch.cuda.memory_summary()可以打印出非常详细的显存分配情况,包括每个显存块的大小、地址、是否正在被使用等。这个输出信息量很大,但如果你仔细看,能找到一些线索。

print(torch.cuda.memory_summary(device=None, abbreviated=False))

输出里会有一个表格,列出所有活跃的显存分配。如果你发现有很多同样大小的显存块在持续增加,那很可能就是泄漏点。比如你看到几百个 4MB 的块,每个都对应一个中间张量,那就说明这些张量没有被释放。

不过memory_summary的输出比较长,在训练循环里频繁打印会影响性能。建议只在怀疑泄漏的时候,在特定的 step 打印一次,或者写个条件判断,只在显存超过阈值的时候打印。

3.3 用 objgraph 追踪 Python 对象引用

如果显存统计确认了泄漏,下一步就是找到到底是哪个 Python 对象留住了计算图。这时候可以用objgraph这个库来追踪对象的引用关系。

import objgraph import torch # 在训练循环的某个 step 调用 objgraph.show_most_common_types(limit=20)

这会打印出当前 Python 堆里数量最多的对象类型。如果你看到Tensor的数量在持续增加,那说明有张量没有被释放。然后可以用objgraph.show_backrefs来查看某个张量的引用链:

import objgraph # 找到一个泄漏的张量 leaked_tensor = None for obj in gc.get_objects(): if isinstance(obj, torch.Tensor) and obj.is_cuda: leaked_tensor = obj break if leaked_tensor is not None: objgraph.show_backrefs([leaked_tensor], max_depth=5)

这会生成一张引用关系图,告诉你谁在引用这个张量。顺着引用链往上找,就能找到那个“罪魁祸首”——通常是一个全局列表、一个类的成员变量,或者一个闭包。

3.4 用 tracemalloc 定位 Python 层面的内存分配

tracemalloc是 Python 标准库里的内存追踪工具,虽然它追踪的是 CPU 内存,但对于定位“哪个变量留住了张量”同样有用。因为张量对象本身在 CPU 上有一个 Python 对象头,tracemalloc能追踪到这个对象的分配位置。

import tracemalloc tracemalloc.start(25) # 保存 25 层调用栈 # 训练循环 for step, (inputs, labels) in enumerate(dataloader): # ... 训练代码 ... if step == 100: snapshot1 = tracemalloc.take_snapshot() if step == 500: snapshot2 = tracemalloc.take_snapshot() top_stats = snapshot2.compare_to(snapshot1, 'lineno') for stat in top_stats[:10]: print(stat)

这会打印出从 step 100 到 step 500 之间,哪些代码行分配的内存增长最多。如果某一行是losses.append(loss)或者类似的代码,那基本就锁定泄漏点了。

3.5 用 PyTorch 的 autograd profiler 看计算图

PyTorch 自带的 profiler 可以记录每个操作的显存分配情况,虽然它主要用于性能分析,但也能用来定位显存泄漏。

from torch.profiler import profile, ProfilerActivity with profile(activities=[ProfilerActivity.CUDA], profile_memory=True, record_shapes=True) as prof: for step, (inputs, labels) in enumerate(dataloader): # ... 训练代码 ... if step >= 10: break print(prof.key_averages().table(sort_by="cuda_memory_usage", row_limit=20))

这会列出显存占用最高的操作。如果某个操作在每次迭代中都分配显存但没有释放,那它很可能就是泄漏源。不过 profiler 本身也会占用显存,所以建议只在排查阶段用,不要一直开着。

4. 常见泄漏场景与修复方案

4.1 把 loss 存进列表导致计算图累积

这是最常见的泄漏场景,没有之一。很多人想记录训练过程中的 loss 曲线,顺手就写了:

losses = [] for step, (inputs, labels) in enumerate(dataloader): outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() optimizer.zero_grad() losses.append(loss) # 这里泄漏了

loss是一个带计算图的标量张量,你把它存进列表,整个计算图就被保留了。每个 step 都存一个,显存线性增长。

修复方法很简单:

losses.append(loss.item()) # 用 .item() 取出 Python 标量

或者如果你需要保留张量用于后续计算,至少要先 detach:

losses.append(loss.detach().cpu()) # 切断梯度并搬到 CPU

注意:.item()只能用于标量张量(只有一个元素的张量)。如果你的 loss 是向量或者矩阵,需要用.detach().cpu().numpy()或者.detach().cpu().tolist()。

4.2 验证阶段没有用 no_grad 导致计算图堆积

验证阶段不需要计算梯度,但如果你忘了加torch.no_grad(),PyTorch 还是会构建计算图。虽然每个 batch 结束后计算图会被释放,但如果验证集很大,或者你在验证的时候也做了“存中间结果”的操作,同样会导致显存问题。

# 错误写法 model.eval() for inputs, labels in val_dataloader: outputs = model(inputs) val_loss = criterion(outputs, labels) val_losses.append(val_loss) # 双重泄漏:没加 no_grad + 存了张量 # 正确写法 model.eval() with torch.no_grad(): for inputs, labels in val_dataloader: outputs = model(inputs) val_loss = criterion(outputs, labels) val_losses.append(val_loss.item())

torch.no_grad()会关闭自动梯度记录,前向传播不会构建计算图,显存占用会大幅降低。对于大模型来说,验证阶段的显存占用可能只有训练阶段的 1/3 到 1/2。

4.3 自定义 loss 里保留了中间变量

如果你自己写了 loss 函数,并且在里面把某个中间变量存到了self上,或者用了闭包捕获,也会导致泄漏。

class MyLoss(nn.Module): def __init__(self): super().__init__() self.intermediate = None # 危险:成员变量会一直保留 def forward(self, outputs, labels): self.intermediate = outputs * 2 # 这里把带梯度的张量存下来了 loss = (self.intermediate - labels).pow(2).mean() return loss

每次调用forward,self.intermediate都会被覆盖,但旧的计算图不会立即释放,因为self.intermediate还引用着它。正确的做法是不要存中间变量,或者存的时候先 detach:

class MyLoss(nn.Module): def __init__(self): super().__init__() def forward(self, outputs, labels): intermediate = outputs * 2 # 局部变量,forward 结束后自动释放 loss = (intermediate - labels).pow(2).mean() return loss

如果确实需要记录中间变量用于调试,用self.intermediate = intermediate.detach()。

4.4 多任务学习里共享计算图导致泄漏

多任务学习的时候,多个 loss 可能共享同一个 backbone 的输出。如果你不小心把 backbone 的输出存下来了,或者多个 loss 的计算图交叉引用,也会导致泄漏。

# 危险写法 features = backbone(inputs) loss1 = task1_head(features) loss2 = task2_head(features) total_loss = loss1 + loss2 total_loss.backward() # 如果 features 被存到了某个地方,整个计算图都会被保留

修复方法是确保features只在当前 iteration 内使用,不要存到全局变量或类的成员变量里。如果需要在多个 iteration 之间传递信息,用features.detach()。

4.5 DataLoader 的 worker 导致的泄漏

这个问题比较隐蔽:如果你在Dataset的__getitem__里做了 CUDA 操作,或者把 CUDA 张量存到了 dataset 对象里,DataLoader 的多个 worker 进程可能会导致显存泄漏。

class MyDataset(Dataset): def __init__(self): self.cache = [] # 危险:如果 cache 里存了 CUDA 张量 def __getitem__(self, idx): data = load_data(idx) tensor = torch.from_numpy(data).cuda() # 危险:在 worker 里做 CUDA 操作 return tensor

DataLoader 的 worker 是独立的进程,每个 worker 有自己的 CUDA 上下文。在 worker 里做 CUDA 操作不仅效率低,还可能导致显存管理混乱。正确的做法是在__getitem__里只做 CPU 操作,把数据搬到 GPU 的操作放在训练循环里:

class MyDataset(Dataset): def __getitem__(self, idx): data = load_data(idx) tensor = torch.from_numpy(data) # 保持 CPU 张量 return tensor # 训练循环里 for inputs, labels in dataloader: inputs = inputs.cuda() # 在这里搬到 GPU labels = labels.cuda()

4.6 梯度累积时的显存管理

梯度累积是一种常见的增大有效 batch size 的方法,但如果实现不当,也会导致显存泄漏。

# 错误写法 for step, (inputs, labels) in enumerate(dataloader): outputs = model(inputs) loss = criterion(outputs, labels) loss = loss / accumulation_steps loss.backward() # 梯度累积,不立即清零 if (step + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()

这个写法本身没问题,但如果你在累积过程中把loss存下来了,或者做了其他保留计算图的操作,就会泄漏。另外,梯度累积时,每个 micro-batch 的计算图在前向传播后就应该被释放,但如果你用了retain_graph=True,计算图会一直保留。

# 危险:retain_graph=True 会导致计算图不释放 loss.backward(retain_graph=True)

除非你有特殊需求(比如需要多次反向传播),否则不要用retain_graph=True。

5. 排查与修复的完整流程

5.1 从现象到根因的排查路线

当你遇到“训练越跑显存越高”的问题时,可以按照下面的流程来排查:

第一步:确认是泄漏还是碎片化。在训练循环里打印memory_allocated和memory_reserved,观察哪个在涨。如果allocated持续上升,是泄漏;如果只有reserved涨,是碎片化。

第二步:缩小范围。如果确认是泄漏,先检查最明显的地方:有没有把 loss 或中间张量存进列表?验证阶段有没有加no_grad()?自定义 loss 或层里有没有保留成员变量?

第三步:用工具定位。如果肉眼看不出来,用tracemalloc或objgraph追踪对象引用,找到具体是哪一行代码留住了计算图。

第四步:修复并验证。找到泄漏点后,用.item()、.detach()或torch.no_grad()切断计算图,然后重新跑训练,观察显存是否稳定。

第五步:加监控防复发。在训练脚本里加一个显存监控,当显存超过阈值时打印警告或自动保存 checkpoint,避免训练到一半 OOM 导致前功尽弃。

5.2 一个完整的排查示例

假设你有一个训练脚本,跑着跑着就 OOM。按照上面的流程,你先在训练循环里加了显存打印:

for step, (inputs, labels) in enumerate(dataloader): inputs = inputs.cuda() labels = labels.cuda() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() optimizer.zero_grad() losses.append(loss) # 可疑点 if step % 100 == 0: print(f"Step {step}: {torch.cuda.memory_allocated()/1024**2:.1f}MB")

输出显示显存从 8GB 涨到了 14GB,确认是泄漏。然后你用tracemalloc对比 step 100 和 step 500 的内存分配:

import tracemalloc tracemalloc.start(25) # 在 step 100 和 step 500 分别 take_snapshot snapshot1 = tracemalloc.take_snapshot() # ... 跑 400 个 step ... snapshot2 = tracemalloc.take_snapshot() top_stats = snapshot2.compare_to(snapshot1, 'lineno') for stat in top_stats[:5]: print(stat)

输出指向了losses.append(loss)这一行。修复方法就是改成losses.append(loss.item())。重新跑训练,显存稳定在 8GB 左右,问题解决。

5.3 显存监控与自动保护机制

与其等到 OOM 再排查,不如在训练脚本里加一个显存监控,提前发现问题。下面是一个简单的监控类:

class MemoryMonitor: def __init__(self, threshold_mb=1000, check_interval=100): self.threshold_mb = threshold_mb self.check_interval = check_interval self.history = [] def check(self, step): if step % self.check_interval != 0: return allocated = torch.cuda.memory_allocated() / 1024**2 self.history.append(allocated) if len(self.history) >= 3: growth = self.history[-1] - self.history[-3] if growth > self.threshold_mb: print(f"[警告] Step {step}: 显存增长过快," f"最近 {self.check_interval*2} 步增长了 {growth:.1f}MB") print(f"当前显存: {allocated:.1f}MB") # 可以在这里触发保存 checkpoint 或中断训练

把这个监控挂到训练循环里,一旦显存增长超过阈值就报警,你就能在 OOM 之前及时介入。

6. 实操心得与避坑经验

6.1 那些年我踩过的显存泄漏坑

说几个我自己在实际项目里遇到的、比较隐蔽的泄漏场景。

第一个坑:在 TensorBoard 里记录图像时忘了 detach。我用writer.add_image('feature', feature_map)记录中间层的特征图,feature_map是带梯度的,TensorBoard 的 writer 会把它存下来,导致计算图泄漏。修复方法是writer.add_image('feature', feature_map.detach().cpu())。

第二个坑:在早停(early stopping)逻辑里保留了最佳模型的 loss。我写了个best_loss = min(best_loss, loss),loss是带梯度的张量,best_loss一直引用着它,导致计算图泄漏。改成best_loss = min(best_loss, loss.item())就好了。

第三个坑:在自定义的 metric 计算里用了带梯度的张量。比如计算准确率的时候,correct = (predictions == labels).sum(),predictions是带梯度的,correct也是带梯度的,如果把它存进了列表,就会泄漏。应该用predictions.detach()或者torch.no_grad()包起来。

第四个坑:在分布式训练里,梯度没有正确 all_reduce。这个比较特殊,但如果你用了DistributedDataParallel但没正确配置,梯度可能会在多个进程间累积,导致显存泄漏。确保每个进程的梯度是独立的,并且在optimizer.step()之前正确同步。

6.2 显存泄漏排查的速查表

症状可能原因排查方法修复方案
显存随 step 线性增长把带梯度的张量存进了列表检查所有append操作用.item()或.detach()
验证阶段显存暴涨没有用torch.no_grad()检查验证循环加with torch.no_grad():
自定义 loss 后显存泄漏loss 里保留了成员变量检查self.xxx = ...改用局部变量或 detach
多任务学习显存泄漏共享特征被保留检查特征是否被存到全局用features.detach()
DataLoader 导致泄漏worker 里做了 CUDA 操作检查__getitem__只在 CPU 上操作
梯度累积时泄漏retain_graph=True检查backward调用去掉retain_graph
显存碎片化频繁申请释放不同大小显存看memory_reserved设置expandable_segments

6.3 几个实用的显存优化技巧

除了修复泄漏,还有一些常规的显存优化技巧,可以帮你把显存占用降下来:

用混合精度训练。torch.cuda.amp可以把部分计算用 float16 来做,显存占用能降低 30% 到 50%。对于大模型来说,这个提升非常明显。

scaler = torch.cuda.amp.GradScaler() for inputs, labels in dataloader: with torch.cuda.amp.autocast(): outputs = model(inputs) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()

用梯度检查点(gradient checkpointing)。对于特别大的模型,可以用torch.utils.checkpoint来用时间换显存。它不会保存所有中间激活值,而是在反向传播时重新计算,显存占用能降低很多,但训练速度会慢一些。

from torch.utils.checkpoint import checkpoint def forward(self, x): x = checkpoint(self.layer1, x) x = checkpoint(self.layer2, x) return x

及时把不需要的张量搬到 CPU。如果你有一些中间结果需要在多个 step 之间保留,但又不需要在 GPU 上计算,可以搬到 CPU 上,释放显存。

# 搬到 CPU features_cpu = features.detach().cpu() # 需要的时候再搬回 GPU features_gpu = features_cpu.cuda()

设置合理的PYTORCH_CUDA_ALLOC_CONF。对于碎片化问题,可以设置expandable_segments:True让 PyTorch 的显存分配器更灵活地管理显存块。

export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True

6.4 关于显存泄漏的几个常见误解

误解一:显存泄漏一定是代码写错了。不一定。有些第三方库或者 PyTorch 本身的某些操作也可能导致显存不释放,比如某些自定义 CUDA 算子、某些版本的 PyTorch 在特定操作下的 bug。遇到这种情况,升级 PyTorch 版本或者换一种实现方式可能就能解决。

误解二:重启训练就能解决泄漏。重启只是把泄漏的显存释放了,但泄漏的代码还在,跑一段时间之后还是会泄漏。重启只能作为临时手段,不能作为解决方案。

误解三:显存泄漏只在大模型上出现。小模型也会泄漏,只是小模型本身显存占用低,泄漏的速度慢,可能跑几千步才 OOM。但如果你跑的是长周期训练,小模型的泄漏同样致命。

误解四:用了torch.no_grad()就不会泄漏。torch.no_grad()只是不构建计算图,但如果你在no_grad块里把张量存进了列表,这些张量本身还是会占用显存。只是它们不带计算图,占用的显存少一些,但数量多了同样会 OOM。

6.5 一个真实的排查案例

最后分享一个我最近遇到的案例。有个项目训练一个分割模型,用的是类似 Mask2Former 的结构,跑着跑着就 OOM。一开始以为是模型太大,把 batch size 从 8 降到 4,还是 OOM。后来加了显存监控,发现显存从 10GB 涨到了 22GB,确认是泄漏。

用tracemalloc排查,发现泄漏点在数据增强的代码里。数据增强用了一个自定义的 transform,里面把增强后的图像存到了一个全局的 cache 里,用于可视化。这个 cache 是带梯度的(因为图像经过了模型的一部分),导致计算图泄漏。

修复方法很简单:在存 cache 之前先detach(),并且限制 cache 的大小,只保留最近 N 个样本。改完之后,显存稳定在 10GB 左右,训练顺利完成。

这个案例给我的教训是:显存泄漏不一定发生在训练循环里,数据加载、数据增强、日志记录、可视化这些环节都可能成为泄漏源。排查的时候不要只盯着loss.backward()和optimizer.step(),要把整个训练流程都过一遍。

6.6 预防显存泄漏的编码习惯

与其等到泄漏了再排查,不如在写代码的时候就养成好习惯:

  • 所有需要记录的标量,一律用.item()转成 Python 数值。
  • 所有需要保留的张量,一律先.detach()再存。
  • 验证和推理阶段,一律加torch.no_grad()。
  • 自定义 loss 和层里,不要用成员变量存中间张量。
  • DataLoader 的__getitem__里,不要做 CUDA 操作。
  • 训练循环里加显存监控,定期打印显存占用。
  • 用torch.cuda.empty_cache()在适当的时候清理缓存(但不要频繁调用,会影响性能)。

这些习惯看起来琐碎,但能帮你避免 90% 以上的显存泄漏问题。剩下的 10%,靠工具排查和耐心定位,也都能解决。

显存泄漏这个问题,说到底就是“谁留住了计算图”的问题。只要理解了 PyTorch 的计算图机制,知道哪些操作会导致计算图无法释放,再配合工具定位,排查起来并不难。难的是养成好的编码习惯,在写代码的时候就避免这些坑。希望这篇文章能帮你少走一些弯路,让训练跑得更稳。

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

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

立即咨询