Prhub

#33118 [PD] Fix false health-503 during decode retraction re-admission

原始 PR 作者 cctry 合并时间 2026-08-04 00:44 文件变更 1 提交数 2 评论 2 代码增减 +11 / -0

执行摘要

修复 PD 解码回退期间健康探测误报 503

PR body 明确指出:is_fully_idle(for_health_check=True) 没有统计 retracted_queue,一旦 running_batch/waiting_queue 排空,健康探测会认为引擎空闲,注入的 fake-bootstrap 请求直到重新接纳完成前无法推进,超时转为 503,而“A router acting on that 503 can eject a healthy decode engine and fail requests with no available engines”。非 PD 模式不受影响,因为被回退的请求会回到 waiting_queue,该队列本来就被统计。

建议优先阅读 python/sglang/srt/managers/scheduler.pyis_fully_idleon_idle 的改动。这是 PD 健康检查核心路径上一个精准的小修复,展示了健康探测与调度队列语义的耦合方式:探测不能只依赖 running_batch,还要把自愈中的回退队列算作活跃。值得关注的设计点是 _engine_paused 豁免和 on_idle 信号补发时序。若团队后续完善 PD 健康检查,应补充针对 retracted_queue 卡住、暂停并发等场景的单元测试。

讨论亮点

本 PR 无实质 review 评论,也没有 review 线程。Issue 区仅有两条自动化消息:Gemini Code Assist 通知(消费者版已停止服务,无代码审查活动)以及作者触发的 /tag-and-rerun-ci CI 重跑指令。因此缺少关于 _engine_paused 豁免语义、retracted_queue 长期卡住时健康检查行为等问题的公开讨论。

实现拆解

本 PR 只修改 python/sglang/srt/managers/scheduler.py,共 2 处核心逻辑调整:

  1. is_fully_idle 健康检查分支补齐 retracted_queue 统计:在原有的 idle &= len(self.waiting_queue) == 0 之后,新增一个带条件的前置判断。仅当满足 for_health_check=True、引擎未暂停(not self._engine_paused)、disaggregation_mode == DisaggregationMode.DECODE 且 prealloc 队列对象存在时,才把 len(self.disagg_decode_prealloc_queue.retracted_queue) == 0 并入 idle 条件。这样解码回退再接纳期间引擎被判定为非空闲,健康探测不会误注入 fake-bootstrap 请求。

  2. on_idle 入口补发延迟的健康检查信号:在原有 if not self.is_fully_idle(): return 之前插入 self.maybe_send_health_check_signal(),把引擎繁忙期间推迟的信号在事件循环空闲检查开始时立即发送,避免探测等待到下一个调度周期。

  3. 条件收敛与兼容性:非健康检查路径原有的 retracted_queue 统计保持不变;引擎暂停(_engine_paused)时沿用旧语义,不把该队列计入忙碌,避免与暂停逻辑冲突。

  4. 测试与配套:本 PR 未新增自动化测试,也未改动配置、schema 或部署脚本;第二个 commit 为 merge main 解决冲突。修复在内部 PD 环境验证:KV 压力下的解码回退不再使 /health_generate 在重新接纳期间翻转 503。

文件 模块 状态 重要度
python/sglang/srt/managers/scheduler.py 调度器 modified 5.98

关键符号

on_idle is_fully_idle maybe_send_health_check_signal

关键源码片段

python/sglang/srt/managers/scheduler.py core-logic

唯一的变更文件,承载 PD 健康检查空闲判定的核心逻辑:`is_fully_idle` 补统计 retracted_queue,`on_idle` 补发延迟健康检查信号,直接决定 `/health_generate` 的探测行为。

def on_idle(self):
    """Idle housekeeping: guard, check, metrics, reset, sleep."""
    # 引擎繁忙期间延迟的健康检查信号在这里补发,
    # 避免探测一直等到下一个空闲周期才被应答
    self.maybe_send_health_check_signal()
​
    if not self.is_fully_idle():
        return
​
    # 以下为原有空闲收尾逻辑:统一内存 flush、内存泄漏检查、
    # tree cache 检查、指标上报、KV 事件发布、负载快照与睡眠等
    # (完整实现详见仓库 head 版本)
​
​
def is_fully_idle(self, for_health_check=False) -> bool:
    # 健康检查依赖 process_output 中真实运行的请求来带活;
    # disagg 各类队列(bootstrap / prealloc / transfer)可能只有
    # 排队请求却没有正在 GPU 上执行的批次(例如握手卡住、KV 满、
    # 传输停滞),因此它们不能单独作为健康依据。
    idle = (
        self.running_batch.is_empty()
        and self.chunked_req is None
        and not self.dllm_manager.any_staging_reqs()
        and (self.last_batch is None or self.last_batch.is_empty())
        and (not self.enable_overlap or len(self.result_queue) == 0)
        and self._pp_microbatches_drained()
    )
​
    # 普通等待队列:waiting + bootstrapping + preallocation + kv transfer (decode)
    idle &= len(self.waiting_queue) == 0
​
    # 健康检查路径:decode 引擎在 KV 压力下会把运行中的请求回退到
    # retracted_queue 等待重新接纳,此时引擎仍在自愈,必须视为活跃;
    # 但引擎显式暂停(_engine_paused)时不干扰原探测语义。
    if (
        for_health_check
        and not self._engine_paused
        and self.disaggregation_mode == DisaggregationMode.DECODE
        and self.disagg_decode_prealloc_queue is not None
    ):
        idle &= len(self.disagg_decode_prealloc_queue.retracted_queue) == 0
​
    if not for_health_check:
        # 非健康检查路径:grammar 队列、prefill inflight 队列等
        # 也会影响引擎是否空闲的判断。
        idle &= len(self.grammar_manager.grammar_queue) == 0
        if self.disaggregation_mode == DisaggregationMode.PREFILL:
            idle &= len(self.disagg_prefill_inflight_queue) == 0
            idle &= len(self.disagg_prefill_bootstrap_queue.queue) == 0
        if self.disaggregation_mode == DisaggregationMode.DECODE:
            idle &= len(self.disagg_decode_prealloc_queue.queue) == 0
            idle &= len(self.disagg_decode_prealloc_queue.retracted_queue) == 0
            idle &= len(self.disagg_decode_transfer_queue.queue) == 0
            if self.decode_offload_manager is not None:
                idle &= len(self.decode_offload_manager.ongoing_offload) == 0
        # HiSparse staging 与 HiCache 异步操作检查此处省略
​
    return idle

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

改动位于调度器健康检查核心路径,主要风险如下:

  1. retracted_queue 非空即视为忙碌的语义风险python/sglang/srt/managers/scheduler.py):如果 retracted_queue 因异常长期卡住(例如重新接纳流程未完成),健康检查会持续判定引擎非空闲,可能掩盖引擎失联。这是刻意取舍:引擎处于自愈中就不应响应探测。

  2. _engine_paused 豁免的不确定性:暂停状态下条件不成立,retracted_queue 不会被计入忙碌,此时健康探测可能给出 idle 结论。暂停与回退并发时的语义需要确认,现有代码注释未展开说明。

  3. on_idle 信号时序变化maybe_send_health_check_signal() 被提前到 idle 判断之前调用,虽然只应在存在延迟信号时发送,但其幂等性和并发时序未经测试覆盖,可能引入重复信号。

  4. 缺少自动化测试:本 PR 没有配套测试文件,修复仅靠内部 PD 环境验证,回归风险由后续 CI 承担。

影响范围集中在 PD(prefill-decode disaggregation)部署的调度器健康检查路径:修复后,解码引擎在 KV 压力下执行回退再接纳(retraction re-admission)期间,/health_generate 不会误报 503,路由器不会因此剔除健康引擎,也不会出现 no available engines 的连锁请求失败。非 PD 模式完全不受影响,因为新增逻辑严格限定在 for_health_checkDisaggregationMode.DECODE 条件下。改动仅增加 11 行判断,不改变 GPU 执行路径与性能特征,对团队而言提升了 PD 场景的运维稳定性。

核心路径变更 缺少测试覆盖 条件行为依赖队列语义

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论