Skip to content

Commit 238cbf4

Browse files
committed
feat(v4): bwd for GDN/KDA Path B + wire Path E training=True (PR #1217 VJP)
Three landings, all enabling actual training (fwd+bwd): 1. cppmega_v4/_tilelang/linear_attention_path_b_bwd.py - gdn_apply_path_b(q, k, v, beta, g) wrapped as @mx.custom_function. - Forward = fast Path B Metal kernel (3-40× over A from earlier bench). - VJP = mx.grad through Path A reference (algebraically identical), guarantees correct grads. Pattern named "path_b_fwd_path_a_bwd" mirroring mamba3's mamba3_mimo_apply_with_state_path_c_fwd_path_b_bwd. - Future revision swaps the VJP for a hand-MSL backward kernel; this wrapper already unlocks fwd+bwd training today. 2. cppmega_v4/_tilelang/kda_path_b_bwd.py — same pattern for KDA. 3. Path E training=True wiring: - Vendored mlx-lm PR #1217's gated_delta_vjp.py (179 LoC, Python chunked-VJP reference) and gated_delta_vjp_metal.py (772 LoC, Metal backward kernel — 8-11× over Python). - Patched _mlx_lm_gated_delta_vendored.py imports to point at the vendored module names (was `from .gated_delta_vjp_metal`). - Added `training=True` flag to our Path E adapter (mlx_lm_gated_delta_update.py); when set, dispatches through the chunked Metal VJP path for finite-memory training at T≥2048. Tests (10 new): - test_path_b_bwd.py (4): fwd parity vs Path A; bwd grad parity for (dq, dk, dv, dbeta, dg); fwd+bwd produces finite grads on larger shape; chain through tanh.sum() works. - test_kda_path_b_bwd.py (3): same for KDA. - test_path_e_training.py (3): training=True produces finite output; matches inference within chunk tolerance (atol 1e-2); grads propagate through mx.grad. v4 suite: 273 passed / 2 skipped.
1 parent cedf2f3 commit 238cbf4

9 files changed

Lines changed: 1243 additions & 6 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""KDA Path B forward + backward — same fwd-fast / bwd-correct pattern as GDN."""
2+
3+
import mlx.core as mx
4+
5+
from cppmega_v4._tilelang.kda_path_b import kda_forward_path_b
6+
from cppmega_v4.nn._external.fla_naive_kda import naive_recurrent_kda
7+
8+
9+
@mx.custom_function
10+
def kda_apply_path_b(
11+
q: mx.array, k: mx.array, v: mx.array, g: mx.array, beta: mx.array,
12+
) -> mx.array:
13+
"""Forward via fast Path B Metal kernel; backward via Path A reference grad."""
14+
y, _ = kda_forward_path_b(q, k, v, g, beta, output_final_state=False)
15+
return y
16+
17+
18+
@kda_apply_path_b.vjp
19+
def _kda_apply_path_b_vjp(primals, cotangent, output):
20+
del output
21+
q, k, v, g, beta = primals
22+
23+
def _loss_proxy(q_, k_, v_, g_, beta_):
24+
y, _ = naive_recurrent_kda(q_, k_, v_, g_, beta_)
25+
return (y * cotangent).sum()
26+
27+
return mx.grad(_loss_proxy, argnums=(0, 1, 2, 3, 4))(q, k, v, g, beta)
28+
29+
30+
__all__ = ["kda_apply_path_b"]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""GDN Path B forward + backward — fwd via fast Metal kernel, bwd via
2+
mx.grad through Path A reference.
3+
4+
This is the *fwd-fast / bwd-correct* path, named ``path_b_fwd_path_a_bwd`` in
5+
the spirit of mamba3's ``mamba3_mimo_apply_with_state_path_c_fwd_path_b_bwd``.
6+
The fast hand-MSL kernel handles forward (3-10× over Path A), while
7+
backward delegates to ``mx.grad`` through the FLA-naive reference so
8+
training is at least *correct* even if not yet fully accelerated.
9+
10+
A future revision (tracked: cppmega_v4._tilelang.linear_attention_path_b_bwd_metal)
11+
replaces the backward with a fused Metal kernel; until then this wrapper
12+
unlocks full V4-stack training with measurable forward speedup.
13+
14+
API:
15+
forward_with_grad(q, k, v, beta, g) -> (o, h_last)
16+
Implemented as ``mx.custom_function`` so ``mx.grad`` traces through.
17+
"""
18+
19+
from typing import Optional
20+
21+
import mlx.core as mx
22+
23+
from cppmega_v4._tilelang.linear_attention_path_b import gdn_forward_path_b
24+
from cppmega_v4.nn._external.fla_naive_gated_delta_rule import (
25+
naive_recurrent_gated_delta_rule,
26+
)
27+
28+
29+
@mx.custom_function
30+
def gdn_apply_path_b(
31+
q: mx.array, k: mx.array, v: mx.array, beta: mx.array, g: mx.array,
32+
) -> mx.array:
33+
"""Forward-only call returning y (no state). Differentiable via custom VJP.
34+
35+
The fast Path B Metal kernel produces y; the VJP differentiates the
36+
Path A reference (algebraically identical), so gradients are correct.
37+
"""
38+
y, _ = gdn_forward_path_b(q, k, v, beta, g, output_final_state=False)
39+
return y
40+
41+
42+
@gdn_apply_path_b.vjp
43+
def _gdn_apply_path_b_vjp(
44+
primals: tuple,
45+
cotangent: mx.array,
46+
output: mx.array,
47+
) -> tuple:
48+
"""Backward via mx.grad through the FLA naive reference.
49+
50+
Path A reference is algebraically identical to Path B (Path B is just
51+
a faster MSL implementation of the same recurrence), so grads from
52+
Path A are correct grads for Path B. This is the "path_b_fwd_path_a_bwd"
53+
pattern — see mamba3_path_c's analogous ``path_c_fwd_path_b_bwd`` mode.
54+
"""
55+
del output # unused — VJP re-runs forward through Path A
56+
q, k, v, beta, g = primals
57+
58+
def _loss_proxy(q_, k_, v_, beta_, g_):
59+
y, _ = naive_recurrent_gated_delta_rule(q_, k_, v_, beta_, g_)
60+
return (y * cotangent).sum()
61+
62+
grad_fn = mx.grad(_loss_proxy, argnums=(0, 1, 2, 3, 4))
63+
return grad_fn(q, k, v, beta, g)
64+
65+
66+
__all__ = ["gdn_apply_path_b"]

cppmega_v4/nn/_external/_mlx_lm_gated_delta_vendored.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,15 +308,15 @@ def gated_delta_update(
308308
)
309309
if can_use_metal:
310310
try:
311-
from .gated_delta_vjp_metal import gated_delta_update_vjp_metal
312-
311+
from ._mlx_lm_gated_delta_vjp_metal_vendored import (
312+
gated_delta_update_vjp_metal,
313+
)
313314
return gated_delta_update_vjp_metal(
314315
q, k, v, a, b, A_log, dt_bias, state, mask
315316
)
316317
except ImportError:
317318
pass
318-
from .gated_delta_vjp import gated_delta_update_vjp
319-
319+
from ._mlx_lm_gated_delta_vjp_vendored import gated_delta_update_vjp
320320
return gated_delta_update_vjp(q, k, v, a, b, A_log, dt_bias, state, mask)
321321

322322
beta = mx.sigmoid(b)

0 commit comments

Comments
 (0)