[BCG][3/N] Enable bcg on dsa & deepep a2a backend (#31987)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yuwei An
2026-07-31 16:45:18 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 3c5f115741
commit 3e0f7c3f30
13 changed files with 319 additions and 116 deletions
+1
View File
@@ -536,6 +536,7 @@ def _maybe_prepare_mlp_sync_batch(batch: ScheduleBatch, model_runner):
if require_mlp_sync(model_runner.server_args):
prepare_mlp_sync_batch_raw(
batch,
model_runner=model_runner,
dp_size=model_runner.server_args.dp_size,
attn_tp_size=get_parallel().attn_tp_size,
attn_cp_size=model_runner.ps.attn_cp_size,
+66 -1
View File
@@ -8,6 +8,10 @@ import torch
from sglang.kernels.ops.quantization.fp8_kernel import is_fp8_fnuz
from sglang.srt.environ import envs
from sglang.srt.layers import deep_gemm_wrapper
from sglang.srt.layers.dp_attention import (
get_is_extend_in_batch,
set_is_extend_in_batch,
)
from sglang.srt.layers.moe import (
get_deepep_mode,
get_moe_a2a_backend,
@@ -21,10 +25,20 @@ from sglang.srt.layers.moe.token_dispatcher.deepep import (
DeepEPLLCombineInput,
DeepEPNormalCombineInput,
)
from sglang.srt.layers.moe.topk import TopKOutput, TopKOutputChecker
from sglang.srt.layers.moe.topk import (
StandardTopKOutput,
TopKOutput,
TopKOutputChecker,
)
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.layers.quantization.fp8 import Fp8Config
from sglang.srt.layers.quantization.w4afp8 import W4AFp8Config, W4AFp8MoEMethod
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph import (
eager_on_graph,
)
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.context import (
is_in_breakable_cuda_graph,
)
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
is_in_tc_piecewise_cuda_graph,
)
@@ -155,11 +169,62 @@ class DeepEPMoE(FusedMoE):
deep_gemm_wrapper.ENABLE_JIT_DEEPGEMM
), f"DeepEP {self.deepep_mode} mode requires deep_gemm"
def _a2a_forward_with_output_impl(
self,
hidden_states: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
router_logits: torch.Tensor,
output: torch.Tensor,
) -> None:
# eager run under breakable cuda graph
saved_is_extend_in_batch = get_is_extend_in_batch()
set_is_extend_in_batch(True)
try:
output.copy_(
self.forward_impl(
hidden_states,
StandardTopKOutput(topk_weights, topk_ids, router_logits),
)
)
finally:
set_is_extend_in_batch(saved_is_extend_in_batch)
def _a2a_forward_capture_stub(
self,
hidden_states: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
router_logits: torch.Tensor,
output: torch.Tensor,
) -> None:
# Capture pass only: record the buffer address, skip the
# rank-coupled a2a. Warmup and replay run the real body.
output.zero_()
a2a_forward_with_output = eager_on_graph(
True, capture_stub=_a2a_forward_capture_stub
)(_a2a_forward_with_output_impl)
def forward(
self,
hidden_states: torch.Tensor,
topk_output: TopKOutput,
):
# DeepEP NORMAL mode is not capturable; run it as an eager node.
if is_in_breakable_cuda_graph():
assert TopKOutputChecker.format_is_standard(
topk_output
), "Only standard topk output is supported for breakable cuda graph"
output = torch.empty_like(hidden_states)
self.a2a_forward_with_output(
hidden_states,
topk_output.topk_weights,
topk_output.topk_ids,
topk_output.router_logits,
output,
)
return output
if is_in_tc_piecewise_cuda_graph():
assert TopKOutputChecker.format_is_standard(
topk_output
+9
View File
@@ -269,6 +269,7 @@ from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
from sglang.srt.sampling.sampling_params import TOP_K_ALL
from sglang.srt.server_args import PortArgs, ServerArgs
from sglang.srt.session.session_controller import SessionController
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker
from sglang.srt.speculative.dflash_utils import validate_dflash_request
from sglang.srt.speculative.eagle_utils import get_draft_recurrent_hidden_state_spec
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
@@ -1882,7 +1883,15 @@ class Scheduler(
)
def init_dp_attn_adapter(self) -> None:
# Spec workers have no .model_runner of their own; the prefill graph
# runner that votes belongs to the target model.
target_worker = (
self.tp_worker.target_worker
if isinstance(self.tp_worker, BaseSpecWorker)
else self.tp_worker
)
self.dp_attn_adapter = SchedulerDPAttnAdapter(
model_runner=target_worker.model_runner,
tp_group=self.tp_group,
req_to_token_pool=self.req_to_token_pool,
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
@@ -33,6 +33,7 @@ from sglang.srt.utils.common import require_mlp_tp_gather
if TYPE_CHECKING:
from sglang.srt.distributed.parallel_state import GroupCoordinator
from sglang.srt.model_executor.model_runner import ModelRunner
_ENABLE_METRICS_DP_ATTENTION = envs.SGLANG_ENABLE_METRICS_DP_ATTENTION.get()
@@ -83,11 +84,11 @@ class MLPSyncBatchInfo:
num_tokens: int
num_tokens_for_logprob: int
can_cuda_graph: bool
can_run_decode_cuda_graph: bool
can_run_prefill_cuda_graph: bool
is_extend_in_batch: bool
local_can_run_tbo: bool
local_forward_mode: int
can_run_breakable_cuda_graph: bool
# some gathered elements
tp0_info: torch.Tensor = None
@@ -102,11 +103,11 @@ class MLPSyncBatchInfo:
[
self.num_tokens,
self.num_tokens_for_logprob,
int(self.can_cuda_graph),
int(self.can_run_decode_cuda_graph),
int(self.is_extend_in_batch),
int(self.local_can_run_tbo),
self.local_forward_mode,
int(self.can_run_breakable_cuda_graph),
int(self.can_run_prefill_cuda_graph),
],
device=device,
dtype=dtype,
@@ -117,11 +118,11 @@ class MLPSyncBatchInfo:
[
0, # num_tokens
0, # num_tokens_for_logprob
1, # can_cuda_graph
1, # can_run_decode_cuda_graph
0, # is_extend_in_batch
1, # local_can_run_tbo
ForwardMode.IDLE.value, # local_forward_mode
0, # can_run_breakable_cuda_graph
0, # can_run_prefill_cuda_graph
],
device=device,
dtype=dtype,
@@ -184,9 +185,9 @@ class MLPSyncBatchInfo:
cpu_data = tp0_info[:, :2].cpu()
self.global_num_tokens = cpu_data[:, 0].tolist()
self.global_num_tokens_for_logprob = cpu_data[:, 1].tolist()
self.can_cuda_graph = bool(tp0_info[:, 2].min().item())
self.can_run_decode_cuda_graph = bool(tp0_info[:, 2].min().item())
self.is_extend_in_batch = bool(tp0_info[:, 3].max().item())
self.can_run_breakable_cuda_graph = bool(tp0_info[:, 6].min().item())
self.can_run_prefill_cuda_graph = bool(tp0_info[:, 6].min().item())
if _ENABLE_METRICS_DP_ATTENTION:
self.dp_cooperation_info = DPCooperationInfo.create(tp0_info[:, 5].tolist())
@@ -212,12 +213,13 @@ def _update_gather_batch(
batch.global_forward_mode = mlp_sync_info.global_forward_mode
# Check forward mode for cuda graph
batch.can_run_dp_cuda_graph = mlp_sync_info.can_cuda_graph
batch.can_run_dp_breakable_cuda_graph = mlp_sync_info.can_run_breakable_cuda_graph
batch.can_run_dp_cuda_graph = mlp_sync_info.can_run_decode_cuda_graph
batch.can_run_dp_breakable_cuda_graph = mlp_sync_info.can_run_prefill_cuda_graph
def prepare_mlp_sync_batch_raw(
local_batch: ScheduleBatch,
model_runner: ModelRunner,
dp_size: int,
attn_tp_size: int,
attn_cp_size: int,
@@ -256,19 +258,38 @@ def prepare_mlp_sync_batch_raw(
)
skip_all_gather = envs.SGLANG_SCHEDULER_SKIP_ALL_GATHER.get()
can_cuda_graph = (
can_run_decode_cuda_graph = (
local_batch is None
or local_batch.forward_mode.is_decode_or_idle()
or local_batch.forward_mode.is_prebuilt()
) and not disable_cuda_graph
# Idle/None ranks are permissive (like can_cuda_graph): the all-gather
# min()-reduces this across DP ranks, so a prefill batch with idle ranks
# still resolves to True (idle ranks become a padded dummy extend).
can_run_breakable_cuda_graph = (
breakable_prefill = check_cuda_graph_backend(Phase.PREFILL, Backend.BREAKABLE)
prefill_graph_runner = (
model_runner.prefill_cuda_graph_runner if breakable_prefill else None
)
can_run_prefill_cuda_graph = (
local_batch is None
or local_batch.forward_mode.is_idle()
or local_batch.forward_mode in (ForwardMode.EXTEND, ForwardMode.MIXED)
) and check_cuda_graph_backend(Phase.PREFILL, Backend.BREAKABLE)
# Breakable Cuda Graph Backend Check.
or (
local_batch.forward_mode in (ForwardMode.EXTEND, ForwardMode.MIXED)
and (
prefill_graph_runner is None
or prefill_graph_runner.can_replay_locally(
batch_size=local_batch.batch_size(),
num_tokens=local_batch.extend_num_tokens,
input_embeds=local_batch.input_embeds,
replace_embeds=None,
prefix_lens=local_batch.prefix_lens,
is_target_verify=local_batch.forward_mode.is_target_verify(),
capture_hidden_mode=None,
return_logprob=local_batch.return_logprob,
lora_ineligible=prefill_graph_runner.enable_lora,
)
)
and breakable_prefill
)
)
is_extend_in_batch = local_batch.forward_mode.is_extend() if local_batch else False
if local_batch is not None:
@@ -307,11 +328,11 @@ def prepare_mlp_sync_batch_raw(
cp_size=attn_cp_size,
num_tokens=num_tokens,
num_tokens_for_logprob=num_tokens_for_logprob,
can_cuda_graph=can_cuda_graph,
can_run_decode_cuda_graph=can_run_decode_cuda_graph,
can_run_prefill_cuda_graph=can_run_prefill_cuda_graph,
is_extend_in_batch=is_extend_in_batch,
local_can_run_tbo=local_can_run_tbo,
local_forward_mode=local_forward_mode,
can_run_breakable_cuda_graph=can_run_breakable_cuda_graph,
)
if not skip_all_gather:
@@ -364,6 +385,7 @@ def prepare_mlp_sync_batch_raw(
@dataclass(kw_only=True, slots=True, frozen=True)
class SchedulerDPAttnAdapter:
model_runner: ModelRunner
tp_group: GroupCoordinator
req_to_token_pool: ReqToTokenPool
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator
@@ -379,6 +401,7 @@ class SchedulerDPAttnAdapter:
def prepare_mlp_sync_batch(self, local_batch: ScheduleBatch):
return prepare_mlp_sync_batch_raw(
local_batch,
model_runner=self.model_runner,
dp_size=self.server_args.dp_size,
attn_tp_size=self.ps.attn_tp_size,
attn_cp_size=self.ps.attn_cp_size,
@@ -50,6 +50,7 @@ import tqdm
from sglang.kernels.ops.kvcache.kv_indices import (
create_chunked_prefix_cache_kv_indices,
)
from sglang.srt.configs.model_config import is_deepseek_dsa
from sglang.srt.distributed.parallel_state import graph_capture
from sglang.srt.layers.attention.dsa.utils import is_dsa_enable_prefill_cp
from sglang.srt.layers.dp_attention import (
@@ -236,6 +237,11 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
buffer population, attention metadata init, and output slicing.
"""
# DSA forces use_mha=False in BCG capture/replay, so the sparse path
# serves any prefix and the MHA-prefix ban does not apply. Class
# default keeps __new__-built test instances on the ban.
dsa_sparse_prefill_forced: bool = False
def __init__(self, model_runner: ModelRunner):
super().__init__(model_runner)
# --- model flags ----------------------------------------------
@@ -316,6 +322,10 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
source=self.buffers,
)
self.dsa_sparse_prefill_forced = is_deepseek_dsa(
self.model_runner.model_config.hf_config
)
self.attention_layers = self.model_runner.attention_layers
self.mha_companion_layers = self.model_runner.mha_companion_layers
self.has_mha_companion_layers = any(
@@ -986,14 +996,6 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
static_forward_batch=static_forward_batch,
)
def _has_unsupported_mha_prefix(self, forward_batch: ForwardBatch) -> bool:
return (
self.prefill_backend_name == Backend.BREAKABLE
and self.has_mha_companion_layers
and forward_batch.extend_prefix_lens_cpu is not None
and any(forward_batch.extend_prefix_lens_cpu)
)
@staticmethod
def _restore_mha_capture_state(forward_batch: ForwardBatch) -> None:
"""Restore Python state omitted from breakable graph segments."""
@@ -1001,59 +1003,113 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
forward_batch.mha_return_lse = False
forward_batch.set_attn_attend_prefix_cache(False)
def can_run_graph(self, forward_batch: ForwardBatch) -> bool:
if self._is_full_backend and forward_batch.batch_size > self._capture_req_slots:
def can_replay_locally(
self,
*,
batch_size: int,
num_tokens: Optional[int],
input_embeds,
replace_embeds,
prefix_lens,
is_target_verify: bool,
capture_hidden_mode,
return_logprob: bool,
lora_ineligible: bool = False,
chunked_prefix_uncapturable: bool = False,
) -> bool:
"""Rank-local replay eligibility: the single source of truth for
``can_run_graph`` (ForwardBatch, forward time) and the dp mlp-sync
vote (ScheduleBatch, schedule time) — all dp ranks must reach the
same replay-vs-eager decision or their collectives mismatch. Pass
``capture_hidden_mode=None`` when unknown at the call site (it is
rank-uniform; forward-time-only checking cannot split the group).
"""
if self._is_full_backend and batch_size > self._capture_req_slots:
return False
# LoRA batches may only replay the graph when prepare_lora_batch put
# their metadata in the static buffers (same predicate); keyed off
# enable_lora, not lora_ids, which is non-None even without LoRA.
if self.enable_lora and not (
self._capture_lora
and self.model_runner.lora_manager.can_use_prefill_cuda_graph(forward_batch)
# LoRA replays need prepare_lora_batch's static metadata. lora_manager
# keeps LoRA prefill eager on every rank under dp attention, so the
# schedule-time vote derives this from enable_lora alone.
if lora_ineligible:
return False
if input_embeds is not None:
return False
if replace_embeds is not None:
return False
# A prefix forces the MHA companion path, whose captured state is
# frozen prefix-free; DSA models are exempt (capture/replay force
# the sparse path, which takes any prefix via device metadata).
if (
self.prefill_backend_name == Backend.BREAKABLE
and self.has_mha_companion_layers
and not self.dsa_sparse_prefill_forced
and prefix_lens is not None
and any(prefix_lens)
):
return False
if forward_batch.input_embeds is not None:
return False
if forward_batch.replace_embeds is not None:
return False
if self._has_unsupported_mha_prefix(forward_batch):
# FullCG's chunked-prefix topology covers a bounded prefix. The flag
# gating it is FULL-backend-only, so this is inert for the breakable
# vote path.
if chunked_prefix_uncapturable:
return False
# tc_piecewise captures with ForwardMode.EXTEND and spec_info=None.
if forward_batch.forward_mode.is_target_verify():
if is_target_verify:
return False
if forward_batch.capture_hidden_mode != self.capture_hidden_mode:
if (
capture_hidden_mode is not None
and capture_hidden_mode != self.capture_hidden_mode
):
return False
# BCG-with-captured-metadata under DP attention: every rank must
# have local tokens, and the batch must declare itself replayable.
# These gates are no-ops for non-DP / non-opt-in paths because
# global_num_tokens_cpu stays None.
if self._has_inactive_dp_rank(forward_batch):
if return_logprob and not self._uses_eager_prefill_tail():
return False
if num_tokens is None:
return True
if num_tokens > self.max_num_tokens:
return False
# No exact-shape check: load_batch bucket-pads; only reject
# disproportionate padding waste.
padded_num_tokens = self._pad_to_bucket(num_tokens, self.capture_num_tokens)
if padded_num_tokens > num_tokens * _MAX_PREFILL_CUDA_GRAPH_PADDING_FACTOR:
return False
return True
def can_run_graph(self, forward_batch: ForwardBatch) -> bool:
# DP check: group verdict from the schedule-time all-gather
# (min-reduced votes; also requires every rank to hold tokens).
if (
forward_batch.global_num_tokens_cpu is not None
and not forward_batch.can_run_dp_breakable_cuda_graph
):
return False
num_tokens = len(forward_batch.input_ids)
if forward_batch.return_logprob and not self._uses_eager_prefill_tail():
# Every dp rank must hold tokens this forward (reads the synced
# table post dp-padding; idle ranks vote permissively upstream).
if self._has_inactive_dp_rank(forward_batch):
return False
if num_tokens > self.max_num_tokens:
return False
padded_num_tokens = self._pad_to_bucket(num_tokens, self.capture_num_tokens)
if padded_num_tokens > num_tokens * _MAX_PREFILL_CUDA_GRAPH_PADDING_FACTOR:
return False
# Other backends and non-MLA FullCG keep using their normal graph with
# replay-refreshed metadata; only this extra topology has a prefix cap.
if (
self._capture_chunked_prefix
and self._has_prefix_hit(forward_batch)
and self._select_prefix_capture_chunks(forward_batch) is None
# Non-DP local check (sole decision for tp-only).
if not self.can_replay_locally(
batch_size=forward_batch.batch_size,
num_tokens=len(forward_batch.input_ids),
input_embeds=forward_batch.input_embeds,
replace_embeds=forward_batch.replace_embeds,
prefix_lens=forward_batch.extend_prefix_lens_cpu,
is_target_verify=forward_batch.forward_mode.is_target_verify(),
capture_hidden_mode=forward_batch.capture_hidden_mode,
return_logprob=forward_batch.return_logprob,
lora_ineligible=self.enable_lora
and not (
self._capture_lora
and self.model_runner.lora_manager.can_use_prefill_cuda_graph(
forward_batch
)
),
chunked_prefix_uncapturable=(
self._capture_chunked_prefix
and self._has_prefix_hit(forward_batch)
and self._select_prefix_capture_chunks(forward_batch) is None
),
):
return False
# load_batch bucket-pads to the nearest captured shape. The factor
# above rejects replays whose padded model work is disproportionate
# to the useful token count.
#
# Multi-req replay is supported by body-capture backends via the
# layer_model.forward monkey-patch in replay(): the captured graph runs
# the transformer stack, then the outer model.forward runs
@@ -130,6 +130,7 @@ class BreakableCudaGraphBackend(DedupedCudaGraphMixin, BaseCudaGraphBackend):
cuda_graph=graph,
pool=self._pool,
stream=self._capture_stream,
barrier_fn=self._tp_group.barrier,
):
out = captured_fn()
out_rows = self._output_rows(out, size)
@@ -25,7 +25,7 @@ buffers to keep break-point tensors at stable addresses.
import logging
import threading
from contextvars import ContextVar
from typing import Any, Callable
from typing import Any, Callable, Optional
import torch
@@ -216,7 +216,7 @@ def _copy_output(dst: Any, src: Any) -> Any:
return src
def eager_on_graph(enable: bool):
def eager_on_graph(enable: bool, capture_stub: Optional[Callable] = None):
def decorator(inner: Callable):
if not enable:
return inner
@@ -231,9 +231,21 @@ def eager_on_graph(enable: bool):
# End the segment that captured up to this break point.
capture._end_current_segment()
# Run the eager function once so it allocates its outputs and
# writes real data into them.
output = inner(*args, **kwargs)
# Re-sync ranks after segment teardown (the slow, variable
# step) before break fns with rank-coupled collectives and hard
# timeouts (DeepEP NORMAL: 100s). Capture-only; replay bypasses
# this wrapper.
if capture._barrier_fn is not None:
capture._barrier_fn()
# Run the break once so its outputs are allocated and their
# addresses recorded. A capture_stub replaces the body during
# capture (contents are never consumed; warmup and replay run
# the real inner), letting rank-coupled bodies skip the work.
if capture_stub is not None:
output = capture_stub(*args, **kwargs)
else:
output = inner(*args, **kwargs)
# Weak-ref captured inputs produced by graph segments. Their storage
# is pinned by the segment CUDAGraphs' mempool use-count, so Python
@@ -308,6 +320,7 @@ class BreakableCUDAGraphCapture:
pool=None,
stream: torch.Stream | None = None,
capture_error_mode: str = "global",
barrier_fn: Callable[[], None] | None = None,
):
assert isinstance(
cuda_graph, BreakableCUDAGraph
@@ -316,6 +329,7 @@ class BreakableCUDAGraphCapture:
self._pool = pool if pool is not None else (0, 0)
self._stream = stream
self._capture_error_mode = capture_error_mode
self._barrier_fn = barrier_fn
self._stream_ctx = None
self._capture_token = None
self._stream_token = None
+8
View File
@@ -1270,6 +1270,13 @@ class DeepseekV2MoE(nn.Module):
shared_output = self._forward_shared_experts(hidden_states)
shared_output.record_stream(self.alt_stream)
shared_event = self.alt_stream.record_event()
if is_in_breakable_cuda_graph():
# The MoE call below is an eager break, so record
# and wait must share one capture; joining here means
# the shared experts overlap nothing. The alt stream
# is kept for record_stream: without that marking the
# allocator recycles shared_output across the break.
torch.cuda.current_stream().wait_event(shared_event)
else:
shared_output = self._forward_shared_experts(hidden_states)
topk_kwargs = (
@@ -1453,6 +1460,7 @@ class DeepseekV2MoE(nn.Module):
and not sbo_enabled_flag
and self.num_fused_shared_experts == 0
and self.alt_stream is not None
and not is_in_breakable_cuda_graph()
):
torch.cuda.current_stream().wait_event(shared_event)
+53 -28
View File
@@ -41,6 +41,7 @@ from sglang.srt.arg_groups.argparse_actions import (
DeprecatedStoreTrueAction,
LoRAPathAction,
)
from sglang.srt.arg_groups.overrides import resolved_view
from sglang.srt.configs.embedding_model_spec import BCGPrefillPolicy
from sglang.srt.configs.linear_attn_model_registry import get_linear_attn_spec_by_arch
from sglang.srt.connector import ConnectorType
@@ -4207,6 +4208,7 @@ class ServerArgs:
def _handle_cuda_graph_config(self):
self._parse_cuda_graph_config()
self._apply_cuda_graph_compatibility()
self._apply_deepep_adjustments()
self._apply_cuda_graph_disaggregation_roles()
self._validate_cuda_graph_config()
# Warn on the final resolved config (not inside the compat cascade —
@@ -4218,6 +4220,30 @@ class ServerArgs:
"Use breakable or tc_piecewise for production workloads."
)
def _apply_deepep_adjustments(self):
"""Config adjustments required by the DeepEP a2a backend."""
if resolved_view(self).moe_a2a_backend != "deepep":
return
# Non-multiple-of-8 prefill buckets can hang DeepEP a2a capture under
# breakable CUDA graph
if self.cuda_graph_config.prefill.backend == Backend.BREAKABLE:
bs = self.cuda_graph_config.prefill.bs
if bs is None:
# 2048 = documented prefill default; max_bs unresolved here.
max_bs = self.cuda_graph_config.prefill.max_bs or 2048
bs = self._generate_prefill_cuda_graph_batch_sizes(max_bs)
aligned = sorted({((b + 7) // 8) * 8 for b in bs})
if aligned != sorted(bs):
logger.info(
"Breakable prefill CUDA graph with DeepEP requires bucket "
"sizes divisible by 8; aligning %s -> %s.",
sorted(bs),
aligned,
)
self.cuda_graph_config.prefill.bs = aligned
self.cuda_graph_config.prefill.max_bs = aligned[-1]
def _parse_cuda_graph_config(self):
"""Resolve cuda_graph_config from explicit JSON, per-phase
convenience flags, legacy global flags, and defaults.
@@ -4321,8 +4347,6 @@ class ServerArgs:
self.cuda_graph_config.prefill.backend = Backend.DISABLED
def _disable_tc_piecewise_cudagraph_if_incompatible(self):
from sglang.srt.arg_groups.overrides import resolved_view as _resolved_view
"""TcPiecewise (torch.compile + piecewise) is incompatible with
these configurations. Most are torch.compile / dynamo limitations.
"""
@@ -4346,7 +4370,7 @@ class ServerArgs:
),
(
"MoE A2A backend",
lambda: _resolved_view(self).moe_a2a_backend != "none",
lambda: resolved_view(self).moe_a2a_backend != "none",
),
# Dynamo blocks LoRA under tc_piecewise (per-batch LoRABatchInfo
# rebinds break guards); breakable/full support LoRA.
@@ -4359,7 +4383,7 @@ class ServerArgs:
(
"GGUF quantization",
lambda: self.load_format == "gguf"
or _resolved_view(self).quantization == "gguf"
or resolved_view(self).quantization == "gguf"
or check_gguf_file(self.model_path),
),
("DLLM (diffusion LLM)", lambda: self.dllm_algorithm is not None),
@@ -4398,17 +4422,21 @@ class ServerArgs:
self.cuda_graph_config.prefill.backend = Backend.DISABLED
def _disable_breakable_cudagraph_if_incompatible(self):
from sglang.srt.arg_groups.overrides import resolved_view as _resolved_view
"""Breakable (segmented capture, no torch.compile). Breakable enforces
memory-saver rejection in its own __init__; config-time rules can be
added here as they're discovered.
"""
from sglang.srt.configs.model_config import is_deepseek_v4
from sglang.srt.configs.model_config import is_deepseek_dsa, is_deepseek_v4
rules = [
# MLA prefill takes a different attn-forward path under BCG.
("MLA attention", lambda: self.use_mla_backend()),
# MLA prefill under BCG takes forward_mha, which has no eager
# breaks. DSA is exempt: BCG forces the sparse path, whose
# indexer already splits eagerly.
(
"MLA attention (non-DSA)",
lambda: self.use_mla_backend()
and not is_deepseek_dsa(self.get_model_config().hf_config),
),
# DSV4 is BCG-compatible but introduces heavy memory pressure: the
# c4 indexer scratch is pinned in the capture pool and OOMs. Disable.
(
@@ -4425,10 +4453,15 @@ class ServerArgs:
"decode context parallel (dcp_size > 1)",
lambda: self.dcp_size > 1,
),
# BCG bucket sizes exceed FlashInfer MoE A2A's dispatch cap.
# TBO capture is unsupported.
(
"MoE A2A backend",
lambda: _resolved_view(self).moe_a2a_backend != "none",
"two-batch overlap",
lambda: self.enable_two_batch_overlap,
),
# Only DeepEP's a2a is validated under BCG.
(
"non-DeepEP a2a backend",
lambda: resolved_view(self).moe_a2a_backend not in ("none", "deepep"),
),
# Multimodal prefill replay faults under BCG; allowlisted archs opt back in.
(
@@ -4826,12 +4859,19 @@ class ServerArgs:
# MLA backend overhead is much higher than expected with fa3.
reserved_mem += 1.5 * 1024
if (
prefill_cuda_graph_config.backend == Backend.BREAKABLE
and resolved_view(self).moe_a2a_backend == "deepep"
):
# Prefill-BCG DeepEP delta (bridge pool + NVL first-touch
# during capture); decode-side DeepEP is a baseline cost.
reserved_mem += 1 * 1024
return reserved_mem
def reserve_for_deepep_a2a_mb(self) -> float:
# DeepEP all-to-all buffers captured in the decode graph are real extra
# allocations, reserved on top of the floor.
from sglang.srt.arg_groups.overrides import resolved_view
decode_cuda_graph_config = self.cuda_graph_config.decode
if (
@@ -4991,7 +5031,6 @@ class ServerArgs:
# flags tier.
from sglang.srt.arg_groups.overrides import (
collect_model_override_declarations,
resolved_view,
validate_declarations,
)
@@ -5524,7 +5563,6 @@ class ServerArgs:
from sglang.srt.arg_groups.overrides import (
_mamba_radix_cache_resolution,
mamba_extra_buffer_of,
resolved_view,
run_post_process_pass,
)
@@ -5574,7 +5612,6 @@ class ServerArgs:
if not use_mla_backend:
# MHA architecture
from sglang.srt.arg_groups.overrides import resolved_view
if is_hopper_with_cuda_12_3() and is_no_spec_infer_or_topk_one(
resolved_view(self)
@@ -5637,7 +5674,6 @@ class ServerArgs:
_fa4_page_constraint,
_intel_xpu_page_constraint,
_mla_backend_page_constraints,
resolved_view,
run_post_process_pass,
)
@@ -5748,7 +5784,6 @@ class ServerArgs:
def _handle_kv4_compatibility(self):
"""Check FP4 KV cache compatibility with the attention backend"""
from sglang.srt.arg_groups.overrides import resolved_view
if self.kv_cache_dtype not in ("nvfp4", "fp4_mx_block16"):
return
@@ -6014,7 +6049,6 @@ class ServerArgs:
)
from sglang.srt.arg_groups.overrides import (
mamba_extra_buffer_of,
resolved_view,
)
if mamba_extra_buffer_of(resolved_view(self)):
@@ -6374,7 +6408,6 @@ class ServerArgs:
_cutlass_moe_env_override,
_moe_runner_backend_quant_constraints,
_moe_runner_fusion_disable,
resolved_view,
run_post_process_pass,
)
@@ -6493,7 +6526,6 @@ class ServerArgs:
"""Fail fast if the FlashInfer A2A dispatcher workspace cannot cover the
largest CuteDSL MoE forward. Runs after speculative decoding is resolved
so cutedsl_moe_max_num_tokens() sees the final num_tokens_per_req."""
from sglang.srt.arg_groups.overrides import resolved_view
view = resolved_view(self)
if not (
@@ -6536,7 +6568,6 @@ class ServerArgs:
_a2a_backend_overrides,
_a2a_ep_size,
_a2a_fusion_adjustments,
resolved_view,
run_post_process_pass,
)
@@ -7022,7 +7053,6 @@ class ServerArgs:
still None, backends haven't settled yet and the resolved (prefill,
decode) pair would be a stale (None, None).
"""
from sglang.srt.arg_groups.overrides import resolved_view
if not self.prefill_only_disable_kv_cache:
return
@@ -7595,7 +7625,6 @@ class ServerArgs:
from sglang.srt.arg_groups.overrides import (
_deterministic_attention_backend,
_deterministic_sampling_backend,
resolved_view,
run_post_process_pass,
)
@@ -7841,7 +7870,6 @@ class ServerArgs:
)
def _handle_other_validations(self):
from sglang.srt.arg_groups.overrides import resolved_view
# Handle optimistic prefill validation
if (
@@ -8276,7 +8304,6 @@ class ServerArgs:
def _resolved(self):
"""Read-only view of the resolving configuration: declared fields
resolve from the declaration stash."""
from sglang.srt.arg_groups.overrides import resolved_view
return resolved_view(self)
@@ -8338,7 +8365,6 @@ class ServerArgs:
view so declared fields resolve from the declaration stash."""
from sglang.srt.arg_groups.overrides import (
attention_backends_of,
resolved_view,
)
return attention_backends_of(resolved_view(self))
@@ -8406,7 +8432,6 @@ class ServerArgs:
# (or mamba_chunk_size if it is defined in the model's config) and page_size.
# It is used to determine the caching point in a sequence during prefill.
if not hasattr(self, "_mamba_cache_chunk_size"):
from sglang.srt.arg_groups.overrides import resolved_view
hf_config = self.get_model_config().hf_config
chunk_size = getattr(hf_config, "mamba_chunk_size", FLA_CHUNK_SIZE)
@@ -191,10 +191,10 @@ def prepare_for_draft_extend(
# Supply CPU mirror (extend_seq_lens are all num_window_tokens) so
# backend max() reads from list without a per-iter D2H sync.
forward_batch.extend_seq_lens_cpu = [num_window_tokens] * bs
can_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
can_run_decode_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
forward_batch
)
if not batch.forward_mode.is_idle() and not can_cuda_graph:
if not batch.forward_mode.is_idle() and not can_run_decode_cuda_graph:
draft_model_runner.attn_backend.init_forward_metadata(forward_batch)
# Planned pre-pad; do NOT opt into post-pad re-plan. DSA's indexer
# cannot rebuild its deep_gemm schedule_meta on a DP-padded batch
@@ -204,7 +204,7 @@ def prepare_for_draft_extend(
# On NPU with --disable-cuda-graph, block_table shape won't match
# after prepare_mlp_sync_batch padding; defer re-init to
# forward_extend (post-pad) instead.
if not is_npu() or can_cuda_graph:
if not is_npu() or can_run_decode_cuda_graph:
forward_batch.mark_forward_metadata_ready()
return forward_batch
@@ -307,10 +307,10 @@ def prepare_for_draft(
capture_hidden_mode=capture_mode,
return_hidden_states_before_norm=False,
)
can_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
can_run_decode_cuda_graph = cuda_graph_runner and cuda_graph_runner.can_run_graph(
forward_batch
)
return forward_batch, can_cuda_graph
return forward_batch, can_run_decode_cuda_graph
def build_eagle_verify_input(
@@ -468,7 +468,7 @@ class EagleDraftWorker(EagleDraftWorkerBase):
def draft(self, batch: ScheduleBatch):
draft_input: EagleDraftInput = batch.spec_info
forward_batch, can_cuda_graph = prepare_for_draft(
forward_batch, can_run_decode_cuda_graph = prepare_for_draft(
draft_input,
self.req_to_token_pool,
batch,
@@ -478,12 +478,12 @@ class EagleDraftWorker(EagleDraftWorkerBase):
self.speculative_num_steps,
)
if (
can_cuda_graph
can_run_decode_cuda_graph
and not forward_batch.forward_mode.is_idle()
and self.seed_dsa_topk_from_draft_extend
and draft_input.dsa_topk_indices is None
):
can_cuda_graph = False
can_run_decode_cuda_graph = False
n_inner = self.speculative_num_steps - 1
canary_outside_ctx = (
@@ -497,7 +497,7 @@ class EagleDraftWorker(EagleDraftWorkerBase):
with canary_outside_ctx:
# Run draft
if can_cuda_graph:
if can_run_decode_cuda_graph:
parent_list, top_scores_index, draft_tokens, draft_probs = (
self.cuda_graph_runner.execute(forward_batch)
)
@@ -882,14 +882,14 @@ class EagleDraftWorker(EagleDraftWorkerBase):
)
# Run draft extend batch in the main compute stream
can_cuda_graph = (
can_run_decode_cuda_graph = (
self.cuda_graph_runner_for_draft_extend
and self.cuda_graph_runner_for_draft_extend.can_run_graph(forward_batch)
)
# Eager path publishes the indexer top-k into a worker buffer (the graph
# path uses the runner's static buffer). Gathered at select_index below.
if self.seed_dsa_topk_from_draft_extend and not can_cuda_graph:
if self.seed_dsa_topk_from_draft_extend and not can_run_decode_cuda_graph:
forward_batch.spec_info.dsa_seed_topk_capture = (
self._get_dsa_extend_topk_buf(forward_batch.input_ids.shape[0])
)
@@ -906,7 +906,7 @@ class EagleDraftWorker(EagleDraftWorkerBase):
else contextlib.nullcontext()
)
with canary_ctx:
if can_cuda_graph:
if can_run_decode_cuda_graph:
draft_logits_output = self.cuda_graph_runner_for_draft_extend.execute(
forward_batch
)
@@ -917,18 +917,18 @@ class EagleDraftWorker(EagleDraftWorkerBase):
maybe_detect_nan(
draft_logits_output.next_token_logits,
f"draft_extend_for_decode (cuda_graph={can_cuda_graph})",
f"draft_extend_for_decode (cuda_graph={can_run_decode_cuda_graph})",
)
maybe_detect_inf(
draft_logits_output.next_token_logits,
f"draft_extend_for_decode (cuda_graph={can_cuda_graph})",
f"draft_extend_for_decode (cuda_graph={can_run_decode_cuda_graph})",
)
# Gather the per-request last-position indexer top-k as the next loop's
# seed (select_index already picks the last accepted position per req).
dsa_seed_topk_indices = None
if self.seed_dsa_topk_from_draft_extend:
if can_cuda_graph:
if can_run_decode_cuda_graph:
dsa_extend_topk_capture = (
self.cuda_graph_runner_for_draft_extend.buffers.dsa_seed_topk_capture
)
@@ -406,7 +406,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
def draft(self, batch: ScheduleBatch):
draft_input: EagleDraftInput = batch.spec_info
forward_batch, can_cuda_graph = prepare_for_draft(
forward_batch, can_run_decode_cuda_graph = prepare_for_draft(
draft_input,
self.req_to_token_pool,
batch,
@@ -732,7 +732,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
forward_batch.spec_info.num_accept_tokens = batch_result.accept_lens
# Run draft extend batch in the main compute stream
can_cuda_graph = (
can_run_decode_cuda_graph = (
self.cuda_graph_runner_for_draft_extend
and self.cuda_graph_runner_for_draft_extend.can_run_graph(forward_batch)
)
@@ -742,7 +742,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
ret_draft_probs = None
next_token_ids_backup = batch_result.next_token_ids.clone()
if can_cuda_graph:
if can_run_decode_cuda_graph:
# Graph replay bypasses ModelRunner.forward, which emits the
# step[...] trace span for every other phase; emit it here.
with profile_range(build_step_span_name(forward_batch)):