[fix] Skip routed expert capture for draft model under spec v2 (#26980)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jiajun Li
2026-06-24 16:56:07 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7e63feee6f
commit e4bf0043fe
5 changed files with 67 additions and 14 deletions
@@ -5,8 +5,11 @@ from sgl_kernel_npu.norm.l1_norm import l1_norm
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
from sglang.srt.eplb.expert_location_dispatch import topk_ids_logical_to_physical
from sglang.srt.layers.moe.topk import StandardTopKOutput, select_experts
from sglang.srt.state_capturer.routed_experts import get_global_experts_capturer
from sglang.srt.layers.moe.topk import (
StandardTopKOutput,
capture_routed_experts_if_allowed,
select_experts,
)
if TYPE_CHECKING:
from sglang.srt.eplb.expert_location_dispatch import ExpertLocationDispatchInfo
@@ -104,10 +107,6 @@ def fused_topk_npu(
if expert_location_dispatch_info is not None:
topk_ids = topk_ids_logical_to_physical(topk_ids, expert_location_dispatch_info)
get_global_expert_distribution_recorder().on_select_experts(topk_ids=topk_ids)
if (cap := get_global_experts_capturer()) is not None:
cap.capture(
layer_id=layer_id,
topk_indices=topk_ids,
)
capture_routed_experts_if_allowed(topk_config, layer_id, topk_ids)
return StandardTopKOutput(topk_weights, topk_ids, router_logits)
@@ -1103,6 +1103,7 @@ class FusedMoE(torch.nn.Module):
topk_output.topk_config.correction_bias,
topk_output.topk_config.renormalize,
self.layer_id,
topk_output.topk_config.allow_routed_experts_capture,
)
else:
# Make sure there is torch lib op registration for the whole moe layer
@@ -1353,6 +1354,7 @@ def fused_moe_bypassed_piecewise_cuda_graph_impl(
correction_bias: Optional[torch.Tensor],
renormalize: bool,
layer_id: int,
allow_routed_experts_capture: bool,
) -> torch.Tensor:
topk_output = BypassedTopKOutput(
hidden_states=hidden_states,
@@ -1363,6 +1365,7 @@ def fused_moe_bypassed_piecewise_cuda_graph_impl(
num_expert_group=num_expert_group,
correction_bias=correction_bias,
renormalize=renormalize,
allow_routed_experts_capture=allow_routed_experts_capture,
),
)
forward_context = get_tc_piecewise_forward_context()
+25 -5
View File
@@ -222,6 +222,9 @@ class TopKConfig:
fused_shared_experts_scaling_factor: Optional[float] = None
output_format: Optional[TopKOutputFormat] = None
scoring_func: str = "softmax"
# Draft-side MoE blocks set this False so they never write the target's
# process-global routed-experts capture buffer.
allow_routed_experts_capture: bool = True
# -------------------------------- TopKOutput ---------------------------------------
@@ -386,6 +389,7 @@ class TopK(MultiPlatformOp):
output_format: Optional[TopKOutputFormat] = None,
fused_shared_experts_scaling_factor: Optional[float] = None,
is_fp4_experts: bool = False,
allow_routed_experts_capture: bool = True,
):
# NOTE: scoring_func is not used for now, but we keep it for future use
# see https://github.com/sgl-project/sglang/pull/4505 for more details
@@ -426,6 +430,7 @@ class TopK(MultiPlatformOp):
fused_shared_experts_scaling_factor=fused_shared_experts_scaling_factor,
output_format=output_format,
scoring_func=scoring_func,
allow_routed_experts_capture=allow_routed_experts_capture,
)
def _apply_deepep_waterfill(
@@ -1597,6 +1602,25 @@ def _remap_topk_for_deepep(
return topk_ids, topk_weights
def capture_routed_experts_if_allowed(
topk_config: TopKConfig,
layer_id: Optional[int],
topk_ids: torch.Tensor,
) -> None:
"""Single capture site for every backend, gated by the per-config opt-out.
Routing all backends through here keeps the draft-side opt-out from being
bypassed by an inlined capturer call.
"""
if not topk_config.allow_routed_experts_capture:
return
if (cap := get_global_experts_capturer()) is not None:
cap.capture(
layer_id=layer_id,
topk_indices=topk_ids,
)
def _post_process_topk_ids(
topk_ids: torch.Tensor,
topk_weights: torch.Tensor,
@@ -1610,11 +1634,7 @@ def _post_process_topk_ids(
fused_shared_experts_scaling_factor = (
topk_config.fused_shared_experts_scaling_factor
)
if (cap := get_global_experts_capturer()) is not None:
cap.capture(
layer_id=layer_id,
topk_indices=topk_ids,
)
capture_routed_experts_if_allowed(topk_config, layer_id, topk_ids)
recorder_topk_ids = None
if _is_cuda:
# LP path: solve LP outside torch.compile (the solver contains an
@@ -192,6 +192,7 @@ from sglang.srt.state_capturer.indexer_topk import (
)
from sglang.srt.state_capturer.routed_experts import (
RoutedExpertsCapturer,
disable_routed_experts_capture_for_draft,
get_global_experts_capturer,
set_global_experts_capturer,
)
@@ -699,6 +700,11 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.load_model()
self._prepare_moe_topk()
# Must run before backend/graph init so no draft graph records a
# routed-experts capture-write kernel.
if self.is_draft_worker:
disable_routed_experts_capture_for_draft(self.model)
# Load the expert backup client
self.expert_backup_client = (
ExpertBackupClient(self.server_args, self)
@@ -960,6 +966,12 @@ class ModelRunner(ModelRunnerKVCacheMixin):
self.model_config.full_attention_layer_ids = full_attention_layer_ids
def init_routed_experts_capturer(self):
if self.is_draft_worker:
# Capture is target-only. The draft worker runs in the same process
# as its target and inits after it, so installing a capturer here
# would overwrite the target's process-global one.
return
if not self.server_args.disable_shared_experts_fusion and hasattr(
self.model, "num_fused_shared_experts"
):
@@ -2960,7 +2972,10 @@ class ModelRunner(ModelRunnerKVCacheMixin):
output.expert_distribution_metrics = recorder_outputs.get("metrics")
no_copy_to_cpu = not self.server_args.disable_overlap_schedule
if (experts_capturer := get_global_experts_capturer()) is not None:
if (
not self.is_draft_worker
and (experts_capturer := get_global_experts_capturer()) is not None
):
output.routed_experts_output = experts_capturer.on_forward_end(
forward_batch=forward_batch,
can_run_graph=output.can_run_graph,
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Any, Optional
import numpy as np
import pybase64
@@ -146,3 +146,19 @@ def extract_routed_experts_from_meta_info(data):
pybase64.b64decode(routed_experts_base64.encode("utf-8")), dtype=np.int32
)
return routed_experts
def disable_routed_experts_capture_for_draft(model: Any) -> None:
"""Opt every draft MoE ``TopK`` out of routed-experts (R3) capture.
Capture is target-only; a draft ``TopK`` must never write the target's
process-global buffer. ``HashTopK`` has no ``topk_config`` and never
captures, so it is left untouched.
"""
# Lazy import: ``layers.moe.topk`` imports ``get_global_experts_capturer``
# from this module, so a top-level import here would be circular.
from sglang.srt.layers.moe.topk import TopK
for module in model.modules():
if isinstance(module, TopK):
module.topk_config.allow_routed_experts_capture = False