[bugfix]: size CuteDSL MoE allgather buffers for the worst-case forward (#26696)

This commit is contained in:
Jimmy Shong
2026-05-30 00:27:20 -07:00
committed by GitHub
parent 7662210406
commit 716e670d3d
4 changed files with 109 additions and 50 deletions
@@ -221,17 +221,9 @@ def resolve_cutedsl_standard_scales(
return w1_alpha, fc2_input_scale, w2_alpha, used_input_scale return w1_alpha, fc2_input_scale, w2_alpha, used_input_scale
def ensure_cutedsl_wrapper(layer: torch.nn.Module, num_tokens: int = 0) -> None: def ensure_cutedsl_wrapper(layer: torch.nn.Module) -> None:
"""Lazily create CuteDslMoEWrapper and resolve scales on first forward. """Lazily create CuteDslMoEWrapper and resolve scales on first forward.
Args:
layer: The FusedMoE layer module.
num_tokens: Current token count entering the MoE layer. Used as
the buffer size for the non-a2a (allgather) path, where the
autotune dummy run passes req_to_token_pool.size * dp_size —
the worst-case post-allgather batch. For the a2a path this
is ignored in favour of the dispatcher's workspace limit.
The wrapper is created lazily (not in __init__ / create_weights) because The wrapper is created lazily (not in __init__ / create_weights) because
it depends on final weight shapes and EP configuration. The wrapper's it depends on final weight shapes and EP configuration. The wrapper's
CUDA-graph buffers are allocated inside CuteDslMoEWrapper.__init__, which CUDA-graph buffers are allocated inside CuteDslMoEWrapper.__init__, which
@@ -259,20 +251,18 @@ def ensure_cutedsl_wrapper(layer: torch.nn.Module, num_tokens: int = 0) -> None:
) )
server_args = get_global_server_args() server_args = get_global_server_args()
use_cuda_graph = server_args is not None and not server_args.disable_cuda_graph use_cuda_graph = not server_args.disable_cuda_graph
# Buffer size must cover the worst-case token count the MoE layer can see. # Size the wrapper's CUDA-graph buffers for the largest number of tokens a
# - A2A path: dispatch returns tensors flattened from # single forward can route through this layer.
# [ep_size, max_tokens_per_rank, ...].
# - Standard allgather path: dp_size * max local tokens per rank.
dispatcher = getattr(layer, "dispatcher", None) dispatcher = getattr(layer, "dispatcher", None)
if hasattr(dispatcher, "max_num_tokens"): if hasattr(dispatcher, "max_num_tokens"):
# A2A path: bounded by the dispatcher's own workspace limit.
max_num_tokens = dispatcher.max_num_tokens * getattr(dispatcher, "ep_size", 1) max_num_tokens = dispatcher.max_num_tokens * getattr(dispatcher, "ep_size", 1)
else: else:
# Standard allgather path: num_tokens from the first forward is # Standard allgather path: the MoE sees up to dp_size local forwards
# req_to_token_pool.size * dp_size (the autotune dummy run's batch), # gathered together, so scale the per-rank forward bound by dp_size.
# which is the worst-case post-allgather token count. max_num_tokens = server_args.dp_size * server_args.cutedsl_moe_max_num_tokens()
max_num_tokens = max(num_tokens, 1)
top_k = layer.top_k if layer.top_k is not None else layer.moe_runner_config.top_k top_k = layer.top_k if layer.top_k is not None else layer.moe_runner_config.top_k
# inference_mode(False) ensures the wrapper's pre-allocated CUDA-graph # inference_mode(False) ensures the wrapper's pre-allocated CUDA-graph
# buffers are normal tensors. This call typically happens inside # buffers are normal tensors. This call typically happens inside
@@ -2200,7 +2200,7 @@ class ModelOptNvFp4FusedMoEMethod(FusedMoEMethodBase):
# v2 standard path (a2a=none/flashinfer): uses CuteDslMoEWrapper # v2 standard path (a2a=none/flashinfer): uses CuteDslMoEWrapper
# with [Up, Gate] interleaved weights and MMA blockscales. # with [Up, Gate] interleaved weights and MMA blockscales.
ensure_cutedsl_wrapper(layer, dispatch_output.hidden_states.shape[0]) ensure_cutedsl_wrapper(layer)
w1_alpha, fc2_input_scale, w2_alpha = layer._cutedsl_scales w1_alpha, fc2_input_scale, w2_alpha = layer._cutedsl_scales
quant_info = CuteDslFp4MoeQuantInfo( quant_info = CuteDslFp4MoeQuantInfo(
w13_weight=layer.w13_weight, w13_weight=layer.w13_weight,
+59 -31
View File
@@ -991,6 +991,9 @@ class ServerArgs:
handle_speculative_decoding(self) handle_speculative_decoding(self)
# Validate the CuteDSL A2A token budget now that num_tokens_per_bs is final.
self._validate_cutedsl_a2a_token_budget()
# Handle model loading format. # Handle model loading format.
self._handle_load_format() self._handle_load_format()
@@ -3328,6 +3331,62 @@ class ServerArgs:
self.ep_size == 1 self.ep_size == 1
), "FP8/MXFP8 Cutlass MoE is only supported with ep_size == 1" ), "FP8/MXFP8 Cutlass MoE is only supported with ep_size == 1"
def cutedsl_moe_max_num_tokens(self) -> int:
"""Largest number of tokens a single forward routes through a CuteDSL
MoE layer on one (DP) rank. Single source of truth for both the
standard-allgather wrapper buffers and the FlashInfer A2A dispatcher
budget. Max over the prefill (max_prefill_tokens), piecewise-prefill
capture (piecewise_cuda_graph_max_tokens), and decode/verify
(cuda_graph_max_bs * num_tokens_per_bs) bounds; num_tokens_per_bs is
speculative_num_draft_tokens under speculative decoding, else 1.
"""
if self.speculative_algorithm:
num_tokens_per_bs = self.speculative_num_draft_tokens or 1
else:
num_tokens_per_bs = 1
prefill_tokens = self.max_prefill_tokens
if not self.disable_piecewise_cuda_graph:
prefill_tokens = max(
prefill_tokens, self.piecewise_cuda_graph_max_tokens or 0
)
decode_tokens = (self.cuda_graph_max_bs or 0) * num_tokens_per_bs
return max(prefill_tokens, decode_tokens)
def _validate_cutedsl_a2a_token_budget(self):
"""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_bs."""
if not (
self.moe_a2a_backend == "flashinfer"
and self.moe_runner_backend == "flashinfer_cutedsl"
and self.max_prefill_tokens > 0
and self.disaggregation_mode != "decode"
):
return
required_tokens = self.cutedsl_moe_max_num_tokens()
max_dispatch_tokens_per_rank = get_int_env_var(
"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK", 1024
)
max_cutedsl_tokens = max_dispatch_tokens_per_rank * self.ep_size
if max_cutedsl_tokens < required_tokens:
required_per_rank = (required_tokens + self.ep_size - 1) // self.ep_size
raise ValueError(
"FlashInfer MoE A2A with flashinfer_cutedsl requires "
"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK * "
"ep_size to cover the largest CuteDSL MoE forward "
f"({required_tokens} tokens). Otherwise the FlashInfer "
"dispatcher can crash at runtime with "
"`ValueError: num_tokens (...) exceeds max_num_tokens (...)`. "
"Current values: "
f"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK="
f"{max_dispatch_tokens_per_rank}, ep_size={self.ep_size}, "
f"capacity={max_cutedsl_tokens}, required={required_tokens}. "
f"Set `export "
f"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK="
f"{required_per_rank}` or lower the relevant limit "
f"(e.g. --max-prefill-tokens) to <= {max_cutedsl_tokens}."
)
def _handle_a2a_moe(self): def _handle_a2a_moe(self):
if self.enable_deepep_waterfill and self.moe_a2a_backend != "deepep": if self.enable_deepep_waterfill and self.moe_a2a_backend != "deepep":
logger.warning( logger.warning(
@@ -3423,37 +3482,6 @@ class ServerArgs:
"flashinfer_cutlass", "flashinfer_cutlass",
"flashinfer_cutedsl", "flashinfer_cutedsl",
], "Flashinfer MoE A2A is only supported with flashinfer_cutlass or flashinfer_cutedsl moe runner backend" ], "Flashinfer MoE A2A is only supported with flashinfer_cutlass or flashinfer_cutedsl moe runner backend"
if (
self.moe_runner_backend == "flashinfer_cutedsl"
and self.max_prefill_tokens is not None
and self.max_prefill_tokens > 0
and self.disaggregation_mode != "decode"
):
max_dispatch_tokens_per_rank = get_int_env_var(
"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK", 1024
)
max_cutedsl_tokens = max_dispatch_tokens_per_rank * self.ep_size
if max_cutedsl_tokens < self.max_prefill_tokens:
required_per_rank = (
self.max_prefill_tokens + self.ep_size - 1
) // self.ep_size
raise ValueError(
"FlashInfer MoE A2A with flashinfer_cutedsl requires "
"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK * "
"ep_size to cover --max-prefill-tokens. Otherwise the "
"FlashInfer dispatcher can crash at runtime with "
"`ValueError: num_tokens (...) exceeds max_num_tokens (...)` "
"when a local DP rank schedules too many prefill tokens. "
"Current values: "
f"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK="
f"{max_dispatch_tokens_per_rank}, ep_size={self.ep_size}, "
f"capacity={max_cutedsl_tokens}, "
f"max_prefill_tokens={self.max_prefill_tokens}. "
f"Set `export "
f"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK="
f"{required_per_rank}` or lower `--max-prefill-tokens` "
f"to <= {max_cutedsl_tokens}."
)
if self.moe_a2a_backend == "mori": if self.moe_a2a_backend == "mori":
self.ep_size = self.tp_size self.ep_size = self.tp_size
@@ -618,5 +618,46 @@ class TestPrefillOnlyDisableKvCache(unittest.TestCase):
ServerArgs(**self._base_kwargs(kv_cache_dtype="fp4_e2m1")) ServerArgs(**self._base_kwargs(kv_cache_dtype="fp4_e2m1"))
class TestCutedslMoeMaxNumTokens(unittest.TestCase):
"""The shared CuteDSL MoE per-forward token bound. Fields are set directly
to exercise the math independently of __post_init__ resolution."""
def _args(self, **overrides):
server_args = ServerArgs(model_path="dummy")
fields = dict(
speculative_algorithm=None,
speculative_num_draft_tokens=None,
max_prefill_tokens=16384,
disable_piecewise_cuda_graph=False,
piecewise_cuda_graph_max_tokens=2048,
cuda_graph_max_bs=512,
)
fields.update(overrides)
for key, value in fields.items():
setattr(server_args, key, value)
return server_args
def test_prefill_dominates_in_default_config(self):
self.assertEqual(self._args().cutedsl_moe_max_num_tokens(), 16384)
def test_speculative_decoding_scales_decode_bound(self):
# decode bound 512 * 8 dominates the small prefill/piecewise bounds
args = self._args(
max_prefill_tokens=512,
piecewise_cuda_graph_max_tokens=512,
speculative_algorithm="EAGLE",
speculative_num_draft_tokens=8,
)
self.assertEqual(args.cutedsl_moe_max_num_tokens(), 4096)
def test_piecewise_bound_excluded_when_disabled(self):
args = self._args(
max_prefill_tokens=512,
disable_piecewise_cuda_graph=True,
cuda_graph_max_bs=64,
)
self.assertEqual(args.cutedsl_moe_max_num_tokens(), 512)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()