Skip to content

vi Feature/mtp - #10683

Open
HelloWorldBeginner wants to merge 4 commits into
hiyouga:mainfrom
HelloWorldBeginner:feature/mtp
Open

vi Feature/mtp#10683
HelloWorldBeginner wants to merge 4 commits into
hiyouga:mainfrom
HelloWorldBeginner:feature/mtp

Conversation

@HelloWorldBeginner

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes # (issue)

Before submitting

llamafactory-mtp and others added 4 commits August 11, 2026 17:05
Adapt the FSDP2 MTP implementation from MindSpeed-LLM
(mindspeed_llm/fsdp2/models/common/mtp.py) to the LlamaFactory v1
architecture, making it HuggingFace-transformers generic so it can be
attached to any Llama/Qwen3/Mistral-style decoder-only causal LM.

Components:
* MultiTokenPredictionBlock / MultiTokenPredictionLayer
  (src/llamafactory/v1/plugins/model_plugins/mtp.py): K MTP heads that
  reuse the base model's decoder layer class, with shared enorm/hnorm
  norms, e_proj/h_proj projections and a final_layernorm, mirroring
  MindSpeed-LLM. Head k predicts token p+k+2 from position p.
* MTPModelPlugin: grafts the block onto the model as `model.mtp` and
  patches `model.forward` to emit `mtp_logits` during training.
* compute_mtp_loss: loss_weights-weighted per-head CE mean, averaged over
  heads; total loss = lm_loss + loss_scale * mtp_loss (loss_scale=0.3).
* ModelArguments.mtp_config plugin field + ModelEngine wiring.
* SFTTrainer MTP-aware compute_loss (non context-parallel path).
* FSDP2: MTP inner decoder layers are sharded automatically by the
  existing prepare_model loop (wraps all modules of the decoder-layer
  class, including mtp.layers.*.layer).
* Example YAML, unit tests, and docs.

Verified on llamafactory/tiny-random-qwen2.5 (attach, forward shapes,
loss + backward, head-offset convention).
Extend the MTP layer to work under Ulysses context parallelism, following
the MTP+CP loss reduction in MindSpeed-LLM's FSDP2 trainer.

Changes:
* compute_mtp_loss (mtp.py): add a `cp_group` argument and a context-parallel
  per-head loss path (`_cp_head_loss`). Under CP, each head all-gathers
  labels / loss_weights / log_probs across the CP group so the per-head loss
  is computed on the full sequence, mirroring the single-head
  `sequence_parallel_loss` plugin. Non-CP behavior is unchanged.
* sequence_parallel.py: refactor the main-head CP loss into a reusable
  helper and add a `sequence_parallel_mtp_loss` plugin that computes the
  main CP loss plus the scaled MTP loss. The MTP decoder layers participate
  in Ulysses attention automatically through the existing global
  `_flash_attention_forward` patch.
* base_trainer.py: route to `sequence_parallel_mtp_loss` in `fit()` when CP
  is enabled and the model has an MTP block.
* Example YAML (train_full_mtp_ulysses_cp.yaml), CP alignment unit test,
  and docs.

The CP alignment test (2 gloo ranks) verifies that the context-parallel
MTP loss reproduces the full-sequence MTP loss exactly. CP requires FSDP2
and flash_attention_2 (not DeepSpeed), same as non-MTP CP.
Upstream hiyouga#10598 replaced the v1 'template' field with apply_chat_template
(+ custom_chat_template). Keeping 'template: qwen3_nothink' makes
HfArgumentParser reject the config with:
  ValueError: Some keys are not used by the HfArgumentParser: ['template']

Qwen3 models apply their tokenizer chat template automatically, so the
field is simply removed from both MTP example configs.
… mtp_loss logging

Address four issues in the v1 MTP (Multi-Token Prediction) implementation:

* Layer selection (Q1): pick a full_attention layer index for each MTP head
  instead of blindly taking the last layer. Hybrid-attention models (Qwen3 mixes
  full/sliding; Qwen3.5 mixes full/GDN) select the attention type by
  config.layer_types[layer_idx], and an MTP head needs full self-attention (global
  context) to predict the token at offset k+2. _select_layer_idx_for_mtp picks the
  last full_attention idx (fallback to N-1 for all-full Llama/Mistral), so the MTP
  head always builds a self_attn module that goes through _flash_attention_forward
  (and is thus covered by the Ulysses CP patch).

* Weight save/load (Q2): strip the shared mtp.embed_tokens/output_layer keys before
  save_pretrained (4 save paths: fsdp2.save_model, fsdp2.save_checkpoint
  save_ckpt_as_hf, base_trainer non-dist save_model, checkpoint standard save) to
  avoid the shared-tensors RuntimeError; load_mtp_weights re-reads mtp.* from the
  checkpoint after apply_mtp (from_pretrained drops them as unexpected). No-op on
  meta device (FSDP2 meta path loads mtp.* via the HF weight loop). Verified
  end-to-end: save no longer raises, weights restored bit-identical, shared
  modules re-tied.

* CP forward shift (Q3): shift_input_ids_for_mtp is context-parallel aware — under
  CP it all-gathers each rank's first token and fills the previous rank's tail with
  the next rank's first token (only the global last rank is padded), instead of a
  plain local roll that drops the real next token at every CP boundary. Attention
  (Ulysses global patch) and loss (_cp_head_loss all-gather) were already correct.

* mtp_loss logging: expose the unscaled per-head-mean MTP loss as model._last_mtp_loss
  in both the non-CP (sft_trainer) and CP (sequence_parallel_mtp_loss) paths; BaseTrainer
  logs it as `mtp_loss` alongside `loss` so MTP convergence is visible during training.

Tests: add test_mtp_save_load (save->reload weight restoration) and
test_mtp_shift_input_ids_cp (2-rank gloo shift vs full-sequence roll). All 8
test_mtp.py tests pass; ruff clean.

Docs/yaml: add docs/v1_mtp.md updates, docs/v1_mtp_testing.md (end-to-end test
flow), scripts/verify_mtp_save_load_e2e.py, and three test yamls (save_load,
resume, cp) using Qwen3-0.6B (non-GDN).
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