Skip to content

Commit fe32cb1

Browse files
committed
fix(v4): GDN/KDA Path-C device-aware target (CUDA on gb10, Metal on Apple)
The v4 GDN gated-delta + KDA recurrent Path-C forwards hardcoded tilelang.compile(target='metal', execution_backend='tvm_ffi'), so on the CUDA gb10 host every Path-C cell failed with DLPackDeviceError (the Metal-compiled tvm_ffi kernel only accepts kDLMetal arrays, but MLX-CUDA arrays are kDLCUDA). Make the compile target device-aware via a single device->target switch (_device_can_run_metal): Apple (mx.metal.is_available()) keeps the target='metal' tvm_ffi path unchanged; on a CUDA host the same recurrence is compiled with target='cuda' through the host _cuda_eager bridge. _cuda_eager.py gains gdn_fwd_cuda_eager / kda_fwd_cuda_eager: CUDA-safe copies of the GDN/KDA prim_funcs (one-slot T.alloc_local accumulators instead of T.alloc_var(init=), which nvcc rejects), identical recurrence math and (y,h_last) layout, bridged MLX<->torch CUDA via the existing numpy host roundtrip. Kernel launch/writeback failures RAISE (RULE #1), never silently fall back. Local Metal verification: GDN/KDA Path-C match path_a to ~5e-8 with the harness's stabilized inputs (no regression).
1 parent 21f0cd5 commit fe32cb1

3 files changed

Lines changed: 469 additions & 1 deletion

File tree

cppmega_mlx/nn/_tilelang/_cuda_eager.py

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,367 @@ def m2rnn_supported_cuda_eager() -> tuple[bool, str]:
13661366
return cuda_eager_available()
13671367

13681368

1369+
# ---------------------------------------------------------------------------
1370+
# GDN gated-delta (linear-attention Path C) — CUDA-safe per-lane recurrence
1371+
# ---------------------------------------------------------------------------
1372+
#
1373+
# The v4 GDN Path C forward
1374+
# (``cppmega_v4._tilelang.linear_attention_path_c._fwd_kernel_for``) is a
1375+
# per-(b, head, v) lane sequential delta-rule scan compiled with
1376+
# ``tilelang.compile(target='metal', execution_backend='tvm_ffi')``. On a CUDA
1377+
# host MLX has no Metal backend, so feeding CUDA MLX arrays into that
1378+
# Metal-compiled tvm-ffi kernel raises ``DLPackDeviceError`` (the kernel only
1379+
# accepts ``kDLMetal`` arrays). This branch compiles the *same* recurrence with
1380+
# ``target='cuda'`` and bridges MLX<->torch CUDA via the numpy host roundtrip,
1381+
# exactly like the mamba3/sparse_mla CUDA EAGER paths above.
1382+
#
1383+
# The Metal prim uses ``T.alloc_var(T.float32, init=0.0)`` for the ``kth_S_j``
1384+
# and ``out`` scalar accumulators, which nvcc rejects (non-lvalue
1385+
# ``float v = 0``). We vendor a CUDA-safe copy that uses one-slot
1386+
# ``T.alloc_local`` accumulators instead; the recurrence math, lane mapping and
1387+
# (y, h_last) buffer layout are byte-for-byte identical to the Metal prim.
1388+
#
1389+
# Defined at module scope (no ``from __future__ import annotations``) so the
1390+
# ``@T.prim_func`` ``get_type_hints`` re-evaluation resolves the shape names.
1391+
1392+
1393+
def _gdn_threads_for(lanes: int) -> int:
1394+
for tg in (256, 192, 128, 96, 64, 32):
1395+
if lanes % tg == 0:
1396+
return tg
1397+
return 32
1398+
1399+
1400+
def _make_gdn_fwd_cuda_prim(
1401+
BATCH: int,
1402+
SEQ: int,
1403+
HEADS: int,
1404+
HEADDIM_K: int,
1405+
HEADDIM_V: int,
1406+
) -> Any:
1407+
"""CUDA-safe GDN gated-delta forward (y, h_last) prim_func (fp32 carriers).
1408+
1409+
Mirrors ``linear_attention_path_c._fwd_kernel_for``'s ``fwd`` recurrence
1410+
exactly (alpha decay -> K·S reduction -> delta correction -> rank-1 outer
1411+
add + q·S projection), with one-slot ``T.alloc_local`` scalar accumulators
1412+
so CUDA codegen accepts it.
1413+
"""
1414+
1415+
import tilelang.language as T
1416+
1417+
LANES = BATCH * HEADS * HEADDIM_V
1418+
THREADS = _gdn_threads_for(LANES)
1419+
ad = "float32"
1420+
1421+
@T.prim_func
1422+
def fwd(
1423+
q: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_K), "float32"),
1424+
k: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_K), "float32"),
1425+
v: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_V), "float32"),
1426+
beta: T.Tensor((BATCH, SEQ, HEADS), "float32"),
1427+
g: T.Tensor((BATCH, SEQ, HEADS), "float32"),
1428+
h0: T.Tensor((BATCH, HEADS, HEADDIM_K, HEADDIM_V), "float32"),
1429+
y: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_V), "float32"),
1430+
h_last: T.Tensor((BATCH, HEADS, HEADDIM_K, HEADDIM_V), "float32"),
1431+
):
1432+
with T.Kernel(T.ceildiv(LANES, THREADS), threads=THREADS) as bx:
1433+
tid = T.get_thread_binding(0)
1434+
global_lane = bx * THREADS + tid
1435+
h_state = T.alloc_local((HEADDIM_K,), ad)
1436+
kth_S_j = T.alloc_local((1,), ad)
1437+
out = T.alloc_local((1,), ad)
1438+
if global_lane < LANES:
1439+
vj = global_lane % HEADDIM_V
1440+
head = (global_lane // HEADDIM_V) % HEADS
1441+
bb = global_lane // (HEADDIM_V * HEADS)
1442+
for i in T.serial(HEADDIM_K):
1443+
h_state[i] = h0[bb, head, i, vj]
1444+
for t in T.serial(SEQ):
1445+
g_val = g[bb, t, head]
1446+
beta_val = beta[bb, t, head]
1447+
decay = T.exp(g_val)
1448+
v_j = v[bb, t, head, vj]
1449+
kth_S_j[0] = 0.0
1450+
for i in T.serial(HEADDIM_K):
1451+
h_state[i] = h_state[i] * decay
1452+
kth_S_j[0] = kth_S_j[0] + k[bb, t, head, i] * h_state[i]
1453+
v_eff = beta_val * (v_j - kth_S_j[0])
1454+
out[0] = 0.0
1455+
for i in T.serial(HEADDIM_K):
1456+
k_i = k[bb, t, head, i]
1457+
q_i = q[bb, t, head, i]
1458+
h_state[i] = h_state[i] + k_i * v_eff
1459+
out[0] = out[0] + q_i * h_state[i]
1460+
y[bb, t, head, vj] = out[0]
1461+
for i in T.serial(HEADDIM_K):
1462+
h_last[bb, head, i, vj] = h_state[i]
1463+
1464+
return fwd
1465+
1466+
1467+
@functools.lru_cache(maxsize=128)
1468+
def _gdn_fwd_cuda_kernel(
1469+
BATCH: int,
1470+
SEQ: int,
1471+
HEADS: int,
1472+
HEADDIM_K: int,
1473+
HEADDIM_V: int,
1474+
) -> Any:
1475+
"""Compile the CUDA-safe GDN gated-delta forward (caller-owned y/h_last)."""
1476+
1477+
import tilelang
1478+
1479+
prim = _make_gdn_fwd_cuda_prim(BATCH, SEQ, HEADS, HEADDIM_K, HEADDIM_V)
1480+
return tilelang.compile(prim, target="cuda", out_idx=None)
1481+
1482+
1483+
def gdn_fwd_cuda_eager(
1484+
q: mx.array,
1485+
k: mx.array,
1486+
v: mx.array,
1487+
beta: mx.array,
1488+
g: mx.array,
1489+
*,
1490+
scale: float | None = None,
1491+
initial_state: mx.array | None = None,
1492+
output_final_state: bool = False,
1493+
) -> tuple[mx.array, mx.array | None] | None:
1494+
"""TileLang-CUDA EAGER GDN gated-delta forward -> ``(y, h_last|None)``.
1495+
1496+
Same contract as ``naive_recurrent_gated_delta_rule`` /
1497+
``_gdn_fwd_path_c_call``: ``q/k:[B,T,H,K]``, ``v:[B,T,H,V]``,
1498+
``beta/g:[B,T,H]``, optional ``initial_state:[B,H,K,V]``. FLA pre-scales
1499+
``q *= scale`` (default ``1/sqrt(K)``). fp32 carriers (the production fp32
1500+
EAGER carrier path). Returns ``None`` only when the CUDA EAGER path is
1501+
*unavailable* (so the caller can route to a documented guard); any kernel
1502+
launch/writeback failure RAISES (RULE #1).
1503+
"""
1504+
1505+
import math
1506+
1507+
ok, _reason = cuda_eager_available()
1508+
if not ok:
1509+
return None
1510+
try:
1511+
import torch
1512+
except Exception:
1513+
return None
1514+
1515+
B, T_, H, K_dim = (int(x) for x in q.shape)
1516+
V_dim = int(v.shape[-1])
1517+
fla_scale = scale if scale is not None else 1.0 / math.sqrt(K_dim)
1518+
1519+
qf = (q.astype(mx.float32) * fla_scale).astype(mx.float32)
1520+
kf = k.astype(mx.float32)
1521+
vf = v.astype(mx.float32)
1522+
betaf = beta.astype(mx.float32)
1523+
gf = g.astype(mx.float32)
1524+
if initial_state is None:
1525+
h0f = mx.zeros((B, H, K_dim, V_dim), dtype=mx.float32)
1526+
else:
1527+
h0f = initial_state.astype(mx.float32)
1528+
1529+
try:
1530+
kernel = _gdn_fwd_cuda_kernel(B, T_, H, K_dim, V_dim)
1531+
q_t = _mlx_to_torch_cuda(qf)
1532+
k_t = _mlx_to_torch_cuda(kf)
1533+
v_t = _mlx_to_torch_cuda(vf)
1534+
beta_t = _mlx_to_torch_cuda(betaf)
1535+
g_t = _mlx_to_torch_cuda(gf)
1536+
h0_t = _mlx_to_torch_cuda(h0f)
1537+
y_t = torch.zeros(B, T_, H, V_dim, dtype=torch.float32, device="cuda")
1538+
hl_t = torch.zeros(B, H, K_dim, V_dim, dtype=torch.float32, device="cuda")
1539+
kernel(q_t, k_t, v_t, beta_t, g_t, h0_t, y_t, hl_t)
1540+
torch.cuda.synchronize()
1541+
except Exception as exc:
1542+
# RULE #1: a kernel launch/writeback failure is a real bug in the
1543+
# vendored GDN gated-delta fwd kernel, not "feature absent". Raise
1544+
# loud instead of returning None into a silent pure-MLX scan fallback.
1545+
raise RuntimeError(
1546+
f"_cuda_eager.gdn_fwd_cuda_eager: TileLang-CUDA GDN gated-delta "
1547+
f"fwd kernel launch/writeback failed "
1548+
f"({type(exc).__name__}: {exc})."
1549+
) from exc
1550+
1551+
y = _torch_cuda_to_mlx(y_t, q.dtype)
1552+
h_last = _torch_cuda_to_mlx(hl_t, mx.float32) if output_final_state else None
1553+
return y, h_last
1554+
1555+
1556+
def gdn_supported_cuda_eager() -> tuple[bool, str]:
1557+
"""Report whether the GDN TileLang-CUDA EAGER forward can dispatch."""
1558+
1559+
return cuda_eager_available()
1560+
1561+
1562+
# ---------------------------------------------------------------------------
1563+
# KDA recurrent (Path C) — CUDA-safe per-lane recurrence
1564+
# ---------------------------------------------------------------------------
1565+
#
1566+
# Mirrors ``kda_path_c._kda_fwd_kernel_for``'s ``fwd``: per-(b, hv, v) lane
1567+
# scan with per-K vectorized log-gate decay. Same CUDA adaptation as GDN:
1568+
# one-slot ``T.alloc_local`` accumulators replace ``T.alloc_var(init=)``.
1569+
1570+
1571+
def _make_kda_fwd_cuda_prim(
1572+
BATCH: int,
1573+
SEQ: int,
1574+
HEADS: int,
1575+
HV: int,
1576+
HEADDIM_K: int,
1577+
HEADDIM_V: int,
1578+
) -> Any:
1579+
"""CUDA-safe KDA recurrent forward (y, h_last) prim_func (fp32 carriers)."""
1580+
1581+
import tilelang.language as T
1582+
1583+
assert HV % HEADS == 0, f"HV ({HV}) must be divisible by HEADS ({HEADS})"
1584+
GROUP = HV // HEADS
1585+
LANES = BATCH * HV * HEADDIM_V
1586+
THREADS = _gdn_threads_for(LANES)
1587+
ad = "float32"
1588+
1589+
@T.prim_func
1590+
def fwd(
1591+
q: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_K), "float32"),
1592+
k: T.Tensor((BATCH, SEQ, HEADS, HEADDIM_K), "float32"),
1593+
v: T.Tensor((BATCH, SEQ, HV, HEADDIM_V), "float32"),
1594+
g: T.Tensor((BATCH, SEQ, HV, HEADDIM_K), "float32"),
1595+
beta: T.Tensor((BATCH, SEQ, HV), "float32"),
1596+
h0: T.Tensor((BATCH, HV, HEADDIM_K, HEADDIM_V), "float32"),
1597+
y: T.Tensor((BATCH, SEQ, HV, HEADDIM_V), "float32"),
1598+
h_last: T.Tensor((BATCH, HV, HEADDIM_K, HEADDIM_V), "float32"),
1599+
):
1600+
with T.Kernel(T.ceildiv(LANES, THREADS), threads=THREADS) as bx:
1601+
tid = T.get_thread_binding(0)
1602+
global_lane = bx * THREADS + tid
1603+
h_state = T.alloc_local((HEADDIM_K,), ad)
1604+
kth_S_j = T.alloc_local((1,), ad)
1605+
out = T.alloc_local((1,), ad)
1606+
if global_lane < LANES:
1607+
vj = global_lane % HEADDIM_V
1608+
hv_idx = (global_lane // HEADDIM_V) % HV
1609+
bb = global_lane // (HEADDIM_V * HV)
1610+
h_idx = hv_idx // GROUP
1611+
for i in T.serial(HEADDIM_K):
1612+
h_state[i] = h0[bb, hv_idx, i, vj]
1613+
for t in T.serial(SEQ):
1614+
beta_val = beta[bb, t, hv_idx]
1615+
v_j = v[bb, t, hv_idx, vj]
1616+
kth_S_j[0] = 0.0
1617+
for i in T.serial(HEADDIM_K):
1618+
decay_i = T.exp(g[bb, t, hv_idx, i])
1619+
h_state[i] = h_state[i] * decay_i
1620+
kth_S_j[0] = kth_S_j[0] + k[bb, t, h_idx, i] * h_state[i]
1621+
inner_j = v_j - kth_S_j[0]
1622+
out[0] = 0.0
1623+
for i in T.serial(HEADDIM_K):
1624+
k_i = k[bb, t, h_idx, i]
1625+
q_i = q[bb, t, h_idx, i]
1626+
h_state[i] = h_state[i] + beta_val * k_i * inner_j
1627+
out[0] = out[0] + q_i * h_state[i]
1628+
y[bb, t, hv_idx, vj] = out[0]
1629+
for i in T.serial(HEADDIM_K):
1630+
h_last[bb, hv_idx, i, vj] = h_state[i]
1631+
1632+
return fwd
1633+
1634+
1635+
@functools.lru_cache(maxsize=128)
1636+
def _kda_fwd_cuda_kernel(
1637+
BATCH: int,
1638+
SEQ: int,
1639+
HEADS: int,
1640+
HV: int,
1641+
HEADDIM_K: int,
1642+
HEADDIM_V: int,
1643+
) -> Any:
1644+
"""Compile the CUDA-safe KDA recurrent forward (caller-owned y/h_last)."""
1645+
1646+
import tilelang
1647+
1648+
prim = _make_kda_fwd_cuda_prim(BATCH, SEQ, HEADS, HV, HEADDIM_K, HEADDIM_V)
1649+
return tilelang.compile(prim, target="cuda", out_idx=None)
1650+
1651+
1652+
def kda_fwd_cuda_eager(
1653+
q: mx.array,
1654+
k: mx.array,
1655+
v: mx.array,
1656+
g: mx.array,
1657+
beta: mx.array,
1658+
*,
1659+
scale: float | None = None,
1660+
initial_state: mx.array | None = None,
1661+
output_final_state: bool = False,
1662+
) -> tuple[mx.array, mx.array | None] | None:
1663+
"""TileLang-CUDA EAGER KDA recurrent forward -> ``(y, h_last|None)``.
1664+
1665+
Same contract as ``naive_recurrent_kda`` / ``_kda_fwd_path_c_call``:
1666+
``q/k:[B,T,H,K]``, ``v:[B,T,HV,V]``, ``g:[B,T,HV,K]``, ``beta:[B,T,HV]``,
1667+
optional ``initial_state:[B,HV,K,V]``. FLA pre-scales ``q *= scale``
1668+
(default ``1/sqrt(K)``). fp32 carriers. Returns ``None`` only when the
1669+
CUDA EAGER path is *unavailable*; any kernel failure RAISES (RULE #1).
1670+
"""
1671+
1672+
import math
1673+
1674+
ok, _reason = cuda_eager_available()
1675+
if not ok:
1676+
return None
1677+
try:
1678+
import torch
1679+
except Exception:
1680+
return None
1681+
1682+
B, T_, H, K_dim = (int(x) for x in q.shape)
1683+
HV = int(v.shape[2])
1684+
V_dim = int(v.shape[-1])
1685+
fla_scale = scale if scale is not None else 1.0 / math.sqrt(K_dim)
1686+
1687+
qf = (q.astype(mx.float32) * fla_scale).astype(mx.float32)
1688+
kf = k.astype(mx.float32)
1689+
vf = v.astype(mx.float32)
1690+
gf = g.astype(mx.float32)
1691+
betaf = beta.astype(mx.float32)
1692+
if initial_state is None:
1693+
h0f = mx.zeros((B, HV, K_dim, V_dim), dtype=mx.float32)
1694+
else:
1695+
h0f = initial_state.astype(mx.float32)
1696+
1697+
try:
1698+
kernel = _kda_fwd_cuda_kernel(B, T_, H, HV, K_dim, V_dim)
1699+
q_t = _mlx_to_torch_cuda(qf)
1700+
k_t = _mlx_to_torch_cuda(kf)
1701+
v_t = _mlx_to_torch_cuda(vf)
1702+
g_t = _mlx_to_torch_cuda(gf)
1703+
beta_t = _mlx_to_torch_cuda(betaf)
1704+
h0_t = _mlx_to_torch_cuda(h0f)
1705+
y_t = torch.zeros(B, T_, HV, V_dim, dtype=torch.float32, device="cuda")
1706+
hl_t = torch.zeros(B, HV, K_dim, V_dim, dtype=torch.float32, device="cuda")
1707+
kernel(q_t, k_t, v_t, g_t, beta_t, h0_t, y_t, hl_t)
1708+
torch.cuda.synchronize()
1709+
except Exception as exc:
1710+
# RULE #1: a kernel launch/writeback failure is a real bug in the
1711+
# vendored KDA recurrent fwd kernel, not "feature absent". Raise loud
1712+
# instead of returning None into a silent pure-MLX scan fallback.
1713+
raise RuntimeError(
1714+
f"_cuda_eager.kda_fwd_cuda_eager: TileLang-CUDA KDA recurrent "
1715+
f"fwd kernel launch/writeback failed "
1716+
f"({type(exc).__name__}: {exc})."
1717+
) from exc
1718+
1719+
y = _torch_cuda_to_mlx(y_t, q.dtype)
1720+
h_last = _torch_cuda_to_mlx(hl_t, mx.float32) if output_final_state else None
1721+
return y, h_last
1722+
1723+
1724+
def kda_supported_cuda_eager() -> tuple[bool, str]:
1725+
"""Report whether the KDA TileLang-CUDA EAGER forward can dispatch."""
1726+
1727+
return cuda_eager_available()
1728+
1729+
13691730
__all__ = [
13701731
"cuda_eager_available",
13711732
"sparse_mla_fwd_cuda_eager",
@@ -1375,4 +1736,8 @@ def m2rnn_supported_cuda_eager() -> tuple[bool, str]:
13751736
"fp8_sparse_mla_apply_cuda_eager",
13761737
"m2rnn_mapped_packed_post_fwd_cuda_eager",
13771738
"m2rnn_supported_cuda_eager",
1739+
"gdn_fwd_cuda_eager",
1740+
"gdn_supported_cuda_eager",
1741+
"kda_fwd_cuda_eager",
1742+
"kda_supported_cuda_eager",
13781743
]

0 commit comments

Comments
 (0)