Prhub

#35748 Fix overlap prebuilt row reuse race

原始 PR 作者 jasonjk-park 合并时间 2026-08-21 17:00 文件变更 3 提交数 1 评论 2 代码增减 +53 / -17

执行摘要

修复 overlap 预构建行复用竞态,调度流等待前向流

PR body 明确指出这是一个 FutureMap 中的竞态条件 bug:publish() 可能在两种情况下被调用:(1) 前向流在 draft+verify 之后;(2) 调度流中,当 PD disagg 启用且 prefill 请求到达时。publish() 会修改 FutureMap 的共享状态,因此必须正确排序以避免竞态。publish_ready 事件不能用于同步,因为 stash() 在 record 之后也会修改 FutureMap 共享状态。Reviewer merrymercy 也确认这是实际存在的竞态:'an actual race condition when an already finished forward is still running, while PD-decode prebuilt requests come in and occupy the same lane.'

值得精读。该 PR 修复的是调度核心路径上的竞态条件,涉及流同步和 FutureMap 的共享状态管理。理解其修复思路(通过显式流等待代替事件链)对处理类似并发问题有参考价值。此外,其测试编写方式(使用 mock 验证调用顺序)也值得学习。

讨论亮点

审查者 hnyls2002 指出:LGTM, thanks for the fix. my previous PR #30435 was trying to fix the similar PR, but I didn't consider the stashing data races between prebuilt batch and normal batches. 这表明此修复解决了之前未覆盖的 stash 数据竞争问题,与历史 PR #30435 相关。merrymercy 确认这是实际竞态,Jialin 的评论强调了竞态发生的场景。

实现拆解

实现分三步:

  1. 核心修复(decode.py):在 get_new_prebuilt_batch 中,当 enable_overlap 为真时,在调用 new_batch.process_prebuilt(self.future_map) 之前,先调用 self.schedule_stream.wait_stream(self.forward_stream),确保前向流中未完成的 publish() 操作已经完成,避免复用同一请求池行时出现数据竞争。

  2. 简化 publish()(overlap_utils.py):移除了 publish() 中对 publish_ready 事件进行链式等待的逻辑(device_module.current_stream().wait_event(self.publish_ready)),因为现在发布操作已经通过流同步保证顺序,不再需要此链式等待,避免了潜在的事件记录顺序问题。

  3. 测试配套(test_priority_scheduling_disaggregation.py):重构并新增测试。将原有 TestDecodePrebuiltPriority 测试类重构为 TestDecodePrebuilt,提取 _new_scheduler 辅助方法,并新增 test_overlap_waits_for_forward_before_processing_prebuilt 测试,验证 prepare_for_prebuiltwait_streamprocess_prebuilt 的执行顺序。

文件 模块 状态 重要度
python/sglang/srt/disaggregation/decode.py 调度器 modified 5.9
python/sglang/srt/managers/overlap_utils.py 重叠调度 modified 5.64
test/registered/unit/managers/test_priority_scheduling_disaggregation.py 测试 modified 6.73

关键符号

get_new_prebuilt_batch process_decode_queue FutureMap.publish FutureMap.stash FutureMap.prepare

关键源码片段

python/sglang/srt/disaggregation/decode.py core-logic

核心修复位置:在 get_new_prebuilt_batch 中,当 enable_overlap 时,在 process_prebuilt 前添加 wait_stream,防止竞态。

# python/sglang/srt/disaggregation/decode.py
# 核心修复:在 prebuilt 批处理前等待前向流,防止竞态
def get_new_prebuilt_batch(self, running_batch):
    # ... 其他逻辑
    # 构造 fake completed prefill
    new_batch.prepare_for_prebuilt()
    if self.enable_overlap:
        # 已完成的请求可能仍有一个冗余前向在飞行中。
        # 在 prebuilt 请求复用可能被复用的请求池行之前,先 drain 它。
        self.schedule_stream.wait_stream(self.forward_stream)
    new_batch.process_prebuilt(self.future_map)
    return new_batch
python/sglang/srt/managers/overlap_utils.py core-logic

简化 publish() 的链式事件记录,避免潜在的问题。

# python/sglang/srt/managers/overlap_utils.py
# publish() 简化:移除链式事件记录,因为已通过流同步保证顺序
def publish(
    self,
    future_indices: torch.Tensor,
    new_seq_lens: torch.Tensor,
    confidence: Optional[torch.Tensor] = None,
) -> None:
    indices = future_indices
    if indices.shape[0] == 0:
        return # DP idle
    self.new_seq_lens_buf[indices] = new_seq_lens.to(self.new_seq_lens_buf.dtype)
    publish_confidence = self.needs_confidence_relay and confidence is not None
    if publish_confidence:
        self.confidence_relay.scatter(indices, confidence)
    # 只有 spec_v2 需要事件;它门控 seq_lens D2H 在私有流上。
    if self.spec_algo.is_some():
        if self.publish_ready is None:
            self.publish_ready = torch.get_device_module(self.device).Event()
        self.publish_ready.record()
        self._publish_fresh = True
    if publish_confidence:
        self.confidence_relay.issue_ring_copy(
            stream=self.fwd_prepare_d2h_stream,
            publish_ready=self.publish_ready,
        )
test/registered/unit/managers/test_priority_scheduling_disaggregation.py test-coverage

新增测试覆盖竞态修复,验证执行顺序。

# test/registered/unit/managers/test_priority_scheduling_disaggregation.py
# 新增测试:验证 overlap 时等待前向流
def test_overlap_waits_for_forward_before_processing_prebuilt(self):
    scheduler = self._new_scheduler(enable_overlap=True)
    scheduler.waiting_queue = [MagicMock(rid="request")]
​
    call_order = []
    new_batch = MagicMock()
    new_batch.prepare_for_prebuilt.side_effect = lambda: call_order.append("prepare")
    scheduler.schedule_stream.wait_stream.side_effect = lambda _: call_order.append("wait")
    new_batch.process_prebuilt.side_effect = lambda *_: call_order.append("process")
​
    with patch(
        "sglang.srt.disaggregation.decode.ScheduleBatch.init_new", return_value=new_batch,
    ), get_context().override_server_args(disaggregation_decode_enable_radix_cache=False):
        ret = SchedulerDisaggregationDecodeMixin.get_new_prebuilt_batch(
            scheduler, scheduler.running_batch
        )
​
    self.assertIs(ret, new_batch)
    scheduler.schedule_stream.wait_stream.assert_called_once_with(scheduler.forward_stream)
    self.assertEqual(call_order, ["prepare", "wait", "process"])

评论区精华

竞态条件的确认 正确性

merrymercy 确认 'actual race condition when an already finished forward is still running, while PD-decode prebuilt requests come in and occupy the same lane.'

结论:确认修复必要。 · 已解决

与历史修复的关联 设计

hnyls2002 指出本修复解决了其之前 PR #30435 中未考虑的 stash 数据竞争问题。

结论:本修复补充了 stash 竞态修复。 · 已解决

风险与影响

风险较低,但需注意:

  • 仅影响启用 overlap 的 PD 分离场景:修复在 enable_overlap 条件下新增了流同步,可能带来轻微的性能开销,但这是必要开销,且只在 PD 分离时发生。
  • 依赖流等待的正确性wait_stream 的调用假定 schedule_streamforward_stream 均存在且有效,如果这些属性未正确初始化,可能会引发异常。但测试中已覆盖。
  • 移除链式记录逻辑:若未来有其他路径在前向流之外调用 publish(),且不经过 get_new_prebuilt_batch 的同步,可能会导致事件顺序问题回归。需要确保所有调用路径都受保护。

影响范围限于启用 overlap 的 PD 分离部署(enable_overlapdisaggregation_decode_enable_radix_cache 等配置)。修复后,请求池行的复用不会出现竞态,提高了稳定性和正确性。对性能影响极小,仅增加一次流等待调用。测试覆盖了优先级调度和 overlap 场景,降低了回归风险。

核心路径变更 新增测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论