Prhub

#45137 [Rust Frontend] Add external→internal request-id map for abort()

原始 PR 作者 sahilsGit 合并时间 2026-06-15 14:51 文件变更 13 提交数 1 评论 10 代码增减 +447 / -27

执行摘要

为 Rust 前端 Llm 添加 abort() 及请求 ID 映射

此 PR 旨在为 Rust 前端提供与 Python AsyncLLM.abort() 等同的中止功能。用户可以通过外部(用户提供的)请求 ID 中止正在进行的生成请求,而无需知道引擎内部随机化后的 ID。PR 解决了外部 ID 到内部 ID 的解析问题,为后续用户级 /abort_requests 端点(#44382)奠定基础。

值得精读,特别是 InflightRequests 的 RAII guard 模式和本地合成中止输出流的实现思路。后续在实现并行采样或跨引擎中止时可以参考此设计。

讨论亮点
  • 本地合成中止输出:tahsintunan 指出在无内部负载均衡时引擎不会为客户端中止返回 finished_requests,导致流永远不 yield。BugenZhao 确认并建议通过 ClientInner 在本地合成终端输出。最终采纳新增 abort_many 方法在 RequestRegistry 层面发送 FinishReason::Abort 输出。
  • external_id 字段访问方式:tahsintunan 建议 PreparedGenerateRequest 直接暴露 external_id 字段以避免 .expect()。Sahil 认为 .expect() 作为不可达断言可以明确语义且不冗余,决定保持现状。
  • 未来并行采样扩展:tahsintunan 提出当未来支持 n>1 并行采样时,resolve 需要展开父请求 ID 为子请求 ID。已添加 TODO 注释,当前 n=1 无需处理。

实现拆解

  1. 新建 InflightRequests 模块 (rust/src/llm/src/inflight.rs):定义 InflightMap(HashMap<String, HashMap<String, usize>>)受 Arc<Mutex<>> 保护。提供 track() 记录映射并返回 RAII RequestGuard,在请求流 drop 时自动递减引用计数并清理映射。resolve() 将外部 ID 列表解析为当前活跃的内部 ID 列表。
  2. 修改 Llm 结构 (rust/src/llm/src/lib.rs):新增 inflight 字段。在 generate() 中调用 client.call 成功后通过 inflight.track() 记录映射,并将 guard 存入 GenerateOutputStream。新增 abort() 方法:调用 inflight.resolve() 获取内部 ID,然后委托 client.abort() 发送中止请求。
  3. 暴露高层 API (rust/src/text/src/lib.rs, rust/src/chat/src/lib.rs):TextLlm 和 ChatLlm 分别添加 abort() 方法,直接委托给内部 Llm::abort()。
  4. 本地合成中止输出 (rust/src/engine-core-client/src/client/state.rs, rust/src/engine-core-client/src/client/imp.rs):RequestRegistry 新增 abort_many() 方法,从注册表移除请求并立即发送 FinishReason::Abort 终端输出。ClientInner 通过 abort_requests_locally() 包装,确保调用者不会因流无输出而挂起。
  5. 修复指标记录 (rust/src/llm/src/request_metrics.rs):在 observe_output 中,当输出没有新 token 时(如合成中止输出),跳过 time-to-first-token 和 inter-token 延迟的观测,避免错误统计。
  6. 测试覆盖:inflight.rs 新增 5 个单元测试,验证映射解析、重复外部 ID、引用计数、guard 清理等。generate.rs 新增 2 个集成测试,验证外部 ID 到内部 ID 的解析以及单个外部 ID 中止多个内部请求的行为。
文件 模块 状态 重要度
rust/src/llm/src/inflight.rs 请求追踪 added 8.78
rust/src/engine-core-client/src/client/state.rs 请求注册表 modified 7.16
rust/src/llm/src/lib.rs Llm 核心 modified 7.01
rust/src/engine-core-client/src/client/imp.rs 客户端内部 modified 6.65
rust/src/llm/tests/generate.rs 集成测试 modified 6.57
rust/src/llm/src/request_metrics.rs 指标追踪 modified 6.0

关键符号

InflightRequests::new InflightRequests::track InflightRequests::resolve RequestGuard::drop Llm::new Llm::generate Llm::abort TextLlm::abort ChatLlm::abort RequestRegistry::abort_many ClientInner::abort_requests_locally RequestMetricsTracker::observe_output

关键源码片段

rust/src/llm/src/inflight.rs core-logic

核心新模块,定义了 InflightRequests 映射和 RAII guard

//! 追踪外部到内部请求 ID 映射,用于支持通过外部 ID 中止请求。
//! 当启用请求 ID 随机化时,引擎核心只知道内部 ID,
//! 因此需要通过此映射将用户提供的外部 ID 解析为内部 ID。
​
use std::collections::HashMap;
use std::sync::{Arc, Weak};
use parking_lot::Mutex;
​
/// 外部 ID → 内部 ID → 活跃 guard 计数。
type InflightMap = HashMap<String, HashMap<String, usize>>;
​
/// 管理所有正在飞行中的外部→内部 ID 映射。
/// 同一个外部 ID 可能对应多个内部 ID(重复提交相同外部 ID 时每个有自己的随机 ID)。
/// 同一 (external, internal) 对可能被多个 guard 持有引用计数。
#[derive(Default)]
pub(crate) struct InflightRequests {
    map: Arc<Mutex<InflightMap>>,
}
​
impl InflightRequests {
    pub(crate) fn new() -> Self {
        Self::default()
    }
​
    /// 记录 `external` → `internal` 关系,返回一个 guard,
    /// 当请求输出流被 drop 时(正常完成或取消)自动移除该边。
    pub(crate) fn track(&self, external: String, internal: String) -> RequestGuard {
        *self
            .map
            .lock()
            .entry(external.clone())
            .or_default()
            .entry(internal.clone())
            .or_insert(0) += 1;
        RequestGuard {
            map: Arc::downgrade(&self.map),
            external,
            internal,
        }
    }
​
    /// 将一组外部 ID 解析为它们对应的、当前仍在飞行中的内部 ID。
    /// 未知或已完成的 ID 贡献空结果。
    pub(crate) fn resolve(&self, external_ids: &[String]) -> Vec<String> {
        let map = self.map.lock();
        external_ids
            .iter()
            .filter_map(|external| map.get(external))
            .flat_map(|internal_ids| internal_ids.keys())
            .cloned()
            .collect()
    }
​
    #[cfg(test)]
    fn is_empty(&self) -> bool {
        self.map.lock().is_empty()
    }
}
​
/// RAII guard:当 drop 时释放一个外部→内部边的引用计数。
/// 如果引用计数降为 0,则移除该内部 ID;如果外部 ID 下所有内部 ID 都被移除,则清理该外部 ID 条目。
/// 使用 `Weak` 引用,所以即使 guard 存活超过 `InflightRequests` 实例不会阻止其释放。
pub(crate) struct RequestGuard {
    map: Weak<Mutex<InflightMap>>,
    external: String,
    internal: String,
}
​
impl Drop for RequestGuard {
    fn drop(&mut self) {
        let Some(map) = self.map.upgrade() else {
            return; // InflightRequests 已被释放,无需清理
        };
        let mut map = map.lock();
        if let Some(internal_ids) = map.get_mut(&self.external) {
            if let Some(count) = internal_ids.get_mut(&self.internal) {
                *count -= 1;
                if *count == 0 {
                    internal_ids.remove(&self.internal);
                }
            }
            if internal_ids.is_empty() {
                map.remove(&self.external);
            }
        }
    }
}
rust/src/engine-core-client/src/client/state.rs core-logic

新增 abort_many 方法,在注册表层合成终端中止输出,避免流挂起

/// 完成客户端发起的中止:从注册表中移除每个请求,并向其流发送一个终端输出(finish_reason = Abort),
/// 然后 drop 掉发送者。返回仍然活跃的请求 ID。
pub fn abort_many<'a>(
    &mut self,
    request_ids: impl IntoIterator<Item = &'a String>,
    timestamp: f64,
) -> Vec<String> {
    let mut aborted = Vec::new();
    for request_id in request_ids {
        let Some((sender, engine_id)) = self.remove(request_id) else {
            continue; // 请求已经完成或不存在,跳过
        };
        // 合成一个终端输出,通知消费者该请求已被中止
        let output = EngineCoreStreamOutput {
            engine_index: engine_id.engine_index().unwrap_or(0),
            timestamp,
            output: EngineCoreOutput {
                request_id: request_id.clone(),
                finish_reason: Some(EngineCoreFinishReason::Abort),
                ..EngineCoreOutput::default()
            },
        };
        let _ = sender.send(Ok(output));
        aborted.push(request_id.clone());
    }
    aborted
}

评论区精华

本地合成中止输出以修复流挂起 正确性

tahsintunan 指出在无内部负载均衡时引擎不会为客户端中止返回 finished_requests,导致流永远不 yield。BugenZhao 建议通过 ClientInner 在本地合成终端输出。

结论:新增 abort_many 方法,在 RequestRegistry 层面发送 FinishReason::Abort 输出 · 已解决

external_id 字段访问方式设计 设计

tahsintunan 建议 PreparedGenerateRequest 直接暴露 external_id 字段以避免 expect。Sahil 认为 expect 作为不可达断言可以明确语义且不冗余。

结论:保持使用 expect,不更改结构体 · 已解决

未来并行采样需扩展 resolve 以处理 parent-to-children 设计

tahsintunan 提出当 n>1 时,resolve 需要展开父请求 ID 为子请求 ID。

结论:已添加 TODO 注释,当前 n=1 无需处理 · acknowledged

风险与影响

  • 新功能与已有生成流程隔离良好,仅影响显式调用 abort 的路径。
  • 本地合成中止输出可能干扰正常输出处理,但 request_metrics 已调整为跳过无 token 输出,风险可控。
  • 请求 ID 映射在随机化关闭时的多 guard refcount 边界情况测试未完全覆盖,但当前逻辑正确。
  • 多引擎或外部分发场景下本地合成行为已讨论,风险已通过 abort_many 缓解。
  • 用户:Rust 前端用户现在可以中止请求,与 Python 前端能力对齐。
  • 系统:新增 InflightRequests 和本地中止路径,不改变已有请求处理核心流程。
  • 团队:本 PR 是 Rust 前端请求生命周期管理的重要补充,为后续用户级 /abort_requests 端点(#44382)提供底层支持。
  • 影响范围:限于 Rust 前端相关 crate(vllm-llm, vllm-chat, vllm-text, vllm-engine-core-client),不影响 Python 后端或其他语言前端。
流挂起风险已修复 外部 ID 随机化映射 合成输出可能影响指标

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论