Prhub

#48107 [Rust][Benchmark] Port in vllm-bench

原始 PR 作者 esmeetu 合并时间 2026-07-17 14:25 文件变更 45 提交数 5 评论 3 代码增减 +16926 / -2

执行摘要

移植 Rust 基准测试工具 vllm-bench

PR body 仅链接到外部仓库 https://github.com/vllm-project/vllm-bench,目的是将原先独立开发的基准测试工具整合进 vLLM 主仓库,便于统一维护、版本管理和 CI 集成。

值得精读,尤其关注:

  • benchmark.rs 中 DNS 预解析和 SpecDecode 指标拉取的实现,体现了高并发下的稳健性设计。
  • config.rsRangeRatio::parse 的灵活解析(支持 float 或 JSON 对象)和语义对齐 Python 的向后兼容处理。
  • output/json.rs 中与 Python 输出 schema 严格匹配的设计,确保工具可互换。
    建议在后续集成时关注与 vllm-rs 的代码复用和 CLI 统一。
讨论亮点

BugenZhao 在审批时评论:"Great to see this! We can make some follow-up changes to integrate the bench entry point more tightly with the existing vllm-rs, and try to reuse more components between them." 表明当前为初步移植,后续将推动与现有 Rust 模块(vllm-rs)的深度融合和组件复用。未出现其他技术争议。

实现拆解

  1. 项目结构引入:在 rust/ 工作空间中新建 bench 子 crate,配置 Cargo.toml 依赖(tokio、reqwest、clap、serde、indicatif、tiktoken-rs 等)并注册为工作空间成员;根目录的 .pre-commit-config.yaml 增加 Rust 格式化检查。

  2. 核心模块移植

    • cli.rs:定义 BackendKindDatasetNameRampUpStrategy 等枚举,以及 Cli 结构体,实现参数解析。
    • config.rsBenchConfig 结构体从 Cli 验证并生成运行时配置,RangeRatio 支持 JSON 或浮点数形式指定随机范围。
    • benchmark.rsrun_benchmark 函数实现主要基准循环,包含 DNS 预解析 (pre_resolve_dns)、SpecDecode 指标采集 (fetch_spec_decode_metrics)、请求调度、进度条显示等。
    • multi_turn.rsrun_multi_turn_benchmark 支持多轮对话评估,包含前缀截断和模型长度过滤。
    • datasets/ 目录:hf_dataset.rs 通过 HF Datasets Server API 下载任意数据集并自动检测列格式;random.rssharegpt.rssonnet.rsmulti_turn.rs 各数据集生成逻辑;random_mm.rs 生成多模态随机请求。
    • output/json.rsbuild_result_json 精确匹配 Python 版输出 schema,确保结果可互换。
    • metrics/calculator.rs 计算 TTFT、TPOT、吞吐量等指标;steady_state.rs 通过滑动窗口检测性能稳定点。
    • sweep.rsrun_concurrency_sweep/run_rate_sweep 自动扫描压力参数。
    • tiktoken.rs:封装 tiktoken-rs 实现 token 编码/解码,支持文件加载和内置模型 BPE。
  3. 构建集成与代码风格:BugenZhao 后续提交修复了 lint 警告、调整 dep 规范、移动资源文件(sonnet.txt)、使用 nightly 版 rustfmt 格式化,并给所有文件添加 SPDX 许可证头。

  4. 测试配套:虽然本次未添加单元测试文件,但源码中包含若干 #[cfg(test)] 内联测试,例如 benchmark.rs 中的 test_filter_requests_by_max_model_len_*multi_turn.rs 中的 test_valid_prefix_len_for_max_model_len_with_history 等。

文件 模块 状态 重要度
rust/src/bench/src/benchmark.rs 基准测试 added 9.08
rust/src/bench/src/config.rs 基准测试 added 8.89
rust/src/bench/src/cli.rs 基准测试 added 8.98
rust/src/bench/src/multi_turn.rs 基准测试 added 8.98
rust/src/bench/src/datasets/hf_dataset.rs 基准测试 added 8.98
rust/src/bench/src/output/json.rs 基准测试 added 8.78

关键符号

run_benchmark pre_resolve_dns fetch_spec_decode_metrics run_multi_turn_benchmark download_hf_dataset build_result_json BenchConfig::from_cli RangeRatio::parse

关键源码片段

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

基准测试核心入口,包含 DNS 预解析、请求调度、进度跟踪、SpecDecode 统计等关键逻辑。

/// Pre-resolve the hostname in `base_url` and pin all resolved IPs on the
/// client builder via [`reqwest::ClientBuilder::resolve_to_addrs`]. This
/// avoids repeated DNS lookups under high concurrency which can cause
/// transient "Temporary failure in name resolution" errors while preserving
/// happy-eyeballs and multi-A failover.
///
/// Skipped when:
/// - URL parse fails
/// - host is already an IP
/// - host is a loopback name (resolved from `/etc/hosts`, no DNS pressure
///   and dual-stack ambiguity between `127.0.0.1` and `::1` breaks
///   IPv4-only servers like vLLM)
/// - resolution fails
pub fn pre_resolve_dns(
    base_url: &str,
    mut builder: reqwest::ClientBuilder,
) -> reqwest::ClientBuilder {
    let parsed = match url::Url::parse(base_url) {
        Ok(u) => u,
        Err(_) => return builder, // 无法解析 URL 时跳过
    };
    let host = match parsed.host_str() {
        Some(h) => h,
        None => return builder, // 无 host 时跳过
    };
    // 如果已经是 IP,跳过预解析
    if host.parse::<std::net::IpAddr>().is_ok() {
        return builder;
    }
    // 避免解析 localhost(IPv4/IPv6 双栈问题)
    let host_lower = host.to_ascii_lowercase();
    if host_lower == "localhost"
        || host_lower == "ip6-localhost"
        || host_lower.ends_with(".localhost")
    {
        return builder;
    }
    let port = parsed.port_or_known_default().unwrap_or(80);
    let addr_str = format!("{host}:{port}");
    match std::net::ToSocketAddrs::to_socket_addrs(&addr_str) {
        Ok(addrs) => {
            // 分离 IPv4 和 IPv6,优先 IPv4
            let mut v4 = Vec::new();
            let mut v6 = Vec::new();
            for addr in addrs {
                if addr.is_ipv4() {
                    v4.push(addr);
                } else {
                    v6.push(addr);
                }
            }
            v4.extend(v6); // IPv4 排前
            if !v4.is_empty() {
                let ips: Vec<_> = v4.iter().map(|a| a.ip()).collect();
                println!("Pre-resolved {host} -> {ips:?}");
                builder = builder.resolve_to_addrs(host, &v4);
            }
        }
        Err(e) => {
            eprintln!("Warning: DNS pre-resolution for '{host}' failed: {e}");
        }
    }
    builder
}
rust/src/bench/src/config.rs core-logic

配置解析核心,定义了 BenchConfig、RangeRatio、GoodputConfig 等关键类型,实现 CLI 到配置的转换与验证。

/// Range ratio for sampling input/output lengths, matching Python
/// `vllm bench serve`: lengths are drawn uniformly from [len*(1-r), len*(1+r)].
/// A single float applies to both; the JSON form '{"input": r1, "output": r2}'
/// controls them independently. Each ratio must be in [0, 1).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RangeRatio {
    pub input: f64,
    pub output: f64,
}impl RangeRatio {
    /// Parse `--random-range-ratio`: a bare float or a JSON object with
    /// "input" and "output" keys.
    pub fn parse(raw: &str) -> Result<Self> {
        let trimmed = raw.trim();
        let (input, output) = if let Ok(v) = trimmed.parse::<f64>() {
            (v, v) // 单个浮点数,输入输出共用
        } else {
            // 尝试解析 JSON 对象
            let v: serde_json::Value = serde_json::from_str(trimmed).map_err(|_| {
                BenchError::Config(format!(
                    "Invalid --random-range-ratio '{raw}': expected a float or \
                     '{{\"input\": r1, \"output\": r2}}'"
                ))
            })?;
            let obj = v.as_object().ok_or_else(|| {
                BenchError::Config(
                    "--random-range-ratio JSON form must be an object with \
                     'input' and 'output' keys"
                        .into(),
                )
            })?;
            // 从对象中提取 input 和 output
            let get = |key: &str| -> Result<f64> {
                obj.get(key).and_then(|v| v.as_f64()).ok_or_else(|| {
                    BenchError::Config(format!(
                        "--random-range-ratio JSON form must contain a numeric '{key}' key"
                    ))
                })
            };
            (get("input")?, get("output")?)
        };
        // 验证范围在 [0,1) 内
        for (name, r) in [("input", input), ("output", output)] {
            if !(0.0..1.0).contains(&r) {
                let hint = if r == 1.0 {
                    " NOTE: semantics now match Python vllm bench serve — lengths are \
                     sampled from [len*(1-r), len*(1+r)] and 0.0 means fixed length. \
                     The old Rust-only default 1.0 ([len*r, len]) is no longer valid."
                } else {
                    ""
                };
                return Err(BenchError::Config(format!(
                    "--random-range-ratio {name} ratio must be in [0, 1), got {r}.{hint}"
                )));
            }
        }
        Ok(Self { input, output })
    }
    // ... input_bounds, output_bounds, is_fixed 等方法省略
}

评论区精华

后续集成计划 other

BugenZhao 在审批中表示后续可以将 bench 入口与现有 vllm-rs 更紧密集成,并尝试在其间复用更多组件。

结论:当前合并,后续优化集成。 · 已解决

风险与影响

  1. 构建时间增加:新增 Rust 子 crate 及依赖(如 tiktoken-rs、huggingface-hub 等)会拉长首次构建时间,CI 中需缓存编译产物。
  2. 维护成本:近 17k 行 Rust 代码需要专人维护,与 Python 版基准测试保持功能对等可能产生 drift。
  3. 兼容性风险:输出 JSON schema 需要与 Python 版严格一致,output/json.rs 中的 build_result_json 若未同步更新会导致下游解析异常。
  4. 多后端支持:对各种后端(尤其是 vLLM 后续 API 变更)的适配可能滞后,导致工具在新版本中不可用。

用户:可使用 vllm bench 命令(若后续绑定 CLI)或直接运行 Rust 二进制进行在线基准测试,获得与 Python 版一致的结果;支持更多数据集类型和多轮对话。
系统:显著扩展了 vLLM 的 Rust 代码库,增加了构建依赖和编译时间;提供独立的基准测试能力,减轻 Python 版性能开销。
团队:维护者需要熟悉 Rust 和基准测试领域;集成后有利于统一工具链,减少双份维护。

新增大量代码 构建时间增加 需维护双份基准测试

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论