Prhub

#23189 feat(scheduler): add adaptive queue-based prefill delayer trigger

原始 PR 作者 YAMY1234 合并时间 2026-05-09 07:54 文件变更 6 提交数 16 评论 16 代码增减 +246 / -8

执行摘要

新增队列感知预填充延迟触发器

基准测试显示,当随机范围比从 1.0 变为 0.8(OSL 从固定变为区间采样)时,输出吞吐从 7830 tok/s 降至 3846 tok/s,下降约 51%。根因分析表明:OSL 方差导致请求逐个结束,调度器每次仅回填 1-2 个请求的小预填充,预填充步骤数增加 62×,p99 解码间隙从 27.6ms 暴涨至 396.9ms。现有的 slot 触发器(--prefill-delayer-token-usage-low-watermark)仅当 max_running_requests - running_batch < max_prefill_bs 时触发,而在聚合部署中 max_running_requests 远大于 max_prefill_bs,因此该条件永不满足,无法阻止碎片化。

推荐精读。该 PR 通过精细的调度优化解决了负载方差导致的吞吐 collapse 问题,设计上保持了与原有触发器的兼容性和可选的启用方式。重点关注其队列触发与 slot 触发的组合逻辑、挂钟超时兜底的设计,以及跨 rank 同步扩展的方式。评论区的讨论(尤其是超时范围争议)也值得回顾。

讨论亮点
  • Fridge003 评论:询问能否用已有参数 --prefill-delayer-forward-passes-buckets 等实现队列触发。YAMY1234 回复:这些参数仅用于 Prometheus 直方图桶配置,不控制触发逻辑,因此需新增参数。

  • Fridge003 评论:询问 slot_trigger 的含义。YAMY1234 将其重命名为 slot_condition 以提高可读性。

  • gemini-code-assist[bot] 评论:指出超时机制同时影响 slot 触发和队列触发,可能导致 slot 限制仍存在时误释放。YAMY1234 在后续提交中修复,将 timeout 仅应用于队列触发组件。

  • Fridge003 要求:添加测试和更新文档。YAMY1234 已补充。最终 Fridge003 批准合并。

实现拆解

  1. 新增 CLI 参数:在 server_args.pyServerArgs 中添加 prefill_delayer_queue_min_ratio(可选 float)和 prefill_delayer_max_delay_ms(可选 float),并注册对应的 argparse 参数。

  2. 扩展全局同步通道:在 prefill_delayer.py 中,将跨 rank 的 _global_info_buffer 从 4 列扩展为 5 列,新增 waiting_queue_len 字段;修改 _gather_info 方法以同步该列。

  3. 传递等待队列长度:在 scheduler.py_get_new_batch_prefill_raw 中,将 len(self.waiting_queue) 传入 PrefillAdder;在 schedule_policy.pyPrefillAdder.__init__ 中存储 self.waiting_queue_len,并在 add_one_req 调用 prefill_delayer_single_pass 时传递该值。

  4. 实现队列触发决策:在 PrefillDelayer._negotiate_should_allow_prefill_pure 中新增队列触发逻辑:计算 queue_min = min(running_req * ratio, max_prefill_bs),若所有 rank 的 waiting_queue_len 最小值小于 queue_min 则触发延迟;同时记录延迟起始时间,当超过 max_delay_ms 时强制释放(超时兜底)。该队列触发与原有 slot 触发是 OR 关系。

  5. 配套测试与文档:在 test_prefill_delayer.py 中新增 4 个单元测试用例(queue_trigger_delayqueue_trigger_immediatequeue_trigger_wall_clock_timeoutqueue_trigger_no_effect_on_slot_trigger),覆盖队列触发延迟、立即放行、超时释放以及与 slot 触发共存等场景。同步在 server_arguments.md 中添加两个新参数的说明表格。

文件 模块 状态 重要度
python/sglang/srt/managers/prefill_delayer.py 预填充延迟器 modified 6.87
test/registered/scheduler/test_prefill_delayer.py 测试 modified 6.35
python/sglang/srt/server_args.py 配置 modified 6.21
python/sglang/srt/managers/schedule_policy.py 策略 modified 5.13
python/sglang/srt/managers/scheduler.py 调度器 modified 4.56
docs/advanced_features/server_arguments.md 文档 modified 1.89

关键符号

PrefillDelayer.__init__ PrefillDelayer._negotiate_should_allow_prefill_pure PrefillDelayer._gather_info PrefillAdder.__init__ Scheduler._get_new_batch_prefill_raw

关键源码片段

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

核心文件,实现队列触发逻辑,包括配置读取、全局缓冲区扩展、触发决策

# 片段 1:初始化部分的队列触发配置与全局缓冲区扩展
class PrefillDelayer:
    def __init__(
        self,
        dp_size: int,
        attn_tp_size: int,
        cpu_group,
        server_args,
        max_delay_passes: int,
        token_usage_low_watermark: Optional[float],
        metrics_collector: Optional["SchedulerMetricsCollector"] = None,
        device: Optional[torch.device] = "cpu",
    ):
        self._max_delay_passes = max_delay_passes
        self._token_usage_low_watermark = token_usage_low_watermark
​
        # 队列触发配置:从 server_args 读取,仅当 queue_min_ratio 非 None 时启用
        self._queue_min_ratio = server_args.prefill_delayer_queue_min_ratio
        self._max_delay_ms = server_args.prefill_delayer_max_delay_ms
        if self._max_delay_ms is None:
            self._max_delay_ms = 5000.0 # 默认 5s 挂钟超时兜底
        self._queue_trigger_enabled = self._queue_min_ratio is not None
​
        # 全局信息缓冲区从 4 列扩展为 5 列,新增 waiting_queue_len
        dp_size_dim = dp_size if self.enable_dp_attention else 1
        self._global_info_buffer = torch.empty(
            (dp_size_dim, attn_tp_size, 5),
            dtype=torch.int64,
            device=device,
        )
        # 其余初始化省略 ...
# 片段 2:队列触发决策逻辑(位于 _negotiate_should_allow_prefill_pure 中)
# 全局信息同步后,从 tp0_info 提取各字段
global_waiting_queue_len = tp0_info[:, 4]# 判断队列触发条件
queue_trigger = False
timeout_triggered = False
if self._queue_trigger_enabled:
    # 计算最小等待队列阈值:min(running_req * ratio, max_prefill_bs)
    queue_min = min(
        max_running_requests * self._queue_min_ratio,
        global_max_prefill_bs.min().item()
    )
    # 当所有 rank 的等待队列长度均小于阈值时触发延迟
    queue_trigger = global_waiting_queue_len.min().item() < queue_min
    if queue_trigger and prev_state is not None:
        elapsed_ms = (time.perf_counter() - prev_state.start_time) * 1000
        if elapsed_ms >= self._max_delay_ms:
            # 挂钟超时,强制放行
            timeout_triggered = True# slot 触发条件(原有逻辑)
slot_condition = (
    global_max_prefill_bs.min().item() > 0
    and global_running_batch.min().item() + global_max_prefill_bs.min().item() > max_running_requests
)# 综合决策:队列触发或 slot 触发任一为 True 且未超时且无节水印强制放行,则延迟
should_delay = (
    (slot_condition or queue_trigger)
    and not timeout_triggered
    and not global_exists_token_watermark_force_allow
)
test/registered/scheduler/test_prefill_delayer.py test-coverage

新增单元测试覆盖队列触发场景,验证核心决策逻辑的正确性

# 片段 3:测试数据结构和队列触发测试用例
@dataclass
class NegotiateCall:
    prefillable: List[bool]
    token_usage: List[float]
    # 可选的调度器状态,None 表示不传递,兼容旧行为
    running_batch: Optional[List[int]] = None
    max_prefill_bs: Optional[List[int]] = None
    waiting_queue_len: Optional[List[int]] = None
    max_running_requests: Optional[int] = None
    sleep_before_s: float = 0.0 # 用于测试挂钟超时@dataclass
class NegotiateTestCase:
    name: str
    max_delay_passes: int
    token_usage_low_watermark: Optional[float]
    calls: List[NegotiateCall]
    expected_allow: bool
    expected_reason: str
    # 队列触发配置,None 表示使用纯 slot 逻辑
    queue_min_ratio: Optional[float] = None
    max_delay_ms: Optional[float] = None# 测试用例:等待队列不足时触发延迟
_NEGOTIATE_TEST_CASES.append(
    NegotiateTestCase(
        name="queue_trigger_delay",
        max_delay_passes=100,
        token_usage_low_watermark=0.8,
        queue_min_ratio=0.5,
        max_delay_ms=5000,
        calls=[
            NegotiateCall(
                prefillable=[True, True, True, True],
                token_usage=[0.9, 0.9, 0.9, 0.9],
                running_batch=[100, 100, 100, 100],
                max_prefill_bs=[80, 80, 80, 80],
                waiting_queue_len=[10, 10, 10, 10], # 远小于 queue_min=50
                max_running_requests=1024,
            ),
            # 第二个调用(skip_first_delayer 消耗了第一个延迟,现在应实际延迟)
            NegotiateCall(
                prefillable=[True, True, True, True],
                token_usage=[0.9, 0.9, 0.9, 0.9],
                running_batch=[100, 100, 100, 100],
                max_prefill_bs=[80, 80, 80, 80],
                waiting_queue_len=[10, 10, 10, 10],
                max_running_requests=1024,
            ),
        ],
        expected_allow=False,
        expected_reason="delay",
    )
)

评论区精华

能否复用已有参数实现队列触发? 设计

Fridge003 询问是否可以用 `--prefill-delayer-forward-passes-buckets` 等既有参数实现。YAMY1234 解释这些参数仅用于 Prometheus 直方图桶配置,不控制触发逻辑,必须新增参数。

结论:确认需要新增 `--prefill-delayer-queue-min-ratio` 和 `--prefill-delayer-max-delay-ms`。 · 已解决

slot_trigger 命名与角色 style

Fridge003 询问 `slot_trigger` 的作用。YAMY1234 将其重命名为 `slot_condition` 以更准确地反映其作为条件而非触发器的角色。

结论:重命名为 slot_condition,提高可读性。 · 已解决

超时兜底的作用范围 正确性

gemini-code-assist[bot] 指出超时同时影响 slot 和队列触发,可能导致 slot 限制仍存在时误释放。YAMY1234 在后续提交中将 timeout 仅限定在队列触发组件内。

结论:超时仅用于队列触发,slot 触发不受超时影响。 · 已解决

测试覆盖要求 测试

Fridge003 要求添加测试。YAMY1234 补充了 4 个单元测试用例覆盖队列触发主要路径。

结论:测试已添加并通过 CI。 · 已解决

风险与影响

  • 回归风险:新参数默认未启用,现有行为不变;启用后队列触发逻辑与 slot 触发逻辑是 OR 关系,任何一方为 True 都会延迟预填充,因此不会意外跳过延迟,但可能增加延迟概率。

  • TTFT 恶化:队列触发可能无限期等待更多请求,但 max_delay_ms 提供挂钟超时兜底(默认 5s),避免 TTFT 无限增长。

  • 超时范围问题:早期版本中超时同时释放 slot 触发和队列触发,经 review 指出后已修复,超时仅作用于队列触发,slot 触发不受超时影响。

  • 跨 rank 通信开销:全局缓冲区从 4 列增至 5 列,每次决策多同步一个 int64 值,开销可忽略。

  • 兼容性:仅影响 enable_prefill_delayer=True 且 DP attention 启用的场景;非 DP 模式无变化。

  • 用户:提供新的性能优化选项,特别适合输出长度有波动的在线服务场景。用户需设置 --prefill-delayer-queue-min-ratio 来启用,典型值 0.1-0.5。

  • 系统:在实验环境中,OSL 方差场景下输出吞吐提升 1.16×-1.85×(conc 16-128),且 GSM8K 准确率无退化。p99 解码间隙从 396.9ms 降至可接受水平。

  • 团队:需维护两个新参数及对应测试,但改动集中在 prefill_delayer.py,模块内聚性较好。

核心路径变更(调度器) 新增 server 参数 跨 rank 通信扩展 超时范围问题(已修复)

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论