@@ -104,10 +104,19 @@ def _gdn_backward_kernel(
104104 f"caller should fall back to Path A grad path"
105105 )
106106 # Multi-simdgroup path when dh > 32: pad threadgroup to a 32-multiple
107- # so simd_sum still works (32 lanes per simdgroup), and use
108- # atomic_fetch_add_explicit for cross-simdgroup grad accumulation.
109- use_atomic = dh > _SIMD_WIDTH
107+ # so simd_sum still works (32 lanes per simdgroup). Cross-simdgroup
108+ # reductions go through a threadgroup-shared-memory tile instead of
109+ # atomic_fetch_add_explicit (atomics serialised badly at Dh>=64 and
110+ # killed the multi-simdgroup speedup).
111+ use_shared = dh > _SIMD_WIDTH
110112 tg_size = ((dh + _SIMD_WIDTH - 1 ) // _SIMD_WIDTH ) * _SIMD_WIDTH
113+ n_simd = tg_size // _SIMD_WIDTH # number of simdgroups in a threadgroup
114+ # Shared-memory budget: one batched "[dh, n_simd]" tile reused across the
115+ # three vector reductions (dq, dk, propagation) plus tiny [n_simd] tiles
116+ # for the two scalar reductions (dbeta, d_alpha). Worst case Dh=256 with
117+ # 8 simdgroups → 256*8*4 = 8192 bytes vector tile + 32+32 = 8256 bytes
118+ # total per threadgroup, well below the 32KB M-series limit.
119+ shared_bytes = (dh * n_simd + 2 * n_simd ) * 4 if use_shared else 0
111120
112121 scale = dh ** - 0.5
113122 q_f = q .astype (mx .float32 ).reshape (- 1 )
@@ -132,15 +141,26 @@ def _gdn_backward_kernel(
132141 # and after T~64 steps tiny rounding errors blow up to inf / NaN.
133142 # Mirroring mamba3_path_c's switch from inverse-walk to cached
134143 # snapshots (see _bwd_state_snapshots_kernel_for).
144+ # Shared-mem declarations only emitted in the multi-simdgroup path.
145+ shared_decls = (
146+ f"""
147+ threadgroup float tg_vec[{ dh * n_simd } ]; // batched per-i partials
148+ threadgroup float tg_scalar0[{ n_simd } ]; // scalar reduction A
149+ threadgroup float tg_scalar1[{ n_simd } ]; // scalar reduction B
150+ """ if use_shared else ""
151+ )
152+
135153 source = f"""
136154 uint tid_in_tg = thread_position_in_threadgroup.x;
137155 uint bh = threadgroup_position_in_grid.x;
138156 uint j = tid_in_tg;
139157 bool active = (j < { dh } u) && (bh < { b * h } u);
158+ uint simd_id = tid_in_tg / 32u;
159+ uint lane = tid_in_tg & 31u;
140160
141161 uint bb = bh / { h } u;
142162 uint head = bh % { h } u;
143-
163+ { shared_decls }
144164 float state[{ dh } ];
145165 float dS[{ dh } ];
146166 float v_eff_hist[{ max (t , 1 )} ];
@@ -240,19 +260,28 @@ def _gdn_backward_kernel(
240260 float contrib = active ? (dY_j * S_t_col[i]) : 0.0f;
241261 float dq_i_sum = simd_sum(contrib);
242262 { (
243- f'''if (active && (j & 31u) == 0u) {{
244- atomic_fetch_add_explicit(
245- (device atomic_float*)&dq[kv_base + i],
246- dq_i_sum * { scale } f, memory_order_relaxed
247- );
248- }}'''
249- if use_atomic else
263+ f'''// Stash this simdgroup's partial; one barrier amortises
264+ // all dh writes (no atomic contention).
265+ if (lane == 0u) tg_vec[i * { n_simd } + simd_id] = dq_i_sum;'''
266+ if use_shared else
250267 f'''if (active && j == 0u) {{
251268 dq[kv_base + i] = dq_i_sum * { scale } f;
252269 }}'''
253270 )}
254271 dS[i] += dY_j * q_i_scaled;
255272 }}
273+ { (
274+ f'''threadgroup_barrier(metal::mem_flags::mem_threadgroup);
275+ // Reduce: one thread per output cell; lanes >= dh idle.
276+ if (tid_in_tg < { dh } u) {{
277+ int oi = (int)tid_in_tg;
278+ float total = 0.0f;
279+ for (int s = 0; s < { n_simd } ; s++) total += tg_vec[oi * { n_simd } + s];
280+ dq[kv_base + oi] = total * { scale } f;
281+ }}
282+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);'''
283+ if use_shared else ""
284+ )}
256285
257286 // ---- (2) S_t[i,j] = S_decayed[i,j] + k_i * v_eff_t ----
258287 // dv_eff[j] = sum_i dS_t[i,j] * k_i (per-j scalar)
@@ -270,17 +299,19 @@ def _gdn_backward_kernel(
270299 float dv_j = dv_eff_j * beta_t;
271300 float dkth_j = -dv_eff_j * beta_t;
272301 float dbeta_contrib = active ? (dv_eff_j * (v_j - kth_t)) : 0.0f;
273- float dbeta_total = simd_sum(dbeta_contrib);
302+ float dbeta_simd = simd_sum(dbeta_contrib);
274303 if (active) dv[kv_base + j] = dv_j;
275304 { (
276- f'''if (active && (j & 31u) == 0u) {{
277- atomic_fetch_add_explicit(
278- (device atomic_float*)&dbeta[g_idx],
279- dbeta_total, memory_order_relaxed
280- );
281- }}'''
282- if use_atomic else
283- f'''if (active && j == 0u) dbeta[g_idx] = dbeta_total;'''
305+ f'''if (lane == 0u) tg_scalar0[simd_id] = dbeta_simd;
306+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
307+ if (tid_in_tg == 0u) {{
308+ float total = 0.0f;
309+ for (int s = 0; s < { n_simd } ; s++) total += tg_scalar0[s];
310+ dbeta[g_idx] = total;
311+ }}
312+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);'''
313+ if use_shared else
314+ f'''if (active && j == 0u) dbeta[g_idx] = dbeta_simd;'''
284315 )}
285316
286317 // ---- (4) kth[j] = sum_i k_i * S_decayed[i,j] ----
@@ -291,16 +322,22 @@ def _gdn_backward_kernel(
291322 float dk_kth = active ? (dkth_j * S_decayed[i]) : 0.0f;
292323 float dk_i_sum = simd_sum(dk_delta + dk_kth);
293324 { (
294- f'''if (active && (j & 31u) == 0u) {{
295- atomic_fetch_add_explicit(
296- (device atomic_float*)&dk[kv_base + i],
297- dk_i_sum, memory_order_relaxed
298- );
299- }}'''
300- if use_atomic else
325+ f'''if (lane == 0u) tg_vec[i * { n_simd } + simd_id] = dk_i_sum;'''
326+ if use_shared else
301327 f'''if (active && j == 0u) dk[kv_base + i] = dk_i_sum;'''
302328 )}
303329 }}
330+ { (
331+ f'''threadgroup_barrier(metal::mem_flags::mem_threadgroup);
332+ if (tid_in_tg < { dh } u) {{
333+ int oi = (int)tid_in_tg;
334+ float total = 0.0f;
335+ for (int s = 0; s < { n_simd } ; s++) total += tg_vec[oi * { n_simd } + s];
336+ dk[kv_base + oi] = total;
337+ }}
338+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);'''
339+ if use_shared else ""
340+ )}
304341
305342 // dS_decayed[i,j] = dS_t[i,j] + dkth[j] * k_i
306343 for (int i = 0; i < { dh } ; i++) {{
@@ -316,16 +353,18 @@ def _gdn_backward_kernel(
316353 for (int i = 0; i < { dh } ; i++) {{
317354 d_alpha_partial += dS[i] * S_prev[i];
318355 }}
319- float d_alpha_total = simd_sum(active ? d_alpha_partial : 0.0f);
356+ float d_alpha_simd = simd_sum(active ? d_alpha_partial : 0.0f);
320357 { (
321- f'''if (active && (j & 31u) == 0u) {{
322- atomic_fetch_add_explicit(
323- (device atomic_float*)&dg[g_idx],
324- d_alpha_total * alpha_t, memory_order_relaxed
325- );
326- }}'''
327- if use_atomic else
328- f'''if (active && j == 0u) dg[g_idx] = d_alpha_total * alpha_t;'''
358+ f'''if (lane == 0u) tg_scalar1[simd_id] = d_alpha_simd;
359+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);
360+ if (tid_in_tg == 0u) {{
361+ float total = 0.0f;
362+ for (int s = 0; s < { n_simd } ; s++) total += tg_scalar1[s];
363+ dg[g_idx] = total * alpha_t;
364+ }}
365+ threadgroup_barrier(metal::mem_flags::mem_threadgroup);'''
366+ if use_shared else
367+ f'''if (active && j == 0u) dg[g_idx] = d_alpha_simd * alpha_t;'''
329368 )}
330369
331370 // Carry dS to next (earlier) timestep.
@@ -405,7 +444,8 @@ def _gdn_apply_path_b_vjp(
405444 del output
406445 q , k , v , beta , g = primals
407446 # Constraints for the real-MSL bwd kernel: shapes match + Dh <= 256
408- # (multi-simdgroup path, atomic accumulation for cross-simdgroup grads).
447+ # (multi-simdgroup path, threadgroup-shared-memory accumulation across
448+ # simdgroups — replaced atomic_fetch_add to remove serialisation).
409449 dh = q .shape [- 1 ]
410450 bwd_ok = (
411451 k .shape == q .shape
0 commit comments