Prhub

#47173 [Frontend] Add /abort_requests to the RLHF dev API router

原始 PR 作者 aoshen02 合并时间 2026-07-12 14:21 文件变更 8 提交数 11 评论 12 代码增减 +69 / -12

执行摘要

为 RLHF dev API 路由添加 /abort_requests 端点

RL rollout框架需要在步骤边界丢弃过采样/长尾生成,同时保留部分输出。现有的/pause?mode=abort会暂停调度器需要/resume,使用不便。disagg路由器已有等效/abort_requests,现在为RLHF开发路由器添加相同能力。

值得精读,特别是跨语言实现相同语义的设计(Python直接收集内部ID,Rust通过空向量表示中止所有)。注意Rust端跟踪时机窗口问题,未来需关注是否引入锁或调整注册时机。

讨论亮点
  • 内部ID标记问题:Codex评论指出空body路径需要设置internal=True否则abort无效,作者后续commit修复。
  • Malformed JSON处理:Codex建议区分空body与malformed JSON避免误终止所有请求,作者通过捕获JSONDecodeError返回400修复。
  • 并行采样父ID泄漏:Codex指出中止所有时只收集子请求ID导致父条目残留,作者通过添加parent_requests.keys()修复。
  • Rust端跟踪时机:Codex提出abort-all可能遗漏尚未跟踪的请求(因为Llm::generate()在await后才添加跟踪),未在PR中解决。
  • 计数不准确:Codex指出返回的aborted计数可能包含未实际中止的ID,未修复。

实现拆解

  1. Python API路由层:在vllm/entrypoints/serve/dev/rlhf/api_router.py中添加POST /abort_requests异步处理函数。解析请求体,若request_ids存在则直接调用engine.abort(request_ids);若缺失则从AsyncLLM.output_processor收集所有内部请求ID(包括父请求ID),并调用engine.abort(request_ids, internal=True)以中止所有请求。同时处理JSONDecodeError返回400。
  2. Rust前端路由层:在rust/src/server/src/routes/abort_requests.rs中将request_ids从必填改为可选,使用unwrap_or_default()替代ok_or_else错误返回,空body时传递空向量给后端。
  3. Rust LLM内核:在rust/src/llm/src/lib.rs中扩展Llm::abort方法:当external_ids为空时调用self.inflight.all_internal_ids()收集所有跟踪的内部ID,否则使用原resolve逻辑。
  4. 跟踪机制:在rust/src/llm/src/inflight.rs中新增all_internal_ids方法,从锁保护的HashMap中收集所有内部ID。
  5. 测试与文档:在rust/src/server/src/routes/tests.rs中将空body预期从400改为200并重命名测试函数;同步更新三份文档文件添加端点说明。
文件 模块 状态 重要度
vllm/entrypoints/serve/dev/rlhf/api_router.py API 路由 modified 7.59
rust/src/server/src/routes/abort_requests.rs Rust 路由 modified 5.96
rust/src/llm/src/lib.rs Rust 内核 modified 5.92
rust/src/llm/src/inflight.rs 请求跟踪 modified 5.47
rust/src/server/src/routes/tests.rs 测试套件 modified 5.83
docs/serving/online_serving/README.md 文档 modified 1.18
docs/training/async_rl.md 文档 modified 1.18
docs/usage/security.md 文档 modified 1.18

关键符号

abort_requests abort_requests (Rust route) abort (Llm) all_internal_ids (InflightRequests) abort_requests_route_rejects_missing_request_ids (test) abort_requests_route_aborts_all_when_request_ids_missing (test)

关键源码片段

vllm/entrypoints/serve/dev/rlhf/api_router.py entrypoint

核心入口,添加 POST /abort_requests 端点,处理指定或全部请求中止,并包含关键修复(internal=True、parent IDs、JSON 拒绝)。

@router.post("/abort_requests")
async def abort_requests(raw_request: Request) -> JSONResponse:
    """Abort in-flight requests without pausing the scheduler.    Empty/missing `request_ids` aborts all in-flight requests.
    """
    engine = engine_client(raw_request)
​
    # Parse JSON body; reject malformed JSON explicitly
    try:
        body = await raw_request.json()
    except json.JSONDecodeError as e:
        raise HTTPException(status_code=400, detail="Invalid JSON format") from e
​
    request_ids = body.get("request_ids")
​
    try:
        if request_ids:
            # User-supplied external IDs
            await engine.abort(request_ids)
        else:
            # Dev RL server uses AsyncLLM; gather all internal IDs
            from vllm.v1.engine.async_llm import AsyncLLM
            assert isinstance(engine, AsyncLLM)
            op = engine.output_processor
            # Include both child (request_states) and parent (parallel-sampling) IDs
            request_ids = [
                *op.request_states.keys(),
                *op.parent_requests.keys(),
            ]
            # Internal flag is required because these are internal suffixed IDs
            await engine.abort(request_ids, internal=True)
        return JSONResponse(
            content={"status": "aborted", "aborted": len(request_ids)},
            status_code=HTTPStatus.OK.value,
        )
    except Exception as err: # pragma: no cover - defensive
        logger.exception("Failed to abort requests")
        return JSONResponse(
            content={"error": f"Failed to abort requests: {err}"},
            status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
        )
rust/src/server/src/routes/abort_requests.rs entrypoint

Rust 前端路由,修改 request_ids 从必填变为可选,实现空 body 转发。

use std::sync::Arc;use axum::Json;
use axum::extract::State;
use axum::extract::rejection::JsonRejection;
use axum::http::StatusCode;
use serde::Deserialize;use crate::error::ApiError;
use crate::state::AppState;
use crate::utils::utility_call_error;#[derive(Debug, Deserialize)]
pub(crate) struct AbortRequestsRequest {
    // `request_ids` is now optional; missing/unwrapped defaults to empty vec
    request_ids: Option<Vec<String>>,
}pub async fn abort_requests(
    State(state): State<Arc<AppState>>,
    body: Result<Json<AbortRequestsRequest>, JsonRejection>,
) -> Result<StatusCode, ApiError> {
    let Json(body) = body.map_err(|error| ApiError::json_parse_error(error.body_text()))?;
    // Empty/missing `request_ids` aborts all in-flight requests.
    let request_ids = body.request_ids.unwrap_or_default();    state
        .chat
        .abort(&request_ids)
        .await
        .map_err(|error| utility_call_error("abort_requests", error))?;    Ok(StatusCode::OK)
}
rust/src/llm/src/lib.rs core-logic

Rust LLM 核心,扩展 abort 方法以支持空 external_ids 时中止所有请求。

    /// Abort in-flight requests by their external (user-supplied) request ids.
    ///
    /// External ids are resolved to the internal engine ids actually known to
    /// engine-core (one external id may map to several internal ids). Unknown
    /// or already-finished ids resolve to nothing and are a safe no-op. The
    /// tracking entries themselves are removed when the corresponding output
    /// streams are dropped, not here.
    pub async fn abort(&self, external_ids: &[String]) -> Result<()> {
        // Empty `external_ids` means abort every in-flight request.
        let internal_ids = if external_ids.is_empty() {
            self.inflight.all_internal_ids()
        } else {
            self.inflight.resolve(external_ids)
        };
        if internal_ids.is_empty() {
            return Ok(());
        }
        self.client.abort(&internal_ids).await?;
        Ok(())
    }

评论区精华

空 body 时未使用 internal=True 导致 abort 无效 正确性

Codex 指出空 body 路径(中止所有)需要 internal=True,否则 AsyncLLM.abort() 将内部 ID 当作外部 ID 处理,找不到请求。

结论:作者在后续 commit 中添加了 internal=True。 · 已解决

Malformed JSON 被当作空 body 可能误终止所有请求 正确性

Codex 建议区分空 body 与 malformed JSON,避免错误的 JSON 导致所有请求被意外中止。

结论:作者通过捕获 JSONDecodeError 并返回 400 修复。 · 已解决

并行采样父 ID 泄漏 正确性

Codex 指出中止所有时只收集了子请求 ID,父请求条目残留。

结论:作者通过添加 parent_requests.keys() 修复。 · 已解决

Rust 端 abort-all 遗漏尚未跟踪的请求 正确性

Codex P1 指出 abort-all 只看到已在 InflightRequests 中的请求,而 Llm::generate() 在 client.call 之后才添加跟踪,存在时间窗口。

结论:未在 PR 中修复,可能认为影响有限或后续处理。 · unresolved

风险与影响

  1. 空body语义可能误终止所有请求,尤其在Rust端跟踪时机窗口内请求未完全注册时。
  2. 并行采样父ID泄漏问题已修复,但返回的计数仍可能不准确。
  3. 依赖AsyncLLM内部结构(output_processor、parent_requests),未来重构可能引发兼容性问题。
  4. Rust端all_internal_ids仅覆盖已注册请求,generate()client.call之前发生的abort-all会遗漏。
  • 用户:RL训练框架开发者获得更精细的请求控制,无需暂停/恢复调度器,简化rollout流程。
  • 系统:新增一个端点,对整体性能无影响。
  • 团队:需要维护Python和Rust两个实现,但逻辑一致,降低长期维护成本。
空 body 误终止 并行父 ID 泄漏(已修复) Rust 跟踪窗口遗漏 aborted 计数不准确

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论