Skip to content

MoE FFN skeleton for DFlash-family drafters (contracts, config, seams) - #811

Closed
sherlockwu wants to merge 2 commits into
mainfrom
kan/moe-1-skeleton
Closed

sherlockwu wants to merge 2 commits into
mainfrom
kan/moe-1-skeleton

Conversation

@sherlockwu

@sherlockwu sherlockwu commented Sep 2, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #798 (kan/pr2-dsv4-dspark); retarget as the stack merges. First of two: this PR lays out the MoE FFN package (modules, contracts, config, and every trainer/checkpoint seam) with no routing math; the follow-up adds the DeepSeek-V4 implementation and the DSV4-Flash ablation arm. Together they supersede #799.

Design

One configurable MoE layer with swappable components, not one block per target family (the Megatron-Core split; see specforge/modeling/draft/moe/DESIGN.md). Family differences are a handful of small functions (score function, balancing policy, renorm/scale, shared-expert gate, activation clamp); the expensive parts (dispatch, checkpoint naming, FSDP layout, deferred balance-update timing) are shared.

specforge/modeling/draft/moe/
  config.py      MoEConfig + moe_preset registry; resolve_moe_config(draft_config)
  router.py      Router contract (x -> RoutingResult) + score-function registry
  balance.py     BalanceController contract (selection shift / observe / deferred apply / aux loss / metrics)
  experts.py     RoutedExperts contract (weights + dispatch)
  shared.py      SharedExpert contract
  layer.py       MoELayer = gate + experts + shared_experts; build_ffn() is the dense/MoE switch
  hooks.py       apply_pending_balance_updates / collect_moe_aux_loss / collect_moe_metrics
  state_dict.py  to/from_checkpoint_state_dict: module layout <-> official file naming
  init.py        WarmStartPlan / select_target_experts

Config contract. Architecture keys use the target checkpoints' native HF names at the draft-JSON top level (n_routed_experts, num_experts_per_tok, moe_intermediate_size, scoring_func, routed_scaling_factor, ...) so a draft JSON can copy them from the target's config.json; moe_preset supplies a family's defaults and any explicit key overrides it for ablations. Training-only knobs live under dflash_config as moe_* and never touch the checkpoint.

Seams wired here (so the implementation PR is components only):

  • Qwen3DFlashDecoderLayer builds its FFN through build_ffn(config, dense=kernels.make_mlp): dense drafts get the kernel provider's MLP verbatim (DFlash, DFlash2 and DSpark all inherit).
  • DFlashDraftModel._init_weights reaches MoE bare Parameters; the model forward applies pending balance updates in training, outside activation-checkpoint regions.
  • DFlash/DSpark strategies add moe/* load metrics to StepOutput.metrics (DP-averaged and logged like any scalar).
  • Checkpoint-naming boundary applied in FSDPTrainingBackend save/load, warm start, materialize_draft, and both exporters. FSDP's full-state-dict hooks index by the module's own FQNs, so the rename cannot live inside state_dict().

Tests

tests/test_modeling/test_moe.py pins the contracts with stub components: config resolution/validation/presets, registries, MoELayer composition (shape, shared-expert add, train-only observe, hook delegation, selection bias affects choice not weights), hooks over module trees, the state-dict boundary, warm-start plans, and DFlash wiring (dense unchanged; MoE layers initialized; balance updates applied per training forward). Existing modeling/training/export/loading suites pass unchanged.

Not in this PR

Any preset, score function, controller, experts backend or shared expert. Aux-loss consumption in an objective (collected, not yet wired; the first preset with an aux-loss policy adds it).

sherlockwu and others added 2 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>

@yushengsu-thu yushengsu-thu left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(cc catch - human verify)
Resume path bypasses the checkpoint-naming boundary: Trainer.__init__ loads state["draft_state_dict"] (file naming) straight into model.draft_model (L347-L349) and raises on unexpected keys. Once an experts backend registers a converter, every resume_from of an MoE drafter fails. Fix: convert like _load_module_state_dict does.

diff --git a/specforge/training/trainer.py b/specforge/training/trainer.py
index d465b947..594ca5b2 100644
--- a/specforge/training/trainer.py
+++ b/specforge/training/trainer.py
@@ -344,7 +344,12 @@ class Trainer:
                         f"{key}={comparison_current}; "
                         f"resume with the original configuration"
                     )
-            saved_weights = state["draft_state_dict"]
+            # Checkpoint FILES use the official parameter naming; the module may
+            # use a native MoE layout (stacked experts). Convert at this boundary
+            # exactly like FSDPTrainingBackend._load_module_state_dict does.
+            from specforge.modeling.draft.moe import from_checkpoint_state_dict
+
+            saved_weights = from_checkpoint_state_dict(state["draft_state_dict"])
             load_result = model.draft_model.load_state_dict(saved_weights, strict=False)
             loaded = len(saved_weights) - len(load_result.unexpected_keys)
             missing_keys = set(load_result.missing_keys)

Comment on lines +55 to +62
def _moe_metrics(model_wrapper: nn.Module) -> Dict[str, Any]:
"""``moe/...`` load diagnostics of the wrapped draft; ``{}`` for dense drafts."""
from specforge.modeling.draft.moe import collect_moe_metrics

draft_model = getattr(model_wrapper, "draft_model", None)
if draft_model is None:
return {}
return collect_moe_metrics(draft_model)

@yushengsu-thu yushengsu-thu Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(cc catch - human verify)
(I know we seldom use DDP training but it's better to modify)
_moe_metrics silently returns {} on the NO_SHARD (DDP) recipe: strategies get the wrapped module (trainer.py L430-L437), and DDP, unlike FSDP, does not forward draft_model (L55-L62). Same root cause already disables the DFlash2 selector loss under DDP (_selector_loss_alpha). Fix: unwrap .module like Eagle3TrainStrategy does.

diff --git a/specforge/training/strategies/base.py b/specforge/training/strategies/base.py
index cbc253da..dcff526c 100644
--- a/specforge/training/strategies/base.py
+++ b/specforge/training/strategies/base.py
@@ -52,11 +52,24 @@ class StepContext:
     total_steps: Optional[int] = None
 
 
+def _unwrap_parallel(model: nn.Module) -> nn.Module:
+    """Reach the composite model behind a parallel wrapper.
+
+    Strategies receive ``backend.prepare_model(...)``'s return value. FSDP
+    forwards attribute lookups to the wrapped module; DDP does not, so
+    ``getattr(ddp, "draft_model")`` / ``getattr(ddp, "selector_loss_alpha")``
+    would silently miss. Same unwrap rule as ``Eagle3TrainStrategy``.
+    """
+    while not hasattr(model, "draft_model") and hasattr(model, "module"):
+        model = model.module
+    return model
+
+
 def _moe_metrics(model_wrapper: nn.Module) -> Dict[str, Any]:
     """``moe/...`` load diagnostics of the wrapped draft; ``{}`` for dense drafts."""
     from specforge.modeling.draft.moe import collect_moe_metrics
 
-    draft_model = getattr(model_wrapper, "draft_model", None)
+    draft_model = getattr(_unwrap_parallel(model_wrapper), "draft_model", None)
     if draft_model is None:
         return {}
     return collect_moe_metrics(draft_model)
@@ -478,20 +491,20 @@ class DFlashTrainStrategy(DraftTrainStrategy):
         return next(self.dflash_model.parameters()).device
 
     def _selector_loss_alpha(self, ctx: Optional[StepContext]) -> float:
-        target = float(getattr(self.dflash_model, "selector_loss_alpha", 0.0))
+        model = _unwrap_parallel(self.dflash_model)
+        target = float(getattr(model, "selector_loss_alpha", 0.0))
         if target <= 0 or ctx is None or not ctx.total_steps:
             return target
 
         total_steps = int(ctx.total_steps)
         warmup_steps = int(
-            total_steps
-            * float(getattr(self.dflash_model, "selector_warmup_ratio", 0.0))
+            total_steps * float(getattr(model, "selector_warmup_ratio", 0.0))
         )
         if ctx.global_step < warmup_steps:
             return 0.0
 
         ramp_steps = int(
-            total_steps * float(getattr(self.dflash_model, "selector_ramp_ratio", 0.0))
+            total_steps * float(getattr(model, "selector_ramp_ratio", 0.0))
         )
         if ramp_steps <= 0:
             return target

yushengsu-thu

This comment was marked as spam.

@@ -0,0 +1,101 @@
# MoE FFN Design (`specforge.modeling.draft.moe`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc really help me quickly understand the design but I not sure if it's proper to leave it or put it here.

def is_moe_config(config: Any) -> bool:
"""True when a draft config asks for an MoE FFN (``n_routed_experts > 0``)."""
value = _get(config, "n_routed_experts", 0)
return int(value or 0) > 0

@yushengsu-thu yushengsu-thu Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Values like -1 and 0.5 silently select dense layers. Could we reject invalid expert counts instead of defaulting to dense?

resolve_moe_config({"n_routed_experts": -1})   # None
resolve_moe_config({"n_routed_experts": 0.5})  # None

This was referenced Oct 17, 2026
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 18, 2026
Reworks the sgl-project#811 skeleton into one configurable MoE layer whose target
families only add files. This commit keeps sgl-project#811's scope (contracts, config,
seams) plus the components every family shares; the routing, balancing and
serving options the Qwen and DeepSeek recipes need follow in the next
commit.

- base.py: Registry, RoutingResult and the TrainingState contract (observe,
  apply_pending_update, aux_loss, metrics, reset_parameters) that the
  router, balance controller and experts share.
- config.py: MoEConfig with architecture and training keys kept apart,
  presets that supply the defaults and a checkpoint layout,
  positive-integer validation, and an error for n_routed_experts without a
  moe_preset.
- router.py: the score-function and router registries and a softmax top-k
  router; renormalized combine weights carry a 1e-20 guard against 0/0.
- balance.py: the BalanceController contract and the "none" policy.
- experts.py: stacked grouped experts (sorted_loop and grouped_mm
  dispatch) and the SwiGLU shared expert.
- layer.py: MoELayer combines routed and shared experts in fp32 and casts
  once, the official DeepSeek order; build_ffn and model-level hooks.
- checkpoint.py: checkpoint layouts converted at every file boundary
  (backend save/load, resume, warm start, from_pretrained, materialize,
  to_hf), and select_target_experts.
- families/: where target families register presets and layouts; the core
  never imports them.

Integration: DraftTrainStrategy.finalize_step_output adds the aux loss and
moe/* metrics for every strategy (TrainerCore calls it, so the optimizer
boundary host-sync test's stand-in strategy now subclasses
DraftTrainStrategy, the type TrainerCore takes), and
AutoDraftModel.from_pretrained reads MoE files through the layout; to_sglang
(EAGLE3 only) is left untouched.
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 18, 2026
…chitecture

- specforge/modeling/draft/dspark_v4.py: training-side port of the DSpark
  head bundled with DeepSeek-V4-Flash-0731 — three DeepSeek-V4 blocks
  (MLA-style low-rank attention, MoE via draft/moe.py, mHC
  hyper-connections) under the official mtp.* naming, so trained state
  dicts round-trip into the bundled layout without renaming
- training.attention_backend=native: the draft model implements its own
  block/window attention (no wrapper-built masks); dspark providers and
  assembly accept the DSparkV4DraftModel architecture; the confidence
  head reads its own feature stream via pop_confidence_hidden
- scripts: official-weight download, init-from-target (identity/average
  main_proj, --stage-layers for spread-capture drafts), export bundling
  into the official servable layout, numeric parity gate vs the
  reference implementation, step-time benchmark
- recipes: official-arch two-node H200 + runbook, 0813-traffic 32K on
  H200 and B200 2-node stacks, with launch scripts

(cherry picked from commit 67fea1a)

[Yusheng Su: cherry-picked onto the MoE framework stack (sgl-project#811 rework)
without its parent sgl-project#799, so dspark_v4.py's `from .moe import MoEGate,
SparseMoE` no longer resolves until a follow-up commit moves the FFN onto
MoELayer. examples/configs/README.md conflict: kept both paragraphs and
dropped the sentence about sgl-project#799's `...-moe.yaml` ablation recipe.]

[Yusheng Su: rebased onto main a580ec5, which gave
_dspark_objective_chunk_terms a positional block_index and a keyword-only
total_blocks; confidence_hidden now follows block_index, so
checkpointed_chunk_reduce still slices it with the other block inputs.]
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
Part 1 of 5 of the sgl-project#811 skeleton commit (ec6dfda), split for review.

- _registry.py: the named-registry helper the MoE registries share.
- config.py: MoEConfig, the moe_preset registry and 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_*.
- __init__.py: the package entry point, exporting the config API.
- tests: config resolution, preset defaults and overrides, validation.
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
Part 2 of 5 of the sgl-project#811 skeleton commit (ec6dfda), split for review.

- router.py: RoutingResult, the score-function registry and the Router
  contract (hidden states -> top-k choices + combine weights) with its
  registry.
- balance.py: the BalanceController contract (selection-only score shift,
  optional aux loss, metrics, deferred updates) and the "none" policy.
- experts.py: the RoutedExperts contract and the experts-backend registry.
- shared.py: the SharedExpert contract and its registry.
- tests: stub components for every contract, registered by the tests, and
  the registries' error messages.
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
…egies

Part 3 of 5 of the sgl-project#811 skeleton commit (ec6dfda), split for review.

- layer.py: MoELayer composes gate (router + balance controller), experts
  and shared_experts under the official attribute names; build_ffn() is the
  dense/MoE switch, so dense drafts get the kernel provider's MLP verbatim.
- hooks.py: apply_pending_balance_updates, collect_moe_aux_loss and
  collect_moe_metrics over any module tree.
- dflash.py: decoder layers build their FFN through build_ffn;
  _init_weights reaches the MoE layers' bare Parameters; the model forward
  applies pending balance updates in training. The moe import is placed in
  isort order (the original commit failed the isort hook here).
- strategies/base.py: the DFlash and DSpark strategies add moe/* load
  metrics.
- tests: layer composition, the model-level hooks and the DFlash wiring.
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
Part 4 of 5 of the sgl-project#811 skeleton commit (ec6dfda), split for review.

- state_dict.py: the to/from_checkpoint_state_dict boundary between the
  modules' native layout and the official file naming, with a registry of
  converters. FSDP's full-state-dict hooks need the module's own FQNs, so
  the rename cannot live in state_dict().
- init.py: WarmStartPlan, select_target_experts and plan_warm_start.
- The boundary is applied in the FSDP backend's save and load,
  materialize_draft, warm start, and both exporters.
- tests: the conversion boundary and warm-start planning.
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
Part 5 of 5 of the sgl-project#811 skeleton commit (ec6dfda), split for review.
Together the five parts are that commit, except for its two lint fixes:
the moe import in dflash.py is in isort order (part 3), and the tests do
not import the unused MoEConfig.

- moe/DESIGN.md: responsibility, why the draft matches the target's MoE,
  the package layout, the contracts that matter, extension points.
- customization.md: the MoE section for DFlash-family drafts.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
Reworks the sgl-project#811 skeleton into one configurable MoE layer whose target
families only add files. This commit keeps sgl-project#811's scope (contracts, config,
seams) plus the components every family shares; the routing, balancing and
serving options the Qwen and DeepSeek recipes need follow in the next
commit.

- base.py: Registry, RoutingResult and the TrainingState contract (observe,
  apply_pending_update, aux_loss, metrics, reset_parameters) that the
  router, balance controller and experts share.
- config.py: MoEConfig with architecture and training keys kept apart,
  presets that supply the defaults and a checkpoint layout,
  positive-integer validation, and an error for n_routed_experts without a
  moe_preset.
- router.py: the score-function and router registries and a softmax top-k
  router; renormalized combine weights carry a 1e-20 guard against 0/0.
- balance.py: the BalanceController contract and the "none" policy.
- experts.py: the RoutedExperts contract and its registry, with stacked
  grouped experts (sorted_loop and grouped_mm dispatch) as "grouped".
- shared.py: the SharedExpert contract and its registry, with the SwiGLU
  shared expert as "swiglu".
- layer.py: MoELayer combines routed and shared experts in fp32 and casts
  once, the official DeepSeek order; build_ffn and model-level hooks.
- checkpoint.py: checkpoint layouts converted at every file boundary
  (backend save/load, resume, warm start, from_pretrained, materialize,
  to_hf), and select_target_experts.
- families/: where target families register presets and layouts; the core
  never imports them.

Integration: DraftTrainStrategy.finalize_step_output adds the aux loss and
moe/* metrics for every strategy (TrainerCore calls it, so the optimizer
boundary host-sync test's stand-in strategy now subclasses
DraftTrainStrategy, the type TrainerCore takes), and
AutoDraftModel.from_pretrained reads MoE files through the layout; to_sglang
(EAGLE3 only) is left untouched.
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
…chitecture

- specforge/modeling/draft/dspark_v4.py: training-side port of the DSpark
  head bundled with DeepSeek-V4-Flash-0731 — three DeepSeek-V4 blocks
  (MLA-style low-rank attention, MoE via draft/moe.py, mHC
  hyper-connections) under the official mtp.* naming, so trained state
  dicts round-trip into the bundled layout without renaming
- training.attention_backend=native: the draft model implements its own
  block/window attention (no wrapper-built masks); dspark providers and
  assembly accept the DSparkV4DraftModel architecture; the confidence
  head reads its own feature stream via pop_confidence_hidden
- scripts: official-weight download, init-from-target (identity/average
  main_proj, --stage-layers for spread-capture drafts), export bundling
  into the official servable layout, numeric parity gate vs the
  reference implementation, step-time benchmark
- recipes: official-arch two-node H200 + runbook, 0813-traffic 32K on
  H200 and B200 2-node stacks, with launch scripts

(cherry picked from commit 67fea1a)

[Yusheng Su: cherry-picked onto the MoE framework stack (sgl-project#811 rework)
without its parent sgl-project#799, so dspark_v4.py's `from .moe import MoEGate,
SparseMoE` no longer resolves until a follow-up commit moves the FFN onto
MoELayer. examples/configs/README.md conflict: kept both paragraphs and
dropped the sentence about sgl-project#799's `...-moe.yaml` ablation recipe.]

[Yusheng Su: rebased onto main a580ec5, which gave
_dspark_objective_chunk_terms a positional block_index and a keyword-only
total_blocks; confidence_hidden now follows block_index, so
checkpointed_chunk_reduce still slices it with the other block inputs.]
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
sgl-project#811's routed-expert and shared-expert contracts and their registries
stay; this adds the built-in implementations, so a draft that names a
preset builds a working layer and the tests run real code.

- experts.py: GroupedExperts, registered as "grouped", holds the routed
  experts as three stacked [E, out, in] parameters with sorted-segment
  dispatch (sorted_loop, or torch._grouped_mm on CUDA) and returns a
  fresh fp32 accumulator, which the RoutedExperts contract now requires.
  Both contracts implement TrainingState.
- shared.py: SwiGLUSharedExpert, registered as "swiglu".
- router.py: TopKRouter ("topk") picks the top-k on the balance
  controller's selection scores and combines with the raw scores, all in
  fp32; a softmax score function is registered and get_score_function
  goes (SCORE_FUNCTIONS.get does the same). Routers implement
  TrainingState and receive tokens_per_sample.
- layer.py: MoELayer adds the shared expert into the fp32 routed sum,
  casts once, and passes tokens_per_sample to the router.
- tests: the router, experts and shared-expert stubs go; the layer tests
  run the built-in components, with reference checks of the routed sum,
  the grouped_mm path and the single cast, and a check that the layer
  builds the implementations the config names.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
- layer.py: MoELayer drives the router, balance controller, routed
  experts and shared expert through one TrainingState loop (components(),
  in a fixed order) for observe, apply_pending_update, aux_loss, metrics
  and reset_parameters, where it used to call the balance controller
  alone; MoELayer.apply_pending_balance_update is now apply_pending_update.
  The model-level hooks (iter_moe_layers, apply_pending_balance_updates,
  collect_moe_aux_loss, collect_moe_metrics) move here and hooks.py goes.
- balance.py: BalanceController implements TrainingState, whose no-op
  defaults and timing rules in base.py replace its own copies; the "none"
  policy is listed.
- dflash.py: the pending-update comment points at base.py.
- tests: the config, router, layer, hook and draft-model tests use
  purpose-built balance controllers instead of sgl-project#811's shared stub and
  keep sgl-project#811's wiring checks: attribute names through build_ffn, eval
  forwards not observing, pending updates reaching every layer, and the
  DFlash forward applying them in training only. The checkpoint and
  trainer tests change with their code in the next two commits.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
Checkpoint files name an MoE layer's tensors the way the target family
does, while modules keep a native layout (stacked [E, out, in] experts).
checkpoint.py replaces sgl-project#811's state_dict.py converters with layouts
registered by name: the preset picks one (moe_layout overrides it), only
that layout runs (sgl-project#811's converters all ran on every state dict), and it
maps one MoE layer at a time.

- checkpoint.py: CheckpointLayout(to_file, from_file), the LAYOUTS
  registry with the "native" layout (files keep module names), and
  to/from_checkpoint_state_dict(state, owner), where owner is the draft
  model or its config; dense drafts pass through unchanged.
  select_target_experts moves here; init.py goes with WarmStartPlan and
  plan_warm_start.
- config.py: MoEConfig.layout (draft JSON key moe_layout, preset
  default); an unknown layout fails when the config resolves.
- families/__init__.py: where target families register a preset and a
  layout; core modules never import it.
- Every file read and write converts through the draft's layout:
  FSDPTrainingBackend save/load, trainer resume, warm start,
  AutoDraftModel.from_pretrained (MoE files are read memory-mapped and
  regrouped, which HF from_pretrained cannot do), materialize_draft and
  the HF exporter. to_sglang (EAGLE3 only) goes back to main's version.
- __init__.py: the public API is the contracts, the registration
  functions, the layer and its hooks, and the checkpoint boundary; the
  registries and build functions stay in their modules.
- tests: the layout config keys, a per-expert test layout's round trip
  (with layer-prefix disambiguation), owner resolution, the native
  identity, select_target_experts, and a from_pretrained round trip
  through the layout.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
sgl-project#811 added the moe/* metrics inside the DFlash and DSpark strategies'
forward_loss and collected the auxiliary loss without adding it to the
objective.

- strategies/base.py: DraftTrainStrategy.finalize_step_output adds the
  draft's summed MoE auxiliary loss to whichever term backpropagates
  (loss, or the loss_terms numerator scaled by this micro-batch's
  denominator, the weighting the objective gets) and the layer-averaged
  moe/* metrics; drafts without MoE layers get their output back
  unchanged. _moe_metrics and its two call sites go.
- controller.py: TrainerCore calls it after forward_loss on every
  training micro-step; evaluation calls forward_loss alone, so eval
  losses exclude the auxiliary term.
- test_dflash_host_syncs.py: the optimizer-boundary test's stand-in
  strategy subclasses DraftTrainStrategy, the type TrainerCore takes.
- tests: the aux loss reaches the gradient with and without loss_terms,
  the moe/* metrics reach a strategy with no MoE code, and a dense
  step's output is untouched.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
- moe/DESIGN.md: rewritten for the framework core: the two ways a draft
  uses the layer (build_ffn in the DFlash family, MoELayer directly in a
  drafter with its own block, which stays its own class), the package
  layout, the rules (core never imports families, every choice is
  explicit, a new family only adds files), and the contracts: detection
  keys on the preset, one TrainingState loop, deferred updates, the
  official accumulation order, attribute names, naming converted at the
  file boundary, and model-level terms for every strategy. sgl-project#811's notes
  that still hold stay: the Megatron-Core split, why the drafter's MoE
  matches the target's (warm start, serving, latency), and the extension
  points, including experts backends and shared experts.
- customization.md: drafts opt in by naming a moe_preset; architecture
  and training-only keys, losses and metrics without strategy code,
  checkpoints and exports through the layout, how new families and
  components register, and how a drafter with its own architecture uses
  MoELayer.
- training/DESIGN.md: DraftTrainStrategy owns finalize_step_output, which
  TrainerCore calls after forward_loss.
yushengsu-thu pushed a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 19, 2026
…chitecture

- specforge/modeling/draft/dspark_v4.py: training-side port of the DSpark
  head bundled with DeepSeek-V4-Flash-0731 — three DeepSeek-V4 blocks
  (MLA-style low-rank attention, MoE via draft/moe.py, mHC
  hyper-connections) under the official mtp.* naming, so trained state
  dicts round-trip into the bundled layout without renaming
- training.attention_backend=native: the draft model implements its own
  block/window attention (no wrapper-built masks); dspark providers and
  assembly accept the DSparkV4DraftModel architecture; the confidence
  head reads its own feature stream via pop_confidence_hidden
- scripts: official-weight download, init-from-target (identity/average
  main_proj, --stage-layers for spread-capture drafts), export bundling
  into the official servable layout, numeric parity gate vs the
  reference implementation, step-time benchmark
- recipes: official-arch two-node H200 + runbook, 0813-traffic 32K on
  H200 and B200 2-node stacks, with launch scripts

(cherry picked from commit 67fea1a)

[Yusheng Su: cherry-picked onto the MoE framework stack (sgl-project#811 rework)
without its parent sgl-project#799, so dspark_v4.py's `from .moe import MoEGate,
SparseMoE` no longer resolves until a follow-up commit moves the FFN onto
MoELayer. examples/configs/README.md conflict: kept both paragraphs and
dropped the sentence about sgl-project#799's `...-moe.yaml` ablation recipe.]

[Yusheng Su: rebased onto main a580ec5, which gave
_dspark_objective_chunk_terms a positional block_index and a keyword-only
total_blocks; confidence_hidden now follows block_index, so
checkpointed_chunk_reduce still slices it with the other block inputs.]
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
sgl-project#811's router, routed-expert and shared-expert contracts and their
registries stay; this adds the built-in implementations, so a draft that
names a preset builds a working layer and the tests run real code.

- router.py: TopKRouter, registered as "topk", picks the top-k on the
  balance controller's selection scores and combines with the raw scores,
  all in fp32; a softmax score function is registered and
  get_score_function goes (SCORE_FUNCTIONS.get does the same). Routers
  implement TrainingState and receive tokens_per_sample.
- experts.py: GroupedExperts, registered as "grouped", holds the routed
  experts as three stacked [E, out, in] parameters with sorted-segment
  dispatch (sorted_loop, or torch._grouped_mm on CUDA) and returns a
  fresh fp32 accumulator, which the RoutedExperts contract now requires.
- shared.py: SwiGLUSharedExpert, registered as "swiglu". Both expert
  contracts implement TrainingState.
- layer.py: MoELayer adds the shared expert into the fp32 routed sum,
  casts once, and passes tokens_per_sample to the router.
- tests: the router, experts and shared-expert stubs go; the layer tests
  run the built-in components, with reference checks of the routed sum,
  the grouped_mm path and the single cast, and a check that the layer
  builds the implementations the config names.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
Checkpoint files name an MoE layer's tensors the way the target family
does, while modules keep a native layout (stacked [E, out, in] experts).
checkpoint.py replaces sgl-project#811's state_dict.py converters with layouts
registered by name: the preset picks one (moe_layout overrides it), only
that layout runs (sgl-project#811's converters all ran on every state dict), and it
maps one MoE layer at a time.

- checkpoint.py: CheckpointLayout(to_file, from_file), the LAYOUTS
  registry with the "native" layout (files keep module names), and
  to/from_checkpoint_state_dict(state, owner), where owner is the draft
  model or its config; dense drafts pass through unchanged.
- init.py goes: WarmStartPlan and select_target_experts have no user in
  this PR.
- config.py: MoEConfig.layout (draft JSON key moe_layout, preset
  default); an unknown layout fails when the config resolves.
- families/__init__.py: where target families register a preset and a
  layout; core modules never import it.
- Every file read and write converts through the draft's layout:
  FSDPTrainingBackend save/load, trainer resume, warm start,
  AutoDraftModel.from_pretrained (MoE files are read memory-mapped and
  regrouped, which HF from_pretrained cannot do), materialize_draft and
  the HF exporter. to_sglang (EAGLE3 only) goes back to main's version.
- __init__.py: the public API is the contracts, the registration
  functions, the layer and its hooks, and the checkpoint boundary; the
  registries and build functions stay in their modules.
- tests: the layout config keys, a per-expert test layout's round trip
  (with layer-prefix disambiguation), owner resolution, the native
  identity, and a from_pretrained round trip through the layout.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
sgl-project#811 added the moe/* metrics inside the DFlash and DSpark strategies'
forward_loss and collected the auxiliary loss without adding it to the
objective.

- strategies/base.py: DraftTrainStrategy.finalize_step_output adds the
  draft's summed MoE auxiliary loss to whichever term backpropagates
  (loss, or the loss_terms numerator scaled by this micro-batch's
  denominator, the weighting the objective gets) and the layer-averaged
  moe/* metrics; drafts without MoE layers get their output back
  unchanged. _moe_metrics and its two call sites go.
- controller.py: TrainerCore calls it after forward_loss on every
  training micro-step; evaluation calls forward_loss alone, so eval
  losses exclude the auxiliary term.
- test_dflash_host_syncs.py: the optimizer-boundary test's stand-in
  strategy subclasses DraftTrainStrategy, the type TrainerCore takes.
- tests: the aux loss reaches the gradient with and without loss_terms,
  the moe/* metrics reach a strategy with no MoE code, and a dense
  step's output is untouched.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
- moe/DESIGN.md: rewritten for the framework core: the package layout,
  the rules (core never imports families, every choice is explicit, a new
  family only adds files), and the contracts: detection keys on the preset, one
  TrainingState loop, deferred updates, the accumulation order, attribute
  names, naming converted at the file boundary, and model-level terms for
  every strategy. sgl-project#811's notes that still hold stay: the Megatron-Core
  split, why a preset per target family (serving, latency), and the
  extension points, including experts backends and shared experts.
- customization.md: drafts opt in by naming a moe_preset; architecture
  and training-only keys, losses and metrics without strategy code,
  checkpoints and exports through the layout, how new families and
  components register.
- training/DESIGN.md: DraftTrainStrategy owns finalize_step_output, which
  TrainerCore calls after forward_loss.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
sgl-project#811's router, routed-expert and shared-expert contracts and their
registries stay; this adds the built-in implementations, so a draft that
names a preset builds a working layer and the tests run real code.

- router.py: TopKRouter, registered as "topk", picks the top-k on the
  balance controller's selection scores and combines with the raw scores,
  all in fp32; a softmax score function is registered and
  get_score_function goes (SCORE_FUNCTIONS.get does the same). Routers
  implement TrainingState and receive tokens_per_sample.
- experts.py: GroupedExperts, registered as "grouped", holds the routed
  experts as three stacked [E, out, in] parameters with sorted-segment
  dispatch (sorted_loop, or torch._grouped_mm on CUDA) and returns a
  fresh fp32 accumulator, which the RoutedExperts contract now requires.
- shared.py: SwiGLUSharedExpert, registered as "swiglu". Both expert
  contracts implement TrainingState.
- layer.py: MoELayer adds the shared expert into the fp32 routed sum,
  casts once, and passes tokens_per_sample to the router.
- tests: the router, experts and shared-expert stubs go; the layer tests
  run the built-in components, with reference checks of the routed sum,
  the grouped_mm path and the single cast, and a check that the layer
  builds the implementations the config names.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
Checkpoint files name an MoE layer's tensors the way the target family
does, while modules keep a native layout (stacked [E, out, in] experts).
checkpoint.py replaces sgl-project#811's state_dict.py converters with layouts
registered by name: the preset picks one (moe_layout overrides it), only
that layout runs (sgl-project#811's converters all ran on every state dict), and it
maps one MoE layer at a time.

- checkpoint.py: CheckpointLayout(to_file, from_file), the LAYOUTS
  registry with the "native" layout (files keep module names), and
  to/from_checkpoint_state_dict(state, owner), where owner is the draft
  model or its config; dense drafts pass through unchanged.
- init.py goes: WarmStartPlan and select_target_experts have no user in
  this PR.
- config.py: MoEConfig.layout (draft JSON key moe_layout, preset
  default); an unknown layout fails when the config resolves.
- families/__init__.py: where target families register a preset and a
  layout; core modules never import it.
- Every file read and write converts through the draft's layout:
  FSDPTrainingBackend save/load, trainer resume, warm start,
  AutoDraftModel.from_pretrained (MoE files are read memory-mapped and
  regrouped, which HF from_pretrained cannot do), materialize_draft and
  the HF exporter. to_sglang (EAGLE3 only) goes back to main's version.
- __init__.py: the public API is the contracts, the registration
  functions, the layer and its hooks, and the checkpoint boundary; the
  registries and build functions stay in their modules.
- tests: the layout config keys, a per-expert test layout's round trip
  (with layer-prefix disambiguation), owner resolution, the native
  identity, and a from_pretrained round trip through the layout.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
sgl-project#811 added the moe/* metrics inside the DFlash and DSpark strategies'
forward_loss and collected the auxiliary loss without adding it to the
objective.

- strategies/base.py: DraftTrainStrategy.finalize_step_output adds the
  draft's summed MoE auxiliary loss to whichever term backpropagates
  (loss, or the loss_terms numerator scaled by this micro-batch's
  denominator, the weighting the objective gets) and the layer-averaged
  moe/* metrics; drafts without MoE layers get their output back
  unchanged. _moe_metrics and its two call sites go.
- controller.py: TrainerCore calls it after forward_loss on every
  training micro-step; evaluation calls forward_loss alone, so eval
  losses exclude the auxiliary term.
- test_dflash_host_syncs.py: the optimizer-boundary test's stand-in
  strategy subclasses DraftTrainStrategy, the type TrainerCore takes.
- tests: the aux loss reaches the gradient with and without loss_terms,
  the moe/* metrics reach a strategy with no MoE code, and a dense
  step's output is untouched.
yushengsu-thu added a commit to yushengsu-thu/SpecForge that referenced this pull request Oct 20, 2026
- moe/DESIGN.md: rewritten for the framework core: the package layout,
  the rules (core never imports families, every choice is explicit, a new
  family only adds files), and the contracts: detection keys on the preset, one
  TrainingState loop, deferred updates, the accumulation order, attribute
  names, naming converted at the file boundary, and model-level terms for
  every strategy. sgl-project#811's notes that still hold stay: the Megatron-Core
  split, why a preset per target family (serving, latency), and the
  extension points, including experts backends and shared experts.
- customization.md: drafts opt in by naming a moe_preset; architecture
  and training-only keys, losses and metrics without strategy code,
  checkpoints and exports through the layout, how new families and
  components register.
- training/DESIGN.md: DraftTrainStrategy owns finalize_step_output, which
  TrainerCore calls after forward_loss.
@maocheng23 maocheng23 closed this Oct 27, 2026
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.

3 participants