Repository navigation
exp - feat(serving): SGLang MoE DSpark draft loader for Kan's MoE drafter (#811/#812), normalizer arch name and equivalence gate (stacked on #931) - #940
Draft
yushengsu-thu wants to merge 9 commits into
Conversation
… seams) Lays out specforge/modeling/draft/moe as one configurable MoE layer with swappable components, and wires every seam a target-family implementation needs — with no routing math yet: - config.py: MoEConfig, moe_preset registry, resolve_moe_config(). Architecture keys use the target checkpoints' native HF names at the draft-JSON top level; training-only knobs live under dflash_config as moe_*. - router/balance/experts/shared.py: component contracts + named registries (score functions, routers, balance controllers, experts backends, shared experts). BalanceController pins the deferred-update timing that keeps activation checkpointing correct. - layer.py: MoELayer composes gate/experts/shared_experts (official attribute names); build_ffn() is the dense/MoE switch — dense drafts get the kernel provider's MLP verbatim. - hooks.py: apply_pending_balance_updates / collect_moe_aux_loss / collect_moe_metrics over any module tree. - state_dict.py: to/from_checkpoint_state_dict boundary (module layout vs official file naming), applied in the FSDP backend, warm start, materialize_draft and both exporters. FSDP full-state-dict hooks need the module's own FQNs, so the rename cannot live in state_dict(). - init.py: WarmStartPlan / select_target_experts. - dflash.py: decoder layers build their FFN through build_ffn; _init_weights reaches MoE bare Parameters; the model forward applies pending balance updates in training. DFlash/DSpark strategies add moe/* load metrics. - DESIGN.md + customization docs; tests pin the contracts with stub components. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RoutingResult carries the differentiable pre-selection scores (optional), and BalanceController.observe() receives the whole result instead of bare counts, so a policy can build an auxiliary balance loss for the same forward. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ation arm
Fills the MoE skeleton with the DeepSeek-V4 recipe and ships the MoE arm of the
dense-vs-MoE DSpark ablation for DeepSeek-V4-Flash.
- topk_router.py: "topk" router; softmax / sigmoid / sqrtsoftplus score
functions; optional group-limited selection (DeepSeek top-2 group scores).
Routing math in fp32; combine weights renormalized then scaled.
- noaux_tc.py: aux-loss-free balancing — fp32 selection bias (kept fp32 through
module dtype casts) moved by a sign controller on all-reduced expert loads,
applied from the model forward outside checkpoint regions. Stored in
checkpoints as gate.bias (the DeepSeek native key SGLang maps onto
e_score_correction_bias) via a state-dict converter.
- grouped_experts.py: experts as stacked [E, out, in] w1/w2/w3 (FSDP tracks 3
tensors, grouped GEMMs read them directly); sorted-segment dispatch with a
torch._grouped_mm path (no host sync) or the portable per-expert loop
(dflash_config.moe_dispatch). Converter keeps files in the official
experts.{i}.w{1,2,3}.weight naming. Experts and gate init with the draft's
initializer_range, like the dense MLP they replace.
- swiglu_shared.py: one ungated SwiGLU shared expert with the V4 clamp.
- presets.py: "deepseek_v4" = sqrtsoftplus + noaux_tc + renorm x1.5 + one
shared expert + swiglu_limit 10.
- init.py: apply_warm_start() seeds a layer from a target layer's dequantized
tensors (selected experts, gate rows/bias, shared expert) through the
checkpoint-naming boundary. Reading/dequantizing DSV4-Flash's fp4 experts is
left to the target tooling.
- configs/deepseek-v4-flash-dspark-moe.json: the dense DSpark config plus
moe_preset deepseek_v4, 64 routed + 1 shared experts, top-6, width 2048
(activated FFN width ~= the dense 12288). The disaggregated recipe mirrors
the dense one exactly except the draft JSON and run/store names.
- Tests: recipe/config resolution, router weights and group-limited routing,
deferred bias updates and fp32 buffer, dense per-token reference forward,
grouped-GEMM vs loop parity (CUDA), official-naming round trips at layer and
model level, warm start, DFlash training/balancing integration.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- MoEConfig.serving_fields(): the resolved recipe in the DeepSeek HF config vocabulary (scoring_func, topk_method=noaux_tc, routed_scaling_factor, n_group/topk_group, norm_topk_prob, swiglu_limit when enabled, ...). export --to hf writes them into config.json so a serving engine needs no knowledge of SpecForge presets. - AutoDraftModel.from_pretrained: HF assigns tensors by key and cannot regroup per-expert files into the stacked module layout (and refuses an explicit state_dict next to a path), so for MoE configs read the safetensors, convert through from_checkpoint_state_dict, and load into a freshly built module. Warm starts from HF export dirs go through this path. - Tests: serving fields; an end-to-end export -> config.json -> from_pretrained round trip on a tiny MoE DFlash draft. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dflash_config.moe_aux_loss_coeff > 0 adds DeepSeek-V3's complementary sequence-wise balance loss (coeff * sum_e f_e P_e over the micro-batch) built from the router's differentiable scores; DFlash/DSpark strategies add it to the training loss and it appears as moe/aux_loss. Why: the bias controller only reorders selection. A from-scratch DFlash-family drafter feeds the router near-identical mask-token embeddings early on, so every token routed to the same experts (load_max_ratio pinned at E/k, ~85% of experts unused through step 100) while the gate logits grew faster than the bias could follow. The differentiable term bounds the logit gaps so input-driven routing can emerge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- moe/deepseek_v4_target.py: dequantize one target layer's ffn.* tensors (packed fp4 e2m1 experts with per-32 ue8m0 scales, fp8 e4m3 shared expert with 128x128-block scales, bf16 gate, fp32 noaux bias) into the official naming apply_warm_start() consumes; hash-routed layers are rejected. - scripts/warm_start_moe_drafter.py: build the draft from its config, seed each MoE layer from a chosen target layer (identity expert mapping when the shapes match, strided subset otherwise), record provenance and the serving fields, and write an HF dir usable as model.draft_checkpoint_path. - Tests for the fp4/fp8 conventions and the layer dequant/naming. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- AutoDraftModel.from_pretrained (MoE path): honor output_loading_info, which the warm-start loader passes; read safetensors memory-mapped and drop the regrouped state once loaded. - warm start: for drafts with a native module layout, read and regroup the files directly instead of materializing a second full model per rank, and let ranks take turns loading. Eight ranks each building a 65 GB CPU model and then loading another 65 GB state + model exceeded the node's RAM and got the trainer OOM-killed on the 256-expert warm-started run. - Test: warm_start_draft_model round-trips a tiny MoE export. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…FSDP dflash_config.moe_freeze_experts keeps the routed experts fixed (router, shared expert and the rest of the draft still train). RoutedExperts opts into FSDP replication when fully frozen, and the backend's frozen-module scan now honors that opt-in next to lm_head/embed_tokens. Why: with DeepSeek-V4-Flash's 256x2048 experts (32.7B params) sharded, every micro-batch re-gathered ~65 GB of weights and reduce-scattered their grads (~3 TB/step, 46 s/step on 8 B200s), and at the drafter's LR (6e-4) AdamW would have overwritten the warm-started experts within ~100 steps anyway. Frozen + replicated: no expert communication, dense-like step time, and the pretrained experts are what gets served. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…name and equivalence gate SGLang's stock Qwen3DSparkModel has a dense MLP: loading a DSpark export whose FFN is a DeepSeek-style MoE (moe_preset drafts, sgl-project#812) silently drops every expert weight, the server starts, and the drafter serves random MLPs (acceptance length ~1.0). Nothing on the serving side could load the MoE arm. - patches/sglang/v0.5.18/dspark-moe-draft.patch: adds sglang/srt/models/dspark_moe.py with Qwen3MoEDSparkModel (the DSpark decoder with each dense MLP replaced by the DeepSeek-V4 routing recipe: sqrtsoftplus scores, aux-loss-free selection bias, renormalized combine weights x1.5, one ungated shared expert, SwiGLU clamp; experts loaded as stacked grouped-GEMM weights), and makes the DFlash/DSpark loaders reject checkpoints whose weights do not map onto the class or leave backbone / markov-head parameters unloaded, instead of serving uninitialised modules. - scripts/gates/normalize_dflash_export.py: write architectures: ["Qwen3MoEDSparkModel"] when the export has n_routed_experts > 0; refuse MoE DFlash/DFlash2 exports, which have no MoE-capable serving class. - scripts/gates/check_dspark_moe_sglang_equivalence.py: checks SGLang's DraftMoEFFN against SpecForge's MoELayer through the checkpoint naming boundary (small config and the real 64-expert/top-6/2048 sizes, grouped_mm and loop dispatch, routing identical, outputs bit-exact). - tests: normalizer cases for the MoE architecture name and the rejection. - docs: serving paragraph in the DeepSeek-V4-Flash DSpark recipe. The patch is the interim home; the class and the strict loader checks belong in SGLang upstream, after which the patch goes away and only the normalizer change stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Serving-side companion to Kan's MoE drafter PRs (#811 skeleton + #812 DeepSeek-V4 MoE FFN, @sherlockwu). Kan's PRs add the MoE FFN on the training side and ship the DeepSeek-V4-Flash dense-vs-MoE DSpark ablation arm, but SGLang has no draft class that can load the MoE export, so the MoE arm could not be served or benchmarked. This PR adds the missing loader (as a patch, pending SGLang upstream), the export normalizer wiring, and an equivalence gate against Kan's
MoELayer, so the #812 arm can be measured end to end. The serving numbers below are the first real acceptance-length results for Kan's MoE arm.Problem
SGLang's stock
Qwen3DSparkModelhas a dense MLP. Loading a DSpark export whose FFN is a DeepSeek-style MoE (moe_preset: deepseek_v4, the MoE arm of Kan's #812) silently drops every expert, gate and shared-expert weight: the server starts, CUDA graphs capture, but the drafter is random (acceptance length 1.006 on gsm8k after a full two-epoch run). Nothing on the serving side could load the MoE arm, so it could only be measured training-side.What this adds
patches/sglang/v0.5.18/dspark-moe-draft.patch(same convention asspec-capture.patch; apply withgit applyon top of it):sglang/srt/models/dspark_moe.pywithQwen3MoEDSparkModel:DSparkDraftModelwith each layer's dense MLP replaced byDraftMoEFFN(gate + stacked routed experts + ungated shared expert). The routing mirrors Kan'sspecforge/modeling/draft/moe(DeepSeek-V4 MoE FFN for DFlash-family drafters + DSV4-Flash ablation arm #812) 1:1 in fp32: sqrtsoftplus scores, aux-loss-free selection bias (gate.bias), renormalized combine weights ×routed_scaling_factor, SwiGLU clamp, optional group-limited selection. Experts are loaded from the official per-expert naming into stacked[E, out, in]weights and run throughtorch._grouped_mm(loop fallback). Draft is replicated (TP1 draft worker), no EP.dflash.py/dspark.py: checkpoint weights that map to no parameter, or backbone / markov-head parameters the checkpoint leaves untouched, now raiseValueErrorinstead of serving uninitialised. Mislabelling the MoE export as the dense class fails at load (985 checkpoint weight(s) do not map to any parameter).scripts/gates/normalize_dflash_export.py: writesarchitectures: ["Qwen3MoEDSparkModel"]when the export hasn_routed_experts > 0; refuses MoE DFlash/DFlash2 exports (no MoE-capable serving class exists for them yet). Two tests added intests/test_scripts/test_gate_orchestration.py.scripts/gates/check_dspark_moe_sglang_equivalence.py: builds SpecForge'sMoELayer, converts it throughto_checkpoint_state_dict, loads it intoDraftMoEFFNvia the serving stacking logic, and compares routing (identical top-k and weights) and outputs for the small config and the real 64-expert / top-6 / 2048 sizes on both dispatch paths; also checks that a truncated expert set is rejected.docs/recipes/deepseek-v4-flash-dspark-disaggregated.md.Validation
Equivalence gate: PASS on B200 (bit-exact outputs, identical routing) for small and DSV4-preset sizes, grouped_mm and loop.
End-to-end with Kan's DeepSeek-V4 MoE FFN for DFlash-family drafters + DSV4-Flash ablation arm #812 DSV4-Flash MoE arm (SGLang v0.5.18 + spec-capture + this patch,
--speculative-algorithm DSPARK, greedy, 512 tokens): strict load lists 64 experts / top-6 / shared / sqrtsoftplus / bias / ×1.5 / clamp 10 with nothing skipped; CUDA graph capture bs 1–8 fine; generation correct.Acceptance grows with training in step with the dense arm (previously stuck at 1.006 with the dense class), and matches the training-side per-position accuracy estimate within ~0.08.
Local:
python -m pytest tests/test_scripts/test_gate_orchestration.py tests/test_runtime/test_package_architecture.py→ 30 passed;black/isortclean on the changed Python files.Follow-ups
Qwen3MoEDSparkModeland the strict loader checks to SGLang upstream (the patch targets v0.5.18; upstreamdspark.py/dflash.pyhave moved, so it needs a rebase), then drop the patch here and keep only the normalizer change.specforge benchmarkcannot render DeepSeek-V4 prompts (no Jinja template); the numbers above used a script around the checkpoint's Python encoder.🤖 Generated with Claude Code