vLLM内存分配
vLLM 的 PageAttention 并不是为每个请求“预留出模型限定的长度的整个内存空间”,而是 按需分配 KV Cache 的“页面”(pages)。
- KV Cache (Key-Value Cache): 在 Attention 机制中,每个 token 的 Key (K) 和 Value (V) 向量在计算 Attention 时会被缓存起来,避免重复计算。这些缓存是推理过程中内存消耗的主要部分。
- PageAttention 的精髓:不定长分配,虚拟内存式管理。
- 传统方式 (非 PageAttention): 很多推理框架会为每个请求预先分配一个固定大小的 KV Cache 缓冲区,这个大小通常就是模型的
max_model_len(例如 32K Token)。这意味着即使你只输入了 10 个 token,也会预分配 32K token 的空间。这导致大量的内存浪费,尤其是在处理大量短请求时。 - vLLM 的 PageAttention: 它将 KV Cache 划分为固定大小的“页面”(pages)。当一个请求进入时,vLLM 不会立即分配整个 32K 的空间。它只会分配当前输入所需要的页面数量。随着生成过程的进行,如果需要更多空间来存储新的 KV 缓存,它会按需动态地分配新的页面。
- “预留”的概念: 这里的“预留”更多指的是 逻辑上的最大上限。vLLM 知道每个请求最多可以扩展到 32K token 的长度,但它不会在物理内存中一次性分配 32K 这么大的一个连续块。它通过页面的方式,将这些 KV Cache 分散存储在 GPU 内存中,并且可以在多个请求之间共享物理内存页面(如果它们恰好有空闲页面)。
- 传统方式 (非 PageAttention): 很多推理框架会为每个请求预先分配一个固定大小的 KV Cache 缓冲区,这个大小通常就是模型的
vLLM 为每个请求 逻辑上支持 到模型限定的最大长度,但 物理内存的分配是按需和按页进行的。它避免了传统方法中对短请求的巨大内存浪费,从而显著提高了 GPU 内存的利用率和吞吐量。
生成token预留
对于生成过程:
- 输入阶段 (Prompt Processing): 当用户输入
N个 token (例如 “你是谁” 是 3 个 token) 时,vLLM 会为这N个 token 分配相应的 KV Cache 页面。 - 生成阶段 (Token Generation):
- 每生成一个新的 token,这个新 token 的 KV 向量也需要存储到 KV Cache 中。
- vLLM 会 按需分配新的 KV Cache 页面 来存储这些新生成的 token 的 KV 向量。
- 这个过程会一直持续,直到:
- 模型生成了结束符 (EOS token)。
- 生成达到用户指定的最大生成长度 (e.g.,
max_new_tokens=50)。 - 整个序列的长度 (输入 + 生成) 达到了模型的最大上下文长度 (例如 32K token)。
所以,对于生成,vLLM 也是按需分配,直到达到最大上下文长度。 如果模型最大支持 32K token,用户输入了 10 个 token,那么理论上还可以生成 32K - 10 = 31990 个 token。vLLM 会在生成过程中,每生成一个 token,就为其分配所需的 KV Cache 空间,直到达到这个上限。
模型分桶 (Model Bucketing)
为什么要分桶?
- 性能优化 (吞吐量): 尽管 vLLM 的 PageAttention 已经很高效,但模型本身在处理不同长度的序列时,计算量还是有差异的。对于短请求,如果它们都涌向一个配置为 128K 长度的模型,虽然内存浪费小了,但可能还是不如一个专门为短请求优化的模型(例如 32K 上下文)来得快。
- 资源利用率: 如果所有请求都使用最大上下文的模型,即使是处理短请求,也可能因为模型内部的一些固定开销而效率不高。分桶可以确保不同长度的请求使用最适合它们的模型实例。
- 成本控制: 运行一个支持 128K 上下文的模型通常需要更多的 GPU 显存和计算资源。如果大多数请求都是短的,那么为它们分配一个较小的模型可以节省资源。
如何分桶?
- 可以根据请求的输入长度或预期的输出长度,将其路由到不同
max_model_len配置的模型实例。 - 例如,一个服务可能部署了两个
deepseek-r1模型实例:- 实例 A: 配置
max_model_len=8K,处理短请求。 - 实例 B: 配置
max_model_len=32K,处理中等请求。 - 实例 C: 配置
max_model_len=128K,处理长请求。
- 实例 A: 配置
- 当一个新请求到达时,根据其输入长度或预估总长度,将其发送到合适的实例。
- 可以根据请求的输入长度或预期的输出长度,将其路由到不同
性能测试指标
普通产品的性能指标一般看平均响应时间、QPS 这些。大模型的交互方式不一样,光看这些不够,还得把首 token、吐字率和输入输出长度拉出来单独看。
吐字率(平均输出 token 速率/s):大模型问答通常是流式输出,不会等整段答案算完才返回,而是算出 token 就往外吐 token(有些系统会合并几个 token 再发)。模型“思考”本来就慢,如果界面一直没动静,用户很容易以为服务卡死了。所以流式场景下,每秒能吐多少 token 是核心指标。单位时间内吐出的 token 越多,decode 侧性能越好。 首 token 耗时(从用户发送问题到返回第一个 token 的时间):吐字率一般会排除首 token。第一个 token 和后续 token 不是同一段计算路径,首 token 主要卡在 prefill。它直接影响用户的第一感知,几十秒不出第一个字,体验基本就崩了。 QPM:这个指标比较容易理解,类似 QPS。只是大模型单次问答耗时太长,按秒统计波动很大,所以更常按分钟看。
其他指标:
输入平均 token 数量(input tokens):输入越长,首 token 耗时通常越长,吐字率也可能被拖下去。压测时必须对齐测试数据长度。长文本场景可以按长度分组,比如 16k32k、32k48k、48k~64k,以此类推。
输出平均 token 数量(output tokens):输出长度会直接影响 QPM。有些模型答案和思考过程很克制,有些模型一开口就停不下来。对比两个模型时,要先确认输出长度是不是同一量级。
Prefill 与 Decode
上面的推理过程可以拆成两个阶段:prefill(预填充)和 decode(解码)。prefill 负责计算 K 矩阵、V 矩阵并生成首 token,K、V 会保存到 cache 中;decode 则用 Q 矩阵从 KV cache 里取 K 和 V,计算注意力和特征,再一个 token 一个 token 地返回给用户。Q、K、V 这里先不用钻太深,粗略理解就是:Q 对应当前问题,K 用来算上下文里哪些 token 更该被关注(权重),V 提供最终生成时要用的特征。
prefill 和 decode 阶段要明确以下几点:
- prefill 和 decode 阶段是可以各自优化的(后面会说 PD 分离)
- 通常首 token 是 prefill 阶段算出来的,decode 阶段负责输出其余 token。所以首 token 耗时评估 prefill,吐字率评估 decode。后面做单点优化时,要先分清该盯首 token 还是盯吐字率。
- prefill 针对相同的用户问题,算出来的 k、v 一定是一模一样的。kv 会缓存到 cache 中,decode 再从 cache 里拉 k、v 去计算。一个优化方向是把缓存做成分布式(模型实例多,如果能共享 KV cache,收益会很直接)。也可以继续用本地缓存,但上层路由要做一致性哈希,保证相同前缀的请求路由到同一个 prefill 节点。缓存命中做的是前缀匹配,不要求用户问题完全一样;哪怕只有前几个 token 一样,这几个 token 也可以复用缓存里的 kv,不用重新计算。
除了精度测试,第二类测试场景就是 prefill 优化。常见做法是优化 KV cache,比如原来 cache 放在 GPU 显存里,可以改到内存里来省显存。测试方法大体还是前面那套,但有几个点要单独注意:
- 正常性能测试中,为了避免命中 KV cache,通常会在测试数据最前面加一个 uuid。只要前缀匹配不上,就不会命中缓存。测试 KV cache 优化时则要把 uuid 去掉,专门验证命中缓存后的收益。PS:decode 阶段没法缓存,每次都要重新生成 token,所以 KV cache 优化理论上不会明显提升吐字率。
- 有些问题输出很长,比如数学题的思考过程可能跑几分钟、十几分钟甚至几十分钟。当前场景关注 prefill 和首 token,不评价 decode 吐字率。为了压缩测试时间,可以把
max_tokens设小,限制输出长度。 - 测试方法可以使用同样的数据发送两次,验证第二次命中缓存后相比第一次节省了多少时间,但这只能作为基准测试,我们仍需评估上线后的缓存命中率和收益情况。因为:
- 线上模型是分桶的,每个桶还有 N 个模型实例。如果 KV cache 保存在本地,想命中就必须把相同前缀(可以是前 N 个字符)的请求路由到同一个模型实例,同时还要保证负载均衡,不能把某个实例打爆。调度策略会直接决定缓存命中率。
- 线上每次命中多少 token 的前缀缓存并不确定,可能只命中前 100 个 token,也可能命中 1W 个 token。不用真实数据,很难评估真实收益。
- 缓存有上限。我们粗算下来,1K 的 KV cache 差不多占 75M 内存(这是我们环境里的表现)。长文本比如 100K,一条请求就会占 7.5G。线上流量一大,内存很快会被沾满,只能淘汰老的 KV cache,原本可能命中的数据也会因为被淘汰而无法命中。
P/D 分离
P/D 分离是一年多前才开始有规模推广的优化方案。原本 prefill 和 decode 都在同一个模型实例里,问题是优化 prefill 可能影响 decode,优化 decode 也可能反过来影响 prefill。拆开之后,一份用户请求先由 prefill 实例计算 KV cache 和首 token,再传给 decode 实例继续生成后续 token,由 decode 把答案返回给用户。
P 和 D 分开后,就可以分别优化,理论上性能空间更大。但 P/D 分离也把系统复杂度直接拉上来了:
- P 和 D 之间的通信是否可靠,上层路由调度服务是否稳定可靠。如果它们之间的通信出问题,要怎么处理,这些都需要故障测试。比如 P 挂了,按设计 D 节点要降级成不分离方案继续服务;D 挂了,P 也要能降级。如果 P 和 D 都没挂,但 P/D 之间出现网络分区,调度器要能感知到,并选择一个实例降级成不分离方案继续提供服务。
- P 和 D 的数量配比要进行测试:1P/1D, 1P/3D , 2P/4D, 10P/10D 等等
- P 和 D 除了要有 NP/ND 的配比外,还需要分不同的 group。每个 group 下都有 NP/ND,防止一个 group 出问题时其他 group 还能继续服务;group 本身也可以作为分桶。比如 1 号和 2 号 group 是 16k 以下短文本桶,3 号和 4 号 group 是 16k 到 48k 长文本桶,5 号和 6 号 group 是 48k 到 128k 超长文本桶。测试人员要在多 group 形态下验证性能和调度策略:比如 KV cache 走前缀匹配,但第一个问题是长文本,第二个问题是短文本,而且命中了第一个问题的前缀,那调度系统应该优先前缀命中,还是坚持长文本走大桶、短文本走小桶?这个需要提前讨论清楚。
- P 和 D 降级后的压测也要做。因为 P 和 D 要保证对方出问题后,自己能降级成不分离方案继续服务。它们的启动参数不同,降级后的性能表现也可能不同。
- P 和 D 的任务积压情况要时刻关注。不同 P/D 数量配比下,可能是 P 的算力先到极限,导致首 token 指标激增;也可能是 D 先被打满。所以要测出不同配比下最合适的并发上限。模型必须限频,超过并发就拦,否则就是把模型往死里压。
MTP
MTP:多头预测,也是明显影响吐字率的加速方案。前面说过,普通推理是用用户问题预测下一个 token,再用用户问题 + 这个 token 预测再下一个 token,每次只算一个 token。MTP 则尝试一次算多个 token。原理很复杂,这里用大白话说一下:
- 增加一个小模型(可能只有几层神经网络),让大模型预测 token A1,再把 A1 给 MTP 小模型,让小模型用 A1 预测下一个 token A2’(A2 后面的一撇表示这是 MTP 小模型预测的)。
- 再让大模型正常预测 token A2,让 MTP 小模型根据 A2 预测 token A3’,然后用算法校验 A2 和 A2’。这是在验证 MTP 小模型的准确性。毕竟 MTP 小模型可能只有 1 层神经网络,算不准很正常;如果它预测的 A2’ 通过校验,就可以继续相信它预测的 A3’。
- 这样大模型就不用再预测 A3,直接使用小模型算出的 A3’ 即可。一个 MTP 小模型每次可以多预测一个 token,3 个 MTP 小模型就可以多预测 3 个 token,吐字率会被明显拉高。
MTP 的收益取决于小模型准确度。它可能只有几层神经网络,如果算不准、过不了校验,最后还是得回到大模型计算,反而引入额外开销。所以测试时要重点关注 MTP 小模型的准确度。
注意:牢记 MTP 是提升吐字率的加速方案,测试时要注意首 token 不能出现波动,根据 MTP 小模型的数量来预期应该提升多少吐字率。
MTP 的核心思想:
- 传统自回归推理: 逐个生成 token,每次生成一个 token 后,将新的序列作为输入,再次进行模型推理。这个过程是串行的,每次推理都涉及整个上下文。
- MTP (多头预测/ speculative decoding / assisted generation / look-ahead decoding): 模型在生成当前 token 的同时,预测未来可能的几个 token。这些预测可以是:
- 并行生成多个候选 token: 模型内部可能在同一个时间步尝试生成多个可能的下一个 token。
- 草稿模型/小模型预测: 使用一个更小、更快的模型(草稿模型/draft model)快速预测未来几个 token 的序列。
- 验证机制: 主模型(大模型)对草稿模型预测的多个 token 进行一次性验证。如果验证通过,这些 token 就被接受;如果验证不通过,则从第一个不通过的 token 处回退,然后主模型进行正常的自回归生成。
多头预测在实际场景中使用的多吗?
用得不少,也是当前大语言模型推理加速里很常见的一条线。
在当前主流的 LLM 推理加速文献中,更常见的名称是:
- Speculative Decoding (推测解码/草稿解码):这是目前最流行和最有效的 MTP 变体。它使用一个较小的 “草稿模型” (draft model) 快速生成一个短序列的候选 token,然后用一个较大的 “主模型” (main model) 并行验证这些 token。如果验证成功,可以一次性接受多个 token,从而显著提高吞吐量。
- Assisted Generation (辅助生成):与 Speculative Decoding 类似,通常指使用某种辅助机制来加速生成。
- Look-ahead Decoding (前瞻解码):广义上也可以指 MTP,即在当前时间步尝试预测未来的 token。
为什么它被广泛使用?
- 显著的加速效果: Speculative Decoding 在保持生成质量不变的情况下,可以实现 2-3 倍甚至更高的推理速度提升。
- 保持生成质量: 关键在于,最终输出的序列与不使用 MTP(即纯自回归解码)时主模型生成的序列是 完全一致的(或者说,是数学上等价的)。这是因为它有一个验证步骤,只有主模型确认的 token 才会被接受。
- 通用性强: 适用于各种自回归大语言模型,无需修改模型架构。
- 硬件友好: 虽然涉及到多次前向传播,但草稿模型的计算量很小,而主模型的验证可以并行进行,有效利用了 GPU 的并行计算能力。
实际应用案例:
- Google 的 MedLM (PaLM 2-S): Google 在其 AI 产品中广泛使用了 Speculative Decoding。
- Hugging Face
transformers库: 已经原生支持 Speculative Decoding,用户可以轻松地在自己的模型上启用。 - 各大模型推理框架和库: 如 vLLM、DeepSpeed-MII 等高性能推理框架都在积极集成和优化 Speculative Decoding。
- 开源社区和研究领域: 围绕 Speculative Decoding 的优化和改进(如如何选择草稿模型、如何更有效地验证等)是当前热门的研究方向。
TP/DP/EP/PP 并行加速
并行加速是行业里很常见的优化方法。模型大到一张 GPU 卡装不下,用户输入也可能动不动上百 K,真实场景下推理很慢。我们希望把模型按某种策略拆成 N 个部分,让 N 张 GPU 分别加载,每张 GPU 只计算完整神经网络的一部分,从而做并行计算、加速推理。
如果每台机器是 8 块 GPU 卡,那么 P/D 分离至少要保证 2 台机器 16 张 GPU 卡。并且这 16 张卡中最多只有 8 张卡做 prefill、8 张卡做 decode,这是 P/D 分离架构限制死的,也就是说 P/D 分离最多就是 TP8。性能测试一定要做对比:跟历史版本比,跟竞品比,sglang 和 vllm 之间也要比。最常见的是 P/D 分离和不分离的对比。P/D 不分离方案可以做 TP16;即便同样是 TP8,两台机器的所有 GPU 也都可以算 prefill 阶段。一般不分离方案还是 prefill 优先,也就是 prefill 能抢到更多算力来计算 KV cache 和首 token。
P/D 分离方案天然就是对首 token 不友好的,在同样的模型下,P/D 分离理论上在首 token 上是打不过非分离方案的,尤其会发现并发到达一定程度后,首 token 会激增,这是因为 P 的算力到达了极限,开始排队了。 但 P/D 分离在吐字率上是可以胜过非分离方案的。
evalscope 模型推理性能压测
本地Transformer推理
CUDA_VISIBLE_DEVICES=0 evalscope perf \
--parallel 1 \
--model Qwen/Qwen2.5-0.5B-Instruct \
--attn-implementation flash_attention_2 \
--log-every-n-query 5 \
--connect-timeout 6000 \
--read-timeout 6000 \
--max-tokens 2048 \
--min-tokens 2048 \
--api local \
--dataset speed_benchmark \
--debug
本地vLLM推理
CUDA_VISIBLE_DEVICES=0 evalscope perf \
--parallel 20 \
--model '/root/resume_summary/Qwen3-4B' \
--log-every-n-query 10 \
--connect-timeout 6000 \
--read-timeout 6000 \
--min-prompt-length 1024 \
--max-prompt-length 2048 \
--api local_vllm \
--dataset random
evalscope perf \
--model '/root/resume_summary/Qwen3-4B'\
--number 20 \
--parallel 30 \
--api local_vllm \
--dataset openqa \
--tokenizer-path '/root/resume_summary/Qwen3-4B' \
--number 1000
测试日志:
radom数据集默认测试
2025-07-21 15:49:11,603 - evalscope - INFO - {
"Time taken for tests (s)": 524.3913,
"Number of concurrency": 1,
"Total requests": 20,
"Succeed requests": 20,
"Failed requests": 0,
"Output token throughput (tok/s)": 49.6633,
"Total token throughput (tok/s)": 50.7789,
"Request throughput (req/s)": 0.0381,
"Average latency (s)": 26.2191,
"Average time to first token (s)": 0.0254,
"Average time per output token (s)": 0.0201,
"Average input tokens per request": 29.25,
"Average output tokens per request": 1302.15,
"Average package latency (s)": 0.0201,
"Average package per request": 1302.15
}
Processing: 100%|█████████████████████████████████████████████████████████████████████████████████████| 20/20 [08:44<00:00, 26.22s/it]
2025-07-21 15:49:11,675 - evalscope - INFO -
Benchmarking summary:
+-----------------------------------+-----------+
| Key | Value |
| ---- | ----- |
| | |
| Time taken for tests (s) | 524.391 |
| ------------------------ | ------- |
| | |
| Number of concurrency | 1 |
| --------------------- | ---- |
| | |
| Total requests | 20 |
| -------------- | ---- |
| | |
| Succeed requests | 20 |
| ---------------- | ---- |
| | |
| Failed requests | 0 |
| --------------- | ---- |
| | |
| Output token throughput (tok/s) | 49.6633 |
| ------------------------------- | ------- |
| | |
| Total token throughput (tok/s) | 50.7789 |
| ------------------------------ | ------- |
| | |
| Request throughput (req/s) | 0.0381 |
| -------------------------- | ------ |
| | |
| Average latency (s) | 26.2191 |
| ------------------- | ------- |
| | |
| Average time to first token (s) | 0.0254 |
| ------------------------------- | ------ |
| | |
| Average time per output token (s) | 0.0201 |
| --------------------------------- | ------ |
| | |
| Average input tokens per request | 29.25 |
| -------------------------------- | ----- |
| | |
| Average output tokens per request | 1302.15 |
| --------------------------------- | ------- |
| | |
| Average package latency (s) | 0.0201 |
| --------------------------- | ------ |
| | |
| Average package per request | 1302.15 |
| --------------------------- | ------- |
| | |
2025-07-21 15:49:11,685 - evalscope - INFO -
Percentile results:
| Percentiles | TTFT (s) | ITL (s) | TPOT (s) | Latency (s) | Input tokens | Output tokens | Output (tok/s) | Total (tok/s) |
| ----------- | -------- | ------- | -------- | ----------- | ------------ | ------------- | ----------
| 10% | 0.0244 | 0.0199 | 0.02 | 17.5403 | 21 | 876 | 49.4577 | 50.3642 |
| ---- | ------ | ------ | ---- | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
| 25% | 0.0248 | 0.02 | 0.02 | 20.1436 | 26 | 1003 | 49.6068 | 50.5037 |
| ---- | ------ | ---- | ---- | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
| 50% | 0.025 | 0.0201 | 0.0201 | 26.651 | 28 | 1325 | 49.7166 | 50.866 |
| ---- | ----- | ------ | ------ | ------ | ---- | ---- | ------- | ------ |
| | | | | | | | | |
| 66% | 0.0251 | 0.0202 | 0.0201 | 29.7055 | 31 | 1474 | 49.7925 | 51.108 |
| ---- | ------ | ------ | ------ | ------- | ---- | ---- | ------- | ------ |
| | | | | | | | | |
| 75% | 0.0265 | 0.0203 | 0.0201 | 31.8914 | 34 | 1580 | 49.9372 | 51.1559 |
| ---- | ------ | ------ | ------ | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
| 80% | 0.0265 | 0.0203 | 0.0202 | 33.7972 | 37 | 1682 | 49.9423 | 51.2321 |
| ---- | ------ | ------ | ------ | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
| 90% | 0.0277 | 0.0205 | 0.0203 | 41.5339 | 41 | 2048 | 50.0938 | 52.6426 |
| ---- | ------ | ------ | ------ | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
| 95% | 0.0278 | 0.0206 | 0.0203 | 41.6199 | 45 | 2048 | 50.2308 | 52.8591 |
| ---- | ------ | ------ | ------ | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
| 98% | 0.0278 | 0.0207 | 0.0203 | 41.6199 | 45 | 2048 | 50.2308 | 52.8591 |
| ---- | ------ | ------ | ------ | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
| 99% | 0.0278 | 0.0207 | 0.0203 | 41.6199 | 45 | 2048 | 50.2308 | 52.8591 |
| ---- | ------ | ------ | ------ | ------- | ---- | ---- | ------- | ------- |
| | | | | | | | | |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
2025-07-21 15:49:11,685 - evalscope - INFO - Save the summary to: outputs/20250721_153833/Qwen3-4B
2025-07-21 15:49:11,685 - evalscope - INFO - Terminating the child process...
INFO 07-21 15:49:11 [launcher.py:80] Shutting down FastAPI HTTP server.
[rank0]:[W721 15:49:12.630540875 ProcessGroupNCCL.cpp:1476] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
2025-07-21 15:49:14,001 - evalscope - INFO - Child process terminated.
radom 数据集1024-2048 input tokens测试
2025-07-21 17:00:41,554 - evalscope - INFO - {
"Time taken for tests (s)": 1554.3859,
"Number of concurrency": 1,
"Total requests": 50,
"Succeed requests": 50,
"Failed requests": 0,
"Output token throughput (tok/s)": 47.6079,
"Total token throughput (tok/s)": 99.9269,
"Request throughput (req/s)": 0.0322,
"Average latency (s)": 31.0872,
"Average time to first token (s)": 0.2238,
"Average time per output token (s)": 0.0208,
"Average input tokens per request": 1626.48,
"Average output tokens per request": 1480.02,
"Average package latency (s)": 0.0209,
"Average package per request": 1480.02
}
Processing: 100%|█████████████████████████████████████████████████████████████████████████████████████| 50/50 [25:54<00:00, 31.09s/it]
2025-07-21 17:00:41,726 - evalscope - INFO -
Benchmarking summary:
+-----------------------------------+-----------+
| Key | Value |
+===================================+===========+
| Time taken for tests (s) | 1554.39 |
+-----------------------------------+-----------+
| Number of concurrency | 1 |
+-----------------------------------+-----------+
| Total requests | 50 |
+-----------------------------------+-----------+
| Succeed requests | 50 |
+-----------------------------------+-----------+
| Failed requests | 0 |
+-----------------------------------+-----------+
| Output token throughput (tok/s) | 47.6079 |
+-----------------------------------+-----------+
| Total token throughput (tok/s) | 99.9269 |
+-----------------------------------+-----------+
| Request throughput (req/s) | 0.0322 |
+-----------------------------------+-----------+
| Average latency (s) | 31.0872 |
+-----------------------------------+-----------+
| Average time to first token (s) | 0.2238 |
+-----------------------------------+-----------+
| Average time per output token (s) | 0.0208 |
+-----------------------------------+-----------+
| Average input tokens per request | 1626.48 |
+-----------------------------------+-----------+
| Average output tokens per request | 1480.02 |
+-----------------------------------+-----------+
| Average package latency (s) | 0.0209 |
+-----------------------------------+-----------+
| Average package per request | 1480.02 |
+-----------------------------------+-----------+
2025-07-21 17:00:41,754 - evalscope - INFO -
Percentile results:
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| Percentiles | TTFT (s) | ITL (s) | TPOT (s) | Latency (s) | Input tokens | Output tokens | Output (tok/s) | Total (tok/s) |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| 10% | 0.1728 | 0.0206 | 0.0205 | 10.4894 | 1212 | 495 | 47.0958 | 79.2054 |
| 25% | 0.1876 | 0.0207 | 0.0207 | 17.4452 | 1343 | 825 | 47.3555 | 85.8073 |
| 50% | 0.2227 | 0.0209 | 0.0208 | 38.006 | 1664 | 1798 | 47.6273 | 93.9511 |
| 66% | 0.2551 | 0.021 | 0.0209 | 42.8781 | 1817 | 2048 | 47.7724 | 110.4161 |
| 75% | 0.2576 | 0.021 | 0.0209 | 43.0006 | 1865 | 2048 | 47.8362 | 130.1493 |
| 80% | 0.2601 | 0.0211 | 0.021 | 43.1035 | 1885 | 2048 | 47.9719 | 145.9114 |
| 90% | 0.2742 | 0.0211 | 0.021 | 43.2356 | 1986 | 2048 | 48.113 | 237.1679 |
| 95% | 0.2764 | 0.0212 | 0.021 | 43.2852 | 2006 | 2048 | 48.2052 | 916.1667 |
| 98% | 0.3366 | 0.0213 | 0.0212 | 43.6579 | 2332 | 2048 | 48.3238 | 1156.3212 |
| 99% | 0.3366 | 0.0214 | 0.0212 | 43.6579 | 2332 | 2048 | 48.3238 | 1156.3212 |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
2025-07-21 17:00:41,754 - evalscope - INFO - Save the summary to: outputs/20250721_163343/Qwen3-4B
2025-07-21 17:00:41,754 - evalscope - INFO - Terminating the child process...
INFO 07-21 17:00:41 [launcher.py:80] Shutting down FastAPI HTTP server.
[rank0]:[W721 17:00:42.531920666 ProcessGroupNCCL.cpp:1476] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
2025-07-21 17:00:43,670 - evalscope - INFO - Child process terminated.
30并发数:
2025-07-21 18:12:35,205 - evalscope - INFO - {
"Time taken for tests (s)": 159.331,
"Number of concurrency": 30,
"Total requests": 50,
"Succeed requests": 50,
"Failed requests": 0,
"Output token throughput (tok/s)": 441.9164,
"Total token throughput (tok/s)": 952.3255,
"Request throughput (req/s)": 0.3138,
"Average latency (s)": 70.9088,
"Average time to first token (s)": 4.5971,
"Average time per output token (s)": 0.049,
"Average input tokens per request": 1626.48,
"Average output tokens per request": 1408.22,
"Average package latency (s)": 0.0471,
"Average package per request": 1408.22
}
Processing: 100%|████████████████████████████████████████████████████████████| 50/50 [02:39<00:00, 3.19s/it]
2025-07-21 18:12:35,384 - evalscope - INFO -
Benchmarking summary:
+-----------------------------------+-----------+
| Key | Value |
+===================================+===========+
| Time taken for tests (s) | 159.331 |
+-----------------------------------+-----------+
| Number of concurrency | 30 |
+-----------------------------------+-----------+
| Total requests | 50 |
+-----------------------------------+-----------+
| Succeed requests | 50 |
+-----------------------------------+-----------+
| Failed requests | 0 |
+-----------------------------------+-----------+
| Output token throughput (tok/s) | 441.916 |
+-----------------------------------+-----------+
| Total token throughput (tok/s) | 952.326 |
+-----------------------------------+-----------+
| Request throughput (req/s) | 0.3138 |
+-----------------------------------+-----------+
| Average latency (s) | 70.9088 |
+-----------------------------------+-----------+
| Average time to first token (s) | 4.5971 |
+-----------------------------------+-----------+
| Average time per output token (s) | 0.049 |
+-----------------------------------+-----------+
| Average input tokens per request | 1626.48 |
+-----------------------------------+-----------+
| Average output tokens per request | 1408.22 |
+-----------------------------------+-----------+
| Average package latency (s) | 0.0471 |
+-----------------------------------+-----------+
| Average package per request | 1408.22 |
+-----------------------------------+-----------+
2025-07-21 18:12:35,409 - evalscope - INFO -
Percentile results:
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| Percentiles | TTFT (s) | ITL (s) | TPOT (s) | Latency (s) | Input tokens | Output tokens | Output (tok/s) | Total (tok/s) |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| 10% | 0.3015 | 0.0298 | 0.0413 | 28.8175 | 1212 | 335 | 13.2633 | 35.1993 |
| 25% | 0.3762 | 0.0403 | 0.0456 | 35.2627 | 1343 | 644 | 18.9711 | 37.195 |
| 50% | 3.3229 | 0.0449 | 0.0491 | 81.9319 | 1664 | 1813 | 19.3318 | 42.0375 |
| 66% | 5.1278 | 0.0471 | 0.05 | 99.0622 | 1817 | 2048 | 19.9106 | 46.6728 |
| 75% | 5.9427 | 0.0474 | 0.0504 | 105.2283 | 1865 | 2048 | 20.6739 | 54.8875 |
| 80% | 6.2452 | 0.0475 | 0.0507 | 106.4079 | 1885 | 2048 | 21.015 | 62.8967 |
| 90% | 7.2961 | 0.0479 | 0.0528 | 106.6621 | 1986 | 2048 | 23.6787 | 119.0351 |
| 95% | 23.5217 | 0.0481 | 0.0732 | 106.8906 | 2006 | 2048 | 24.1472 | 193.4843 |
| 98% | 31.062 | 0.0489 | 0.0972 | 106.9622 | 2332 | 2048 | 27.1139 | 203.4513 |
| 99% | 31.062 | 0.2826 | 0.0972 | 106.9622 | 2332 | 2048 | 27.1139 | 203.4513 |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
2025-07-21 18:12:35,409 - evalscope - INFO - Save the summary to: outputs/20250721_180852/Qwen3-4B
300并发数
+-----------------------------------+-----------+
| Key | Value |
+===================================+===========+
| Time taken for tests (s) | 171.463 |
+-----------------------------------+-----------+
| Number of concurrency | 300 |
+-----------------------------------+-----------+
| Total requests | 50 |
+-----------------------------------+-----------+
| Succeed requests | 50 |
+-----------------------------------+-----------+
| Failed requests | 0 |
+-----------------------------------+-----------+
| Output token throughput (tok/s) | 450.949 |
+-----------------------------------+-----------+
| Total token throughput (tok/s) | 925.244 |
+-----------------------------------+-----------+
| Request throughput (req/s) | 0.2916 |
+-----------------------------------+-----------+
| Average latency (s) | 104.164 |
+-----------------------------------+-----------+
| Average time to first token (s) | 10.4115 |
+-----------------------------------+-----------+
| Average time per output token (s) | 0.0801 |
+-----------------------------------+-----------+
| Average input tokens per request | 1626.48 |
+-----------------------------------+-----------+
| Average output tokens per request | 1546.42 |
+-----------------------------------+-----------+
| Average package latency (s) | 0.0606 |
+-----------------------------------+-----------+
| Average package per request | 1546.42 |
+-----------------------------------+-----------+
2025-07-21 18:26:47,167 - evalscope - INFO -
Percentile results:
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| Percentiles | TTFT (s) | ITL (s) | TPOT (s) | Latency (s) | Input tokens | Output tokens | Output (tok/s) | Total (tok/s) |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| 10% | 1.6911 | 0.0295 | 0.0532 | 36.4678 | 1212 | 399 | 8.1124 | 20.4868 |
| 25% | 3.0504 | 0.0461 | 0.0544 | 88.4334 | 1343 | 1098 | 12.7055 | 27.7091 |
| 50% | 6.2555 | 0.0474 | 0.0551 | 116.5973 | 1664 | 2048 | 16.963 | 33.0331 |
| 66% | 8.3319 | 0.0486 | 0.0594 | 117.0038 | 1817 | 2048 | 17.5325 | 35.8655 |
| 75% | 9.3036 | 0.049 | 0.0666 | 120.7334 | 1865 | 2048 | 17.5716 | 37.48 |
| 80% | 9.9621 | 0.0496 | 0.0712 | 141.4979 | 1885 | 2048 | 17.6667 | 43.7002 |
| 90% | 11.3036 | 0.05 | 0.1137 | 154.7253 | 1986 | 2048 | 17.9381 | 72.2866 |
| 95% | 14.5828 | 0.0502 | 0.1282 | 165.228 | 2006 | 2048 | 18.0516 | 115.6813 |
| 98% | 116.3881 | 0.2272 | 0.8727 | 171.3481 | 2332 | 2048 | 18.1686 | 145.1348 |
| 99% | 116.3881 | 0.3302 | 0.8727 | 171.3481 | 2332 | 2048 | 18.1686 | 145.1348 |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
2025-07-21 18:26:47,167 - evalscope - INFO - Save the summary to: outputs/20250721_182242/Qwen3-4B
025-07-21 18:30:37,015 - evalscope - INFO - {
"Time taken for tests (s)": 124.9001,
"Number of concurrency": 300,
"Total requests": 40,
"Succeed requests": 40,
"Failed requests": 0,
"Output token throughput (tok/s)": 458.2943,
"Total token throughput (tok/s)": 986.1163,
"Request throughput (req/s)": 0.3203,
"Average latency (s)": 92.9427,
"Average time to first token (s)": 10.0523,
"Average time per output token (s)": 0.0617,
"Average input tokens per request": 1648.125,
"Average output tokens per request": 1431.025,
"Average package latency (s)": 0.0579,
"Average package per request": 1431.025
}
Processing: 15%|████████▊ | 45/300 [02:29<18:56, 4.46s/it
300并发:
Benchmarking summary:
+-----------------------------------+-----------+
| Key | Value |
+===================================+===========+
| Time taken for tests (s) | 862.26 |
+-----------------------------------+-----------+
| Number of concurrency | 300 |
+-----------------------------------+-----------+
| Total requests | 300 |
+-----------------------------------+-----------+
| Succeed requests | 300 |
+-----------------------------------+-----------+
| Failed requests | 0 |
+-----------------------------------+-----------+
| Output token throughput (tok/s) | 521.695 |
+-----------------------------------+-----------+
| Total token throughput (tok/s) | 1094.9 |
+-----------------------------------+-----------+
| Request throughput (req/s) | 0.3479 |
+-----------------------------------+-----------+
| Average latency (s) | 453.685 |
+-----------------------------------+-----------+
| Average time to first token (s) | 364.146 |
+-----------------------------------+-----------+
| Average time per output token (s) | 0.0612 |
+-----------------------------------+-----------+
| Average input tokens per request | 1647.5 |
+-----------------------------------+-----------+
| Average output tokens per request | 1499.46 |
+-----------------------------------+-----------+
| Average package latency (s) | 0.0597 |
+-----------------------------------+-----------+
| Average package per request | 1499.46 |
+-----------------------------------+-----------+
2025-07-21 18:42:55,422 - evalscope - INFO -
Percentile results:
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| Percentiles | TTFT (s) | ITL (s) | TPOT (s) | Latency (s) | Input tokens | Output tokens | Output (tok/s) | Total (tok/s) |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
| 10% | 7.6619 | 0.0472 | 0.0548 | 119.9869 | 1193 | 444 | 0.9646 | 3.755 |
| 25% | 125.1115 | 0.0477 | 0.0567 | 239.9528 | 1371 | 891 | 2.1703 | 4.7951 |
| 50% | 356.3014 | 0.0484 | 0.0578 | 461.2453 | 1657 | 1883 | 3.2939 | 6.8899 |
| 66% | 474.6099 | 0.0491 | 0.059 | 585.7884 | 1849 | 2048 | 4.7121 | 9.9237 |
| 75% | 580.7949 | 0.0505 | 0.0621 | 670.6546 | 1920 | 2048 | 6.1084 | 12.6201 |
| 80% | 593.6266 | 0.0509 | 0.0639 | 708.4548 | 1972 | 2048 | 7.7167 | 15.7069 |
| 90% | 708.7958 | 0.0513 | 0.0723 | 799.7401 | 2062 | 2048 | 16.028 | 30.2447 |
| 95% | 763.0271 | 0.0518 | 0.084 | 820.0354 | 2132 | 2048 | 16.967 | 38.0616 |
| 98% | 789.6511 | 0.3434 | 0.1006 | 857.1797 | 2264 | 2048 | 17.1682 | 43.895 |
| 99% | 790.7011 | 0.3534 | 0.1368 | 861.3407 | 2340 | 2048 | 17.2395 | 50.9503 |
+-------------+----------+---------+----------+-------------+--------------+---------------+----------------+---------------+
2025-07-21 18:42:55,422 - evalscope - INFO - Save the summary to: outputs/20250721_182728/Qwen3-4B
各个指标含义:
| 指标 | 含义 |
|---|---|
| Number of concurrency | 并发数 |
Total requests | 一共跑了 50 个 prompt |
Output token throughput (tok/s) | 每秒生成多少个输出 token,约 47.6 tok/s |
Total token throughput (tok/s) | 每秒处理的总 token(输入+输出),约 99.9 tok/s |
Request throughput (req/s) | 每秒能完成多少个完整请求,0.032 req/s(非常慢) |
Average latency (s) | 每个请求从提交到生成完成平均耗时(含排队+生成),约 31.1 秒 |
Average time per output token (s) | 单个输出 token 平均生成耗时,约 20.8 ms/token |
Average time to first token (s) | 从提交请求到拿到第一个 token 的时间,约 0.22 秒 |
1. Percentiles (百分位数)
- 意义: 百分位数用来描述数据分布,在看延迟时尤其有用。它表示在某个百分位点上,有多少比例的请求延迟低于该值。
- 如何计算: 把所有测量值(比如所有请求的延迟)从小到大排序。某个百分位数
P的值,就是排序后位于P%位置的数据点。- P50 (50th percentile) / Median (中位数): 表示 50% 的请求延迟低于这个值。它比平均值更稳,不容易被离群值带偏。
- P90 (90th percentile): 表示 90% 的请求延迟低于这个值,用来看大多数用户体验到的偏慢情况。
- P95 (95th percentile): 表示 95% 的请求的延迟低于这个值。
- P99 (99th percentile): 表示 99% 的请求延迟低于这个值,通常用来盯尾延迟(tail latency)。
- 含义:
- 低百分位数(如 P50) 看典型体验。
- 高百分位数(如 P90, P95, P99) 看尾部体验和稳定性。高百分位延迟越低,服务越稳。
2. TTFT (s) - Time To First Token (秒)
- 意义: 从发送请求到收到第一个 token 的时间。
- 如何计算:
TTFT = (收到第一个输出令牌的时间) - (发送请求的时间) - 含义: 这是用户感知到的初始响应速度。TTFT 越低,模型越像是在及时响应;聊天、Agent 这类交互式应用尤其敏感。
3. ITL (s) - Inter-Token Latency (秒)
- 意义: 模型生成连续两个 token 之间的时间间隔。
- 如何计算:
ITL = (生成第二个令牌的时间 - 生成第一个令牌的时间)ITL = (生成第三个令牌的时间 - 生成第二个令牌的时间)…以此类推,然后取这些时间间隔的平均值或百分位数。 - 含义: ITL 反映后续输出的流畅性。ITL 越低,内容越像连续流出来;ITL 抖动大,用户会明显感觉卡顿。
4. TPOT (s) - Time Per Output Token (秒)
- 意义: 模型每生成一个输出 token 所需的平均时间。
- 如何计算:
TPOT = (总输出生成时间) / (总输出令牌数)其中,总输出生成时间通常是(收到最后一个输出令牌的时间) - (收到第一个输出令牌的时间)。 或者更严格地,对于单个请求:TPOT = (Total Latency - TTFT) / (Output Tokens - 1) - 含义: TPOT 可以理解成整个输出过程的平均 token 成本。TPOT 越低,每个 token 生成得越快。它和
Output (tok/s)近似互为倒数。
5. Latency (s) - Total Latency (秒)
- 意义: 从发送请求到收到模型生成的所有 token 所需的时间,也就是完整响应时间。
- 如何计算:
Total Latency = (收到最后一个输出令牌的时间) - (发送请求的时间) - 含义: 这是用户从提问到看到完整答案的总等待时间。需要完整回复才能进入下一步的场景,会更关注这个指标。
- 关系:
Total Latency ≈ TTFT + (Output Tokens - 1) * ITL(近似值,因为 ITL 通常是平均值)。
6. Input tokens
- 意义: 单个请求中提示(prompt)的 token 数量。
- 如何计算: 统计请求文本在模型分词器(tokenizer)下产生的 token 数量。
- 含义: 用来衡量输入长度。输入 token 越多,prefill 压力越大,通常会推高 TTFT 和 Total Latency。
7. Output tokens
- 意义: 单个请求中模型生成响应的 token 数量。
- 如何计算: 统计模型生成响应文本在模型分词器下产生的 token 数量。
- 含义: 用来衡量输出长度。输出 token 越多,Total Latency 越长;如果 ITL 稳定,
Output (tok/s)通常也会比较稳定。
8. Output (tok/s) - Output Tokens Per Second
- 意义: 模型每秒生成输出 token 的数量。
- 如何计算:
Output (tok/s) = (Output tokens) / (Total Latency - TTFT)或者Output (tok/s) = 1 / TPOT对整个测试会话,也可以看所有请求的Total num output tokens / 总生成时间。 - 含义: 这是衡量模型生成速度的关键指标。数值越高,内容生成越快。
9. Total (tok/s) - Total Tokens Per Second
- 意义: 模型每秒处理的总 token 数量,包括输入 token 和输出 token。
- 如何计算:
Total (tok/s) = (Input tokens + Output tokens) / Total Latency对整个测试会话,也可以看所有请求的(Total num prompt tokens + Total num output tokens) / 总运行时间。 - 含义: 这是衡量模型整体吞吐量的指标,反映系统处理给定负载的效率。数值越高,整体处理能力越强。
指标汇总:
| 参数 | 意义 | 理想值 | 影响因素 |
|---|---|---|---|
| Percentiles | 数据分布(如延迟的 P50, P90, P99) | 越低越好 (延迟) | 模型架构、硬件、负载、网络 |
| TTFT (s) | 首次响应时间 | 越低越好 | 模型大小、提示长度、硬件、网络 |
| ITL (s) | 连续令牌生成间隔 | 越低越好 | 模型架构、硬件、负载 |
| TPOT (s) | 每输出令牌时间 | 越低越好 | 模型架构、硬件、负载 |
| Latency (s) | 总响应时间 | 越低越好 | TTFT, ITL, 输出令牌数, 提示长度 |
| Input tokens | 输入提示的令牌数 | - | 用户输入长度 |
| Output tokens | 输出响应的令牌数 | - | 模型生成内容长度 |
| Output (tok/s) | 每秒输出令牌数 (生成速度) | 越高越好 | 模型架构、硬件、负载、输出令牌复杂度 |
| Total (tok/s) | 每秒总令牌数 (整体吞吐量) | 越高越好 | 模型架构、硬件、负载、输入/输出令牌总数 |
max_num_batched_seq 和 max_num_batched_tokens 配置
已有条件:Qwen3-4B,GPU 为 NVIDIA A10 24GB,输入 prompt 平均 1000 tokens,目标是找出 最合理的 max_num_batched_seq(最大并发请求数)和 max_num_batched_tokens(最大 token 数)配置。
一、核心思路
这两个参数背后的瓶颈是:KV Cache 在显存中所占空间。
显存消耗主要来自三个部分:
- 模型本体权重(不可变)
- 每个 token 的 KV cache(attention 缓存,持续增长)
- 临时显存(调度器 buffer、activation、显卡系统开销)
二、KV Cache 计算公式(近似)
每个 token 的 KV cache 占用量:
KV size ≈ num_layers × num_heads × head_dim × 2 × bytes_per_fp
对 Qwen3-4B(推测配置):
| 参数 | 值 |
|---|---|
层数 L | 32 |
注意力头数 H | 32 |
每头维度 D | 128 |
| 数据类型 | FP16 (2 bytes) |
| 单 token KV 空间 | 32×32×128×2×2 = 524,288 bytes ≈ 0.5 MB/token |
一个 token ≈ 0.5MB KV Cache
三、计算你能容纳多少 token?
A10 24GB 显卡,要预留空间:
- 模型参数:Qwen3-4B FP16 大概占 ~8GB(估算)
- buffer+activation:~4GB
- 系统保留+上下文处理开销:~2GB
剩余用于 KV Cache 的大概是: 24GB - 8GB - 4GB - 2GB = ~10GB
最多可支持的 tokens ≈ 10GB / 0.5MB ≈ 20,000 tokens
四、根据你的 prompt 情况来推算
- 输入 prompt ≈ 1000 tokens
- 假设输出也要 200 tokens
- 单请求 token ≈ 1200 tokens
最多能支持的请求数(即 batch 大小)为:20000 / 1200 ≈ 16.6 → 最大并发请求数 ≈ 16
五、推荐设置参数
| 参数 | 推荐值 | 理由 |
|---|---|---|
--max-num-batched-seq | 16 | 符合 A10 显存承受上限 |
--max-num-batched-tokens | 20480 | 16 请求 × 1280 tokens(富余一些) |
六、如果想更激进一点压榨显存:
可以尝试:
--max-num-batched-seq 20
--max-num-batched-tokens 24000
但你需要:
- 控制输出长度,避免生成 token 无限增长;
- 启用
--max-model-len限制上下文总长(如 2048); - 启用 swap-to-CPU(A10 没 NVLink,不建议频繁 swap);
- 在显存不够时,把暂时不用的中间张量(如 KV cache)转移到 CPU 内存里,等用到时再搬回来。
- A10 没有 NVLink:GPU <-> CPU 之间的数据传输只能走 PCIe 通道,带宽低、延迟高。
- swap 操作涉及 频繁搬运大块张量数据(比如几个 GB 的 KV cache),这在 PCIe 上非常慢,直接拖垮性能。
❗ 注意事项
- 如果你发现吞吐不高,优先关注日志中的:
GPU KV cache usage是否爆满。 - 建议配合
--disable-log-requests以避免调度器被打印卡住。 - A10 是消费级卡,吞吐和调度能力远不如 A100、H100,需要合理 batch 大小来避免队列长时间 pending。