[Bugfix] Fix full prefill CUDA graph padding and EAGLE capture (#35588)
This commit is contained in:
@@ -815,7 +815,7 @@ def build_prefill_registry(
|
||||
source: Optional[Any] = None,
|
||||
) -> CudaGraphBufferRegistry:
|
||||
"""Registry mirroring the **token-axis** FB-shared buffers for the
|
||||
piecewise / breakable (prefill) cuda-graph runners.
|
||||
piecewise / breakable / full (prefill) cuda-graph runners.
|
||||
|
||||
``register_input_embeds`` (default ``True``) registers the multimodal
|
||||
``input_embeds`` slot; the eager extend path passes ``False`` so it is
|
||||
@@ -910,13 +910,18 @@ def build_prefill_registry(
|
||||
# blank real tokens whenever raw < bucket. Recompute the local
|
||||
# count against the padded bucket from the batch's un-adjusted
|
||||
# global count, mirroring the decode registry's post_fill.
|
||||
if require_gathered_buffer and not enable_prefill_cp:
|
||||
buf.fill_(
|
||||
compute_local_num_token_non_padded_cpu(
|
||||
global_num_token_non_padded=fb.num_token_non_padded_cpu,
|
||||
num_tokens_per_dp=ctx.padded_num_tokens,
|
||||
if require_gathered_buffer:
|
||||
if not enable_prefill_cp:
|
||||
buf.fill_(
|
||||
compute_local_num_token_non_padded_cpu(
|
||||
global_num_token_non_padded=fb.num_token_non_padded_cpu,
|
||||
num_tokens_per_dp=ctx.padded_num_tokens,
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Non-gathered FullCG still needs the live boundary rather
|
||||
# than a stale/absent ForwardBatch tensor.
|
||||
buf.fill_(ctx.raw_num_tokens)
|
||||
|
||||
slots.append(
|
||||
GraphSlot(
|
||||
|
||||
@@ -322,14 +322,14 @@ def capture_prefill_graph(
|
||||
# Skip prefill CG for EAGLE target on tc_piecewise when the fixed server
|
||||
# capture ceiling is below FULL. EAGLE target prefill requests FULL, so a
|
||||
# NULL or LAST graph is dead; capturing it can perturb FP4/TRTLLM-MoE
|
||||
# state and corrupt decode replay (see #28386 and #28870). BCG captures
|
||||
# FULL for EAGLE target in PrefillCudaGraphRunner.__init__, so it does not
|
||||
# need this skip.
|
||||
# state and corrupt decode replay (see #28386 and #28870). BCG and FullCG
|
||||
# capture FULL for EAGLE targets in PrefillCudaGraphRunner.__init__, so
|
||||
# they do not need this skip.
|
||||
if (
|
||||
model_runner.spec_algorithm.is_eagle()
|
||||
and not model_runner.is_draft_worker
|
||||
and get_server_return_hidden_states_mode() < CaptureHiddenMode.FULL
|
||||
and not check_cuda_graph_backend(Phase.PREFILL, Backend.BREAKABLE)
|
||||
and check_cuda_graph_backend(Phase.PREFILL, Backend.TC_PIECEWISE)
|
||||
):
|
||||
logger.info(
|
||||
"Disable prefill CUDA graph for EAGLE target on tc_piecewise "
|
||||
|
||||
@@ -296,16 +296,20 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
self.capture_forward_mode = ForwardMode.EXTEND
|
||||
# Hidden-state capture mode cases:
|
||||
# - Breakable EAGLE draft: LAST.
|
||||
# - Breakable EAGLE target: FULL.
|
||||
# - EAGLE target: FULL.
|
||||
# - Return-hidden-states or DFLASH: FULL.
|
||||
# - Otherwise: NULL.
|
||||
is_breakable_eagle = (
|
||||
is_eagle = model_runner.spec_algorithm.is_eagle()
|
||||
is_breakable_eagle_draft = (
|
||||
self.prefill_backend_name == Backend.BREAKABLE
|
||||
and model_runner.spec_algorithm.is_eagle()
|
||||
and is_eagle
|
||||
and model_runner.is_draft_worker
|
||||
)
|
||||
if is_breakable_eagle and model_runner.is_draft_worker:
|
||||
if is_breakable_eagle_draft:
|
||||
self.capture_hidden_mode = CaptureHiddenMode.LAST
|
||||
elif is_breakable_eagle or model_runner.spec_algorithm.is_dflash_family():
|
||||
elif (is_eagle and not model_runner.is_draft_worker) or (
|
||||
model_runner.spec_algorithm.is_dflash_family()
|
||||
):
|
||||
self.capture_hidden_mode = CaptureHiddenMode.FULL
|
||||
else:
|
||||
self.capture_hidden_mode = self.return_hidden_states_mode
|
||||
@@ -350,7 +354,12 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
|
||||
hidden_size=input_embeds_hidden_size,
|
||||
embed_dtype=self.model_runner.dtype,
|
||||
enable_mamba_track=self.mamba_track_enabled,
|
||||
enable_num_token_non_padded=enable_num_token_non_padded(),
|
||||
# FullCG always pads to a capture bucket. Models that mask padded
|
||||
# hidden rows need the live boundary even without expert parallelism.
|
||||
enable_num_token_non_padded=(
|
||||
enable_num_token_non_padded()
|
||||
or self.prefill_backend_name == Backend.FULL
|
||||
),
|
||||
require_gathered_buffer=require_gathered_buffer(),
|
||||
enable_prefill_cp=(
|
||||
is_dsa_enable_prefill_cp() or is_mla_prefill_cp_enabled()
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
The Qwen3-8B test checks end-to-end accuracy with FlashInfer. The smaller
|
||||
DeepSeek-Coder-V2-Lite test checks that an MLA radix-prefix hit selects the
|
||||
OSS FA4 cached-prefix graph variant and matches an eager cold request.
|
||||
The EAGLE3 test checks that target prefills replay FullCG rather than silently
|
||||
falling back to eager while speculative decoding remains active.
|
||||
|
||||
The attention backend is pinned to flashinfer: plain EXTEND under full
|
||||
CUDA graph requires the backend's init_forward_metadata_out_graph to
|
||||
@@ -20,6 +22,7 @@ from sglang.srt.utils import get_device_sm, kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.mock_model.utils import run_mock_model_bench_serving
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
@@ -30,7 +33,18 @@ from sglang.test.test_utils import (
|
||||
|
||||
# OSS FA4 coverage requires Blackwell. The PP test uses two GPUs; the other
|
||||
# tests use one GPU.
|
||||
register_cuda_ci(est_time=240, stage="base-b", runner_config="4-gpu-b200")
|
||||
register_cuda_ci(est_time=300, stage="base-b", runner_config="4-gpu-b200")
|
||||
|
||||
|
||||
def _prefill_graph_count(base_url: str) -> float:
|
||||
metrics = requests.get(base_url + "/metrics", timeout=30).text
|
||||
matches = re.findall(
|
||||
r'^sglang:cuda_graph_passes_total\{[^}]*mode="prefill_cuda_graph"[^}]*\}'
|
||||
r"\s+([0-9.eE+-]+)$",
|
||||
metrics,
|
||||
re.MULTILINE,
|
||||
)
|
||||
return sum(map(float, matches), 0.0)
|
||||
|
||||
|
||||
class TestFullCudaGraphPrefill(CustomTestCase):
|
||||
@@ -93,6 +107,52 @@ class TestFullCudaGraphPipelineParallel(CustomTestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestFullCudaGraphPrefillWithEagle3(Eagle3Base):
|
||||
"""EAGLE3 target prefills replay a FullCG captured with full hidden states."""
|
||||
|
||||
spec_steps = 3
|
||||
spec_topk = 1
|
||||
spec_tokens = 4
|
||||
mem_fraction_static = 0.6
|
||||
max_running_requests = 1
|
||||
chunked_prefill_size = 64
|
||||
extra_args = (
|
||||
"--enable-metrics",
|
||||
"--disable-flashinfer-autotune",
|
||||
"--cuda-graph-config",
|
||||
(
|
||||
'{"decode":{"backend":"full","max_bs":1},'
|
||||
'"prefill":{"backend":"full","bs":[16],'
|
||||
'"full_prefill_max_req":1}}'
|
||||
),
|
||||
)
|
||||
|
||||
def test_eagle_target_prefill_replays_full_cuda_graph(self):
|
||||
prompt = (
|
||||
"The capital of France is Paris. Write a concise paragraph about "
|
||||
"its history, architecture, food, and culture."
|
||||
)
|
||||
input_ids = self.tokenizer.encode(prompt)[:16]
|
||||
self.assertEqual(len(input_ids), 16)
|
||||
graph_count = _prefill_graph_count(self.base_url)
|
||||
|
||||
response = requests.post(
|
||||
self.base_url + "/generate",
|
||||
json={
|
||||
"input_ids": input_ids,
|
||||
"sampling_params": {"max_new_tokens": 32, "temperature": 0},
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
output = response.json()
|
||||
|
||||
self.assertTrue(output["output_ids"])
|
||||
self.assertGreater(output["meta_info"]["spec_verify_ct"], 0)
|
||||
self.assertGreater(output["meta_info"]["spec_num_proposed_drafts"], 0)
|
||||
self.assertEqual(_prefill_graph_count(self.base_url), graph_count + 1)
|
||||
|
||||
|
||||
@unittest.skipIf(get_device_sm() < 100, "Test requires CUDA SM 100 or higher")
|
||||
class TestFullCudaGraphChunkedPrefix(unittest.TestCase):
|
||||
"""A radix-cache hit replays the OSS FA4 FullCG prefix variant."""
|
||||
@@ -117,10 +177,12 @@ class TestFullCudaGraphChunkedPrefix(unittest.TestCase):
|
||||
"--skip-server-warmup",
|
||||
"--enable-metrics",
|
||||
"--cuda-graph-config",
|
||||
'{"decode":{"backend":"disabled"},'
|
||||
'"prefill":{"backend":"full","bs":[32],"max_bs":32,'
|
||||
'"full_prefill_max_req":1,'
|
||||
'"full_prefill_prefix_chunk_tokens":64}}',
|
||||
(
|
||||
'{"decode":{"backend":"disabled"},'
|
||||
'"prefill":{"backend":"full","bs":[32],"max_bs":32,'
|
||||
'"full_prefill_max_req":1,'
|
||||
'"full_prefill_prefix_chunk_tokens":64}}'
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+30
-3
@@ -40,8 +40,11 @@ as tests.
|
||||
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kl_multiturn_utils import (
|
||||
make_mamba_decode_assert,
|
||||
@@ -97,7 +100,9 @@ def _random_suffixes(n: int, length: int, seed: int) -> list[list[int]]:
|
||||
return [[rng.randint(1, 30000) for _ in range(length)] for _ in range(n)]
|
||||
|
||||
|
||||
def _base_args(mamba_strategy: str = "extra_buffer") -> list[str]:
|
||||
def _base_args(
|
||||
mamba_strategy: str = "extra_buffer", *, mem_fraction_static: float = 0.6
|
||||
) -> list[str]:
|
||||
return [
|
||||
"--trust-remote-code",
|
||||
"--attention-backend",
|
||||
@@ -114,13 +119,24 @@ def _base_args(mamba_strategy: str = "extra_buffer") -> list[str]:
|
||||
# the static pool leaves ~19 GB for the prefill graphs, the fa4 workspace
|
||||
# and the chunked-prefill activations, which is what this config needs.
|
||||
"--mem-fraction-static",
|
||||
"0.6",
|
||||
str(mem_fraction_static),
|
||||
"--mamba-track-interval",
|
||||
str(TRACK_INTERVAL),
|
||||
"--enable-deterministic-inference",
|
||||
]
|
||||
|
||||
|
||||
def _prefill_graph_count(base_url: str) -> float:
|
||||
metrics = requests.get(base_url + "/metrics", timeout=30).text
|
||||
matches = re.findall(
|
||||
r'^sglang:cuda_graph_passes_total\{[^}]*mode="prefill_cuda_graph"[^}]*\}'
|
||||
r"\s+([0-9.eE+-]+)$",
|
||||
metrics,
|
||||
re.MULTILINE,
|
||||
)
|
||||
return sum(map(float, matches), 0.0)
|
||||
|
||||
|
||||
class TestUnifiedHybridBitExact(CustomTestCase):
|
||||
"""Prefill and decode must score a token identically once every kernel on the
|
||||
path is batch-invariant, so any drift is a stale conv/mamba checkpoint or a
|
||||
@@ -321,7 +337,9 @@ class TestUnifiedHybridMTPBitExact(CustomTestCase):
|
||||
def setUpClass(cls):
|
||||
cls.model = _MODEL_PATH
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
other_args = _base_args() + [
|
||||
# Target FullCG and both MTP draft workers retain graph pools, so this
|
||||
# class needs more dynamic-memory headroom than the non-spec tests.
|
||||
other_args = _base_args(mem_fraction_static=0.58) + [
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--enable-multi-layer-eagle",
|
||||
@@ -333,6 +351,7 @@ class TestUnifiedHybridMTPBitExact(CustomTestCase):
|
||||
"3",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--enable-metrics",
|
||||
]
|
||||
if _MODEL_REVISION:
|
||||
other_args += ["--revision", _MODEL_REVISION]
|
||||
@@ -353,6 +372,9 @@ class TestUnifiedHybridMTPBitExact(CustomTestCase):
|
||||
terminate_and_kill_process_tree(cls.process, wait_timeout=60)
|
||||
|
||||
def _run(self, helper):
|
||||
server_info = requests.get(self.base_url + "/server_info", timeout=30).json()
|
||||
self.assertEqual(server_info["cuda_graph_config"]["prefill"]["backend"], "full")
|
||||
graph_count = _prefill_graph_count(self.base_url)
|
||||
helper(
|
||||
self.base_url,
|
||||
{self.model: {"kl_div": KL_DIV_THRESHOLD}},
|
||||
@@ -361,6 +383,11 @@ class TestUnifiedHybridMTPBitExact(CustomTestCase):
|
||||
max_new_tokens=MAX_NEW_TOKENS,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
self.assertGreater(
|
||||
_prefill_graph_count(self.base_url),
|
||||
graph_count,
|
||||
"MTP target prefill did not replay the configured Full CUDA graph.",
|
||||
)
|
||||
|
||||
def test_logprobs_match(self):
|
||||
self._run(assert_logprobs_match)
|
||||
|
||||
@@ -1403,13 +1403,12 @@ class TestPrefillNumTokenNonPaddedPostFill(unittest.TestCase):
|
||||
# pads. local = clamp(1018 - 512, 0, 512).
|
||||
self.assertEqual(self._fill(attn_tp_rank=1, attn_tp_size=2), 506)
|
||||
|
||||
def test_non_gathered_keeps_plain_fb_copy(self):
|
||||
# Without a gathered buffer there is no attn-TP scatter; the plain FB
|
||||
# copy must be preserved (post_fill no-op), mirroring the decode
|
||||
# registry's contract.
|
||||
def test_non_gathered_uses_raw_token_count(self):
|
||||
# Full prefill graphs need the live raw boundary even without a
|
||||
# gathered buffer so model layers can discard the padded bucket tail.
|
||||
self.assertEqual(
|
||||
self._fill(attn_tp_rank=0, attn_tp_size=2, require_gathered_buffer=False),
|
||||
509,
|
||||
1018,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -152,28 +152,25 @@ class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
|
||||
|
||||
def test_eagle_target_tc_piecewise_skips_last_mode_capture(self):
|
||||
eager_runner = object()
|
||||
# The server-side hidden-state ceiling is a bag leaf.
|
||||
# The server-side hidden-state ceiling and graph config are bag leaves.
|
||||
override = get_context().override_server_args(
|
||||
enable_return_hidden_states=True,
|
||||
return_hidden_states_mode="last",
|
||||
cuda_graph_config=SimpleNamespace(
|
||||
prefill=SimpleNamespace(backend=Backend.TC_PIECEWISE)
|
||||
),
|
||||
)
|
||||
override.install()
|
||||
self.addCleanup(override.restore)
|
||||
model_runner = SimpleNamespace(
|
||||
is_draft_worker=False,
|
||||
spec_algorithm=SimpleNamespace(is_eagle=lambda: True),
|
||||
server_args=SimpleNamespace(),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
graph_setup,
|
||||
"check_cuda_graph_backend",
|
||||
return_value=False,
|
||||
):
|
||||
capture = capture_prefill_graph(
|
||||
model_runner=model_runner,
|
||||
eager_runner=eager_runner,
|
||||
)
|
||||
capture = capture_prefill_graph(
|
||||
model_runner=model_runner,
|
||||
eager_runner=eager_runner,
|
||||
)
|
||||
|
||||
self.assertIs(capture.runner, eager_runner)
|
||||
|
||||
@@ -234,6 +231,36 @@ class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
|
||||
|
||||
self.assertIs(static_batch.mm_input_embeds, mm_input_embeds)
|
||||
|
||||
def test_eagle_target_full_reaches_graph_construction(self):
|
||||
override = get_context().override_server_args(
|
||||
enable_return_hidden_states=True,
|
||||
return_hidden_states_mode="last",
|
||||
cuda_graph_config=SimpleNamespace(
|
||||
prefill=SimpleNamespace(backend=Backend.FULL)
|
||||
),
|
||||
)
|
||||
override.install()
|
||||
self.addCleanup(override.restore)
|
||||
model_runner = SimpleNamespace(
|
||||
is_draft_worker=False,
|
||||
lora_manager=None,
|
||||
model=object(),
|
||||
spec_algorithm=SimpleNamespace(is_eagle=lambda: True),
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
graph_setup,
|
||||
"resolve_language_model",
|
||||
side_effect=RuntimeError("reached graph construction"),
|
||||
),
|
||||
self.assertRaisesRegex(RuntimeError, "reached graph construction"),
|
||||
):
|
||||
capture_prefill_graph(
|
||||
model_runner=model_runner,
|
||||
eager_runner=object(),
|
||||
)
|
||||
|
||||
def test_prefix_chunk_capacity_is_aggregate_and_can_be_overridden(self):
|
||||
graph_config = SimpleNamespace(
|
||||
prefill=SimpleNamespace(full_prefill_prefix_chunk_tokens=None, max_bs=8)
|
||||
@@ -243,7 +270,7 @@ class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
|
||||
override = get_context().override_server_args(
|
||||
chunked_prefill_size=16, cuda_graph_config=graph_config
|
||||
)
|
||||
published = override.install()
|
||||
override.install()
|
||||
self.addCleanup(override.restore)
|
||||
model_runner = SimpleNamespace(
|
||||
server_args=SimpleNamespace(),
|
||||
|
||||
Reference in New Issue
Block a user