Skip to content

exp.[rebase] MoE FFN for DFlash-family drafters (#811/#812) on current main, to run the DeepSeek-V4-Flash MoE arm - #931

Draft
yushengsu-thu wants to merge 8 commits into
sgl-project:mainfrom
yushengsu-thu:moe-stack-rebased
Draft

yushengsu-thu wants to merge 8 commits into
sgl-project:mainfrom
yushengsu-thu:moe-stack-rebased

Conversation

@yushengsu-thu

Copy link
Copy Markdown

Draft. This is @sherlockwu's MoE FFN stack (#811 + #812, branch kan/moe-2-dsv4) rebased onto current main, opened so the DeepSeek-V4-Flash DSpark MoE drafter arm can be trained on today's main while those PRs wait for review. All eight commits keep their original author; nothing in the MoE package was changed. It is not meant to supersede #811/#812: close this once they land or Kan rebases them himself.

What the rebase touched

  • kan/moe-2-dsv4 was 151 commits behind main. git rebase origin/main replayed the eight commits cleanly except one hunk in docs/recipes/deepseek-v4-flash-dspark-disaggregated.md, where main had added the "On AMD MI355X" section at the same place the stack adds "MoE-FFN arm (ablation)". Both sections are kept, AMD first.
  • main had already moved docs/advanced_features/customization.md to docs/sections/advanced_features/; git followed the rename, so the stack's MoE section landed in the new location.

Checks

CPU, torch 2.13.0 / transformers 5.12.1, on the rebased tree:

python -m unittest 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 158 tests — OK (skipped=5: CUDA-only)

The lint job will inherit the pre-existing autoflake findings on the stack's own files that @fg11991 noted on #883; they are not introduced by the rebase.

Plan for this draft

Run examples/configs/online/disaggregated/external/deepseek-v4-flash-dspark-moe-disaggregated.yaml (two TP2 capture servers + DP4 trainer on one 8x B200 node, per the runbook) on this branch and attach the run notes here: whether the recipe starts and trains on current main, moe/* load metrics, step time, and any fixes the rebase needs.

🤖 Generated with Claude Code

sherlockwu and others added 8 commits October 5, 2026 12:50
… 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>
@yushengsu-thu

Copy link
Copy Markdown
Author

Smoke run of the DeepSeek-V4-Flash DSpark MoE arm on this branch (GB300, 50 optimizer steps)

Setup. One 4x GB300 node (284 GB each, aarch64, lmsysorg/sglang:v0.5.18-cu130 arm64 image: torch 2.13.0+cu130, sglang 0.5.18, transformers 5.12.1, flashinfer 0.6.17, mooncake-transfer-engine-cuda13 0.3.12.post1, spec-capture patch applied). The 8-GPU recipe layout was folded onto 4 GPUs: two TP1 capture servers (GPU 0/1, the 155 GiB FP8/FP4 checkpoint fits one GB300; --moe-runner-backend flashinfer_mxfp4 works on sm_103 after the usual DeepGEMM warmup + trtllm FP4 MoE autotune) and a DP2 trainer (GPU 2/3) with training.accumulation_steps=64, so the global batch stays 128 and the recipe's math is unchanged. Data: prepare_data.py --dataset sharegpt --sample-size 8000 (7997 prompts after filtering), 2 prompt epochs, max_steps=50, no wandb. Everything else is the checked-in yaml.

Three 50-step runs with identical overrides, capture servers kept up and only store_id changed:

run step time (steps ≥5) ce@1 ce@3 ce@10 ce@25 ce@50 loss@50 acc@50 walk_accepted_length@50 l1@50 experts unused @10 → @50 grad_norm max
deepseek-v4-flash-dspark-moe (as checked in) 8.9 s 58.5 494 37.3 23.2 14.9 3.24 0.028 1.035 1.96 0.86 → 0.74 108
deepseek-v4-flash-dspark (dense control) 6.6 s 60.3 232 38.8 23.6 15.8 3.38 0.020 1.021 1.98 – 149
MoE + dflash_config.moe_aux_loss_coeff: 0.01 8.9 s 57.8 499 29.2 26.4 18.3 3.65 0.008 1.012 1.98 0.71 → 0.57 107

What works.

  • The rebased stack trains end to end on current main: MoE draft builds, FSDP (SHARD_GRAD_OP) over the stacked [E, out, in] experts, grouped GEMM dispatch on CUDA, deferred noaux_tc bias updates, moe/* metrics on the scalar channel, and the step-50 checkpoint is written in the official per-expert naming (960 layers.{l}.mlp.experts.{i}.w{1,2,3}.weight keys + 5 gate.bias) through the FSDP backend's state-dict boundary.
  • GPU unit tests on the box: test_moe_deepseek_v4 (incl. test_grouped_mm_matches_sorted_loop on CUDA), test_dflash2, test_dflash_offline_launch — 55 tests, all pass on aarch64/GB300.
  • Cost at this layout: MoE arm 8.9 s/step vs dense 6.6 s/step (≈1.35x) and about 140–154 GB per trainer rank; the MoE training_state.pt is 112 GB vs 15 GB dense.

What to watch.

  • Early-step instability (ce spike to 200–500 at step 2–3, grad norm 100–150 before clipping at 1.0) shows up in both arms, so it is the recipe/data at LR 6e-4 from scratch, not the MoE path. Both arms are at the same place after 50 steps (ce ≈ 15, acc ≈ 2–3%, walk_accepted_length ≈ 1.0, i.e. no draft contribution yet) — 50 steps is far too short to say anything about accept length; the +5% result in DeepSeek-V4 MoE FFN for DFlash-family drafters + DSV4-Flash ablation arm #812 was at ~1.9k steps.
  • Routing collapse from scratch, exactly as the noaux_tc.py docstring warns: by step 2, 74% of the 64 experts receive no tokens and moe/load_max_ratio sits at the 64/6 = 10.67 ceiling; the selection bias grows linearly (0.064/step at this accumulation) but had only recovered to 74% unused by step 50. moe_aux_loss_coeff=0.01 helps (57% unused at step 50, trend still improving) but the aux term is ~0.013 against a loss of ~4, so 0.01 is probably too small for a from-scratch drafter; the checked-in ablation JSON leaves it at 0. Worth stating in the runbook which balancing setting the validated run used, or defaulting the MoE arm to a non-zero aux coefficient.

Logs: train_run1.log (MoE), train_dense.log, train_moe-aux.log on the devbox; summary table produced by a small log parser. Happy to run the full 2-epoch recipe on this layout if useful (≈1.9k steps × 9 s ≈ 4.7 h).

🤖 Generated with Claude Code

@yushengsu-thu yushengsu-thu changed the title [rebase] MoE FFN for DFlash-family drafters (#811/#812) on current main, to run the DeepSeek-V4-Flash MoE arm exp.[rebase] MoE FFN for DFlash-family drafters (#811/#812) on current main, to run the DeepSeek-V4-Flash MoE arm Oct 5, 2026

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