TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

用小模型亲自测试 vLLM:负载、KV 缓存容量、前缀缓存、块大小

状态:草稿(2026-10-07),实测文章。 所有数字均为笔者在下述环境中亲自测得的值,计算得出的标记为「计算」,未能测得的标记为「未实测」。由于这是一个环境、一个模型、一次测试批次的组合,比起绝对值,更希望大家能带走 形态与原理。编写规则:以实测数据为基础,未能测得的标记为「未实测」。

目录

1. 想了解什么

vLLM 的文档有许多选项,而在面试和运维中,常会被问到「开启这个选项会带来什么变化,变化有多大」。第 4 篇通过文档整理了原理(KV 缓存公式、前缀缓存、调度器),而本文则用数字回答同样的问题。

  1. 当并发请求增加时,吞吐量和延迟会在哪里发生转折。
  2. KV 缓存容量是否与公式计算值相符,该容量与发生转折的点是如何关联的。
  3. 前缀缓存、KV 8-bit、CUDA 图形、块大小以及并发序列上限分别会改变什么。

2. 环境与方法

项目 值
GPU 单张 8GB 级笔记本 GPU(内存约 7.6GiB),无其他任务
服务器 vLLM 0.31.0 官方镜像,PyTorch 2.13(CUDA 13)
模型 Qwen3-1.7B(bf16,服务器加载的权重 3.22GiB),模型设置为 28 层,KV 头 8,head_dim 128
公共选项 --max-model-len 4096, --gpu-memory-utilization 0.85, --max-num-seqs 64(仅对比测试为 16)
客户端 与服务器在同一容器内的异步客户端,通过流式传输记录每个 token 的时间戳,以计算首字延迟(TTFT)和 token 间隔(ITL)
输入 每个请求采用不同的随机词序列(避免前缀缓存偶然命中),输出 128 个 token(ignore_eos),温度 0
重复 对于并发度 c,请求数为 3c(长输入测试为 2c),前两个请求作为预热丢弃

每次测试都重新启动了服务器(初次启动和重新启动时的 KV 缓存容量不同,详见第 3 节)。表中的值从单次批次中读取,重复波动单独进行了测量。 在同一服务器上重复相同测试 5 次时(并发 32,输入 512,输出 128),吞吐量为 831、833、831、832、831 tok/s,TTFT 中位数为 666~670ms,TTFT p95 为 1,606~1,608ms,ITL p99 为 223~224ms,误差在 0.5% 以内。即便将服务器重新启动 3 次,并发 1 仍为 66 tok/s,并发 32 仍为 831~832 tok/s。前缀缓存(3 次)以及插队情况(3 次,最大 ITL 分别为 227、226、226ms)也保持一致。这意味着在此环境(单张 GPU,无其他任务)中,测试本身的波动较小,若更换至其他设备或存在其他邻居进程的环境,则可能会有所不同。

3. KV 缓存容量符合公式

每个 token 的 KV 大小为 2 × 층 수 × KV 헤드 수 × head_dim × 자료형 바이트。该模型拥有 28 层、8 个 KV 头、head_dim 为 128,且 KV 采用 bf16(2 字节),因此计算结果如下(计算)。

2 × 28 × 8 × 128 × 2 = 114,688 바이트/토큰 (112KiB)
设置 服务器日志中的 KV 缓存内存 服务器日志中的 token 数量 每个 token 的字节数(计算) 公式计算值
bf16 KV(默认) 2.36GiB 22,048 约 114,900 114,688
fp8 KV(--kv-cache-dtype fp8) 2.24GiB 41,920 约 57,400 57,344

4. 增加并发请求时:吞吐量趋于迟钝,随后在 KV 容量处队列爆炸

输入 512,输出 128, --max-num-seqs 64:

并发请求 吞吐量(tok/s) TTFT 中位数 TTFT p95 ITL 中位数 ITL p99
1 66 73ms 73ms 15ms 16ms
8 383 351ms 474ms 17ms 19ms
32 831 666ms 1.6s 24ms 223ms
64 787 5.7s 6.7s 25ms 228ms

若将输入增加到 2,048 个 token,则单个请求为 2,176 个 token,缓存可容纳的请求数约为 10 个(计算)。

并发请求 吞吐量(tok/s) TTFT 中位数 TTFT p95
4 151 0.73s 1.0s
16 207 4.0s 6.7s
32 213 12.5s 16.7s

当并发数从 4 个增加到 16 个(4 倍)时,吞吐量仅增加了 37%,而 TTFT 变为了 5.4 倍。在长输入情况下, 吞吐量开始拐弯的并发度会显著降低。 这就是为什么在运维中,并发限制不应设为「GPU 所能承受的极限」,而应根据「平均输入长度下 KV 所能容纳的请求数」来设定的原因。

当 KV 不足时:是仅排队,还是被驱逐

通过服务器指标(vllm:num_preemptions_total)读取了抢占次数。在重复 5 次输入 512、并发 32 的过程中(KV 可容纳的请求数在 34 个以内),抢占次数为 0次。当发送输入 2,048、并发 32(64 个请求)时为 7次,接着发送输入 1,500、输出 256、并发 64(128 个请求)时,累计达到 29次(在此负载下为 22 次)。此时的 TTFT 中位数分别达到了 12.5 秒和 35.4 秒。当 KV 不足时,请求不仅会在队列中等待, 正在执行的请求还会交出 KV 被驱逐,然后从头开始重新处理(根据文档标准,抢占是指重新计算被回退的请求,因此会额外消耗一次输入处理)。抢占次数不为零的区间是吞吐量不增且延迟急剧上升的区间,因此非常适合用作报警指标。

并发序列上限(--max-num-seqs)只会改变队列的位置

并发 64,输入 512:

设置 吞吐量 TTFT 中位数
--max-num-seqs 64 787 tok/s 5.7s(初次启动服务器,上述重复值为 842 tok/s,2.1~2.2s)
--max-num-seqs 16 608 tok/s 10.3s

当上限降低到 16 时,吞吐量下降了 23%,TTFT 变为了 1.8 倍。如果上限低于容量(约 34 个),即使容量有剩余也会产生队列,这是得不偿失的;而即使高于容量,如果 KV 不足也没有好处。 上限是一个契合 KV 容量所能容纳的请求数左右的数值。

5. KV 8-bit:容量增加时,长输入的延迟会减少

测试 bf16 KV fp8 KV
并发 1,输入 512(吞吐量,ITL) 66 tok/s, 15ms 66 tok/s, 15ms
并发 16,输入 2048(吞吐量) 207 tok/s 303 tok/s (+46%)
并发 16,输入 2048(TTFT 中位数) 4.0s 0.94s (−76%)
并发 32,输入 2048(吞吐量) 213 tok/s 299 tok/s (+40%)
并发 32,输入 2048(TTFT 中位数) 12.5s 7.0s (−44%)
KV 精度 注意力后端 困惑度 相较于 bf16
bf16 自动选择 (FLASH_ATTN) 17.7775 基准
bf16 FLASH_ATTN(固定,重复 2 次) 17.7796, 17.7796 +0.01%
bf16 FLASHINFER 17.7750 -0.01%
bf16 TRITON_ATTN 17.7732 -0.02%
fp8 FLASHINFER 17.4228 -2.0%
fp8 TRITON_ATTN 17.0340 -4.2%

6. 前缀缓存:存在公共前缀时,首个 Token 的速度可提升 14 倍

我们将 3,000 个 Token 的公共前缀附加每个请求不同的 32 个 Token 尾部,按顺序逐个发送(输出 1 个 Token)。

配置 首次请求 TTFT 后续请求 TTFT 中位数 完全没有前缀的 3,032 个 Token
启用前缀缓存(默认) 394ms 28ms 395ms
--no-enable-prefix-caching 395ms 393ms 393ms

7. 分块预填充(Chunked Prefill):长输入暂停解码的时间由分块大小决定

在运行一个正在解码的请求(300 个 Token)期间,插入 3 个 3,500 个 Token 的输入请求,并测量插入区间的 Token 间隔。

--max-num-batched-tokens 插入区间最大 ITL 插入区间中位数 ITL 长输入本身的 TTFT
512 69ms 53ms 481~494ms
默认(确认值为 2,048) 227ms(重复 3 次分别为 227、226、226) 14ms 466~480ms
8192(输入为 3,500 个 Token,因此实际生效的分块为 3,500) 445ms 14ms 461~474ms

8. 当关闭 CUDA Graph 时(--enforce-eager)

配置 服务器准备时间 并发 1 吞吐量 并发 32 吞吐量 ITL 中位数(并发 1)
默认(CUDA Graph) 71 秒(首次启动,包含 19 秒编译),后续重启 35~55 秒 66 tok/s 831 tok/s 15ms
--enforce-eager 30 秒 64 tok/s 814 tok/s 15ms

对于这种规模的模型,CUDA Graph 对速度的提升微乎其微,仅约 3%(并发 1 时 66 对比 64),而服务器准备时间缩短了约 25~40 秒。推测这是因为在每个 Token 耗时 14~15ms 的模型中,内核启动间隔在整体中所占的比例较小。我们曾写过在更小的模型中收益可能会更大,用 Qwen3-0.6B 重复 3 次相同的测试后,事实确实如此。在并发 1 下,CUDA Graph 默认的 168 tok/s(ITL 6ms)对比 --enforce-eager 108 tok/s(ITL 9ms), Graph 快了 55%,而在并发 32 下,为 1,557 对比 1,503 tok/s,差异约为 3.6%。服务器准备时间为 55 秒对 30 秒。推测这是因为模型较小,当单 Token 时间达到 6ms 级别时,内核启动间隔构成了整体的大头(推测)。我们未能测试更快的 GPU(未实测)。在更大的模型 Qwen3-4B-FP8 中,并发 1 下 Graph 默认的 49 tok/s 对比 --enforce-eager 47 tok/s,收益约为 4%(ITL 19ms 对比 20ms)。Graph 的收益仅在单 Token 耗时很短的极小模型中显著,而在 1.7B 及以上模型中,均稳定在 3~4% 左右。

9. 服务器首次启动所需的时间

根据服务器日志的时间戳读取的数值。

阶段 数值
进程启动与模块加载(直至引擎初始化开始) 约 15~17 秒
权重加载(从磁盘到 GPU) 4~6 秒(当模型文件已存在时)
引擎初始化(编译、性能分析、KV 缓存分配、CUDA Graph 捕获、预热) 首次启动 29 秒(包含 19 秒编译),若有编译缓存则为 7.5 秒
准备就绪总计 首次启动 71 秒,重启 35~55 秒(开启 LoRA 的服务器为 70 秒)

下载模型的时间已在上表中剔除(独立计算)。在自动扩缩容(Autoscaling)中,新 Pod 能够接收流量所需的时间需在此数字上叠加镜像下载和模型下载的时间,因此, 预热副本与 模型缓存卷的必要性从数字上显现了出来。

10. 运维实操要点

  1. 并发上限 = KV 容量 ÷ 每个请求的 Token 数。 公式源自第 3 节,依据源自第 4 节。超出上限后,吞吐量不会增加,只有 TTFT 会增加。
  2. 分别关注 TTFT p95 与 ITL p99。 即使中位数正常,尾部延迟也很大(ITL p99 达 223ms)。
  3. 通过架构设计来制造公共前缀。 将系统提示词和文档置于前方,将各请求的内容置于后方,前缀缓存能带来 14 倍的收益。
  4. KV 8-bit 不是吞吐量选项,而是容量选项。 在长输入且队列很长时效果显著。请先进行质量评估。
  5. 分块大小是流畅度与输入处理速度之间的权衡。 对话服务设小,批处理设大。
  6. 性能测试应确保每次输入各不相同,并与硬件极限进行对比。

11. 面试常见问题

外部依据:通过论文和官方资料确认的内容

本文中的所有数值均为实测值。我们查阅了报告相似现象的外部资料,核对数值与条件,并记录了其与我们结果的关系。由于外部数据的模型、GPU 和负载不同,我们 没有进行直接对比,而是仅判定其方向与结构的一致性。

主题 资料及确认位置 确认的内容与条件 与本文的关系
KV 大小公式 Kwon 等人,PagedAttention, SOSP 2023, arXiv 2309.06180,第 3 节及表 1 OPT-13B 每个 token 的 KV 为 2(键值)× 5,120(隐藏层大小)× 40(层)× 2(字节)= 800KB。在 A100 40GB 上,减去 26GB 的权重,将 12GB 用于 KV,则为 15.7K 个 token,与 12GiB ÷ 800KiB 的算术结果相符 支持。 与本文公式(第 3 节)相同
现有系统的 KV 浪费 同上论文第 1 节及图 2 实际以 token 状态写入的 KV 比例为 20.4%~38.2%。这是作者亲自实现的三个 Orca 变体的平均值,不包含 FasterTransformer 相关。以 16 个 token 为单位进行块管理(本文中确认的默认值)是减少这种浪费的方法
块大小与 token 间延迟 Agrawal 等人,Sarathi-Serve, OSDI 2024, arXiv 2403.02310,第 4.3 节、图 6 及第 5.4.1 节 块(token 预算)越小,重复时间越短,token 间延迟就越低;在计算受限区间内,重复时间与 token 数量呈线性关系。在块大小为 512 时,总输入处理时间最多增加约 25%(Yi-34B,张量并行度 2),而在 2,048 时几乎可以忽略不计 支持。 与第 7 节的方向相同。由于模型和 GPU 不同,我们无法对 TTFT 的增幅(3~5%)进行规模对比
并发性与吞吐量的形态 同上论文第 3.1 节及图 3(Mistral-7B, 单张 A100, 输入 1,024) 批处理使解码吞吐量几乎呈线性增长,对输入处理几乎没有影响 有限支持。 与第 21 篇短上下文(在并发达 16 时为 14~15 倍)的形态相同。第 4 节中输入为 512 的表格饱和得更早
超出容量时的延迟暴增 PagedAttention 第 6.2 节 当请求率超过系统容量时,队列会无休止地变长,导致延迟暴增 支持。 与第 4 节中 TTFT 呈 8.6 倍增长的现象一致
容量耗尽前吞吐量趋于平缓的情况 Recasens 等人,《Mind the Memory Gap》,arXiv 2503.08311,第 3~5 节 在 H100 上使用 OPT-2.7B 等模型时,即使在大批次下也停留在内存瓶颈处,批次大小为 1 的 225 tok/s 在批次大小为 256 时仅达到 7,607 tok/s(约为 33.8 倍,而非预期的 256 倍) 有限支持。 证明第 4 节中的性能趋缓可能不仅仅是 KV 容量问题所致的依据
KV 8 位的质量 vLLM 官方博客《The State of FP8 KV-Cache and Attention Quantization in vLLM》(2026-04-22) 在未经校准的 FP8(缩放比例 1.0)下,Qwen3-30B-A3B-Thinking-2507 的推理任务准确率最多下降 1~2 分,Qwen3.5-27B 最多下降 0.7 分(H100) 支持。 文献中的变化虽小,但取决于模型和校准。我们的模型(1.7B)及评估请参考第 5 节
KV 8 位(整数) LMDeploy 文档(INT8 KV, OpenCompass) llama2-7b-chat 的 MMLU 从 35.64 变为 35.58,internlm2.5-chat-7b 的 MMLU 从 72.30 变为 72.27。由于这是 INT8 而非 FP8,因此无法直接套用 相关。与 vLLM 的 --kv-cache-dtype fp8 方式不同
抢占(Preemption)方式 vLLM 官方文档(优化页面)、v0.30.0 scheduler.py,PagedAttention 第 4.5 节和第 7.3 节 V1 的抢占默认采用重新计算而非交换,它会归还被牺牲请求的所有块,将计算出的 token 数量重置为 0,并放回队列头部。论文说明在小块大小下重新计算是高效的 支持。 与第 4 节的抢占描述一致(抢占后前缀缓存是否命中未进行验证)
默认值 v0.30.0 config/cache.py 默认开启前缀缓存,块大小为 16 支持。 与在 0.31.0 中直接读取的值(开启、16、块预算 2,048)一致

供应商及项目博客(vLLM、LMDeploy)的数值,仅搬运了正文中写明条件的部分。由于无法打开 ACM 的 PagedAttention 页面,故遵循了 arXiv 摘要页面的表述。

未能确认的事项

确认的版本与日期

截至 2026-10-07。vLLM 0.31.0, PyTorch 2.13 (CUDA 13), Qwen3-1.7B (bf16)。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论