Skip to content

Commit 4635186

Browse files
committed
perf(v4): GDN Path B bwd — threadgroup shared-mem reductions replace atomics
Multi-simdgroup path (Dh > 32) previously serialised cross-simdgroup reductions through atomic_fetch_add_explicit, killing the parallelism we gained from spreading the j-axis across 2..8 simdgroups. This commit switches to a batched threadgroup-shared-memory reduction: - Each simdgroup writes its simd_sum() partial to tg_vec[i*N + simd_id] (one tile reused across dq, dk, propagation; tg_scalar0/1 for dbeta, d_alpha). - One threadgroup_barrier amortises all dh stash writes per scalar output. - Threads with tid < dh perform the final cross-simdgroup sum and write directly to global memory (no atomic contention). Worst-case shared memory: dh * n_simd * 4 + 2 * n_simd * 4 bytes = 256 * 8 * 4 + 64 = 8256 bytes per threadgroup (Dh=256, 8 simdgroups), well under the 32KB M-series limit. Single-simdgroup fast path (Dh <= 32) unchanged. Tests stay green at original tolerances (atol=1e-5/2e-4).
1 parent 6c7c13e commit 4635186

1 file changed

Lines changed: 77 additions & 37 deletions

File tree

cppmega_v4/_tilelang/linear_attention_path_b_bwd.py

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)