2. 推理任务显存画像

要给一次推理任务做显存画像,核心不是背几个公式,而是回答一个问题:这块 GPU 上,显存到底被谁拿走了,各自拿走多少,谁随请求变化,谁在启动时就固定下来。只有把占用拆成可解释的几块,才能判断“还能不能再加一个并发”“为什么 batch 从 8 加到 16 就 OOM”“为什么空闲时显存也降不下来”。

从工程视角看,一次推理的显存占用可以拆成五类:模型权重、KV Cache、激活值、临时缓冲区、CUDA Graph 及其捕获资源。前两类是主体,后三类是容易被忽略、却经常在边界条件下压垮显存的“隐性开销”。

单次推理任务显存占用结构(示例:7B 模型 / FP16 / 单卡) GPU 显存 24 GB(示例) 模型权重 ≈14 GB 静态·常驻 KV Cache ≈6 GB 随并发/长度 动态增长 激活值 ≈1.5 GB 临时缓冲区 CUDA Graph 模型权重 参数量 × 每参数字节数 7B × 2B = 14 GB(FP16) 量化 INT8 → 7 GB,INT4 → 3.5 GB KV Cache 2 × 层数 × 头数 × 头维 × 长度 × 并发 × 字节 示例:32层×32头×128维×2048长×8并发×2B ≈ 6 GB,是并发扩展的主要瓶颈 激活值 前向中间张量,与 batch、序列长度相关 prefill 阶段峰值高,decode 阶段较低 临时缓冲区 通信 all-reduce、采样、排序、workspace CUDA Graph 捕获固定地址,额外占用输入/输出/中间池 显存占用关系速查 静态占用(启动即固定) 权重 + CUDA Graph 捕获池 + 框架预留 动态占用(随请求变化) KV Cache + 激活值 + 临时缓冲区 峰值出现时机 prefill 长序列 + 高并发同时到达 OOM 常见原因 KV Cache 预留不足 / 碎片 / Graph 池过大 优化优先级 量化权重 → 限制并发 → 分页 KV → 调 Graph 观测手段 nvidia-smi / torch.cuda.memory_summary

一、模型权重:最“老实”的一块,但量化会改变全局

模型权重是显存画像里最容易算的一块:参数量 × 每个参数的字节数。一个 7B 参数的模型,FP16 下每个参数 2 字节,权重约 14 GB;如果换成 INT8,约 7 GB;INT4 则约 3.5 GB。这部分在模型加载完成后基本固定,不随请求数变化,属于“静态占用”。

但权重的意义不只是它自己占多少。它决定了剩余显存的上限。24 GB 卡上放 14 GB 权重,只剩约 10 GB 给 KV Cache、激活值和缓冲区;如果权重压到 7 GB,就多出 7 GB 可分配给并发。所以量化往往不是“省一点”,而是直接决定并发能力翻倍与否。需要注意的是,量化后部分算子可能引入额外的反量化临时张量,实际节省会略小于理论值。

二、KV Cache:并发扩展的真正瓶颈

KV Cache 是自回归解码的核心缓存。每生成一个 token,都要把该层该头的 Key 和 Value 存下来,供后续 token 做注意力计算。它的容量公式可以写成:

KV Cache 字节数 =
  2(K 和 V)
  × 层数 L
  × KV 头数 H_kv
  × 头维度 d
  × 序列长度 S
  × 并发请求数 B
  × 每元素字节数(FP16 为 2)

以一个示例配置说明:32 层、32 个 KV 头、头维 128、序列长度 2048、并发 8、FP16。代入得 2×32×32×128×2048×8×2 ≈ 6.0 GB。可以看到,KV Cache 与并发数、序列长度都成正比。并发从 8 提到 16,这一项就翻倍到约 12 GB,很容易直接 OOM。这也是为什么长上下文场景下,分页 KV Cache(如 PagedAttention)和 KV 量化会成为关键手段——它们改变的是同一公式里的“每元素字节数”和“碎片浪费”。

三、激活值与临时缓冲区:峰值往往藏在这里

激活值是前向传播过程中产生的中间张量。在 prefill 阶段,整段 prompt 一次性过网络,激活值峰值与 batch、序列长度强相关;在 decode 阶段,每次只处理一个 token,激活值相对小。因此显存峰值通常出现在 prefill 长序列叠加高并发时,而不是 decode 稳态。

临时缓冲区则包括通信 all-reduce 的 workspace、采样与排序的临时空间、cuBLAS/cuDNN 的 workspace 等。它们单块不大,但种类多、生命周期短,容易造成碎片。工程上常见的现象是:nvidia-smi 显示已用 22 GB,但真正活跃的张量只有 18 GB,差额就是碎片和缓存池。

四、CUDA Graph:用显存换启动开销

CUDA Graph 把一串 kernel 启动捕获成图,回放时几乎无 CPU 启动开销,对 decode 阶段的小 kernel 密集场景收益明显。但它的代价是:捕获时使用的输入、输出和中间张量地址会被固定下来,必须常驻显存。如果为多个 batch size 分别捕获图,每个图都占一份池子,累积起来可能达到 GB 级。因此启用 CUDA Graph 后,必须重新做一次显存画像,不能沿用未捕获时的并发上限。

做显存画像时,建议按“权重 → KV Cache → 激活值 → 临时缓冲 → CUDA Graph”的顺序逐项估算,再用 torch.cuda.memory_summary() 对照实际分配,差额就是碎片与框架预留。

五、把画像落到可执行的判断上

假设 24 GB 卡、7B FP16 权重 14 GB、框架预留约 1 GB、CUDA Graph 池约 1 GB,则可用于 KV Cache 与激活值的约 8 GB。若单请求 2048 长度占 0.75 GB KV,则理论上限约 10 并发;但 prefill 峰值还要占激活值,实际安全并发应留 20% 余量,取 8 左右。这个“先算静态、再算动态、最后留余量”的过程,就是显存画像的落地方式。

占用项典型量级(示例)是否随请求变化主要优化手段
模型权重14 GB(7B FP16)否INT8/INT4 量化、张量并行
KV Cache6 GB(8 并发×2048)是,正比于并发×长度分页 KV、KV 量化、限制并发
激活值1~2 GB(prefill 峰值)是,prefill 时高chunked prefill、限制 batch
临时缓冲区0.5~1 GB是,短时波动复用 workspace、减少通信
CUDA Graph0.5~2 GB否(捕获后固定)减少捕获的 batch size 种类

把这张表当作起点,每次调整模型精度、并发上限或 Graph 策略后重新估算一遍,就能在 OOM 发生之前知道是哪一项越了界,而不是等到服务报错再回头猜。