Skip to content

Commit 4a62306

Browse files
committed
fix(v4): GDN/KDA Path-B device-aware target (CUDA on gb10, Metal on Apple)
The v4 GDN gated-delta + KDA recurrent Path-B forwards run a hand-MSL kernel via mx.fast.metal_kernel, so on the CUDA gb10 host every Path-B cell failed with RuntimeError: [metal_kernel] No Metal back-end. Make the forward device-aware via the same single device->target switch Path-C uses (_device_can_run_metal): Apple (mx.metal.is_available()) keeps the hand-MSL mx.fast.metal_kernel path byte-for-byte unchanged; on a CUDA host the same GDN/KDA recurrence routes through the host _cuda_eager bridge (gdn_fwd_cuda_eager / kda_fwd_cuda_eager — the exact vendored CUDA-safe prim_funcs Path-C already uses on CUDA). CUDA EAGER failures RAISE (RULE #1) — never a silent fall-through to the Apple MSL kernel (which cannot run on CUDA) or to Path A. Local Metal verification: GDN/KDA Path-B match path_a to ~4.5e-8 with the harness's stabilized inputs (no regression — Metal path unchanged).
1 parent fe32cb1 commit 4a62306

2 files changed

Lines changed: 118 additions & 1 deletion

File tree

cppmega_v4/_tilelang/kda_path_b.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,24 @@ def _kda_forward_kernel(
198198
return out.reshape(b, t, hv, vdim), sf.reshape(b, hv, kdim, vdim)
199199

200200

201+
def _device_can_run_metal() -> bool:
202+
"""True iff the live MLX device is an Apple GPU with a working Metal backend.
203+
204+
The single device->target switch for KDA Path B (mirrors
205+
``kda_path_c._device_can_run_metal``): True -> hand-MSL
206+
``mx.fast.metal_kernel`` (Apple); False -> the host ``_cuda_eager`` bridge
207+
(the CUDA gb10 host where ``mx.metal.is_available()`` is False but the
208+
default device is the CUDA gpu). Running ``mx.fast.metal_kernel`` on CUDA
209+
arrays raises ``RuntimeError: [metal_kernel] No Metal back-end.``
210+
"""
211+
metal = getattr(mx, "metal", None)
212+
return (
213+
mx.default_device() == mx.gpu
214+
and metal is not None
215+
and metal.is_available()
216+
)
217+
218+
201219
def kda_forward_path_b(
202220
q: mx.array,
203221
k: mx.array,
@@ -211,8 +229,15 @@ def kda_forward_path_b(
211229
):
212230
"""KDA Path B forward, signature matching ``naive_recurrent_kda``.
213231
232+
Device-aware target (mirrors KDA Path C): on Apple run the hand-MSL
233+
``mx.fast.metal_kernel`` forward; on a CUDA host route the same KDA
234+
recurrence through the host ``_cuda_eager`` bridge (the MSL kernel raises
235+
``[metal_kernel] No Metal back-end.`` on CUDA arrays). CUDA EAGER failures
236+
RAISE (RULE #1).
237+
214238
Falls back to Path A only when HV is not divisible by H (architectural
215-
mismatch). Supports ``initial_state`` and custom ``scale``.
239+
mismatch — a selection guard, not an on-failure fallback). Supports
240+
``initial_state`` and custom ``scale``.
216241
"""
217242
if v.shape[2] % q.shape[2] != 0:
218243
from cppmega_v4.nn._external.fla_naive_kda import naive_recurrent_kda
@@ -221,6 +246,38 @@ def kda_forward_path_b(
221246
scale=scale, initial_state=initial_state,
222247
output_final_state=output_final_state,
223248
)
249+
250+
# CUDA EAGER branch: Metal is unavailable, so the hand-MSL kernel cannot
251+
# accept the CUDA-resident MLX arrays. Run the same KDA recurrence via the
252+
# host _cuda_eager bridge (target='cuda'), the exact vendored prim_func KDA
253+
# Path C uses on CUDA.
254+
if not _device_can_run_metal():
255+
from cppmega_mlx.nn._tilelang._cuda_eager import (
256+
cuda_eager_available,
257+
kda_fwd_cuda_eager,
258+
)
259+
260+
cuda_ok, cuda_reason = cuda_eager_available()
261+
if not cuda_ok:
262+
raise RuntimeError(
263+
"KDA Path B: Metal backend unavailable (mx.fast.metal_kernel "
264+
"cannot run on the CUDA host) and the TileLang-CUDA EAGER "
265+
f"forward is also unavailable: {cuda_reason}. Use path_a or "
266+
"path_c on CUDA."
267+
)
268+
result = kda_fwd_cuda_eager(
269+
q, k, v, g, beta,
270+
scale=scale,
271+
initial_state=initial_state,
272+
output_final_state=output_final_state,
273+
)
274+
if result is None:
275+
raise RuntimeError(
276+
"KDA Path B: TileLang-CUDA EAGER dispatch returned None "
277+
"(cuda_eager_available() was True — this is a bug)"
278+
)
279+
return result
280+
224281
o, sf = _kda_forward_kernel(q, k, v, g, beta, scale=scale, h0=initial_state)
225282
return o, (sf if output_final_state else None)
226283

cppmega_v4/_tilelang/linear_attention_path_b.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,25 @@ def _gdn_forward_kernel(
199199
return o, state_final
200200

201201

202+
def _device_can_run_metal() -> bool:
203+
"""True iff the live MLX device is an Apple GPU with a working Metal backend.
204+
205+
The single device->target switch for Path B (mirrors
206+
``linear_attention_path_c._device_can_run_metal``): when True the hand-MSL
207+
``mx.fast.metal_kernel`` forward runs (the existing Apple path); when False
208+
(the CUDA gb10 host where ``mx.default_device()`` is the CUDA gpu but
209+
``mx.metal.is_available()`` is False) Path B must route to the host
210+
``_cuda_eager`` bridge, otherwise ``mx.fast.metal_kernel`` raises
211+
``RuntimeError: [metal_kernel] No Metal back-end.`` on the CUDA host.
212+
"""
213+
metal = getattr(mx, "metal", None)
214+
return (
215+
mx.default_device() == mx.gpu
216+
and metal is not None
217+
and metal.is_available()
218+
)
219+
220+
202221
def gdn_forward_path_b(
203222
q: mx.array,
204223
k: mx.array,
@@ -212,11 +231,52 @@ def gdn_forward_path_b(
212231
):
213232
"""Path B forward, signature matching ``naive_recurrent_gated_delta_rule``.
214233
234+
Device-aware target (mirrors GDN Path C): on Apple (Metal available) run
235+
the hand-MSL ``mx.fast.metal_kernel`` forward unchanged. On a CUDA host
236+
(Metal unavailable, ``mx.default_device()`` is the CUDA gpu) route the same
237+
GDN gated-delta recurrence through the host ``_cuda_eager`` bridge — the
238+
MSL kernel's ``mx.fast.metal_kernel`` would otherwise raise
239+
``RuntimeError: [metal_kernel] No Metal back-end.`` on the CUDA arrays.
240+
241+
Any failure of the CUDA EAGER path RAISES (RULE #1) — never a silent
242+
fall-through to the Apple MSL kernel (which cannot run on CUDA anyway).
243+
215244
Supports:
216245
- ``initial_state`` shape [B, H, K, V] (streaming decode).
217246
- Custom ``scale`` (defaults to 1/sqrt(K) per FLA convention).
218247
- ``head_k_dim != head_v_dim``.
219248
"""
249+
# CUDA EAGER branch: Metal is unavailable, so the hand-MSL kernel cannot
250+
# accept the CUDA-resident MLX arrays. Run the same GDN gated-delta
251+
# recurrence via the host _cuda_eager bridge (target='cuda'), the exact
252+
# same vendored prim_func GDN Path C uses on CUDA.
253+
if not _device_can_run_metal():
254+
from cppmega_mlx.nn._tilelang._cuda_eager import (
255+
cuda_eager_available,
256+
gdn_fwd_cuda_eager,
257+
)
258+
259+
cuda_ok, cuda_reason = cuda_eager_available()
260+
if not cuda_ok:
261+
raise RuntimeError(
262+
"GDN Path B: Metal backend unavailable (mx.fast.metal_kernel "
263+
"cannot run on the CUDA host) and the TileLang-CUDA EAGER "
264+
f"forward is also unavailable: {cuda_reason}. Use path_a or "
265+
"path_c on CUDA."
266+
)
267+
result = gdn_fwd_cuda_eager(
268+
q, k, v, beta, g,
269+
scale=scale,
270+
initial_state=initial_state,
271+
output_final_state=output_final_state,
272+
)
273+
if result is None:
274+
raise RuntimeError(
275+
"GDN Path B: TileLang-CUDA EAGER dispatch returned None "
276+
"(cuda_eager_available() was True — this is a bug)"
277+
)
278+
return result
279+
220280
o, sf = _gdn_forward_kernel(q, k, v, beta, g, scale=scale, h0=initial_state)
221281
return o, (sf if output_final_state else None)
222282

0 commit comments

Comments
 (0)