【Bug已解决】How to free GPU memory in PyTorch 解决方案

发布时间:2026/8/28 23:03:13
【Bug已解决】How to free GPU memory in PyTorch 解决方案 【Bug已解决】How to free GPU memory in PyTorch 解决方案问题描述在 PyTorch 训练过程中GPU 内存泄漏和内存不足OOM是最常见的问题之一。很多开发者发现即使删除了不再需要的张量GPU 显存并没有被释放或者在训练循环中GPU 内存持续增长最终导致CUDA out of memory错误。典型的错误信息RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB (GPU 0; 11.00 GiB total capacity; 8.50 GiB already allocated; 1.20 GiB free; 9.00 GiB reserved in total by PyTorch)这个问题的根源在于 PyTorch 的 GPU 内存管理机制。PyTorch 使用一个内存缓存分配器caching allocator来管理 GPU 内存它会保留已释放的内存块以便快速重用而不是立即归还给操作系统。这意味着即使你删除了张量GPU 的nvidia-smi显示的内存使用量可能不会减少。常见的问题场景包括训练循环中 GPU 内存持续增长模型评估/推理阶段内存不释放多模型切换时内存累积张量被意外引用导致无法回收计算图未正确释放导致内存泄漏错误复现以下代码演示了几种常见的 GPU 内存泄漏场景import torch import torch.nn as nn # 场景1计算图未释放 model nn.Linear(1000, 1000).cuda() optimizer torch.optim.SGD(model.parameters(), lr0.01) # 错误没有调用 optimizer.zero_grad() 或 loss.backward() 后未释放 losses [] for i in range(100): x torch.randn(32, 1000).cuda() y model(x) loss y.sum() loss.backward() # 错误保存了 loss 张量计算图也被保存 losses.append(loss) # 没有调用 optimizer.zero_grad()梯度不断累积 optimizer.step() print(fGPU 内存: {torch.cuda.memory_allocated() / 1024**2:.1f} MB) # 内存持续增长 # 场景2张量被意外引用 model nn.Linear(1000, 1000).cuda() cached_outputs [] for i in range(100): x torch.randn(32, 1000).cuda() y model(x) # 错误缓存了所有输出GPU 内存不释放 cached_outputs.append(y) print(f缓存输出数量: {len(cached_outputs)}) print(fGPU 内存: {torch.cuda.memory_allocated() / 1024**2:.1f} MB) # 内存持续增长 # 场景3评估时未使用 torch.no_grad() model nn.Linear(1000, 1000).cuda() # 错误评估时没有使用 no_grad构建了计算图 for i in range(100): x torch.randn(32, 1000).cuda() y model(x) # 构建计算图保存中间激活值 loss y.sum() print(fGPU 内存: {torch.cuda.memory_allocated() / 1024**2:.1f} MB) # 内存持续增长 # 场景4全局变量引用 model nn.Linear(1000, 1000).cuda() global_buffer None for i in range(100): x torch.randn(32, 1000).cuda() y model(x) # 错误全局变量引用了最后一个输出 global_buffer y print(fGPU 内存: {torch.cuda.memory_allocated() / 1024**2:.1f} MB)根因分析1. PyTorch 缓存分配器机制PyTorch 使用缓存分配器来管理 GPU 内存。当张量被删除时其占用的 GPU 内存不会立即归还给 CUDA 驱动而是被标记为可重用并保留在 PyTorch 的内存池中。这样做的目的是避免频繁的 CUDA 内存分配/释放操作提高性能。nvidia-smi 显示的内存 PyTorch 缓存池内存 其他进程内存 torch.cuda.memory_allocated() 实际被张量占用的内存 torch.cuda.memory_reserved() PyTorch 缓存池总内存包含已释放但未归还的2. 计算图和自动微分PyTorch 的自动微分机制会在前向传播时构建计算图。如果requires_gradTrue的张量参与计算中间结果会被保存用于反向传播。如果这些张量没有被正确释放计算图会一直保留在内存中。# 每次前向传播都会构建一个新的计算图 # 如果不调用 backward() 或不使用 detach()计算图会累积 x torch.randn(100, requires_gradTrue) y model(x) # 构建计算图 # y 保存了从 x 到 y 的所有中间结果3. 梯度累积默认情况下PyTorch 会在backward()调用时累积梯度而非覆盖。如果不调用optimizer.zero_grad()梯度会不断累积占用越来越多的内存。4. Python 引用计数Python 使用引用计数进行垃圾回收。如果一个 GPU 张量被 Python 变量引用如存储在列表、字典、全局变量中即使你不再需要它它的 GPU 内存也不会被释放直到所有引用都被删除。5. CUDA 流和异步操作PyTorch 的 CUDA 操作是异步的。当你执行一个 GPU 操作时它被提交到 CUDA 流中但可能没有立即执行。如果你在操作完成前检查内存使用量可能会看到不一致的结果。使用torch.cuda.synchronize()可以确保所有操作完成。解决方案方案一正确使用torch.no_grad()和torch.detach()在推理和评估阶段使用torch.no_grad()避免构建计算图import torch import torch.nn as nn model nn.Linear(1000, 1000).cuda() # 正确评估时使用 no_grad with torch.no_grad(): for i in range(100): x torch.randn(32, 1000).cuda() y model(x) # 不构建计算图不保存中间结果 # y 不需要梯度 print(fGPU 内存 (no_grad): {torch.cuda.memory_allocated() / 1024**2:.1f} MB) # 内存稳定 # 使用 detach() 获取不需要梯度的张量 x torch.randn(32, 1000, requires_gradTrue).cuda() y model(x) y_detached y.detach() # 从计算图中分离不保存中间结果方案二正确管理训练循环中的内存import torch import torch.nn as nn import gc model nn.Linear(1000, 1000).cuda() optimizer torch.optim.SGD(model.parameters(), lr0.01) # 正确的训练循环 for epoch in range(10): for batch_idx, (data, target) in enumerate(train_loader): data data.cuda() target target.cuda() # 1. 清除上一步的梯度 optimizer.zero_grad() # 2. 前向传播 output model(data) loss criterion(output, target) # 3. 反向传播 loss.backward() # 4. 更新参数 optimizer.step() # 5. 释放不需要的张量 del output, loss # 6. 定期清理缓存 if batch_idx % 100 0: # 只清理未使用的缓存不影响正在使用的张量 pass # 一般不需要频繁调用 empty_cache # 每个 epoch 结束后清理 gc.collect() torch.cuda.empty_cache()方案三使用torch.cuda.empty_cache()和gc.collect()import torch import gc def free_gpu_memory(): 彻底释放 GPU 内存 # 1. 触发 Python 垃圾回收 gc.collect() # 2. 清理 PyTorch GPU 缓存 torch.cuda.empty_cache() # 3. 打印内存状态 if torch.cuda.is_available(): allocated torch.cuda.memory_allocated() / 1024**3 reserved torch.cuda.memory_reserved() / 1024**3 print(fGPU 内存 - 已分配: {allocated:.2f} GB, 已缓存: {reserved:.2f} GB) def force_free_all_gpu_memory(): 强制释放所有 GPU 内存极端情况 # 1. 清空所有缓存 gc.collect() torch.cuda.empty_cache() # 2. 重置 CUDA 内存分配器 # 这会释放所有缓存的内存块 torch.cuda.reset_peak_memory_stats() # 3. 同步 CUDA 流 torch.cuda.synchronize() print(所有 GPU 内存已强制释放) # 使用示例 free_gpu_memory()方案四使用上下文管理器管理内存import torch import gc from contextlib import contextmanager contextmanager def gpu_memory_manager(devicecuda, clear_cacheTrue): GPU 内存管理上下文管理器。 在代码块执行前后自动管理 GPU 内存。 # 记录初始内存 initial_allocated torch.cuda.memory_allocated(device) if torch.cuda.is_available() else 0 try: yield finally: # 清理 Python 垃圾 gc.collect() # 清理 GPU 缓存 if clear_cache and torch.cuda.is_available(): torch.cuda.empty_cache() # 报告内存变化 if torch.cuda.is_available(): final_allocated torch.cuda.memory_allocated(device) diff (final_allocated - initial_allocated) / 1024**2 print(fGPU 内存变化: {diff:.1f} MB f(当前: {final_allocated/1024**2:.1f} MB)) contextmanager def inference_mode(devicecuda): 推理模式上下文管理器。 自动使用 no_grad 和内存管理。 with torch.no_grad(): with gpu_memory_manager(device, clear_cacheFalse): yield # 使用示例 with gpu_memory_manager(): model torch.nn.Linear(1000, 1000).cuda() x torch.randn(32, 1000).cuda() y model(x) # 退出上下文后自动清理 ![配图](https://i-blog.csdnimg.cn/img_convert/45059327d12f008392c801cf37ee193a.png) with inference_mode(): # 推理代码自动不构建计算图 output model(x)完整修复代码以下是一个完整的 GPU 内存管理工具和训练示例import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader import gc import time from typing import Optional, Dict, Any from contextlib import contextmanager class GPUMemoryMonitor: GPU 内存监控和管理工具。 提供内存跟踪、泄漏检测和自动清理功能。 def __init__(self, device: Optional[torch.device] None): self.device device or torch.device( cuda if torch.cuda.is_available() else cpu ) self.peak_memory 0 self.memory_history [] self.tracked_tensors set() def get_memory_stats(self) - Dict[str, float]: 获取当前 GPU 内存统计 if not torch.cuda.is_available(): return {allocated_mb: 0, reserved_mb: 0, peak_mb: 0} allocated torch.cuda.memory_allocated(self.device) / 1024**2 reserved torch.cuda.memory_reserved(self.device) / 1024**2 peak torch.cuda.max_memory_allocated(self.device) / 1024**2 return { allocated_mb: allocated, reserved_mb: reserved, peak_mb: peak, free_mb: reserved - allocated, } def log_memory(self, tag: str ): 记录当前内存状态 stats self.get_memory_stats() stats[tag] tag stats[timestamp] time.time() self.memory_history.append(stats) if stats[allocated_mb] self.peak_memory: self.peak_memory stats[allocated_mb] print(f[Memory{f - {tag} if tag else }] fAllocated: {stats[allocated_mb]:.1f} MB | fReserved: {stats[reserved_mb]:.1f} MB | fPeak: {stats[peak_mb]:.1f} MB) def clear_cache(self, aggressive: bool False): 清理 GPU 缓存。 Args: aggressive: 是否进行激进清理包括 gc.collect if aggressive: gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() if aggressive: torch.cuda.synchronize() def reset_peak(self): 重置峰值内存统计 if torch.cuda.is_available(): torch.cuda.reset_peak_memory_stats() self.peak_memory 0 def detect_leak(self, threshold_mb: float 100) - bool: 检测内存泄漏。 通过比较历史记录判断是否有内存持续增长。 Args: threshold_mb: 判定泄漏的内存增长阈值MB Returns: 是否检测到内存泄漏 if len(self.memory_history) 2: return False recent self.memory_history[-5:] if len(recent) 2: return False first recent[0][allocated_mb] last recent[-1][allocated_mb] if last - first threshold_mb: print(f[LEAK WARNING] 内存增长 {last - first:.1f} MB f(从 {first:.1f} 到 {last:.1f} MB)) return True return False def print_summary(self): 打印内存使用摘要 print(\n * 60) print(GPU 内存使用摘要) print( * 60) if not self.memory_history: print(无内存记录) return allocated_values [h[allocated_mb] for h in self.memory_history] reserved_values [h[reserved_mb] for h in self.memory_history] print(f记录次数: {len(self.memory_history)}) print(f已分配内存 - 最小: {min(allocated_values):.1f} MB, f最大: {max(allocated_values):.1f} MB, f平均: {sum(allocated_values)/len(allocated_values):.1f} MB) print(f缓存内存 - 最小: {min(reserved_values):.1f} MB, f最大: {max(reserved_values):.1f} MB) print(f峰值内存: {self.peak_memory:.1f} MB) if len(self.memory_history) 1: print(f\n内存历史:) for h in self.memory_history: print(f {h.get(tag, N/A)}: {h[allocated_mb]:.1f} MB f(reserved: {h[reserved_mb]:.1f} MB)) class MemoryEfficientTrainer: 内存高效的训练器。 自动管理 GPU 内存防止内存泄漏。 def __init__(self, model, optimizer, criterion, devicecuda): self.model model self.optimizer optimizer self.criterion criterion self.device torch.device(device) self.monitor GPUMemoryMonitor(self.device) # 将模型移到设备 self.model self.model.to(self.device) def train_epoch(self, dataloader, epoch, gradient_accumulation_steps1): 训练一个 epoch self.model.train() total_loss 0 total_correct 0 total_samples 0 self.optimizer.zero_grad() for batch_idx, (data, target) in enumerate(dataloader): data data.to(self.device, non_blockingTrue) target target.to(self.device, non_blockingTrue) # 前向传播 output self.model(data) loss self.criterion(output, target) # 梯度累积 loss loss / gradient_accumulation_steps loss.backward() # 每 accumulation_steps 步更新一次 if (batch_idx 1) % gradient_accumulation_steps 0: self.optimizer.step() self.optimizer.zero_grad() # 统计 total_loss loss.item() * gradient_accumulation_steps pred output.argmax(dim1) total_correct pred.eq(target).sum().item() total_samples target.size(0) # 释放中间变量 del output, loss, data, target # 定期监控 if batch_idx % 200 0: self.monitor.log_memory(fEpoch {epoch} Batch {batch_idx}) # 检测泄漏 if self.monitor.detect_leak(threshold_mb500): print([WARNING] 检测到可能的内存泄漏) self.monitor.clear_cache(aggressiveTrue) avg_loss total_loss / len(dataloader) accuracy 100. * total_correct / total_samples return {loss: avg_loss, accuracy: accuracy} torch.no_grad() def validate(self, dataloader): 验证模型使用 no_grad 避免内存泄漏 self.model.eval() total_loss 0 total_correct 0 total_samples 0 for data, target in dataloader: data data.to(self.device, non_blockingTrue) target target.to(self.device, non_blockingTrue) output self.model(data) loss self.criterion(output, target) total_loss loss.item() pred output.argmax(dim1) total_correct pred.eq(target).sum().item() total_samples target.size(0) del output, loss, data, target avg_loss total_loss / len(dataloader) accuracy 100. * total_correct / total_samples return {loss: avg_loss, accuracy: accuracy} def train(self, train_loader, val_loader, epochs10): 完整训练流程 print(f{*60}) print(f开始训练 | 设备: {self.device} | 轮数: {epochs}) print(f{*60}) self.monitor.reset_peak() self.monitor.log_memory(训练开始) best_val_acc 0 for epoch in range(1, epochs 1): # 训练 train_metrics self.train_epoch(train_loader, epoch) # 验证 val_metrics self.validate(val_loader) print(fEpoch {epoch}/{epochs} | fTrain Loss: {train_metrics[loss]:.4f}, fAcc: {train_metrics[accuracy]:.2f}% | fVal Loss: {val_metrics[loss]:.4f}, fAcc: {val_metrics[accuracy]:.2f}%) # 保存最佳模型 if val_metrics[accuracy] best_val_acc: best_val_acc val_metrics[accuracy] # 保存到 CPU 避免占用 GPU 内存 torch.save( {k: v.cpu() for k, v in self.model.state_dict().items()}, best_model.pth ) # 每个 epoch 清理 self.monitor.clear_cache(aggressiveTrue) self.monitor.log_memory(fEpoch {epoch} 结束) self.monitor.print_summary() return self.model # 辅助工具函数 def print_gpu_memory(): 打印当前 GPU 内存状态 if not torch.cuda.is_available(): print(CUDA 不可用) return allocated torch.cuda.memory_allocated() / 1024**2 reserved torch.cuda.memory_reserved() / 1024**2 max_allocated torch.cuda.max_memory_allocated() / 1024**2 print(fGPU 内存状态:) print(f 已分配: {allocated:.1f} MB) print(f 已缓存: {reserved:.1f} MB) print(f 峰值分配: {max_allocated:.1f} MB) print(f 可用缓存: {reserved - allocated:.1f} MB) def find_tensor_references(obj, nameobj, depth0, max_depth5): 查找对象中引用的 GPU 张量。 用于调试内存泄漏。 if depth max_depth: return if isinstance(obj, torch.Tensor): if obj.is_cuda: print(f{ * depth}{name}: GPU 张量 fshape{obj.shape}, dtype{obj.dtype}, fsize{obj.element_size() * obj.nelement() / 1024**2:.1f} MB) elif isinstance(obj, dict): for k, v in obj.items(): find_tensor_references(v, f{name}[{k!r}], depth 1, max_depth) elif isinstance(obj, (list, tuple)): for i, v in enumerate(obj): find_tensor_references(v, f{name}[{i}], depth 1, max_depth) elif hasattr(obj, __dict__): for k, v in obj.__dict__.items(): find_tensor_references(v, f{name}.{k}, depth 1, max_depth) # 示例模型和数据 class SimpleModel(nn.Module): def __init__(self, input_dim784, hidden_dim256, num_classes10): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, num_classes), ) def forward(self, x): return self.net(x) class SyntheticDataset(Dataset): def __init__(self, n1000, dim784, classes10): self.x torch.randn(n, dim) self.y torch.randint(0, classes, (n,)) def __len__(self): return len(self.x) def __getitem__(self, i): return self.x[i], self.y[i] if __name__ __main__: # 创建数据 train_ds SyntheticDataset(2000) val_ds SyntheticDataset(400) train_loader DataLoader(train_ds, batch_size64, shuffleTrue) val_loader DataLoader(val_ds, batch_size64) # 创建模型 model SimpleModel() optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() # 训练 trainer MemoryEfficientTrainer(model, optimizer, criterion, devicecuda) trained_model trainer.train(train_loader, val_loader, epochs5) # 最终内存状态 print(\n最终 GPU 内存状态:) print_gpu_memory() # 清理 gc.collect() torch.cuda.empty_cache() print(\n清理后:) print_gpu_memory()常见陷阱与注意事项1.empty_cache()不释放正在使用的内存torch.cuda.empty_cache()只释放 PyTorch 缓存池中未被任何张量引用的内存块。如果有张量仍在使用这些内存不会被释放。必须先删除张量引用del tensor再调用empty_cache()。2.del不立即释放内存Python 的del只是减少引用计数。如果有其他引用存在如列表、字典、闭包张量不会被回收。使用gc.collect()可以强制回收循环引用。3.loss.item()会断开计算图调用loss.item()会将 loss 从计算图中分离并转为 Python 标量。在训练循环中保存loss张量而非loss.item()会导致计算图无法释放# 错误保存 loss 张量 losses.append(loss) # 保存了整个计算图 # 正确保存标量值 losses.append(loss.item()) # 只保存数值4. 梯度累积的内存影响loss.backward()默认累积梯度。如果不调用optimizer.zero_grad()梯度张量会一直保留在内存中。在梯度累积训练中这是有意为之但需要确保在正确的时机清零。5.pin_memory的内存开销DataLoader 的pin_memoryTrue会将数据预分配为页锁定内存加速 CPU 到 GPU 的传输。但这会增加 CPU 内存使用。如果 CPU 内存紧张可以关闭此选项。6. 多 GPU 的内存管理使用nn.DataParallel或nn.DistributedDataParallel时每个 GPU 都会保存模型副本和梯度。确保使用DistributedDataParallel更高效而非DataParallel有内存开销。7. 模型保存时的内存问题保存模型时state_dict中的张量仍在 GPU 上。保存到 CPU 可以避免# 推荐方式 torch.save({k: v.cpu() for k, v in model.state_dict().items()}, model.pth)总结GPU 内存管理是 PyTorch 训练中的关键技能。核心要点如下理解缓存分配器torch.cuda.empty_cache()释放缓存池不释放正在使用的内存memory_allocated()显示实际使用量memory_reserved()显示缓存池大小。使用torch.no_grad()推理和评估阶段必须使用避免构建计算图。正确管理训练循环optimizer.zero_grad()清除梯度del删除不需要的张量loss.item()保存标量值。定期清理gc.collect()torch.cuda.empty_cache()组合使用但不要过度频繁调用。监控内存使用GPUMemoryMonitor类跟踪内存使用检测泄漏。避免意外引用不要在列表、字典、全局变量中保存 GPU 张量。梯度累积使用梯度累积模拟大 batch size减少内存使用。通过系统性地管理 GPU 内存可以避免 OOM 错误提高训练效率并在有限的 GPU 资源下训练更大的模型。

相关新闻