​作者​:昇腾实战派
​知识地图​:https://blog.csdn.net/Lumos_Lovegood/article/details/161601003

1. 背景介绍

在大模型服务化部署与性能评估领域,Aisbench与Evalscope作为业界主流的开源性能测试工具,已广泛应用于模型推理性能验证场景。其中,Aisbench 作为华为官方推荐使用的测试工具,凭借其对 vLLM 等主流推理框架的深度支持、稳态性能分析能力及可视化特性,已在多个内部及客户项目中得到验证与应用。

近期,我们发现,针对同一 vLLM 服务实例(使用相同输入与输出长度配置),Aisbench 测得的 TPOT(Time Per Output Token,每输出一个 Token 所需时间)性能指标显著优于 Evalscope 测试结果。

TPOT 作为衡量大模型推理性能的核心指标之一,直接影响着实际业务场景中的推理效率与资源规划。当前差异现象不仅关系到工具选择的准确性,更涉及性能评估标准的理解深度与工具推荐的可信度。为确保一致、准确的性能评估,亟需系统性地分析并定位 Aisbench 与 Evalscope 在 TPOT 测试结果上存在差异的根本原因。

2. 定位记录

2.1 准备测试复现脚本

在此报告中,使用DeepSeek-V4-Flash-w8a8-mtp模型作为样例进行测试,vLLM-Ascend 使用0.21.0rc1版本镜像,服务部署脚本如下:

export OMP_PROC_BIND=false
export OMP_NUM_THREADS=10
export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True
export LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libjemalloc.so.2:$LD_PRELOAD
export HCCL_BUFFSIZE=1024
export VLLM_ASCEND_APPLY_DSV4_PATCH=1
export VLLM_ASCEND_ENABLE_FLASHCOMM1=1
export TASK_QUEUE_ENABLE=1
export HCCL_OP_EXPANSION_MODE="AIV"

vllm serve /home/weights/DeepSeek-V4-Flash-w8a8-mtp \
    --max_model_len 133120 \
    --max-num-batched-tokens 8192 \
    --served-model-name dsv4 \
    --gpu-memory-utilization 0.9 \
    --max-num-seqs 48 \
    --data-parallel-size 1 \
    --tensor-parallel-size 8 \
    --enable-expert-parallel \
    --tokenizer-mode deepseek_v4 \
    --tool-call-parser deepseek_v4 \
    --enable-auto-tool-choice \
    --reasoning-parser deepseek_v4 \
    --safetensors-load-strategy 'prefetch' \
    --profiler-config '{"profiler": "torch", "torch_profiler_dir": "./vllm_profile", "torch_profiler_with_stack": true}' \
    --no-enable-prefix-caching \
    --model-loader-extra-config='{"enable_multithread_load": "true", "num_threads": 128}' \
    --quantization ascend \
    --port 8900 \
    --block-size 128 \
    --hf-overrides '{"use_index_cache": true, "index_topk_freq": 4}' \
    --speculative-config '{"num_speculative_tokens": 3,"method": "mtp","enforce_eager": true}' \
    --compilation-config '{"cudagraph_mode": "FULL_DECODE_ONLY"}'\
    --async-scheduling \
    --additional-config '{
        "ascend_compilation_config":{
            "enable_npugraph_ex":true,
            "enable_static_kernel":false
        },
        "enable_cpu_binding": true,
        "enable_dsa_cp": true,
        "multistream_overlap_shared_expert":true
    }'
2.1.1 Evalscope 性能测试

Evalscope 最初使用的是 ShareGPT 数据集,后来更换使用和 Aisbench 相同的 GSM8K 数据集。输入长度 5120,输出长度 512,测试脚本如下:

from evalscope.perf.main import run_perf_benchmark
from evalscope.perf.arguments import Arguments

task_cfg = Arguments(
    parallel=[1,2,4],
    number=[16,32,64],
    model='dsv4',
    url='http://0.0.0.0:8900/v1/chat/completions',
    api='openai',
    dataset='openqa',
    dataset_path='/home/benchmark/ais_bench/datasets/gsm8k/test.jsonl',
    min_tokens=512,
    max_tokens=512,
    prefix_length=0,
    min_prompt_length=5120,
    max_prompt_length=5120,
    tokenizer_path='/home/weights/DeepSeek-V4-Flash-w8a8-mtp',
    extra_args={'ignore_eos': True}
)

results = run_perf_benchmark(task_cfg)

print(results)
2.1.2 Aisbench 性能测试

Aisbench 测试同样使用 GSM8K 数据集,同步修改 benchmark/ais_bench/benchmark/configs/models/vllm_api/vllm_api_stream_chat.py文件中的相关配置,测试命令:

ais_bench --models vllm_api_stream_chat --datasets gsm8k_gen_0_shot_cot_str_perf  --debug --summarizer default_perf --mode perf --num-prompts ${TOTAL_REQ}

其中 ${TOTAL_REQ} 为并发数的 16 倍,即请求 16 轮。

2.2 性能收集

在输入相同、输出 token 数相同的情况下,对比 Aisbench 和 Evalscope 的测试结果,整体趋势如下:

  • TTFT 上两者耗时一致,相差较小;
  • TPOT 上两者差异较大,Evalscope 的 TPOT 明显高于 Aisbench(约为其两倍),即 Aisbench 的 TPOT 指标更优。

2.3 输入输出分析

因 TPOT 性能数据差异较大,首先查看两个工具输出的 token 情况:

在这里插入图片描述

对测试结果进行整理,发现 Aisbench 和 Evalscope 测试时输入的请求内容相同,输出内容前 10+ 个 token 相同,无法看出其他有用信息。

2.4 vLLM 日志分析

观察推理时的 vLLM 服务日志,查看在 Avg generation tokens 速率和 MTP 采信率上是否存在差异。

  • 单并发下,Evalscope 测试时,MTP 采信率大概在 65%~73% 之间,同时 Avg generation tokens 在 25 个左右,与 TPOT 数据能对应:

    在这里插入图片描述

  • 单并发下,Aisbench 测试时,MTP 采信率大概在 60%~78% 之间,同时 Avg generation tokens 在 40 个左右,与 TPOT 结果能对应:

    在这里插入图片描述

Evalscope 和 Aisbench 在 MTP 采信率上并无差异,但在整体 Avg generation tokens 生成速率上,却存在较大差异,因此需要采集 Profiling 分析原因。

3. 采集分析 Profiling

vLLM-Ascend 自带 profiling 采集功能,在 vLLM 启动命令中添加 --profiler-config '{"profiler": "torch", "torch_profiler_dir": "./vllm_profile", "torch_profiler_with_stack": false}' \即可。具体使用可以查看官方指导。

在此文档中,因为需要查看具体的堆栈信息,所以需要将 "torch_profiler_with_stack": 改为 true。

在 Aisbench 或 Evalscope 执行性能测试的时候,我们通过执行脚本完成 profiling 的采集:

curl -X POST http://localhost:8900/start_profile
sleep 5
curl -X POST http://localhost:8900/stop_profile

采集完之后进行解压:

from torch_npu.profiler.profiler import analyse
analyse("./vllm_profile/localhost.localdomain_*_ascend_pt/")

解压完成后通过 MindstudioInsight 工具 进行 profiling 的查看。

3.1 Profiling 耗时分析

在 Profiling 对比中,发现两个工具的耗时差异主要体现在单次 decode 及 MTP 阶段:

Evalscope 单次 decode(含 MTP)耗时较高:

在这里插入图片描述

其中 MTP 阶段耗时占比较高:

在这里插入图片描述

Aisbench 单次 decode(含 MTP)耗时相对较低,明显低于 Evalscope:

在这里插入图片描述

其中 MTP 阶段耗时占比相对较低:

在这里插入图片描述

综合来看,两者主模型耗时基本相当,耗时差异主要集中在 MTP 推理阶段。这里我们先将 MTP 特性关闭,观察 TPOT 性能数据是否一致。

3.2 MTP 性能影响验证

关闭 MTP 后,排除 MTP 对性能的影响,分别使用 Aisbench 和 Evalscope 工具测试性能,观察性能数据。

在 vLLM 服务中删除 --speculative-config '{"num_speculative_tokens": 3,"method": "mtp","enforce_eager": true}' \,即关闭 MTP。

关闭 MTP 后的测试结果显示,Aisbench 和 Evalscope 的 TTFT 与 TPOT 表现一致,因此可以将问题定界在 MTP 推理特性上。

3.3 MTP 阶段 profiling 分析

MTP 推理目前采用的是 enforce_eager 模式,推理过程中会存在 Free 空泡。观察 Aisbench 和 Evalscope 的 Free 耗时占比:

Aisbench MTP 阶段推理耗时情况:

在这里插入图片描述

Evalscope MTP 阶段推理耗时情况:

在这里插入图片描述

对比发现,Aisbench 和 Evalscope 的耗时差异主要在 Free 耗时阶段,Evalscope 的 Free 耗时明显高于 Aisbench(多出数十毫秒)。

从 “Communication (Not Overlapped)” 耗时为 0 可以看出,通信耗时已被计算耗时掩盖,因此这里不统计。

观察 Evalscope 在 CPU 上的算子下发情况,发现在 MTP 推理开始前存在一大段的 copy 耗时(占比明显偏高),导致后续 MTP 推理算子需要临时下发,产生 Free 空泡:

在这里插入图片描述

在这里插入图片描述

而 Aisbench 测试时,则没有这一段 copy 耗时,保证 MTP 推理算子早早下发,减少 Free 空泡:

在这里插入图片描述

在这里插入图片描述

这说明,Aisbench 工具测试和 Evalscope 工具测试,在 vLLM 推理时走的代码逻辑存在不一致,因此需要根据 profiling 堆栈信息进入代码查看。

代码定位与验证

观察 profiling 堆栈信息,发现两者在 /vllm-workspace/vllm/vllm/v1/sample/logits_processor/builtin.py:apply_with_spec_decode 方法上存在差异:

Aisbench 测试时,在这个方法上没有走任何逻辑:

在这里插入图片描述

Evalscope 测试时,则存在大段的 copy 耗时:

在这里插入图片描述

代码添加打点与验证

走读代码,发现在方法最开始的地方,就会判断 self.min_toks 参数,如果它是 None,则会直接 return,不进行任何处理:

## builtin.py:apply_with_spec_decode

def apply_with_spec_decode(
    self,
    logits: torch.Tensor,
    num_draft_tokens: list[int],
) -> torch.Tensor:
    """Spec-decode version of apply().
    Priority: ``min_tokens`` > ``stop_token_ids`` / EOS.
    Example: ``num_draft_tokens = [2, 3, 1]``
      → ``logits`` shape ``[6, V]``, ``cumsum = [0, 2, 5, 6]``
      → request 0 owns rows 0‑1, request 1 rows 2‑4, request 2 row 5.
    """
    if not self.min_toks:		# ← 猜测Aisbench测试时,self.min_toks为None,直接return
        return logits		

    num_draft_arr = np.array(num_draft_tokens, dtype=np.int64)
    cumsum = np.concatenate([[0], np.cumsum(num_draft_arr)])
	## ...
	## 省略剩余代码

在 apply_with_spec_decode 方法最前段添加 print(self.min_toks) 验证猜测:

Aisbench 测试,self.min_toks 为空字典:

在这里插入图片描述

Evalscope 测试,self.min_toks 不为空:

在这里插入图片描述

因此可以判定,Aisbench 工具和 Evalscope 工具在测试时,self.min_toks 不一致,导致 decode 耗时存在差异。

询问 deepwiki,走读代码调用栈发现,请求中的 min_tokens 参数会影响 MinTokensLogitsProcessor 类的 self.min_toks 参数的更新逻辑:

在这里插入图片描述

self.min_toks 参数更新逻辑代码调用栈

其中 Aisbench 请求未设置 min_tokens 参数,所以 self.min_toks 始终为空字典;而 Evalscope 请求中将 min_tokens 设置为 512,self.min_toks 参数不为空。

将 Evalscope 测试代码中的 min_tokens 参数设置为 None,重新测试,观察测试结果,两个工具测得的性能数据能对齐。

经验总结

在大模型服务性能评估实践中,不同测试工具(如 Aisbench 与 Evalscope)虽均遵循行业标准规范,但因统计逻辑、采样策略或测试参数的细微差异,可能导致同一服务的 TPOT(Time Per Output Token)等核心指标出现差异。作为业界开源且广泛验证的测试工具,其统计方法在设计原则上应保持合理性和一致性。当遇到此类指标差异时,建议通过以下方法进行系统性排查:

  1. vLLM 日志深度分析:验证 Avg Generation Tokens 速率与 TPOT 的正相关性(即生成速率越高,TPOT 值越低,性能越优)。若测试指标为 TTFT(Time to First Token),则需同步关注 Avg Prompt Tokens 速率与 TTFT 的关联性,确保指标计算逻辑一致。
  2. MTP 特性专项验证:如服务启用了多请求处理(MTP)特性,需重点核查 MTP 采信率是否异常。MTP 采信率波动可能导致并发性能统计偏差,影响 TPOT 计算结果。
  3. 请求参数一致性校验:在 vLLM 服务端捕获并比对两个测试工具的完整客户端请求参数(包括输入序列长度、输出序列长度、温度参数、top_p 等关键配置),确保测试条件完全一致。
  4. 系统级性能剖析:通过 profiling 工具采集深度性能数据,分析关键算子耗时分布、计算与 CPU 资源调度、请求队列处理等环节,精准定位性能瓶颈所在。

通过以上四步结构化排查方法,可有效识别测试工具差异的根本原因,确保性能评估结果的客观性与可比性。此方法论已通过多个客户现场验证,能显著提升性能差异定位效率,为模型服务优化提供可靠依据。

附录:min_tokens 逻辑

min_tokens 参数能确保在允许输出结束符(如 EOS 或 stop_token_ids)出现之前,每个输出序列必须生成的最小 token 数量。当存在 ignore_eos=True 参数时,不需要指定 min_tokens 参数,也能达到最大的 max_tokens 输出长度。

当存在这个参数时,vLLM 中的逻辑会是:

  1. 当 MTP(speculative decoding)特性开启时,只有 MinTokensLogitsProcessor 会被加载,其他 logits processors 会被禁用 init 方法。因为 speculative decoding 需要特殊的处理逻辑来确保 draft tokens 的验证正确性:

    在这里插入图片描述

  2. 在 RejectionSampler 中,MinTokensLogitsProcessor 通过 apply_with_spec_decode 方法被特殊调用:

    在这里插入图片描述

  3. apply_with_spec_decode 方法会根据 draft tokens 的数量和位置,对尚未达到最小 token 数的请求的 stop token 进行掩码处理,具体流程为:

    3.1. 计算 draft token 分布: 将 num_draft_tokens 转换为 numpy 数组,并计算累积和,确定每个请求在 logits tensor 中的行范围。例如:num_draft_tokens = [2, 3, 1] → cumsum = [0, 2, 5, 6],表示请求 0 占用行 0-1,请求 1 占用行 2-4,请求 2 占用行 5;

    在这里插入图片描述

    3.2. 筛选需要处理的请求:从 self.min_toks 中提取有 stop_token_ids 的请求条目;

    在这里插入图片描述

    3.3. 计算掩码位置:对每个请求,计算还需要生成多少 token 才能达到 min_tokens,然后确定需要掩码的 draft token 位置数量;

    在这里插入图片描述

    3.4. 构建掩码索引:对需要掩码的位置,构建行索引和 token 索引,使用 np.repeat 和 np.tile 进行扩展;

    在这里插入图片描述

    3.5. 应用掩码:将所有需要掩码的位置的 logits 设置为 -inf,阻止模型在这些位置生成 stop token;

    在这里插入图片描述

  4. 非 MTP(speculative Decoding)模式下,所有内置的 logits processors 都会被加载,MinTokensLogitsProcessor 使用标准的 apply 方法而不是 apply_with_spec_decode 方法,因此当不开启 MTP 时,Aisbench 和 Evalscope 测试的 TPOT 结果并无差异:

    在这里插入图片描述

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐