Text Generation
Transformers
Safetensors
English
hf_mtp
multi-token-prediction
speculative-decoding
soft-mtp
custom_code
Instructions to use JaydenTeoh/soft-mtp-332m-model with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use JaydenTeoh/soft-mtp-332m-model with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="JaydenTeoh/soft-mtp-332m-model", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("JaydenTeoh/soft-mtp-332m-model", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use JaydenTeoh/soft-mtp-332m-model with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "JaydenTeoh/soft-mtp-332m-model" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "JaydenTeoh/soft-mtp-332m-model", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/JaydenTeoh/soft-mtp-332m-model
- SGLang
How to use JaydenTeoh/soft-mtp-332m-model with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "JaydenTeoh/soft-mtp-332m-model" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "JaydenTeoh/soft-mtp-332m-model", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "JaydenTeoh/soft-mtp-332m-model" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "JaydenTeoh/soft-mtp-332m-model", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use JaydenTeoh/soft-mtp-332m-model with Docker Model Runner:
docker model run hf.co/JaydenTeoh/soft-mtp-332m-model
Checkpoint from Multi-Token Prediction Done Right (arXiv:2610.38331)
Browse files- README.md +83 -0
- blocks.py +167 -0
- config.json +34 -0
- config.py +71 -0
- generation_config.json +4 -0
- loss.py +180 -0
- merges.txt +0 -0
- model.py +359 -0
- model.safetensors +3 -0
- tokenizer.json +0 -0
- tokenizer_config.json +9 -0
- vocab.json +0 -0
README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
datasets:
|
| 6 |
+
- HuggingFaceFW/fineweb-edu
|
| 7 |
+
tags:
|
| 8 |
+
- multi-token-prediction
|
| 9 |
+
- speculative-decoding
|
| 10 |
+
- soft-mtp
|
| 11 |
+
library_name: transformers
|
| 12 |
+
pipeline_tag: text-generation
|
| 13 |
+
inference: false
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Soft-MTP — 332m
|
| 17 |
+
|
| 18 |
+
Multi-token prediction against the model's own next-token verifier. One of twelve checkpoints released with
|
| 19 |
+
[**Multi-Token Prediction Done Right**](https://arxiv.org/abs/2610.38331) (arXiv:2610.38331):
|
| 20 |
+
three objectives x four sizes, trained under an identical recipe so they are directly comparable.
|
| 21 |
+
|
| 22 |
+
Code: [JaydenTeoh/Soft-MTP](https://github.com/JaydenTeoh/Soft-MTP) — `configs/332m_fineweb.yaml`.
|
| 23 |
+
|
| 24 |
+
## Objective
|
| 25 |
+
|
| 26 |
+
Each MTP head is trained against the model's own next-token distribution `q` rather than the realized token `e_y`, via a straight-through KL-recovery coupling that costs one backward pass and no vocabulary softmax. See the paper for the gradient decomposition this removes.
|
| 27 |
+
|
| 28 |
+
## Usage
|
| 29 |
+
|
| 30 |
+
```python
|
| 31 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 32 |
+
|
| 33 |
+
model = AutoModelForCausalLM.from_pretrained("JaydenTeoh/soft-mtp-332m-model", trust_remote_code=True)
|
| 34 |
+
tok = AutoTokenizer.from_pretrained("gpt2") # the corpus tokenizer
|
| 35 |
+
|
| 36 |
+
ids = tok("The capital of France is", return_tensors="pt").input_ids
|
| 37 |
+
print(tok.decode(model.generate(ids, max_new_tokens=20)[0]))
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
`trust_remote_code=True` is required: the architecture (`model_type: hf_mtp`) is defined by the
|
| 41 |
+
modelling files in this repo, not by `transformers`.
|
| 42 |
+
|
| 43 |
+
The tokenizer is **stock GPT-2** — this repo ships no tokenizer files. `vocab_size` is padded from
|
| 44 |
+
50257 to 50304 for alignment; the extra rows are unused.
|
| 45 |
+
|
| 46 |
+
## Model
|
| 47 |
+
|
| 48 |
+
| | |
|
| 49 |
+
|---|---|
|
| 50 |
+
| hidden size x layers | 768 x 36 |
|
| 51 |
+
| attention heads | 12 |
|
| 52 |
+
| context length | 2048 |
|
| 53 |
+
| MTP heads | 3 (DeepSeek-style cascade, 1 block each) |
|
| 54 |
+
| parameters | 357M total |
|
| 55 |
+
| **deployed (NTP path)** | **357M** |
|
| 56 |
+
|
| 57 |
+
MTP heads are auxiliary: they are discarded at inference, so what you deploy is an ordinary next-token transformer. They can also serve as a draft model for self-speculative decoding.
|
| 58 |
+
|
| 59 |
+
## Training
|
| 60 |
+
|
| 61 |
+
| | |
|
| 62 |
+
|---|---|
|
| 63 |
+
| data | FineWeb-Edu, GPT-2 BPE |
|
| 64 |
+
| tokens | 31.5B (60,000 steps x 256 x 2048) |
|
| 65 |
+
| optimizer | Muon on 2D hidden matrices + AdamW on embeddings/head/norms |
|
| 66 |
+
| peak LR | 0.0015, cosine to 1e-05, 2,000 warmup steps |
|
| 67 |
+
| weight decay | 0.1 |
|
| 68 |
+
| precision | bf16 autocast over fp32 master weights |
|
| 69 |
+
| seed | 13 |
|
| 70 |
+
|
| 71 |
+
Single run per cell, one seed. The twelve checkpoints share seed, data order and schedule, so
|
| 72 |
+
differences between them are attributable to the objective.
|
| 73 |
+
|
| 74 |
+
## Citation
|
| 75 |
+
|
| 76 |
+
```bibtex
|
| 77 |
+
@article{teoh2026multitokenpredictionright,
|
| 78 |
+
title={Multi-Token Prediction Done Right},
|
| 79 |
+
author={Jayden Teoh},
|
| 80 |
+
journal={arXiv preprint arXiv:2610.38331},
|
| 81 |
+
year={2026},
|
| 82 |
+
}
|
| 83 |
+
```
|
blocks.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pure-PyTorch Llama-style transformer block (no FLA/triton; runs on CPU + GPU).
|
| 2 |
+
|
| 3 |
+
RMSNorm (fp32-upcast, matches Llama) + RoPE (rotate_half convention) + GQA attention via
|
| 4 |
+
`F.scaled_dot_product_attention` (uses FlashAttention on CUDA) + SwiGLU. Verified equal to
|
| 5 |
+
HF `LlamaForCausalLM` to 1e-4. One `Block` is reused
|
| 6 |
+
for trunk layers and MTP heads, and `torch.compile`s cleanly (regional compile).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from contextlib import nullcontext
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class RMSNorm(nn.Module):
|
| 17 |
+
def __init__(self, dim, eps=1e-5):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 20 |
+
self.eps = eps
|
| 21 |
+
|
| 22 |
+
def forward(self, x):
|
| 23 |
+
in_dtype = x.dtype # upcast LOW precision to fp32 (matches Llama; stable under bf16),
|
| 24 |
+
h = x.float() if in_dtype in (torch.float16, torch.bfloat16) else x # but never downcast fp64
|
| 25 |
+
h = h * torch.rsqrt(h.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 26 |
+
return (self.weight * h).to(in_dtype)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def build_rope_cache(seq_len, head_dim, theta, device, dtype=torch.float32):
|
| 30 |
+
"""cos/sin table [T, head_dim]. Trig computed in fp32, cast to `dtype` last."""
|
| 31 |
+
dev = device if isinstance(device, torch.device) else torch.device(device)
|
| 32 |
+
# Disable any outside autocast context if any, to really force fp32 (autocast has no policy for
|
| 33 |
+
# `meta`, which the param-count probes build on, so the context is guarded rather than assumed).
|
| 34 |
+
ctx = (torch.autocast(device_type=dev.type, enabled=False)
|
| 35 |
+
if dev.type in ("cpu", "cuda", "xpu") else nullcontext())
|
| 36 |
+
with ctx:
|
| 37 |
+
inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
|
| 38 |
+
freqs = torch.outer(torch.arange(seq_len, device=device).float(), inv_freq)
|
| 39 |
+
emb = torch.cat((freqs, freqs), dim=-1) # [T, head_dim]
|
| 40 |
+
return emb.cos().to(dtype), emb.sin().to(dtype)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _rotate_half(x):
|
| 44 |
+
x1, x2 = x.chunk(2, dim=-1)
|
| 45 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def apply_rope(q, k, cos, sin):
|
| 49 |
+
"""cos/sin are [T, D] (batch-shared positions) or [B, T, D] (per-row `position_ids`, the HF
|
| 50 |
+
convention — `generate()` derives them as `attention_mask.cumsum(-1) - 1`, so left-padded rows
|
| 51 |
+
get 0..n-1 on their REAL tokens). Both broadcast to q/k's [B, H, T, D]."""
|
| 52 |
+
if cos.dim() == 2:
|
| 53 |
+
cos, sin = cos[None, None], sin[None, None] # [1, 1, T, D]
|
| 54 |
+
else:
|
| 55 |
+
cos, sin = cos[:, None], sin[:, None] # [B, 1, T, D]
|
| 56 |
+
return q * cos + _rotate_half(q) * sin, k * cos + _rotate_half(k) * sin
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class Attention(nn.Module):
|
| 60 |
+
def __init__(self, cfg):
|
| 61 |
+
super().__init__()
|
| 62 |
+
self.n_heads, self.n_kv = cfg.num_heads, cfg.num_kv_heads
|
| 63 |
+
self.hd = cfg.hidden_size // cfg.num_heads
|
| 64 |
+
self.q_proj = nn.Linear(cfg.hidden_size, self.n_heads * self.hd, bias=False)
|
| 65 |
+
self.k_proj = nn.Linear(cfg.hidden_size, self.n_kv * self.hd, bias=False)
|
| 66 |
+
self.v_proj = nn.Linear(cfg.hidden_size, self.n_kv * self.hd, bias=False)
|
| 67 |
+
self.o_proj = nn.Linear(self.n_heads * self.hd, cfg.hidden_size, bias=False)
|
| 68 |
+
|
| 69 |
+
def forward(self, x, cos, sin, past_key_values=None, layer_idx=0, cache_position=None,
|
| 70 |
+
cache_readonly=False, attention_mask=None):
|
| 71 |
+
"""`attention_mask` is the pre-built ADDITIVE 4D mask [B, 1, T, k_len] (causal + padding), or
|
| 72 |
+
None for the training/cache-free path which uses the compile-clean `is_causal` kernel."""
|
| 73 |
+
B, T, _ = x.shape
|
| 74 |
+
q = self.q_proj(x).view(B, T, self.n_heads, self.hd).transpose(1, 2)
|
| 75 |
+
k = self.k_proj(x).view(B, T, self.n_kv, self.hd).transpose(1, 2)
|
| 76 |
+
v = self.v_proj(x).view(B, T, self.n_kv, self.hd).transpose(1, 2)
|
| 77 |
+
q, k = apply_rope(q, k, cos, sin)
|
| 78 |
+
if past_key_values is not None:
|
| 79 |
+
if cache_readonly:
|
| 80 |
+
# READ-ONLY: attend against the cache without writing to it. transformers'
|
| 81 |
+
# `update()` is unconditionally append-in-place, so this is hand-rolled. The sequential
|
| 82 |
+
# drafter needs it — its last positions consume unconfirmed DRAFT tokens, which must
|
| 83 |
+
# attend but must never enter the cache, or a rejected draft poisons later drafts.
|
| 84 |
+
layer = past_key_values.layers[layer_idx]
|
| 85 |
+
pk, pv = getattr(layer, "keys", None), getattr(layer, "values", None)
|
| 86 |
+
if pk is not None and pk.numel():
|
| 87 |
+
k = torch.cat([pk, k], dim=-2)
|
| 88 |
+
v = torch.cat([pv, v], dim=-2)
|
| 89 |
+
else: # inference KV cache (eager): append + read full K/V
|
| 90 |
+
k, v = past_key_values.update(k, v, layer_idx)
|
| 91 |
+
if self.n_kv != self.n_heads: # GQA: expand kv heads
|
| 92 |
+
rep = self.n_heads // self.n_kv
|
| 93 |
+
k, v = k.repeat_interleave(rep, dim=1), v.repeat_interleave(rep, dim=1)
|
| 94 |
+
if attention_mask is None and past_key_values is not None and cache_position is not None:
|
| 95 |
+
# SAFETY NET: a cached call whose caller forgot the pre-built mask would otherwise run
|
| 96 |
+
# BIDIRECTIONAL attention with no error — exactly the f0816ed regression that left
|
| 97 |
+
# `_dsmtp_layer` non-causal on multi-position chunks while every decode gate kept
|
| 98 |
+
# passing (the chunk's LAST position is causal by accident, and that is the only one
|
| 99 |
+
# sampled from). Model-level callers still pass the shared mask, so this never fires on
|
| 100 |
+
# the trunk/sequential paths and their build-once economics are unchanged.
|
| 101 |
+
attention_mask = build_causal_mask(cache_position, k.size(2), None, x.device, q.dtype)
|
| 102 |
+
if past_key_values is None and attention_mask is None:
|
| 103 |
+
# training / cache-free without padding: clean compiled is_causal kernel. This mirrors
|
| 104 |
+
# HF's contract exactly — `create_causal_mask` returns None when the sdpa backend can
|
| 105 |
+
# use `is_causal` (no padding, no cache), and the attention interface falls back to it.
|
| 106 |
+
out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
|
| 107 |
+
else: # cached, and/or padded cache-free: additive mask
|
| 108 |
+
# `attention_mask` here is the FINAL additive mask, already combining causal + padding,
|
| 109 |
+
# built ONCE per forward by `build_causal_mask` and shared by every layer — the same split
|
| 110 |
+
# HF uses (DeepseekV3Model calls `create_causal_mask` and DeepseekV3Attention only passes
|
| 111 |
+
# the result to the attention interface). Building it here instead would reallocate the
|
| 112 |
+
# same [B,1,T,k_len] tensor once per layer.
|
| 113 |
+
out = F.scaled_dot_product_attention(q, k, v, attn_mask=attention_mask)
|
| 114 |
+
return self.o_proj(out.transpose(1, 2).reshape(B, T, -1))
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def build_causal_mask(cache_position, k_len, attention_mask, device, dtype):
|
| 118 |
+
"""The additive [B, 1, T, k_len] mask for the CACHED path: causal AND padding, built once per
|
| 119 |
+
forward and shared by every layer (HF's `create_causal_mask` plays this role for DeepSeek-V3 /
|
| 120 |
+
Llama; their attention modules never construct a mask themselves).
|
| 121 |
+
|
| 122 |
+
causal : key position <= query's absolute `cache_position`.
|
| 123 |
+
padding: `attention_mask` is the HF convention [B, k_len], 1 = real token, 0 = pad. Without this
|
| 124 |
+
term a query attends PAD keys, which silently degrades ANY batched generation with mixed
|
| 125 |
+
prompt lengths — lm-eval left-pads its generate_until batches.
|
| 126 |
+
|
| 127 |
+
Returns a 2D [T, k_len] mask when there is no padding to apply, so the common single-sequence case
|
| 128 |
+
does not pay for a [B,1,T,k_len] broadcast.
|
| 129 |
+
"""
|
| 130 |
+
T = cache_position.numel()
|
| 131 |
+
kpos = torch.arange(k_len, device=device)
|
| 132 |
+
allow = kpos[None, :] <= cache_position[:, None]
|
| 133 |
+
mask = torch.zeros(T, k_len, device=device, dtype=dtype).masked_fill(~allow, float("-inf"))
|
| 134 |
+
if attention_mask is None:
|
| 135 |
+
return mask
|
| 136 |
+
am = attention_mask[:, :k_len]
|
| 137 |
+
mask = mask[None, None].expand(am.size(0), 1, T, k_len).clone()
|
| 138 |
+
return mask.masked_fill_(am[:, None, None, :] == 0, float("-inf"))
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class SwiGLU(nn.Module):
|
| 142 |
+
def __init__(self, cfg):
|
| 143 |
+
super().__init__()
|
| 144 |
+
self.gate_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
|
| 145 |
+
self.up_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
|
| 146 |
+
self.down_proj = nn.Linear(cfg.intermediate_size, cfg.hidden_size, bias=False)
|
| 147 |
+
|
| 148 |
+
def forward(self, x):
|
| 149 |
+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class Block(nn.Module):
|
| 153 |
+
"""Pre-norm transformer block; reused for trunk layers and MTP heads."""
|
| 154 |
+
|
| 155 |
+
def __init__(self, cfg):
|
| 156 |
+
super().__init__()
|
| 157 |
+
self.attn_norm = RMSNorm(cfg.hidden_size, cfg.norm_eps)
|
| 158 |
+
self.attn = Attention(cfg)
|
| 159 |
+
self.mlp_norm = RMSNorm(cfg.hidden_size, cfg.norm_eps)
|
| 160 |
+
self.mlp = SwiGLU(cfg)
|
| 161 |
+
|
| 162 |
+
def forward(self, x, cos, sin, past_key_values=None, layer_idx=0, cache_position=None,
|
| 163 |
+
cache_readonly=False, attention_mask=None):
|
| 164 |
+
x = x + self.attn(self.attn_norm(x), cos, sin, past_key_values, layer_idx, cache_position,
|
| 165 |
+
cache_readonly, attention_mask)
|
| 166 |
+
x = x + self.mlp(self.mlp_norm(x))
|
| 167 |
+
return x
|
config.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"HFMTPForCausalLM"
|
| 4 |
+
],
|
| 5 |
+
"auto_map": {
|
| 6 |
+
"AutoConfig": "config.HFMTPConfig",
|
| 7 |
+
"AutoModelForCausalLM": "model.HFMTPForCausalLM"
|
| 8 |
+
},
|
| 9 |
+
"ce_kernel": "naive",
|
| 10 |
+
"dtype": "float32",
|
| 11 |
+
"hidden_size": 768,
|
| 12 |
+
"intermediate_size": 2048,
|
| 13 |
+
"kl_recovery": "ghost",
|
| 14 |
+
"lambda_kl": 1.0,
|
| 15 |
+
"max_position_embeddings": 2048,
|
| 16 |
+
"model_type": "hf_mtp",
|
| 17 |
+
"mtp_arch": "deepseek",
|
| 18 |
+
"mtp_detach_lmhead": false,
|
| 19 |
+
"mtp_head_layers": 1,
|
| 20 |
+
"mtp_per_head_norm": true,
|
| 21 |
+
"mtp_share_weights": false,
|
| 22 |
+
"mtp_weight": 1.0,
|
| 23 |
+
"mtp_weight_decay": null,
|
| 24 |
+
"n_mtp": 3,
|
| 25 |
+
"norm_eps": 1e-05,
|
| 26 |
+
"num_heads": 12,
|
| 27 |
+
"num_hidden_layers": 36,
|
| 28 |
+
"num_kv_heads": 12,
|
| 29 |
+
"objective": "soft_mtp",
|
| 30 |
+
"rope_theta": 10000.0,
|
| 31 |
+
"tie_word_embeddings": false,
|
| 32 |
+
"transformers_version": "4.57.6",
|
| 33 |
+
"vocab_size": 50304
|
| 34 |
+
}
|
config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HF config for the MTP model.
|
| 2 |
+
|
| 3 |
+
Standalone `PretrainedConfig` (no FLA). `num_hidden_layers` is the NTP trunk depth; `n_mtp`
|
| 4 |
+
heads are added on top (Meta) or sequential (DeepSeek), per `mtp_arch`. `objective` lives on the
|
| 5 |
+
config so the model's `forward` can return the right loss for the HF `Trainer` (model-returns-
|
| 6 |
+
loss pattern); the training script sets it.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from transformers import PretrainedConfig
|
| 10 |
+
|
| 11 |
+
# `soft_mtp` = KL / soft-target MTP (the mirror of `hard_mtp`'s one-hot targets). Do NOT confuse with
|
| 12 |
+
# metric names that contain `kl` (e.g. `mtp_kl_verifier` = KL-to-verifier) — those are unrelated.
|
| 13 |
+
OBJECTIVES = ("ntp", "hard_mtp", "soft_mtp", "direct_kl")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class HFMTPConfig(PretrainedConfig):
|
| 17 |
+
model_type = "hf_mtp"
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
vocab_size=50304,
|
| 22 |
+
hidden_size=768,
|
| 23 |
+
num_hidden_layers=12, # NTP trunk depth
|
| 24 |
+
num_heads=12,
|
| 25 |
+
num_kv_heads=None, # None -> = num_heads (no GQA)
|
| 26 |
+
n_mtp=3, # number of MTP heads / sequential depths
|
| 27 |
+
mtp_head_layers=1, # transformer blocks per MTP head/depth
|
| 28 |
+
mtp_arch="deepseek", # "deepseek" (sequential, DeepSeek-V3 style) | "meta" (parallel)
|
| 29 |
+
# The two differ in what a head READS, not just its shape: a
|
| 30 |
+
# sequential depth consumes the teacher-forced token x_{t+k} and
|
| 31 |
+
# is cropped to T-k, a meta head ignores input_ids entirely.
|
| 32 |
+
# Code built from this default rather than from a shipped
|
| 33 |
+
# config therefore gets a structurally different model.
|
| 34 |
+
mtp_per_head_norm=False, # DeepSeek-faithful: each MTP head gets its OWN final RMSNorm before
|
| 35 |
+
# the SHARED lm_head (official `shared_head.norm` is per-module; only
|
| 36 |
+
# the head weight is shared). False (default) = one shared final norm
|
| 37 |
+
# for NTP + all heads.
|
| 38 |
+
|
| 39 |
+
max_position_embeddings=1024,
|
| 40 |
+
rope_theta=10000.0,
|
| 41 |
+
norm_eps=1e-5,
|
| 42 |
+
intermediate_size=None, # None -> ~8/3 * hidden, rounded to 128
|
| 43 |
+
tie_word_embeddings=False, # untied by default
|
| 44 |
+
objective="soft_mtp", # ntp | hard_mtp | soft_mtp | direct_kl
|
| 45 |
+
mtp_weight=1.0, # auxiliary weight on the MTP loss term (the STABLE-phase value if scheduled)
|
| 46 |
+
ce_kernel="naive", # MTP-head CE: "naive" (materialize logits) | "liger" (fused linear CE, low mem)
|
| 47 |
+
**kwargs,
|
| 48 |
+
):
|
| 49 |
+
self.hidden_size = hidden_size
|
| 50 |
+
self.num_hidden_layers = num_hidden_layers
|
| 51 |
+
self.num_heads = num_heads
|
| 52 |
+
self.num_kv_heads = num_kv_heads or num_heads
|
| 53 |
+
self.n_mtp = n_mtp
|
| 54 |
+
self.mtp_head_layers = mtp_head_layers
|
| 55 |
+
self.mtp_arch = mtp_arch
|
| 56 |
+
self.mtp_per_head_norm = mtp_per_head_norm
|
| 57 |
+
self.max_position_embeddings = max_position_embeddings
|
| 58 |
+
self.rope_theta = rope_theta
|
| 59 |
+
self.norm_eps = norm_eps
|
| 60 |
+
if intermediate_size is None:
|
| 61 |
+
inter = int(8 * hidden_size / 3)
|
| 62 |
+
intermediate_size = (inter + 127) // 128 * 128
|
| 63 |
+
self.intermediate_size = intermediate_size
|
| 64 |
+
self.objective = objective
|
| 65 |
+
self.mtp_weight = mtp_weight
|
| 66 |
+
self.ce_kernel = ce_kernel
|
| 67 |
+
super().__init__(vocab_size=vocab_size, tie_word_embeddings=tie_word_embeddings, **kwargs)
|
| 68 |
+
assert hidden_size % num_heads == 0
|
| 69 |
+
assert num_heads % self.num_kv_heads == 0
|
| 70 |
+
assert mtp_arch in ("meta", "deepseek")
|
| 71 |
+
assert self.objective in OBJECTIVES, f"unknown objective {objective!r}"
|
generation_config.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"transformers_version": "4.57.6"
|
| 4 |
+
}
|
loss.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MTP losses: ntp | hard_mtp | soft_mtp | direct_kl.
|
| 2 |
+
|
| 3 |
+
soft_mtp trains each MTP head against the model's own next-token distribution instead of the
|
| 4 |
+
realized token, using a straight-through KL-recovery coupling: adding `s_k * (sg(z_k) - z_k)` into the
|
| 5 |
+
aligned slice of the NTP hidden leaves the forward value exactly unchanged, while the backward
|
| 6 |
+
routes the NTP CE's own hidden gradient into head k. The head nets `coeff_k * (p_k - q)W`, the KL
|
| 7 |
+
gradient, in one backward — no teacher cache, no second softmax.
|
| 8 |
+
|
| 9 |
+
`labels` is PRE-SHIFTED and the same length as `input_ids`: `labels[:, t]` is the target of
|
| 10 |
+
position t. Head k is supervised on its first `T - k` positions with target `labels[:, k:]`; NTP is
|
| 11 |
+
head 0 with target `labels`. This is NOT HF's convention (`ForCausalLMLoss` shifts internally); we
|
| 12 |
+
shift in the collator, so every input position is supervised.
|
| 13 |
+
|
| 14 |
+
⚠ Teacher forcing and supervision read DIFFERENT sequences: the sequential forces depth k from
|
| 15 |
+
`input_ids[:, k:]`, supervision comes from `labels`. They coincide in pretraining, which is the only
|
| 16 |
+
reason one tensor ever sufficed — under SFT `labels` is IGNORE_INDEX over the prompt and embedding
|
| 17 |
+
-100 would crash.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import importlib
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
import torch.nn.functional as F
|
| 24 |
+
|
| 25 |
+
IGNORE_INDEX = -100
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def direct_kl_loss_sum(student_logits, teacher_logits_aligned, labels, lam=1.0, ignore_index=IGNORE_INDEX):
|
| 29 |
+
"""Forward KL D(q‖p) against the detached verifier, summed over non-ignored positions."""
|
| 30 |
+
# torch's KL has no ignore_index (only cross_entropy does), so the mask is hand-rolled.
|
| 31 |
+
mask = (labels != ignore_index).unsqueeze(-1)
|
| 32 |
+
hi = lambda t: t.float() if t.dtype in (torch.float16, torch.bfloat16) else t # never DOWNcast fp64
|
| 33 |
+
logq = F.log_softmax(hi(teacher_logits_aligned), dim=-1).detach()
|
| 34 |
+
q = logq.exp()
|
| 35 |
+
logp = F.log_softmax(hi(student_logits), dim=-1)
|
| 36 |
+
kl = q * (logq - logp) # [..., V]
|
| 37 |
+
return lam * (kl * mask).sum()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _ce_mean(logits, target, ignore_index=IGNORE_INDEX):
|
| 41 |
+
if logits.dtype in (torch.float16, torch.bfloat16):
|
| 42 |
+
logits = logits.float()
|
| 43 |
+
return F.cross_entropy(logits.reshape(-1, logits.size(-1)), target.reshape(-1), ignore_index=ignore_index)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
_LIGER_FLCE = None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@torch.compiler.disable
|
| 50 |
+
def _liger_flce(W, z, y, ignore_index):
|
| 51 |
+
# The disable is required: dynamo cannot trace Liger's autograd Function under bf16 autocast
|
| 52 |
+
# (its fused addmm(out_dtype=fp32) fails fake-tensor propagation). Costs a graph break per CE
|
| 53 |
+
# call, not the kernel.
|
| 54 |
+
global _LIGER_FLCE
|
| 55 |
+
if _LIGER_FLCE is None:
|
| 56 |
+
# importlib, not a literal `from liger_kernel...` line: liger is OPTIONAL, but HF's
|
| 57 |
+
# trust_remote_code loader statically regexes every import in a vendored modelling file
|
| 58 |
+
# and refuses to load unless each one is installed. A real import statement here would
|
| 59 |
+
# make an optional dependency mandatory for anyone loading this model from the Hub.
|
| 60 |
+
mod = importlib.import_module("liger_kernel.transformers.fused_linear_cross_entropy")
|
| 61 |
+
_LIGER_FLCE = mod.LigerFusedLinearCrossEntropyLoss(ignore_index=ignore_index, reduction="mean")
|
| 62 |
+
return _LIGER_FLCE(W, z.reshape(-1, z.size(-1)), y.reshape(-1))
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def ce_loss(z, W, y, ce_kernel, ignore_index=IGNORE_INDEX):
|
| 66 |
+
"""Mean per-token CE against `W`, for any head. `naive` materializes `[*, V]` logits; `liger`
|
| 67 |
+
streams over the vocabulary and never does, which is the dominant memory term at large V."""
|
| 68 |
+
if ce_kernel == "liger":
|
| 69 |
+
return _liger_flce(W, z, y, ignore_index)
|
| 70 |
+
return _ce_mean(F.linear(z, W), y, ignore_index)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _direct_kl_loss(model, hiddens, h_ntp, labels, W, mtp_weight, ce_kernel):
|
| 74 |
+
"""Full-KD ablation: the KL gradient reaches the trunk AND the shared `W`, where soft_mtp
|
| 75 |
+
recovers it into the trunk only and leaves `W` the hard push. Materializes `[B, T, V]` softmaxes.
|
| 76 |
+
|
| 77 |
+
Logging units stay cross-arm comparable: `mtp_heads` is the counterfactual hard CE (what the
|
| 78 |
+
other objectives report), `mtp_heads_kl` is the KL that actually enters `total`.
|
| 79 |
+
"""
|
| 80 |
+
ntp_hidden = h_ntp
|
| 81 |
+
ntp_loss = ce_loss(ntp_hidden, W, labels, ce_kernel)
|
| 82 |
+
parts = {"ntp": ntp_loss.detach(), "mtp_heads": [], "mtp_heads_kl": []}
|
| 83 |
+
hi = lambda t: t.float() if t.dtype in (torch.float16, torch.bfloat16) else t
|
| 84 |
+
verifier_logits = (hi(ntp_hidden) @ hi(W).t()).detach()
|
| 85 |
+
head_terms = []
|
| 86 |
+
for head in range(1, model.n_heads_total):
|
| 87 |
+
head_targets = labels[:, head:]
|
| 88 |
+
head_span = head_targets.size(1)
|
| 89 |
+
head_hidden = model.final_norm(hiddens[head], head)[:, :head_span]
|
| 90 |
+
student_logits = hi(head_hidden) @ hi(W).t()
|
| 91 |
+
# The teacher is a plain crop: head `head` at t and NTP at t+head are supervised under the
|
| 92 |
+
# SAME condition, so every student position has a real teacher and there is no tail to mask.
|
| 93 |
+
teacher_logits = verifier_logits[:, head:head + head_span]
|
| 94 |
+
n_head_supervised = (head_targets != IGNORE_INDEX).sum().clamp(min=1)
|
| 95 |
+
head_terms.append(direct_kl_loss_sum(student_logits, teacher_logits, head_targets)
|
| 96 |
+
/ n_head_supervised)
|
| 97 |
+
parts["mtp_heads"].append(_ce_mean(student_logits.detach(), head_targets).detach())
|
| 98 |
+
parts["mtp_heads_kl"].append(head_terms[-1].detach())
|
| 99 |
+
mtp_mean = torch.stack(head_terms).mean()
|
| 100 |
+
total = ntp_loss + mtp_weight * mtp_mean
|
| 101 |
+
parts["mtp"] = torch.stack(parts["mtp_heads"]).mean()
|
| 102 |
+
parts["total"] = total.detach()
|
| 103 |
+
return total, parts
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def combined_mtp_loss(model, H, input_ids, labels, objective=None, mtp_weight=None):
|
| 107 |
+
"""One scalar loss for ntp | hard_mtp | soft_mtp | direct_kl -> (total, parts).
|
| 108 |
+
|
| 109 |
+
hard_mtp and soft_mtp share one head loop: identical per-head CE against the same undetached
|
| 110 |
+
`W`, differing only in that soft_mtp also accumulates the KL-recovery term and runs the NTP CE on
|
| 111 |
+
the coupled hidden. The loss VALUE is the hard CE either way — the KL enters through the gradient.
|
| 112 |
+
|
| 113 |
+
`parts` (detached, for logging): {'ntp', 'mtp', 'mtp_heads', 'total'}; direct_kl adds
|
| 114 |
+
'mtp_heads_kl', the terms that actually enter `total`.
|
| 115 |
+
"""
|
| 116 |
+
cfg = model.config
|
| 117 |
+
objective = cfg.objective if objective is None else objective
|
| 118 |
+
mtp_weight = cfg.mtp_weight if mtp_weight is None else mtp_weight
|
| 119 |
+
ce_kernel = getattr(cfg, "ce_kernel", "naive")
|
| 120 |
+
W = model.lm_head.weight
|
| 121 |
+
|
| 122 |
+
hiddens = model.all_head_hiddens(H, input_ids) # [h0, ..., h_n_mtp] pre-norm
|
| 123 |
+
h_ntp = model.final_norm(hiddens[0], 0)
|
| 124 |
+
|
| 125 |
+
assert labels.shape[:2] == input_ids.shape[:2], (
|
| 126 |
+
f"labels must be PRE-SHIFTED and the same shape as input_ids (labels[:, t] = target of "
|
| 127 |
+
f"position t); got {tuple(labels.shape)} vs {tuple(input_ids.shape)}. HF's convention is the "
|
| 128 |
+
f"other one (unshifted labels, shifted inside the loss) -- collate with mtp_collate_dict."
|
| 129 |
+
)
|
| 130 |
+
# Guard, not a branch: with an empty head slice `mtp_mean` would average over fewer heads than
|
| 131 |
+
# `kl_recovery_scale` divides by, silently turning soft_mtp into a partial-removal arm.
|
| 132 |
+
assert labels.size(1) > model.n_heads_total - 1, (
|
| 133 |
+
f"seq_len ({labels.size(1)}) must exceed n_mtp ({model.n_heads_total - 1}): the deepest head "
|
| 134 |
+
f"would have no target."
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
if objective == "ntp" or cfg.n_mtp == 0:
|
| 138 |
+
ntp_loss = ce_loss(h_ntp, W, labels, ce_kernel)
|
| 139 |
+
return ntp_loss, {"ntp": ntp_loss.detach(), "mtp_heads": [],
|
| 140 |
+
"mtp": torch.zeros((), device=H.device), "total": ntp_loss.detach()}
|
| 141 |
+
|
| 142 |
+
if objective == "direct_kl":
|
| 143 |
+
return _direct_kl_loss(model, hiddens, h_ntp, labels, W, mtp_weight, ce_kernel)
|
| 144 |
+
|
| 145 |
+
# ---- shared head loop: hard_mtp | soft_mtp ---------------------------------------------------
|
| 146 |
+
is_soft = objective == "soft_mtp"
|
| 147 |
+
ntp_targets = labels
|
| 148 |
+
ntp_hidden = h_ntp
|
| 149 |
+
n_heads = model.n_heads_total - 1
|
| 150 |
+
|
| 151 |
+
# float64 BEFORE dividing: an int64/int64 tensor divide is float32 in torch, and a trailing
|
| 152 |
+
# .to() only re-rounds what was already lost.
|
| 153 |
+
n_ntp_supervised = ((ntp_targets != IGNORE_INDEX).sum().clamp(min=1).to(torch.float64)
|
| 154 |
+
if is_soft else None)
|
| 155 |
+
|
| 156 |
+
head_losses = []
|
| 157 |
+
for head in range(1, model.n_heads_total):
|
| 158 |
+
# Head k predicts the token k ahead, so head has seq_len-k supervised positions.
|
| 159 |
+
head_targets = labels[:, head:]
|
| 160 |
+
head_hidden = model.final_norm(hiddens[head], head)[:, :head_targets.size(1)]
|
| 161 |
+
head_losses.append(ce_loss(head_hidden, W, head_targets, ce_kernel))
|
| 162 |
+
if not is_soft:
|
| 163 |
+
continue
|
| 164 |
+
|
| 165 |
+
# straight-through gradient routing
|
| 166 |
+
# s_k undoes the NTP CE's 1/T mean and re-applies k-th MTP head's own
|
| 167 |
+
# 1/(T-k) mean and the 1/K head average, so the routed gradient
|
| 168 |
+
# lands at exactly the scale the MTP head's own CE gradient uses.
|
| 169 |
+
kl_recovery_scale = (mtp_weight * n_ntp_supervised
|
| 170 |
+
/ (n_heads * (head_targets != IGNORE_INDEX).sum().clamp(min=1).to(torch.float64))
|
| 171 |
+
).to(head_hidden.dtype)
|
| 172 |
+
|
| 173 |
+
# ntp_hidden is numerically unchanged; only its backward changes
|
| 174 |
+
ntp_hidden[:, head:] = ntp_hidden[:, head:] + kl_recovery_scale * (head_hidden.detach() - head_hidden)
|
| 175 |
+
|
| 176 |
+
ntp_loss = ce_loss(ntp_hidden, W, ntp_targets, ce_kernel)
|
| 177 |
+
mtp_mean = torch.stack(head_losses).mean()
|
| 178 |
+
total = ntp_loss + mtp_weight * mtp_mean
|
| 179 |
+
return total, {"ntp": ntp_loss.detach(), "mtp_heads": [m.detach() for m in head_losses],
|
| 180 |
+
"mtp": mtp_mean.detach(), "total": total.detach()}
|
merges.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MTP model — parallel (Meta-style) and sequential (DeepSeek-style) heads in one class.
|
| 2 |
+
|
| 3 |
+
HF `PreTrainedModel` over the verified pure-PyTorch `Block` (no FLA/triton), so it runs under
|
| 4 |
+
the HF `Trainer` with a single combined backward and also imports/runs locally. `forward`
|
| 5 |
+
returns the combined MTP loss (model-returns-loss pattern) so `Trainer` picks it up directly.
|
| 6 |
+
|
| 7 |
+
trunk: num_hidden_layers Blocks -> H (pre-norm); NTP reads H directly.
|
| 8 |
+
meta: head j = mtp_head_layers Block(s) on H (parallel) -> predicts x_{t+1+j}
|
| 9 |
+
deepseek: depth k = block_k(proj_k[RMSNorm(h^{k-1}); RMSNorm(emb(x_{t+k}))]) (sequential)
|
| 10 |
+
shared final RMSNorm + lm_head (so every head goes through the same W -> recovery identity).
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from typing import Optional
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
import torch.nn as nn
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
from transformers import AutoConfig, AutoModelForCausalLM, DynamicCache, GenerationMixin, PreTrainedModel
|
| 20 |
+
from transformers.modeling_outputs import CausalLMOutputWithPast
|
| 21 |
+
|
| 22 |
+
from .blocks import RMSNorm, Block, build_causal_mask, build_rope_cache
|
| 23 |
+
from .config import HFMTPConfig
|
| 24 |
+
from .loss import combined_mtp_loss
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class MTPCausalLMOutput(CausalLMOutputWithPast):
|
| 29 |
+
"""`CausalLMOutputWithPast` + the NAMED per-step train-loss breakdown (detached tensors), so the
|
| 30 |
+
Trainer logs by field name instead of unpacking a positional tensor. All fields are plain tensors
|
| 31 |
+
(pytree-safe under whole-model torch.compile / DDP). Units are cross-arm-consistent: `mtp_loss`
|
| 32 |
+
is hard CE on EVERY arm (for direct_kl the counterfactual CE); `kl_loss` is the KL training loss,
|
| 33 |
+
present only on direct_kl (there `loss = ntp_loss + w*mean(kl_loss)`, else `+ w*mean(mtp_loss)`).
|
| 34 |
+
|
| 35 |
+
Logging convention (implemented in `MTPTrainer.log`): a scalar field logs under its own name; a
|
| 36 |
+
per-head vector field `<x>_loss` ([n_mtp]) logs its MEAN under `<x>_loss` plus `<x>_h{j}_loss`
|
| 37 |
+
per head (1-based)."""
|
| 38 |
+
ntp_loss: Optional[torch.FloatTensor] = None # scalar: NTP CE
|
| 39 |
+
mtp_loss: Optional[torch.FloatTensor] = None # [n_mtp]: per-head hard CE (MTP arms)
|
| 40 |
+
kl_loss: Optional[torch.FloatTensor] = None # [n_mtp]: per-head KL (direct_kl only)
|
| 41 |
+
|
| 42 |
+
@classmethod
|
| 43 |
+
def breakdown_fields(cls):
|
| 44 |
+
"""The breakdown = exactly the fields this class ADDS over `CausalLMOutputWithPast` —
|
| 45 |
+
derived, not hand-listed, so adding a field here is the only change ever needed."""
|
| 46 |
+
from dataclasses import fields
|
| 47 |
+
base = {f.name for f in fields(CausalLMOutputWithPast)}
|
| 48 |
+
return tuple(f.name for f in fields(cls) if f.name not in base)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class HFMTPPreTrainedModel(PreTrainedModel):
|
| 52 |
+
config_class = HFMTPConfig
|
| 53 |
+
_no_split_modules = ["Block"]
|
| 54 |
+
|
| 55 |
+
def _init_weights(self, module):
|
| 56 |
+
std = 0.02
|
| 57 |
+
if isinstance(module, nn.Linear):
|
| 58 |
+
nn.init.normal_(module.weight, std=std)
|
| 59 |
+
if module.bias is not None:
|
| 60 |
+
nn.init.zeros_(module.bias)
|
| 61 |
+
elif isinstance(module, nn.Embedding):
|
| 62 |
+
nn.init.normal_(module.weight, std=std)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class HFMTPForCausalLM(HFMTPPreTrainedModel, GenerationMixin):
|
| 66 |
+
_tied_weights_keys = ["lm_head.weight"]
|
| 67 |
+
|
| 68 |
+
def __init__(self, config: HFMTPConfig):
|
| 69 |
+
super().__init__(config)
|
| 70 |
+
Blk = Block
|
| 71 |
+
self.embed = nn.Embedding(config.vocab_size, config.hidden_size)
|
| 72 |
+
self.trunk = nn.ModuleList(Blk(config) for _ in range(config.num_hidden_layers))
|
| 73 |
+
D, base = config.n_mtp, config.num_hidden_layers
|
| 74 |
+
if config.mtp_arch == "meta":
|
| 75 |
+
self.mtp_heads = nn.ModuleList(
|
| 76 |
+
nn.ModuleList(Blk(config) for _ in range(config.mtp_head_layers)) for _ in range(D)
|
| 77 |
+
)
|
| 78 |
+
else: # deepseek sequential
|
| 79 |
+
self.proj = nn.ModuleList(nn.Linear(2 * config.hidden_size, config.hidden_size, bias=False) for _ in range(D))
|
| 80 |
+
self.norms_h = nn.ModuleList(RMSNorm(config.hidden_size, config.norm_eps) for _ in range(D))
|
| 81 |
+
self.norms_e = nn.ModuleList(RMSNorm(config.hidden_size, config.norm_eps) for _ in range(D))
|
| 82 |
+
self.mtp_blocks = nn.ModuleList(
|
| 83 |
+
nn.ModuleList(Blk(config) for _ in range(config.mtp_head_layers)) for _ in range(D)
|
| 84 |
+
)
|
| 85 |
+
self.norm = RMSNorm(config.hidden_size, config.norm_eps) # NTP (head 0) final norm; also the
|
| 86 |
+
# shared final norm for the MTP heads UNLESS mtp_per_head_norm (then each head has its own, below —
|
| 87 |
+
# the DeepSeek-faithful `shared_head.norm`; the lm_head weight stays shared either way).
|
| 88 |
+
if getattr(config, "mtp_per_head_norm", False):
|
| 89 |
+
self.mtp_final_norms = nn.ModuleList(
|
| 90 |
+
RMSNorm(config.hidden_size, config.norm_eps) for _ in range(D))
|
| 91 |
+
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
| 92 |
+
self._rope = None
|
| 93 |
+
self.post_init()
|
| 94 |
+
|
| 95 |
+
# --- HF plumbing ---
|
| 96 |
+
def get_input_embeddings(self):
|
| 97 |
+
return self.embed
|
| 98 |
+
|
| 99 |
+
def set_input_embeddings(self, value):
|
| 100 |
+
self.embed = value
|
| 101 |
+
|
| 102 |
+
def get_output_embeddings(self):
|
| 103 |
+
return self.lm_head
|
| 104 |
+
|
| 105 |
+
def set_output_embeddings(self, value):
|
| 106 |
+
self.lm_head = value
|
| 107 |
+
|
| 108 |
+
# --- core ---
|
| 109 |
+
@property
|
| 110 |
+
def n_heads_total(self):
|
| 111 |
+
return 1 + self.config.n_mtp
|
| 112 |
+
|
| 113 |
+
def _rope_cache(self, T, device, dtype):
|
| 114 |
+
"""Cache the fp32 table and cast on return, as HF does (it buffers only the dtype-stable
|
| 115 |
+
`inv_freq` and ends with `cos.to(dtype=x.dtype)`). Caching the CAST result went stale across a
|
| 116 |
+
dtype change -- the check below sees length and device but not dtype -- so an fp32 forward then
|
| 117 |
+
`.to(bfloat16)` fed fp32 cos/sin to a bf16 model and SDPA raised on mixed q/k/v dtypes."""
|
| 118 |
+
if self._rope is None or self._rope[0].size(0) < T or self._rope[0].device != device:
|
| 119 |
+
hd = self.config.hidden_size // self.config.num_heads
|
| 120 |
+
self._rope = build_rope_cache(T, hd, self.config.rope_theta, device, torch.float32)
|
| 121 |
+
cos, sin = self._rope
|
| 122 |
+
return cos[:T].to(dtype), sin[:T].to(dtype)
|
| 123 |
+
|
| 124 |
+
def trunk_forward(self, input_ids, past_key_values=None, use_cache=False, cache_position=None,
|
| 125 |
+
attention_mask=None, position_ids=None, inputs_embeds=None):
|
| 126 |
+
if (input_ids is None) == (inputs_embeds is None):
|
| 127 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 128 |
+
x = self.embed(input_ids) if inputs_embeds is None else inputs_embeds
|
| 129 |
+
T = x.size(1)
|
| 130 |
+
if past_key_values is None and cache_position is None:
|
| 131 |
+
if attention_mask is not None and not bool(attention_mask.all()):
|
| 132 |
+
# padded cache-free batch (e.g. lm-eval generate_until with use_cache=False): the
|
| 133 |
+
# additive causal+padding mask replaces `is_causal`. The `.all()` skip mirrors HF's
|
| 134 |
+
# `allow_is_causal_skip` — an all-ones mask keeps the fast kernel. Eval-only branch
|
| 135 |
+
# (training never passes a mask), so the CPU sync is fine and compile never sees it.
|
| 136 |
+
cp = torch.arange(T, device=x.device)
|
| 137 |
+
attn_mask = build_causal_mask(cp, T, attention_mask, x.device, x.dtype)
|
| 138 |
+
if position_ids is None:
|
| 139 |
+
cos, sin = self._rope_cache(T, x.device, x.dtype)
|
| 140 |
+
else:
|
| 141 |
+
cos_all, sin_all = self._rope_cache(int(position_ids.max()) + 1, x.device, x.dtype)
|
| 142 |
+
cos, sin = cos_all[position_ids], sin_all[position_ids]
|
| 143 |
+
for block in self.trunk:
|
| 144 |
+
x = block(x, cos, sin, attention_mask=attn_mask)
|
| 145 |
+
return x
|
| 146 |
+
# training / cache-free fast path (compile-clean: contiguous [0:T] rope + is_causal SDPA)
|
| 147 |
+
cos, sin = self._rope_cache(T, x.device, x.dtype)
|
| 148 |
+
for block in self.trunk:
|
| 149 |
+
x = block(x, cos, sin)
|
| 150 |
+
return x # pre-norm trunk hidden H (NTP reads this)
|
| 151 |
+
# inference with a KV cache (eager): used by the cached self-spec decoders + self-spec eval.
|
| 152 |
+
if use_cache and past_key_values is None:
|
| 153 |
+
past_key_values = DynamicCache()
|
| 154 |
+
if cache_position is None:
|
| 155 |
+
past = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 156 |
+
cache_position = torch.arange(past, past + T, device=x.device)
|
| 157 |
+
# RoPE: per-row `position_ids` when given (HF convention — generate() derives them from the
|
| 158 |
+
# attention mask, so left-padded rows count 0..n-1 on real tokens), else the batch-shared
|
| 159 |
+
# absolute `cache_position` (equivalent up to a uniform shift, which relative RoPE cancels).
|
| 160 |
+
if position_ids is not None:
|
| 161 |
+
cos_all, sin_all = self._rope_cache(int(position_ids.max()) + 1, x.device, x.dtype)
|
| 162 |
+
cos, sin = cos_all[position_ids], sin_all[position_ids]
|
| 163 |
+
else:
|
| 164 |
+
cos_all, sin_all = self._rope_cache(int(cache_position.max()) + 1, x.device, x.dtype)
|
| 165 |
+
cos, sin = cos_all[cache_position], sin_all[cache_position]
|
| 166 |
+
# Build the additive causal+padding mask ONCE and share it across layers — HF's split
|
| 167 |
+
# (DeepseekV3Model calls create_causal_mask; the attention modules only consume it). Per-layer
|
| 168 |
+
# construction would reallocate the same [B,1,T,k_len] tensor num_hidden_layers times.
|
| 169 |
+
k_len = int(cache_position.max()) + 1
|
| 170 |
+
attn_mask = build_causal_mask(cache_position, k_len, attention_mask, x.device, x.dtype)
|
| 171 |
+
for i, block in enumerate(self.trunk):
|
| 172 |
+
x = block(x, cos, sin, past_key_values, i, cache_position, attention_mask=attn_mask)
|
| 173 |
+
return x
|
| 174 |
+
|
| 175 |
+
def _meta_head(self, H, j):
|
| 176 |
+
if j == 0:
|
| 177 |
+
return H
|
| 178 |
+
cos, sin = self._rope_cache(H.size(1), H.device, H.dtype)
|
| 179 |
+
x = H
|
| 180 |
+
for block in self.mtp_heads[j - 1]:
|
| 181 |
+
x = block(x, cos, sin)
|
| 182 |
+
return x
|
| 183 |
+
|
| 184 |
+
def _dsmtp_layer(self, h_prev, token_ids, depth,
|
| 185 |
+
past_key_values=None, cache_position=None, cache_readonly=False):
|
| 186 |
+
"""Sequential depth `depth`. `h_prev` is h^{depth-1}; `token_ids[t]` is the teacher-forced token
|
| 187 |
+
x_{t+depth}.
|
| 188 |
+
|
| 189 |
+
INFERENCE KV CACHE (optional; the training path is untouched and bit-identical). With
|
| 190 |
+
`past_key_values`/`cache_position` the depth's blocks attend over the cached prefix, so a
|
| 191 |
+
drafter can extend the sequential by a few positions per round instead of re-running it over the
|
| 192 |
+
whole sequence. Mirrors `trunk_forward`'s cached branch: rope at ABSOLUTE positions, one
|
| 193 |
+
`layer_idx` per block within THIS depth's own cache.
|
| 194 |
+
|
| 195 |
+
`cache_readonly=True` attends against the cache without writing. The caller owns the split:
|
| 196 |
+
depth d at position t consumes x_{t+d}, so positions with t+d <= (last confirmed index) are
|
| 197 |
+
CONFIRMED and may be written (append-only, monotone, no crop -- the cache stays contiguous),
|
| 198 |
+
while the trailing positions consume unconfirmed DRAFTS and must be read-only or a rejected
|
| 199 |
+
draft poisons later draft distributions. See
|
| 200 |
+
`decoding/sequential.py`.
|
| 201 |
+
"""
|
| 202 |
+
i = depth - 1
|
| 203 |
+
e = self.embed(token_ids.clamp(min=0))
|
| 204 |
+
h = self.proj[i](torch.cat((self.norms_h[i](h_prev), self.norms_e[i](e)), dim=-1))
|
| 205 |
+
# Carrier-precision parity: under bf16 autocast the proj emits bf16 while the trunk carrier
|
| 206 |
+
# h_prev is fp32, an asymmetry no production stack has. Match the carrier rather than forcing
|
| 207 |
+
# .float() — under a weight-cast bf16 model a forced fp32 would crash the block GEMMs.
|
| 208 |
+
h = h.to(h_prev.dtype)
|
| 209 |
+
if past_key_values is None and cache_position is None:
|
| 210 |
+
# training / cache-free fast path — UNCHANGED (contiguous [0:T] rope + is_causal SDPA)
|
| 211 |
+
cos, sin = self._rope_cache(h.size(1), h.device, h.dtype)
|
| 212 |
+
for block in self.mtp_blocks[i]:
|
| 213 |
+
h = block(h, cos, sin)
|
| 214 |
+
else:
|
| 215 |
+
past = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 216 |
+
if cache_position is None:
|
| 217 |
+
cache_position = torch.arange(past, past + h.size(1), device=h.device)
|
| 218 |
+
cos_all, sin_all = self._rope_cache(int(cache_position.max()) + 1, h.device, h.dtype)
|
| 219 |
+
cos, sin = cos_all[cache_position], sin_all[cache_position] # rope at ABSOLUTE positions
|
| 220 |
+
# Causal mask built once and shared by this depth's blocks. k_len is the post-update key
|
| 221 |
+
# width `past + T`, NOT cache_position.max()+1 — the latter under-counts for a read-only
|
| 222 |
+
# overlap chunk, whose new keys duplicate already-cached positions.
|
| 223 |
+
attn_mask = build_causal_mask(cache_position, past + h.size(1), None, h.device, h.dtype)
|
| 224 |
+
for bi, block in enumerate(self.mtp_blocks[i]):
|
| 225 |
+
h = block(h, cos, sin, past_key_values, bi, cache_position, cache_readonly, attn_mask)
|
| 226 |
+
return h
|
| 227 |
+
|
| 228 |
+
# Public alias (the cache-free sequential self-spec decoder calls `model.dsmtp_layer(...)`, matching
|
| 229 |
+
# the FLA `dsmtp_transformer` interface).
|
| 230 |
+
def dsmtp_layer(self, h_prev, token_ids, depth,
|
| 231 |
+
past_key_values=None, cache_position=None, cache_readonly=False):
|
| 232 |
+
return self._dsmtp_layer(h_prev, token_ids, depth,
|
| 233 |
+
past_key_values=past_key_values, cache_position=cache_position,
|
| 234 |
+
cache_readonly=cache_readonly)
|
| 235 |
+
|
| 236 |
+
def head_forward(self, H, j):
|
| 237 |
+
"""Pre-norm hidden of head j (0 = NTP trunk). Matches the FLA model interface so
|
| 238 |
+
the shared `metrics.distill.evaluate_distill` / `trunk_grad_agreement` run on HF too.
|
| 239 |
+
DeepSeek depth j needs the sequential context -> use `all_head_hiddens` instead there."""
|
| 240 |
+
if self.config.mtp_arch == "meta":
|
| 241 |
+
return self._meta_head(H, j)
|
| 242 |
+
raise NotImplementedError("DeepSeek head_forward: use all_head_hiddens(H, labels)")
|
| 243 |
+
|
| 244 |
+
def all_head_hiddens(self, H, input_ids=None):
|
| 245 |
+
"""Pre-norm hidden per head [h^0, ..., h^{n_mtp}]. Meta: parallel (input_ids unused).
|
| 246 |
+
|
| 247 |
+
DeepSeek: teacher-forced sequential. Depth k consumes the INPUT TOKEN x_{t+k}, i.e.
|
| 248 |
+
`input_ids[:, k:]`, so it is defined for t in [0, T-k) and h^k is CROPPED to T-k positions --
|
| 249 |
+
exactly the span on which head k is supervised (it predicts x_{t+1+k}).
|
| 250 |
+
|
| 251 |
+
⚠ TEACHER FORCING READS INPUTS, NOT LABELS. These are the same sequence only in
|
| 252 |
+
pretraining. Under supervised fine-tuning `labels` is IGNORE_INDEX over the prompt, and
|
| 253 |
+
embedding -100 would crash -- so the sequential must never be fed labels. Nothing is read past the
|
| 254 |
+
input, so there is no padded target matrix and no mask.
|
| 255 |
+
|
| 256 |
+
The crop is per-depth and the loop is unrolled at trace time, so every slice length is a
|
| 257 |
+
compile-time constant -- no dynamic shapes, no recompilation.
|
| 258 |
+
"""
|
| 259 |
+
if self.config.mtp_arch == "meta":
|
| 260 |
+
return [self._meta_head(H, j) for j in range(self.n_heads_total)]
|
| 261 |
+
T = input_ids.size(1)
|
| 262 |
+
outs, h_prev = [H], H
|
| 263 |
+
for k in range(1, self.n_heads_total):
|
| 264 |
+
L = T - k # positions whose teacher token is in-window
|
| 265 |
+
if L <= 0:
|
| 266 |
+
break
|
| 267 |
+
h_prev = self._dsmtp_layer(h_prev[:, :L], input_ids[:, k:k + L], k)
|
| 268 |
+
outs.append(h_prev)
|
| 269 |
+
return outs
|
| 270 |
+
|
| 271 |
+
def final_norm(self, hidden, head_idx=0):
|
| 272 |
+
"""Final RMSNorm before the shared lm_head. head_idx 0 = NTP -> self.norm. With
|
| 273 |
+
mtp_per_head_norm, head_idx k>=1 uses the head's OWN norm (DeepSeek `shared_head.norm`);
|
| 274 |
+
otherwise every head reuses self.norm (a NO-OP threading when the flag is off -> callers can
|
| 275 |
+
always pass the index and stay bit-identical)."""
|
| 276 |
+
if head_idx == 0 or not getattr(self.config, "mtp_per_head_norm", False):
|
| 277 |
+
return self.norm(hidden)
|
| 278 |
+
return self.mtp_final_norms[head_idx - 1](hidden)
|
| 279 |
+
|
| 280 |
+
def logits(self, hidden, head_idx=0):
|
| 281 |
+
return F.linear(self.final_norm(hidden, head_idx), self.lm_head.weight)
|
| 282 |
+
|
| 283 |
+
def forward(
|
| 284 |
+
self,
|
| 285 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 286 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 287 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 288 |
+
past_key_values=None,
|
| 289 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 290 |
+
labels: Optional[torch.LongTensor] = None,
|
| 291 |
+
use_cache: Optional[bool] = None,
|
| 292 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 293 |
+
logits_to_keep: "int | torch.Tensor" = 0,
|
| 294 |
+
return_all_heads: Optional[bool] = None,
|
| 295 |
+
**kwargs,
|
| 296 |
+
) -> MTPCausalLMOutput:
|
| 297 |
+
"""Signature follows `DeepseekV3ForCausalLM.forward` (HF `modeling_deepseek_v3.py`): HF
|
| 298 |
+
argument order and names (`position_ids`, `inputs_embeds`, int-or-tensor `logits_to_keep`),
|
| 299 |
+
and `past_key_values` returned on the output.
|
| 300 |
+
|
| 301 |
+
⚠ `position_ids` in this signature is BEHAVIOUR, not decoration.
|
| 302 |
+
`GenerationMixin.prepare_inputs_for_generation` inspects the signature and, finding the name,
|
| 303 |
+
derives per-row positions as `attention_mask.cumsum(-1) - 1`, so a left-padded row gets RoPE
|
| 304 |
+
0..n-1 on its real tokens. That makes batched and unbatched generation exactly equal.
|
| 305 |
+
|
| 306 |
+
⚠ The cache args are load-bearing. If they fall into `**kwargs` and get dropped while
|
| 307 |
+
`trunk_forward` runs cache-free, `generate()` (which defaults to `use_cache=True`) feeds only
|
| 308 |
+
the newest token from step 2 on, believing the prefix is cached — the model then sees one
|
| 309 |
+
token with no context and emits degenerate repetition after a plausible first token.
|
| 310 |
+
|
| 311 |
+
The cache is created here, not in `trunk_forward`, only so we hold a reference to return;
|
| 312 |
+
`Attention.forward` is what appends to it, in place, so returning the same object is correct.
|
| 313 |
+
|
| 314 |
+
Training is untouched: with no cache or positions, `trunk_forward` takes its compile-clean
|
| 315 |
+
`is_causal` fast path.
|
| 316 |
+
"""
|
| 317 |
+
if (labels is not None or return_all_heads) and input_ids is None:
|
| 318 |
+
raise ValueError("MTP paths teacher-force from token ids: `labels`/`return_all_heads` "
|
| 319 |
+
"require `input_ids`, not `inputs_embeds`")
|
| 320 |
+
if use_cache and past_key_values is None:
|
| 321 |
+
past_key_values = DynamicCache()
|
| 322 |
+
H = self.trunk_forward(input_ids, past_key_values=past_key_values, use_cache=use_cache,
|
| 323 |
+
cache_position=cache_position, attention_mask=attention_mask,
|
| 324 |
+
position_ids=position_ids, inputs_embeds=inputs_embeds)
|
| 325 |
+
if return_all_heads: # eval/distill: teacher-forced all-head logits [B, T-n_mtp, 1+n_mtp, V]
|
| 326 |
+
# Head j is supervised on T-j positions, so the sequential returns RAGGED hiddens. Stacking
|
| 327 |
+
# needs a rectangle -> take the common prefix (the deepest head's span, T-n_mtp). Nothing
|
| 328 |
+
# meaningful is dropped: the tail positions this omits are exactly the ones no deep head
|
| 329 |
+
# can predict, and every consumer compares heads AT THE SAME position.
|
| 330 |
+
hs = self.all_head_hiddens(H, input_ids)
|
| 331 |
+
n_pos = min(h.size(1) for h in hs)
|
| 332 |
+
stacked = torch.stack([self.logits(h[:, :n_pos], j) for j, h in enumerate(hs)], dim=2)
|
| 333 |
+
return MTPCausalLMOutput(logits=stacked)
|
| 334 |
+
if labels is not None: # training: combined MTP loss, one backward (full logits omitted to save mem)
|
| 335 |
+
total, parts = combined_mtp_loss(self, H, input_ids, labels)
|
| 336 |
+
# NAMED detached breakdown on the output (see MTPCausalLMOutput): the Trainer logs by field
|
| 337 |
+
# name — no positional packing. `logits` stays None during training (full logits are
|
| 338 |
+
# deliberately not materialized to save memory; eval uses return_all_heads).
|
| 339 |
+
heads = parts["mtp_heads"]
|
| 340 |
+
kl_heads = parts.get("mtp_heads_kl") # direct_kl only
|
| 341 |
+
return MTPCausalLMOutput(
|
| 342 |
+
loss=total,
|
| 343 |
+
ntp_loss=parts["ntp"],
|
| 344 |
+
mtp_loss=torch.stack(heads) if heads else None,
|
| 345 |
+
kl_loss=torch.stack(kl_heads) if kl_heads else None,
|
| 346 |
+
)
|
| 347 |
+
# `logits_to_keep` (DeepSeek-V3 / Llama convention): generation needs only the last position,
|
| 348 |
+
# and at V=50304 a 900-token prefill would otherwise materialise ~181 MB of unused fp32 logits.
|
| 349 |
+
# HF's exact form: int -> trailing slice (0 -> slice(0, None) == everything); Tensor -> fancy
|
| 350 |
+
# index of explicit positions (`DeepseekV3ForCausalLM.slice_indices`).
|
| 351 |
+
sl = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 352 |
+
return MTPCausalLMOutput(logits=self.logits(H[:, sl, :]), past_key_values=past_key_values)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
try:
|
| 356 |
+
AutoConfig.register(HFMTPConfig.model_type, HFMTPConfig)
|
| 357 |
+
AutoModelForCausalLM.register(HFMTPConfig, HFMTPForCausalLM)
|
| 358 |
+
except ValueError:
|
| 359 |
+
pass
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9fd2151603c308bc0b7f10b3d1c695e3a96a6799783592fa26376ff32f6d2ac2
|
| 3 |
+
size 1427681336
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"bos_token": "<|endoftext|>",
|
| 4 |
+
"clean_up_tokenization_spaces": false,
|
| 5 |
+
"eos_token": "<|endoftext|>",
|
| 6 |
+
"model_max_length": 2048,
|
| 7 |
+
"tokenizer_class": "GPT2Tokenizer",
|
| 8 |
+
"unk_token": "<|endoftext|>"
|
| 9 |
+
}
|
vocab.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|