Skip to content

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
sgl-project:mainfrom
yushengsu-thu:feat/qwen3.8-27b-dflash2-moe
Draft

yushengsu-thu wants to merge 10 commits into
sgl-project:mainfrom
yushengsu-thu:feat/qwen3.8-27b-dflash2-moe

Conversation

@yushengsu-thu

Copy link
Copy Markdown

Draft, stacked on #811 / #812. This branch is main + a merge of kan/moe-2-dsv4 (the MoE FFN skeleton and the DeepSeek-V4 preset; eight commits, one docs conflict resolved by keeping both sections) + one commit of its own (1e1c335d). The MoE package itself is unchanged from #812. Once #811/#812 land, this PR shrinks to the last commit; until then review that commit (git diff 6e3f0842..1e1c335d).

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.

  • qwen3 MoE preset (modeling/draft/moe/presets.py): the Qwen3-MoE routing recipe — softmax scores over all experts, top-k combine weights renormalized (norm_topk_prob), no scaling, no shared expert, plain SwiGLU. Balancing is the aux-loss-free noaux_tc selection bias already in the package; dflash_config.moe_aux_loss_coeff adds the complementary balance loss when a from-scratch run collapses onto a few experts.
  • num_experts as 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 say num_experts where DeepSeek says n_routed_experts. Both resolve to MoEConfig.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 under dflash_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.
  • Docs: 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; with SHARD_GRAD_OP the unsharded bf16 draft is about 12 GB per rank during a step, well within the 4+4 layout's trainer budget. grouped_mm compute 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_experts agreement, 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 in draft_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 builds Qwen3MLP).

Run locally on CPU with torch 2.13.0 / transformers 5.12.1:

python -m unittest tests.test_modeling.test_moe_qwen3 tests.test_modeling.test_moe_deepseek_v4 \
  tests.test_modeling.test_dflash2 tests.test_modeling.test_draft_registry \
  tests.test_config.test_example_draft_config_wiring tests.test_config.test_recipe_readme \
  tests.test_config.test_schema tests.test_runtime.test_package_architecture \
  tests.test_runtime.test_export tests.test_runtime.test_model_loading
# Ran 171 tests — OK (skipped=5: CUDA-only)

autoflake, isort and black (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

🤖 Generated with Claude Code

sherlockwu and others added 10 commits September 2, 2026 23:19
… 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>
@yushengsu-thu yushengsu-thu changed the title feat(moe): qwen3 preset and the Qwen3.8-27B DFlash2 MoE ablation arm (stacked on #811/#812) exp - feat(moe): qwen3 preset and the Qwen3.8-27B DFlash2 MoE ablation arm (stacked on #811/#812) Oct 5, 2026
@yushengsu-thu

Copy link
Copy Markdown
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 yushengsu-thu/SpecForge:feat/qwen3.8-27b-dflash2-moe in case a Qwen-family MoE drafter is picked up later.

@yushengsu-thu

Copy link
Copy Markdown
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

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