Prhub

#29017 [model-gateway] PD router: cancel paired decode when prefill fails

原始 PR 作者 chenkaiyue 合并时间 2026-07-03 08:03 文件变更 1 提交数 2 评论 4 代码增减 +71 / -4

执行摘要

PD 路由:prefill 失败时立即取消 decode 请求

生产环境中,PD 分离部署的 prefill 节点故障(如过载、崩溃)会导致配对 decode 请求在 KVPoll.WaitingForInput 状态阻塞长达 300s(默认 SGLANG_DISAGGREGATION_WAITING_TIMEOUT),从而耗尽 decode 并发槽位,引发级联死锁,使集群恢复时间超过 1 小时。关联 Issue #28651 描述了类似场景。

值得精读,特别是 tokio::select! 在 PD 路由中的用法和断路器归属设计。merge 前建议补充测试覆盖 prefill 失败、decode 先完成、客户端断开等场景。

讨论亮点

Gemini Code Assist bot 提出优化建议:当 decode future 先于 prefill future 完成时,可以立即 fail-fast 并返回,因为 PD 模式下 decode 不可能在 prefill 之前成功。但作者 chenkaiyue 拒绝该建议,理由有三:① 极罕见情况下 decode 可能返回 2xx(如极短生成时序竞态),直接走错误处理会误报;② 早期返回会重复下游已有的 decode 错误处理和断路器逻辑;③ 性能收益甚微,因为 prefill 完成通常只需毫秒级,且 prefill 失败后才需要快速失败(此时已有优化)。

实现拆解

  1. tokio::join! 替换为 tokio::select!: 在 execute_dual_dispatch_internal 方法中,prefill 和 decode 的 future 被固定(tokio::pin!),并在一个 biasedselect! 循环中驱动,优先确保 prefill 完成。decode 如果先完成则暂存其结果(decode_early),但不中断循环。
  2. 判断 prefill 结果并快速失败: 当 prefill 返回非 2xx 状态码或传输错误时,记录日志并调用 process_prefill_response 生成错误响应(4xx 透传,5xx/传输错误转为 502),同时丢弃仍在进行的 decode future(通过离开作用域隐式 drop),关闭对应 HTTP 连接。这使 decode 引擎在约 4-8s 内检测到连接断开并调用 abort_request,释放解码槽位。
  3. 断路器归属处理: 根据 prefill 实际状态记录断路器结果(4xx 记为客户端错误,其余不记录),decode 端不记录(避免因 prefill 故障风暴误伤正常 decode 断路器)。响应中插入 BreakerOutcomesRecorded 扩展标记,通知外层调度器跳过重复记录。
  4. 正常路径保持不变: 若 prefill 成功,则从 decode_early 或再次 await decode future 获取结果,后续处理逻辑(状态检查、流式处理、断路器跟踪等)完全不变。
  5. 保留上游取消行为: 两个 future 仍然在同一个 handler 任务内执行(未使用 tokio::spawn),因此客户端断开连接会取消整个任务,进而取消 pending 的 decode 请求,避免孤立的 decode 继续运行。
文件 模块 状态 重要度
sgl-model-gateway/src/routers/http/pd_router.rs 路由 modified 7.08

关键符号

execute_dual_dispatch_internal

关键源码片段

sgl-model-gateway/src/routers/http/pd_router.rs data-contract

核心变更文件,PD 路由的并发控制从 `tokio::join!` 重构为 `tokio::select!`,实现 prefill 失败时快速关闭 decode 连接。

// sgl-model-gateway/src/routers/http/pd_router.rs// 将 prefill 和 decode 的 future 固定,确保它们在同一个任务中运行
let prefill_fut = prefill_request.send();
let decode_fut = decode_request.send();
tokio::pin!(prefill_fut);
tokio::pin!(decode_fut);let prefill_result;
let mut decode_early: Option<Result<reqwest::Response, reqwest::Error>> = None;
loop {
    tokio::select! {
        biased; // 优先 poll prefill,避免 decode 先完成时误判
        pr = &mut prefill_fut => {
            prefill_result = pr;
            break;
        }
        dr = &mut decode_fut, if decode_early.is_none() => {
            // decode 先完成则暂存结果,但不中断等待 prefill
            decode_early = Some(dr);
        }
    }
}// 判断 prefill 是否失败(非 2xx 或传输错误)
let prefill_failed = match &prefill_result {
    Ok(resp) => !resp.status().is_success(),
    Err(_) => true,
};if prefill_failed {
    warn!(
        "Prefill failed, aborting paired decode request decode_url={} prefill_url={}",
        decode.url(),
        prefill.url()
    );    // 仅记录 prefill 断路器结果,decode 不记录避免误伤
    let prefill_ok = match &prefill_result {
        Ok(r) => r.status().is_client_error(),
        Err(_) => false,
    };
    prefill.record_outcome(prefill_ok);    // 生成错误响应(4xx 透传,5xx/ 传输错误 -> 502)
    let mut response = match self
        .process_prefill_response(prefill_result, prefill.url(), false)
        .await
    {
        Err(error_response) => error_response,
        Ok(_) => error::bad_gateway(
            "prefill_server_error",
            "Prefill reported failure but returned a success response".to_string(),
        ),
    };
    response.extensions_mut().insert(BreakerOutcomesRecorded);
    return response;
}// Prefill 成功:获取 decode 结果(若尚未完成则等待)
let decode_result = match decode_early {
    Some(dr) => dr,
    None => (&mut decode_fut).await,
};// 后续处理与之前一致(状态检查、流式处理等)

评论区精华

Fail-Fast on Early Decode Failures 性能

Gemini 机器人建议当 decode future 先于 preflight future 完成时立即 fail-fast 并返回,因为 PD 模式下 decode 不可能成功。

结论:作者拒绝,理由:① decode 可能因竞态返回 2xx(虽罕见);② 会重复已有 decode 错误处理逻辑;③ 性能收益微乎其微。 · 已解决

风险与影响

  1. 回归风险:修改了 PD 路由的核心控制流,若 select!biased 行为理解有误,可能导致死锁或任务泄漏。但代码已通过 tokio::pin! 确保 future 生命周期正确。
  2. 兼容性风险:断路器记录逻辑变更(decode 不记录),若其他组件依赖 decode 断路器计数,可能影响熔断判断。但作者声明该变更仅影响 prefill 失败场景,且 BreakerOutcomesRecorded 标记可防止外层重复记录。
  3. 无测试配套:本次变更仅修改源码文件,未包含测试文件,可能遗漏边缘场景(如两 future 同时完成、decode 先完成且为 2xx)。
  1. 用户影响:prefill 失败时,错误响应返回时间从 300s 缩短至约 0.4s(prefill 响应时间 + 处理时间),decode 插槽释放时间从 300s 缩短至约 4-8s。
  2. 系统影响:显著降低 PD 模式下 prefill 故障对 decode 并发度的冲击,提升集群恢复速度。正常请求路径无性能变化。
  3. 团队影响:涉及 sgl-model-gateway 模块的核心路由逻辑,需要 @Kangyan-Zhou 等熟悉 PD 路由的成员 review。
核心路径变更 缺少测试覆盖 并发模型变更

关联 Issue

#28651 [disaggregation][mooncake] Notify decode peer when a prefill request is aborted

完整报告

参与讨论