Prhub

#51592 [Rust][Benchmark] Align speed-bench CLI flags with Python and add flag parity test

原始 PR 作者 esmeetu 合并时间 2026-08-20 16:50 文件变更 8 提交数 6 评论 6 代码增减 +406 / -16

执行摘要

Rust bench CLI 对齐 Python,新增一致性测试

PR body 明确指出:VLLM_USE_RUST_BENCH=1 委托(#50081)会用原始 argv 直接 exec vllm-bench,因此 Python 文档化的参数必须在 Rust CLI 中可解析。而 speed-bench 参数已经漂移:--speed-bench-output-len 在 Rust 中不存在,直接报 unexpected argument;缺失时输出长度静默回退到 --sharegpt-output-len → --random-output-len(默认 128,Python 为 4096),导致委托后的基准结果不可比。Slack 上已有用户反馈 vllm bench serve 拒绝 --speed-bench-output-len 而 --help=all 却列出它。另经复查,Rust 侧 /metrics 解析器早于 Python #41916 的修复,_created 时间戳序列与 label 值污染计数的同类潜在缺陷仍然存在。

值得精读。核心看点不是参数本身,而是“双端快照 + 双向白名单”的一致性测试设计:python_only_allowlist_is_not_stale 防白名单陈旧、python_only_entries_exist_in_snapshot 防白名单悬空,配合 Python 侧自动生成快照,形成低成本、可持续的跨语言 CLI 契约守护模式,可借鉴到其他双实现入口。parse_spec_decode_metrics 的“先取 metric 名再判 _total”也是处理 Prometheus 文本格式的教科书写法。

讨论亮点

四条有效评论均围绕细节而非方案本身,核心方案(快照 + 双向白名单)获得维护者明确认可:

  • codex(P1,testing):快照再生成命令用裸 python 可能绕开仓库虚拟环境(AGENTS.md 的 no-bare-python 约定),且错误命令会被写进快照头。结论:统一改为 .venv/bin/python,docstring、快照头、pytest 失败提示三处同步更新(commit 8ed085e)。
  • codex(P1,style):AGENTS.md 指令文件“只增不减”违反编辑守则,要求用合并/删减抵消新增内容。结论:commit “lean AGENTS.md” 将说明并入既有 Build & Test 段落。
  • claude[bot](documentation):--tokenizer-mode 的 clap doc 注释仍宣称支持 (auto, hf, slow, mistral),与实际“非 auto 忽略并告警”矛盾。结论:同 PR 内更新 help 文案。
  • BugenZhao(design):批准时评价 “The parity tests look quite clever! 😀”,并追加提交让快照变更触发 CI。

实现拆解

  1. CLI 参数对齐(rust/src/bench/src/cli.rs):为 BenchServeArgs 新增 --speed-bench-output-len 参数(usize,default_value_t = 4096,与 Python 默认值一致);同时把 Python 拼写的 --speed-bench-dataset-subset 挂为 speed_bench_config 的 visible_alias,委托模式下两种拼写均可解析,不再 unexpected argument。
  2. 数据流接线(rust/src/bench/src/config.rs + rust/src/bench/src/benchmark.rs):BenchConfig 新增 speed_bench_output_len 字段并在 from_args 中透传;run_benchmark 的 DatasetName::SpeedBench 分支改用 config.speed_bench_output_len 传给 load_speed_bench_dataset,删除原先 sharegpt_output_len.unwrap_or(random_output_len) 的静默回退,根除“默认 128 vs 4096”的可比性问题。
  3. 移植 spec-decode metrics 解析修复(rust/src/bench/src/benchmark.rs):把解析逻辑从 fetch_spec_decode_metrics 抽成纯函数 parse_spec_decode_metrics(text: &str):先取行首 token、在第一个 { 处截断得到 metric 名,要求以 _total 结尾,再按 metric 名分派累加。配套 3 个单元测试:跨 engine 求和、跳过 _created 系列与 label 值干扰、无 spec 指标返回 None。
  4. 双向 CLI 一致性测试(新增 rust/src/bench/tests/cli_parity.rs、tests/benchmarks/test_rust_bench_cli_parity.py、rust/src/bench/tests/python_serve_flags.txt):Rust 侧用 clap::CommandFactory 枚举全部 long flag 与别名,逐项与 Python 快照比对——每个 Python flag 必须是已知 flag/别名或 PYTHON_ONLY 白名单项(24 个刻意缺口:asr/bfcl/blazedit/spec-bench/timed-trace 数据集、绘图与 self-timed 等);另有两条反向测试防白名单“陈旧”(flag 已被支持仍未移除)与“悬空”(快照已删除仍保留)。Python 侧 pytest 从 vllm.benchmarks.serve.add_cli_args 实时收集 flag 与快照比对,并支持一键再生成。BugenZhao 的追加提交将测试挂入 .buildkite/test_areas/benchmarks.yaml。
  5. 透明化 --tokenizer-mode(rust/src/bench/src/config.rs + rust/src/bench/README.md):该参数仅为兼容 Python CLI 而接受,实际未生效(此前是死代码字段)。现在非 auto 值在配置期触发 tracing::warn,说明真实解析链:HF tokenizer.json → tiktoken → 服务端 /tokenize 回退(mistral_common 不支持本地分词),README 同步说明。
文件 模块 状态 重要度
rust/src/bench/src/benchmark.rs 基准引擎 modified 7.46
rust/src/bench/tests/cli_parity.rs 一致性测试 added 7.22
tests/benchmarks/test_rust_bench_cli_parity.py 快照同步 added 6.36
rust/src/bench/src/config.rs 基准配置 modified 6.23
rust/src/bench/src/cli.rs 参数定义 modified 5.43
rust/src/bench/tests/python_serve_flags.txt 参数快照 added 3.62
.buildkite/test_areas/benchmarks.yaml CI 配置 modified 2.36
rust/src/bench/README.md 使用文档 modified 2.17

关键符号

parse_spec_decode_metrics fetch_spec_decode_metrics run_benchmark BenchConfig::from_args python_serve_flags_parse_or_are_allowlisted python_only_allowlist_is_not_stale python_only_entries_exist_in_snapshot test_serve_flag_snapshot_is_current _current_flags test_speed_bench_flags_match_python

关键源码片段

rust/src/bench/src/benchmark.rs core-logic

核心逻辑改动:抽出 `parse_spec_decode_metrics` 纯函数并移植 #41916 修复,同时将 speed-bench 输出长度改为使用新参数,消除静默回退。

// rust/src/bench/src/benchmark.rs —— spec-decode 指标解析(移植 Python 侧 vllm#41916 修复)。
//
// 修复要点:
// 1. 用“花括号前的 metric 名”做匹配,label 值(如 engine="0")不再干扰判名;
// 2. 只读取以 `_total` 结尾的 counter 样本,跳过 `_created` 时间戳序列,
// 避免 unix 时间戳被错误累加进计数(改动前会整行 contains 匹配)。
​
/// 解析 Prometheus 文本格式中的 spec decode 计数器。
///
/// 返回 `None` 表示未启用 spec decode 或没有可用指标。
pub(crate) fn parse_spec_decode_metrics(text: &str) -> Option<SpecDecodeMetrics> {
    let mut num_drafts: u64 = 0;
    let mut num_draft_tokens: u64 = 0;
    let mut num_accepted_tokens: u64 = 0;
    let mut accepted_per_pos: HashMap<u64, u64> = HashMap::new();
    let mut found_spec_decode = false;
​
    for line in text.lines() {
        let line = line.trim();
        // 跳过空行与 `# HELP` / `# TYPE` 注释行
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        // 取行首 token,并在第一个 `{` 处截断得到纯 metric 名
        let first_token = match line.split_whitespace().next() {
            Some(t) => t,
            None => continue,
        };
        let metric_name = first_token.split('{').next().unwrap_or(first_token);
        // 只关心 spec_decode 且以 `_total` 结尾的 counter 系列
        if !metric_name.starts_with("vllm:spec_decode") || !metric_name.ends_with("_total") {
            continue;
        }
        found_spec_decode = true;
        let val = match line.split_whitespace().last().and_then(|s| s.parse::<f64>().ok()) {
            Some(v) => v as u64,
            None => continue,
        };
​
        // 注意匹配顺序:`num_accepted_tokens_per_pos` 必须在 `num_accepted_tokens` 之前,
        // 否则 per-position 系列会被前者截胡
        if metric_name.contains("num_drafts") {
            num_drafts += val;
        } else if metric_name.contains("num_draft_tokens") {
            num_draft_tokens += val;
        } else if metric_name.contains("num_accepted_tokens_per_pos") {
            // position 标签仍需从原始行解析:position="N"
            if let Some(start) = line.find("position=\"") {
                let start = start + "position=\"".len();
                if let Some(end) = line[start..].find('"')
                    && let Ok(pos) = line[start..start + end].parse::<u64>()
                {
                    *accepted_per_pos.entry(pos).or_insert(0) += val;
                }
            }
        } else if metric_name.contains("num_accepted_tokens") {
            num_accepted_tokens += val;
        }
    }
​
    if !found_spec_decode {
        return None;
    }
​
    Some(SpecDecodeMetrics {
        num_drafts,
        num_draft_tokens,
        num_accepted_tokens,
        accepted_per_pos,
    })
}
rust/src/bench/src/cli.rs core-logic

CLI 入口:新增 `--speed-bench-output-len`(默认 4096)并给 `--speed-bench-config` 加 `--speed-bench-dataset-subset` 可见别名,是委托场景解析的关键。

// rust/src/bench/src/cli.rs —— BenchServeArgs 中与 Python 对齐的 speed-bench 参数定义。
​
/// SPEED-Bench 数据集配置/切分(qualitative、throughput_1k、throughput_2k、
/// throughput_8k、throughput_16k、throughput_32k)。
///
/// Python 侧参数名为 `--speed-bench-dataset-subset`,Rust 侧原名
/// `--speed-bench-config`;通过 visible_alias 同时接受两种拼写,
/// 保证 `VLLM_USE_RUST_BENCH=1` 委托时原始 argv 能原样解析。
#[arg(
    long,
    visible_alias = "speed-bench-dataset-subset",
    default_value = "qualitative"
)]
pub speed_bench_config: SpeedBenchConfig,
​
/// 每个请求的输出 token 数(仅 SPEED-Bench 数据集)。
///
/// 默认 4096 与 Python `speed_bench_output_len` 一致;此前 Rust 侧
/// 缺失该参数,长度会静默回退到 sharegpt/random 的 128,
/// 导致委托出的基准结果不可比。
#[arg(long, default_value_t = 4096)]
pub speed_bench_output_len: usize,

评论区精华

快照再生成命令应使用 .venv/bin/python 测试

codex(P1):贡献者按文档用裸 `python` 执行再生成命令时可能解析到系统解释器,绕过仓库虚拟环境要求(AGENTS.md no-bare-python 约定);同一错误命令还会被写进快照文件头。

结论:统一改为 `.venv/bin/python`,docstring、快照头与 pytest 失败提示三处同步更新(commit 8ed085e)。 · 已解决

AGENTS.md 指令文件应控制增长 style

codex(P1):AGENTS.md 编辑守则要求指令文件改动通过删减 / 合并抵消,本次只增了 6 行说明。

结论:commit “lean AGENTS.md” 将说明并入既有 Build & Test 段落,控制指令文件膨胀。 · 已解决

--tokenizer-mode 的 --help 文案与实际行为矛盾 documentation

claude[bot](黄标):`BenchServeArgs::tokenizer_mode` 的 clap doc 注释仍写 `(auto, hf, slow, mistral)`,让 `--help` 呈现为功能完整,而本 PR 实际加了“非 auto 忽略并告警”的运行时行为。

结论:同一 PR 内更新 doc 注释为“接受但非 auto 忽略”,与其他文档面保持一致。 · 已解决

一致性测试设计获维护者认可 设计

BugenZhao 批准时评价 “The parity tests look quite clever! 😀”,并追加提交将 parity 测试挂入 benchmarks CI。

结论:APPROVED 并合入;快照变更现在也会触发 CI。 · 已解决

风险与影响

  1. 默认行为变更:依赖旧回退逻辑(speed-bench 输出长度 128)跑 Rust 基准的用户,升级后默认变为 4096,单请求时长与吞吐口径会变化。这是刻意修复(与 Python 对齐),但对比历史基准数据时需留意。
  2. 快照同步负担:Python 解析器新增参数后,若快照与 PYTHON_ONLY 未同步更新,cargo 或 pytest 任一 CI 会失败。BugenZhao 已将其挂入 benchmarks CI,但提交者仍需承担“每次动参数就跑再生成脚本”的流程成本。
  3. metrics 解析兼容面:parse_spec_decode_metrics 只统计 _total 后缀的 counter,若某观测端点用非标准命名暴露 spec 指标,统计会静默变 None;position 标签仍靠整行 line.find("position=\"") 解析,label 顺序变化时仍可能漏解析(相比改动前已大幅收敛)。
  4. 影响范围:仅 rust/src/bench/ 客户端与 CI 配置;模型服务、Python 主路径不受影响。委托路径从“可用但结果不可比”变为“参数与 Python 等价”。

用户侧:使用 VLLM_USE_RUST_BENCH=1 委托的用户获得与 Python 一致的 speed-bench 默认行为与参数拼写;直接跑 vllm-bench spec-decode 统计的用户获得与 Python #41916 相同的计数口径。系统侧:无模型推理路径改动,Rust 客户端新增 1 个 CLI 参数、1 个别名与 1 个纯解析函数。团队侧:CI 增加一道 Python/Rust CLI 契约门禁,未来任一侧新增参数都会被另一方测试提醒,代价是需要维护快照与白名单。

基准默认输出长度 128→4096 行为变更 CLI 快照同步依赖人工维护 metrics 解析仅接受 _total 计数器 新增 CI 契约门禁

关联 Issue

#41916 Fix spec decode benchmark metrics

完整报告

参与讨论