Round 2 · 整轮综合 · RTX 4090 · Qwen2.5-7B-Instruct

选型指南

本报告把五份子报告的结论合成一条选型路径:vLLM FP16 与 llama.cpp Q4_K_M 各自强在哪,生产上该怎么选。 测试条件相同:同一张 4090、同一个 7B 模型、同一份压测脚本,共五组测试、约 3,600 条请求。

GPU
RTX 4090 · 24 GB · 580.159.03
A 组
vLLM 0.27.1 · bfloat16
B 组
llama.cpp eab8ee41f · Q4_K_M
吞吐交叉点
C ≈ 8.3
帕累托交叉
p99 TTFT(首字延迟)≈ 730 ms
子报告
5 份 · 同目录

结论三句话

两个引擎没有绝对的好坏,只是取舍曲线不同,而且两条曲线在很低的位置就交叉了。

真正的分界线不是并发数,是延迟预算

p99 TTFT ≈ 730 ms

分界点在 p99 ≈ 730 ms:预算比这紧,llama.cpp 赢;比这松,vLLM 赢。 按吞吐算,两条曲线在 C ≈ 8.3 换位。但生产上的并发由 SLO 反推,不能自由选, 所以横轴应该是"你能忍受多大的 TTFT"。

llama.cpp 单流优势

2.61 ×

161.5 vs 61.9 tok/s(C=1)

vLLM 峰值优势

1.30 ×

1,387.8 vs 1,064.8 tok/s(C=64)

C=64 批处理效率

81 vs 25 %

同一条 roofline,前者是后者的 3.2 倍

llama.cpp KV 容量

2.18 ×

262,144 vs 120,064 token(标称)

本报告只用到四个量。完整定义见各子报告:主线指标(TTFT / ITL / TPOT / E2EL / 吞吐)见《vllm-01》开篇,MBU / MFU / 算术强度见《vllm-03》,排队分解与抢占见《vllm-02》《vllm-04》。
指标量的是什么为什么这份报告需要它
TTFT从发出请求到收到第一个 token本报告的选型分界线画在它的 p99 上。它是用户按下回车后等待的时间,SLO 通常也定在它的 p99 上。
ITL相邻两个输出 token 的间隔出字的流畅度。ITL 正常而 TTFT 爆炸,说明在排队;ITL 也慢,说明 decode 本身有问题。这是两种病。
SLO事先定下的延迟承诺例如"p99 TTFT ≤ 1 秒"。有了 SLO,"该开多大并发"才能算出来。
MFU / MBU算力 / 带宽跑到峰值的百分之多少用来判断瓶颈在哪堵墙上。decode 是访存密集(看 MBU),prefill 是算力密集(看 MFU)。

选型先定延迟预算,再反推并发

换个 SLO,结论就翻面。 "交叉点 C≈8"假设你能自由选择并发。但生产上的并发由 SLO 反推: 先定"p99 TTFT 不能超过 X",再看这张卡在这条线下最多能接多少并发。

Figure 1

吞吐 / 延迟帕累托前沿:横轴是你能忍受的 p99 TTFT

vLLM FP16 llama.cpp Q4_K_M SLO 线 500 / 1000 / 2000 ms

两条线在 p99 ≈ 730 ms 附近交叉:左边橙线在上(llama.cpp 赢), 右边蓝线在上(vLLM 赢)。每个点是一个并发档,越靠左上越好(同样延迟下吞吐更高)。 llama.cpp 的 C=32 点是反常的(见《llama-cpp-压测》的「反常」一节)。

给定 SLO,各自能跑到的最大并发与对应吞吐。并发与吞吐都在实测点之间做 log–log 插值。
SLOvLLM 并发vLLM 吞吐llama.cpp 并发llama.cpp 吞吐谁赢
p99 < 500 ms5.3286 tok/s5.3355 tok/sllama.cpp +24%
p99 < 1000 ms11.152310.3460vLLM +14%
p99 < 2000 ms22.683620.0512vLLM +63% *
不设 SLO(峰值)641,388641,065vLLM +30%

* "vLLM +63%" 应读作 vLLM 优势的上界。 llama.cpp 侧的 C=20.0 落在 C=16 与 C=32 之间,而 C=32 那一档反常塌陷 (444 tok/s,比它自己的 C=16 还低),所以插值偏低。 500 ms 那一行与 730 ms 的交叉点都只用到 C≤8,不受影响。

这张表是整份报告最该记住的东西

同一份硬件、同一个模型,结论在 500 ms 和 2000 ms 之间翻了个面。 任何"A 引擎比 B 引擎快 N 倍"的说法,不说清延迟约束就没有意义。

原因:SLO 越紧 → 允许的并发越低 → 越接近单流 → 量化的带宽红利占主导; SLO 越松 → 并发越高 → 越依赖批处理效率 → vLLM 的调度优势占主导。

什么时候该用 llama.cpp

在下面这些场景,llama.cpp 不是"凑合",是更优解: 单人 / 低并发(C ≤ 8)、边缘设备、显存吃紧但要长上下文、 延迟 SLO 非常紧(p99 < 500 ms)。 它单流快 2.6 倍,每一档的 p50 TTFT 都更低(p99 从 C=8 起反而更高),KV 容量是 vLLM 的 2 倍多。

什么时候该用 vLLM

适用场景:多租户在线服务、并发 > 10、要用 SLO 做容量承诺、 过载时需要抢占 / 排队而不是直接失败。 并发一过 8,批处理效率就成了唯一重要的事。 这一项 vLLM 是 llama.cpp 的 3 倍以上,C=16 到 C=64 一直保持在 3 倍以上。

对照各自强在哪

下表汇总五份子报告里的优势项。粗体是这一行的赢家, 最后一列是数据出处。

负载统一为 1024 输入 / 256 输出(除标注外)。延迟单位 ms。
比什么vLLM FP16llama.cpp Q4_K_M出处
单流 decode 步长15.835.84(2.71×)llama-cpp-压测 · 字节账
单流吞吐61.9 tok/s161.5(2.61×)llama-cpp-压测 · 交叉
TTFT p50(各档)102 → 78793 → 441llama-cpp-压测 · 数据
C=64 峰值吞吐1,387.8(1.30×)1,064.8llama-cpp-压测 · 交叉
C=64 批处理效率81%25%llama-cpp-压测 · 摊薄
每多一条并发的成本0.106 ms(地板 1.6×)0.468 ms(7.1×)llama-cpp-压测 · 摊薄
KV 池容量(标称)120,064 tok262,144(2.18×)llama-cpp-压测 · 显存
KV 分配方式按 block 共享每槽硬分配,用不完不能借llama-cpp-压测 · 显存
过载时的行为排队 + 抢占,零失败未测vllm-04-断崖
混批(chunked prefill)有,C=4 时仅 3.4% 停顿无,C=4 已 50.7% 停顿llama-cpp-压测 · 波次
可观测性/metrics 全套计数器只有日志里的自报速率全部五份
容量可预测性启动日志直接给上限要自己算 -np × -cvllm-03-KV的线性 · vllm-04-断崖

机制为什么会这样

上表中,llama.cpp 赢的全是"单条请求"的项,vLLM 赢的全是"一批请求"的项。 背后是两条独立的机制。

机制一:prefill 和 decode 是两种完全不同的负载

decode 是访存密集的:生成一个 token 要把整个模型权重读一遍。 量化把字节压小,步长就按同样比例变短。

prefill 是算力密集的:一次前向处理上千个 token,权重读一次摊给所有 token。 瓶颈在 FLOPs,不在带宽;Q4_K 的解量化只会让 FLOPs 更多。

# 同一颗芯片,瓶颈随批量大小在两堵墙之间移动(vLLM 计数器实测)
C=1    MBU 88.0%   MFU  2.5%    ← 贴着访存墙,算力几乎闲置
C=64   MBU 43.2%   MFU 57.3%    ← 反过来了,交点在 C≈44

# 长度扫描把并发钉死、只拉输入长度,走到了同一个地方
L=256  MBU 82.1%   MFU 14.3%
L=4096 MBU 43.6%   MFU 53.1%

# 真正的自变量既不是并发也不是长度,而是"一次迭代里参与计算的 token 总数"
# 两条轴按 tok/iter 对齐后,MFU 逐点差 <0.7 pt、MBU <3.6 pt

推论:量化收益随负载形状变化

对话类负载(短输入长输出)量化收益大; RAG / 长文摘要(长输入短输出)收益小得多。 量化换来的是decode 速度和显存,不是 TTFT。 服务端量到的纯 prefill 是 llama.cpp 85.3 ms、vLLM 93.5 ms,只快 9%, 而且这 9% 来自实现细节,不是位宽。

机制二:省字节是一次性红利,摊薄字节才可复利

批处理的原理:一个 decode 步把权重读一次,同时喂给 C 条序列。 C 越大,每个 token 分摊到的权重字节越少。 理论地板 (权重 + C × 单条KV) / 带宽 随 C 增长得非常慢。 用这个地板除以实测 ITL p50,就得到"批处理红利吃到了多少"。

Figure 2

roofline 达成率:两个引擎在同一条标尺上

vLLM 达成率 llama.cpp 达成率

两条地板之间的距离就是量化红利,而它是恒定的。 拉开差距的完全是"各自离自己的地板有多远":蓝线几乎是平的(89.0% → 81.0%), 橙线一路塌到 11%。达成率 = roofline 地板 ÷ 实测 ITL p50。

# 每多接一条并发,解码步长要多付多少
理论地板   单条 KV ÷ 带宽                   = 0.066 ms / 条
vLLM       (22.50 − 15.83) ÷ 63             = 0.106 ms   ← 地板的 1.6 倍
llama.cpp  (35.32 −  5.84) ÷ 63             = 0.468 ms   ← 地板的 7.1 倍

llama.cpp 的批量 decode,更像是"把单流做了 C 遍"

llama.cpp 的边际成本 0.468 ms 是地板的 7.1 倍, 说明权重每步被重复读、重复解量化的成分很大。 如果批处理只多读 KV,边际成本应该贴着 0.066 ms;vLLM 的 0.106 说明它基本吃到了红利。

这解释了全部现象:C=1 时 vLLM 每步读的字节是 llama.cpp 的 3 倍,所以 llama.cpp 赢; C 变大后,vLLM 把自己的 14.14 GB 摊薄了 64 次,llama.cpp 几乎没摊薄自己的 4.68 GB,所以输。

直接证据在《llama-cpp-压测》里:CUDA graph 复用率从 99.2% 崩到 0%, 与达成率的塌陷同步发生。两者相关性极强,但要证明因果,还需要 -DGGML_CUDA_USE_GRAPHS=OFF 的对照实验。

容量能算出来,不用压测

容量是全篇唯一纯算术的部分,精确到不需要压测。 上面两条机制决定"跑多快",容量决定"能跑多少"。

# 每 token 的 KV 字节,完全由模型结构决定
2 (K,V) × 28 层 × 4 KV head × 128 dim × 2 B = 56.0 KiB   GQA 让它比 MHA 小 7 倍

# 于是容量红线是一道除法
单请求占满 8,192 上下文 = 8192 × 56 KiB = 448 MiB
vLLM 池子 6.41 GiB ÷ 448 MiB           = 14.65 条
                                          ↑ 启动日志自己就打出来了:Maximum concurrency 14.66x

# 压爆那一轮的实测:驻留均值 15.65 条,比预告高 5%
# (因为请求刚进来只占 6144 token,要解码 2048 步才涨到 8192)
# 第一轮同样是 11.17 vs 10.60 —— 两台机器,同一个 +5% 偏差率
Figure 3

24 GB 卡上的显存去向:量化红利兑现在哪

量化省下 10.1 GiB 权重,换来 2.18 倍 KV 容量。 但这是标称值:llama.cpp 的槽位上下文是硬分配的,vLLM 的 KV 池按 block 共享。 vLLM 四段来自启动日志原文。llama.cpp 的总量 19,089 MiB 是 nvidia-smi 实测; 权重加 KV 算出来是 18.36 GiB,与实测 18.64 相差 1.5%,账对得上。

越过容量,线性关系全部作废

把序列拉到 6144/2048、并发设为 64,KV 池会超卖 4.4 倍: 单请求满上下文要 448 MiB,64 条要 28.0 GiB,而 KV 池只有 6.41 GiB。 下表两组都是 C=64,只改了序列长度:

两行都是 C=64,唯一的差别是请求形状。详见《vllm-04-断崖》。
指标C=64 · 1024/256C=64 · 6144/2048倍数
KV 需求 / KV 池4.4 / 6.41 GiB(够)28.0 / 6.41 GiB(爆)—
实际驻留 Running59.9 / 6415.65 / 640.26×
输出吞吐1,387.8 tok/s532.40.38×
TTFT p500.79 s169.0 s215×
ITL p5022.50 ms23.39 ms1.04×
抢占次数038—
失败请求00—

压爆不等于崩溃 —— 这是 PagedAttention 的价值

PagedAttention 把硬失败变成了软降级。 超卖 4.4 倍、KV 顶到 100%、抢占 38 次,结果是零失败、零错误、输出 token 一个不少, 代价只是排队 169 秒。

ITL 和 TTFT 的组合本身就能用来诊断: ITL 正常 + TTFT 爆炸 = 容量不够,在排队 (加卡、减并发、缩短 max_model_len 都能解决); ITL 也慢 = decode 本身有问题(要查 batch 配置、kernel、量化实现)。 前者是《vllm-04-断崖》里 vLLM 的病,后者是《llama-cpp-压测》里 llama.cpp 的病。两种病,两种药。

落地如果你要在生产上跑它

诚实没搞清的、没测的

没搞清的

没测的

一个被证伪的假设,也记在这里

假设是"第一轮的 repetition_penalty 1.05 让 vLLM 偏悲观",这是第二轮重跑 vLLM 的动机。 结果七个档位的差异全在 ±1.2% 以内,方向也不一致。这是噪声,假设错了。 排除一个假设也是结果:重跑把"采样参数不同构"从怀疑名单里划掉了, 剩下的差异只能归因于引擎本身。详见《vllm-02-TTFT解剖》。

索引五份子报告分别回答什么

全部在同一目录下。本报告的每个数都能在其中一份里找到出处与推导。
报告负载回答的问题
vllm-01-Qwen2.5-7B并发曲线1024/256 · C=1→64吞吐涨到多少才开始拿延迟去换?拐点在 C=8~16,C=64 时扩展效率只剩 35.1%。含两堵墙(MBU/MFU 交点 C≈44)。
vllm-02-TTFT解剖1024/256 · C=1→64TTFT 涨在哪一段?排队、prefill、前端三段分解,以及功耗墙和混批比例。含两轮复现性对照。
vllm-03-KV的线性C=16 · L=256→4096KV 占用是不是线性的?R²=0.99993,56 KiB/token 两次独立验证。含 roofline 换边与算术强度。
vllm-04-断崖6144/2048 · C=64越过 KV 容量会怎样?超卖 4.4 倍、驻留只剩 16 条、抢占 38 次,但零失败。含抢占负反馈控制环。
llama-cpp-压测1024/256 · C=1→64 · 双引擎量化省下了什么?单流 2.71×、批处理效率 81% vs 25%、CUDA graph 复用率崩塌。含 C=32 反常。