Skip to content

Commit af7ca0e

Browse files
committed
feat(v4): ROI 2 — SequentialMTPHead (DeepSeek-V3 depth-D MTP)
Side-by-side V3-faithful Multi-Token-Prediction head. The existing MinimalMTPHead reuses one shared block recursively across depths; V3 ships D sequential transformer-style blocks, one per future depth, each with its own RMSNorms + projection + transformer kernel, while sharing the model's token embedding and lm_head. cppmega_v4/nn/mtp_v4.py: - SequentialMTPDepthBlock: per-depth (hidden_norm, embedding_norm, proj, transformer, output_norm). Reuses MinimalMTPSharedBlock as the transformer primitive to keep the shape contract identical. - SequentialMTPHead: D distinct depth blocks; same __call__ and loss surface as MinimalMTPHead so the training loop is unchanged. - attach_sequential_mtp_head(model, config): mirror of attach_mtp_head from cppmega_mlx.training.mtp but installs the V3 variant. CUDA FastMTP detach semantics preserved: - hidden_states enters under mx.stop_gradient (no backprop into main decoder) - lm_head.weight is detached during per-depth logits projection (reuses existing _lm_head_with_stopped_weight helper) 16 new tests in tests/v4/test_mtp_v4.py: - Depth-block forward shape + hidden-size validation - Head owns D distinct module instances (id-based check) - Zero-depth returns empty tuple - attach aliases token_embedding + lm_head (no copy) - Forward shape per depth, rank + shape rejection - Loss surface returns (mtp_loss, per_depth, depth_weights) - doc_ids threading with no NaN - Sequential has strictly more params than Minimal for same depth - Gradient isolation: backward on depth-0 hits only depth-0 block - stop_gradient invariant: zero grad through hidden_states - Gradient flows through shared token_embedding 115/115 regression tests green (v4 + existing MTP + extensions + checkpoint). Pipeline: Implementer -> Code Review (clean, single cppmega_mlx import) -> Perf Optimization (no issues — D-block compute is the intended architecture) -> Regression Tests.
1 parent 8353464 commit af7ca0e

2 files changed

Lines changed: 429 additions & 0 deletions

File tree

cppmega_v4/nn/mtp_v4.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""DeepSeek-V3-style Sequential Multi-Token-Prediction head plugin.
2+
3+
The existing ``MinimalMTPHead`` (``cppmega_mlx/training/mtp.py``) reuses one
4+
shared block recursively for every depth — a contracted reference. DeepSeek-V3
5+
ships D **sequential transformer-style blocks**, one per future-token depth,
6+
each with its own RMSNorms, projection, and transformer kernel, but sharing the
7+
model's token embedding and lm head.
8+
9+
This plugin lands the V3-faithful surface side-by-side without touching the
10+
existing ``MinimalMTPHead``.
11+
12+
Sharing contract (intentional):
13+
- ``token_embedding`` and ``lm_head`` are aliased to the model's existing
14+
instances (no copy) — gradients flow back through the shared parameters.
15+
- Each depth owns its own ``hidden_norm``, ``embedding_norm``, ``proj``,
16+
transformer kernel, and ``output_norm`` — D distinct module instances.
17+
18+
CUDA FastMTP detach semantics are preserved:
19+
- ``hidden_states`` enters under ``mx.stop_gradient`` (no backprop through
20+
the main decoder).
21+
- ``lm_head.weight`` is detached during the per-depth logits projection.
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import mlx.core as mx
27+
import mlx.nn as nn
28+
29+
from cppmega_mlx.training.mtp import (
30+
MinimalMTPSharedBlock,
31+
MTPLossConfig,
32+
_lm_head_with_stopped_weight,
33+
compute_weighted_mtp_loss,
34+
mtp_cross_entropy_from_logits,
35+
roll_and_mask_mtp_ids,
36+
roll_and_mask_mtp_labels,
37+
)
38+
39+
40+
class SequentialMTPDepthBlock(nn.Module):
41+
"""One MTP depth-block: per-depth norms + projection + transformer kernel."""
42+
43+
def __init__(self, hidden_size: int):
44+
super().__init__()
45+
if hidden_size <= 0:
46+
raise ValueError(f"hidden_size must be positive, got {hidden_size}")
47+
self.hidden_norm = nn.RMSNorm(hidden_size)
48+
self.embedding_norm = nn.RMSNorm(hidden_size)
49+
self.proj = nn.Linear(2 * hidden_size, hidden_size, bias=False)
50+
# Reuse the existing single-block kernel as the per-depth transformer
51+
# primitive. This keeps the shape contract identical to MinimalMTPHead.
52+
self.transformer = MinimalMTPSharedBlock(hidden_size)
53+
self.output_norm = nn.RMSNorm(hidden_size)
54+
55+
def __call__(self, hidden_states: mx.array, teacher_emb: mx.array) -> mx.array:
56+
h_mtp = self.proj(
57+
mx.concatenate(
58+
[self.hidden_norm(hidden_states), self.embedding_norm(teacher_emb)],
59+
axis=-1,
60+
)
61+
)
62+
return self.output_norm(self.transformer(h_mtp))
63+
64+
65+
class SequentialMTPHead(nn.Module):
66+
"""V3-faithful depth-D MTP head with D distinct per-depth blocks.
67+
68+
Public API mirrors ``MinimalMTPHead`` so the rest of the training loop is
69+
unchanged: ``__call__(hidden_states, target_tokens, ...)`` returns one
70+
logits tensor per depth, and ``loss(...)`` returns
71+
``(mtp_loss, per_depth_losses, depth_weights)``.
72+
"""
73+
74+
def __init__(
75+
self,
76+
token_embedding: nn.Embedding,
77+
lm_head: nn.Linear,
78+
*,
79+
config: MTPLossConfig | None = None,
80+
):
81+
super().__init__()
82+
self.token_embedding = token_embedding
83+
self.lm_head = lm_head
84+
self.config = config or MTPLossConfig()
85+
if self.config.depth < 0:
86+
raise ValueError("MTP depth must be non-negative")
87+
hidden_size = int(token_embedding.weight.shape[1])
88+
# D distinct depth blocks — this is the V3 contract.
89+
self.depth_blocks = [
90+
SequentialMTPDepthBlock(hidden_size) for _ in range(self.config.depth)
91+
]
92+
93+
def __call__(
94+
self,
95+
hidden_states: mx.array,
96+
target_tokens: mx.array,
97+
*,
98+
document_ids: mx.array | None = None,
99+
) -> tuple[mx.array, ...]:
100+
if hidden_states.ndim != 3:
101+
raise ValueError(
102+
f"hidden_states must be shaped (B, T, D), got {hidden_states.shape}"
103+
)
104+
if hidden_states.shape[:2] != target_tokens.shape:
105+
raise ValueError(
106+
f"hidden_states prefix shape {hidden_states.shape[:2]} must match "
107+
f"target_tokens {target_tokens.shape}"
108+
)
109+
if self.config.depth == 0:
110+
return ()
111+
112+
teacher_ids = roll_and_mask_mtp_ids(
113+
target_tokens,
114+
depth=self.config.depth,
115+
document_ids=document_ids,
116+
)
117+
logits_by_depth: list[mx.array] = []
118+
# CUDA FastMTP detach: main decoder states do not receive grad through MTP.
119+
h = mx.stop_gradient(hidden_states)
120+
for depth_idx, ids in enumerate(teacher_ids):
121+
teacher_emb = self.token_embedding(ids)
122+
h = self.depth_blocks[depth_idx](h, teacher_emb)
123+
logits_by_depth.append(_lm_head_with_stopped_weight(self.lm_head, h))
124+
return tuple(logits_by_depth)
125+
126+
def loss(
127+
self,
128+
hidden_states: mx.array,
129+
target_tokens: mx.array,
130+
*,
131+
document_ids: mx.array | None = None,
132+
) -> tuple[mx.array, tuple[mx.array, ...], mx.array]:
133+
labels = roll_and_mask_mtp_labels(
134+
target_tokens,
135+
depth=self.config.depth,
136+
ignore_index=self.config.ignore_index,
137+
document_ids=document_ids,
138+
)
139+
logits_by_depth = self(
140+
hidden_states,
141+
target_tokens,
142+
document_ids=document_ids,
143+
)
144+
per_depth = tuple(
145+
mtp_cross_entropy_from_logits(
146+
logits,
147+
label,
148+
ignore_index=self.config.ignore_index,
149+
)
150+
for logits, label in zip(logits_by_depth, labels, strict=True)
151+
)
152+
mtp_loss, depth_weights = compute_weighted_mtp_loss(
153+
per_depth,
154+
decay=self.config.decay,
155+
)
156+
return mtp_loss, per_depth, depth_weights
157+
158+
159+
def attach_sequential_mtp_head(
160+
model: nn.Module,
161+
*,
162+
config: MTPLossConfig | None = None,
163+
) -> SequentialMTPHead:
164+
"""Attach a persistent SequentialMTPHead to a model via direct aliasing.
165+
166+
Mirrors ``cppmega_mlx.training.mtp.attach_mtp_head`` but emits the V3
167+
sequential variant. Does not modify the model's class — only sets the
168+
``mtp_head`` attribute.
169+
"""
170+
token_embedding = getattr(model, "token_embedding", None)
171+
lm_head = getattr(model, "lm_head", None)
172+
if not isinstance(token_embedding, nn.Embedding):
173+
raise TypeError(
174+
"SequentialMTPHead requires model.token_embedding to be an nn.Embedding"
175+
)
176+
if not isinstance(lm_head, nn.Linear):
177+
raise TypeError("SequentialMTPHead requires model.lm_head to be an nn.Linear")
178+
179+
head = SequentialMTPHead(token_embedding, lm_head, config=config)
180+
setattr(model, "mtp_head", head)
181+
return head
182+
183+
184+
__all__ = [
185+
"SequentialMTPDepthBlock",
186+
"SequentialMTPHead",
187+
"attach_sequential_mtp_head",
188+
]

0 commit comments

Comments
 (0)