Skip to content

Commit 20860b7

Browse files
committed
perf(mamba3-bwd): replace lane-disjoint global atomic_add with non-atomic RMW
PROFILE (M4 Max, full-scale local_gb10_quarter, hidden=3584 S=4096): - GPU-vs-CPU split (Metal System Trace, metal-gpu-intervals): the 0.58-0.64s per reverse time-step is ~100% PURE GPU compute. Each launch = ONE Compute Command; CPU->GPU latency ~0.2-1.0ms (<0.2%). Not a CPU/eval/sync problem. - Dispatch: T.Kernel(1, threads=1024) -> a SINGLE threadgroup => ~1/40 of the 40 GPU cores. Confirmed from the generated PrimFunc source. - Replay fraction: checkpoint-interval sweep {8,4,2,1} -> steady 0.64/0.54/ 0.52/0.510s. Even with ZERO replay (interval=1) it is 0.510s/step, so the per-step backward math (not the replay) dominates the floor; each extra replay row adds only ~0.039s. - Limiter: NOT ALU. Diagnostic (atomic->scratch) showed the in_proj_weight_grad scatter loop's entire 0.198s/step cost IS the two relaxed global atomic_add ops -- removing the atomics but keeping the 18784xH loop+MAC was free. Metal serializes relaxed global atomics; the kernel is atomic/memory-bound. FIX: every grad-scatter whose global address is uniquely owned by its thread lane within a launch (in/out-proj weight, conv weight/bias, dt_bias, B/C norm & bias) is converted from T.atomic_add to a plain read-modify-write. The time loop is serial within the single threadgroup, so cross-step accumulation stays correct; no two lanes ever touch the same address in one step. hidden_grad and the shared-scratch reductions (D, a/dt_grad, b/c_group_grad, project_grad angle) genuinely collide across lanes and KEEP their atomics. RESULT (full scale): - no-replay floor: 0.510 -> 0.348s/step (1.47x, -32%) - production interval=8 block average: ~0.595 -> ~0.466s/step (~1.28x, -22%) - window=2 steady: ~1.19s -> ~0.94s (~0.47s/step) GRADIENT PARITY: bitwise-identical. tiny-smoke fwd+bwd, 15 non-zero mamba3 grad buffers, worst max|baseline-fixed| = 0.000e+00. Adds isolation probes: _bwd_shape_introspect (analytic per-step cost), _bwd_dump_source (kernel launch + loop structure), _bwd_ckpt_sweep (replay fraction). The dominant remaining inefficiency is the single-threadgroup dispatch (~1/40 GPU); see report for the grid-parallel feasibility plan.
1 parent b18f415 commit 20860b7

4 files changed

Lines changed: 421 additions & 39 deletions

File tree

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11581,10 +11581,12 @@ def _mamba3_emit_recompute_row(
1158111581
access_by_buffer,
1158211582
f"{out_dim} * {inner_dim} + {feature}",
1158311583
)
11584+
# Lane-disjoint: lane==feature, address==out_dim*inner_dim+feature, so
11585+
# each lane owns a distinct out_proj_weight_grad column. Serial time loop
11586+
# => non-atomic RMW is byte-identical and avoids the global-atomic stall.
1158411587
body.append(
11585-
f"{indent * 8}T.atomic_add({out_grad_ref}, "
11586-
f"{stage_grad}[0] * {out_inner}[{feature}], "
11587-
"memory_order=\"relaxed\")"
11588+
f"{indent * 8}{out_grad_ref} = {out_grad_ref} + "
11589+
f"({stage_grad}[0] * {out_inner}[{feature}])"
1158811590
)
1158911591
body.append(f"{indent * 6}T.sync_threads()")
1159011592
body.append(
@@ -11845,17 +11847,17 @@ def _mamba3_emit_recompute_row(
1184511847
access_by_buffer,
1184611848
grad_flat,
1184711849
)
11850+
# Lane-disjoint: lane==rank_loop, grad_flat=(rank_loop*groups+.)*sd+.
11851+
# so each lane owns distinct addresses; serial time loop => RMW.
1184811852
body.append(
11849-
f"{indent * 9}T.atomic_add({b_norm_grad_ref}, "
11850-
f"{scalar0}[0] * {conv}[{conv_b_offset} + {grad_flat}] * "
11851-
f"{b_inv}[{rank_loop} * {groups} + {group_loop}], "
11852-
"memory_order=\"relaxed\")"
11853+
f"{indent * 9}{b_norm_grad_ref} = {b_norm_grad_ref} + "
11854+
f"({scalar0}[0] * {conv}[{conv_b_offset} + {grad_flat}] * "
11855+
f"{b_inv}[{rank_loop} * {groups} + {group_loop}])"
1185311856
)
1185411857
if b_bias_grad is not None:
1185511858
b_bias_grad_ref = _indexed_buffer_ref(b_bias_grad, access_by_buffer, grad_flat)
1185611859
body.append(
11857-
f"{indent * 9}T.atomic_add({b_bias_grad_ref}, {scalar0}[0], "
11858-
"memory_order=\"relaxed\")"
11860+
f"{indent * 9}{b_bias_grad_ref} = {b_bias_grad_ref} + {scalar0}[0]"
1185911861
)
1186011862
body.append(
1186111863
f"{indent * 9}{conv_grad}[{conv_b_offset} + {grad_flat}] = "
@@ -11876,17 +11878,16 @@ def _mamba3_emit_recompute_row(
1187611878
access_by_buffer,
1187711879
grad_flat,
1187811880
)
11881+
# Lane-disjoint (lane==rank_loop); serial time loop => non-atomic RMW.
1187911882
body.append(
11880-
f"{indent * 9}T.atomic_add({c_norm_grad_ref}, "
11881-
f"{scalar1}[0] * {conv}[{conv_c_offset} + {grad_flat}] * "
11882-
f"{c_inv}[{rank_loop} * {groups} + {group_loop}], "
11883-
"memory_order=\"relaxed\")"
11883+
f"{indent * 9}{c_norm_grad_ref} = {c_norm_grad_ref} + "
11884+
f"({scalar1}[0] * {conv}[{conv_c_offset} + {grad_flat}] * "
11885+
f"{c_inv}[{rank_loop} * {groups} + {group_loop}])"
1188411886
)
1188511887
if c_bias_grad is not None:
1188611888
c_bias_grad_ref = _indexed_buffer_ref(c_bias_grad, access_by_buffer, grad_flat)
1188711889
body.append(
11888-
f"{indent * 9}T.atomic_add({c_bias_grad_ref}, {scalar1}[0], "
11889-
"memory_order=\"relaxed\")"
11890+
f"{indent * 9}{c_bias_grad_ref} = {c_bias_grad_ref} + {scalar1}[0]"
1189011891
)
1189111892
body.append(
1189211893
f"{indent * 9}{conv_grad}[{conv_c_offset} + {grad_flat}] = "
@@ -11935,9 +11936,9 @@ def _mamba3_emit_recompute_row(
1193511936
)
1193611937
if dt_bias_grad is not None:
1193711938
dt_bias_grad_ref = _indexed_buffer_ref(dt_bias_grad, access_by_buffer, hidden_dim)
11939+
# Lane-disjoint (lane==hidden_dim); serial time loop => non-atomic RMW.
1193811940
body.append(
11939-
f"{indent * 9}T.atomic_add({dt_bias_grad_ref}, {scalar3}[0], "
11940-
"memory_order=\"relaxed\")"
11941+
f"{indent * 9}{dt_bias_grad_ref} = {dt_bias_grad_ref} + {scalar3}[0]"
1194111942
)
1194211943
body.append(f"{indent * 9}for {hidden_loop} in T.serial(0, {hidden_size}):")
1194311944
if in_proj_weight_grad is not None:
@@ -11951,15 +11952,16 @@ def _mamba3_emit_recompute_row(
1195111952
access_by_buffer,
1195211953
f"({trap_offset} + {hidden_dim}) * {hidden_size} + {hidden_loop}",
1195311954
)
11955+
# Lane-disjoint (lane==hidden_dim => distinct rows dt_offset+hidden_dim /
11956+
# trap_offset+hidden_dim). The later main scatter touches the same rows
11957+
# only across a sync_threads barrier (sequential) => non-atomic RMW.
1195411958
body.append(
11955-
f"{indent * 10}T.atomic_add({next_dt_grad_ref}, "
11956-
f"{_mamba3_hidden_expr(f'({time_idx} + 1)', hidden_loop)} * {scalar3}[0], "
11957-
"memory_order=\"relaxed\")"
11959+
f"{indent * 10}{next_dt_grad_ref} = {next_dt_grad_ref} + "
11960+
f"({_mamba3_hidden_expr(f'({time_idx} + 1)', hidden_loop)} * {scalar3}[0])"
1195811961
)
1195911962
body.append(
11960-
f"{indent * 10}T.atomic_add({next_trap_grad_ref}, "
11961-
f"{_mamba3_hidden_expr(f'({time_idx} + 1)', hidden_loop)} * {scalar4}[0], "
11962-
"memory_order=\"relaxed\")"
11963+
f"{indent * 10}{next_trap_grad_ref} = {next_trap_grad_ref} + "
11964+
f"({_mamba3_hidden_expr(f'({time_idx} + 1)', hidden_loop)} * {scalar4}[0])"
1196311965
)
1196411966
if hidden_grad is not None:
1196511967
hidden_grad_ref = _indexed_buffer_ref(
@@ -12012,9 +12014,11 @@ def _mamba3_emit_recompute_row(
1201212014
)
1201312015
if dt_bias_grad is not None:
1201412016
dt_bias_grad_ref = _indexed_buffer_ref(dt_bias_grad, access_by_buffer, head)
12017+
# Lane-disjoint (lane==head); separated from the earlier dt_bias scatter
12018+
# by a sync_threads => non-atomic RMW accumulates correctly.
1201512019
body.append(
12016-
f"{indent * 7}T.atomic_add({dt_bias_grad_ref}, "
12017-
f"{dt_grad}[{head}] * {scalar0}[0], memory_order=\"relaxed\")"
12020+
f"{indent * 7}{dt_bias_grad_ref} = {dt_bias_grad_ref} + "
12021+
f"({dt_grad}[{head}] * {scalar0}[0])"
1201812022
)
1201912023
body.append(
1202012024
f"{indent * 7}if -T.log(1.0 + T.exp({projected}[{a_offset} + {head}])) < -0.01:"
@@ -12038,9 +12042,9 @@ def _mamba3_emit_recompute_row(
1203812042
)
1203912043
if conv_bias_grad is not None:
1204012044
conv_bias_ref = _indexed_buffer_ref(conv_bias_grad, access_by_buffer, conv_ch)
12045+
# Lane-disjoint (lane==conv_ch); serial time loop => non-atomic RMW.
1204112046
body.append(
12042-
f"{indent * 7}T.atomic_add({conv_bias_ref}, {scalar1}[0], "
12043-
"memory_order=\"relaxed\")"
12047+
f"{indent * 7}{conv_bias_ref} = {conv_bias_ref} + {scalar1}[0]"
1204412048
)
1204512049
if history_len > 0:
1204612050
body.append(f"{indent * 7}for {kernel_pos} in T.serial(0, {history_len}):")
@@ -12072,9 +12076,11 @@ def _mamba3_emit_recompute_row(
1207212076
access_by_buffer,
1207312077
f"{conv_ch} * {kernel} + {kernel_pos}",
1207412078
)
12079+
# Lane-disjoint: lane==conv_ch owns row conv_ch*kernel+. ; serial time
12080+
# loop => non-atomic RMW byte-identical to the atomic.
1207512081
body.append(
12076-
f"{indent * 8}T.atomic_add({conv_weight_grad_ref}, "
12077-
f"{scalar1}[0] * {scalar2}[0], memory_order=\"relaxed\")"
12082+
f"{indent * 8}{conv_weight_grad_ref} = {conv_weight_grad_ref} + "
12083+
f"({scalar1}[0] * {scalar2}[0])"
1207812084
)
1207912085
body.append(f"{indent * 8}if {src_row} >= 0:")
1208012086
body.append(f"{indent * 9}for {hidden_loop} in T.serial(0, {hidden_size}):")
@@ -12084,11 +12090,13 @@ def _mamba3_emit_recompute_row(
1208412090
access_by_buffer,
1208512091
f"({x_offset} + {conv_ch}) * {hidden_size} + {hidden_loop}",
1208612092
)
12093+
# Lane-disjoint: lane==conv_ch => row (x_offset+conv_ch)*H+. ; the
12094+
# main in-proj scatter (a sync_threads later) hits the same rows only
12095+
# SEQUENTIALLY, so non-atomic RMW accumulates correctly.
1208712096
body.append(
12088-
f"{indent * 10}T.atomic_add({in_proj_grad_ref}, "
12089-
f"{_mamba3_hidden_expr(src_row, hidden_loop)} * {scalar1}[0] * "
12090-
f"{_mamba3_conv_weight_expr(conv_ch, kernel_pos)}, "
12091-
"memory_order=\"relaxed\")"
12097+
f"{indent * 10}{in_proj_grad_ref} = {in_proj_grad_ref} + "
12098+
f"({_mamba3_hidden_expr(src_row, hidden_loop)} * {scalar1}[0] * "
12099+
f"{_mamba3_conv_weight_expr(conv_ch, kernel_pos)})"
1209212100
)
1209312101
if hidden_grad is not None:
1209412102
hidden_grad_ref = _indexed_buffer_ref(
@@ -12108,10 +12116,11 @@ def _mamba3_emit_recompute_row(
1210812116
access_by_buffer,
1210912117
f"{conv_ch} * {kernel} + {history_len}",
1211012118
)
12119+
# Lane-disjoint (lane==conv_ch); serial time loop => non-atomic RMW.
1211112120
body.append(
12112-
f"{indent * 7}T.atomic_add({current_conv_weight_grad_ref}, "
12113-
f"{scalar1}[0] * {projected}[{x_offset} + {conv_ch}], "
12114-
"memory_order=\"relaxed\")"
12121+
f"{indent * 7}{current_conv_weight_grad_ref} = "
12122+
f"{current_conv_weight_grad_ref} + "
12123+
f"({scalar1}[0] * {projected}[{x_offset} + {conv_ch}])"
1211512124
)
1211612125
body.append(
1211712126
f"{indent * 7}{project_grad}[{x_offset} + {conv_ch}] = "
@@ -12130,12 +12139,18 @@ def _mamba3_emit_recompute_row(
1213012139
access_by_buffer,
1213112140
f"{proj_dim} * {hidden_size} + {hidden_loop}",
1213212141
)
12142+
# Lane-disjoint scatter: lane==proj_dim, so each lane owns a distinct
12143+
# in_proj_weight_grad ROW (proj_dim*H + .) and the serial time loop runs
12144+
# one step at a time. No two threads ever target the same address within
12145+
# a launch -> a non-atomic read-modify-write is byte-identical to the
12146+
# atomic and ~10x cheaper (relaxed global atomics serialize on Metal).
1213312147
body.append(
12134-
f"{indent * 8}T.atomic_add({in_proj_grad_ref}, "
12135-
f"{_mamba3_hidden_expr(time_idx, hidden_loop)} * {project_grad}[{proj_dim}], "
12136-
"memory_order=\"relaxed\")"
12148+
f"{indent * 8}{in_proj_grad_ref} = {in_proj_grad_ref} + "
12149+
f"({_mamba3_hidden_expr(time_idx, hidden_loop)} * {project_grad}[{proj_dim}])"
1213712150
)
1213812151
if hidden_grad is not None:
12152+
# hidden_grad[time_idx*H + hidden_loop]: ALL proj_dim lanes accumulate
12153+
# into the same hidden_loop column -> lanes COLLIDE; keep atomic.
1213912154
hidden_grad_ref = _indexed_buffer_ref(
1214012155
hidden_grad,
1214112156
access_by_buffer,

scripts/_bwd_ckpt_sweep.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
"""Replay-fraction probe: steady per-launch time vs checkpoint interval.
3+
4+
Drives the SAME mamba3 bwd window=1 launches as the window sweep, but for a
5+
given ``--checkpoint-interval`` (monkeypatched). Because window=1 fixes one
6+
reverse time-step per launch and the replay loop re-runs the forward recompute
7+
for ``[checkpoint_start, time_idx]`` (up to checkpoint_interval rows), the steady
8+
per-launch time isolates the REPLAY cost. Smaller interval => fewer replays =>
9+
less recompute => faster, IF replay dominates.
10+
11+
We time the FIRST launch (time_idx=4095) where the replay length == the worst
12+
case for that interval, plus the steady launches. Reports per-launch time so we
13+
can attribute the fraction that is checkpoint-replay vs the fixed backward math.
14+
"""
15+
from __future__ import annotations
16+
17+
import argparse
18+
import dataclasses
19+
import json
20+
import sys
21+
import time
22+
import traceback
23+
from pathlib import Path
24+
from typing import Any
25+
26+
ROOT = Path(__file__).resolve().parents[1]
27+
for p in (str(ROOT), str(ROOT / "scripts")):
28+
if p not in sys.path:
29+
sys.path.insert(0, p)
30+
31+
import mlx.core as mx # noqa: E402
32+
import m04_train_step as m # noqa: E402
33+
import cppmega_mlx.runtime.path_c_fusion_schedules as sched # noqa: E402
34+
35+
36+
def _find_mamba3_segment(chain):
37+
for seg in chain.segments:
38+
if any(n.op_name == "mamba3_mimo_bwd" for n in seg.region.nodes):
39+
return seg
40+
raise RuntimeError("no mamba3_mimo_bwd segment")
41+
42+
43+
def main() -> int:
44+
parser = argparse.ArgumentParser()
45+
parser.add_argument("--checkpoint-interval", type=int, required=True)
46+
parser.add_argument("--max-launches", type=int, default=5)
47+
args = parser.parse_args()
48+
49+
sched.MAMBA3_BWD_ROWS_PER_KERNEL_LAUNCH = 1
50+
sched.MAMBA3_BWD_REPLAY_CHECKPOINT_INTERVAL = int(args.checkpoint_interval)
51+
print(f"[ckpt] checkpoint_interval={args.checkpoint_interval} window=1", flush=True)
52+
53+
profile, route_symbols, regions = m._local_gb10_path_c_model_regions()
54+
sel = m._select_path_c_model_route_region(regions)
55+
scheduled = m.plan_path_c_fusion_schedule_for_region(sel, include_backward=True)
56+
chain = m.plan_path_c_direct_fusion_chain_for_region(
57+
scheduled.region, include_backward=True,
58+
)
59+
mamba_seg = _find_mamba3_segment(chain)
60+
artifacts = m.compile_path_c_direct_fusion_chain_artifacts(chain)
61+
sub_chain = dataclasses.replace(chain, segments=(mamba_seg,))
62+
63+
launch_times: list[float] = []
64+
state: dict[str, Any] = {"last": None}
65+
real_sync = mx.synchronize
66+
67+
def timed_sync(*a, **k):
68+
r = real_sync(*a, **k)
69+
now = time.perf_counter()
70+
if state["last"] is not None:
71+
launch_times.append(now - state["last"])
72+
state["last"] = now
73+
return r
74+
75+
mx.synchronize = timed_sync
76+
77+
real_launches_fn = m._path_c_segment_time_chunk_launches
78+
limit = int(args.max_launches)
79+
80+
def limited_launches(pf):
81+
full = real_launches_fn(pf)
82+
return full[:limit] if full else full
83+
84+
m._path_c_segment_time_chunk_launches = limited_launches
85+
86+
specs = m._path_c_direct_chain_required_logical_buffer_specs(sub_chain)
87+
buffers = {}
88+
for name, spec in specs.items():
89+
dtype = getattr(mx, str(spec["dtype"]))
90+
buffers[name] = mx.zeros(tuple(int(d) for d in spec["shape"]), dtype=dtype)
91+
mx.eval(*buffers.values())
92+
93+
status = "ok"
94+
err = None
95+
t0 = time.perf_counter()
96+
state["last"] = t0
97+
try:
98+
m.run_path_c_direct_fusion_chain_route(
99+
chain=sub_chain, logical_buffers=buffers, artifacts=artifacts,
100+
)
101+
except BaseException as exc: # noqa: BLE001
102+
status = "FAIL"
103+
err = f"{type(exc).__name__}: {exc}"
104+
traceback.print_exc()
105+
finally:
106+
mx.synchronize = real_sync
107+
m._path_c_segment_time_chunk_launches = real_launches_fn
108+
109+
for i, dt in enumerate(launch_times):
110+
print(f"[ckpt] launch[{i}] = {dt:.3f}s", flush=True)
111+
steady = (sum(launch_times[1:]) / max(1, len(launch_times) - 1)
112+
if len(launch_times) > 1 else None)
113+
out = {
114+
"checkpoint_interval": int(args.checkpoint_interval),
115+
"status": status,
116+
"error": err,
117+
"first_launch_s": round(launch_times[0], 4) if launch_times else None,
118+
"steady_per_launch_s": round(steady, 4) if steady is not None else None,
119+
}
120+
print("\n[ckpt] RESULT:\n" + json.dumps(out, indent=2), flush=True)
121+
return 0 if status == "ok" else 7
122+
123+
124+
if __name__ == "__main__":
125+
raise SystemExit(main())

0 commit comments

Comments
 (0)