2、显存基础概念:显存与内存的区别、显存带宽与容量、显存分配机制
好,咱们正式开始聊显存。说实话,很多同学跑模型遇到 OOM(Out Of Memory),第一反应就是「显存不够了,换卡」。但你真的了解显存吗?它和电脑里的内存到底有啥区别?为什么有时候显存看着还有剩,程序却报错了?
这一节,我就带你把这些基础概念彻底捋清楚。搞懂了这些,后面排查问题才能有的放矢。
2.1 显存 vs 内存:两个世界的故事
先问个问题:你的电脑有 64GB 内存,显卡有 24GB 显存。为什么模型参数不能直接放在内存里,让显卡去读?
答案是——显卡不认识内存。或者说,GPU 只能访问它自己的显存。
我打个比方:内存是仓库,显存是工作台。CPU 负责从仓库(内存)搬货,GPU 只在工作台(显存)上干活。你要让 GPU 处理数据,得先把数据从仓库搬到工作台上。这个搬运过程,就是 PCIe 总线传输。
具体区别,我整理了一张表:
| 对比维度 | 内存(DRAM) | 显存(VRAM) |
|---|---|---|
| 物理位置 | 主板上,CPU 直连 | 显卡 PCB 上,GPU 直连 |
| 访问主体 | CPU | GPU(CPU 也能通过驱动间接访问) |
| 带宽 | DDR5 约 50-80 GB/s | HBM2e 约 1-2 TB/s,GDDR6X 约 700-900 GB/s |
| 延迟 | 约 100ns | 约 200-400ns(更高) |
| 容量 | 常见 16-128 GB | 常见 8-80 GB(H100 可达 80GB) |
| ECC 纠错 | 消费级通常无 | 专业卡(A100/H100)强制 ECC |
看到带宽那行没?显存带宽是内存的 10 倍以上。这就是为什么 GPU 能并行处理海量数据——它读取数据的速度极快。但代价是容量小、延迟高。
核心结论:显存是 GPU 的「专属高速缓存区」。模型参数、中间激活值、优化器状态,全得塞进显存里。塞不下的,要么换大卡,要么做分布式。
2.2 显存带宽与容量:两个关键指标
显存有两个核心指标:容量和带宽。很多人只盯着容量看,忽略了带宽。我吃过这个亏。
2.2.1 容量:你能装下多大的模型?
容量决定了你能跑多大的模型。一个 7B 参数的模型,用 FP16 精度,光参数就要 14GB。加上梯度、优化器状态(Adam 需要额外 2 倍)、中间激活值…… 实际占用往往是参数量的 3-4 倍。
举个例子:
- 模型参数(FP16):7B × 2 bytes = 14 GB
- 梯度(FP16):7B × 2 bytes = 14 GB
- 优化器状态(FP32):7B × 4 bytes × 2(m + v)= 56 GB
- 中间激活值:取决于 batch size 和序列长度,通常 2-8 GB
你看,一个 7B 模型,用 Adam 优化器,显存需求轻松超过 80GB。这就是为什么很多人用 24GB 的 4090 跑 7B 模型会 OOM——不是卡不行,是显存真的不够。
我的习惯:估算显存时,我会先算参数占用量,然后乘以 3(纯推理)或乘以 5(训练)。这样留出余量,避免踩坑。
2.2.2 带宽:你跑得多快?
带宽决定了 GPU 的计算单元「饿不饿」。你想想看,GPU 有几千个核心,每个核心都在疯狂算。如果数据喂不上去,核心就在空转——这叫「计算受限」。
带宽的计算公式很简单:
显存带宽 = 显存频率 × 位宽 × 2(DDR 双倍速率)
举个例子:RTX 4090 的显存频率是 21 Gbps,位宽 384-bit,带宽就是:
21 × 384 / 8 × 2 = 2016 GB/s ≈ 2 TB/s
嗯,2 TB/s,听着很吓人。但你要知道,H100 的 HBM2e 显存带宽是 3.35 TB/s。这就是为什么 H100 训练大模型比 4090 快那么多——不光算力强,带宽也翻倍了。
避坑指南:我曾经在项目里用 RTX 3090 跑一个 NLP 模型,batch size 设得特别大。结果发现 GPU 利用率只有 60%,显存带宽成了瓶颈。后来把 batch size 降下来,利用率反而上去了。所以别一味追求大 batch,带宽不够时,大 batch 反而拖慢速度。
2.3 显存分配机制:到底谁在吃显存?
搞清楚了容量和带宽,咱们来看看显存到底是怎么分配的。很多同学跑模型时用 nvidia-smi 一看,显存用了 20GB,但模型参数明明只有 10GB——剩下的 10GB 去哪了?
我画了一张图,帮你理清显存分配的逻辑:
从图里你能看到,显存里住着好几拨「住户」:
- 模型参数:这是最明显的,权重和偏置。推理和训练都需要。
- 梯度:只有训练时才有。大小和参数一样。
- 优化器状态:Adam 需要存一阶动量和二阶动量,大小是参数量的 2 倍(FP32)。
- 中间激活值:前向传播时,每一层的输出都要存下来,供反向传播用。这是显存消耗的大头,尤其是长序列、大 batch 时。
- 临时缓冲区:框架内部的开销,比如 NCCL 通信缓冲区、CUDA 上下文等。
关键认知:很多人以为显存只存模型参数,其实中间激活值才是「隐形杀手」。我见过一个项目,模型参数只占 8GB,但激活值占了 32GB——batch size 设太大了。
2.4 显存分配的实际过程
咱们再深入一点,看看 PyTorch 是怎么分配显存的。你可能会好奇:为什么 nvidia-smi 显示的显存占用,和 PyTorch 报告的不一样?
原因在于:PyTorch 有自己的缓存分配器。它不会用完就立刻释放显存,而是留着给下次用。这样可以避免频繁向驱动申请/释放显存,提升性能。
举个例子:
import torch
# 分配 1GB 显存
x = torch.randn(256, 1024, 1024, device='cuda')
print(torch.cuda.memory_allocated()) # 约 1GB
# 删除张量
del x
print(torch.cuda.memory_allocated()) # 还是约 1GB!因为缓存没释放
print(torch.cuda.memory_reserved()) # 约 1GB,被缓存了
# 强制清空缓存
torch.cuda.empty_cache()
print(torch.cuda.memory_allocated()) # 0
print(torch.cuda.memory_reserved()) # 0
看到没?del x 之后,显存并没有真正释放。PyTorch 把它缓存起来了。只有调用 empty_cache() 才会还给驱动。
避坑指南:我曾经在调试一个模型时,发现显存占用越来越高,以为是内存泄漏。查了半天,发现是 PyTorch 的缓存机制在「作祟」。如果你在训练循环里频繁创建和销毁张量,缓存会越积越多。建议在关键节点调用 torch.cuda.empty_cache(),或者用 torch.cuda.memory_summary() 查看详细分配情况。
2.5 如何监控显存?
最后,给你几个实用的监控命令。我平时排查问题,基本就靠这几招:
# 1. 实时监控显存使用
watch -n 1 nvidia-smi
# 2. 查看 PyTorch 显存分配详情
import torch
print(torch.cuda.memory_summary())
# 3. 查看当前进程的显存占用
nvidia-smi --query-compute-apps=pid,used_memory --format=csv
# 4. 设置环境变量,打印每次显存分配
export CUDA_LAUNCH_BLOCKING=1
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128
嗯,这些命令够你应付大部分场景了。记住一点:别只看 nvidia-smi 的总占用,要看 PyTorch 内部的分配详情。很多时候,显存「看起来」还有剩,但 PyTorch 的缓存碎片化导致无法分配连续内存——这才是 OOM 的真正原因。
我的习惯:每次跑新模型前,我会先用一个小 batch 跑一次,看 memory_summary() 的输出。这样能提前知道显存分配模式,避免正式训练时突然 OOM。
好了,显存的基础概念就聊到这儿。你理解了显存和内存的区别,知道了带宽和容量哪个更重要,也搞清楚了显存里到底住了哪些「住户」。下一节,咱们会深入聊一个更头疼的问题——显存碎片化。为什么明明有 20GB 显存,却分配不出一个 1GB 的张量?到时候你就知道了。