JaydenTeoh commited on
Commit
0334ed9
·
verified ·
1 Parent(s): be1eca4

Checkpoint from Multi-Token Prediction Done Right (arXiv:2610.38331)

Browse files
Files changed (12) hide show
  1. README.md +83 -0
  2. blocks.py +167 -0
  3. config.json +34 -0
  4. config.py +71 -0
  5. generation_config.json +4 -0
  6. loss.py +180 -0
  7. merges.txt +0 -0
  8. model.py +359 -0
  9. model.safetensors +3 -0
  10. tokenizer.json +0 -0
  11. tokenizer_config.json +9 -0
  12. 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