Prhub

#51316 [Rust Frontend][gRPC] Add RL lifecycle control

原始 PR 作者 connorcarpenter15 合并时间 2026-08-15 02:32 文件变更 17 提交数 7 评论 28 代码增减 +614 / -16

执行摘要

Rust gRPC 控制面新增 RL 生命周期与权重更新 RPC

PR body 明确说明:"This lets trusted out-of-process RL sidecars use the native Rust gRPC frontend instead of the development HTTP server. It adds no listener, port, or separate weight-transfer implementation." 此前 RL 训练侧动态控制推理引擎(暂停采样、释放显存、热更新权重)只能依赖开发用 HTTP server,缺少生产级 gRPC 控制面;本 PR 将能力协商前置到 ready 握手,并在分派前拒绝未配置操作。

值得精读,尤其是关注 Rust 前端控制面和 RL 训练集成的工程师。值得借鉴的设计决策:通过 ready 握手广播能力并配合 FAILED_PRECONDITION 前置拦截、前端 rl_lock 串行化状态变更、复用既有 collective_rpc / call_utility_consensus 通道而不新造传输,以及后端 supports_draft_weight_update 属性驱动的精确能力判定。

讨论亮点

review 中最有价值的交锋集中在三处:

  • draft 能力判定精度:njhill 指出最初用 use_spec_decode 判定 supports_draft_weight_updates 不够精确,应同时检查权重传输是否启用、投机解码类型(EAGLE 或 draft)以及后端 supports_draft_weight_update 属性;作者改为在 model executor 上新增方法并通过 collective RPC 汇聚各 worker 真实能力。
  • pause 前置检查是否必要:njhill 认为前端不需要强制要求生成已暂停,且与 mode="wait" 和两阶段 DP 暂停语义冲突,如需该语义应改引擎内部行为;作者同意并移除该检查,测试名改为 control_forwards_weight_update_without_pause_guard
  • 是否在前端重复 reset mm cache:njhill 曾建议在 pause / sleep 时调用 reset_mm_cache(),作者说明引擎 pause_scheduler() 内部 _reset_caches() 已处理,njhill 两次撤回建议。
  • 另有两个小问题:proto 字段号 10 预留给 #52031 的 max_loras;空 ready 响应时能力函数返回值的边界问题,作者以「客户端保证至少一个 engine 响应」为由保持现状。

实现拆解

  1. Proto 契约扩展rust/proto/control.proto 新增 RlCapabilitiesPauseMode 以及 pause / resume / sleep / wake / weight transfer 系列消息;ServerInfo 增加 rl_capabilities 字段(编号 11,跳过 10 为 #52031 的 max_loras 预留)。
  2. EngineCore 能力上报vllm/v1/engine/core.py_make_ready_response() 向握手响应写入 weight_transfer_backendenable_sleep_modesupports_draft_weight_updates;Python 的 EngineCoreReadyResponsevllm/v1/engine/__init__.py)与 Rust 的 rust/src/engine-core-client/src/protocol/handshake.rs 同步新增字段,mock_engine.rs 补默认值保证向后兼容。
  3. 能力判定下沉到 workervllm/v1/executor/abstract.py 新增 supports_draft_weight_updates() 通过集体 RPC 汇聚各 worker 结果;worker_base.py 默认返回 Falsegpu_worker.py 实际判定「有权重传输引擎 && 后端支持草稿更新 && 有草稿模型 && 有投机解码配置」。
  4. 客户端方法封装rust/src/engine-core-client/src/client.rs 新增 init_weight_transfer_enginestart_weight_updatestart_draft_weight_updateupdate_weightsfinish_weight_updateset_weight_versionget_weight_version 7 个方法,权重传输类复用 collective_rpc 广播到所有 engine,版本查询走 call_utility_consensus 保证全局一致。
  5. gRPC 服务实现rust/src/server/src/grpc/control.rs 引入 rl_lock 串行化所有状态变更操作;require_weight_transfer() / require_sleep_mode() 做前置校验,未配置时返回 FAILED_PRECONDITION;实现 pause / resume / is_paused / sleep / wake_up / is_sleeping 与权重传输全套 RPC。
  6. 测试与文档配套tests.rs 重构出 setup_grpc_service_with_engine_script 以便注入自定义 ready 响应与消息脚本,并新增 control_forwards_weight_update_without_pause_guard 等用例;docs/usage/security.md 记录未认证控制面的管理信任边界,docs/training/weight_transfer/README.md 补充说明。
文件 模块 状态 重要度
rust/src/server/src/grpc/control.rs 控制面 modified 8.84
rust/src/engine-core-client/src/client.rs 引擎客户端 modified 8.44
rust/src/server/src/grpc/tests.rs 控制面测试 modified 7.37
vllm/v1/engine/core.py 引擎核心 modified 6.09
vllm/v1/executor/abstract.py 执行器 modified 5.58
vllm/v1/worker/gpu_worker.py 推理执行 modified 6.22
rust/src/engine-core-client/src/protocol/handshake.rs 握手协议 modified 5.92
vllm/v1/worker/worker_base.py 基础工作器 modified 4.89
rust/src/engine-core-client/src/mock_engine.rs 模拟引擎 modified 4.4
vllm/v1/engine/__init__.py 协议定义 modified 5.2
rust/proto/control.proto 接口契约 modified 5.75
docs/usage/security.md 安全文档 modified 2.07

关键符号

pause_generation resume_generation is_paused sleep wake_up is_sleeping init_weight_transfer_engine start_weight_update start_draft_weight_update update_weights finish_weight_update set_weight_version get_weight_version rl_capabilities require_weight_transfer require_sleep_mode supports_draft_weight_updates

关键源码片段

rust/src/server/src/grpc/control.rs core-logic

核心变更点:新增 pause/resume/sleep/wake 与权重更新全套 RPC,引入 rl_lock 串行化、能力汇总与前置校验。

// 从所有 engine 的 ready 响应汇总权重传输后端:仅当所有 engine 配置了
// 同一后端时才视为可用,避免多卡场景下能力不一致被误报。
fn weight_transfer_backend(&self) -> Option<&str> {
    let responses = self.client().ready_responses();
    let backend = responses.first()?.weight_transfer_backend.as_deref()?;
    responses
        .iter()
        .all(|ready| ready.weight_transfer_backend.as_deref() == Some(backend))
        .then_some(backend)
}fn sleep_mode_enabled(&self) -> bool {
    self.client().ready_responses().iter().all(|ready| ready.enable_sleep_mode)
}fn draft_weight_updates_enabled(&self) -> bool {
    self.client()
        .ready_responses()
        .iter()
        .all(|ready| ready.supports_draft_weight_updates)
}// 汇总为对外公布的 RL 能力快照,随 get_server_info 返回给调用方,
// 供训练 sidecar 在发起操作前判断当前服务是否支持对应功能。
fn rl_capabilities(&self) -> pb::RlCapabilities {
    let backend = self.weight_transfer_backend();
    pb::RlCapabilities {
        weight_transfer_enabled: backend.is_some(),
        weight_transfer_backend: backend.unwrap_or_default().to_string(),
        sleep_mode_enabled: self.sleep_mode_enabled(),
        draft_weight_updates_enabled: self.draft_weight_updates_enabled(),
    }
}// 前置校验:未配置权重传输时直接返回 FAILED_PRECONDITION,
// 避免请求下发到引擎后才发现不支持。
fn require_weight_transfer(&self) -> Result<(), Status> {
    self.weight_transfer_backend().map(|_| ()).ok_or_else(|| {
        Status::failed_precondition(
            "weight transfer is not configured; start vLLM with --weight-transfer-config",
        )
    })
}// 暂停生成:所有调度器与权重相关变更操作都先取 rl_lock,
// 保证 pause / resume / sleep / wake / 权重更新之间串行执行。
async fn pause_generation(
    &self,
    request: Request<pb::PauseGenerationRequest>,
) -> Result<Response<pb::PauseGenerationResponse>, Status> {
    let request = request.into_inner();
    let mode = pause_mode(request.mode)?;
    let clear_cache = request.clear_cache.unwrap_or(true);
    let _guard = self.rl_lock.lock().await;
    self.client()
        .pause_scheduler(mode, clear_cache)
        .await
        .map_err(|error| utility_status("pause_generation", error))?;
    Ok(Response::new(pb::PauseGenerationResponse {}))
}
rust/src/engine-core-client/src/client.rs core-logic

Rust 客户端新增 7 个权重传输与版本查询方法,复用 collective_rpc 与 call_utility_consensus 通道。

/// 初始化 RL 权重传输后端,init_info 为框架无关的 JSON 配置,
/// 实际逻辑由各 worker 上的 backend 解释。
pub async fn init_weight_transfer_engine(&self, init_info: JsonValue) -> Result<()> {
    self.collective_rpc(
        "init_weight_transfer_engine",
        None,
        Vec::<JsonValue>::new(),
        BTreeMap::from([("init_info".to_string(), init_info)]),
    )
    .await?;
    Ok(())
}/// 开始基础模型权重更新;草稿模型需走 start_draft_weight_update。
pub async fn start_weight_update(&self) -> Result<()> {
    self.collective_rpc(
        "start_weight_update",
        None,
        Vec::<JsonValue>::new(),
        BTreeMap::<String, JsonValue>::new(),
    )
    .await?;
    Ok(())
}/// 流式应用权重元数据块:张量数据不走 msgpack,而是通过
/// 后端各自的传输通道(NCCL / IPC 等),这里只同步元数据。
pub async fn update_weights(&self, update_info: JsonValue) -> Result<()> {
    self.collective_rpc(
        "update_weights",
        None,
        Vec::<JsonValue>::new(),
        BTreeMap::from([("update_info".to_string(), update_info)]),
    )
    .await?;
    Ok(())
}// 读取已提交权重版本:要求所有 engine 返回一致结果(AND 聚合),
// 否则视为失败,避免训练侧读到不一致的版本。
pub async fn get_weight_version(&self) -> Result<String> {
    self.call_utility_consensus("get_weight_version", ()).await
}
vllm/v1/worker/gpu_worker.py core-logic

实现草稿权重更新能力的真实判定,综合引擎、后端、草稿模型与投机配置。

def supports_draft_weight_updates(self) -> bool:
    # 草稿权重更新的前提条件:存在权重传输引擎且后端支持草稿更新、
    # runner 具备 get_draft_model 能力且确实配置了草稿模型、
    # 同时有投机解码配置,任一项不满足都应向控制面上报不支持。
    engine = self.weight_transfer_engine
    speculative_config = self.speculative_config
    get_draft_model = getattr(self.model_runner, "get_draft_model", None)
    return (
        engine is not None
        and engine.supports_draft_weight_update
        and callable(get_draft_model)
        and get_draft_model() is not None
        and speculative_config is not None
        and speculative_config.draft_model_config is not None
    )

评论区精华

supports_draft_weight_updates 判定精度 正确性

njhill 指出最初仅用 use_spec_decode 判定不够精确:还应依赖整体是否启用权重传输、投机解码类型(EAGLE 或 draft),并检查后端 supports_draft_weight_update 属性。

结论:作者改为在 model executor 上新增 supports_draft_weight_updates(),通过 collective RPC 汇聚各 worker 的真实能力,并在 gpu_worker 中同时校验引擎、后端属性、草稿模型与投机配置。 · 已解决

proto 字段号 10 预留 question

njhill 询问 rl_capabilities 为何使用字段号 11 而非 10。

结论:作者答复 #52031 会把 max_loras 放到字段号 10,因此此处跳号预留。 · 已解决

pause / sleep 时是否额外 reset mm cache 设计

njhill 建议在 pause_generation clear_cache 与 sleep 时调用 reset_mm_cache();作者说明 EngineCore 的 pause_scheduler() 内部已有 _reset_caches() 处理。

结论:njhill 认可并撤回建议,最终未在前端重复调用。 · 已解决

前端是否需要 pause 前置检查 设计

njhill 认为无需在 control.rs 检查是否已暂停,现有接口不这么做,且 wait 模式与两阶段 DP 暂停下语义不符;若需要应改引擎内部行为。

结论:作者移除相关检查,测试更名为 control_forwards_weight_update_without_pause_guard,验证权重更新无需 pause 前置即可转发。 · 已解决

空 ready 响应下能力函数的健壮性 style

njhill 提出 sleep_mode_enabled / draft_weight_updates_enabled 在没有 ready 响应时返回 true 的边界问题。

结论:作者解释成功的客户端至少有一个 engine 响应(engine 数为 0 时启动即失败),保持现状。 · 已解决

风险与影响

控制面未认证:文档明确该 gRPC 控制面属于管理信任边界,依赖网络层隔离;若暴露在不可信网络,任何能触达端点的调用方都能睡眠引擎或替换权重。跨语言协议同步成本:Python / Rust 两套 EngineCoreReadyResponse 需同步维护,#[serde(default)] 提供向后兼容但语义漂移不易察觉。启动握手新增一次集体 RPC(supports_draft_weight_updates),多 worker / 多 DP 下增加一次握手往返,属一次性成本。rl_lock 仅串行化 Rust 前端进程内的控制操作,引擎侧仍须自行保证与推理的互斥;update_weights 等命令以 JSON 传递元数据,类型安全弱于强类型消息。对现有推理路径无影响,未添加监听端口与传输实现。

对 RL 训练基础设施影响较大:受信任的外部训练 sidecar 现在可以直接对接原生 Rust gRPC 前端,替代开发用 HTTP server,且能力在握手阶段就可协商。对多 engine / 数据并行部署,能力字段采用 AND 聚合,保证所有 worker 能力一致才对外公布。对现有推理 API 用户无行为变化;对团队而言,后续维护需要同时关注 Rust 与 Python 两端的握手协议定义,并持续维护控制面安全文档。

未认证控制面 跨语言协议同步 启动期集体 RPC 控制面锁范围有限

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论