Motivation
HuggingFace just merged native Multi-Token Prediction (MTP) support into transformers
(PR #46229).
It adds an in-library MTP layer (MtpLayer / MtpLayerStack), lets a model declare MTP through its config
(num_mtp_layers), automatically finds and loads MTP weights from a checkpoint, and provides
both an inference path and a training path.
Today our MTP finetuning is built from three custom pieces that we maintain ourselves, and
each one grows every time a new model ships with native MTP:
- Convert / extract (
src/speculators/convert/mtp/converter.py) — pulls the MTP weights
out of the checkpoint, renames every weight through hand-written lookup tables, and packs
MoE expert weights together.
- Finetune (
src/speculators/models/mtp/core.py, .../model_definitions.py) — we
re-implement the MTP layer once per model family (qwen3, qwen3_next, qwen3_5,
qwen3_5_moe), and run our own training loop with FastMTP per-step loss weighting.
- Stitch (
scripts/stitch_mtp.py) — the exact reverse of step 1: rename every weight
back, unpack the MoE experts, and merge the finetuned weights into the original checkpoint
so vLLM can serve it. This is our most fragile piece — any mismatch with step 1 silently
corrupts weights.
Now that HuggingFace defines the MTP layer for us, we may be able to delete some of this. The
biggest potential win is removing the stitch step entirely: if we can load the native
weights, finetune them, and save them back in the same native format, there is nothing to
rename or merge back. That is worth checking before we add the next model family to the
current pathway.
This RFC is a request to investigate and report back, it is not a
proposal to migrate yet. The goal is a clear recommendation.
Proposed Change
Run a short investigation (read HuggingFace's merged code, then finetune one Qwen MTP model
through a HuggingFace-native path and compare token-acceptance against our current pathway).
Write up the answers and recommend one of three directions:
- Adopt fully — HuggingFace's layer works for our models, round-trips weights correctly,
and supports our loss weighting. We delete the per-model layer code, the rename tables, and
the stitch script.
- Adopt partially — use HuggingFace's layer (delete our per-model subclasses) but keep our
own training loop and/or stitch step where HuggingFace falls short.
- Not yet — too early (unreleased, missing model coverage, or a training-quality gap).
Record what would need to change for us to revisit.
The investigation should answer these questions, grouped by the pathway stage each affects.
The MTP layer itself
- Does HuggingFace's
MtpLayer behave the same as ours — two inputs (verifier hidden states
plus ground-truth token embeddings), separate norms, concatenate, project down to the hidden
size, run a decoder layer, and feed each step's output into the next step?
- Which model families does HuggingFace's layer cover today? Does it handle the mixed
linear/full-attention models we support (qwen3_next, qwen3_5_moe), including picking the
correct attention layer index?
Training quality (highest risk)
- Does HuggingFace support per-step loss weighting, or only a single overall loss coefficient
(mtp_loss_coef)? Our acceptance rate depends on FastMTP exponential-decay step weights
(roughly [0.51, 0.31, 0.18]). If HuggingFace only offers one coefficient, using its
trainer as-is would likely lower quality unless we can plug our weighting in.
- Can we keep our online-training setup, where hidden states are streamed from a live vLLM
server during training, or does HuggingFace's path assume a full local forward pass?
Weight loading and saving (this is what could kill the stitch step)
- What weight layout does HuggingFace save? Does it match the original native layout that vLLM
already reads, so a saved checkpoint serves without any stitching?
- If so, can we delete the rename tables and
stitch_mtp.py outright — or does HuggingFace
introduce its own conversion map we'd end up maintaining instead?
- Does HuggingFace handle MoE expert packing for us, removing our pack/unpack code?
Coverage and compatibility (possible blockers)
- HuggingFace's PR states DeepSeek v4 is not supported yet (its layer naming differs). We
need to confirm which of our target models HuggingFace's layer actually covers, and whether
gaps would force us to keep the current pathway for some models.
- Which released
transformers version includes this? It is merged, but we need a released
version, and adopting it raises our minimum transformers requirement.
- Does a checkpoint saved through the HuggingFace path still serve on vLLM with
{"method":"mtp","num_speculative_tokens":N} exactly as today?
Any Other Things
- Scope. In scope: reading the merged HuggingFace code and a throwaway experiment on one
Qwen model to check acceptance parity. Out of scope: HuggingFace's own inference/generation
path (we deploy on vLLM, not HuggingFace generate) and any actual migration, this RFC
produces a recommendation, not a code change.
- Deliverable. A short findings write-up answering the questions above, one
acceptance-rate comparison (HuggingFace-native finetune vs. our current pathway), and a
recommendation (adopt fully / partially / not yet) with the concrete list of files we would
delete or keep.
References
Motivation
HuggingFace just merged native Multi-Token Prediction (MTP) support into
transformers(PR #46229).
It adds an in-library MTP layer (
MtpLayer/MtpLayerStack), lets a model declare MTP through its config(
num_mtp_layers), automatically finds and loads MTP weights from a checkpoint, and providesboth an inference path and a training path.
Today our MTP finetuning is built from three custom pieces that we maintain ourselves, and
each one grows every time a new model ships with native MTP:
src/speculators/convert/mtp/converter.py) — pulls the MTP weightsout of the checkpoint, renames every weight through hand-written lookup tables, and packs
MoE expert weights together.
src/speculators/models/mtp/core.py,.../model_definitions.py) — were-implement the MTP layer once per model family (
qwen3,qwen3_next,qwen3_5,qwen3_5_moe), and run our own training loop with FastMTP per-step loss weighting.scripts/stitch_mtp.py) — the exact reverse of step 1: rename every weightback, unpack the MoE experts, and merge the finetuned weights into the original checkpoint
so vLLM can serve it. This is our most fragile piece — any mismatch with step 1 silently
corrupts weights.
Now that HuggingFace defines the MTP layer for us, we may be able to delete some of this. The
biggest potential win is removing the stitch step entirely: if we can load the native
weights, finetune them, and save them back in the same native format, there is nothing to
rename or merge back. That is worth checking before we add the next model family to the
current pathway.
This RFC is a request to investigate and report back, it is not a
proposal to migrate yet. The goal is a clear recommendation.
Proposed Change
Run a short investigation (read HuggingFace's merged code, then finetune one Qwen MTP model
through a HuggingFace-native path and compare token-acceptance against our current pathway).
Write up the answers and recommend one of three directions:
and supports our loss weighting. We delete the per-model layer code, the rename tables, and
the stitch script.
own training loop and/or stitch step where HuggingFace falls short.
Record what would need to change for us to revisit.
The investigation should answer these questions, grouped by the pathway stage each affects.
The MTP layer itself
MtpLayerbehave the same as ours — two inputs (verifier hidden statesplus ground-truth token embeddings), separate norms, concatenate, project down to the hidden
size, run a decoder layer, and feed each step's output into the next step?
linear/full-attention models we support (
qwen3_next,qwen3_5_moe), including picking thecorrect attention layer index?
Training quality (highest risk)
(
mtp_loss_coef)? Our acceptance rate depends on FastMTP exponential-decay step weights(roughly
[0.51, 0.31, 0.18]). If HuggingFace only offers one coefficient, using itstrainer as-is would likely lower quality unless we can plug our weighting in.
server during training, or does HuggingFace's path assume a full local forward pass?
Weight loading and saving (this is what could kill the stitch step)
already reads, so a saved checkpoint serves without any stitching?
stitch_mtp.pyoutright — or does HuggingFaceintroduce its own conversion map we'd end up maintaining instead?
Coverage and compatibility (possible blockers)
need to confirm which of our target models HuggingFace's layer actually covers, and whether
gaps would force us to keep the current pathway for some models.
transformersversion includes this? It is merged, but we need a releasedversion, and adopting it raises our minimum
transformersrequirement.{"method":"mtp","num_speculative_tokens":N}exactly as today?Any Other Things
Qwen model to check acceptance parity. Out of scope: HuggingFace's own inference/generation
path (we deploy on vLLM, not HuggingFace
generate) and any actual migration, this RFCproduces a recommendation, not a code change.
acceptance-rate comparison (HuggingFace-native finetune vs. our current pathway), and a
recommendation (adopt fully / partially / not yet) with the concrete list of files we would
delete or keep.
References
docs/user_guide/algorithms/mtp.md,docs/user_guide/tutorials/train_mtp_online.mdsrc/speculators/convert/mtp/converter.pysrc/speculators/models/mtp/core.py,.../model_definitions.pyscripts/stitch_mtp.py