这篇学完你能回答什么
- 直接调 API 和自建开源模型推理,这笔账到底怎么算?什么时候自建才划算?
- vLLM 为什么快?PagedAttention 和 continuous batching 各自解决什么问题?到了 2026 年,这个答案还成立吗?
- KV Cache 是什么?为什么上下文一长,显存就爆、吞吐就塌,而 GPU 算力却一直闲着?
从一个真实故障讲起
一个企业文档问答服务,单张 H100 跑 Llama-3.1-8B,用 vLLM 起服务。压测阶段用的是内部准备的样例,平均 8K 上下文,能稳定跑 50 多路并发,P95 首字延迟 400 毫秒出头。团队据此算出"一张卡够撑到 DAU 三万",把容量规划写进了立项文档。
上线两周后来了一批客户,用法是把整本产品手册直接传上来问。监控上的现象很怪:
- GPU 的算力利用率一直在 20% 上下晃,从来没打满过
- 但请求排队时间从 50 毫秒涨到 12 秒
- 日志开始刷 vLLM 的 preemption 警告
- 实际同时在跑的请求数,从 50 多掉到了个位数
团队第一反应是加卡。加到两张,排队时间只降了不到一半。第二反应是"模型太大了",换成 3B,好一点,但没解决。
真因是一行算术。上下文从 8K 拉到 128K,每条请求的 KV Cache 从 1 GiB 涨到 16 GiB。这张 80 GB 的卡刨掉权重(16.1 GiB)和激活与临时缓冲,能留给 KV Cache 的约 54 GiB——8K 时装得下 50 多条并发,128K 时只装得下 3 条。序列长 16 倍,并发掉 16 倍,吞吐跟着掉 16 倍。而算力单元始终是闲的,所以加卡只能线性地多加那三条,加不出数量级。
三层教训,恰好是本篇的三条主线:
- 长上下文的瓶颈是显存和带宽,不是算力——"加卡"是最贵也最低效的一条路;
- 容量规划不能用"压测能跑多少并发",要用 goodput(满足延迟约束的有效吞吐);
- "自建还是调 API"这笔账,本质上算的就是你有没有本事把卡的利用率喂到足够高。
压测 8K,稳跑 50 多路
P95 首字 400 毫秒出头,写进立项文档
客户传整本产品手册
上下文从 8K 拉到 128K
每条 KV:1 → 16 GiB断点
54 GiB 的 KV 池只装得下 3 条
排队 50 毫秒 → 12 秒
日志开始刷 vLLM 的 preemption 警告
加到两张卡
排队只降了不到一半,换 3B 也没解决
核心概念:先打比方,再给定义
KV Cache(键值缓存)。模型每生成一个字都要"回看"前面所有字,不缓存的话生成第 1000 个字时要把前 999 个重算一遍,总计算量是平方级的。KV Cache 把每个 token 的 Key / Value 向量存下来供直接查表。一句人话:用显存换算力,这笔交易几乎总是划算的——代价是显存变成了新瓶颈。
prefill / decode(预填充 / 解码)。一次请求分两段:prefill 把整个 prompt 一次喂进去算出全部 KV,一步处理几千个 token;decode 逐个吐字,每步只处理 1 个 token。像把一整箱货一次搬上传送带,和从箱子里一粒一粒往外掏。这两段性能特征完全相反,是本篇几乎所有结论的源头。
continuous batching(连续批处理,又叫 in-flight batching、迭代级调度)。旧的 static batching 像跟团游,一车人得等最慢那位逛完才能走;它像急诊分诊,谁看完谁走,空出的床位立刻给下一位。定义上是把调度粒度从"一次请求"降到"一次迭代"。
PagedAttention(分页注意力)。操作系统的虚拟内存分页搬到 KV Cache 上:不给每条请求预留一整块连续显存(那必然要按"最多会生成多长"预留),而是切成固定大小的 block 按需分配,共享前缀的请求还能指向同一批 block。
goodput(有效吞吐)。在满足延迟约束的前提下每秒能完成的请求数。一句人话:throughput 是"你跑了多少",goodput 是"其中有多少是用户还愿意等的"。
一步就把整箱搬完
搬得动,是因为一次搬得够多
一步处理几千个 token
算术强度 ≈ 这一步的 token 总数;4000 ≫ 295 → compute-bound(卡算力)
每次只掏出一粒
手快没用,来回那趟路才是瓶颈
每步只处理 1 个 token
算术强度 ≈ 并发数;batch=32 时 32 ≪ 295 → memory-bound(卡带宽)
原理拆解
H_kv:Llama-3.1-8B 有 32 个注意力头,只有 8 个 KV 头KV_bytes2×L×H_kv×D_head×S×B×Pgpu_memory_utilization 0.92)| 算什么 | 怎么算 | 得数 | 读法 |
|---|---|---|---|
| 每 token 的 KV | 2 × 32 × 8 × 128 × 2 字节 | 128 KB | H_kv 写成 32 就错 4 倍;70B 是 64 比 8,错 8 倍 |
| 128K 单条要多少 | 128 KB × 131072 | 16.0 GiB | 顶得上 16.1 GiB 的一整份权重 |
| 卡上留给 KV 多少 | 79.6 × 0.92 − 16.1 − 约 3 | ≈ 54 GiB | 权重与激活先扣,KV 走剩余法 |
| 8K 能并发几条 | 54 GiB ÷ 1 GiB | ≈ 54 条 | 54 GiB ÷ 128 KB ≈ 44 万 token |
| 128K 能并发几条 | 54 GiB ÷ 16 GiB | ≈ 3 条 | 长 16 倍,并发掉 16 倍,吞吐跟着掉 16 倍 |
| FP8 KV 何时才划算 | 存储减半,每 token 成本降到 54% | 约 7000 token 打平 | 短上下文下反而更慢,所以默认不开 |
瓶颈要分两段答:prefill 一步塞进 4000 个 token,4000 ≫ 295 → 卡算力;decode 每步只出 1 个 token,算术强度 ≈ 并发数,batch=32 卡带宽,batch=512 又变回卡算力 —— 「decode 卡带宽」是关于 batch size 的陈述。
带宽账(H100 的 HBM 3.35 TB/s):batch=1 / 8K 每步读 17.1 GiB → 最快 5.5 ms;128K 读 32.1 GiB → 10.3 ms;batch=32 / 32K 读 144.1 GiB → 46.2 ms,KV 占 89% 流量。
一、KV Cache 到底占多少显存
标准公式(这一行必须背对):
KV_bytes = 2 × L × H_kv × D_head × S × B × P 2 :K 和 V 各存一份 L :层数 num_hidden_layers H_kv :KV 头数 num_key_value_heads ← 不是注意力头数! D_head :头维度 head_dim S :序列长度(prompt + 已生成) B :并发请求数 P :精度字节数 bf16=2, fp8=1, int4=0.5
H_kv 是最容易写错的地方。 不少资料写的是注意力头数,因为它们举的例子是 Llama 2 7B——MHA 模型,两个数恰好相等。换成现代 GQA 模型就会错得离谱:Llama-3.1-8B 是 32 个注意力头但只有 8 个 KV 头,写错就是 4 倍;70B 是 64 比 8,写错就是 8 倍。
代入具体数字(bf16,参数取自官方 config):
| 模型 | L | H_kv | D_head | 每 token KV |
|---|---|---|---|---|
| Llama-3.1-8B | 32 | 8 | 128 | 128 KB |
| Llama-3.1-70B | 80 | 8 | 128 | 320 KB |
| DeepSeek-V3(MLA) | 61 | — | — | 68.6 KB |
一个值得记住的对照:Llama-3.1-8B 的权重是 16.1 GiB,而它在 128K 上下文下,batch=1 的 KV Cache 就是 16.0 GiB——一条长请求的 KV,顶得上一整份模型权重。
再看显存怎么分(H100 80GB 跑 8B,vLLM 的 gpu_memory_utilization 默认 0.9,截至 2026-08 主线已调到 0.92):
┌──────────────── H100 80 GB ──────────────────────────┐ │ 预算 = 79.6 × 0.92 = 73.2 GiB │ │ ├── 模型权重 16.1 GiB ████ │ │ ├── 激活+CUDA图 ~3 GiB █ │ │ └── KV Cache 拿走 ~54 GiB ████████████████ │ ← 剩余法分配 │ │ │ 54 GiB ÷ 128 KB = 约 44 万 token 的缓存容量 │ │ 8K 上下文 → 可并发 ~54 条 │ │ 128K 上下文 → 可并发 ~3 条 ← 开头那个故障 │ └────────────────────────────────────────────────────────┘
讲"吞吐塌陷"要用并发数讲,不要用显存 GB 讲。「同一张卡,上下文长 16 倍,并发从 54 掉到 3」这一句比任何显存曲线都有说服力。
二、为什么瓶颈分成两段
判断一个计算卡算力还是卡带宽,看算术强度(从显存搬一个字节能摊到多少次浮点运算)。对 Transformer 的矩阵乘法,它约等于这一步参与计算的 token 总数;H100 的临界值约 295 FLOPs/byte(峰值算力÷HBM 带宽)。
prefill:一步塞进 4000 个 token → 4000 ≫ 295 → compute-bound(卡算力)
推论:单个足够长的请求,一条就能把 GPU 打满
decode :每条请求每步只出 1 个 token
算术强度 ≈ 并发数
batch=32 → 32 ≪ 295 → memory-bound(卡带宽)
batch=512 → 512 > 295 → 又变回 compute-bound所以"decode 是 memory-bound"是关于 batch size 的陈述,不是 decode 的本质属性。 这是面试里的强分水岭,也直接推出下一层:vLLM 官方明确说投机解码(speculative decoding)只在"中低 QPS、memory-bound"下有效——高 QPS 时 decode 已卡算力,多出的验证开销纯亏。
带宽账可以直接算(H100 的 HBM 带宽 3.35 TB/s,decode 每步要把权重和整个 KV Cache 读一遍):
| 场景(8B, bf16) | 每步要读 | 理论最快 ITL |
|---|---|---|
| batch=1, 8K | 16.1 + 1.0 = 17.1 GiB | 5.5 ms |
| batch=1, 128K | 16.1 + 16.0 = 32.1 GiB | 10.3 ms |
| batch=32, 32K | 16.1 + 128 = 144.1 GiB | 46.2 ms(KV 占 89% 流量) |
batch 一大、上下文一长,权重读取被 KV 读取彻底淹没——此时加算力毫无用处,只有减小 KV 体积或复用 KV 才有效。
三、PagedAttention 与 continuous batching 各解决什么
这两件事常被混为一谈,其实解决的是两个不同的浪费。
continuous batching 解决"等"的浪费。
static batching(旧) req A ████████████░░░░░░░░ ← 早就完事了,但坑位不释放 req B ████░░░░░░░░░░░░░░░░ req C ████████████████████ ← 全车人等它 新请求 ......................(只能等下一批) continuous batching(现在) req A ████████████→ 走人,坑位立刻给 D req B ████→ 走人,坑位立刻给 E req C ████████████████████ req D ████████████ req E ████████
源头是 Orca(OSDI 2022),机制是迭代级调度 + 选择性批处理。Anyscale 那篇经典测评给的是相对朴素 static batching 约 8 倍吞吐,但有个必须说出来的条件:增益只在输出长度方差大时成立——如果所有请求输出长度都差不多,static batching 本来也没浪费多少。
PagedAttention 解决"占"的浪费。 论文(SOSP 2023)测出当时系统里真正存住 token 状态的显存只占 20.4%–38.2%,其余是三类浪费:为未来生成预留的槽位、按最大长度分配的内部碎片、分配器造成的外部碎片。分页后官方博客的说法是浪费降到"低于 4%"。
报倍数之前必须先问"跟谁比",这是这道题最有效的过滤器:
| 对比对象 | 倍数 |
|---|---|
| vs 裸 HuggingFace Transformers | 最高 24× |
| vs HuggingFace TGI(已有 continuous batching) | 最高 3.5× |
| vs Orca(论文正文) | 1.7–2.7× |
张口就是"vLLM 快 24 倍"的人,等于承认自己没看过论文正文。
一个 2026 年的反转(截至 2026-08)。 vLLM 在 v0.25.0(2026-07-11)的 release notes 里写着「PagedAttention has been removed」——那份 V0 时代手写的 paged_attention_*.cu 内核确实被删了,因为 FlashAttention / FlashInfer 生态里的 paged 变体更快。但分块 KV 管理机制完全健在(block_pool、kv_cache_manager、block_table 全都在),只是改由上游注意力库消费 block table。准确的说法是:概念赢了并成为行业地基,那份专用内核输给了上游生态。 说"vLLM 不用 PagedAttention 了"是错的。
四、goodput:为什么 throughput 会骗人
DistServe 给了最好用的例子:系统 throughput 是 10 rps,但只有 3 rps 落在延迟约束内,goodput 就是 3——你按 10 rps 付了 GPU 的钱,只交付了 3 rps,有效成本是账面的 3.3 倍。
而 throughput 与延迟之间只有一个旋钮:batch size。
batch ↑ → 权重读取被摊薄 → 吞吐 ↑↑
→ 排队 + 单步计算量 ↑ → TTFT ↑、TPOT ↑
→ KV 占用 ↑ → 触发抢占重算 → 尾延迟 ↑↑
batch ↓ → GPU 大量空转 → 吞吐 ↓,但单请求延迟最优所以唯一正确的优化目标是 goodput:它把延迟约束内生化,逼你在 SLO 边界上找最大 batch,而不是无脑推吞吐或压延迟。NVIDIA 的 AIPerf 已支持直接测,可同时对 TTFT 和 ITL 设阈值。
工程实践(截至 2026-08)
自建 vs 调 API:这笔账的四个维度
别一上来算单价,真正决定结论的是这四条:
| 维度 | 倒向调 API | 倒向自建 |
|---|---|---|
| 利用率 | 流量有明显波峰波谷、日调用量不稳 | 有稳定的高基线负载,能把卡喂到 60% 以上 |
| 数据主权与合规 | 无特殊要求 | 数据不能出域、行业强监管、需要物理隔离 |
| 模型控制 | 用通用能力就够 | 要跑自己微调的权重、要改采样逻辑、要 LoRA 多租户 |
| 团队 | 没有专职推理工程 | 有人能读懂 preemption 日志、会调 chunk size |
最容易被漏掉的是最后一条。 自建的隐性成本不是卡钱,是"有人得懂这个"——开头那个故障里两次试错的人力成本,够买很久的 API。
算账的三条硬判据:
- 用 goodput 而不是 throughput 折算单位成本。峰值吞吐除以 SLO 内的达标率,才是你真正能卖出去的量。
- 算利用率,别算峰值。一张卡包月的钱是固定的,跑 20% 和跑 80% 一样贵;API 按 token 计费,天然只为用掉的部分付钱——这是 API 最被低估的优势。
- 自建要打平的不是 API 的标价,是打完折之后的价:批处理四家一律五折、prompt 缓存命中是输入价的 0.1 倍(详见 T6-4)。
推理引擎选型(截至 2026-08)
2026 年功能集已高度趋同——continuous batching、分块 KV、前缀缓存、chunked prefill、投机解码、P/D 分离三家都有。选型看的是别的:
| 场景 | 选型 | 理由 |
|---|---|---|
| 通用生产、模型覆盖最广、多种硬件 | vLLM | 社区最活跃,硬件矩阵最全,KV connector 生态最厚 |
| 高前缀复用(agent 多轮、长 system prompt、大规模 MoE) | SGLang | RadixAttention + 按最长共享前缀重排请求的调度 |
| 纯 NVIDIA 集群、要最新数值格式(FP4)首发 | TensorRT-LLM | Blackwell 特性首发,代价是硬件锁定 |
两个容易过时的认知要更正:
- "TensorRT-LLM 必须提前 build engine、换模型要重编译"是 2023–2024 的老答案。 截至 2026-08,官方主推的是 PyTorch-native 架构 +
trtllm-serve。 - vLLM 与 SGLang 的前缀缓存差别不在"有没有",在"调度器会不会为命中率重排请求"。 vLLM 是哈希索引 + LRU、调度默认 FCFS;SGLang 用基数树,把"最长共享前缀优先"做成一等公民并证明了最优性。
别问"谁更快"。 三家的相对性能完全取决于你自己的输入输出长度分布、并发度和 SLO,只能实测。上来就报某个倍数,是只读过没做过的最明显信号。
压 KV Cache 的四条路(按成熟度排序)
| 手段 | 效果 | 代价 / 什么时候不该用 |
|---|---|---|
| GQA / MLA(结构级) | GQA 压 4–16×(= Q 头数 / KV 头数);MLA 等效 2.25 组的 GQA,比 GQA-8 再压 3.5× | 已是模型自带,选型时就定了。MLA 不能用现成 Llama 权重,内核生态窄 |
| FP8 KV Cache | 存储直接减半;decode 每 token 成本降到 bf16 的 54%(不是 50%,有反量化开销);128K MRCR 恢复 97–98% | 有约 7000 token 的盈亏平衡线,短上下文下反而更慢;head_dim=256 的模型 TTFT 恶化约 1.6×。默认不开 |
| 前缀复用 / KV 卸载 | 命中时 TTFT 可降数倍 | 有带宽临界点:32 Gbps 互联下,输入要超过 256K 才比重算划算。低带宽时"搬 KV 比重算还慢" |
| KV 驱逐(H2O / SnapKV 等) | 纸面能压到 6–25% | 生产上基本不可用,见下 |
KV 驱逐为什么是这一章最值得讲的"反向判断":
- 生产用 FlashAttention,它从不把注意力分数矩阵写进显存——驱逐方法想看的打分信号根本不存在。参考实现是靠退回 eager attention 绕过的,这在生产里等于不可用。
- 分页布局下,驱逐 90% 的 token 几乎释放不了显存:幸存者散落在几乎每个 block 里,块不空就不回收。
- 现有 benchmark 普遍是"问题已拼进上下文之后"才压缩,真实部署却是"先压缩、后来问题"。在公平设定下,部署最广的 SnapKV 平均输给"保留开头 + 最近窗口"这个最朴素的基线。
避坑清单
max_num_batched_tokens是跷跷板:小(如 2048)ITL 好、TTFT 差;大(>8192)吞吐好。想清楚你在优化哪个。- 看到 preemption 警告就别再调延迟了,先看 KV Cache 占用率——那是显存不够,不是调度问题。
- 前缀缓存在 vLLM V1 默认开启,命中率为 0 时吞吐损失也小于 1%;但分布式下真正的杀手是路由——负载均衡器把同前缀请求打散到不同实例,缓存局部性就没了。
- 别把"KV 卸载"和旧的"GPU↔CPU swap"搞混:后者为处理抢占而生、在 vLLM V1 已被移除(抢占直接重算),前者是为提高跨请求命中率。
| 维度 | 倒向调 API | 倒向自建 |
|---|---|---|
| 利用率 | 流量有明显波峰波谷、日调用量不稳 | 有稳定的高基线负载,能把卡喂到 60% 以上 |
| 数据主权与合规 | 无特殊要求 | 数据不能出域、行业强监管、需要物理隔离 |
| 模型控制 | 用通用能力就够 | 要跑自己微调的权重、要改采样逻辑、要 LoRA 多租户 |
| 团队最容易被漏掉 | 没有专职推理工程 | 有人能读懂 preemption 日志、会调 chunk size |
- 算账三判据
- 用 goodput 而不是 throughput 折算单位成本;算利用率别算峰值;打平的是折后价
- 引擎选型
- 通用生产 vLLM;高前缀复用(agent 多轮、大 MoE)SGLang;要 FP4 首发 TensorRT-LLM
- 别问谁更快
- 三家功能已高度趋同,相对性能取决于你的输入输出长度分布、并发度和 SLO,只能实测
- FP8 KV Cache
- 存储直接减半、decode 每 token 降到 54%;盈亏平衡线约 7000 token,默认不开
- KV 驱逐别上生产
- FlashAttention 不写注意力分数矩阵;分页下驱逐 90% 也释放不了显存;SnapKV 输给最朴素基线
- 看到 preemption
- 先看 KV Cache 占用率 —— 那是显存不够,不是调度问题,别再去调延迟参数
面试视角
- 别先报单价先说这笔账取决于四件事:利用率、数据主权、模型控制、团队
- 用 goodput 收尾「我们按 goodput 折算单位成本,不是按压测峰值」
- vLLM 拆成两件事continuous batching 治「等」,PagedAttention 治「占」
- 报倍数先说跟谁比vs 裸 HF 24×、vs TGI 3.5×、vs Orca 论文正文 1.7–2.7×
- KV 分两段再收尾容量限并发、带宽限单步;同卡 8K 跑 54 条、128K 只剩 3 条
- 公式里写成「注意力头数」,或根本不知道 GQA 会改这个数
- 「vLLM 快 24 倍」—— 不问跟谁比,等于承认没看过论文正文
- 把长上下文慢一律归因为注意力的平方复杂度,然后提议加卡
- 说「我们上了 H2O / SnapKV 压 KV,省了 90% 显存」
- 谈自建 vs API 只报单价对比,不提利用率
- 脱口而出 8B 每 token 128 KB、70B 320 KB,并说清 4 倍与 8 倍的来历
- 知道 8× 只在输出长度方差大时成立,也说得出「内核删了、机制还在」
- 把瓶颈拆成 prefill 卡算力、decode 卡带宽,用并发数而不是 GB 讲塌陷
- 说得出 FP8 KV 的盈亏平衡线约 7000 token —— 知道它什么时候不该开
- 分得清 throughput 与 goodput,算过「有效成本是账面的 3.3 倍」
面试官怎么问
答题结构建议
Q6-01(自建 vs API):别先报单价。先说"这笔账取决于四件事"——利用率、数据主权、模型控制、团队能力,再落到自己的场景。收尾一定要有一句:"我们按 goodput 折算单位成本,不是按压测峰值。"
Q6-02(vLLM 为什么快):先拆成两件事——continuous batching 解决"等"的浪费,PagedAttention 解决"占"的浪费,报数字时主动说"跟谁比"。想拿满分就补上 2026 的现状:内核删了、机制还在。
Q6-03(KV Cache):先给公式并强调是 KV 头数,再分两段答瓶颈(容量限制并发数、带宽限制单步延迟),最后用"同一张卡 8K 并发 54 条、128K 只剩 3 条"这个对照收尾。
分水岭信号
什么回答暴露只读过:
- 公式里写成"注意力头数",或者根本不知道 GQA 会改这个数
- "vLLM 快 24 倍"——不问跟谁比
- 把"长上下文慢"一律归因为注意力的平方复杂度,然后给出"加卡加算力"的扩容方案
- 说"我们上了 H2O / SnapKV 压 KV,省了 90% 显存"
- 谈自建 vs API 只报单价对比,不提利用率
- 把 P/D 分离说成"提升吞吐的优化"(官方文档原话是它不提升吞吐,买的是 TTFT 与 ITL 可独立调)
什么证明真做过:
- 能脱口而出"Llama-3.1-8B 每 token 128 KB、70B 每 token 320 KB",并解释为什么 8B 是 4 倍压缩而 70B 是 8 倍
- 把瓶颈拆成 prefill 卡算力、decode 卡带宽,并指出**"decode 卡带宽"是关于 batch size 的陈述,并发推到 500 以上它也会变成卡算力**
- 用并发数而不是显存 GB 来讲吞吐塌陷
- 主动区分 throughput 和 goodput,并算过"有效成本是账面的几倍"
- 说得出 FP8 KV 的盈亏平衡线(约 7000 token)——"知道它什么时候不该开"比"知道它能省一半"值钱
- 谈引擎选型时拒绝报倍数,改成"取决于你的输入输出长度分布,得实测"
小结与延伸
三条主线收成一句话:推理服务的成本,本质是"这张卡的显存喂多满、算力用多久"的乘积。
- KV Cache 把计算换成显存,于是显存容量决定并发多少,显存带宽决定每步多快。
- continuous batching 与 PagedAttention 分别消灭了"等"和"占"两种浪费,它们是地基不是加分项——2026 年单独夸这两件事已经拿不到分。
- goodput 是唯一诚实的容量指标:跑得快但用户不愿等的那部分,你是白付了钱的。
延伸方向:本篇只讲单机视角,跨实例的 KV 复用与前缀感知路由是 2026 年真正的战场;成本侧的三刀归因见 T6-4。
继续深入
本篇归属第 6 章「部署与成本」,去做这一章的题。