Skip to content

Commit c00c62b

Browse files
committed
feat(v4): ROI 3.B-F + 3.5.B-D + 3.7 + 5 + 7 + 8 + 9 — all scaffolds + tests
Lands plumbing for every remaining ROI as a runnable scaffold with full test coverage. Backends that need real Metal/TileLang kernel work delegate to Path A's pure-MLX reference until the kernel land; the API surface, status objects, and env override are stable and committed today. cppmega_v4/_tilelang/_dispatch.py — shared PathStatus + auto_pick infrastructure mirroring mamba3_path_c's mature pattern. cppmega_v4/_tilelang/linear_attention_paths.py (ROI 3.B-F): - Path A: pure-MLX (always available) - Path B: hand-MSL — scaffold w/ explicit pending reason; fallback Path A - Path C: TileLang DSL via tilelang.compile — scaffold; checks tilelang import - Path D: Triton frontend — scaffold; checks triton import - Path E: vendored mlx-lm PR #1217 — scaffold; checks for vendored module - gated_delta_recurrent_dispatch + linear_attention_auto_mode_for_inputs + linear_attention_path_statuses - Env: CPPMEGA_V4_KERNEL_PATH__LINEAR_ATTENTION={path_a..e,auto} cppmega_v4/_tilelang/kda_paths.py (ROI 3.5.B-D): - Same shape as GDN paths but A/B/C/D only (no Path E for KDA) - Env: CPPMEGA_V4_KERNEL_PATH__KDA={path_a..d,auto}; rejects path_e cppmega_v4/_tilelang/benchmark_receipt.py (ROI 3.7): - CellShape + CellReceipt dataclasses; JSON schema matches reports/raw/cppmega_1b_path_matrix_cells/ shape - measure_cell(shape) runs forward, records elapsed + backend status - write_receipt(receipt, out_dir) drops <block>_<path>.json cppmega_v4/nn/mla_absorb.py (ROI 5): - absorb_weights(W_UK, W_UV, W_O) -> (W_UK^T, W_UV @ W_O) precompute - absorbed_mla_decode(q, c_kv, w_uk_abs, w_uv_w_o, ...) — full absorb path in MLX algebra (per FlashMLA docs/20250422-new-kernel-deep-dive.md) - standard_mla_decode(...) — non-absorbed oracle for parity - Numerical parity test: absorbed == standard (atol 1e-4) cppmega_v4/nn/lightning_indexer.py (ROI 7): - LightningIndexer module — fp32 scaffold of V3.2 indexer (deepseek-ai/DeepSeek-V3.2-Exp/inference/model.py class Indexer) - _apply_non_interleaved_rope respects the V3.2 gotcha that indexer RoPE is non-interleaved while MLA's is interleaved - Top-k indexing via mx.argpartition; clipped to seq length cppmega_v4/nn/sparse_attention_v4.py (ROI 8/9): - NativeSparseAttention scaffold (ROI 8 — NSA, arxiv 2502.11089) - CsaHcaHybridAttention scaffold (ROI 9 — V4 CSA+HCA hybrid) - Both fall back to dense causal SDPA today; replace with three-branch and compressed-sparse paths in research follow-up 44 new tests across 4 files: - tests/v4/test_path_dispatch.py — 18 tests for GDN+KDA path scaffolding, env override correctness, dispatch == Path A parity - tests/v4/test_benchmark_receipt.py — 5 tests for receipt schema + JSON - tests/v4/test_mla_absorb.py — 5 tests including numerical parity vs standard_mla_decode (atol 1e-4) - tests/v4/test_lightning_indexer.py — 7 tests including non-interleaved RoPE math correctness - tests/v4/test_sparse_attention_v4.py — 9 tests for NSA + CSA+HCA scaffolds 166/166 v4 + regression green. Plugin invariant preserved: zero modifications under cppmega_mlx/. All 11+ ROIs from INTEGRATION_PLAN_CPPMEGA_MLX.md now have committed code and tests; deferred backends explicitly carry their pending reason in the PathStatus and delegate to Path A so the v4 stack runs end-to-end today.
1 parent 0addd18 commit c00c62b

13 files changed

Lines changed: 1523 additions & 0 deletions

cppmega_v4/_tilelang/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Side-by-side TileLang/Metal dispatch scaffolding for V4 blocks.
2+
3+
Each Path B/C/D/E module exposes the same API surface as Path A and falls
4+
back to Path A's reference kernel when the underlying backend (Metal MSL /
5+
TileLang DSL / Triton frontend / vendored mlx-lm op) is not available on
6+
the host or is still pending implementation.
7+
8+
This keeps the v4 plugin runnable end-to-end on day one while leaving room
9+
for each backend to be filled in incrementally.
10+
"""

cppmega_v4/_tilelang/_dispatch.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Common path-dispatch scaffolding for V4 linear-attention backends.
2+
3+
Mirrors the ``mamba3_path_c_*_status`` / ``*_auto_mode_for_inputs`` pattern
4+
from ``cppmega_mlx/nn/_tilelang/mamba3_path_c.py`` so v4 paths plug into the
5+
same conceptual machinery without depending on cppmega_mlx internals.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import os
11+
from dataclasses import dataclass
12+
from typing import Literal
13+
14+
PathName = Literal["path_a", "path_b", "path_c", "path_d", "path_e"]
15+
16+
17+
@dataclass(frozen=True)
18+
class PathStatus:
19+
"""Describes whether a given backend is available on this host."""
20+
21+
path: PathName
22+
available: bool
23+
reason: str
24+
25+
def __bool__(self) -> bool: # truthy when available
26+
return self.available
27+
28+
29+
def env_override(env_var: str) -> str | None:
30+
"""Read an env override (e.g., 'path_a', 'path_b', ...). None = auto."""
31+
value = os.environ.get(env_var, "").strip().lower()
32+
if not value or value == "auto":
33+
return None
34+
if value not in ("path_a", "path_b", "path_c", "path_d", "path_e"):
35+
raise ValueError(
36+
f"unsupported {env_var}={value!r}; "
37+
"expected one of: path_a, path_b, path_c, path_d, path_e, auto"
38+
)
39+
return value
40+
41+
42+
def auto_pick(
43+
statuses: dict[PathName, PathStatus],
44+
preference: tuple[PathName, ...] = ("path_c", "path_b", "path_e", "path_d", "path_a"),
45+
) -> PathName:
46+
"""Pick the first available path in the preference order."""
47+
for path in preference:
48+
st = statuses.get(path)
49+
if st is not None and st.available:
50+
return path
51+
# Always falls back to path_a (the pure-MLX reference is always available).
52+
return "path_a"
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""ROI 3.7 — Head-to-head benchmark receipt schema for GDN/KDA paths.
2+
3+
Mirrors the JSON shape used by ``reports/raw/cppmega_1b_path_matrix_cells/``
4+
so the v4 benchmark plugs straight into the existing matrix HTML render.
5+
The full benchmark runner (training cell via ``scripts/m04_train_step.py`` on
6+
``data/parquet_samples/gb10/clang_semantic_4k_v10/val_00000.parquet``) lands
7+
when Paths B/C/D/E for GDN/KDA become non-fallback; this module ships the
8+
schema + dispatch-aware single-cell measurement now so the harness can hook
9+
in incrementally.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import json
15+
import time
16+
from dataclasses import asdict, dataclass
17+
from pathlib import Path
18+
from typing import Literal
19+
20+
import mlx.core as mx
21+
22+
from cppmega_v4._tilelang.kda_paths import kda_recurrent_dispatch
23+
from cppmega_v4._tilelang.linear_attention_paths import gated_delta_recurrent_dispatch
24+
25+
Block = Literal["gdn", "kda"]
26+
Path_ = Literal["path_a", "path_b", "path_c", "path_d", "path_e"]
27+
28+
29+
@dataclass(frozen=True)
30+
class CellShape:
31+
block: Block
32+
path: Path_
33+
batch: int
34+
seq_len: int
35+
num_heads: int
36+
head_dim_k: int
37+
head_dim_v: int
38+
num_v_heads: int | None = None
39+
dtype: str = "float32"
40+
41+
42+
@dataclass
43+
class CellReceipt:
44+
"""One (block, path, shape, dtype) measurement cell."""
45+
46+
cell_shape: CellShape
47+
fwd_seconds: float
48+
backend_available: bool
49+
backend_reason: str
50+
output_shape: tuple[int, ...]
51+
output_dtype: str
52+
53+
def to_json(self) -> dict[str, object]:
54+
return {
55+
"cell_shape": asdict(self.cell_shape),
56+
"fwd_seconds": self.fwd_seconds,
57+
"backend_available": self.backend_available,
58+
"backend_reason": self.backend_reason,
59+
"output_shape": list(self.output_shape),
60+
"output_dtype": self.output_dtype,
61+
}
62+
63+
64+
def measure_cell(shape: CellShape) -> CellReceipt:
65+
"""Run a single forward and emit a receipt."""
66+
from cppmega_v4._tilelang.kda_paths import kda_path_statuses
67+
from cppmega_v4._tilelang.linear_attention_paths import linear_attention_path_statuses
68+
69+
statuses = (
70+
linear_attention_path_statuses() if shape.block == "gdn" else kda_path_statuses()
71+
)
72+
st = statuses[shape.path]
73+
if shape.block == "gdn":
74+
q = mx.random.normal((shape.batch, shape.seq_len, shape.num_heads, shape.head_dim_k))
75+
k = mx.random.normal((shape.batch, shape.seq_len, shape.num_heads, shape.head_dim_k))
76+
v = mx.random.normal((shape.batch, shape.seq_len, shape.num_heads, shape.head_dim_v))
77+
beta = mx.random.normal((shape.batch, shape.seq_len, shape.num_heads))
78+
g = mx.random.normal((shape.batch, shape.seq_len, shape.num_heads)) * 0.1
79+
t0 = time.perf_counter()
80+
o, _ = gated_delta_recurrent_dispatch(q, k, v, beta, g)
81+
mx.eval(o)
82+
elapsed = time.perf_counter() - t0
83+
else: # kda
84+
hv = shape.num_v_heads if shape.num_v_heads is not None else shape.num_heads
85+
q = mx.random.normal((shape.batch, shape.seq_len, shape.num_heads, shape.head_dim_k))
86+
k = mx.random.normal((shape.batch, shape.seq_len, shape.num_heads, shape.head_dim_k))
87+
v = mx.random.normal((shape.batch, shape.seq_len, hv, shape.head_dim_v))
88+
g = mx.random.normal((shape.batch, shape.seq_len, hv, shape.head_dim_k)) * 0.05
89+
beta = mx.random.normal((shape.batch, shape.seq_len, hv))
90+
t0 = time.perf_counter()
91+
o, _ = kda_recurrent_dispatch(q, k, v, g, beta)
92+
mx.eval(o)
93+
elapsed = time.perf_counter() - t0
94+
95+
return CellReceipt(
96+
cell_shape=shape,
97+
fwd_seconds=elapsed,
98+
backend_available=st.available,
99+
backend_reason=st.reason,
100+
output_shape=tuple(o.shape),
101+
output_dtype=str(o.dtype),
102+
)
103+
104+
105+
def write_receipt(receipt: CellReceipt, out_dir: Path) -> Path:
106+
"""Drop the receipt JSON into ``out_dir/<block>_<path>.json``."""
107+
out_dir.mkdir(parents=True, exist_ok=True)
108+
fname = f"{receipt.cell_shape.block}_{receipt.cell_shape.path}.json"
109+
out_path = out_dir / fname
110+
out_path.write_text(json.dumps(receipt.to_json(), indent=2))
111+
return out_path
112+
113+
114+
__all__ = ["Block", "CellReceipt", "CellShape", "Path_", "measure_cell", "write_receipt"]

cppmega_v4/_tilelang/kda_paths.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""KDA multi-path scaffolding (Paths B/C/D + auto-mode).
2+
3+
Same shape as ``linear_attention_paths.py`` but for the KDA backend. KDA has
4+
no mlx-lm equivalent op (no Path E).
5+
6+
Backend status (May 2026):
7+
- Path A: pure-MLX naive recurrent KDA (golden) — always available.
8+
- Path B: hand-MSL KDA kernel — scaffold; pending Metal kernel.
9+
- Path C: TileLang DSL — scaffold; should lift ``tilelang/examples/kda/*.py``
10+
(11 prims, ~3340 LoC) through ``tilelang.compile(target='metal',
11+
execution_backend='tvm_ffi')`` mirroring ``mamba3_path_c.py`` skeleton.
12+
- Path D: Triton frontend — scaffold; should lift FLA KDA Triton kernels.
13+
14+
Env override: ``CPPMEGA_V4_KERNEL_PATH__KDA``.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import importlib
20+
21+
from cppmega_v4._tilelang._dispatch import PathName, PathStatus, auto_pick, env_override
22+
from cppmega_v4.nn._external.fla_naive_kda import naive_recurrent_kda
23+
24+
ENV_VAR = "CPPMEGA_V4_KERNEL_PATH__KDA"
25+
26+
27+
def _path_a_status() -> PathStatus:
28+
return PathStatus(path="path_a", available=True, reason="pure-MLX KDA reference")
29+
30+
31+
def _path_b_status() -> PathStatus:
32+
return PathStatus(
33+
path="path_b",
34+
available=False,
35+
reason=(
36+
"hand-MSL KDA kernel pending — DPLR transport A = αI + β u v^T plus "
37+
"gated-LA branch needs Metal kernel work beyond GDN's recurrence"
38+
),
39+
)
40+
41+
42+
def _path_c_status() -> PathStatus:
43+
try:
44+
importlib.import_module("tilelang")
45+
except Exception as exc:
46+
return PathStatus(
47+
path="path_c",
48+
available=False,
49+
reason=f"tilelang not importable: {exc}",
50+
)
51+
return PathStatus(
52+
path="path_c",
53+
available=False,
54+
reason=(
55+
"TileLang importable but KDA Path C wiring pending: lift "
56+
"tilelang/examples/kda/{chunk_delta_h_fwd,chunk_delta_bwd,chunk_o,"
57+
"chunk_bwd_intra,chunk_inter_solve_fused,chunk_bwd_dqkwg,"
58+
"chunk_bwd_dv,chunk_bwd_gla_dA,chunk_intra_token_parallel,"
59+
"wy_fast,wy_fast_bwd}.py via tilelang.compile(target='metal')"
60+
),
61+
)
62+
63+
64+
def _path_d_status() -> PathStatus:
65+
try:
66+
importlib.import_module("triton")
67+
except Exception:
68+
return PathStatus(
69+
path="path_d",
70+
available=False,
71+
reason="triton not importable",
72+
)
73+
return PathStatus(
74+
path="path_d",
75+
available=False,
76+
reason="Triton frontend KDA wiring pending",
77+
)
78+
79+
80+
def _fallback(*args, **kwargs):
81+
return naive_recurrent_kda(*args, **kwargs)
82+
83+
84+
def kda_path_statuses() -> dict[PathName, PathStatus]:
85+
return {
86+
"path_a": _path_a_status(),
87+
"path_b": _path_b_status(),
88+
"path_c": _path_c_status(),
89+
"path_d": _path_d_status(),
90+
}
91+
92+
93+
def kda_auto_mode_for_inputs(*, env_var: str = ENV_VAR) -> PathName:
94+
forced = env_override(env_var)
95+
if forced is not None:
96+
if forced == "path_e":
97+
raise ValueError("KDA has no Path E; use path_a, path_b, path_c, path_d, or auto")
98+
return forced # type: ignore[return-value]
99+
return auto_pick(
100+
kda_path_statuses(),
101+
preference=("path_c", "path_b", "path_d", "path_a"),
102+
)
103+
104+
105+
def kda_recurrent_dispatch(*args, **kwargs):
106+
"""Call the auto-selected KDA backend, falling back to Path A."""
107+
path = kda_auto_mode_for_inputs()
108+
# All B/C/D currently delegate to Path A.
109+
del path
110+
return _fallback(*args, **kwargs)
111+
112+
113+
__all__ = [
114+
"ENV_VAR",
115+
"kda_auto_mode_for_inputs",
116+
"kda_path_statuses",
117+
"kda_recurrent_dispatch",
118+
]

0 commit comments

Comments
 (0)