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 去哪了?

我画了一张图,帮你理清显存分配的逻辑:

显存分配全景图 GPU 显存(VRAM) 模型参数 权重、偏置 大小:参数量 × 精度 梯度 反向传播计算 大小:同参数大小 优化器状态 m、v 等动量 大小:参数 × 2~3 中间激活值(Activation) 前向传播的中间结果 大小:batch_size × seq_len × hidden_dim 临时缓冲区 通信缓冲区、CUDA 内部开销 大小:取决于框架和配置 训练时:参数 + 梯度 + 优化器状态 + 激活值 + 临时缓冲区 推理时:参数 + 激活值(可复用)+ 临时缓冲区

从图里你能看到,显存里住着好几拨「住户」:

  1. 模型参数:这是最明显的,权重和偏置。推理和训练都需要。
  2. 梯度:只有训练时才有。大小和参数一样。
  3. 优化器状态:Adam 需要存一阶动量和二阶动量,大小是参数量的 2 倍(FP32)。
  4. 中间激活值:前向传播时,每一层的输出都要存下来,供反向传播用。这是显存消耗的大头,尤其是长序列、大 batch 时。
  5. 临时缓冲区:框架内部的开销,比如 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 的张量?到时候你就知道了。


可可精品资料库