Repository navigation
exp - feat(moe): qwen3 preset and the Qwen3.8-27B DFlash2 MoE ablation arm (stacked on #811/#812) - #930
Draft
yushengsu-thu wants to merge 10 commits into
Draft
exp - feat(moe): qwen3 preset and the Qwen3.8-27B DFlash2 MoE ablation arm (stacked on #811/#812)#930yushengsu-thu wants to merge 10 commits into
yushengsu-thu wants to merge 10 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>
…8-27b-dflash2-moe # Conflicts: # docs/recipes/deepseek-v4-flash-dspark-disaggregated.md
Adds the Qwen3-MoE routing recipe to the MoE FFN package (softmax scores, renormalized top-k, no shared expert, plain SwiGLU; aux-loss-free noaux_tc balancing like deepseek_v4) and accepts Qwen's `num_experts` spelling of the routed-expert count alongside DeepSeek's `n_routed_experts`. Ships the MoE arm of a dense-vs-MoE DFlash2 ablation for Qwen3.8-27B: configs/qwen3.8-27b-dflash2-moe.json is the dense draft plus the MoE keys (16 experts x 4352, top-4: activated width == the dense 17408 at 4x the FFN parameters, ~5.9B total) and the managed-local 4-server/DP4 recipe differs from the dense arm only in the draft JSON and run names. Trains on the existing FSDP backend; no expert parallelism. Tests pin the preset, the num_experts alias (agreement, conflict, dense), the config/recipe A/B contracts, softmax routing against a per-token reference, official checkpoint naming without shared-expert keys, and DFlash2 integration (training plus deferred balance updates, conv projections still zero-initialized, strict reload, dense config unaffected). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
|
Closing for now: the MoE-drafter training we need runs on the existing #811/#812 stack (DeepSeek-V4-Flash DSpark MoE arm, one 8-GPU node) without this change. The qwen3 preset and the Qwen3.8-27B MoE ablation config remain on the fork branch |
Author
|
Reopening: we are going to run the Qwen3.8-27B MoE arm on H200 with settings identical to the dense arm (same recipe, only the draft JSON differs) and attach the results here. |
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.
What this adds
A MoE arm for the Qwen3.8-27B DFlash2 drafter, trained on the existing FSDP backend (no expert parallelism), as a dense-vs-MoE ablation pair in the style of #812's DSV4-Flash arm.
qwen3MoE preset (modeling/draft/moe/presets.py): the Qwen3-MoE routing recipe —softmaxscores over all experts, top-k combine weights renormalized (norm_topk_prob), no scaling, no shared expert, plain SwiGLU. Balancing is the aux-loss-freenoaux_tcselection bias already in the package;dflash_config.moe_aux_loss_coeffadds the complementary balance loss when a from-scratch run collapses onto a few experts.num_expertsas the Qwen spelling of the expert count (moe/config.py): the package's architecture keys are "the target checkpoint's native HF names", and Qwen checkpoints saynum_expertswhere DeepSeek saysn_routed_experts. Both resolve toMoEConfig.n_routed_experts; stating both with different values is an error.configs/qwen3.8-27b-dflash2-moe.json: the checked-in dense DFlash2 draft plus the MoE keys only — 16 routed experts of width 4352, top-4, so the activated FFN width (4 × 4352) equals the dense 17408 at 4× the FFN parameters (about 5.9B total vs 1.9B dense). Training knobs underdflash_config:moe_bias_update_rate: 0.001,moe_dispatch: grouped_mm.examples/.../managed-local/qwen3.8-27b-dflash2-moe-4server-dp4-disaggregated.yaml: identical to the dense 4-server/DP4 recipe except the draft JSON and run/store names, so the A/B diff is the FFN. Capture servers, flow control and Mooncake settings are shared.customization.md(preset + spelling), the Qwen3.8-27B runbook (new "MoE-FFN arm" section with the memory and balancing notes),examples/configs/README.md,moe/DESIGN.md.Why FSDP is enough here
Expert weights are stacked
[E, out, in]parameters that FSDP1 (use_orig_params) shards like any other; withSHARD_GRAD_OPthe unsharded bf16 draft is about 12 GB per rank during a step, well within the 4+4 layout's trainer budget.grouped_mmcompute scales with routed tokens, not expert count. EP (#882/#883) only becomes necessary for DeepSeek-class expert sets.Tests
tests/test_modeling/test_moe_qwen3.py(13 tests, CPU): preset recipe;num_experts/n_routed_expertsagreement, conflict and dense cases; checked-in config = dense config + MoE keys and the iso-activated-width invariant; the recipe differs from the dense arm only indraft_model_config,cache_dir,run_id,output_dir,control_dir,consumer_state_dir; softmax top-k weights renormalize to one and match a per-token dense reference; bf16 forward keeps the fp32 balance bias and the bias moves selection only; deferred balance update; official checkpoint naming round-trips with no shared-expert keys;serving_fields(); DFlash2 integration (two MoE layers train, DFlash2's zero-initialized conv projections survive MoE init, bias update applied on the next forward, strict reload through the naming boundary, dense DFlash2 config still buildsQwen3MLP).Run locally on CPU with torch 2.13.0 / transformers 5.12.1:
autoflake,isortandblack(pinned versions from.pre-commit-config.yaml) are clean on the changed Python files.Not yet run: a GPU smoke of the recipe itself (the trainer's
moe/*load metrics, step time and acceptance against the dense arm). That is the next step before this leaves draft.Out of scope / follow-ups
dflash.py) builds a denseDFlashMLP, so the MoE arm is measured training-side (moe/*metrics,spec_generate) until a MoE-aware DFlash draft class exists on the serving side (SGLANG_EXTERNAL_MODEL_PACKAGEcan host it without patching SGLang).🤖 Generated with Claude Code