Prhub

#45805 [Rust Frontend] Support hybrid/external DP LB in Python supervised bootstrap

原始 PR 作者 BugenZhao 合并时间 2026-06-17 15:32 文件变更 8 提交数 4 评论 2 代码增减 +159 / -6

执行摘要

Rust 前端支持 external/hybrid DP 负载均衡

需要为Python-supervised的Rust前端添加数据并行负载均衡支持,以覆盖external和hybrid两种模式。在external模式下每个DP rank对应一个独立的前端进程,在hybrid模式下每个节点管理多个本地DP rank。此变更使得Rust前端无需依赖远程DP协调器即可在Python监督下工作。

值得阅读,特别关注Python如何根据local_engines_only推导engine_start_index以及Rust侧如何验证引擎ID范围,体现了跨语言参数传递的最佳实践。设计决策中选择了显式传递而非自动发现,简化了混合模式下的实例绑定。

讨论亮点

Reviewer(njhill)指出:"Technically external lb mode is where there is 1-1 frontend to engine proc / dp rank, hybrid is 1-n. I'm guessing if not it shouldn't be much change, and we can then also add test_hybrid_lb_db.py to the tests." 作者回复已添加补丁,hybrid模式也得到支持,测试文件已补充。讨论确认了hybrid模式覆盖,团队达成一致。

实现拆解

  1. Rust传输层扩展:在TransportMode::Bootstrapped中新增engine_start_index: u32字段,在connect_bootstrapped函数中利用该索引计算期望的引擎ID范围(engine_start_index .. engine_start_index + engine_count),替代硬编码的0..engine_count
  2. Python监督启动适配:在vllm/entrypoints/cli/serve.py中,当使用Rust前端时,根据parallel_config.local_engines_only决定engine_start_index(hybrid模式下取data_parallel_rank,external模式下取0)和engine_count(hybrid取data_parallel_size_local,external取data_parallel_size),并传递给RustFrontendProcessManager
  3. Rust前端进程管理:在vllm/v1/utils.py中,RustFrontendProcessManager.__init__新增engine_start_index参数,并转换为CLI参数--engine-start-index;同时扩展args_json的排除列表,加入data_parallel_rankdata_parallel_external_lbdata_parallel_hybrid_lb,避免重复或冲突。
  4. Rust CLI参数解析:在rust/src/cmd/src/cli.rs中添加--engine-start-index选项,对应FrontendConfig.engine_start_index,并在CLI测试中验证非零起始索引和外部协调器场景。
  5. 测试与CI集成:新增Rust单元测试bootstrapped_connects_with_nonzero_engine_start_indexbootstrapped_rejects_unexpected_engine_id_for_start_index,验证非零索引的连接和错误拒绝;Python端添加test_external_lb_dp.pytest_hybrid_lb_dp.py并注册到.buildkite/test_areas/rust_frontend.yaml
文件 模块 状态 重要度
rust/src/engine-core-client/src/tests/client.rs 客户端 modified 7.4
vllm/entrypoints/cli/serve.py 入口点 modified 5.8
vllm/v1/utils.py 进程管理 modified 5.75
rust/src/engine-core-client/src/client.rs 客户端库 modified 5.57
rust/src/engine-core-client/src/transport.rs 传输层 modified 5.57
rust/src/cmd/src/cli.rs CLI modified 5.39
.buildkite/test_areas/rust_frontend.yaml CI 配置 modified 3.13

关键符号

connect_bootstrapped RustFrontendProcessManager.__init__ bootstrapped_connects_with_nonzero_engine_start_index bootstrapped_rejects_unexpected_engine_id_for_start_index serve (serve.py 中的启动逻辑 )

关键源码片段

rust/src/engine-core-client/src/tests/client.rs test-coverage

新增两个单元测试验证非零 engine_start_index 和意外引擎 ID 拒绝,是关键的正确性保障

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn bootstrapped_connects_with_nonzero_engine_start_index() {
    init_tracing();
    let ipc = IpcNamespace::new().unwrap();
    let input_address = ipc.input_endpoint();
    let output_address = ipc.output_endpoint();    let client_task = tokio::spawn({
        let input_address = input_address.clone();
        let output_address = output_address.clone();
        async move {
            // 使用 engine_start_index = 3, engine_count = 1
            EngineCoreClient::connect(bootstrapped_test_config_with_start_index(
                input_address,
                output_address,
                3, // engine_start_index
                1, // engine_count
                Duration::from_secs(2),
                0,
                None,
            ))
            .await
            .unwrap()
        }
    });    // mock engine 注册的 id 必须对应 engine_start_index (3)
    let (_dealer, _push) =
        setup_bootstrapped_mock_engine(input_address, output_address, &[0x03, 0x00]).await;
    let client = client_task.await.unwrap();    assert_eq!(client.engine_count(), 1);
    let engine_ids =
        client.engine_identities().into_iter().map(|id| id.to_vec()).collect::<Vec<_>>();
    assert_eq!(engine_ids, vec![vec![0x03, 0x00]]);    client.shutdown().await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn bootstrapped_rejects_unexpected_engine_id_for_start_index() {
    init_tracing();
    let ipc = IpcNamespace::new().unwrap();
    let input_address = ipc.input_endpoint();
    let output_address = ipc.output_endpoint();    let client_task = tokio::spawn({
        let input_address = input_address.clone();
        let output_address = output_address.clone();
        async move {
            // 期望 engine id 从 3 开始
            EngineCoreClient::connect(bootstrapped_test_config_with_start_index(
                input_address,
                output_address,
                3,
                1,
                Duration::from_secs(2),
                0,
                None,
            ))
            .await
        }
    });    // 注册一个 id 为 0x00 的 engine,不符合 start_index,应被拒绝
    let _ = crate::mock_engine::connect_to_bootstrapped_frontend(
        input_address,
        output_address,
        &[0x00, 0x00],
        crate::mock_engine::MockEngineConfig {
            local: true,
            headless: true,
            ..Default::default()
        },
    )
    .await;
    let error = client_task.await.unwrap_err();
    assert!(matches!(
        error,
        Error::UnexpectedEngineRegistration { actual: 0x00, expected_start: 3 }
    ));
}
vllm/v1/utils.py core-logic

RustFrontendProcessManager 核心修改,传递 engine_start_index 并过滤 args_json

def __init__(
    self,
    binary_path: str,
    sock: Any,
    args: argparse.Namespace,
    input_address: str,
    output_address: str,
    engine_start_index: int, # 新增参数,表示本前端负责的第一个 engine 的 DP rank
    engine_count: int,
    stats_update_address: str | None = None,
):
    import os
    import subprocess
​
    fd = sock.fileno()
    os.set_inheritable(fd, True)
​
    cmd = [
        binary_path,
        "frontend",
        "--listen-fd", str(fd),
        "--input-address", input_address,
        "--output-address", output_address,
        "--engine-start-index", str(engine_start_index), # 传递给 Rust 进程
        "--engine-count", str(engine_count),
    ]
    if stats_update_address is not None:
        cmd.extend(["--coordinator-address", stats_update_address])
​
    from vllm.entrypoints.serve.utils.api_utils import jsonify_non_default_args
    # 构造 args_json 时排除 Python 已经通过显式参数传递的设置
    args_json = json.dumps(
        jsonify_non_default_args(
            args,
            exclude={
                "api_server_count",
                "data_parallel_rank", # 已被 engine_start_index 替代
                "data_parallel_external_lb", # 已由参数模式隐含
                "data_parallel_hybrid_lb", # 已由参数模式隐含
            },
        ),
        sort_keys=True,
    )
    cmd.extend(["--args-json", args_json])
​
    logger.info("Launching Rust frontend: %s", " ".join(cmd))
    self._proc = subprocess.Popen(cmd, pass_fds=(fd,))
​
    # 创建进程包装器用于监控
    self.processes: list[_SubprocessWrapper] = [
        _SubprocessWrapper(self._proc, "RustFrontend")
    ]
    self._finalizer = weakref.finalize(self, _shutdown_subprocesses, self.processes)

评论区精华

Hybrid LB 模式支持确认 设计

njhill: Technically external lb mode is where there is 1-1 frontend to engine proc / dp rank, hybrid is 1-n. I'm guessing if not it shouldn't be much change, and we can then also add test_hybrid_lb_db.py to the tests.

结论:作者确认已添加补丁支持 hybrid 模式,并补充了对应的测试文件 test_hybrid_lb_dp.py。 · 已解决

风险与影响

  • 跨语言参数一致性:Python侧计算的engine_start_index必须与Rust侧解析的完全一致,若双方对配置理解不同(如local_engines_only判断),可能导致引擎注册失败或错乱。
  • 默认值兼容engine_start_index默认值为0,现有未使用此参数的手动启动场景不会受影响,但需确保新逻辑不会意外覆盖外部传递的配置。
  • 测试覆盖:新增的e2e测试仅在Buildkite Rust Frontend区域运行,其他CI可能遗漏;单元测试覆盖了正向和负向场景,但缺少对异常路径(如注册超时、索引范围重叠)的全面覆盖。
  • 性能:无显著影响。
  • 用户:使用vLLM v1 API并采用External/Hybrid DP部署的用户可直接通过Python监督启动Rust前端,无需额外手动配置;现有Rust managed-engine模式用户仍需远程DP协调器。
  • 系统:新增CLI参数和内部验证逻辑,提高启动正确性;args_json排除部分字段减少冗余。
  • 团队:需要维护新增的测试和CI配置;Rust前端与Python启动逻辑的耦合度略有增加。
跨语言参数传递 默认值兼容性 测试覆盖依赖特定 CI

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论