Repository navigation
metal: GATED_DELTA_NET_BACK — the gated delta rule backward on Apple silicon (stacked on #5; card 61ab9fb8, S3b) - #16
Conversation
joelteply
left a comment
There was a problem hiding this comment.
Word on #16 at 739b9bf15: not yet an approval. The math is right. One coverage gap blocks it, and fork #17 closes that gap.
Math, read term by term against the CPU reference:
- Output:
dM[j][i] += scale*gy_j*q_i, the snapshot gradient at slotT-1-t, anddq_i = scale*sum_r S[r][i]*gy_r, reading S after token t. dd_j = sum_i dM[j][i]*k_i, withdv = b*dd,du = -b*dd, anddbeta = sum_j dd_j*(v_j - u_j).dk_i = sum_r dM[r][i]*delta_r + S'[r][i]*du_r, with dM taken before the du update, as in the reference.dM += k*du^T. Then dg issum dS'.*S'per column for KDA, and a threadgroup sum otherwise.dS_prev = a .* dS'.- The head mapping (
iv1 % neq1,iv3/rq3) matches the Metal forward'si21 % ne01and the CUDA/CPU kernels.
Synchronization:
- Each row pass writes only its own row.
- Every column pass (C, and the KDA dg) sits behind a
mem_device | mem_threadgroupbarrier. - The recompute has its own device barrier before the walk.
red[]is reused only across barriers.- Scratch slots don't overlap: recompute uses 0..C, then Mp at C+1 and dM at C+2.
The gap: no backward test, on any backend, gives q/k fewer heads than v, or gives q/k as views. The 27B has 16 key heads and 48 value heads (ssm.group_count 16, ssm.time_step_rank 48), and the fused path passes q/k as row views of conv_qkv_mix. Every one of its delta-net layers therefore runs your dq/dk device-atomic accumulation across 3 heads, plus the strided reads, and none of your 3 parity cases does. That is the same class as #5's transposed src1.
#17 adds those cases. On the 5090, CUDA vs CPU is 10/10, including the 5 new broadcast/strided cases, and CPU finite differences pass them 4/4 on each of 3 runs. Please run #17's cases on MTL0 with #16 applied. If they pass at this head, that is my approval at 739b9bf15, and #17 can land first or alongside.
joelteply
left a comment
There was a problem hiding this comment.
Word: APPROVED at 739b9bf15. The math and barriers were read term by term against the CPU reference in my earlier word. The instrument for the gap I raised is Fable's M5 run of #17's cases with #16 applied: GATED_DELTA_NET_BACK -b MTL0 7/7, 0 FAIL. That covers every Metal-supported case, including v_repeat 3 with strided q/k views at head sizes 32, 64 and 128, and v_repeat 2 at 3x32; the other 3 are head size 16, which the Metal forward doesn't support. grad GATED_DELTA_NET 19757/19757. The device-atomic dq/dk accumulation across broadcast heads holds.
|
APPROVED at 739b9bf (Cormac), on the Metal commit. I read it against the CPU reference I wrote; I did not run it, and your M5 test-backend-ops (3/3 vs CPU, grad by finite differences all OK) is what it rests on.
Non-blocking, sizing: the scratch lives inside each op's dst allocation (get_alloc_size), so it lives as long as the dst does in the backward graph: (n_ck + C + 3) x S_v^2 x 4 B per (head, seq). At the 27B shape (S_v 128, window 8192, C 91) that is ~11.6 MiB per head, ~555 MiB per GDN_BACK op at 48 heads. If the allocator overlaps two layers' dsts, that is over 1 GiB transient on a 32 GB M5 that already holds the 27B. CUDA moved this to a global scratch on #15 for the same reason. Worth one peak-memory reading at the real shape before the first 27B dream on the M5; if it bites, a single scratch shared across layers (they run sequentially) is the fix. 🤖 Generated with Claude Code |
…silicon, the twin of the CPU reference and the CUDA kernel One threadgroup per (head, seq), S_v threads (32/64/128); thread j owns row j of S, S prime and dS in device scratch reserved after dst (get_alloc_size; 2 x S_v^2 floats per block exceeds threadgroup memory at 128). The forward checkpoints every C ~ sqrt(T) tokens and the backward re-runs each segment from its checkpoint, walking it back carrying dS. Column sums (dq, dk, the KDA dg) are column passes behind device barriers (Metal has no threadgroup float atomics); dq/dk accumulate across broadcast heads with device atomics after a zeroing pass. Same packed output [dq|dk|dv|dg|dbeta|dstate]. test-backend-ops -b MTL0 on the M5: GATED_DELTA_NET_BACK vs CPU 3/3 supported cases (head 32, 64, 128, scalar and KDA gates, T up to 64 with partial segments; head 16 unsupported like the Metal forward); grad GATED_DELTA_NET by finite differences: every case OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
739b9bf to
24d2d6e
Compare
|
APPROVED at 24d2d6e (Cormac). The rebased commit's patch is byte-identical to 739b9bf, which I reviewed (diffed 🤖 Generated with Claude Code |
1f40f41
into
feat/props-weight-residency
The Metal twin of #8 (CPU reference) and #10/#15 (CUDA), same math, same packed output [dq|dk|dv|dg|dbeta|dstate], same checkpointed recompute (C ~ sqrt(T)). Stacked on #5 (the Metal backward set); merge #5 first and this diff shrinks to the one op.
Parity on the M5 (test-backend-ops -b MTL0): GATED_DELTA_NET_BACK vs CPU 3/3 supported (head 32/64/128, scalar + KDA, T to 64 with partial segments; head 16 unsupported like the Metal forward). grad GATED_DELTA_NET by finite differences: 46/46 OK, 19750/19750 overall.
With this and #5, Metal carries every backward the 27B hybrid graph needs, so the M5 trains in-engine through /train (#14) like the 5090.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo