3、显存分配机制:CUDA显存分配策略、PyTorch缓存分配器、TensorFlow显存管理

显存分配,说白了就是GPU怎么给你“划地盘”。

很多同学跑模型时报OOM(显存溢出),第一反应是“显存不够大”。其实不一定。很多时候是分配策略没用好,导致显存碎片化严重,或者缓存没释放。我调过不少案例,把分配策略优化一下,同样的模型能省下20%-30%的显存。

3.1 CUDA显存分配策略

CUDA底层提供了两套分配接口:cudaMalloc 和 cudaMallocManaged。前者是显式分配,后者是统一内存(UM)。

cudaMalloc:直接向GPU要一块连续显存。优点是速度快、延迟低。缺点是必须手动管理生命周期,容易产生碎片。

cudaMallocManaged:让CPU和GPU共享同一块虚拟地址空间。数据会自动在CPU和GPU之间迁移。听起来很美好,对吧?但我个人建议慎用。为什么呢?因为自动迁移有性能开销。我在项目中遇到过,用了UM之后,训练速度直接掉了30%。

避坑指南:我曾经在一个推荐系统项目里,为了省事全用了cudaMallocManaged。结果显存占用忽高忽低,训练速度还慢。后来全部改成cudaMalloc + 手动流管理,速度直接翻倍。

CUDA还提供了显存池(Memory Pool)机制。从CUDA 11.2开始,你可以用cudaMemPool来管理显存。它的核心思想是:预先分配一大块显存,然后按需切分。这样可以减少cudaMalloc的调用次数,降低碎片。

// 创建显存池
cudaMemPool_t pool;
cudaMemPoolCreate(&pool, 0);

// 从池中分配
void* ptr;
cudaMallocAsync(&ptr, size, pool);

// 释放回池
cudaFreeAsync(ptr, stream);

嗯,这里要注意:显存池不是万能的。如果你的模型动态变化很大(比如NLP里的变长序列),池子里的碎片还是会累积。我一般建议每训练1000步,手动重置一下池子。

3.2 PyTorch缓存分配器

PyTorch的显存管理,核心是缓存分配器(Caching Allocator)。它不直接调用cudaMalloc,而是自己维护一个显存缓存池。

为什么这么做?因为cudaMalloc是内核态调用,很慢。PyTorch的做法是:先向GPU要一大块显存(比如2GB),然后切成小块给张量用。张量释放后,显存不还给OS,而是留在缓存池里,给下一个张量复用。

你可以用torch.cuda.memory_summary()查看当前缓存状态。我个人习惯在训练脚本里每100步打印一次,看看缓存利用率。

import torch

# 查看显存分配详情
print(torch.cuda.memory_summary())

# 清空缓存(不释放给OS,但内部整理)
torch.cuda.empty_cache()

# 重置峰值统计
torch.cuda.reset_peak_memory_stats()
关键参数:PYTORCH_CUDA_ALLOC_CONF环境变量。你可以设置:
  • max_split_size_mb:控制缓存块的最大切分大小。默认很大,容易产生碎片。我建议设成128或256。
  • expandable_segments:True:启用可扩展段。从PyTorch 2.0开始支持,能显著减少碎片。

举个例子,我调过一个BERT训练任务,显存占用8.5GB,但实际模型参数只有3GB。剩下的全是缓存碎片。加上expandable_segments:True之后,直接降到5.5GB。

# 启动时设置
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128,expandable_segments:True
python train.py

你想想看,这省下来的3GB,是不是可以再塞一个batch?

3.3 TensorFlow显存管理

TensorFlow的显存管理,和PyTorch思路不太一样。它默认是贪心分配:启动时就把所有显存都占上。你跑一个单卡模型,它直接吃掉全部显存。很多新手第一次看到都吓一跳。

其实这是故意的。TF认为“占满”可以减少后续分配的开销。但如果你要同时跑多个模型,或者和PyTorch共享GPU,那就得改配置。

import tensorflow as tf

# 方式一:按需增长
gpus = tf.config.list_physical_devices('GPU')
for gpu in gpus:
    tf.config.experimental.set_memory_growth(gpu, True)

# 方式二:限制最大显存
tf.config.set_logical_device_configuration(
    gpus[0],
    [tf.config.LogicalDeviceConfiguration(memory_limit=4096)]  # 限制4GB
)

我个人更推荐按需增长模式。它不会一开始就占满,而是随着计算逐步分配。但要注意:这个模式有性能损耗,因为每次分配都要和驱动交互。如果你的模型显存需求很稳定,不如直接设一个固定上限。

TF 2.x还引入了PluggableDevice机制,允许自定义显存分配器。不过说实话,我很少用。大部分场景下,set_memory_growth就够用了。

小技巧:如果你在TF里遇到显存泄漏,试试在训练循环里加一句 tf.keras.backend.clear_session()。我曾经在一个强化学习项目里,每轮迭代都创建新模型,结果显存越涨越高。加上这句之后,问题就解决了。

3.4 三种分配策略对比

特性 CUDA原生 PyTorch缓存分配器 TensorFlow显存管理
分配方式 直接cudaMalloc 缓存池 + 切分 贪心/按需增长
碎片控制 差(手动管理) 中(可配置) 好(预分配)
多进程友好 是 否(单进程) 是
典型场景 高性能计算 动态图训练 静态图/生产部署

说白了,没有银弹。PyTorch适合研究阶段,灵活但碎片多。TF适合生产环境,稳定但不够灵活。CUDA原生适合你写自定义算子的时候用。

嗯,最后说一句:不管用哪个框架,监控显存都是基本功。我习惯用nvidia-smi配合gpustat实时看显存变化。遇到OOM,先看碎片率,再看缓存占用,最后才考虑是不是真的显存不够。

显存分配机制全景图 CUDA显存分配 PyTorch缓存分配器 TensorFlow显存管理 cudaMalloc cudaMallocManaged 显存池(MemPool) 缓存池 expandable_segments max_split_size_mb 贪心分配 按需增长 memory_limit 核心思路:减少碎片 + 提高复用 监控工具:nvidia-smi | gpustat | torch.cuda.memory_summary()
一句话总结:显存分配的核心不是“要多少给多少”,而是“怎么要、怎么还、怎么复用”。理解这三者的差异,你就能在OOM面前多一分从容。

可可精品资料库