Skip to content

Generalize dense-layer MLP hooks across interleaved MoE architectures - #1666

Merged
jlarson4 merged 5 commits into
devfrom
bug/moe-dense-hooks
Aug 13, 2026
Merged

Generalize dense-layer MLP hooks across interleaved MoE architectures#1666
jlarson4 merged 5 commits into
devfrom
bug/moe-dense-hooks

Conversation

@jlarson4

Copy link
Copy Markdown
Collaborator

Description

  • Fix DeepSeek V2 dense MLP hook semantics #1651 fixed the dense-prefix hook semantics for DeepSeek V2 with a per-arch MoEBridge subclass carrying fallback-list aliases. The same mapping exists in sixteen other adapters, so this generalizes the fix into MoEBridge itself and retires that subclass.
  • MoEBridge.set_original_component now dispatches per layer at bind time: when a layer's module actually exposes the adapter-declared dense projections, that instance adopts the gated-MLP alias set (hook_pre, hook_pre_linear, hook_post) plus W_gate/W_in/W_out and b_*. Sparse layers are untouched. Detection is positive-only — declared and present — so no sparse block can be mis-detected by a naming quirk and stripped of its hooks.
  • The keys are deliberately not gate/in/out: on a sparse layer of the same model gate is the router, so reusing it would make blocks.N.mlp.gate.hook_out mean the router on some layers and a d_mlp projection on others. dense_* is what llada2_moe, llama4, and laguna already declared, so those three are fixed by the shared component with no edit.
  • Ungated dense feed-forwards are expressible too: dense_in/dense_out alone bind the MLP-style alias set (no hook_pre_linear, no W_gate), which is what Switch Transformers' wi/wo needs.
  • Rollout: deepseek_v2, deepseek_v3, glm4_moe, glm4_moe_lite, glm_moe_dsa, ernie4_5_moe, afmoe, jamba, switch_transformers, laguna, llama4, llada2_moe, and the Qwen MoE family (qwen2_moe, qwen3_moe, qwen3_next, qwen3_vl_moe) — where the MoE-only submodules were additionally non-optional, so an mlp_only_layers / decoder_sparse_step checkpoint crashed at boot instead of degrading. qwen3_next and llama4 also gain the router hooks they never mapped. qwen3_5_moe is deliberately excluded: HF builds its sparse block unconditionally, so its dense declarations were dead and its MoE submodules are required.
  • optional alone cannot say "required on sparse layers, absent on dense ones", which made a renamed HF router bind silently with no hooks — the failure this work removes. Adapters now declare sparse_required=(...); a sparse layer missing one raises from a validate_after_setup hook called once setup_submodules knows the skipped set. The tuple is validated at construction, since a typo would otherwise silently disable the guard. Symmetrically, a declared dense_gate that does not resolve on a dense layer is treated as a rename and raises, rather than binding as ungated and aliasing hook_pre to the up projection.
  • get_params() keyed on submodules literally named in/out, so dense MoE layers fell into a bare except AttributeError and were silently zero-filled — real weights on the component, zeros in the parameter dict. It now resolves dense_* on dense binds, and the remaining zero-fill path logs a warning naming the layer instead of failing silent.
  • Dense instances drop hook_router_scores rather than advertise a hook that can never fire (and forward no longer calls it), the dispatch is symmetric so a rebinding harness cannot leave a chimera, and hook_aliases/property_aliases are exempt from GeneralizedComponent.__setattr__'s passthrough — without that, a per-layer rebind is forwarded to the wrapped HF module whenever it exposes the attribute and silently vanishes.
  • Alias collection: both collectors are now cycle-safe with a path-scoped guard (a globally-visited set cuts cycles but also silently drops a component legitimately shared under two names); _collect_component_aliases runs first on every hook_dict access and previously raised RecursionError on a 2-cycle. The block walk is unified into one loop, warns on unresolved aliases naming the stale-fallback consequence, and is memoized against (registry size, alias generation) — size alone cannot see a dense↔sparse rebind, which leaves the registry the same size while changing what the aliases point at. List-valued targets are skipped by the template collector, whose consumer reverse-matches with str.endswith and is lru_cached.
  • Tests, each verified to fail with the fix reverted: unit dispatch coverage (dense/sparse binding, weight accessors pinned to the wrapped HF projections with a negative control, dropped router hook, gate-name non-overlap, template independence, symmetric rebind, __setattr__ passthrough survival, positive-detection-only, ungated dense, renamed-gate and renamed-router guards) bound through the real setup_submodules; a self-checking roster that greps the adapters directory and fails if a dense-declaring adapter is outside the guards; alias-traversal tests for cycles and diamonds; and integration on katuni4ka/tiny-random-deepseek-v3 covering hook shapes, alias identities, get_params weights, logits parity, and cache invalidation across a rebind.
  • Bridge-vs-HF parity for the tiny roster now uses a measured, scale-relative tolerance instead of a platform-keyed constant. Measured across all 12 checkpoints on macOS-arm64: worst drift 2.2e-07, nine bit-identical, against a former 1e-2 gate — a 10% error injected into the reconstructed attention scale passed everywhere and now fails. Relative because the roster's logit spreads differ by ~700x, so no single absolute number is both stable on the widest checkpoint and discriminating on the narrowest.
  • CI model caches: the tiny checkpoints these suites need are in the path lists of every job that runs them (including the macOS/MPS job, which ran them against a cache listing only TinyStories), the key is bumped so the additions actually save, and the benchmark job no longer shares that key with a one-path list.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have not rewritten tests relating to key interfaces which would affect backward compatibility

@jlarson4
jlarson4 merged commit 5c44ea7 into dev Aug 13, 2026
25 checks passed
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.

1 participant