Skip to content

Commit d948cbc

Browse files
committed
feat(v4): UnifiedSuperblockV4 real factories — no more pass-through stubs
Replaced 7 pass-through lambdas in BLOCK_BUILDERS with real factories: - "gdn" → LinearAttentionBlock(LinearAttentionConfig(...)) (FLA-naive GDN; Path B/C/E dispatchable via env) - "kda" → KimiDeltaAttentionBlock(KimiDeltaAttentionConfig(...)) - "moe" → V4MoE(V4MoEConfig(d_model=...)) wrapped to expose .output (V4MoE returns MoEOutput dataclass) - "attention" → standard multi-head self-attention with causal mask + RMSNorm; o_proj zero-init for identity-at-init. Also the fallback for "mla" and "mla_absorb" until we land a full MLA block. - "lightning_indexer" → LightningIndexerFP8 wrapped as residual no-op (indexer's natural output is top-k indices, not a residual — real consumption happens inside CSA+HCA). Dispatch in UnifiedSuperblockV4.__call__ extended: - _DOC_ID_KW_KINDS = {"gdn", "kda"} — these accept document_ids as a keyword argument (their recurrent state needs the doc-reset signal). - Engram remains positional-token-ids + keyword-doc-ids (special-cased). Local imports inside each builder avoid circular-import issues at the unified_superblock_v4 module level. Tests (18 new in tests/v4/test_unified_superblock_real_factories.py): - 7 parametrized: each kind is no longer a pass-through lambda. - gdn / kda / moe / attention(×3 for mla aliases) builds and runs. - GDN block threads doc_ids → output differs across doc-boundary configurations (after perturbing o_proj, which is zero-init). - attention zero-init identity check (out == in via residual). - lightning_indexer residual identity (out == in by design). - Full V4 stack end-to-end (engram + gdn + kda + attention + nsa + csa_hca + moe + mlp) — runs forward and propagates gradients. scripts/v4_1b_parquet_smoke.py updated: per-kind layer count spread across all 7 attention-like kinds (gdn, kda, attention, nsa, csa_hca, moe, mlp) so the smoke now exercises the *whole* V4 cohort, not just the previously-non-stub subset. Smoke run (hidden=1024, L=12, T=256, B=1, 3 steps): loss decreases 11.18 → 11.02 → 10.96 (sanity check) fwd median 59 ms, total 91 ms, ~2 800 tokens/sec (was 10 191 tokens/sec on the engram+nsa+csa_hca+mlp-only subset, drop is expected — we now run 6× more block kinds, including attention/MoE compute that was being silently skipped before) v4 suite: 303 passed / 2 skipped.
1 parent 40a83ef commit d948cbc

3 files changed

Lines changed: 406 additions & 38 deletions

File tree

cppmega_v4/models/unified_superblock_v4.py

Lines changed: 144 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,29 +72,152 @@ def __call__(self, x):
7272

7373

7474
def _build_pass_through_unsupported(kind: str):
75-
"""Block kinds with circular imports or external dep — pass-through."""
75+
"""Last-resort no-op for kinds with no factory yet. Residual-only."""
7676
class _PassThrough(nn.Module):
7777
def __init__(self):
7878
super().__init__()
7979
self._kind = kind
8080
def __call__(self, x):
81-
return x # zero contribution, residual-only path
81+
return x # zero contribution
8282
return _PassThrough()
8383

8484

85+
def _build_gdn(hidden_size: int, params: dict) -> nn.Module:
86+
"""Real GatedDeltaNet block (Path A — Path B/C/E land via env override)."""
87+
from cppmega_v4.nn.linear_attention import ( # local import: avoids circular
88+
LinearAttentionBlock, LinearAttentionConfig,
89+
)
90+
# Defaults sensible for our 1B smoke configs.
91+
params.setdefault("num_heads", max(1, hidden_size // 64))
92+
params.setdefault("head_dim", hidden_size // params["num_heads"])
93+
cfg = LinearAttentionConfig(hidden_size=hidden_size, **params)
94+
return LinearAttentionBlock(cfg)
95+
96+
97+
def _build_kda(hidden_size: int, params: dict) -> nn.Module:
98+
"""Real Kimi Delta Attention block."""
99+
from cppmega_v4.nn.kimi_delta_attention import (
100+
KimiDeltaAttentionBlock, KimiDeltaAttentionConfig,
101+
)
102+
params.setdefault("num_heads", max(1, hidden_size // 64))
103+
params.setdefault("head_dim", hidden_size // params["num_heads"])
104+
cfg = KimiDeltaAttentionConfig(hidden_size=hidden_size, **params)
105+
return KimiDeltaAttentionBlock(cfg)
106+
107+
108+
def _build_moe(hidden_size: int, params: dict) -> nn.Module:
109+
"""Real V4 MoE. V4MoE returns MoEOutput — wrap to expose .output."""
110+
from cppmega_v4.nn.moe_v4 import V4MoE, V4MoEConfig
111+
112+
params.setdefault("expert_hidden_size", hidden_size * 4)
113+
cfg = V4MoEConfig(d_model=hidden_size, **params)
114+
inner = V4MoE(cfg)
115+
116+
class _MoEWrap(nn.Module):
117+
def __init__(self):
118+
super().__init__()
119+
self.moe = inner
120+
121+
def __call__(self, x):
122+
return self.moe(x).output
123+
124+
return _MoEWrap()
125+
126+
127+
def _build_attention(hidden_size: int, params: dict) -> nn.Module:
128+
"""Standard multi-head self-attention (causal). Used for `attention` and
129+
as the fallback for `mla` / `mla_absorb` until we land a full MLA block.
130+
"""
131+
num_heads = params.get("num_heads", max(1, hidden_size // 64))
132+
head_dim = params.get("head_dim", hidden_size // num_heads)
133+
norm_eps = params.get("norm_eps", 1e-6)
134+
135+
class _SelfAttn(nn.Module):
136+
def __init__(self):
137+
super().__init__()
138+
self.q_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
139+
self.k_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
140+
self.v_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
141+
self.o_proj = nn.Linear(num_heads * head_dim, hidden_size, bias=False)
142+
self.norm = nn.RMSNorm(hidden_size, eps=norm_eps)
143+
# Zero-init out so the block is identity at init.
144+
self.o_proj.weight = mx.zeros_like(self.o_proj.weight)
145+
146+
def __call__(self, x):
147+
B, S, _ = x.shape
148+
q = self.q_proj(x).reshape(B, S, num_heads, head_dim)
149+
k = self.k_proj(x).reshape(B, S, num_heads, head_dim)
150+
v = self.v_proj(x).reshape(B, S, num_heads, head_dim)
151+
q = mx.transpose(q, (0, 2, 1, 3))
152+
k = mx.transpose(k, (0, 2, 1, 3))
153+
v = mx.transpose(v, (0, 2, 1, 3))
154+
scale = head_dim ** -0.5
155+
scores = mx.matmul(q, mx.transpose(k, (0, 1, 3, 2))) * scale
156+
mask = mx.tril(mx.ones((S, S), dtype=mx.bool_))
157+
scores = mx.where(mask, scores, mx.full(scores.shape, -1e9,
158+
dtype=scores.dtype))
159+
w = mx.softmax(scores.astype(mx.float32), axis=-1).astype(scores.dtype)
160+
o = mx.matmul(w, v)
161+
o = mx.transpose(o, (0, 2, 1, 3)).reshape(B, S, num_heads * head_dim)
162+
return self.norm(self.o_proj(o))
163+
164+
return _SelfAttn()
165+
166+
167+
def _build_lightning_indexer(hidden_size: int, params: dict) -> nn.Module:
168+
"""LightningIndexer is a top-k helper, not a residual block. Wrap as a
169+
residual no-op for stack composition — the real callsite is inside
170+
CSA+HCA. RunTemplate users who want the indexer as an inline gate-pre
171+
pass get a configurable hidden-pass-through wrapper.
172+
"""
173+
from cppmega_v4.nn.lightning_indexer_fp8 import (
174+
LightningIndexerFP8, LightningIndexerFP8Config,
175+
)
176+
n_heads = params.get("n_heads", max(1, hidden_size // 64))
177+
cfg = LightningIndexerFP8Config(
178+
hidden_size=hidden_size,
179+
n_heads=n_heads,
180+
head_dim=params.get("head_dim", 32),
181+
rope_head_dim=params.get("rope_head_dim", 16),
182+
q_lora_rank=params.get("q_lora_rank", hidden_size),
183+
index_topk=params.get("index_topk", 64),
184+
fp8_blocks=params.get("fp8_blocks", True),
185+
)
186+
indexer = LightningIndexerFP8(cfg)
187+
188+
class _IndexerResidualNoOp(nn.Module):
189+
"""Residual pass-through wrapper for LightningIndexer.
190+
191+
Lightning Indexer's natural output is top-k indices (int32), not a
192+
residual. In a RunTemplate context, the block contributes zero
193+
delta — its presence in the stack signals that downstream
194+
CSA/HCA / sparse-MLA layers should consume the indexer's outputs.
195+
Real wiring lives in CSA+HCA's select_indices argument.
196+
"""
197+
def __init__(self):
198+
super().__init__()
199+
self.indexer = indexer
200+
201+
def __call__(self, x):
202+
return mx.zeros_like(x)
203+
204+
return _IndexerResidualNoOp()
205+
206+
85207
BLOCK_BUILDERS: dict[str, Callable[[int, dict], nn.Module]] = {
86208
"engram": _build_engram,
87209
"nsa": _build_nsa,
88210
"csa_hca": _build_csa_hca,
89211
"mlp": _build_mlp,
90-
# Pass-through for blocks that need optional deps not always in scope:
91-
"gdn": lambda h, p: _build_pass_through_unsupported("gdn"),
92-
"kda": lambda h, p: _build_pass_through_unsupported("kda"),
93-
"mla_absorb": lambda h, p: _build_pass_through_unsupported("mla_absorb"),
94-
"mla": lambda h, p: _build_pass_through_unsupported("mla"),
95-
"attention": lambda h, p: _build_pass_through_unsupported("attention"),
96-
"moe": lambda h, p: _build_pass_through_unsupported("moe"),
97-
"lightning_indexer": lambda h, p: _build_pass_through_unsupported("lightning_indexer"),
212+
"gdn": _build_gdn,
213+
"kda": _build_kda,
214+
"moe": _build_moe,
215+
"attention": _build_attention,
216+
# mla / mla_absorb fall back to standard attention until we land a
217+
# full MLA block (mla_absorb.py is a pure algebra module, not nn.Module).
218+
"mla": _build_attention,
219+
"mla_absorb": _build_attention,
220+
"lightning_indexer": _build_lightning_indexer,
98221
}
99222

100223

@@ -106,15 +229,19 @@ class _BuiltBlock:
106229
needs_token_ids: bool
107230

108231

232+
_DOC_ID_KW_KINDS = {"gdn", "kda"} # accept doc_ids via *kwargs
233+
_TOKEN_ID_POS_KINDS = {"engram"} # take token_ids positional
234+
235+
109236
def _build_one(spec: BlockSpec, hidden_size: int) -> _BuiltBlock:
110237
builder = BLOCK_BUILDERS.get(spec.kind)
111238
if builder is None:
112239
raise ValueError(f"no builder registered for block kind {spec.kind!r}")
113240
mod = builder(hidden_size, dict(spec.params))
114241
return _BuiltBlock(
115242
kind=spec.kind, module=mod,
116-
needs_doc_ids=(spec.kind == "engram"),
117-
needs_token_ids=(spec.kind == "engram"),
243+
needs_doc_ids=(spec.kind in _DOC_ID_KW_KINDS or spec.kind == "engram"),
244+
needs_token_ids=(spec.kind in _TOKEN_ID_POS_KINDS),
118245
)
119246

120247

@@ -168,10 +295,14 @@ def __call__(
168295
)
169296
h = hidden_states
170297
for b in self.blocks:
171-
if b.needs_doc_ids and b.needs_token_ids:
298+
if b.needs_token_ids and b.needs_doc_ids:
299+
# Engram: positional token_ids + keyword document_ids.
172300
delta = b.module(h, token_ids, document_ids=document_ids)
173301
elif b.needs_token_ids:
174302
delta = b.module(h, token_ids)
303+
elif b.needs_doc_ids:
304+
# GDN/KDA: doc_ids keyword (block uses it for doc-reset).
305+
delta = b.module(h, doc_ids=document_ids)
175306
else:
176307
delta = b.module(h)
177308
h = h + delta

scripts/v4_1b_parquet_smoke.py

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,35 +42,50 @@
4242

4343

4444
def _make_template(hidden_size: int, layers: int, vocab_size: int) -> RunTemplate:
45-
"""Build a V4 template close to ~1B params at hidden=2048, layers=12."""
45+
"""Build a V4 template close to ~1B params at hidden=2048, layers=12.
46+
47+
Real-factories version: includes GDN (Path A→C dispatchable), KDA,
48+
attention, MoE, NSA, CSA+HCA, Engram, MLP — the full V4 cohort.
49+
"""
50+
n_heads = max(1, hidden_size // 64)
51+
h_dim = hidden_size // n_heads
52+
per_kind = max(1, layers // 6) # spread layers across 6 attention-like kinds
53+
blocks = [
54+
BlockSpec(kind="engram", repeat=1, params={
55+
"num_ngram_layers": 2, "max_ngram_size": 4,
56+
"num_embed_table_per_ngram": 4, "embed_dim": 64,
57+
"embed_table_size": 1024,
58+
}),
59+
BlockSpec(kind="gdn", repeat=per_kind, params={
60+
"num_heads": n_heads, "head_dim": h_dim, "use_short_conv": False,
61+
}),
62+
BlockSpec(kind="kda", repeat=per_kind, params={
63+
"num_heads": n_heads, "head_dim": h_dim, "use_short_conv": False,
64+
}),
65+
BlockSpec(kind="attention", repeat=per_kind, params={
66+
"num_heads": n_heads, "head_dim": h_dim,
67+
}),
68+
BlockSpec(kind="nsa", repeat=per_kind, params={
69+
"num_heads": n_heads, "head_dim": h_dim,
70+
"compress_block_size": 64, "select_topk": 16,
71+
"sliding_window": 256,
72+
}),
73+
BlockSpec(kind="csa_hca", repeat=per_kind, params={
74+
"num_heads": n_heads, "head_dim": h_dim,
75+
"m_csa": 4, "m_hca": 16,
76+
}),
77+
BlockSpec(kind="moe", repeat=per_kind, params={
78+
"num_experts": 8, "top_k": 2,
79+
"expert_hidden_size": hidden_size * 2,
80+
}),
81+
BlockSpec(kind="mlp", repeat=max(1, layers - 6 * per_kind),
82+
params={"intermediate_size": hidden_size * 4}),
83+
]
4684
return RunTemplate(
4785
name=f"v4_smoke_h{hidden_size}_L{layers}",
4886
hidden_size=hidden_size,
4987
vocab_size=vocab_size,
50-
blocks=[
51-
# Engram boost first (cheap doc-aware n-gram).
52-
BlockSpec(kind="engram", repeat=1, params={
53-
"num_ngram_layers": 2, "max_ngram_size": 4,
54-
"num_embed_table_per_ngram": 4, "embed_dim": 64,
55-
"embed_table_size": 1024,
56-
}),
57-
# NSA × layers/3.
58-
BlockSpec(kind="nsa", repeat=max(1, layers // 3), params={
59-
"num_heads": max(1, hidden_size // 64),
60-
"head_dim": 64,
61-
"compress_block_size": 64, "select_topk": 16,
62-
"sliding_window": 256,
63-
}),
64-
# CSA+HCA × layers/3.
65-
BlockSpec(kind="csa_hca", repeat=max(1, layers // 3), params={
66-
"num_heads": max(1, hidden_size // 64),
67-
"head_dim": 64,
68-
"m_csa": 4, "m_hca": 16,
69-
}),
70-
# MLP × remainder.
71-
BlockSpec(kind="mlp", repeat=max(1, layers - 2 * (layers // 3)),
72-
params={"intermediate_size": hidden_size * 4}),
73-
],
88+
blocks=blocks,
7489
)
7590

7691

0 commit comments

Comments
 (0)