Repository navigation
MoE FFN support for DFlash-family drafts - #1003
yushengsu-thu wants to merge 34 commits into
Conversation
configs/qwen-8b-moe.json is qwen3-8b-dspark.json plus the Qwen3-MoE config fields (num_experts, num_experts_per_tok, moe_intermediate_size, norm_topk_prob, router_aux_loss_coef, decoder_sparse_step, mlp_only_layers). Nothing reads them yet, so the draft still builds the dense FFN. The managed-local recipe trains it online on one 8-GPU node: one capture server on GPU 0 and seven trainer ranks.
Qwen3DFlashDecoderLayer now builds its FFN with build_ffn(config, dense=kernels.make_mlp) from the new specforge.modeling.draft.moe package. A draft config without a moe_preset gets dense(config), the same native or Liger MLP as before, so every existing draft builds unchanged. A config that names a moe_preset raises NotImplementedError until the MoE layer lands.
e5ec8db to
23b5dc0
Compare
… recipe Mirror exp/qwen3-8b-dspark-moe-8expert-1activate-dense-same: 2 layers, GQA 8/2, block 16, no confidence head, is_causal, 8 experts x 1536 top-1 without renorm, no shared expert, aux 0.001, and its 12-epoch recipe with Liger. Main has no ddp_bucket_cap_mb, flatten/cache_vocab_projection or consumer_dispatch, so those stay out; router centering is not part of this branch.
|
training and save ckpt commits config until here |
planning.py rejects model.use_liger_kernel=true for dspark: on main only the dflash algorithm wires Liger kernels into its draft. The exp branch could set it because it merged sgl-project#963. The first online smoke of this recipe failed on it.
TopKRouter keeps a Switch-style load-balance loss (the MoE aux loss) on gate.aux_loss after every training forward: E * sum_e load_e * mean_prob_e over the kept tokens, K when balanced. Scores are L1-normalized per token so sigmoid and sqrtsoftplus routers share the formula. The qwen3_5_moe family passes router_aux_loss_coef through; nothing consumes the loss yet.
MoELayer.token_mask marks this batch's kept slots; the training forward passes it to the router so the load-balance loss skips padding blocks. _forward_draft_blocks sets it for DFlash, Domino and DSpark from block_keep_mask. MoE drafts refuse activation checkpointing: a recompute would not see this per-batch state.
take_moe_aux_loss pops the routers' per-forward loss as (coefficient-weighted mean, raw mean) and clears the per-batch state; the weighting then follows each objective's own normalization. OnlineDFlashModel folds the weighted mean where each objective is built: the DFlash numerator the trainer normalizes globally, Domino's loss and loss_terms, DSpark's plain loss; the raw mean is reported as moe/aux_loss. The router clears aux_loss at every forward so no training value survives into eval. Dense drafts are untouched.
…step TopKRouter keeps a persistent fp32 selection_bias that shifts the top-k picks and a per-step tokens_per_expert count. The counts accumulate in forward; at the step boundary the trainer calls the strategy's on_optimizer_step, the DFlash-family model forwards it to step_moe_balancing, which pools every router's counts in one all-reduce and applies the zero-mean sign rule (rate x sign(mean - count)). Enabled per draft config with dflash_config.moe_router_selection_bias; the rate is moe_router_bias_update_rate (0.001 per optimizer step, as in DeepSeek-V3).
moe_router_selection_bias true with the DeepSeek-V3 rate of 0.001 per optimizer step, next to the 0.001 load-balance loss.
|
balancing related features done and use step-level (instead of micro-step) loss and mechanism |
…token Same draft as qwen3-8b-moe16x768-16A1-dspark.json with the 12288-wide FFN split eight ways instead of sixteen (same total expert parameters, twice the active width per token), router balancing at the sgl-project#1003 defaults (aux loss 0.005, selection-bias step 0.001).
…token Same draft as qwen3-8b-moe16x768-16A1-dspark.json with the 12288-wide FFN split eight ways instead of sixteen (same total expert parameters, twice the active width per token), router balancing at the sgl-project#1003 defaults (aux loss 0.005, selection-bias step 0.001).
maocheng23
left a comment
There was a problem hiding this comment.
I'd recommend splitting this PR into two: one for the MoE module itself and one for load balancing (the split you offered in the description works for me).
The module part should be self-contained and easy to review. The balancing part reaches into several stages of training: the router forward, the loss assembly of each objective, the strategy, and the trainer step. That makes it feel a bit invasive and hard to follow. With a separate PR we could land the module quickly and give the balancing design a focused review and cleanup.
For the balancing PR, it may be worth following torchtitan's structure:
- inject the aux-loss gradient from inside the router, so objectives don't need to fold it into the loss;
- pass the token mask as an explicit forward argument instead of setting it on the router;
- keep the step-boundary work (count all-reduce, bias update, metrics) in a single hook that the trainer registers once.
| # Qwen3.8's config omits it; transformers always renormalizes. | ||
| norm_topk_prob=getattr(config, "norm_topk_prob", True), | ||
| initializer_range=std, | ||
| # torchtitan's units: 1 when balanced, one coefficient per layer. |
There was a problem hiding this comment.
We are supporting torch titan?
There was a problem hiding this comment.
No torchtitan dependency; the comment only cites the torchtitan loss units (1 when balanced) becasue we refer it
| from specforge.modeling.draft.moe.balance import ( | ||
| set_moe_token_mask, | ||
| step_moe_balancing, | ||
| take_moe_aux_loss, |
There was a problem hiding this comment.
why moe only in dflash?
This should be a common part for other draft models, e.g. eagle?
Here I think the coupling part is the baclancing?
There was a problem hiding this comment.
The coupling was the balancing; #1031 moves it to a trainer-registered hook. EAGLE3 wiring is one line, follow-up.
|
@yushengsu-thu There are three concerns that I think need to be handled.
|
The loss is 1 when balanced and the coefficient applies per layer; the torchtitan reference read as a dependency.
The MoE layer, experts, top-k router, the qwen3_5_moe family and the build_ffn switch from sgl-project#1003, without the load balancing: no selection bias, no load-balance loss, no per-step hook. Those follow as stage 2 and stage 3 of this PR.
…ient 0 MoELayer takes aux_loss_coef and aux_loss_type (microbatch_wise, or the default batch_wise over the step's counts pooled across ranks) and, in training with a non-zero coefficient, leaves this forward's loss in aux_loss for the balancing hook to fold before backward. At coefficient 0 nothing is computed: no graph and no per-micro-batch collective. The loss formula and units are sgl-project#1003's, moved from the router to the layer.
…the step hook MoEBalancing.before_backward collects each MoE layer's loss times its coefficient, adds the sum to the loss and, when the strategy reports loss_terms, to the numerator scaled by the denominator, and logs the raw mean as moe/aux_loss; TrainerCore runs the registered hooks before every backward. The arithmetic is sgl-project#1003's if_add_moe_aux_loss, done once here instead of at the end of each objective.
…_wise 0.005 in the DSpark recipe dflash_config.moe_router_aux_loss_coef and moe_router_aux_loss_type (batch_wise by default, microbatch_wise the alternative) reach MoELayer from the family builder; the default coefficient 0 keeps drafts without the keys loss-free. The Qwen3-8B MoE DSpark recipe trains with batch_wise 0.005 next to the expert bias, as sgl-project#1003 did.
|
@maocheng23 Done in #1031, one concern per commit:
|
|
@jiapingW Done in #1031, one concern per commit:
|
…draft starts a fresh window tokens_per_expert is not saved, and from_pretrained builds the model on the meta device, so without this the counts of a warm-started draft (model.draft_checkpoint_path) held uninitialized memory until the first optimizer step reset them. sgl-project#1003 zeroed them in the router's reset_parameters; the layer does the same now. The from_pretrained test also checks that a trained expert_bias round-trips.
|
@jiapingW @maocheng23
|
Rebased from sgl-project#1003 onto sgl-project#1031 (dev-moe-ffn): the served router reads dflash_config.moe_router_enable_expert_bias and loads gate.expert_bias, the names sgl-project#1031 gives the DeepSeek-style bias in its checkpoints. Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
select now receives the shifted scores (choice) and only picks the top-k, so a family that overrides select, such as DeepSeek-V3's group-limited routing, needs no knowledge of the bias. The weights still come from the unbiased scores. No behavior change: the bitwise probe against sgl-project#1003 stays identical.
select now receives the shifted scores (choice) and only picks the top-k, so a family that overrides select, such as DeepSeek-V3's group-limited routing, needs no knowledge of the bias. The weights still come from the unbiased scores. No behavior change: the bitwise probe against sgl-project#1003 stays identical.
…t#1003 code, 5-epoch cosine) Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…p script and py-spy dump Training finished 2026-11-02 12:35 UTC (53,820 steps, 2.45 s/step); epochs 1-5 on HF yushengsu/dense-1-8-only-5-epoch. Train acc at the last step of each epoch 0.6032 / 0.6441 / 0.6582 / 0.6714 / 0.6624 (1,000-step means 0.6117 / 0.6420 / 0.6564 / 0.6610 / 0.6696). The managed producer hung in streaming_ref_channel.poll at shutdown again (PR sgl-project#1003 code). Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>



Summary
An MoE FFN for DFlash-family drafts (DFlash, Domino, DSpark), starting with the Qwen3.5-MoE family (Qwen3-Next / Qwen3.5 / Qwen3.8). A draft config turns it on by naming a
moe_preset; every other draft keeps its dense MLP. Load balancing follows torchtitan: a selection bias moved once per optimizer step, plus a small load-balance loss.I re-write +factor the PRs (#811, #812, #800) to make it more maintainable/readable and easier add new MoE layer to draft models in the future
Layout
lines of code:
production code: +557 / −1
doc and config: +155 / −0
ci test: +861 / −5
Wiring:
Note: the moe code 90% is quick simple and intuitive. However, to train moe and and balance experts, we must introduce
selection_bias(dsk's) andaxu_loss(Qwen's) mechanism inbalance.pyto change the router score. Those parts are not that intuitive. If you feel confused about that part. Please kindly let me know and I can divide the PRs into:axu_losselection_biasfor you
Design notes
layer.py: the MoE layer.MoELayerputs the router, the routed experts and the optional shared expert together;build_ffngives each draft layer a dense MLP or an MoE layer.experts.py: the experts.RoutedExpertscomputes the outputs of the experts each token was routed to;SharedExpertis an always-on expert added on top.router.py: the router.TopKRouterpicks each token's top-k experts and keeps the selection bias and the step's expert counts.balance.py: load balancing. Provides the balance loss, the balance metrics, and the hooks the model calls to add the loss and update the selection bias once per optimizer step.families/: model families. Each family builds the MoE layer from its model's config, selected bymoe_preset;qwen3_5_moe.pyis the Qwen3.5-MoE family and reads the balancing settingsmoe_router_aux_loss_coef,moe_router_aux_loss_type(batch_wiseby default),moe_router_selection_biasandmoe_router_bias_update_rate.Verification
Both runs share this setup:
configs/qwen3-8b-moe-dspark.json).Result: the two runs differ very little. Walk accept length is 4.27 vs 4.26 after epoch 1 and 4.64 vs 4.60 after epoch 2, slightly in favor of #1003, with cross-entropy and accuracy just as close. Both runs stay balanced, within 2% of 1 at the end of each epoch.
Out of scope
selection_biasare the stacked PR serving: SGLang draft classes for the MoE DSpark/DFlash drafts (stacked on #1003) #1016.score_fn,selection_bias,route_scale, the expertactivation), but no such family is built here.Revised version