第2章:PyTorch显存管理机制

显存管理,说白了就是PyTorch怎么跟GPU打交道。我刚开始做模型训练时,经常遇到OOM(显存溢出),那时候我以为是模型太大,后来才发现——很多时候是我不懂PyTorch的显存管理机制。

这一章,我们就来扒一扒PyTorch的显存管理内幕。你搞懂了这些,显存优化就成功了一半。

2.1 Tensor存储原理

Tensor在GPU上是怎么存的?这个问题我问过不少新人,答案五花八门。其实核心就两点:存储(Storage)和视图(View)。

2.1.1 存储与视图分离

每个Tensor背后都有一个Storage对象。Storage是真正的数据块,而Tensor只是对这个数据块的“视图”。

import torch

# 创建一个Tensor
x = torch.tensor([[1, 2, 3], [4, 5, 6]], device='cuda')
print(x.storage())  # 查看底层存储
# 输出: 1 2 3 4 5 6

# 取一个切片
y = x[0, :]  # 视图,不复制数据
print(y.storage() is x.storage())  # True,共享存储

我个人习惯用.storage()来检查是否真的复制了数据。很多OOM问题,就是因为不小心触发了数据复制。

核心要点:视图操作(切片、转置、expand等)不分配新显存。只有clone()、contiguous()等操作才会复制数据。

2.1.2 显存分配粒度

PyTorch分配显存不是按字节来的。它有一个最小分配单元,通常是512字节或2MB。你想想看,如果你只存一个float32(4字节),PyTorch也会给你分配512字节。

数据类型 元素大小 最小分配单元
float32 4字节 512字节
float16 2字节 512字节
int8 1字节 512字节

我在项目中遇到过一个问题:训练一个很小的模型,显存却占了1GB。排查了半天,发现是频繁创建小Tensor导致的碎片化。

2.2 CUDA缓存分配器

PyTorch的显存管理,底层用的是CUDA缓存分配器(CUDA Caching Allocator)。这个机制很有意思——它不直接向GPU申请显存,而是搞了个“缓存池”。

2.2.1 工作原理

说白了就是:PyTorch先向GPU申请一大块显存(比如2GB),然后自己管理这块显存。当你需要显存时,它从缓存池里切一块给你。当你释放Tensor时,它不还给GPU,而是放回缓存池。

import torch

# 查看缓存分配器状态
print(torch.cuda.memory_stats())

# 清空缓存
torch.cuda.empty_cache()

# 查看当前已分配显存
print(torch.cuda.memory_allocated())
# 查看缓存中保留的显存
print(torch.cuda.memory_reserved())

个人经验:我建议在训练循环开始前调用一次torch.cuda.empty_cache(),可以避免上一轮训练留下的缓存碎片。

2.2.2 缓存策略

缓存分配器有几个关键策略:

  • 按需分配:需要多少显存,就从缓存池里切多少
  • 延迟回收:释放的显存不立即还给GPU
  • 碎片整理:当缓存池碎片太多时,会尝试合并相邻的空闲块

嗯,这里要注意:缓存分配器不是万能的。如果你频繁创建和销毁不同大小的Tensor,缓存池会变得支离破碎。

2.3 显存回收策略

显存回收,就是PyTorch怎么把不用的显存“还回来”。这里有两个机制:引用计数和垃圾回收。

2.3.1 引用计数

每个Tensor都有一个引用计数。当引用计数归零时,PyTorch会立即回收这块显存(放回缓存池)。

import torch

def create_tensor():
    x = torch.randn(1000, 1000, device='cuda')
    return x

# 函数返回后,x的引用计数归零
# 显存被回收(放回缓存池)
y = create_tensor()
print(torch.cuda.memory_allocated())  # 显存已被回收

2.3.2 垃圾回收

Python的垃圾回收器(GC)也会参与显存管理。当循环引用发生时,GC会帮忙清理。

我曾经踩过的坑:在训练循环中,如果某个Tensor被列表或字典引用着,即使你不再需要它,显存也不会被回收。我曾经因为这个原因,一个8GB的模型跑着跑着就OOM了。

2.4 torch.cuda最佳实践

说了这么多理论,来点实战的。以下是我总结的几条最佳实践:

2.4.1 监控显存

我习惯在训练脚本里加上显存监控:

import torch
import gc

def print_memory_usage():
    allocated = torch.cuda.memory_allocated() / 1024**3
    reserved = torch.cuda.memory_reserved() / 1024**3
    print(f"已分配: {allocated:.2f} GB, 已预留: {reserved:.2f} GB")

# 在每个epoch结束时调用
for epoch in range(num_epochs):
    train_one_epoch()
    print_memory_usage()
    gc.collect()  # 手动触发垃圾回收
    torch.cuda.empty_cache()  # 清空缓存

2.4.2 避免显存碎片

显存碎片是OOM的隐形杀手。我建议:

  • 预分配大Tensor:一次性分配好,不要频繁创建销毁
  • 使用固定大小:batch size、序列长度尽量固定
  • 复用Tensor:用zero_()或fill_()重置,而不是重新创建
# 不好的做法
for i in range(1000):
    x = torch.randn(1024, 1024, device='cuda')
    # 使用x...
    del x  # 频繁创建销毁

# 好的做法
x = torch.randn(1024, 1024, device='cuda')
for i in range(1000):
    x.normal_()  # 复用Tensor,只修改数据
    # 使用x...

2.4.3 显存分析工具

PyTorch自带了一些好用的工具:

工具 用途
torch.cuda.memory_summary() 打印详细的显存使用报告
torch.cuda.memory_snapshot() 获取显存快照,用于分析
torch.cuda.set_per_process_memory_fraction() 限制进程可用显存比例

我的习惯:在调试阶段,我会在关键位置插入torch.cuda.memory_summary(),看看显存到底被谁吃了。这个习惯帮我解决过不少OOM问题。

2.5 知识体系总览

下面这张图,是我自己整理的PyTorch显存管理知识体系。你把它记在脑子里,以后遇到显存问题就知道从哪下手了。

PyTorch显存管理机制 Tensor存储原理 • 存储(Storage)与视图(View)分离 • 视图操作不分配新显存 • 最小分配单元:512B / 2MB • 数据类型决定元素大小 • 共享存储避免显存浪费 CUDA缓存分配器 • 缓存池机制:预分配大块显存 • 按需分配:从缓存池切分 • 延迟回收:不立即还给GPU • 碎片整理:合并相邻空闲块 • memory_allocated vs memory_reserved 显存回收策略 • 引用计数:归零即回收 • Python GC:处理循环引用 • empty_cache():清空缓存池 • gc.collect():手动触发GC • 避免引用导致显存泄漏 torch.cuda最佳实践 • 监控显存:memory_summary() • 避免碎片:预分配、复用 • 固定batch size和序列长度 • 使用zero_()/fill_()复用 • 限制进程显存比例 理解机制 → 定位问题 → 优化显存

这张图把四个核心模块串起来了。你从Tensor存储原理入手,理解数据怎么存;再看缓存分配器,知道显存怎么管;然后学回收策略,明白显存怎么还;最后用最佳实践,把知识落地。

嗯,这一章的内容就到这里。记住:显存管理不是玄学,是有章可循的。你掌握了这些机制,以后遇到OOM就不会慌了。

可可精品资料库