状态:草稿(2026-10-27),实际测量。 于 2026-10-27 使用单张 8GB 级笔记本 GPU(Ada 架构)、vLLM 0.31.0、Qwen3-Reranker-0.6B 进行测量。相同三大功能的原理和 1.7B 模型的首次测量记录在《LLM KV 缓存剖析》第 2 篇和《Toss ML/AI Infra 准备》第 14 篇中,因此本文仅探讨前几篇文章未涉及的内容(重复测量、关闭功能的基线、关闭 chunk、cache_salt)。 2026-10-27 新增。 在第 6-1 节中补充了同一天使用 vLLM 0.32.0 再次运行相同测试的结果。0.31.0 的数值保持不变。 编写规则:以实际测量数据为基础,无法测量的内容标记为“未测量”,未确认的内容标记为“未确认”。
目录
- 1. 为什么要重新测量
- 2. 环境与方法
- 3. 连续批处理:关闭后吞吐量降至 1/10
- 4. chunked prefill:解码暂停的时间与 chunk 大小成正比
- 5. 前缀缓存:重复请求仍为 18.9ms
- 6. cache_salt:即使前缀相同,租户之间也不共享
- 6-1. 2026-10-27 重新测量:0.32.0 中是否相同
- 7. 运维经验总结
- 8. 未能确认的内容
- 参考
1. 为什么要重新测量
PagedAttention、连续批处理和前缀缓存的原理在“服务引擎内部的 KV 缓存”中讨论过,而使用 1.7B 模型首次测得的数值在“使用小模型直接重新测量 vLLM”中进行了探讨。当时的测量存在三个漏洞。
- 每个条件只测量了一次,因此不知道其离散程度。
- 无法关闭连续批处理,因此未能展示“如果没有它会怎样”。
- 未观察完全关闭 chunk 的情况,以及多用户共享缓存时的隔离性(cache_salt)。
这一次,所有条件都重复了三到四次,第一组作为预热被单独剔除,未计入中位数。关闭功能的服务器在同一天、同一个 Pod 中紧接着启动。
2. 环境与方法
- GPU:单张 8GB 级笔记本 GPU(Ada 架构),驱动程序 595.99。容器内存限制 4GiB。
- vLLM 0.31.0 官方容器(PyTorch 2.13.0,CUDA 13.0),日志显示的注意力后端为 FLASH_ATTN。
- 模型:Qwen3-Reranker-0.6B,bf16,权重 1.12GiB。28 层,8 个 KV 头,头维度 128,因此单个 token 的 KV 为 2 × 28 × 8 × 128 × 2 字节 = 114,688 字节。这与日志中的“KV 4.3GiB 对应 40,208 个 token”相符。
- 公共服务器选项:
--max-model-len 4096 --gpu-memory-utilization 0.80。其余为默认值。在此版本中,如果 GPU 内存小于 70GB,服务器会将max_num_batched_tokens的默认值设为 2,048,将max_num_seqs设为 256(在源码vllm/engine/arg_utils.py中确认)。 - 客户端:在同一个 Pod 内使用 aiohttp 发送流式请求,测量首个 chunk 到达时间(TTFT)以及 chunk 之间的间隔(ITL)。输入是由 30 个英文单词随机拼接而成的文本,输出长度固定为
ignore_eos。 - 重复性:负载测试针对每个并发级别发送 4 组(第一组用于预热),取其余 3 组的中位数;前缀测试则使用 5 个新前缀,分别发送 1 次冷请求和 4 次热请求。
3. 连续批处理:关闭后吞吐量降至 1/10
没有直接关闭连续批处理本身的选项。相反,如果使用 --max-num-seqs 1 让每个步骤只运行一个请求,它就变成了“逐个处理请求的服务器”。我们以并发度 c 发送了 2c 个输入 256 token、输出 128 token 的请求。
| 服务器 | 并发请求数 | 吞吐量 (tok/s) | TTFT 中位数 | ITL 中位数 | ITL p99 |
|---|---|---|---|---|---|
单次处理(--max-num-seqs 1) |
1 | 174.4 | 18.9ms | 5.6ms | 6.2ms |
| 单次处理 | 16 | 174.6 | 11,008ms | 5.6ms | 6.5ms |
| 默认 | 1 | 174.2 | 18.8ms | 5.6ms | 6.7ms |
| 默认 | 4 | 593.8 | 50.7ms | 6.4ms | 7.5ms |
| 默认 | 16 | 1,683.1 | 131.0ms | 8.2ms | 9.4ms |
| 默认 | 64 | 2,899.3 | 272.2ms | 16.6ms | 90.6ms |
三组的吞吐量在 0.3% 的误差范围内保持一致(并发 16 默认值:1,681.3、1,683.1、1,685.4)。
- 对于只处理单个请求的服务器,即使提高并发度,吞吐量也保持不变。 由于解码的每一个步骤主要涉及从内存中读取全部权重,因此无论运行一个请求还是十六个请求,步骤耗时都没有太大差异。在默认服务器中,并发 16 时的 token 间隔为 8.2ms,仅比单独运行时(5.6ms)长 1.5 倍,但吞吐量却达到了 9.6 倍。
- 代价是等待时间。 在单次处理服务器中,并发 16 个请求的首个 token 中位数为 11 秒。这是因为它们必须排队等待前一个请求完成。
- 在并发 64 时,ITL p99 飙升至 90.6ms。这是因为新请求的 prefill 插入到了解码步骤之间,而下一节中的 chunk 大小决定了这一尾部延迟。
4. chunked prefill:解码暂停的时间与 chunk 大小成正比
在运行一个解码请求(900 个 token)的同时,连续插入五次约 3,700 个 token 的输入,并测量了插入区间内解码 token 间隔的最大值。
| 服务器 | 单步输入 token 上限 | 插入区间的最大 ITL(5 次) | 其余 ITL 中位数 | 插入输入的 TTFT 中位数 |
|---|---|---|---|---|
--max-num-batched-tokens 512 |
512 | 35.6ms(35.5, 35.5, 35.6, 35.6, 35.6) | 5.7ms | 232.0ms |
| 默认 | 2,048 | 109.8ms(108.3, 108.6, 109.1, 109.2, 109.8) | 5.7ms | 216.2ms |
--no-enable-chunked-prefill --max-num-batched-tokens 4096 |
4,096(无 chunk) | 197.6ms(191.8, 195.7, 197.1, 197.4, 197.6) | 5.8ms | 214.1ms |
在此 GPU 上,prefill 速度约为在 174.7ms 内处理 3,200 个 token,即每秒约 18,000 个 token。计算表明,一个 2,048 token 的 chunk 耗时约 111ms,整个 3,700 token 耗时约 200ms,这与表中的最大 ITL 相吻合。五次测量值汇聚在 1ms 左右,绝非巧合。
- 较小的 chunk 可以保护解码过程,但会稍微延迟 prefill。 512 token 的 chunk 将插入输入的 TTFT 从 216ms 增加到 232ms(增加了 7%),但将解码的最大暂停时间减少了三分之一。
- 关闭 chunk 会使行为接近 V0 时代的表现。 此时,
max_num_batched_tokens必须大于等于max_model_len服务器才能启动(与文档中的警告一致)。 - 在并发 16 负载(输入 256 token)下,512 token 的 chunk 并未改变吞吐量(1,682.7 对比 1,683.1 tok/s),TTFT 中位数从 131.0ms 降至 86.3ms,但 ITL p99 从 9.4ms 增至 22.9ms。在短输入密集的负载下,chunk 大小只是一个用于选择“减少哪种尾部延迟”的旋钮,并非免费的性能提升。由于未查看调度日志,该差异的原因未确认。
其形态与使用 1.7B 模型测得的旧值(512 token 时 69ms,2,048 token 时 227ms)相同,但大小随模型计算量的减少而减小。
5. 前缀缓存:重复请求仍为 18.9ms
我们在 3,000 个单词(约 3,200 个 token)的公共前缀后附加了每请求各不相同的 32 个单词进行发送。更换前缀并运行了五组测试。
| 服务器 | 冷启动首个请求 TTFT(5 个前缀) | 热请求 TTFT(20 次) |
|---|---|---|
| 默认(开启前缀缓存) | 中位数 174.7ms(174.6~175.3) | 中位数 18.9ms(最小 18.0,最大 19.9) |
--no-enable-prefix-caching |
中位数 173.9ms,仅服务器首个请求为 236.0ms | 中位数 173.8ms(最小 173.0,最大 178.2) |
由于热请求只需计算 3,200 个 token 中的最后一个块和尾部,因此速度提高了 9.2 倍。关闭前缀缓存的服务器的第一个请求为 236ms,比其他冷请求慢 60ms,这就是服务器启动后的预热开销。这就是为什么必须在测量中将第一个值单独剔除的原因。
还要记住缓存是以块(block)为单位的。由于 vLLM 只对充满的块(默认 16 个 token)进行哈希和存储,如果公共部分在块边界处断开,最后一片将重新计算。从 0.11 版本开始,哈希默认使用 sha256,并且可以通过 --prefix-caching-hash-algo 进行更改。
6. cache_salt:即使前缀相同,租户之间也不共享
前缀缓存存在一个安全问题。如果发送与其他用户发送的文档相同的前端部分,且首个 token 返回较快,就可以推断出“有人已经看过这个文档了”。这是一种通过时间差窃取信息的定时攻击。vLLM 允许在请求中附加 cache_salt ,将该值混合到第一个块的哈希中,从而确保只有相同 salt 的请求才能共享缓存。
--enable-prompt-tokens-details 通过让响应返回缓存的 token 数量,并发送相同 3,000 单词前缀但更换 salt 共五组来验证这一点。
一组包含五个具有相同前缀的请求。其中使用 salt a 发送两次,salt b 发送两次,不带 salt 发送一次。
| 顺序 | cache_salt | 缓存的 token(响应中的 cached_tokens) |
TTFT 中位数(第 2~5 组) |
|---|---|---|---|
| 1 | tenant-a | 0 | 173.9ms |
| 2 | tenant-a | 3,184 或 3,200 | 17.9ms |
| 3 | tenant-b | 0 | 173.7ms |
| 4 | tenant-b | 3,184 或 3,200 | 18.3ms |
| 5 | 无 | 0 | 173.9ms |
- 相同 salt 之间复用了 3,200 个 token 中直到块边界的部分(作为 16 的倍数的 3,184 或 3,200 个 token)。
- 如果 salt 不同,即使前缀连一个字符都不差,也无法使用缓存,耗时也与冷请求相同。不带 salt 的请求同样无法使用带 salt 请求的缓存。
- 第一组的首个请求为 228.5ms,之后的请求为 27.6ms,比其他组慢。这是服务器启动后首个请求的预热开销。
cached_tokens 必须将服务器以 --enable-prompt-tokens-details 启动才会显示在响应中。如果关闭,该字段将为空,只能通过时间来推测缓存命中。整个服务器的命中情况可以通过 /metrics 的 vllm:prefix_cache_hits_total 和 vllm:prefix_cache_queries_total 来查看。
6-1. 2026-10-27 重新测量:0.32.0 中是否相同
使用相同的 GPU、相同的重排序模型以及相同的客户端,在 vLLM 0.32.0 官方容器中重新进行了测量(“2026-10-27 0.31.0 测量 → 0.32.0 重新测量”)。
| 项目 | 0.31.0 | 0.32.0 |
|---|---|---|
| 吞吐量,并发 1 | 174.2 tok/s | 174.2 tok/s |
| 吞吐量,并发 16(默认) | 1,683.1 tok/s | 1,683.6 tok/s |
| 吞吐量,并发 16(一次一个) | 174.6 tok/s | 174.3 tok/s |
| 吞吐量,并发 64 | 2,899.3 tok/s | 2,899.7 tok/s |
| 3,700 个 token 插入时的解码最大停顿(默认分块) | 109.8ms | 108.2ms |
| 3,200 个 token 前缀的首个 token(冷 → 热) | 174.7 → 18.9ms | 175.2 → 18.9ms |
所有数值在 1.5% 的误差范围内保持一致。0.32.0 版本发布说明中的重大更改(扩展 Model Runner V2、移除级联注意力、 max_tokens 不在上下文中提前剔除等)在这个小模型的默认负载下没有改变数据。对话用的 Qwen3-0.6B(相同结构)在并发 1、16、64 下也保持一致,分别为 174.1、1,678.0、2,898.7 tok/s。
7. 运维层面的经验总结
- 如果是并发请求较多的服务,连续批处理(Continuous Batching)贡献了绝大部分的吞吐量。
max_num_seqs调低该参数虽然能节省 KV 内存,但会直接削减这部分收益。 - 如果解码的长尾延迟很重要,就减小
max_num_batched_tokens;如果长输入的 TTFT 很重要,就将其增大。在此 GPU 上,单个分块的时间大致等于分块 token 数除以每秒 18,000。只要测量一次自己 GPU 的预填充速度,就可以根据延迟目标反推并选择分块大小。 - 如果多个客户共享一个引擎,可以按客户维度附加
cache_salt。同一客户内部的复用得以保留,同时仅防止客户间的时序泄漏。 - 在测量时,排除启动服务器后的第一个请求,并多次运行相同条件以记录离散程度。这次同样发现,第一个请求比其他值慢 35%。
8. 未验证的事项
- 大模型、多 GPU、实际对话数据(前缀长度和复用间隔混合的负载)下的数值尚未实际测量。
- 在 512 token 分块下短输入负载的 ITL p99 增加的原因,由于未查看调度日志而未确认。
- 虽然确认了版本发布说明中提到 0.28.0 中大 GPU 的默认
max_num_batched_tokens从 8,192 提高到了 16,384,但尚未在该规模的 GPU 上进行实际测量。
参考
- Kwon 等人,“Efficient Memory Management for Large Language Model Serving with PagedAttention”,SOSP 2023。在相同延迟下,相比 FasterTransformer 和 Orca,吞吐量提升 2~4 倍(基于摘要)。https://arxiv.org/abs/2309.06180
- Yu 等人,“Orca: A Distributed Serving System for Transformer-Based Generative Models”,OSDI 2022。https://www.usenix.org/conference/osdi22/presentation/yu
- Agrawal 等人,“Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve”,arXiv 2403.02310。在长尾延迟约束下,单张 A100 运行 Mistral-7B 的服务容量是 vLLM 的 2.6 倍(基于摘要)。https://arxiv.org/abs/2403.02310
- vLLM 文档,Optimization and Tuning (Chunked Prefill),Automatic Prefix Caching 设计 (cache_salt):https://docs.vllm.ai/en/latest/configuration/optimization.html,https://docs.vllm.ai/en/latest/design/prefix_caching.html
- 先前测量:推理引擎内部的 KV 缓存,用小模型重新测试 vLLM