@@ -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