Skip to content

Commit 54332ac

Browse files
committed
fix(path-c): Metal BACKWARD watchdog cap + row-chunk independent heavy bwd ops; isolate mamba3 compiler-crash blocker
Clears the FIRST full-scale direct-chain BACKWARD watchdog blocker (region ..._chain_7_10) and the m2rnn_bwd watchdog, and precisely isolates the remaining deeper blocker (mamba3_mimo_bwd Metal pipeline-state compiler crash). ROOT CAUSE (measured at full local_gb10_quarter: depth=13 hidden=3584 max_seq=4096, each backward op ISOLATED in a FRESH process so a prior GPU error does not pollute): seg op dispatch shader monolithic result --- ------------------------------ -------------- ------- ----------------------------- 4 sparse_mla_fp8_apply_bwd grid_chunks 88 KB ~12s -> WATCHDOG TIMEOUT (+attn_qkv_bwd+rmsnorm_bwd grid_chunks 115 KB ~10-25s -> WATCHDOG (chain_7_10) fused as 3-op ..._chain_7_10) 5 attention_qkv_projection_bwd grid_chunks 40 KB ~10s -> WATCHDOG TIMEOUT 7 m2rnn_bwd launcher_chunks 64 KB 512 launches x ~4.0s (near edge) 9 mamba3_mimo_bwd launcher_chunks 129 KB XPC COMPILER CRASH 9 mamba3_mimo_bwd grid_chunks 127 KB XPC COMPILER CRASH Key findings: * The watchdog at ..._chain_7_10 is NOT just "several ops summing in one command buffer": BOTH sparse_mla_fp8_apply_bwd AND attention_qkv_projection_bwd time out the ~5-6s macOS GPU watchdog even ISOLATED in a 1-op grid_chunks command buffer. These ops are per-row-INDEPENDENT (each output row depends only on its own input row + shared weights; NO carried reverse-time state). * mamba3_mimo_bwd CRASHES MTLCompilerService at newComputePipelineState (XPC_ERROR_CONNECTION_INTERRUPTED) in BOTH grid_chunks AND launcher_chunks at full scale. The kernel is a fixed ~127 KB / 114-MSL-loop body INDEPENDENT of hidden_size (tested 896/1792/3584) and sequence_length (tested 512/2048/4096) -- only runtime loop bounds change, never the emitted code. The metallib build succeeds; the crash is the lazy AIR->GPU-ISA pipeline-state stage. fix (d)'s premise ("mamba3 monolithic is watchdog-safe at production S") is FALSE at full hidden=3584: mamba3 backward does not compile to a pipeline state in any dispatch mode. Splitting that single op's 114-loop emitter into multiple pipeline states is a codegen restructure (cppmega Python descriptor fragment emitter) with gradient-correctness risk -- left as the documented deeper blocker. FIX (Metal-gated; CUDA unchanged; explicit by target/op; RULE #1: route RAISES on failure, no silent fallback): * METAL_BACKWARD_MAX_SEGMENT_NODES = 1 (resolve-from-target sentinel; CUDA=None) -- sibling of METAL_FORWARD_MAX_SEGMENT_NODES. Splits the 3-op ..._chain_7_10 backward mega-kernel into one segment per op. New planner param backward_max_segment_nodes on plan_path_c_direct_fusion_chain_for_region / plan_path_c_direct_fusion_chains_for_model. * _ROW_CHUNKED_INDEPENDENT_BACKWARD_OPS = {sparse_mla_fp8_apply_bwd, attention_qkv_projection_bwd} -- these per-row-independent heavy ops now take the SAME launcher_chunks row-windowing as the recurrent reverse-scan ops, splitting each into K=512 command buffers of 8 rows. No carry buffers are added for them (_row_phased_launcher_carry_buffers_for_nodes adds carries ONLY for mamba3/m2rnn); path_c_first_row_launch zeroes owner grads once on (chunk 0, subchunk 0). VERIFIED (full local_gb10_quarter scale, real measurements): * sparse_mla_fp8_apply_bwd row-chunked: ran END-TO-END status=ok, 512 launches, ~1.3s/launch, total wall 676.9s -- NO watchdog timeout (was ~12s monolithic kill). * attention_qkv_projection_bwd row-chunked: ~0.42s/launch -- watchdog-safe. * Determinism: the 12 (sparse_mla: 9) DIRECT-WRITE grad buffers are BIT-EXACT (max_abs_diff = 0.0) across two independent row-windowed runs on identical inputs; only the relaxed-T.atomic_add reductions (rope_inv_freq_grad, residual_norm_hidden_grad) vary by ~5e-6 float-reassociation noise, which is dispatch-independent and inherent to the op -- NOT a row-chunking bug. * Full-scale chain now plans + native-compiles all 11 segments (forward 4 + backward 7). New grouping: bwd seg4 sparse_mla(launcher), seg5 attn_qkv(launcher), seg6 rmsnorm(grid), seg7 m2rnn(launcher), seg8 rmsnorm(grid), seg9 mamba3(launcher, COMPILER-CRASHES at pipeline state), seg10 entry_rmsnorm(grid). * Smoke-scale executor test runs the FULL route end-to-end status=ok with the new 11-segment grouping (mamba3 compiles at hidden=16). REMAINING BLOCKER: the full backward cannot complete end-to-end at full scale because mamba3_mimo_bwd does not compile to a Metal pipeline state (127 KB / 114 loops) in any dispatch mode. This is the next deeper fix (split the mamba3 backward emitter into multiple pipeline states), not a watchdog/grouping issue. Tests: test_path_c_fusion_ir.py 119 passed (was 116/3-fail: fixed 3 stale "blocked"->"ready" mamba3-no-longer-blocked-in-planner assertions). Updated 6 m04 tests for the legitimately-changed Metal backward grouping (11 segments; +5 cross-segment backward grads exposed by the split: qkv q/kv fp8+scale grads + R residual_norm_hidden_grad). Remaining m04 failures are pre-existing on baseline (CLI/data/JSONDecodeError/tokens), unrelated to this change. Probe scripts (full-scale reproducers): scripts/_bwdgate_per_segment_probe.py, _bwdgate_launch_probe.py, _bwdgate_rowchunk_probe.py, _bwdgate_granularity_probe.py, _bwdgate_determinism_probe.py.
1 parent 154ce8f commit 54332ac

8 files changed

Lines changed: 874 additions & 89 deletions

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,39 @@
161161
"mamba3_mimo_bwd",
162162
}
163163
)
164+
# Per-row-INDEPENDENT heavy BACKWARD ops that exceed the macOS GPU watchdog
165+
# (~5-6s per command buffer) when run as ONE monolithic grid_chunks command
166+
# buffer at full local_gb10_quarter scale (depth=13 hidden=3584 max_seq=4096).
167+
#
168+
# Measured (full scale, each op ISOLATED in its own segment, grid_chunks, run in
169+
# a FRESH process so no prior GPU error pollutes the device):
170+
# sparse_mla_fp8_apply_bwd monolithic grid_chunks -> ~12s -> WATCHDOG KILL
171+
# (kIOGPUCommandBufferCallbackErrorTimeout)
172+
# attention_qkv_projection_bwd monolithic grid_chunks -> ~10s -> WATCHDOG KILL
173+
# residual_rmsnorm_bwd monolithic grid_chunks -> ~0.08s -> safe
174+
# Both heavy ops time out the watchdog even ALONE (not from summing several ops
175+
# in one command buffer), so a fused-segment op cap alone does NOT clear them --
176+
# each needs its single op split across multiple command buffers over its
177+
# per-row axis. Unlike the recurrent reverse-time scans (m2rnn_bwd /
178+
# mamba3_mimo_bwd), these ops are per-row-INDEPENDENT: each output row depends
179+
# only on its own input row plus the shared weights, with NO carried reverse-time
180+
# state. So launcher_chunks row-windowing splits them into K command buffers over
181+
# rows with zero cross-launch state (see _row_phased_launcher_carry_buffers_for_nodes
182+
# which adds carry buffers ONLY for mamba3/m2rnn, never for these). The weight
183+
# gradients still accumulate correctly: path_c_first_row_launch zeroes the owner
184+
# grads exactly once on the first (chunk 0, subchunk 0) launch, then every launch
185+
# adds its rows' contribution.
186+
# Measured row-windowed (launcher_chunks, max_rows_per_launch=64 ->
187+
# rows_per_kernel_launch=8 -> 512 launches of 8 rows each):
188+
# attention_qkv_projection_bwd ~0.42s per launch -> watchdog-safe.
189+
# Metal-only (CUDA's compiler/scheduler has no such per-command-buffer watchdog,
190+
# so CUDA keeps the monolithic grid_chunks path).
191+
_ROW_CHUNKED_INDEPENDENT_BACKWARD_OPS = frozenset(
192+
{
193+
"sparse_mla_fp8_apply_bwd",
194+
"attention_qkv_projection_bwd",
195+
}
196+
)
164197
DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_BUFFERS = frozenset(
165198
{
166199
"attention_hidden",
@@ -231,6 +264,31 @@
231264
# limit, so this cap is gated to Metal only (CUDA keeps greedy forward fusion).
232265
METAL_FORWARD_MAX_SEGMENT_NODES = 2
233266

267+
# Metal-only BACKWARD fused-segment op-count cap.
268+
#
269+
# Sibling of METAL_FORWARD_MAX_SEGMENT_NODES, for the reverse-autograd backward
270+
# phase. The greedy direct-chain planner otherwise fuses backward ops up to the
271+
# portable buffer limit, producing the 3-op backward mega-kernel
272+
# ``sparse_mla_fp8_apply_bwd + attention_qkv_projection_bwd + residual_rmsnorm_bwd``
273+
# (region ``..._chain_7_10``). Measured at full local_gb10_quarter scale that
274+
# fused segment is ONE ~115 KB grid_chunks command buffer that runs ~10-25s of
275+
# GPU time and is killed by the macOS GPU watchdog
276+
# (kIOGPUCommandBufferCallbackErrorTimeout) -- the FIRST backward segment to die.
277+
# Capping backward segments at 1 op puts each backward op in its OWN segment /
278+
# command buffer, which (a) keeps each command buffer small and (b) lets the two
279+
# per-row-INDEPENDENT heavy ops (sparse_mla_fp8_apply_bwd,
280+
# attention_qkv_projection_bwd; see _ROW_CHUNKED_INDEPENDENT_BACKWARD_OPS) get
281+
# launcher_chunks row-windowing so each command buffer holds only a row window's
282+
# worth of work (~0.42s) -- well under the watchdog. The light backward ops
283+
# (residual_rmsnorm_bwd / entry_rmsnorm_bwd) run monolithically in ~0.08s.
284+
#
285+
# NOTE: an op cap alone does NOT clear sparse_mla_fp8_apply_bwd /
286+
# attention_qkv_projection_bwd -- each times out the watchdog even ISOLATED in a
287+
# 1-op segment, so isolation MUST be paired with row-windowing (the cap of 1
288+
# guarantees the isolation those ops need to be row-chunked). CUDA has no such
289+
# watchdog, so the cap is Metal-only (CUDA keeps greedy backward fusion).
290+
METAL_BACKWARD_MAX_SEGMENT_NODES = 1
291+
234292

235293
class _ResolveFromTarget:
236294
"""Sentinel: resolve a planner default from the active lowering target.
@@ -246,6 +304,7 @@ def __repr__(self) -> str: # pragma: no cover - debug aid
246304

247305

248306
_RESOLVE_FORWARD_CAP_FROM_TARGET = _ResolveFromTarget()
307+
_RESOLVE_BACKWARD_CAP_FROM_TARGET = _ResolveFromTarget()
249308
_GENERIC_MODEL_REAL_ABI_INPUT_SUFFIXES = (
250309
"residual_norm_weight",
251310
# Block A: the per-brick entry RMSNorm weight emitted by the model-
@@ -14230,6 +14289,9 @@ def plan_path_c_direct_fusion_chain_for_region(
1423014289
forward_max_segment_nodes: int | None | _ResolveFromTarget = (
1423114290
_RESOLVE_FORWARD_CAP_FROM_TARGET
1423214291
),
14292+
backward_max_segment_nodes: int | None | _ResolveFromTarget = (
14293+
_RESOLVE_BACKWARD_CAP_FROM_TARGET
14294+
),
1423314295
registry: PathCFusionScheduleRegistry | None = None,
1423414296
) -> PathCFusionScheduleChainPlan:
1423514297
"""Greedily split a Path C region into direct-buffer fused segments.
@@ -14249,11 +14311,22 @@ def plan_path_c_direct_fusion_chain_for_region(
1424914311
``MTLCompilerService`` at ``newComputePipelineState`` with
1425014312
``XPC_ERROR_CONNECTION_INTERRUPTED`` — splits into two smaller
1425114313
pipeline-safe kernels); on CUDA it is ``None`` (no cap, the CUDA compiler
14252-
has no such pipeline-state size limit, so CUDA fusion is unchanged). The
14253-
cap applies ONLY to forward segments; backward segments keep greedy fusion
14254-
up to the buffer limit (their reverse-time watchdog split is handled
14255-
separately via launcher_chunks). Pass an explicit value to override; pass
14256-
``None`` to disable the cap entirely.
14314+
has no such pipeline-state size limit, so CUDA fusion is unchanged).
14315+
14316+
``backward_max_segment_nodes`` is the sibling Metal-only cap for the
14317+
reverse-autograd BACKWARD phase. The default
14318+
(``_RESOLVE_BACKWARD_CAP_FROM_TARGET``) resolves to
14319+
:data:`METAL_BACKWARD_MAX_SEGMENT_NODES` = 1 on Metal and ``None`` on CUDA.
14320+
Capping backward segments at 1 op splits the 3-op backward mega-kernel
14321+
``sparse_mla_fp8_apply_bwd + attention_qkv_projection_bwd +
14322+
residual_rmsnorm_bwd`` (region ``..._chain_7_10``, ~115 KB, ~10-25s GPU time
14323+
-> macOS GPU watchdog kill) into one segment per op, which both shrinks each
14324+
command buffer AND lets the per-row-INDEPENDENT heavy ops
14325+
(``sparse_mla_fp8_apply_bwd`` / ``attention_qkv_projection_bwd``;
14326+
:data:`_ROW_CHUNKED_INDEPENDENT_BACKWARD_OPS`) get launcher_chunks
14327+
row-windowing so each command buffer holds only a row window (~0.42s) -- well
14328+
under the watchdog. Pass an explicit value to override; pass ``None`` to
14329+
disable (CUDA keeps greedy backward fusion).
1425714330
"""
1425814331

1425914332
if not isinstance(region, PathCFusionRegion):
@@ -14279,6 +14352,23 @@ def plan_path_c_direct_fusion_chain_for_region(
1427914352
raise ValueError(
1428014353
"forward_max_segment_nodes must be positive when provided"
1428114354
)
14355+
if isinstance(backward_max_segment_nodes, _ResolveFromTarget):
14356+
# Resolve the backward fusion cap from the active lowering target: Metal
14357+
# caps at 1 op/segment (so the watchdog-killing 3-op backward segment
14358+
# ..._chain_7_10 splits per op and the heavy per-row-independent ops can
14359+
# be row-windowed); CUDA stays uncapped (no watchdog).
14360+
backward_max_segment_nodes = (
14361+
METAL_BACKWARD_MAX_SEGMENT_NODES
14362+
if _path_c_default_target() == "metal"
14363+
else None
14364+
)
14365+
if (
14366+
backward_max_segment_nodes is not None
14367+
and backward_max_segment_nodes <= 0
14368+
):
14369+
raise ValueError(
14370+
"backward_max_segment_nodes must be positive when provided"
14371+
)
1428214372
working_region = (
1428314373
build_path_c_aot_autograd_region(region)
1428414374
if include_backward
@@ -14335,6 +14425,25 @@ def plan_path_c_direct_fusion_chain_for_region(
1433514425
f"(Metal newComputePipelineState size cap)"
1433614426
)
1433714427
break
14428+
# BACKWARD-only Metal watchdog cap. Backward fused segments larger
14429+
# than ``backward_max_segment_nodes`` ops run as ONE grid_chunks
14430+
# command buffer whose GPU time exceeds the macOS GPU watchdog
14431+
# (kIOGPUCommandBufferCallbackErrorTimeout) -- the 3-op
14432+
# ..._chain_7_10 mega-kernel was the FIRST backward segment to die.
14433+
# Stop extending the backward segment so each backward op lands in
14434+
# its own command buffer (and the per-row-independent heavy ops can
14435+
# then be row-windowed below). Forward segments are unaffected.
14436+
if (
14437+
execution_phase == DESCRIPTOR_EXECUTION_STAGE_BACKWARD
14438+
and backward_max_segment_nodes is not None
14439+
and end - start > backward_max_segment_nodes
14440+
):
14441+
first_failure = (
14442+
f"backward direct-buffer segment reached "
14443+
f"backward_max_segment_nodes={backward_max_segment_nodes} "
14444+
f"(Metal GPU watchdog command-buffer cap)"
14445+
)
14446+
break
1433814447
target = selector.select(candidate_region)
1433914448
if target is None:
1434014449
first_failure = (
@@ -14372,12 +14481,30 @@ def plan_path_c_direct_fusion_chain_for_region(
1437214481
# mamba3_mimo_bwd; m2rnn_bwd runs FIRST in the reverse chain and tripped
1437314482
# the watchdog before mamba3 was reached (verified timeout at region
1437414483
# ..._chain_10_11 / op=m2rnn_bwd), so both recurrent ops are included.
14484+
#
14485+
# ROW-WINDOWING for the per-row-INDEPENDENT heavy backward ops
14486+
# (sparse_mla_fp8_apply_bwd / attention_qkv_projection_bwd; see
14487+
# _ROW_CHUNKED_INDEPENDENT_BACKWARD_OPS). At full scale each of these
14488+
# times out the macOS GPU watchdog even ISOLATED in its own 1-op
14489+
# grid_chunks command buffer (~10-12s). They have NO carried
14490+
# reverse-time state (each output row depends only on its own input
14491+
# row + shared weights), so launcher_chunks splits them into K command
14492+
# buffers over the independent ROW axis with zero cross-launch state
14493+
# (_row_phased_launcher_carry_buffers_for_nodes adds carry buffers ONLY
14494+
# for mamba3/m2rnn, never for these). Weight grads still accumulate
14495+
# correctly via the path_c_first_row_launch one-time owner-grad zero.
14496+
# Both recurrent and independent watchdog-heavy ops therefore take the
14497+
# SAME launcher_chunks treatment; the carry-buffer plumbing distinguishes
14498+
# them downstream. (Metal-only watchdog split; CUDA keeps grid_chunks.)
1437514499
if (
1437614500
execution_phase == DESCRIPTOR_EXECUTION_STAGE_BACKWARD
1437714501
and direct_target.max_rows_per_launch is not None
1437814502
and any(
1437914503
_path_c_descriptor_stage_node_op_name(node)
14380-
in _TIME_CHUNKED_RECURRENT_BACKWARD_OPS
14504+
in (
14505+
_TIME_CHUNKED_RECURRENT_BACKWARD_OPS
14506+
| _ROW_CHUNKED_INDEPENDENT_BACKWARD_OPS
14507+
)
1438114508
for node in candidate_region.nodes
1438214509
)
1438314510
):
@@ -14480,6 +14607,9 @@ def plan_path_c_direct_fusion_chains_for_model(
1448014607
forward_max_segment_nodes: int | None | _ResolveFromTarget = (
1448114608
_RESOLVE_FORWARD_CAP_FROM_TARGET
1448214609
),
14610+
backward_max_segment_nodes: int | None | _ResolveFromTarget = (
14611+
_RESOLVE_BACKWARD_CAP_FROM_TARGET
14612+
),
1448314613
registry: PathCFusionScheduleRegistry | None = None,
1448414614
sequence_length: int | None = None,
1448514615
) -> tuple[PathCFusionScheduleChainPlan, ...]:
@@ -14490,9 +14620,9 @@ def plan_path_c_direct_fusion_chains_for_model(
1449014620
model's brick graph first, then split each discovered region into generic
1449114621
direct-buffer segments. It does not consult named acceptance fixtures.
1449214622

14493-
``forward_max_segment_nodes`` is forwarded to
14494-
:func:`plan_path_c_direct_fusion_chain_for_region` (Metal-only forward
14495-
fusion cap; see that function's docstring).
14623+
``forward_max_segment_nodes`` / ``backward_max_segment_nodes`` are forwarded
14624+
to :func:`plan_path_c_direct_fusion_chain_for_region` (Metal-only fusion caps
14625+
for the forward / backward phases; see that function's docstring).
1449614626
"""
1449714627

1449814628
regions = build_path_c_model_regions_from_model(
@@ -14509,6 +14639,7 @@ def plan_path_c_direct_fusion_chains_for_model(
1450914639
max_kernel_buffers=max_kernel_buffers,
1451014640
max_segment_nodes=max_segment_nodes,
1451114641
forward_max_segment_nodes=forward_max_segment_nodes,
14642+
backward_max_segment_nodes=backward_max_segment_nodes,
1451214643
registry=registry,
1451314644
)
1451414645
for region in regions
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python3
2+
"""FAST determinism check for row-windowed (launcher_chunks) independent backward.
3+
4+
Runs the FIRST N row-window launches of a per-row-INDEPENDENT backward op TWICE
5+
on the same random inputs (re-initialising owner grads each time), and asserts
6+
every DIRECT-WRITE grad buffer is bit-exact across the two runs. The only
7+
non-deterministic buffer is rope_inv_freq_grad (reduced via relaxed T.atomic_add,
8+
verified in the generated source) -- its run-to-run float-reassociation variance
9+
is inherent to the op in ANY dispatch mode and is reported (relative) but not
10+
required to be bit-exact. This proves the row-windowing + path_c_first_row_launch
11+
one-time owner-grad zero is deterministic on the non-atomic grads.
12+
"""
13+
from __future__ import annotations
14+
15+
import argparse
16+
import dataclasses
17+
import sys
18+
from pathlib import Path
19+
from typing import Any
20+
21+
import numpy as np
22+
23+
ROOT = Path(__file__).resolve().parents[1]
24+
for p in (str(ROOT), str(ROOT / "scripts")):
25+
if p not in sys.path:
26+
sys.path.insert(0, p)
27+
28+
import mlx.core as mx # noqa: E402
29+
import m04_train_step as m # noqa: E402
30+
import cppmega_mlx.runtime.path_c_fusion_schedules as S # noqa: E402
31+
32+
PATH_C_SCALAR_KERNEL_PARAM_DEFAULTS = m.PATH_C_SCALAR_KERNEL_PARAM_DEFAULTS
33+
path_c_kernel_buffer_order = m.path_c_kernel_buffer_order
34+
35+
36+
def main() -> int:
37+
parser = argparse.ArgumentParser()
38+
parser.add_argument("--op", default="attention_qkv_projection_bwd")
39+
parser.add_argument("--max-launches", type=int, default=24)
40+
args = parser.parse_args()
41+
42+
profile, route_symbols, regions = m._local_gb10_path_c_model_regions()
43+
sel = m._select_path_c_model_route_region(regions)
44+
scheduled = m.plan_path_c_fusion_schedule_for_region(sel, include_backward=True)
45+
chain = m.plan_path_c_direct_fusion_chain_for_region(scheduled.region, include_backward=True)
46+
seg = next(s for s in chain.segments if [n.op_name for n in s.region.nodes] == [args.op])
47+
seg0 = dataclasses.replace(seg, index=0)
48+
chain1 = dataclasses.replace(chain, segments=(seg0,))
49+
arts = m.compile_path_c_direct_fusion_chain_artifacts(chain1)
50+
art = arts[0]
51+
prim_func = seg0.schedule_target.schedule_template(seg0.region)
52+
53+
specs = m._path_c_direct_chain_required_logical_buffer_specs(chain1)
54+
rng = np.random.default_rng(0)
55+
init = {}
56+
for nm, sp in specs.items():
57+
shape = tuple(int(d) for d in sp["shape"]); dt = str(sp["dtype"])
58+
if dt in ("float16", "bfloat16", "float32"):
59+
arr = rng.standard_normal(shape) * 0.1
60+
init[nm] = arr.astype("float32") if dt == "float32" else arr.astype("float16")
61+
else:
62+
init[nm] = np.zeros(shape, dtype=dt)
63+
64+
kbuf_shapes = m._path_c_kernel_buffer_shapes(prim_func)
65+
korder = path_c_kernel_buffer_order(prim_func)
66+
gate = str(getattr(prim_func, "_cppmega_path_c_backward_gate_param", "path_c_run_backward") or "path_c_run_backward")
67+
cparam = str(getattr(prim_func, "_cppmega_path_c_row_chunk_index_param", "") or "")
68+
sparam = str(getattr(prim_func, "_cppmega_path_c_row_subchunk_index_param", "") or "")
69+
launches = m._path_c_segment_time_chunk_launches(prim_func)
70+
n = min(len(launches), args.max_launches)
71+
72+
def run() -> dict[str, np.ndarray]:
73+
bufs = {nm: mx.array(init[nm]) for nm in specs}
74+
mx.eval(*bufs.values())
75+
kargs: list[Any] = []; spos: dict[str, int] = {}
76+
for nm in korder:
77+
if nm in bufs:
78+
kargs.append(m._path_c_exact_kernel_buffer(bufs[nm], kbuf_shapes.get(nm), mx_module=mx))
79+
elif nm == gate:
80+
kargs.append(1)
81+
elif nm in PATH_C_SCALAR_KERNEL_PARAM_DEFAULTS:
82+
spos[str(nm)] = len(kargs); kargs.append(PATH_C_SCALAR_KERNEL_PARAM_DEFAULTS[nm])
83+
else:
84+
raise ValueError(f"unbound {nm}")
85+
arrays = tuple(kargs); barrays = tuple(a for a in arrays if hasattr(a, "shape"))
86+
mx.eval(*barrays)
87+
cp, sp = spos[cparam], spos[sparam]
88+
for ci, si in launches[:n]:
89+
la = list(arrays); la[cp] = int(ci); la[sp] = int(si)
90+
art(*la); mx.eval(*barrays); mx.synchronize()
91+
return {nm: np.asarray(bufs[nm].astype(mx.float32)) for nm in bufs}
92+
93+
# Buffers written via relaxed T.atomic_add are non-deterministic in float
94+
# summation order in ANY dispatch mode (dispatch-independent), so they are
95+
# reported (relative) but not required bit-exact. Every OTHER grad is a
96+
# direct indexed write and MUST be bit-exact across the two row-windowed runs.
97+
import re
98+
atomic_targets = {
99+
mt.group(1)
100+
for line in prim_func._cppmega_path_c_generated_source.split("\n")
101+
for mt in [re.search(r"T\.atomic_add\(([A-Za-z0-9_]+)", line)]
102+
if mt
103+
}
104+
105+
a = run(); b = run()
106+
det_max = 0.0; det_worst = None; atomic_max_abs = 0.0; changed = 0
107+
for nm in a:
108+
if not np.allclose(a[nm], 0):
109+
changed += 1
110+
if nm in atomic_targets:
111+
atomic_max_abs = max(atomic_max_abs, float(np.max(np.abs(a[nm] - b[nm]))) if a[nm].size else 0.0)
112+
continue
113+
d = float(np.max(np.abs(a[nm] - b[nm]))) if a[nm].size else 0.0
114+
if d > det_max:
115+
det_max = d; det_worst = nm
116+
print(f"[det] op={args.op} launches={n}/{len(launches)} changed_out={changed} "
117+
f"atomic_targets={sorted(t.split('_')[-2]+'_'+t.split('_')[-1] for t in atomic_targets)} "
118+
f"direct_write_grads_max_abs_diff={det_max:.3e} (worst={det_worst}) "
119+
f"atomic_grads_max_abs_diff={atomic_max_abs:.3e}", flush=True)
120+
ok = det_max == 0.0 and changed > 0
121+
print("[det] PASS (direct-write grads bit-exact; atomic grads vary by float noise)"
122+
if ok else "[det] FAIL", flush=True)
123+
return 0 if ok else 1
124+
125+
126+
if __name__ == "__main__":
127+
raise SystemExit(main())

0 commit comments

Comments
 (0)