Add MTP support for Step 3.5 Flash - #901
Conversation
4644f8f to
ac83f11
Compare
Step 3.5 Flash is a 196B MoE model (288 experts, top-8 routing, ~11B active params) with 3 MTP prediction layers. The MLX community 4-bit conversion strips MTP weights and lacks MTP-aware modeling code. This adds: - scripts/add_mtp_weights_step3p5.py: Downloads BF16 MTP shards from the original model, extracts layers 45-47, remaps to mtp.layers.*, quantizes to 4-bit, and installs the MTP modeling file - scripts/modeling_step3p5_mtp.py: Full MLX-native model implementation with MTP support (Step3p5MTP, Step3p5MTPLayer, Step3p5SharedHead) - Reasoning parser alias "step3p5" (reuses deepseek_r1 <think> parser) - Documentation updates in README.md and docs/reference/models.md Note: The custom modeling file is a workaround until ml-explore/mlx-lm#901 is merged upstream. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Step 3.5 Flash ships MTP weights, but `sanitize()` dropped every `.mtp` key, so the head was discarded at load and speculative decoding had nothing to draft with. This adds the head and the plumbing to use it: - `Step3p5MTPLayer`: eh_proj over the concatenated normalized hidden state and next-token embedding, then a decoder block. It borrows a real layer's attention config rather than inventing one, picking the first `sliding_attention` entry in `layer_types` so RoPE and the attention settings match what the checkpoint was trained with. - `Step3p5SharedHead`: per-MTP-layer norm plus output projection. - `mtp_forward()` / `make_mtp_cache()`, and `sanitize()` keeps the MTP weights instead of dropping them. Rebased onto current main. ml-explore#949 landed in the meantime and gave sliding attention layers a `RotatingKVCache`, which the MTP cache has to follow: the MTP layer runs sliding attention, so a plain `KVCache` there grows without bound across a generation while the main model's equivalent layers stay capped at `sliding_window`. `make_mtp_cache()` now keys off `self_attn.is_sliding` rather than assuming, because which layer it borrows depends on `layer_types`. Covered by `test_step3p5_mtp_cache_follows_the_main_model_policy`, which asserts the class, the window, and that the cache actually stays bounded over 4x sliding_window steps — a plain `KVCache` fails it. `tests/test_models.py`: 78 passed, 1 skipped, 52 subtests.
ac83f11 to
3f2400b
Compare
|
Rebased onto current The base model landed upstream in the meantime — for k, v in weights.items():
if ".mtp" in k:
continueSo the checkpoint ships MTP weights that are discarded at load, and speculative decoding has nothing to draft with. This PR is not stale — it just never got picked up. One real conflict the rebase surfaced, and it was semantic rather than textual. #949 gave sliding attention layers a Fixed by following the same policy, keyed off return [
RotatingKVCache(max_size=self.args.sliding_window)
if layer.self_attn.is_sliding else KVCache()
for layer in self.mtp.layers
]
One caveat I would rather state than have found in review: I no longer run Step 3.5 Flash, so this round is structural verification only — I cannot re-measure MTP acceptance against real weights the way the original numbers were taken. The cache fix is checked against behaviour, not just types, but if you want live acceptance figures before merging, someone with the model would need to take them. |
|
Closing this — not because it went stale, but because I can no longer stand behind it. State as of today, for anyone who finds this later:
What I cannot do is the part that matters for merging it: I no longer run Step 3.5 Flash, so I cannot re-measure MTP acceptance against real weights. The original numbers came from a live deployment that no longer exists, and shipping a speculative-decoding path whose acceptance rate nobody can currently verify is not something I want to advocate for. The branch stays up. If someone with the model wants to pick it up, the code and tests are here and the cache fix above is the non-obvious part. |
Summary
num_nextn_predict_layers > 0Changes
Step3p5SharedHeadStep3p5MTPLayerStep3p5MTPModel.__call__return_hiddenparameter for MTP integrationModel.mtp_forwardModel.make_mtp_cachesanitizequant_predicateAlso fixes two pre-existing
ruffE741 lint warnings (l→layerin generator expressions).MTP Architecture
Each MTP layer uses sliding attention with dense SwiGLU MLP (not MoE), matching the original StepFun architecture.
Test plan
return_hidden=Truereturns prenorm hidden statesmtp_forward()produces correct-shaped logitsmake_mtp_cache()returns KVCache list matching MTP layer countruff formatandruff checkpass cleanlymlx-community/Step-3.5-Flash-4bit(111 GB, requires Apple Silicon)🤖 Generated with Claude Code