location

导航

# 第 0 篇:KV cache 计算

第 0 篇:KV cache 计算

日期:2026-10-14

记录把 verl、vLLM 接到强化学习训推上的过程。这篇不写安装步骤,也不写「我跑通了 GRPO」。训练还没开始。我先弄清一次生成里,显存到底被什么占住。

机器上现成的东西:8 张 A800 80GB,本地 verl 在 /home/wujingxin/verl(v0.8.0),verl_env 里的 vLLM 是 0.11.0。

Prefill 和 Decode

Prefill(预填充阶段) 把整段 prompt(提示词)一起算完,顺手写下每个 token 的 K 和 V。Decode(解码阶段) 每次只算 1 个新 token,用这个 token 的 Q 去看已经写下的全部 K。Prompt 越长,第一步越重。生成越长,后面每一步都要多读一点历史 K、V。

同一层里,\(QK^\top\) 那种分数矩阵只在 prefill 时短暂存在,用完就扔。留下来的是 KV cache。

一层 KV 的公式

一层、一条序列、bf16:

\[\underbrace{2}_{K,V\text{ 两份}} \times \underbrace{n_{\text{kv}}}_{\text{KV 头数量}} \times \underbrace{d_{\text{head}}}_{\text{每个头维度}} \times \underbrace{S}_{\text{序列长度}} \times \underbrace{2}_{\text{bf16,每个元素 2 字节}} \]

用 Qwen2.5-0.5B、序列长度 512 把每个因子代进去:

\[2 \times 2 \times 64 \times 512 \times 2 = 256\text{ KB} \]

  1. 第一个 \(2\):K 矩阵和 V 矩阵,两份。
  2. \(n_{\text{kv}} = 2\):Qwen2.5-0.5B 只有 2 个 KV 头(GQA,远少于 14 个 Query 头)。
  3. \(64\):\(d_{\text{head}}\),也就是 head_dim,每个头的向量维度。
  4. \(512\):序列长度 \(S\),token 数量。
  5. 最后一个 \(2\):bf16,一个数值占 2 个字节。

这五个数乘出来是这一层、这一条序列的 KV。模型有 24 层,所以一条序列大约 \(256\text{ KB} \times 24 = 6\text{ MB}\)。这个数对不齐 nvidia-smi。权重和 CUDA 上下文是几百 MB 到几 GB,6 MB 被淹没了。0.5B 只用来把公式算熟。

换到 Qwen2.5-7B 时,五个因子里只有中间三个变了:KV 头从 2 变成 4,头维度从 64 变成 128,序列长度从 512 变成 2048。两头的 2 不变。一层是

\[2 \times 4 \times 128 \times 2048 \times 2 = 4\text{ MB} \]

28 层约 112 MB。batch 为 64 时,光 KV 大约 7 GB。权重可以多条请求共用,KV 不行,每个请求一份。序列一长、并发一高,显存先被 cache 吃掉。

PagedAttention 不改变注意力的算法,它把 KV 切成固定页再按需领取,避免每条请求按最大长度占一块连续显存。这个说法我还没有在源码里对上。

下一步只打开两个文件,找「谁决定算哪些 token」和「KV 放在哪」:

  • verl_env 里的 vllm/v1/core/sched/scheduler.py
  • vllm/v1/worker/gpu_model_runner.py

posted on 2026-10-14 21:43  location  阅读(3)  评论(0)    收藏  举报