Skip to content

Commit 9854e55

Browse files
committed
feat(v4): wire GDN/KDA blocks through path dispatch — env-override actually works now
LinearAttentionBlock.__call__ and KimiDeltaAttentionBlock.__call__ previously hardcoded naive_recurrent_* (Path A) regardless of the CPPMEGA_V4_KERNEL_PATH__LINEAR_ATTENTION / __KDA env var. All the Path B/C/E speedup work was effectively dead inside the UnifiedSuperblockV4 stack. Both blocks now route through gated_delta_recurrent_dispatch / kda_recurrent_dispatch, which honour the env override and auto-pick the fastest available backend (Path B/C/E when Metal/tilelang/vendored mlx-lm op are reachable, falling back to Path A). The doc_ids state-reset path also dispatches through the same routing (per-segment). Tests (9 new in test_block_dispatch_env_override.py): - GDN block honours env override for path_a / path_b - KDA block honours env override for path_a / path_b - path_a vs path_b outputs close within float32 simd_sum drift (atol=5e-4 rtol=5e-3) - default auto-mode produces valid output (no env override) - doc_ids path also dispatches through the path system v4 dispatch tests: 9/9 passed.
1 parent e8378a5 commit 9854e55

3 files changed

Lines changed: 132 additions & 5 deletions

File tree

cppmega_v4/nn/kimi_delta_attention.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ def __call__(self, x: mx.array, *, doc_ids: mx.array | None = None) -> mx.array:
150150
g = g.reshape(batch, seq_len, cfg._num_v_heads, cfg.head_k_dim)
151151

152152
if doc_ids is None:
153-
o, _ = naive_recurrent_kda(q, k, v, g, beta)
153+
from cppmega_v4._tilelang.kda_paths import kda_recurrent_dispatch
154+
o, _ = kda_recurrent_dispatch(q, k, v, g, beta)
154155
else:
155156
o = self._recurrent_with_doc_reset(q, k, v, g, beta, doc_ids)
156157

@@ -178,7 +179,8 @@ def _recurrent_with_doc_reset(q, k, v, g, beta, doc_ids: mx.array) -> mx.array:
178179
runs.append((start, seq_len))
179180
outs = []
180181
for s, e in runs:
181-
ob, _ = naive_recurrent_kda(
182+
from cppmega_v4._tilelang.kda_paths import kda_recurrent_dispatch
183+
ob, _ = kda_recurrent_dispatch(
182184
q[b:b + 1, s:e],
183185
k[b:b + 1, s:e],
184186
v[b:b + 1, s:e],

cppmega_v4/nn/linear_attention.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,17 @@ def __call__(self, x: mx.array, *, doc_ids: mx.array | None = None) -> mx.array:
181181
g = mx.log(mx.sigmoid(self.a_proj(x)) + cfg.norm_eps) # (B, T, num_v_heads)
182182

183183
if doc_ids is None:
184-
o, _ = naive_recurrent_gated_delta_rule(q, k, v, beta, g)
184+
# Dispatch through the path system: env-overridable via
185+
# CPPMEGA_V4_KERNEL_PATH__LINEAR_ATTENTION, auto-mode picks the
186+
# fastest available backend (Path B/C/E if Metal/tilelang/mlx-lm
187+
# vendored op are reachable; falls back to Path A).
188+
from cppmega_v4._tilelang.linear_attention_paths import (
189+
gated_delta_recurrent_dispatch,
190+
)
191+
o, _ = gated_delta_recurrent_dispatch(q, k, v, beta, g)
185192
else:
186193
# Document-boundary state reset: split into runs of contiguous
187-
# doc_id, recur within each, concatenate outputs.
194+
# doc_id, dispatch each run through the path system, concat.
188195
o = self._recurrent_with_doc_reset(q, k, v, beta, g, doc_ids)
189196

190197
# o has shape [B, T, H_v, V_dim]; flatten head axis for o_proj.
@@ -232,7 +239,10 @@ def _recurrent_with_doc_reset(
232239
vb = v[b:b + 1, s:e]
233240
bb = beta[b:b + 1, s:e]
234241
gb = g[b:b + 1, s:e]
235-
ob, _ = naive_recurrent_gated_delta_rule(qb, kb, vb, bb, gb)
242+
from cppmega_v4._tilelang.linear_attention_paths import (
243+
gated_delta_recurrent_dispatch,
244+
)
245+
ob, _ = gated_delta_recurrent_dispatch(qb, kb, vb, bb, gb)
236246
run_outputs.append(ob)
237247
per_batch_outputs.append(mx.concatenate(run_outputs, axis=1))
238248
return mx.concatenate(per_batch_outputs, axis=0)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""Tests that LinearAttentionBlock / KimiDeltaAttentionBlock honour the
2+
CPPMEGA_V4_KERNEL_PATH__* env override (i.e. the block dispatches through
3+
linear_attention_paths / kda_paths, not the naive recurrent kernel directly).
4+
"""
5+
6+
import mlx.core as mx
7+
import numpy as np
8+
import pytest
9+
10+
from cppmega_v4._tilelang.kda_paths import ENV_VAR as KDA_ENV
11+
from cppmega_v4._tilelang.linear_attention_paths import ENV_VAR as GDN_ENV
12+
from cppmega_v4.nn.kimi_delta_attention import (
13+
KimiDeltaAttentionBlock,
14+
KimiDeltaAttentionConfig,
15+
)
16+
from cppmega_v4.nn.linear_attention import (
17+
LinearAttentionBlock,
18+
LinearAttentionConfig,
19+
)
20+
21+
22+
def _gdn_block(d=64, h=4):
23+
cfg = LinearAttentionConfig(
24+
hidden_size=d, num_heads=h, head_dim=d // h, use_short_conv=False,
25+
)
26+
blk = LinearAttentionBlock(cfg)
27+
# Perturb o_proj so block delta is non-zero (default is zero-init).
28+
rng = np.random.default_rng(0)
29+
blk.o_proj.weight = mx.array(
30+
rng.standard_normal(blk.o_proj.weight.shape).astype(np.float32) * 0.05
31+
)
32+
return blk
33+
34+
35+
def _kda_block(d=64, h=4):
36+
cfg = KimiDeltaAttentionConfig(
37+
hidden_size=d, num_heads=h, head_dim=d // h, use_short_conv=False,
38+
)
39+
blk = KimiDeltaAttentionBlock(cfg)
40+
rng = np.random.default_rng(0)
41+
blk.o_proj.weight = mx.array(
42+
rng.standard_normal(blk.o_proj.weight.shape).astype(np.float32) * 0.05
43+
)
44+
return blk
45+
46+
47+
@pytest.mark.parametrize("path", ["path_a", "path_b"])
48+
def test_gdn_block_honours_env_override(monkeypatch, path):
49+
monkeypatch.setenv(GDN_ENV, path)
50+
blk = _gdn_block()
51+
x = mx.random.normal((1, 8, blk.config.hidden_size))
52+
out = blk(x)
53+
assert out.shape == x.shape
54+
assert not bool(mx.any(mx.isnan(out)).item())
55+
56+
57+
@pytest.mark.parametrize("path", ["path_a", "path_b"])
58+
def test_kda_block_honours_env_override(monkeypatch, path):
59+
monkeypatch.setenv(KDA_ENV, path)
60+
blk = _kda_block()
61+
x = mx.random.normal((1, 8, blk.config.hidden_size))
62+
out = blk(x)
63+
assert out.shape == x.shape
64+
assert not bool(mx.any(mx.isnan(out)).item())
65+
66+
67+
def test_gdn_block_path_a_and_path_b_close(monkeypatch):
68+
"""Block output via Path A and Path B should be numerically close."""
69+
blk = _gdn_block()
70+
x = mx.random.normal((1, 8, blk.config.hidden_size))
71+
72+
monkeypatch.setenv(GDN_ENV, "path_a")
73+
out_a = np.array(blk(x))
74+
monkeypatch.setenv(GDN_ENV, "path_b")
75+
out_b = np.array(blk(x))
76+
np.testing.assert_allclose(out_a, out_b, atol=5e-4, rtol=5e-3)
77+
78+
79+
def test_kda_block_path_a_and_path_b_close(monkeypatch):
80+
blk = _kda_block()
81+
x = mx.random.normal((1, 8, blk.config.hidden_size))
82+
83+
monkeypatch.setenv(KDA_ENV, "path_a")
84+
out_a = np.array(blk(x))
85+
monkeypatch.setenv(KDA_ENV, "path_b")
86+
out_b = np.array(blk(x))
87+
np.testing.assert_allclose(out_a, out_b, atol=5e-4, rtol=5e-3)
88+
89+
90+
def test_gdn_block_default_auto_picks_available_path(monkeypatch):
91+
"""Without env override, auto-mode should still produce valid output."""
92+
monkeypatch.delenv(GDN_ENV, raising=False)
93+
blk = _gdn_block()
94+
x = mx.random.normal((1, 4, blk.config.hidden_size))
95+
out = blk(x)
96+
assert not bool(mx.any(mx.isnan(out)).item())
97+
98+
99+
def test_kda_block_default_auto_picks_available_path(monkeypatch):
100+
monkeypatch.delenv(KDA_ENV, raising=False)
101+
blk = _kda_block()
102+
x = mx.random.normal((1, 4, blk.config.hidden_size))
103+
out = blk(x)
104+
assert not bool(mx.any(mx.isnan(out)).item())
105+
106+
107+
def test_gdn_block_with_doc_ids_uses_dispatch(monkeypatch):
108+
"""doc_ids path also dispatches through linear_attention_paths."""
109+
monkeypatch.setenv(GDN_ENV, "path_b")
110+
blk = _gdn_block()
111+
x = mx.random.normal((1, 8, blk.config.hidden_size))
112+
doc_ids = mx.array([[0, 0, 0, 0, 1, 1, 1, 1]], dtype=mx.int32)
113+
out = blk(x, doc_ids=doc_ids)
114+
assert out.shape == x.shape
115+
assert not bool(mx.any(mx.isnan(out)).item())

0 commit comments

Comments
 (0)