Skip to content

Commit 12811bf

Browse files
committed
feat(v4): KDA Paths B/C/D — mirror GDN's real implementations
Three new modules under cppmega_v4/_tilelang/: - kda_path_b.py — hand-MSL recurrent forward via mx.fast.metal_kernel. Per-thread layout: one thread per (b, hv_idx, v_index). Each thread owns one column of the K x V state, decays per-K via exp(g[B,T,HV,K]) (vectorized gate, KDA-specific), reduces over K for KS dot, computes β-corrected delta, rank-1 outer adds, then projects q · S. Supports arbitrary (B, T, H, HV, K, V) with HV % H == 0. Falls back to Path A for initial_state, custom scale, or HV not divisible by H. - kda_path_c.py — TileLang DSL @T.prim_func lowered through cppmega_mlx's dispatch_lower + tilelang.compile(target='metal', execution_backend='tvm_ffi'). Per-lane recurrent scan modeled on mamba3_path_c.py: LANES = B * HV * V threads, state held in registers as K floats. Host TileLang→MSL infra imported read-only. - kda_path_d.py — Triton-frontend seam over FLA KDA chunk kernels (11 prims, ~3340 LoC). Two-stage probe (triton+frontend, then fla.ops.kda.chunk); status names the actionable blocker (op_mapping needs matmul/exp/masked-load emitters). kda_paths.py rewritten to wire B/C/D into real dispatch (previously all three fell through to Path A). Auto-mode preference unchanged (C > B > D > A). Tests: 16 new in tests/v4/test_kda_paths.py covering all three paths: - B: status reports Metal kernel; parity vs Path A (atol 1e-4); final-state return; env-forced dispatch. - C: module import w/o tilelang; status text precision; runtime↔dispatch round-trip; env-forced dispatch returns valid output; fallback parity bit-exact when tilelang missing. - D: module import; concrete blocker named; runtime↔dispatch round-trip; both probes return tuples; env-forced fallback; lowering seam returns (None, msg). + 1 dispatch sanity test (path keys unchanged). Existing test_kda_dispatch_returns_same_as_path_a relaxed to assert_allclose(atol=1e-5) since auto-mode now picks Path B (real Metal float32) over Path A (MLX float64-then-cast). v4 suite: 153 passed / 11 skipped.
1 parent 146d6c2 commit 12811bf

6 files changed

Lines changed: 731 additions & 42 deletions

File tree

cppmega_v4/_tilelang/kda_path_b.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""KDA Path B — hand-MSL recurrent forward via mx.fast.metal_kernel.
2+
3+
Mirrors ``linear_attention_path_b.py`` but for the KDA recurrence (FLA naive):
4+
5+
q, k: [B, T, H, K] (q scaled by 1/sqrt(K), then both repeat to HV)
6+
v: [B, T, HV, V]
7+
g: [B, T, HV, K] (per-K vectorized log-gate)
8+
beta: [B, T, HV]
9+
S: [B, HV, K, V]
10+
11+
for t in [0, T):
12+
S *= exp(g_t) # per-K decay, broadcast over V
13+
inner = v_t - sum_k(k_t * S)
14+
S += (beta_t * k_t)[:, :, None] * inner[:, None, :]
15+
o_t = sum_k(q_t * S)
16+
17+
Per-thread layout: one thread per (batch, hv_head, v_index). Each thread
18+
owns one column of the K x V state for one (b, hv) and walks time
19+
serially while reducing over K for state-derived quantities.
20+
21+
Group expansion (HV / H): each ``hv`` maps to ``h = hv // G`` for indexing
22+
into q and k. The kernel performs that mapping inline.
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import mlx.core as mx
28+
29+
from cppmega_v4._tilelang._kernel_cache import get_or_build_kernel
30+
31+
32+
def _kda_forward_kernel(
33+
q: mx.array,
34+
k: mx.array,
35+
v: mx.array,
36+
g: mx.array,
37+
beta: mx.array,
38+
) -> tuple[mx.array, mx.array]:
39+
"""Metal forward for KDA recurrence.
40+
41+
Args:
42+
q, k: [B, T, H, K]
43+
v: [B, T, HV, V]
44+
g: [B, T, HV, K] per-K vectorized log-gate
45+
beta: [B, T, HV]
46+
47+
Returns:
48+
o: [B, T, HV, V]
49+
S_final: [B, HV, K, V]
50+
"""
51+
if q.ndim != 4 or k.shape != q.shape:
52+
raise ValueError(f"q/k must be [B,T,H,K]; got q={q.shape}, k={k.shape}")
53+
if v.ndim != 4 or v.shape[:2] != q.shape[:2]:
54+
raise ValueError(f"v must be [B,T,HV,V]; got v={v.shape}")
55+
if g.shape != (*v.shape[:3], k.shape[-1]):
56+
raise ValueError(f"g must be [B,T,HV,K]; got g={g.shape}")
57+
if beta.shape != v.shape[:3]:
58+
raise ValueError(f"beta must be [B,T,HV]; got beta={beta.shape}")
59+
60+
b, t, h, kdim = q.shape
61+
hv, vdim = v.shape[2], v.shape[-1]
62+
if hv % h != 0:
63+
raise ValueError(f"HV ({hv}) must be divisible by H ({h})")
64+
group = hv // h
65+
66+
q = q.astype(mx.float32) * (kdim ** -0.5)
67+
k = k.astype(mx.float32)
68+
v = v.astype(mx.float32)
69+
g = g.astype(mx.float32)
70+
beta = beta.astype(mx.float32)
71+
72+
q_flat = q.reshape(-1)
73+
k_flat = k.reshape(-1)
74+
v_flat = v.reshape(-1)
75+
g_flat = g.reshape(-1)
76+
beta_flat = beta.reshape(-1)
77+
78+
source = f"""
79+
uint vj = thread_position_in_grid.x;
80+
uint bhv = thread_position_in_grid.y;
81+
82+
if (vj >= {vdim}u || bhv >= {b * hv}u) return;
83+
84+
uint bb = bhv / {hv}u;
85+
uint hv_idx = bhv % {hv}u;
86+
uint h_idx = hv_idx / {group}u;
87+
88+
// Per-thread state column: S[bb, hv_idx, :, vj] size K
89+
float state[{kdim}];
90+
for (int i = 0; i < {kdim}; i++) state[i] = 0.0f;
91+
92+
for (int ti = 0; ti < {t}; ti++) {{
93+
// g_base: g[bb, ti, hv_idx, 0]
94+
int g_base = ((bb * {t} + ti) * {hv} + hv_idx) * {kdim};
95+
// beta scalar: beta[bb, ti, hv_idx]
96+
int beta_idx = (bb * {t} + ti) * {hv} + hv_idx;
97+
float beta_t = beta[beta_idx];
98+
99+
// k/q base for this (b, ti, h_idx)
100+
int qk_base = ((bb * {t} + ti) * {h} + h_idx) * {kdim};
101+
// v scalar: v[bb, ti, hv_idx, vj]
102+
int v_idx = ((bb * {t} + ti) * {hv} + hv_idx) * {vdim} + vj;
103+
float v_j = v[v_idx];
104+
105+
// Phase 1: per-K decay AND KS reduction along K (interleaved)
106+
float kth_S_j = 0.0f;
107+
for (int i = 0; i < {kdim}; i++) {{
108+
float decay_i = exp(g[g_base + i]);
109+
state[i] *= decay_i;
110+
kth_S_j += k[qk_base + i] * state[i];
111+
}}
112+
113+
// Phase 2: delta correction
114+
float inner_j = v_j - kth_S_j;
115+
116+
// Phase 3: rank-1 outer add S[i,j] += beta * k[i] * inner AND o[j] = sum_i q[i] * S[i,j]
117+
float o_j = 0.0f;
118+
for (int i = 0; i < {kdim}; i++) {{
119+
float k_i = k[qk_base + i];
120+
float q_i = q[qk_base + i];
121+
state[i] += beta_t * k_i * inner_j;
122+
o_j += q_i * state[i];
123+
}}
124+
125+
// output[bb, ti, hv_idx, vj]
126+
int o_idx = ((bb * {t} + ti) * {hv} + hv_idx) * {vdim} + vj;
127+
output[o_idx] = o_j;
128+
}}
129+
130+
// Final state column: S_final[bb, hv_idx, :, vj]
131+
int sf_base = (bb * {hv} + hv_idx) * {kdim * vdim} + vj;
132+
for (int i = 0; i < {kdim}; i++) {{
133+
state_final[sf_base + i * {vdim}] = state[i];
134+
}}
135+
"""
136+
137+
name = f"v4_kda_fwd_{b}_{t}_{h}_{hv}_{kdim}_{vdim}"
138+
kernel = get_or_build_kernel(
139+
name=name,
140+
input_names=["q", "k", "v", "g", "beta"],
141+
output_names=["output", "state_final"],
142+
source=source,
143+
)
144+
145+
grid = (vdim, b * hv, 1)
146+
tg_x = min(vdim, 64)
147+
threadgroup = (tg_x, 1, 1)
148+
149+
out, sf = kernel(
150+
inputs=[q_flat, k_flat, v_flat, g_flat, beta_flat],
151+
output_shapes=[
152+
(b * t * hv * vdim,),
153+
(b * hv * kdim * vdim,),
154+
],
155+
output_dtypes=[mx.float32, mx.float32],
156+
grid=grid,
157+
threadgroup=threadgroup,
158+
)
159+
return out.reshape(b, t, hv, vdim), sf.reshape(b, hv, kdim, vdim)
160+
161+
162+
def kda_forward_path_b(
163+
q: mx.array,
164+
k: mx.array,
165+
v: mx.array,
166+
g: mx.array,
167+
beta: mx.array,
168+
*,
169+
scale: float | None = None,
170+
initial_state: mx.array | None = None,
171+
output_final_state: bool = False,
172+
):
173+
"""KDA Path B forward, signature matching ``naive_recurrent_kda``.
174+
175+
Falls back to Path A for unsupported configs (initial_state, custom
176+
scale, or HV not divisible by H) so the dispatch never produces wrong
177+
numerics silently.
178+
"""
179+
if (
180+
initial_state is not None
181+
or scale is not None
182+
or v.shape[2] % q.shape[2] != 0
183+
):
184+
from cppmega_v4.nn._external.fla_naive_kda import naive_recurrent_kda
185+
return naive_recurrent_kda(
186+
q, k, v, g, beta,
187+
scale=scale, initial_state=initial_state,
188+
output_final_state=output_final_state,
189+
)
190+
o, sf = _kda_forward_kernel(q, k, v, g, beta)
191+
return o, (sf if output_final_state else None)
192+
193+
194+
__all__ = ["kda_forward_path_b"]

cppmega_v4/_tilelang/kda_path_c.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""KDA Path C — TileLang DSL ``@T.prim_func`` lowered to Metal via tvm_ffi.
2+
3+
Mirrors ``linear_attention_path_c.py`` but for the KDA recurrence:
4+
5+
q, k: [B, T, H, K] (q pre-scaled by 1/sqrt(K), then both repeat to HV)
6+
v: [B, T, HV, V]
7+
g: [B, T, HV, K] per-K vectorized log-gate
8+
beta: [B, T, HV]
9+
S: [B, HV, K, V]
10+
11+
Per-lane scan: one lane per (b, hv, v_idx). State held in registers as K
12+
floats per lane. Inner loop: per-K decay + KS reduction (interleaved),
13+
δ correction, rank-1 outer add + q·S projection.
14+
15+
Plugin invariant: imports ``dispatch_lower`` / ``_msl_transform`` from the
16+
host ``cppmega_mlx`` package read-only — never modifies them.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from functools import lru_cache
22+
from typing import Any
23+
24+
import mlx.core as mx
25+
26+
27+
def _tilelang_importable() -> tuple[bool, str]:
28+
try:
29+
import tilelang # noqa: F401
30+
import tilelang.language as _T # noqa: F401
31+
from tilelang.engine.lower import lower as _lower # noqa: F401
32+
except Exception as exc:
33+
return False, f"tilelang import failed: {exc.__class__.__name__}: {exc}"
34+
try:
35+
from cppmega_mlx.nn._tilelang._engine_dispatch import dispatch_lower # noqa: F401
36+
from cppmega_mlx.nn._tilelang import _msl_transform # noqa: F401
37+
except Exception as exc:
38+
return False, f"host TileLang→MSL infra not reachable: {exc}"
39+
return True, "tilelang + host TileLang→MSL infra reachable"
40+
41+
42+
def _threads_for(lanes: int) -> int:
43+
for tg in (256, 192, 128, 96, 64, 32):
44+
if lanes % tg == 0:
45+
return tg
46+
return 32
47+
48+
49+
@lru_cache(maxsize=64)
50+
def _kda_fwd_kernel_for(
51+
BATCH: int,
52+
SEQ: int,
53+
HEADS: int,
54+
HV: int,
55+
HEADDIM_K: int,
56+
HEADDIM_V: int,
57+
q_dtype: str = "float32",
58+
k_dtype: str = "float32",
59+
v_dtype: str = "float32",
60+
g_dtype: str = "float32",
61+
beta_dtype: str = "float32",
62+
h0_dtype: str = "float32",
63+
y_dtype: str = "float32",
64+
h_last_dtype: str = "float32",
65+
) -> tuple[Any, Any]:
66+
"""Build (and cache) the KDA fwd Path C kernel for this shape/dtype tuple."""
67+
import tilelang
68+
import tilelang.language as T
69+
from cppmega_mlx.nn._tilelang import _msl_transform
70+
from cppmega_mlx.nn._tilelang._engine_dispatch import dispatch_lower
71+
72+
assert HV % HEADS == 0, f"HV ({HV}) must be divisible by HEADS ({HEADS})"
73+
GROUP = HV // HEADS
74+
LANES = BATCH * HV * HEADDIM_V
75+
THREADS = _threads_for(LANES)
76+
accum_dtype = "float32"
77+
78+
@T.prim_func
79+
def fwd(
80+
q: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_K), q_dtype),
81+
k: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_K), k_dtype),
82+
v: T.Tensor((BATCH, SEQ, HV, HEADDIM_V), v_dtype),
83+
g: T.Tensor((BATCH, SEQ, HV, HEADDIM_K), g_dtype),
84+
beta: T.Tensor((BATCH, SEQ, HV), beta_dtype),
85+
h0: T.Tensor((BATCH, HV, HEADDIM_K, HEADDIM_V), h0_dtype),
86+
y: T.Tensor((BATCH, SEQ, HV, HEADDIM_V), y_dtype),
87+
h_last: T.Tensor((BATCH, HV, HEADDIM_K, HEADDIM_V), h_last_dtype),
88+
):
89+
with T.Kernel(T.ceildiv(LANES, THREADS), threads=THREADS) as bx:
90+
tid = T.get_thread_binding(0)
91+
global_lane = bx * THREADS + tid
92+
h_state = T.alloc_local((HEADDIM_K,), accum_dtype)
93+
if global_lane < LANES:
94+
vj = global_lane % HEADDIM_V
95+
hv_idx = (global_lane // HEADDIM_V) % HV
96+
bb = global_lane // (HEADDIM_V * HV)
97+
h_idx = hv_idx // GROUP
98+
for i in T.serial(HEADDIM_K):
99+
h_state[i] = T.cast(h0[bb, hv_idx, i, vj], accum_dtype)
100+
for t in T.serial(SEQ):
101+
beta_val = T.cast(beta[bb, t, hv_idx], accum_dtype)
102+
v_j = T.cast(v[bb, t, hv_idx, vj], accum_dtype)
103+
# Phase 1: per-K decay + KS reduction (interleaved).
104+
kth_S_j = T.alloc_var(T.float32, init=0.0)
105+
for i in T.serial(HEADDIM_K):
106+
decay_i = T.exp(T.cast(g[bb, t, hv_idx, i], accum_dtype))
107+
h_state[i] = h_state[i] * decay_i
108+
kth_S_j += T.cast(k[bb, t, h_idx, i], accum_dtype) * h_state[i]
109+
inner_j = v_j - kth_S_j
110+
# Phase 2: rank-1 outer add + q·h projection.
111+
out = T.alloc_var(T.float32, init=0.0)
112+
for i in T.serial(HEADDIM_K):
113+
k_i = T.cast(k[bb, t, h_idx, i], accum_dtype)
114+
q_i = T.cast(q[bb, t, h_idx, i], accum_dtype)
115+
h_state[i] = h_state[i] + beta_val * k_i * inner_j
116+
out += q_i * h_state[i]
117+
y[bb, t, hv_idx, vj] = T.cast(out, y_dtype)
118+
for i in T.serial(HEADDIM_K):
119+
h_last[bb, hv_idx, i, vj] = T.cast(h_state[i], h_last_dtype)
120+
121+
artifact = dispatch_lower(fwd, target="metal", return_msl=True)
122+
kernel = tilelang.compile(
123+
fwd,
124+
target=_msl_transform._as_metal_target("metal"),
125+
execution_backend="tvm_ffi",
126+
out_idx=[6, 7], # y, h_last
127+
)
128+
return kernel, artifact
129+
130+
131+
def _path_c_runtime_status() -> tuple[bool, str]:
132+
return _tilelang_importable()
133+
134+
135+
def _kda_fwd_path_c_call(
136+
q: mx.array,
137+
k: mx.array,
138+
v: mx.array,
139+
g: mx.array,
140+
beta: mx.array,
141+
*,
142+
scale: float | None = None,
143+
initial_state: mx.array | None = None,
144+
output_final_state: bool = False,
145+
):
146+
"""KDA Path C entry — same signature as ``naive_recurrent_kda``."""
147+
import math
148+
149+
B, T_, H, K_dim = q.shape
150+
HV, V_dim = v.shape[2], v.shape[-1]
151+
fla_scale = scale if scale is not None else 1.0 / math.sqrt(K_dim)
152+
q_scaled = (q.astype(mx.float32) * fla_scale).astype(q.dtype)
153+
154+
h0 = initial_state
155+
if h0 is None:
156+
h0 = mx.zeros((B, HV, K_dim, V_dim), dtype=mx.float32)
157+
158+
kernel, _ = _kda_fwd_kernel_for(
159+
B, T_, H, HV, K_dim, V_dim,
160+
q_dtype=str(q.dtype).rsplit(".", 1)[-1],
161+
k_dtype=str(k.dtype).rsplit(".", 1)[-1],
162+
v_dtype=str(v.dtype).rsplit(".", 1)[-1],
163+
g_dtype=str(g.dtype).rsplit(".", 1)[-1],
164+
beta_dtype=str(beta.dtype).rsplit(".", 1)[-1],
165+
)
166+
y, h_last = kernel(q_scaled, k, v, g, beta, h0)
167+
return y, (h_last if output_final_state else None)
168+
169+
170+
__all__ = [
171+
"_kda_fwd_kernel_for",
172+
"_kda_fwd_path_c_call",
173+
"_path_c_runtime_status",
174+
"_tilelang_importable",
175+
]

0 commit comments

Comments
 (0)