[Spec] Allow speculative workers to stage prefill shared reads (#38554)

This commit is contained in:
paulzhang-tm
2026-09-14 14:03:27 -07:00
committed by GitHub
parent 5c2de3f355
commit 9128d57966
3 changed files with 56 additions and 10 deletions
@@ -20,7 +20,7 @@ import inspect
import logging import logging
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Union from typing import Callable, Optional, Union
import torch import torch
import torch.distributed as dist import torch.distributed as dist
@@ -438,6 +438,9 @@ class ModelRunner:
# Read-done mailbox: the scheduler's WAR barrier reads it from the runner # Read-done mailbox: the scheduler's WAR barrier reads it from the runner
# its worker names, and treats None as the coarse whole-forward fence. # its worker names, and treats None as the coarse whole-forward fence.
self.shared_read_done_event: Optional[torch.cuda.Event] = None self.shared_read_done_event: Optional[torch.cuda.Event] = None
# Scoped by a speculative worker to stage its shared reads before
# the target prefill graph publishes the read-done event.
self.prefill_shared_read_stager: Optional[Callable[[ForwardBatch], bool]] = None
# CPU offload # CPU offload
set_offloader(create_offloader(dp_rank=self.ps.dp_rank)) set_offloader(create_offloader(dp_rank=self.ps.dp_rank))
@@ -31,17 +31,17 @@ def maybe_publish_prefill_shared_read_done(
return return
if forward_batch.forward_mode != ForwardMode.EXTEND: if forward_batch.forward_mode != ForwardMode.EXTEND:
return return
# TODO(Jialin): Relax for EAGLE/MTP after validating the later # The record lands right after replay prep, so PRE_REPLAY only.
# draft-extend reader's WAR boundary. declared = model_runner.attn_backend.shared_read_ends(forward_batch.forward_mode)
if declared is not SharedReadEnds.PRE_REPLAY:
return
if ( if (
not model_runner.spec_algorithm.is_none() not model_runner.spec_algorithm.is_none()
and not model_runner.spec_algorithm.is_dflash_family() and not model_runner.spec_algorithm.is_dflash_family()
): ):
# Other speculative algorithms may have a later draft-extend reader. # Stage the draft's shared reads before publishing the read-done event.
return stage = getattr(model_runner, "prefill_shared_read_stager", None)
# The record lands right after replay prep, so PRE_REPLAY only. if stage is None or not stage(forward_batch):
declared = model_runner.attn_backend.shared_read_ends(forward_batch.forward_mode)
if declared is not SharedReadEnds.PRE_REPLAY:
return return
logger.info_once( logger.info_once(
"Prefill shared-read-done fastpath active (%s)", "Prefill shared-read-done fastpath active (%s)",
@@ -1,5 +1,5 @@
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import create_autospec from unittest.mock import Mock, call, create_autospec
import pytest import pytest
@@ -35,6 +35,7 @@ def _model_runner(*, spec_algorithm=SpeculativeAlgorithm.NONE, compliant=True):
spec_algorithm=spec_algorithm, spec_algorithm=spec_algorithm,
attn_backend=attn_backend, attn_backend=attn_backend,
shared_read_done_event=None, shared_read_done_event=None,
prefill_shared_read_stager=None,
) )
@@ -65,10 +66,52 @@ def test_disabled_when_flag_is_false():
) )
def test_dflash_family_target_prefill_publishes(algorithm): def test_dflash_family_target_prefill_publishes(algorithm):
runner = _model_runner(spec_algorithm=algorithm) runner = _model_runner(spec_algorithm=algorithm)
runner.prefill_shared_read_stager = Mock(return_value=False)
with envs.SGLANG_ENABLE_PREFILL_WAR_READ_DONE.override(True): with envs.SGLANG_ENABLE_PREFILL_WAR_READ_DONE.override(True):
maybe_publish_prefill_shared_read_done(runner, _batch(), _DEVICE_MODULE) maybe_publish_prefill_shared_read_done(runner, _batch(), _DEVICE_MODULE)
published = runner.shared_read_done_event published = runner.shared_read_done_event
assert isinstance(published, _Event) and published.recorded assert isinstance(published, _Event) and published.recorded
runner.prefill_shared_read_stager.assert_not_called()
@pytest.mark.parametrize("staged", [False, True])
def test_speculative_prefill_publishes_only_after_staging(staged):
runner, batch = _model_runner(spec_algorithm=SpeculativeAlgorithm.EAGLE), _batch()
calls = Mock()
runner.prefill_shared_read_stager = calls.stage
calls.stage.return_value = staged
calls.Event.side_effect = _Event
with envs.SGLANG_ENABLE_PREFILL_WAR_READ_DONE.override(True):
maybe_publish_prefill_shared_read_done(
runner, batch, SimpleNamespace(Event=calls.Event)
)
assert calls.mock_calls == [call.stage(batch)] + ([call.Event()] if staged else [])
published = runner.shared_read_done_event
if staged:
assert isinstance(published, _Event) and published.recorded
else:
assert published is None
@pytest.mark.parametrize(
"enabled,mode,compliant",
[
(False, ForwardMode.EXTEND, True),
(True, ForwardMode.TARGET_VERIFY, True),
(True, ForwardMode.MIXED, True),
(True, ForwardMode.DECODE, True),
(True, ForwardMode.EXTEND, False),
],
)
def test_prefill_gates_skip_staging(enabled, mode, compliant):
runner = _model_runner(
spec_algorithm=SpeculativeAlgorithm.EAGLE, compliant=compliant
)
runner.prefill_shared_read_stager = Mock(return_value=True)
with envs.SGLANG_ENABLE_PREFILL_WAR_READ_DONE.override(enabled):
maybe_publish_prefill_shared_read_done(runner, _batch(mode), _DEVICE_MODULE)
runner.prefill_shared_read_stager.assert_not_called()
assert runner.shared_read_done_event is None
def test_gates_exclude_non_prefill_unsupported_algorithm_and_noncompliant_backend(): def test_gates_exclude_non_prefill_unsupported_algorithm_and_noncompliant_backend():