Prhub

#52031 [Rust Frontend][gRPC] Advertise LoRA capabilities

原始 PR 作者 connorcarpenter15 合并时间 2026-08-18 04:27 文件变更 10 提交数 2 评论 7 代码增减 +53 / -0

执行摘要

Rust 前端新增 LoRA 能力通告与握手校验

PR body 明确指出目的是通过现有的 engine ready handshake 与 gRPC discovery API 通告 Rust 前端的 LoRA 能力与容量:'Advertise the Rust frontend's LoRA capability and capacity through the existing engine ready handshake and gRPC discovery APIs.'。此前 Rust 前端与 gRPC 客户端无法感知后端是否启用 LoRA 以及最大活跃 adapter 数,外部客户端无法在接入前做能力判断或路由决策。作者在搜索后确认没有重叠的开放 PR 或 issue,因此这是一个补齐能力盲区的新增通告。

值得精读。PR 虽小,但展示了跨语言协议演进的规范做法:Python dataclass 与 Rust struct 双端同步、无默认值让协议差异快速暴露、启动期强校验把配置漂移扼杀在握手阶段、跨语言 fixture 保证序列化兼容。对准备为 Rust 前端新增其他能力通告(如多模态、工具调用、量化支持)的开发者,validate_lora_capabilitiespython_compat.py 是两个可直接借鉴的模板。

讨论亮点

本 PR 实质性技术讨论很少,主要结论来自自动化 review 与维护者 approve:

  • Codex review:BugenZhao 触发 @codex review,Codex 回复 "Didn't find any major issues. Bravo.",对提交 990f58b70b 给出正面结论,无需修改。
  • claude[bot]:指出 fork 来源 PR 的自动 review 被禁用,需维护者手动触发或人工审核。
  • BugenZhao:先后触发两次 CI(Buildkite #84165、#84212)并 approve("LGTM thanks @connorcarpenter15")。
  • njhill:作为 merge 者 approve(body 为空)。

没有针对 validate_lora_capabilities 校验策略、字段类型选择或协议兼容性的公开辩论;两条 approve 均为直接放行。

实现拆解

  1. Python 侧扩展 ReadyResponse 数据契约vllm/v1/engine/__init__.pyEngineCoreReadyResponse dataclass 新增必填字段 supports_lora: boolmax_loras: int(无默认值,让新旧协议差异在序列化时立即暴露);vllm/v1/engine/core.py_make_ready_response()self.vllm_config.lora_config 推导这两个值:lora_config is not None 表示启用 LoRA,容量取 lora_config.max_loras,未启用时上报 0。

  2. Rust 侧同步握手结构体rust/src/engine-core-client/src/protocol/handshake.rsEngineCoreReadyResponse 新增 supports_lora: boolmax_loras: u32rust/src/engine-core-client/src/mock_engine.rsdefault_ready_response() 补齐默认值 false / 0,保证 mock 引擎启动路径也能通过校验。

  3. 启动期能力校验rust/src/engine-core-client/src/client.rsfrom_connected() 创建 client 状态前调用新增的 validate_lora_capabilities(&connected.engines)?。该校验做两层检查:单 rank 内部要求 supports_lora == (max_loras > 0),防止“宣称支持但容量为 0”的矛盾;跨 rank 要求所有 engine 上报完全一致的 LoRA 能力,防止 DP 多引擎配置漂移导致后续路由行为不一致。

  4. gRPC discovery 透传rust/proto/control.protoServerInfo 增加 uint32 max_loras = 10、在 ModelInfo 增加 bool supports_lora = 22rust/src/server/src/grpc/control.rsget_server_info() 返回 max_lorasget_model_info() 返回 supports_lora,数据来源均为 ready 握手阶段缓存的元数据。

  5. 测试配套rust/src/engine-core-client/src/tests/python_compat.py 在跨语言 msgpack fixture 中加入 supports_lora=True, max_loras=8rust/src/engine-core-client/src/tests/client.rs 对应断言 Rust 解码结果与 Python 编码一致,守住双端序列化兼容;tests/v1/engine/test_engine_core_client.py 补齐 supports_lora=False, max_loras=0 构造参数。

文件 模块 状态 重要度
rust/src/engine-core-client/src/client.rs 引擎客户端 modified 7.13
vllm/v1/engine/core.py 引擎核心 modified 5.96
rust/src/engine-core-client/src/protocol/handshake.rs 握手协议 modified 5.11
rust/src/engine-core-client/src/mock_engine.rs 测试引擎 modified 5.1
vllm/v1/engine/__init__.py 引擎核心 modified 5.1
rust/src/server/src/grpc/control.rs 控制服务 modified 4.89
rust/proto/control.proto 协议定义 modified 2.14
rust/src/engine-core-client/src/tests/python_compat.py 兼容测试 modified 4.2
tests/v1/engine/test_engine_core_client.py 引擎测试 modified 4.03
rust/src/engine-core-client/src/tests/client.rs 客户端测试 modified 3.86

关键符号

validate_lora_capabilities _make_ready_response get_server_info get_model_info default_ready_response

关键源码片段

rust/src/engine-core-client/src/client.rs core-logic

核心校验逻辑所在:新增 validate_lora_capabilities,在 from_connected 启动路径中拦截不一致或混合能力的 engine rank。

//! rust/src/engine-core-client/src/client.rs
//! 校验所有已连接 engine 上报的 LoRA 能力元数据是否自洽且互相一致。
//!
//! 该校验在 `from_connected` 中、client 状态初始化之前执行,任何失败都会
//! 以 `UnexpectedHandshakeMessage` 错误终止启动进程。规则:
//! 1. 单 rank 内部 `supports_lora` 必须与 `max_loras > 0` 等价;
//! 2. 所有 rank 的 LoRA 能力必须完全一致,防止 DP 多引擎配置漂移。
fn validate_lora_capabilities(engines: &[ConnectedEngine]) -> Result<()> {
    let first = engines.first().expect("engine core client requires at least one engine");    for engine in engines {
        let ready = &engine.ready_response;        // 单 engine 内部一致性:宣称支持就必须有容量,反之亦然。
        if ready.supports_lora != (ready.max_loras > 0) {
            return Err(Error::UnexpectedHandshakeMessage {
                message: format!(
                    "engine {:?} reported inconsistent LoRA capability (supports_lora={}, max_loras={})",
                    engine.engine_id, ready.supports_lora, ready.max_loras
                ),
            });
        }        // 跨 engine 一致性:以第一个 engine 为基准,其余必须完全相同。
        if ready.supports_lora != first.ready_response.supports_lora
            || ready.max_loras != first.ready_response.max_loras
        {
            return Err(Error::UnexpectedHandshakeMessage {
                message: format!(
                    "engine {:?} reported LoRA capability inconsistent with engine {:?}",
                    engine.engine_id, first.engine_id
                ),
            });
        }
    }    Ok(())
}
vllm/v1/engine/core.py core-logic

Python 侧数据源:_make_ready_response 从 lora_config 推导 supports_lora 与 max_loras,是整套通告的输入起点。

def _make_ready_response(self) -> EngineCoreReadyResponse:
    """构造 engine 启动完成后上报给前端的 ReadyResponse。    该响应经 msgpack 序列化后由 Rust engine-core client 解析,新增字段
    必须与 `rust/src/engine-core-client/src/protocol/handshake.rs` 中的
    `EngineCoreReadyResponse` 保持同步。
    """
    parallel_config = self.vllm_config.parallel_config
    scheduler_config = self.vllm_config.scheduler_config
    return EngineCoreReadyResponse(
        max_model_len=self.vllm_config.model_config.max_model_len,
        num_gpu_blocks=self.vllm_config.cache_config.num_gpu_blocks or 0,
        block_size=self.vllm_config.cache_config.block_size,
        dp_stats_address=self.frontend_stats_publish_address,
        dtype=str(self.vllm_config.model_config.dtype).removeprefix("torch."),
        vllm_version=VLLM_VERSION,
        world_size=self.vllm_config.parallel_config.world_size,
        data_parallel_size=parallel_config.data_parallel_size,
        instance_id=self.vllm_config.instance_id,
        # LoRA 是否启用:直接以 lora_config 是否存在为判断依据。
        supports_lora=self.vllm_config.lora_config is not None,
        # 最大活跃 adapter 数:未启用 LoRA 时上报 0,供 Rust 侧强校验消费。
        max_loras=(
            self.vllm_config.lora_config.max_loras
            if self.vllm_config.lora_config is not None
            else 0
        ),
        # 其余字段保持原有语义,本 PR 不改变任何运行期行为。
        kv_events_config=self.scheduler.get_kv_event_publisher_config(),
        weight_transfer_backend=(
            self.vllm_config.weight_transfer_config.backend
            if self.vllm_config.weight_transfer_config is not None
            else None
        ),
        enable_sleep_mode=self.vllm_config.model_config.enable_sleep_mode,
        supports_draft_weight_updates=self.model_executor.supports_draft_weight_updates(),
    )
rust/src/server/src/grpc/control.rs core-logic

gRPC discovery 出口:get_server_info 返回 max_loras,get_model_info 返回 supports_lora,完成对外能力通告。

//! rust/src/server/src/grpc/control.rs
//! 把 engine ready 阶段已校验过的元数据透传给 gRPC discovery 客户端。
impl pb::control_server::Control for ControlServiceImpl {
    async fn get_server_info(
        &self,
        _request: Request<pb::GetServerInfoRequest>,
    ) -> Result<Response<pb::ServerInfo>, Status> {
        let ready = self.ready();
        Ok(Response::new(pb::ServerInfo {
            engine_version: ready.vllm_version.clone(),
            api_version: GRPC_API_VERSION.to_string(),
            instance_id: ready.instance_id.clone(),
            parallelism: Some(self.parallelism_info()),
            max_model_len: self.state.engine_core_client().max_model_len(),
            kv_block_size: ready.block_size.min(u64::from(u32::MAX)) as u32,
            total_kv_blocks: self.state.engine_core_client().total_num_gpu_blocks(),
            max_running_requests: ready.max_num_seqs,
            max_batched_tokens: ready.max_num_batched_tokens,
            // 新增:对外通告 LoRA 容量,客户端可据此做路由决策。
            max_loras: ready.max_loras,
            rl_capabilities: Some(self.rl_capabilities()),
        }))
    }    async fn get_model_info(
        &self,
        _request: Request<pb::GetModelInfoRequest>,
    ) -> Result<Response<pb::ModelInfo>, Status> {
        let served = self.state.served_model_names();
        Ok(Response::new(pb::ModelInfo {
            model_id: self.state.chat.text().model_id().to_string(),
            served_model_name: self.state.primary_model_name().to_string(),
            served_model_aliases: served.iter().skip(1).cloned().collect(),
            supports_text_input: true,
            supports_token_ids_input: true,
            // 新增:对外通告模型是否支持 LoRA。
            supports_lora: self.ready().supports_lora,
            supports_multimodal: self.state.chat.supports_multimodal(),
            // reasoning_parser / tool_call_parser 等字段保持不变。
            reasoning_parser: self
                .state
                .chat
                .reasoning_parser_name()
                .unwrap_or_default()
                .to_string(),
            tool_call_parser: self
                .state
                .chat
                .tool_call_parser_name()
                .unwrap_or_default()
                .to_string(),
        }))
    }
}

评论区精华

Codex 自动 review:未发现重大问题 other

BugenZhao 通过评论触发 @codex review;Codex 回复 "Didn't find any major issues. Bravo.",仅对提交 990f58b70b 给出正面结论。

结论:无需代码修改,能力通告与校验实现得到自动化 review 认可。 · 已解决

fork PR 的自动化 review 限制 other

claude[bot] 指出该 PR 来自 fork,自动 review 被禁用,需维护者手动触发或人工审核。

结论:由维护者 BugenZhao 与 njhill 人工 review 并 approve,BugenZhao 另触发 CI(Buildkite #84165、#84212)验证。 · 已解决

风险与影响

  1. 跨语言协议兼容性(中风险)EngineCoreReadyResponse 新字段在 Python dataclass 与 Rust struct 中均为必填、无 serde(default) 兜底。旧版 Rust client 与新版 Python engine、或新版 Rust client 与旧版 Python engine 混跑时,msgpack 解码会直接失败。vLLM 在 handshake.rs 注释中明确要求 engine 与 client 版本匹配,因此风险可控,但发布时必须保证双端同步升级。

  2. 启动路径硬校验(低风险)validate_lora_capabilitiesfrom_connected() 中以 Error::UnexpectedHandshakeMessage 硬拒绝任何不一致 rank。当前架构下 LoRA 配置必然全局一致,所以这是合理的防御性设计;但若未来引入 per-engine 或 per-request 的异构 LoRA 配置,该校验需要放宽。

  3. 校验错误路径测试缺失(中风险):测试只覆盖了成功路径(fixture 断言)和 mock 默认值,没有为 supports_lora=true, max_loras=0、跨 rank 不一致等错误分支新增 Rust 单元测试,校验逻辑的回归保护偏弱。

  4. 整数边界(低风险):Rust 侧 max_loras: u32 与 proto uint32 对 Python 任意大整数有上限约束,实际配置不会触及该边界,可忽略。

用户侧:gRPC discovery API 的调用方现在可以提前查询服务是否支持 LoRA 及其容量上限,便于客户端在接入前做能力判断或路由决策,无需再依赖猜测或失败的请求。系统侧:engine 启动握手新增一次轻量校验,能在多 rank 配置漂移时快速失败而非在运行期暴露问题,提升了 Rust 前端分布式启动的稳健性。团队侧:这是 Rust 前端能力向 Python vLLM 对齐的一步,确立了“Python 侧数据契约 + Rust 侧强校验 + 跨语言 fixture 同源验证”的扩展模式,后续其他能力通告可复用此套路。整体影响面集中在元数据与 API 表面,推理路径零改动。

跨语言协议扩展(需双端同步升级) 启动握手新增硬校验 校验错误路径测试覆盖不足

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论