Skip to content

Commit 9f74055

Browse files
committed
fix(path-c): clear Metal FORWARD watchdog gate — row-chunk independent heavy fwd ops; isolate them per watchdog-bounded segment
Clears the full-scale direct-chain FORWARD watchdog blocker (region ..._chain_4_6 / seg[2] FORWARD), the forward analog of the already-merged backward fix (54332ac). The full-scale local_gb10_quarter (depth=13 hidden=3584 max_seq=4096) Metal direct-chain --execute previously tripped the macOS GPU watchdog (kIOGPUCommandBufferCallbackErrorTimeout) at ~9.6s in seg[2] FORWARD, because the heavy FORWARD ops attention_qkv_projection + sparse_mla_fp8_apply ran as a monolithic grid_chunks command buffer over S=4096. FIX (Metal-gated; CUDA unchanged; explicit by target/op; RULE #1: no silent fallback): * _ROW_CHUNKED_INDEPENDENT_FORWARD_OPS = {attention_qkv_projection, sparse_mla_fp8_apply} -- the per-row-INDEPENDENT heavy FORWARD ops. Both produce per-row outputs with NO carried cross-row state: - attention_qkv_projection: per-token Q/KV projection + RoPE + FP8 prepare; output row R reads input hidden row R + shared weights only. Its q_fp8/q_scale/kv_fp8/kv_scale outputs are caller-owned full-sequence KV-history workspace buffers, written across the K row-launches. - sparse_mla_fp8_apply: per-query-row attention out + LSE; query row R reads its own q row + the SHARED full-sequence KV-history workspace (kv_fp8/kv_scale, top-k indices all <= R for causal), produced by the SEPARATE earlier attention_qkv_projection segment. * The forward fusion cap is lowered to 1 for any forward window containing a row-chunked forward op (_effective_forward_max_segment_nodes_for_window), so attention_qkv_projection and sparse_mla_fp8_apply each land ALONE in their own segment. That (a) makes each its own watchdog-bounded command buffer and (b) guarantees sparse_mla reads a FULLY-MATERIALIZED KV workspace (a fused row window would read KV positions not yet written) -> forward parity preserved. Mirrors the backward cap=1 isolation of the independent heavy bwd ops. * Each isolated heavy forward segment is switched to launcher_chunks row-windowing, so the existing runtime row-chunk loop drives it as K command buffers over the independent ROW axis. NO carry buffers are added (_row_phased_launcher_carry_buffers_for_nodes adds carries ONLY for mamba3/m2rnn, never for these). The runtime (run_path_c_direct_fusion_chain_route) needed NO change: it keys off the compiled prim_func's row_dispatch_mode=='launcher_chunks' and is phase-agnostic. * Gated on _path_c_default_target()=="metal" AND the forward cap being active (forward_row_chunk_isolation): a caller passing forward_max_segment_nodes=None disables ALL Metal forward mitigations together; CUDA resolves the cap to None, keeping the monolithic grid_chunks forward path + greedy fusion byte-for-byte unchanged. NEW GROUPING (Metal, full + smoke scale): 12 segments. Forward grew 4->5: seg0 [entry_rmsnorm,mamba3_mimo] grid seg1 [residual_rmsnorm,m2rnn] grid seg2 [residual_rmsnorm] grid (split off) seg3 [attention_qkv_projection] launcher (isolated) seg4 [sparse_mla_fp8_apply] launcher (isolated) Backward unchanged (7 segments): seg5 sparse_mla_bwd(launcher), seg6 attn_qkv_bwd(launcher), seg7 rmsnorm_bwd(grid), seg8 m2rnn_bwd(launcher), seg9 rmsnorm_bwd(grid), seg10 mamba3_bwd(launcher), seg11 entry_rmsnorm_bwd(grid). VERIFIED (real, on-device M4 Max): * FORWARD GATE CLEARED. Full-scale repro_fullscale_directchain.py --execute now runs ~3686s (~61 min) and gets PAST the entire FORWARD phase + backward segments 5-9 with ZERO forward watchdog timeout (was killed at 9.6s in seg[2] FORWARD). All 12 segments plan + native-compile (newComputePipelineState ok in ~5s). The execute now dies DEEPER, at seg[10] BACKWARD (region ..._chain_12_13, op mamba3_mimo_bwd) on its FIRST launcher launch (chunk=0,subchunk=0) with kIOGPUCommandBufferCallbackErrorTimeout -- this is the SAME mamba3_mimo_bwd deeper blocker already documented on 54332ac (a SINGLE 114-MSL-loop reverse scan launch still exceeds the watchdog even row-windowed). That is the NEXT gate (progress, not a regression of this change): mamba3_bwd is a backward op this forward-only change does not touch. * FORWARD PARITY (smoke scale): the row-chunked forward activations are BITWISE IDENTICAL to the canonical maximally-fused (greedy all-grid_chunks) forward -- 6/6 forward activations (q_fp8/q_scale/kv_fp8/kv_scale + sparse_mla out/lse) diff=0.0 at two seeds, no NaN (scripts/_fwd_rowchunk_parity.py --greedy- reference). Confirmed launcher_chunks==grid_chunks for the SAME isolation (diff=0.0), and that the SHIPPED cap=2 grouping's pre-existing FP8-quantization divergence from greedy is unrelated to row-chunking (it diverges with NO row-chunking too), so greedy is the correct parity oracle. * NO REGRESSION. test_path_c_fusion_ir.py: 119 passed (the forward_max_segment_nodes=None greedy tests are unchanged -- the row-chunk isolation is bound to the same switch). test_m04_train_step.py: the direct-chain executor test passes after updating the legitimately-changed forward grouping (segment_count 11->12, 4->5 forward segments, time-chunked launcher indices {4,5,7,9}->{3,4,5,6,8,10}, and the kernel_arg_count formula: forward launcher segments bind 2 launcher index scalars, backward launcher segments bind 3). Remaining 10 m04 failures are PRE-EXISTING on baseline (CLI/data/env/'tokens' array), unchanged by this change (verified via stash). Forward parity harness: scripts/_fwd_rowchunk_parity.py (greedy-reference A/B).
1 parent f687b4f commit 9f74055

3 files changed

Lines changed: 454 additions & 38 deletions

File tree

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,54 @@
194194
"attention_qkv_projection_bwd",
195195
}
196196
)
197+
# Per-row-INDEPENDENT heavy FORWARD ops that exceed the macOS GPU watchdog
198+
# (~5-6s per command buffer) when run as ONE monolithic grid_chunks command
199+
# buffer at full local_gb10_quarter scale (depth=13 hidden=3584 max_seq=4096).
200+
# These are the FORWARD analog of _ROW_CHUNKED_INDEPENDENT_BACKWARD_OPS.
201+
#
202+
# Measured at full scale, seg[2] FORWARD region ..._chain_4_6 (the fused
203+
# residual_rmsnorm + attention_qkv_projection + sparse_mla_fp8_apply forward
204+
# segment) runs as a monolithic grid_chunks command buffer over S=4096 and is
205+
# killed at ~9.6s by the watchdog (kIOGPUCommandBufferCallbackErrorTimeout).
206+
# The two heavy ops are attention_qkv_projection and sparse_mla_fp8_apply.
207+
#
208+
# Both are per-row-INDEPENDENT in the FORWARD direction:
209+
# * attention_qkv_projection is a per-token (per-row) Q/KV linear projection +
210+
# split-half RoPE + per-head FP8 prepare. Output row R reads input hidden
211+
# row R plus the shared projection weights only -- there is NO cross-row /
212+
# cross-time carried state. Its outputs (q_fp8/q_scale/kv_fp8/kv_scale) are
213+
# caller-owned full-sequence KV-history workspace buffers (see
214+
# _is_attention_kv_history_workspace_output): across the K row-launches every
215+
# row [0, S) is written into those persistent buffers.
216+
# * sparse_mla_fp8_apply writes a per-query-row attention output (and per-row
217+
# LSE). Query row R reads the SHARED full-sequence KV-history workspace
218+
# (kv_fp8/kv_scale, indexed by the row's top-k sparse indices, all <= R for
219+
# causal) plus its own q row -- again NO carried cross-row state; the KV it
220+
# reads is produced by attention_qkv_projection in an EARLIER segment and is
221+
# fully materialized before this op runs.
222+
# So launcher_chunks row-windowing splits each into K command buffers over the
223+
# independent ROW axis with zero cross-launch state. As with the independent
224+
# backward ops, _row_phased_launcher_carry_buffers_for_nodes adds carry buffers
225+
# ONLY for mamba3/m2rnn, never for these -- no carry buffers are added here.
226+
#
227+
# CORRECTNESS / PARITY REQUIREMENT: because sparse_mla_fp8_apply row R reads KV
228+
# at positions written by attention_qkv_projection for OTHER rows, these two ops
229+
# must NOT be row-windowed while fused in one segment (a row window would read
230+
# KV positions not yet written in that window). The forward fusion cap is
231+
# therefore lowered to 1 op for any forward segment containing a row-chunked
232+
# forward op (see _effective_forward_max_segment_nodes_for_window), isolating
233+
# attention_qkv_projection and sparse_mla_fp8_apply each into their OWN segment.
234+
# attention_qkv_projection then writes the FULL KV workspace across its K
235+
# committed launches BEFORE sparse_mla_fp8_apply's segment row-windows over the
236+
# now-complete KV -> forward activations are bitwise unchanged by chunking.
237+
# Metal-only (CUDA has no per-command-buffer watchdog, so CUDA keeps the
238+
# monolithic grid_chunks forward path and greedy forward fusion).
239+
_ROW_CHUNKED_INDEPENDENT_FORWARD_OPS = frozenset(
240+
{
241+
"attention_qkv_projection",
242+
"sparse_mla_fp8_apply",
243+
}
244+
)
197245
DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_BUFFERS = frozenset(
198246
{
199247
"attention_hidden",
@@ -611,6 +659,47 @@ def _path_c_descriptor_stage_node_op_name(node: Any) -> str:
611659
return str(getattr(node, "op_name", ""))
612660

613661

662+
def _nodes_contain_row_chunked_forward_op(nodes: Iterable[Any]) -> bool:
663+
"""True if any node is a row-chunked-independent FORWARD op.
664+
665+
These ops (:data:`_ROW_CHUNKED_INDEPENDENT_FORWARD_OPS`) must be ISOLATED in
666+
their own forward segment so each can be launcher_chunks row-windowed without
667+
a fused sibling reading partially-written cross-row workspace (see that
668+
frozenset's docstring for the KV-history parity requirement).
669+
"""
670+
671+
return any(
672+
_path_c_descriptor_stage_node_op_name(node)
673+
in _ROW_CHUNKED_INDEPENDENT_FORWARD_OPS
674+
for node in nodes
675+
)
676+
677+
678+
def _effective_forward_max_segment_nodes_for_window(
679+
candidate_nodes: Iterable[Any],
680+
forward_max_segment_nodes: int | None,
681+
) -> int | None:
682+
"""Forward op cap for a candidate segment window.
683+
684+
Lowers the configured ``forward_max_segment_nodes`` to 1 whenever the window
685+
contains a row-chunked-independent forward op so that
686+
``attention_qkv_projection`` / ``sparse_mla_fp8_apply`` each land alone in
687+
their own segment (the watchdog isolation that lets them be row-windowed and
688+
that keeps sparse-MLA's KV reads bitwise-correct -- see
689+
:data:`_ROW_CHUNKED_INDEPENDENT_FORWARD_OPS`). Returns the unchanged cap
690+
otherwise; ``None`` stays ``None`` only when there is no row-chunked forward
691+
op (a row-chunked forward op always forces a cap of 1, even on an otherwise
692+
uncapped target -- but this is reached only on Metal, where the cap is set).
693+
"""
694+
695+
nodes = tuple(candidate_nodes)
696+
if not _nodes_contain_row_chunked_forward_op(nodes):
697+
return forward_max_segment_nodes
698+
if forward_max_segment_nodes is None:
699+
return 1
700+
return min(int(forward_max_segment_nodes), 1)
701+
702+
614703
def _path_c_descriptor_stage_rows_per_kernel_launch_for_node(node: Any) -> int:
615704
op_name = _path_c_descriptor_stage_node_op_name(node)
616705
if op_name in {"attention_qkv_projection_bwd", "mamba3_mimo_bwd"}:
@@ -14548,6 +14637,21 @@ def plan_path_c_direct_fusion_chain_for_region(
1454814637
raise ValueError(
1454914638
"backward_max_segment_nodes must be positive when provided"
1455014639
)
14640+
# Metal-only watchdog mitigation for the heavy per-row-INDEPENDENT FORWARD
14641+
# ops (attention_qkv_projection / sparse_mla_fp8_apply): isolate each in its
14642+
# own 1-op segment and switch that segment to launcher_chunks row-windowing.
14643+
# Gated explicitly on the lowering target AND on the forward cap being active
14644+
# (RULE #1: explicit by target, no silent fallback): CUDA -- which has no
14645+
# per-command-buffer GPU watchdog -- resolves the forward cap to ``None`` and
14646+
# so keeps the monolithic grid_chunks forward path + greedy forward fusion
14647+
# byte-for-byte unchanged. A caller that explicitly passes
14648+
# ``forward_max_segment_nodes=None`` disables ALL Metal forward mitigations
14649+
# (the pipeline-state split AND this watchdog row-chunk isolation) together,
14650+
# so the row-chunk isolation is bound to the SAME switch as the forward cap.
14651+
forward_row_chunk_isolation = (
14652+
_path_c_default_target() == "metal"
14653+
and forward_max_segment_nodes is not None
14654+
)
1455114655
working_region = (
1455214656
build_path_c_aot_autograd_region(region)
1455314657
if include_backward
@@ -14593,15 +14697,41 @@ def plan_path_c_direct_fusion_chain_for_region(
1459314697
# extending the forward segment here so it splits into smaller
1459414698
# pipeline-safe kernels. Backward segments are exempt (they keep
1459514699
# greedy fusion up to the buffer limit).
14700+
#
14701+
# The cap is FURTHER lowered to 1 for any forward window containing a
14702+
# row-chunked-independent forward op (attention_qkv_projection /
14703+
# sparse_mla_fp8_apply; see _ROW_CHUNKED_INDEPENDENT_FORWARD_OPS).
14704+
# That isolates each heavy forward op in its OWN segment so (a) it can
14705+
# be launcher_chunks row-windowed below to stay under the macOS GPU
14706+
# watchdog (the monolithic 3-op forward segment ..._chain_4_6 was
14707+
# killed at ~9.6s at full scale), and (b) sparse_mla_fp8_apply's KV
14708+
# reads of the full-sequence workspace written by the SEPARATE
14709+
# attention_qkv_projection segment stay bitwise-correct (a fused row
14710+
# window would read KV positions not yet written). Mirrors the
14711+
# backward cap=1 isolation of the per-row-independent heavy bwd ops.
14712+
effective_forward_cap = (
14713+
_effective_forward_max_segment_nodes_for_window(
14714+
candidate_region.nodes,
14715+
forward_max_segment_nodes,
14716+
)
14717+
if forward_row_chunk_isolation
14718+
else forward_max_segment_nodes
14719+
)
1459614720
if (
1459714721
execution_phase == DESCRIPTOR_EXECUTION_STAGE_FORWARD
14598-
and forward_max_segment_nodes is not None
14599-
and end - start > forward_max_segment_nodes
14722+
and effective_forward_cap is not None
14723+
and end - start > effective_forward_cap
1460014724
):
1460114725
first_failure = (
1460214726
f"forward direct-buffer segment reached "
14603-
f"forward_max_segment_nodes={forward_max_segment_nodes} "
14604-
f"(Metal newComputePipelineState size cap)"
14727+
f"forward_max_segment_nodes={effective_forward_cap} "
14728+
f"(Metal newComputePipelineState size cap"
14729+
+ (
14730+
" / row-chunked forward op isolation"
14731+
if effective_forward_cap != forward_max_segment_nodes
14732+
else ""
14733+
)
14734+
+ ")"
1460514735
)
1460614736
break
1460714737
# BACKWARD-only Metal watchdog cap. Backward fused segments larger
@@ -14693,6 +14823,44 @@ def plan_path_c_direct_fusion_chain_for_region(
1469314823
DESCRIPTOR_DEFAULT_MAX_ROWS_PER_LAUNCH,
1469414824
DESCRIPTOR_ROW_DISPATCH_LAUNCHER_CHUNKS,
1469514825
)
14826+
# ROW-WINDOWING for the per-row-INDEPENDENT heavy FORWARD ops
14827+
# (attention_qkv_projection / sparse_mla_fp8_apply; see
14828+
# _ROW_CHUNKED_INDEPENDENT_FORWARD_OPS). The FORWARD analog of the
14829+
# backward block above. At full scale the fused 3-op forward segment
14830+
# ..._chain_4_6 (residual_rmsnorm + attention_qkv_projection +
14831+
# sparse_mla_fp8_apply) runs as ONE monolithic grid_chunks command
14832+
# buffer over S=4096 and is killed at ~9.6s by the macOS GPU watchdog
14833+
# (kIOGPUCommandBufferCallbackErrorTimeout). The effective forward cap
14834+
# above isolates each heavy op in its OWN 1-op segment; here that
14835+
# isolated segment is switched to launcher_chunks so its per-row body
14836+
# (`for row in T.serial(row_chunk_start, row_chunk_stop)`) is driven as
14837+
# K command buffers over the independent ROW axis -- each command
14838+
# buffer holds only a row window of work, well under the watchdog.
14839+
# These ops have NO carried cross-row state
14840+
# (_row_phased_launcher_carry_buffers_for_nodes adds carries ONLY for
14841+
# mamba3/m2rnn, never for these), so no carry buffers are added.
14842+
# Across the K launches every row [0, S) is written into the
14843+
# caller-owned full-sequence buffers, so the forward activations are
14844+
# bitwise unchanged by chunking; sparse_mla_fp8_apply reads the
14845+
# full KV-history workspace already written by the SEPARATE isolated
14846+
# attention_qkv_projection segment. (Metal-only watchdog split; CUDA
14847+
# keeps the monolithic grid_chunks forward path.)
14848+
if (
14849+
forward_row_chunk_isolation
14850+
and execution_phase == DESCRIPTOR_EXECUTION_STAGE_FORWARD
14851+
and direct_target.max_rows_per_launch is not None
14852+
and any(
14853+
_path_c_descriptor_stage_node_op_name(node)
14854+
in _ROW_CHUNKED_INDEPENDENT_FORWARD_OPS
14855+
for node in candidate_region.nodes
14856+
)
14857+
):
14858+
direct_target = _target_with_max_rows_per_launch(
14859+
direct_target,
14860+
candidate_region,
14861+
DESCRIPTOR_DEFAULT_MAX_ROWS_PER_LAUNCH,
14862+
DESCRIPTOR_ROW_DISPATCH_LAUNCHER_CHUNKS,
14863+
)
1469614864
try:
1469714865
parameter_count = _kernel_parameter_count_for_target(
1469814866
candidate_region,

0 commit comments

Comments
 (0)