Skip to content

feat(serving): SGLang draft classes for the Qwen3.8-27B DSpark MoE drafter (SGLang FusedMoE path) + tuned fused-MoE configs - #941

Draft
yushengsu-thu wants to merge 1 commit into
sgl-project:mainfrom
yushengsu-thu:feat/sglang-qwen3-moe-dspark-serving
Draft

yushengsu-thu wants to merge 1 commit into
sgl-project:mainfrom
yushengsu-thu:feat/sglang-qwen3-moe-dspark-serving

Conversation

@yushengsu-thu

Copy link
Copy Markdown
Contributor

What

SGLang cannot load SpecForge's MoE-FFN drafters, so the Qwen3.8-27B DSpark MoE drafters trained by #811 / #812 / kan/moe-3-qwen38 (e.g. RadixArk/qwen38-dspark-moe-3ep-cont-step9916) could only be served from a private SGLang copy, and untuned. This PR adds a serving package SGLang loads through SGLANG_EXTERNAL_MODEL_PACKAGE, plus the tuned fused-MoE kernel configs for that drafter's expert shape.

  • specforge/serving/sglang_models/dflash_moe.py: Qwen3MoeDSparkModel (and DFlashMoEDraftModel / DFlash2MoEDraftModel) = SGLang's own Qwen3DSparkModel / DFlashDraftModel / DFlash2DraftModel with each layer's MLP replaced by SGLang's own MoE block (TopK + FusedMoE + a sigmoid-gated shared expert, the same modules as Qwen2MoeSparseMoeBlock). Checkpoints load through the modules' own weight loaders (FusedMoE.make_expert_params_mapping for the per-expert tensors), so expert-width padding, runner-specific layouts (Triton / flashinfer TRT-LLM), quantised checkpoints and TP sharding are handled by SGLang. A strict check refuses partially matching exports. Router logits are computed in fp32 with the folded centering bias exactly as the trainer does, so a token selects the same experts as in training. Qwen (experts.{i}.gate_proj) and DeepSeek (experts.{i}.w1) checkpoint naming are both accepted.
  • specforge/serving/sglang_models/moe_ffn.py: the same FFN in plain PyTorch (no SGLang import), used as the reference by the gate and the tests.
  • patches/sglang/moe_configs/: tuned fused-MoE Triton configs E=512,N=512 for H200 and GB300 (SGLang ships none, so this shape runs with the generic tile config), the stand-in Qwen3MoeForCausalLM config and a wrapper for SGLang's tuner, README with usage and numbers.
  • scripts/gates/check_dspark_moe_sglang_equivalence.py: served block vs plain reference at a small shape and at the real one (512 x 512 top-10, shared 2048): identical top-k, combine weights within 1e-6, bf16-level output drift, truncated export refused; --moe-runner-backend triton|flashinfer_trtllm. Compares the reference against the trainer's MoELayer too when that package is in the checkout (it is not on main yet).
  • tests/test_serving/: CPU tests for the reference recipe, preset defaults and name mapping.

Supported recipe: the qwen3_5_moe preset (softmax, renormalised top-k, optional router bias, one sigmoid-gated shared expert). DeepSeek-V4-style recipes (sqrtsoftplus, selection bias, groups, SwiGLU clamp) raise a clear error.

Usage

pip install -e .            # SpecForge with this package
# tuned kernel config (optional, +0.5-2%): copy into SGLang's config dir
C=$(python -c "import sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe_triton_config as m, os; print(os.path.dirname(m.__file__))")/configs/triton_3_7_1
cp patches/sglang/moe_configs/configs/triton_3_7_1/E=512,N=512,device_name=*.json "$C/"

SGLANG_EXTERNAL_MODEL_PACKAGE=specforge.serving.sglang_models python -m sglang.launch_server \
  --model-path Qwen/Qwen3.8-27B --speculative-algorithm DSPARK --speculative-dspark-block-size 7 \
  --speculative-draft-model-path RadixArk/qwen38-dspark-moe-3ep-cont-step9916 \
  --speculative-draft-model-quantization unquant --attention-backend triton --speculative-draft-attention-backend triton \
  --moe-runner-backend triton      # or flashinfer_trtllm on Blackwell

The export's config.json must say architectures: ["Qwen3MoeDSparkModel"] with the Qwen MoE keys (num_experts, num_experts_per_tok, moe_intermediate_size, shared_expert_intermediate_size, moe_router_bias), which SpecForge's normalizer on the MoE branches writes.

Validation

Gate (check_dspark_moe_sglang_equivalence.py), real shape, 7 / 56 / 448 tokens: PASS on GB300 with the Triton and the flashinfer TRT-LLM runner, and on H200 (Triton); routing identical to the reference, max |dy| 1.6e-2 to 3.1e-2 at mean |y| 0.5 (bf16 level).

End-to-end, bf16 target Qwen/Qwen3.8-27B, drafters bf16, specforge benchmark gsm8k / mt-bench (100 / 200 prompts, 512 new tokens), DSPARK block 7, triton attention, mamba cache 160, max 32 running requests. accept length / tok/s (speedup vs no draft):

GB300 (4 GPUs, one server per GPU):

dataset c none official dense v1 MoE, default config MoE, tuned config MoE, TRT-LLM runner
gsm8k 1 101 5.16 / 339 (3.37x) 5.21 / 336 (3.34x) 5.21 / 340 (3.38x) 5.21 / 343 (3.41x)
gsm8k 8 665 5.15 / 1465 (2.20x) 5.20 / 1435 (2.16x) 5.20 / 1457 (2.19x) 5.20 / 1469 (2.21x)
gsm8k 32 1804 5.17 / 3695 (2.05x) 5.23 / 3473 (1.92x) 5.23 / 3487 (1.93x) 5.23 / 3521 (1.95x)
mt-bench 1 101 3.63 / 218 (2.17x) 3.72 / 217 (2.15x) 3.72 / 220 (2.18x) 3.72 / 221 (2.20x)
mt-bench 8 678 3.89 / 1095 (1.62x) 3.99 / 1065 (1.57x) 3.99 / 1084 (1.60x) 3.99 / 1097 (1.62x)
mt-bench 32 1885 4.02 / 2682 (1.42x) 4.11 / 2380 (1.26x) 4.11 / 2392 (1.27x) 4.11 / 2411 (1.28x)

(dense measured alone after the others; the MoE rows ran concurrently.)

Loader check, this package (SGLang's weight loaders) vs a stacked direct-copy loader on the same checkpoint, kernel and config, started together: GB300 gsm8k c=1/8/32 339 / 1367 / 3395 vs 330 / 1342 / 3381 tok/s, mt-bench 218 / 1039 / 2353 vs 215 / 1025 / 2336; H200 (1 GPU, mamba 48, c=1/8) gsm8k 243 / 1173 vs 243 / 1128, mt-bench 157 / 815 vs 157 / 841. Same speed within noise; the 39 GB export loads in 8 s (GB300) / 38 s (H200). Accept lengths identical.

Tuning the kernel config is worth 0.4-1.8% here (0.5-1.1% with an FP8 target, see the README): the drafter's MoE FFN is bandwidth-bound at this expert shape and is 9-21% of a speculative step.

Relation to other PRs

🤖 Generated with Claude Code

…after (Kan's MoE drafters) + tuned fused-MoE configs

SGLang cannot load SpecForge's MoE-FFN drafters (the Qwen3.8-27B DSpark MoE
exports of sgl-project#811/sgl-project#812 and kan/moe-3-qwen38, e.g. RadixArk/qwen38-dspark-moe-3ep-cont-step9916).
This adds `specforge.serving.sglang_models`, an SGLANG_EXTERNAL_MODEL_PACKAGE
with `Qwen3MoeDSparkModel` (plus DFlash/DFlash2 MoE variants): SGLang's own
`Qwen3DSparkModel` with every layer's MLP replaced by SGLang's own MoE block
(`TopK` + `FusedMoE` + sigmoid-gated shared expert, as in `Qwen2MoeSparseMoeBlock`).
Weights load through the modules' own weight loaders
(`FusedMoE.make_expert_params_mapping` for the per-expert tensors), so
expert-width padding, runner-specific layouts (Triton / flashinfer TRT-LLM),
quantised checkpoints and TP sharding stay SGLang's business; a strict check
refuses partially matching exports. Router logits are computed in fp32 with
the folded centering bias exactly as the trainer does, so routing matches
training; both the Qwen and the DeepSeek checkpoint naming are accepted.

Also ships the tuned fused-MoE Triton configs for this drafter's expert shape
(512 x 512, top-10) for H200 and GB300 under patches/sglang/moe_configs, with
the stand-in config and a wrapper for SGLang's tuner (SGLang has no config for
E=512,N=512 and runs the kernel untuned otherwise), plus the equivalence gate
(served block vs plain-PyTorch reference, identical routing, bf16-level
drift, truncated export refused; passes on H200 and GB300 for the Triton and
the TRT-LLM runner) and CPU unit tests.

Measured (bf16 target Qwen/Qwen3.8-27B, FP8-free, gsm8k/mt-bench, c=1/8/32):
on GB300 the MoE drafter now matches the official dense drafter at c=1/8 and
trails 5-10% at c=32 with 1-2% higher accept length; tuned config +0.4-1.8%,
TRT-LLM runner +1-3%. The standard loader is as fast as a direct stacked
copy end to end (GB300 +0.4-2.5%, H200 within noise) and loads in 8 s (GB300)
/ 38 s (H200).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 03:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@yushengsu-thu
yushengsu-thu marked this pull request as draft October 8, 2026 03:03
@yushengsu-thu

Copy link
Copy Markdown
Contributor Author

Follow-up: if need to support - DeepSeek-V4 MoE drafters (#812)

sgl-project/SpecForge#941 serves the Qwen recipe only. The DSV4-Flash drafter (64 x 2048 experts, top-6, ungated shared expert, sqrtsoftplus scoring, noaux_tc selection bias, scaling 1.5, SwiGLU clamp 10) needs:

Routing: build TopK with scoring_func="sqrtsoftplus", correction_bias= the export's fp32 gate.bias (selection only, not added to logits), use_grouped_topk / num_expert_group / topk_group, routed_scaling_factor=1.5. SGLang already implements all of this.
Experts: FusedMoE(..., swiglu_limit=10.0); Triton runner only (the TRT-LLM runner has no sqrtsoftplus routing). Loading is unchanged.
Shared expert: ungated, with the same clamp (add a small swiglu_clamped activation; SiluAndMul has no clamp).
Model class: a DeepSeekMoESparseBlock sibling of QwenMoESparseBlock, and a draft class based on SGLang's DeepseekV4ForCausalLMDSpark (V4 attention/residual layout, not Qwen3's); extend check_loaded for gate.bias, no shared_expert_gate.
Export: the normalizer writes the new architecture name; the DeepSeek config keys are already there.
Validation: add the deepseek_v4 case to the gate at 64 x 2048 (the plain reference already supports it); tune E=64,N=2048 for the serving GPU.

About 150 lines, no SGLang changes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants