GPU显存基础:架构、带宽、容量与分配释放机制

各位同学好,我是你们的老朋友。今天咱们来聊聊GPU显存——这个深度学习炼丹师最熟悉的陌生人。

说实话,我见过太多同学把显存当成一个黑盒子。模型跑不起来就加显存,OOM了就调batch size。但你真的了解显存吗?它为什么这么快?为什么有时候明明显存没满,程序却报错?

嗯,这节课我们就把它彻底搞明白。

一、显存架构:GPU的“高速仓库”

先说说显存长什么样。你可以把GPU想象成一个超级工厂,而显存就是它的原材料仓库。

核心概念:显存(VRAM)是GPU专用的高速内存,用于存储待处理的数据和计算结果。它和CPU的内存(DRAM)是两套独立的系统。

我刚开始做CUDA优化时,犯过一个低级错误——以为显存和内存是共享的。结果数据拷贝来拷贝去,性能惨不忍睹。后来才明白,它们之间需要通过PCIe总线通信,这个带宽瓶颈可坑了不少人。

显存内部的结构,我画了张图帮大家理解:

GPU芯片 (计算核心) 显存控制器 (内存管理单元) 地址映射/调度/ECC 显存颗粒 (GDDR6/HBM2e) 数据存储单元 内存总线 数据通道 GPU显存架构示意图 关键参数: • 显存位宽:决定一次能传输多少数据(如384-bit) • 显存频率:决定数据传输速度(如1750 MHz) • 显存带宽 = 位宽 × 频率 × 8(GDDR6为16倍) • 延迟:从请求到数据到达的时间(约200-400个时钟周期)

你看,GPU芯片和显存之间不是直连的,中间有个显存控制器。这个控制器负责调度所有读写请求。我调优时发现,很多性能问题就出在这个控制器上——请求太多,它忙不过来。

二、显存带宽:性能的命脉

显存带宽,说白了就是显存和GPU核心之间的“高速公路”有多宽。单位是GB/s。

为什么带宽这么重要?你想想看,GPU有几千个核心,每个核心都在疯狂计算。如果数据供应不上,核心就得干等着。这就是所谓的“内存墙”问题。

GPU型号 显存类型 位宽 带宽
NVIDIA A100 HBM2e 5120-bit 2039 GB/s
NVIDIA V100 HBM2 4096-bit 900 GB/s
NVIDIA RTX 3090 GDDR6X 384-bit 936 GB/s
NVIDIA RTX 4090 GDDR6X 384-bit 1008 GB/s

避坑指南:我曾经在优化一个Transformer模型时,发现计算核心利用率只有30%。查了半天,原来是数据布局不合理,导致显存带宽利用率极低。后来改成连续内存访问,带宽利用率直接飙到85%。

这里有个关键点:显存带宽不是固定的。它取决于你的访问模式。连续访问能跑满带宽,随机访问就只能用个零头。我见过有人把带宽从800 GB/s用成80 GB/s,还怪GPU不行。

三、显存容量:能装多少东西

显存容量决定了你能跑多大的模型。这个很好理解,但有几个坑要注意。

首先,显存容量不是全部可用的。系统会预留一部分给驱动、上下文管理、CUDA runtime等。我实测过,一张8GB的卡,实际可用大概7.5GB左右。

其次,模型占用的显存不只是参数本身。还有:

  • 参数权重:模型本身的参数
  • 梯度:反向传播用的梯度信息
  • 优化器状态:Adam的动量、方差等
  • 激活值:中间层的输出(这个最吃显存)
  • 临时缓冲区:各种中间计算结果

经验公式:一个FP32的模型,训练时显存占用 ≈ 参数量的4倍(权重+梯度+优化器状态)+ 激活值(取决于batch size和序列长度)。

举个例子,一个7B参数的模型,光参数就要28GB。加上梯度和优化器状态,轻松破100GB。所以现在大家都在用混合精度训练,就是这个道理。

四、显存分配与释放机制

这部分是很多人的知识盲区。显存怎么分配?怎么释放?为什么有时候显存没满却报OOM?

CUDA的显存管理,其实分两层:

  1. 驱动层:负责物理显存的管理,以页为单位
  2. Runtime层:提供cudaMalloc/cudaFree等API

我习惯把显存分配想象成“借书”的过程。你调用cudaMalloc,相当于去图书馆借书。但图书馆不会真的把书搬给你,而是给你一个索引(指针)。真正分配物理页,是在你第一次访问这块内存时——这叫“惰性分配”。

// 示例:显存分配与释放
float *d_data;
size_t size = 1024 * 1024 * 1024; // 1GB

// 分配显存(此时只是虚拟地址,不占物理页)
cudaMalloc(&d_data, size);

// 第一次访问(触发物理页分配)
cudaMemset(d_data, 0, size);

// 使用显存...
kernel<<<grid, block>>>(d_data);

// 释放显存
cudaFree(d_data);

注意:cudaFree释放后,显存不会立即归还给系统。CUDA会缓存一部分已释放的显存,供后续快速分配。这就是为什么你看到nvidia-smi显示的显存占用可能比实际使用的多。

我曾经遇到过一个诡异的问题:程序跑着跑着突然OOM,但nvidia-smi显示还有空闲显存。查了半天,原来是显存碎片化导致的。频繁分配释放小内存块,会让显存变成“蜂窝煤”,大块连续内存分配不出来。

解决方案?我建议:

  • 尽量一次性分配大块内存,自己管理子分配
  • 使用cudaMallocAsync(CUDA 11.2+)支持流式分配
  • 避免在热点路径频繁分配释放

个人经验:我写深度学习框架时,会预先分配一个显存池。所有张量都在这个池子里分配,用完归还。这样既避免了碎片化,又减少了cudaMalloc的开销。效果立竿见影,训练速度提升了15%。

最后说一个很多人不知道的:显存泄漏。和CPU内存泄漏一样,如果你分配了显存但忘记释放,程序跑久了就会OOM。但更坑的是,CUDA上下文不销毁,显存就不会归还。所以进程退出前,一定要确保所有cudaFree都调用了。

嗯,显存基础就讲到这里。这些东西看似简单,但真正用好却不容易。下次遇到显存问题,别急着加卡,先想想是不是访问模式不对,或者分配策略有问题。

想要更多资料,联系可可