Prhub

#45030 [Rust Frontend][Metrics] Export `vllm:lora_requests_info` from frontend

原始 PR 作者 wseaton 合并时间 2026-06-11 21:45 文件变更 8 提交数 6 评论 8 代码增减 +383 / -36

执行摘要

Rust 前端添加 LoRA 请求指标导出

需要暴露 LoRA 适配器指标供外部路由器(如 llm-d 的 lora-aware-routing 亲和性评分器)使用。该指标在 Python 前端已存在,Rust 前端需要与之兼容。

该 PR 值得重点关注其设计权衡:LoRA 信息来源的选择(前端跟踪 vs 调度器直接输出)。当前方案复制了 Python 前端的模式,未来可能需重新评估。此外,LoraAdapterNames 的 newtype 实现和 Prometheus label 编码方式可作参考。

讨论亮点

BugenZhao 指出从请求生命周期导出的指标可能不适合控制平面路由,因为它反映的是前端视图而非调度器真实状态,建议重新设计使用调度器端信息。作者 wseaton 回应先保持兼容性,后续再重新设计。BugenZhao 接受并批准,并推送了一个小提交用于重构(使用 newtype 替代自定义辅助函数)。

实现拆解

  1. 请求注册表扩展state.rs):在 TrackedRequest 中新增 lora: Option<LoraRequestState> 字段,添加 LoraPhase 枚举(Waiting / Running)和 LoraRequestState 结构体。register() 方法新增 lora_name: Option<String> 参数,创建 LoraRequestState 并初始为 Waiting 阶段。
  2. 事件驱动阶段更新state.rs):apply_lora_events() 方法根据引擎输出中的事件类型(Queued / Preempted 设置为 WaitingScheduled 设置为 Running)推进请求的 LoRA 阶段。该方法在 sender_for_output() 开始时调用,从而在每次输出处理时保持阶段最新。
  3. 适配器状态聚合state.rs):lora_adapter_states() 遍历所有活跃请求,收集运行中和等待中的适配器名称集合(BTreeSet<String>),分别返回给调用者。
  4. 指标导出器metrics.rs):新增 LoraInfoExporter 结构体,持有一个 current: Option<LoraInfoLabels> 缓存。update() 方法接收运行和等待集合,若标签集有变化则先移除旧系列,再使用当前时间戳(now_unix_secs())设置新 gauge。单元测试覆盖了 emit、replace、drain 等场景。
  5. 集成到输出循环imp.rs):在 run_output_dispatcher_loop() 中创建 LoraInfoExporter 实例,每次处理完引擎输出后调用 inner.lora_adapter_states() 获取快照并调用 lora_info.update()
  6. 字段清理stats.rs):从 SchedulerStats 中删除从未填充的 waiting_lora_adaptersrunning_lora_adapters 字段,因为这些信息由前端跟踪更准确。
  7. 调用链修改client.rs):在 EngineCoreClient::add_request() 中提取 lora_name 并传递给 register_request()
文件 模块 状态 重要度
rust/src/engine-core-client/src/client/state.rs 请求注册表 modified 8.84
rust/src/engine-core-client/src/metrics.rs 指标导出 modified 8.64
rust/src/metrics/src/scheduler.rs 指标定义 modified 6.82
rust/src/engine-core-client/src/client/imp.rs 客户端内部 modified 6.8

关键符号

LoraInfoExporter::update apply_lora_events lora_adapter_states register LoraAdapterNames::encode now_unix_secs run_output_dispatcher_loop

关键源码片段

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

实现 LoRA 阶段跟踪核心逻辑:添加 LoraPhase 枚举、LoraRequestState 结构体,修改 register 方法接受 lora_name,实现 apply_lora_events 和 lora_adapter_states。

/// LoRA 请求的调度阶段,与 Python 前端 `LoRARequestStates` 对应
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LoraPhase {
    Waiting,
    Running,
}/// 前端侧每个 LoRA 请求的状态
#[derive(Debug)]
struct LoraRequestState {
    adapter_name: String,
    phase: LoraPhase,
}/// 请求注册表中的条目扩展 LoRA 状态
#[derive(Debug)]
struct TrackedRequest {
    sender: OutputSender,
    engine_id: EngineId,
    lora: Option<LoraRequestState>, // 新增:LoRA 请求状态
}impl RequestRegistry {
    /// 注册请求时接受可选的 lora_name
    pub fn register(
        &mut self,
        request_id: String,
        lora_name: Option<String>, // 新增参数
        data_parallel_rank: Option<u32>,
    ) -> Result<(EngineId, OutputReceiver)> {
        // ... 原有逻辑不变 ...
        self.requests.insert(
            request_id,
            TrackedRequest {
                sender: tx,
                engine_id: engine_id.clone(),
                lora: lora_name.map(|adapter_name| LoraRequestState {
                    adapter_name,
                    phase: LoraPhase::Waiting, // 初始化为 Waiting
                }),
            },
        );
        // ...
    }    /// 根据引擎输出的事件推进 LoRA 阶段
    fn apply_lora_events(&mut self, output: &EngineCoreOutput) {
        let Some(events) = output.events.as_ref() else { return };
        let Some(lora) = self
            .requests
            .get_mut(output.request_id.as_str())
            .and_then(|t| t.lora.as_mut()) else { return };
        for event in events {
            lora.phase = match event.r#type {
                EngineCoreEventType::Queued | EngineCoreEventType::Preempted => LoraPhase::Waiting,
                EngineCoreEventType::Scheduled => LoraPhase::Running,
            };
        }
    }    /// 收集所有活跃 LoRA 请求的适配器名称,分为运行中和等待中两组
    pub fn lora_adapter_states(&self) -> (BTreeSet<String>, BTreeSet<String>) {
        let mut running = BTreeSet::new();
        let mut waiting = BTreeSet::new();
        for req in self.requests.values() {
            if let Some(lora) = &req.lora {
                match lora.phase {
                    LoraPhase::Running => { running.insert(lora.adapter_name.clone()); },
                    LoraPhase::Waiting => { waiting.insert(lora.adapter_name.clone()); },
                }
            }
        }
        (running, waiting)
    }
}
rust/src/engine-core-client/src/metrics.rs core-logic

实现 LoraInfoExporter 聚合指标并导出到 Prometheus gauge,提供单元测试验证 emit/replace/drain 语义。

/// 导出 `vllm:lora_requests_info` 系列,覆盖所有跨引擎的 LoRA 请求
#[derive(Default)]
pub(crate) struct LoraInfoExporter {
    current: Option<LoraInfoLabels>,
}impl LoraInfoExporter {
    pub(crate) fn update(
        &mut self,
        metrics: &SchedulerMetrics,
        running: BTreeSet<String>,
        waiting: BTreeSet<String>,
    ) {
        // 当没有活跃 LoRA 时返回 None
        let next = (!running.is_empty() || !waiting.is_empty()).then_some(LoraInfoLabels {
            running_lora_adapters: LoraAdapterNames(running),
            waiting_lora_adapters: LoraAdapterNames(waiting),
        });        // 如果标签集发生变化,移除旧的 Prometheus 系列
        if self.current != next
            && let Some(prev) = &self.current
        {
            metrics.lora_info.remove(prev);
        }        // 设置当前值:与 Python 前端一致,取 Unix 时间戳
        if let Some(labels) = &next {
            metrics.lora_info.get_or_create(labels).set(now_unix_secs());
        }        self.current = next;
    }
}/// 获取当前 Unix 时间戳(秒,浮点数)
fn now_unix_secs() -> f64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs_f64())
        .unwrap_or(0.0)
}#[cfg(test)]
mod tests {
    #[test]
    fn lora_info_emits_clears_stale_and_drains() {
        // 验证:初始无适配器时不发射;有适配器时正确发射;更新时替换旧系列;全部完成后清空。
    }
}

评论区精华

LoRA 指标来源设计:前端跟踪 vs 调度器端 设计

BugenZhao 指出从请求生命周期导出的指标可能不适合控制平面路由,因为它反映的是前端视图而非调度器真实状态,建议重新设计使用调度器端信息。

结论:作者 wseaton 回应先保持兼容性,后续再重新设计。BugenZhao 接受并批准。 · 已解决

风险与影响

  1. 锁竞争风险lora_adapter_states() 在持有 RequestRegistry 锁的情况下遍历所有请求,输出循环中每次 scheduler stats 更新都会调用,可能轻微增加锁竞争,但锁持有时间极短,且输出循环频率不高,风险低。
  2. 与 Python 前端行为一致性:指标语义必须与 Python 前端完全相同,否则下游路由系统可能产生歧义。当前实现已仔细对标,但未来若 Python 侧逻辑变化需同步。
  3. 指标值语义:Gauge 值是 Unix 时间戳而非计数,与 Python 前端保持一致。该设计虽非典型,但兼容现有路由系统。
  4. 缺失适配器负载状态:当前不暴露适配器的加载状态,仅报告正在运行的请求,主体已说明需后续通过 engine/proto 变更补充。

用户:Rust 前端现在暴露 vllm:lora_requests_info 指标,外部路由器(如 llm-d)可用其实现 LoRA 感知路由。删除从未填充的 SchedulerStats 字段不会影响现有用户,因这些字段从未被填充。
系统:指标导出在输出循环中同步完成,不会阻塞引擎核心,性能影响忽略。
团队:Rust 前端维护者需注意 LoRA 指标相关逻辑与 Python 前端的同步演进。

核心路径变更 锁竞争风险低 与 Python 前端行为一致性需维护

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论