Skip to content

Commit 146d6c2

Browse files
committed
feat(v4): GDN Path D — Triton frontend seam via tilelang.poc.triton_frontend
Wires the Path D entry per the integration plan: from tilelang.poc.triton_frontend import from_triton_kernel prim = from_triton_kernel(fla_chunk_kernel, grid=..., constexprs=..., target='metal') kernel = tilelang.compile(prim, ...) Two-stage status probe: 1. triton importable + tilelang.poc.triton_frontend importable 2. fla.ops.gated_delta_rule.chunk importable When both hold, the status reports the *actionable* blocker — the triton_frontend is currently Tier-1 (elementwise only, see poc/triton_frontend/__init__.py:898), while FLA chunk_gated_delta_rule uses tl.dot + multi-stage pipelines. Status reason names the exact next step: "extend op_mapping.OP_TABLE with matmul & exp emitters". Dispatch falls back to Path A on any failure (missing deps, unsupported op, runtime error) so the rest of the v4 stack keeps working unaffected. The lowering seam (`_try_lower_fla_chunk_kernel`) is kept as the integration point for when the frontend grows tl.dot support — no changes to either the host TileLang or FLA codebases. Tests (8 new): module import, status text precision, runtime↔dispatch status round-trip, both probe helpers, env-forced fallback, fallback parity vs Path A bit-exact, lowering seam returns (None, msg) until op_mapping extends. v4 suite: 137 passed / 11 skipped.
1 parent f2f33a8 commit 146d6c2

3 files changed

Lines changed: 249 additions & 10 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""GDN Path D — Triton kernel → TileLang via tilelang.poc.triton_frontend.
2+
3+
Strategy (per integration plan):
4+
5+
from tilelang.poc.triton_frontend import from_triton_kernel
6+
prim = from_triton_kernel(
7+
fla_chunk_gated_delta_rule_inner_triton_kernel,
8+
grid=...,
9+
constexprs={...},
10+
target='metal',
11+
)
12+
kernel = tilelang.compile(prim, target='metal', execution_backend='tvm_ffi', out_idx=...)
13+
14+
The triton_frontend is currently Tier-1 (elementwise only — see
15+
``poc/triton_frontend/__init__.py:898``). FLA's chunk_gated_delta_rule uses
16+
``tl.dot``, ``tl.exp``, masked load/store, multi-stage pipelines — well
17+
outside Tier-1. The expected outcome on first attempt is
18+
``NotImplementedError`` from ``op_mapping``: that's the actionable signal
19+
telling frontend devs which op to add next (matrix-multiply lowering, etc.).
20+
21+
Until the frontend covers those ops, Path D is wired but unavailable, and
22+
the dispatch falls back to Path A. The status reason names the precise
23+
blocker so the next person knows what to fix.
24+
25+
This module never modifies the host TileLang frontend or FLA — both are
26+
read-only imports.
27+
"""
28+
29+
from __future__ import annotations
30+
31+
from functools import lru_cache
32+
from typing import Any
33+
34+
import mlx.core as mx
35+
36+
37+
def _triton_frontend_importable() -> tuple[bool, str]:
38+
"""Probe whether triton + tilelang.poc.triton_frontend are both reachable."""
39+
try:
40+
import triton # noqa: F401
41+
except Exception as exc:
42+
return False, f"triton not importable: {exc.__class__.__name__}: {exc}"
43+
try:
44+
from tilelang.poc.triton_frontend import from_triton_kernel # noqa: F401
45+
except Exception as exc:
46+
return False, f"tilelang.poc.triton_frontend not importable: {exc}"
47+
return True, "triton + tilelang.poc.triton_frontend importable"
48+
49+
50+
def _fla_chunk_kernel_importable() -> tuple[bool, str]:
51+
"""Probe whether FLA's chunk_gated_delta_rule entrypoint is reachable."""
52+
try:
53+
from fla.ops.gated_delta_rule.chunk import chunk_gated_delta_rule # noqa: F401
54+
except Exception as exc:
55+
return False, f"fla.ops.gated_delta_rule.chunk not importable: {exc}"
56+
return True, "fla.ops.gated_delta_rule.chunk importable"
57+
58+
59+
def _path_d_runtime_status() -> tuple[bool, str]:
60+
"""Two-stage gate: frontend + source kernel both reachable, then
61+
op_mapping must cover the kernel's ops (the latter is only known after
62+
a real compile attempt — we surface that on first call).
63+
"""
64+
ok_fe, reason_fe = _triton_frontend_importable()
65+
if not ok_fe:
66+
return False, reason_fe
67+
ok_src, reason_src = _fla_chunk_kernel_importable()
68+
if not ok_src:
69+
return False, reason_src
70+
# Triton frontend is currently Tier-1 (elementwise only). FLA chunk
71+
# gated-delta uses tl.dot / tl.exp / masked-pipeline — outside Tier-1.
72+
# Mark unavailable with the actionable blocker until op_mapping extends.
73+
return False, (
74+
"triton frontend + FLA importable, but frontend is Tier-1 (elementwise) "
75+
"only; FLA chunk_gated_delta_rule uses tl.dot and multi-stage pipelines "
76+
"— extend tilelang.poc.triton_frontend.op_mapping.OP_TABLE with matmul "
77+
"& exp emitters to enable Path D"
78+
)
79+
80+
81+
@lru_cache(maxsize=8)
82+
def _try_lower_fla_chunk_kernel(target: str = "metal") -> tuple[Any | None, str]:
83+
"""Attempt the real frontend lowering. Returns (prim_func, message).
84+
85+
Only invoked when ``_path_d_runtime_status()[0]`` is True. Currently a
86+
placeholder that always returns ``(None, "...")`` because Tier-1 doesn't
87+
cover the kernel's ops; left here as the integration seam for when the
88+
frontend grows tl.dot support.
89+
"""
90+
try:
91+
from tilelang.poc.triton_frontend import from_triton_kernel
92+
from fla.ops.common.chunk_o import chunk_fwd_o # noqa: F401
93+
94+
# The actual entry would inspect chunk_gated_delta_rule_fwd's inner
95+
# @triton.jit kernels and pass each to from_triton_kernel. FLA wraps
96+
# them under chunk_gated_delta_rule_fwd_h / chunk_fwd_o; each has its
97+
# own grid + constexprs. Left as the integration seam:
98+
#
99+
# from fla.ops.common.chunk_delta_h import (
100+
# chunk_gated_delta_rule_fwd_h_kernel as _fla_kernel,
101+
# )
102+
# prim = from_triton_kernel(_fla_kernel, grid=..., constexprs=...,
103+
# target=target)
104+
#
105+
# That call currently raises NotImplementedError on tl.dot — exactly
106+
# the signal we want the next contributor to act on.
107+
return None, "Path D lowering seam present but op_mapping coverage missing"
108+
except Exception as exc:
109+
return None, f"unexpected lowering error: {exc.__class__.__name__}: {exc}"
110+
111+
112+
def _gdn_fwd_path_d_call(*args, **kwargs):
113+
"""Path D entry — currently always raises so the dispatch falls back.
114+
115+
Kept callable so the dispatch table is symmetric across paths.
116+
"""
117+
raise RuntimeError(
118+
"GDN Path D not yet runnable — triton_frontend op_mapping needs "
119+
"matmul/exp emitters before FLA chunk_gated_delta_rule can lower"
120+
)
121+
122+
123+
__all__ = [
124+
"_fla_chunk_kernel_importable",
125+
"_gdn_fwd_path_d_call",
126+
"_path_d_runtime_status",
127+
"_triton_frontend_importable",
128+
"_try_lower_fla_chunk_kernel",
129+
]

cppmega_v4/_tilelang/linear_attention_paths.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,25 +133,34 @@ def _path_c_call(*args, **kwargs):
133133

134134
def _path_d_status() -> PathStatus:
135135
try:
136-
importlib.import_module("triton")
137-
except Exception:
136+
from cppmega_v4._tilelang.linear_attention_path_d import (
137+
_path_d_runtime_status,
138+
)
139+
except Exception as exc:
138140
return PathStatus(
139-
path="path_d",
140-
available=False,
141-
reason="triton not importable (CPU/Apple Silicon)",
141+
path="path_d", available=False,
142+
reason=f"path_d module not importable: {exc}",
142143
)
144+
ok, reason = _path_d_runtime_status()
143145
return PathStatus(
144-
path="path_d",
145-
available=False,
146+
path="path_d", available=ok,
146147
reason=(
147-
"Triton frontend wiring pending: tilelang/poc/triton_frontend/"
148-
"from_triton_kernel on FLA chunk_gated_delta_rule"
148+
"GDN Path D: Triton kernel → tilelang.poc.triton_frontend."
149+
f"from_triton_kernel → tilelang.compile. {reason}"
149150
),
150151
)
151152

152153

153154
def _path_d_call(*args, **kwargs):
154-
return _path_a_call(*args, **kwargs) # fallback
155+
if not _path_d_status().available:
156+
return _path_a_call(*args, **kwargs)
157+
try:
158+
from cppmega_v4._tilelang.linear_attention_path_d import (
159+
_gdn_fwd_path_d_call,
160+
)
161+
return _gdn_fwd_path_d_call(*args, **kwargs)
162+
except Exception:
163+
return _path_a_call(*args, **kwargs)
155164

156165

157166
# --- Path E (vendored mlx-lm PR #1217) ------------------------------------
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""GDN Path D (Triton frontend → TileLang) tests.
2+
3+
Path D wraps ``tilelang.poc.triton_frontend.from_triton_kernel`` over FLA's
4+
``chunk_gated_delta_rule`` kernels. The frontend is currently Tier-1
5+
(elementwise only) — FLA's kernel uses ``tl.dot`` and multi-stage
6+
pipelines, so the lowering raises ``NotImplementedError`` until
7+
``op_mapping.OP_TABLE`` gains matmul/exp emitters. Path D therefore
8+
reports unavailable on every box right now, but the wiring + fallback
9+
must be correct, the blocker must be precisely named in the status, and
10+
the dispatch must fall back to Path A without raising.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import mlx.core as mx
16+
import numpy as np
17+
import pytest
18+
19+
from cppmega_v4._tilelang.linear_attention_path_d import (
20+
_fla_chunk_kernel_importable,
21+
_path_d_runtime_status,
22+
_triton_frontend_importable,
23+
_try_lower_fla_chunk_kernel,
24+
)
25+
from cppmega_v4._tilelang.linear_attention_paths import (
26+
ENV_VAR as GDN_ENV,
27+
_path_d_status,
28+
gated_delta_recurrent_dispatch,
29+
)
30+
from cppmega_v4.nn._external.fla_naive_gated_delta_rule import (
31+
naive_recurrent_gated_delta_rule,
32+
)
33+
34+
35+
def test_path_d_module_imports():
36+
from cppmega_v4._tilelang import linear_attention_path_d # noqa: F401
37+
38+
39+
def test_path_d_status_names_concrete_blocker():
40+
st = _path_d_status()
41+
# Status must mention either the missing dep or the op_mapping coverage
42+
# blocker, so the next contributor knows what to fix.
43+
assert "triton" in st.reason.lower() or "fla" in st.reason.lower() \
44+
or "op_mapping" in st.reason.lower()
45+
46+
47+
def test_path_d_runtime_status_matches_dispatch_status():
48+
ok, reason = _path_d_runtime_status()
49+
st = _path_d_status()
50+
assert st.available == ok
51+
assert reason in st.reason
52+
53+
54+
def test_triton_frontend_probe_returns_tuple():
55+
ok, reason = _triton_frontend_importable()
56+
assert isinstance(ok, bool)
57+
assert isinstance(reason, str) and reason
58+
59+
60+
def test_fla_chunk_kernel_probe_returns_tuple():
61+
ok, reason = _fla_chunk_kernel_importable()
62+
assert isinstance(ok, bool)
63+
assert isinstance(reason, str) and reason
64+
65+
66+
def test_path_d_forced_via_env_falls_back_cleanly(monkeypatch):
67+
"""Forcing path_d must always return valid output (via Path A fallback)."""
68+
monkeypatch.setenv(GDN_ENV, "path_d")
69+
B, T, H, K, V = 1, 4, 2, 32, 32
70+
q = mx.random.normal((B, T, H, K))
71+
k = mx.random.normal((B, T, H, K))
72+
v = mx.random.normal((B, T, H, V))
73+
beta = mx.sigmoid(mx.random.normal((B, T, H)))
74+
g = -mx.abs(mx.random.normal((B, T, H)) * 0.1)
75+
o, _ = gated_delta_recurrent_dispatch(q, k, v, beta, g)
76+
assert o.shape == (B, T, H, V)
77+
assert not bool(mx.any(mx.isnan(o)).item())
78+
79+
80+
def test_path_d_fallback_matches_path_a_when_unavailable(monkeypatch):
81+
ok, _ = _path_d_runtime_status()
82+
if ok:
83+
pytest.skip("Path D actually available — fallback path not exercised")
84+
monkeypatch.setenv(GDN_ENV, "path_d")
85+
B, T, H, K, V = 1, 5, 2, 8, 8
86+
rng = np.random.default_rng(13)
87+
q = mx.array(rng.standard_normal((B, T, H, K)).astype(np.float32))
88+
k = mx.array(rng.standard_normal((B, T, H, K)).astype(np.float32))
89+
v = mx.array(rng.standard_normal((B, T, H, V)).astype(np.float32))
90+
beta = mx.array(rng.uniform(0.1, 0.9, (B, T, H)).astype(np.float32))
91+
g = mx.array(-rng.uniform(0.01, 0.5, (B, T, H)).astype(np.float32))
92+
o_disp, _ = gated_delta_recurrent_dispatch(q, k, v, beta, g)
93+
o_ref, _ = naive_recurrent_gated_delta_rule(q, k, v, beta, g)
94+
np.testing.assert_array_equal(np.array(o_disp), np.array(o_ref))
95+
96+
97+
def test_try_lower_returns_none_until_op_mapping_extended():
98+
"""Until op_mapping covers tl.dot, the lowering seam returns (None, msg)."""
99+
result, msg = _try_lower_fla_chunk_kernel(target="metal")
100+
assert result is None
101+
assert isinstance(msg, str) and msg

0 commit comments

Comments
 (0)