Skip to content

Commit 956f8ae

Browse files
committed
fix(mamba3-pathc): flag-ON direct-chain emits FULL backward (non-mamba bwd + chunked B2/B1/B0), seeds mamba delta cotangent
ROOT CAUSE: the direct-chain planner (plan_path_c_direct_fusion_chain_for_region) skipped build_path_c_aot_autograd_region whenever ANY _bwd node was already in the region. Flag ON appends the chunked mamba B2/B1/B0 _bwd surfaces into the forward node list, so that guard suppressed the AOT-backward derivation for ALL OTHER bricks -> the chain had only the 3 chunked mamba bwd (interleaved mid-forward) and nothing seeded the mamba delta_grad cotangent (zero mamba grads). FIX (shared by both flag states): - build_path_c_aot_autograd_region: drop explicit owner_output _bwd nodes from the forward list; _aot_backward_surfaces_for walks reversed(forward_nodes) and emits each chunked-mamba brick's pre-built B2/B1/B0 at that reverse position, so residual_rmsnorm_bwd (which produces {brick}_delta_grad) runs BEFORE the mamba backward and seeds its cotangent. - planner guard now short-circuits only on a SYNTHESIZED (non-owner_output) _bwd. VERIFIED: flag-OFF chain unchanged (14 segs, byte-identical). flag-ON chain now 18 segs (9 fwd + 9 bwd) in correct reverse order; brick_10_M_delta_grad nonzero and read by B2; live route grads 2 -> 18 (m2rnn+attention+suffix now flow). New tests: full-backward-chain OFF/ON ordering + delta_grad seam (13 passed). REMAINING (separate feature, NOT this change): 163-grad parity still blocked by the absence of an eager mamba projection fwd/bwd bridge -- the chunked projected inputs (x/B/C/A/dt/z/h0) are zero-seeded in the pre-step owner (no in_proj/conv/norm forward), so chunked grads are zero AND are w.r.t. SSD-core inputs, not the mamba3_*_weight model params the grad-tree aliases expect. Documented in scratch/MAMBA3-RUNTIME-WIRING-RESULTS.md. RULE #1: did NOT alias mismatched grads. Flag CPPMEGA_PATH_C_MAMBA3_CHUNKED_SCAN default OFF (merge-safe).
1 parent f23a673 commit 956f8ae

5 files changed

Lines changed: 334 additions & 9 deletions

File tree

cppmega_mlx/runtime/path_c_fusion.py

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2475,27 +2475,127 @@ def _aot_backward_surface_for_node(node: FusionNode) -> FusionKernelSurface:
24752475
)
24762476

24772477

2478-
def _aot_backward_surfaces_for(region: PathCFusionRegion) -> tuple[FusionKernelSurface, ...]:
2479-
return tuple(
2480-
_aot_backward_surface_for_node(node)
2481-
for node in reversed(region.nodes)
2482-
if node.backward == "aot_autograd"
2478+
# The chunked mamba3 SSD-core ops emit their OWN backward surfaces (owner_output)
2479+
# rather than relying on the synthesized AOT ``_bwd`` derivation. The forward F0/F1/
2480+
# F2 ops are forward-phase owner_output nodes; their explicit B2/B1/B0 ``_bwd`` ops
2481+
# (in forward-list order B2,B1,B0 -- already the correct reverse-of-forward order)
2482+
# are appended into the brick's surface list by
2483+
# ``_emit_mamba3_chunked_bwd_model_brick_surfaces``.
2484+
_MAMBA3_CHUNKED_FORWARD_OP_NAMES = (
2485+
"mamba3_chunk_precompute",
2486+
"mamba3_inter_chunk_recur",
2487+
"mamba3_chunk_scan_combine",
2488+
)
2489+
2490+
2491+
def _is_mamba3_chunked_forward_node(node: FusionNode) -> bool:
2492+
return (
2493+
str(node.op_name) in _MAMBA3_CHUNKED_FORWARD_OP_NAMES
2494+
and str(node.backward) == "owner_output"
2495+
)
2496+
2497+
2498+
def _is_owner_output_backward_node(node: FusionNode) -> bool:
2499+
"""True for an EXPLICIT backward surface already present in a forward region.
2500+
2501+
These are the chunked mamba3 B2/B1/B0 ``_bwd`` ops emitted alongside the
2502+
forward F0/F1/F2 surfaces (op_name ends ``_bwd`` and backward=owner_output).
2503+
They are removed from the forward-node list and re-emitted, in correct
2504+
reverse-of-forward position, by ``_aot_backward_surfaces_for``.
2505+
"""
2506+
2507+
return (
2508+
str(node.op_name).endswith("_bwd")
2509+
and str(node.backward) == "owner_output"
24832510
)
24842511

24852512

2513+
def _explicit_backward_nodes_for_forward(
2514+
forward_node: FusionNode,
2515+
region: PathCFusionRegion,
2516+
) -> tuple[FusionNode, ...]:
2517+
"""Return the explicit ``_bwd`` nodes belonging to a chunked-mamba brick.
2518+
2519+
The chunked mamba3 brick named ``{brick}`` lowers F0/F1/F2 with names
2520+
``{brick}_chunk_precompute`` / ``_inter_chunk_recur`` / ``_chunk_scan_combine``
2521+
and the matching B2/B1/B0 with the SAME stems + ``_bwd``. The three backward
2522+
surfaces are attached to the F0 (first) forward node so the reverse derivation
2523+
emits them once per brick, in their forward-list (B2,B1,B0 = reverse) order,
2524+
at the reverse position of the brick's forward span.
2525+
"""
2526+
2527+
name = str(forward_node.name)
2528+
if not name.endswith("_chunk_precompute"):
2529+
return ()
2530+
brick = name[: -len("_chunk_precompute")]
2531+
bwd_stems = (
2532+
f"{brick}_chunk_scan_combine_bwd",
2533+
f"{brick}_inter_chunk_recur_bwd",
2534+
f"{brick}_chunk_precompute_bwd",
2535+
)
2536+
by_name = {str(node.name): node for node in region.nodes}
2537+
selected = []
2538+
for stem in bwd_stems:
2539+
node = by_name.get(stem)
2540+
if node is None:
2541+
raise ValueError(
2542+
"chunked mamba3 backward surface missing for forward brick "
2543+
f"{brick!r}: expected {stem!r} in region {region.name!r}"
2544+
)
2545+
selected.append(node)
2546+
return tuple(selected)
2547+
2548+
2549+
def _aot_backward_surfaces_for(region: PathCFusionRegion) -> tuple[FusionKernelSurface, ...]:
2550+
"""Return backward surfaces in reverse-of-forward execution order.
2551+
2552+
For each FORWARD node (reversed): an ``aot_autograd`` node derives a
2553+
synthesized ``_bwd`` surface; a chunked-mamba forward (owner_output) node
2554+
emits its pre-built explicit B2/B1/B0 ``_bwd`` surfaces at that reverse
2555+
position (so the model graph's later-in-reverse residual_rmsnorm_bwd, which
2556+
produces the brick ``delta_grad`` cotangent, runs BEFORE the mamba backward).
2557+
Explicit ``_bwd`` nodes already in the region are NOT re-derived as forward.
2558+
"""
2559+
2560+
surfaces: list[FusionKernelSurface] = []
2561+
for node in reversed(region.nodes):
2562+
if _is_owner_output_backward_node(node):
2563+
# Explicit backward surfaces are emitted via their forward node below.
2564+
continue
2565+
if node.backward == "aot_autograd":
2566+
surfaces.append(_aot_backward_surface_for_node(node))
2567+
elif _is_mamba3_chunked_forward_node(node):
2568+
surfaces.extend(
2569+
_surface_from_node(bwd_node)
2570+
for bwd_node in _explicit_backward_nodes_for_forward(node, region)
2571+
)
2572+
return tuple(surfaces)
2573+
2574+
24862575
def build_path_c_aot_autograd_region(
24872576
region: PathCFusionRegion,
24882577
*,
24892578
region_name: str | None = None,
24902579
) -> PathCFusionRegion:
2491-
"""Return ``region`` plus symbolic AOT backward graph surfaces."""
2580+
"""Return ``region`` plus symbolic AOT backward graph surfaces.
2581+
2582+
The forward node list keeps every FORWARD-phase surface (including the chunked
2583+
mamba3 F0/F1/F2 owner_output forward ops) but drops the explicit ``_bwd``
2584+
surfaces that the chunked brick appended alongside its forward; those are
2585+
re-emitted, in correct reverse position, by ``_aot_backward_surfaces_for``.
2586+
"""
24922587

24932588
if not isinstance(region, PathCFusionRegion):
24942589
raise TypeError("region must be PathCFusionRegion")
2590+
forward_surfaces = tuple(
2591+
_surface_from_node(node)
2592+
for node in region.nodes
2593+
if not _is_owner_output_backward_node(node)
2594+
)
24952595
return build_path_c_fusion_region(
24962596
region_name=region_name or region.name,
24972597
surfaces=(
2498-
*(_surface_from_node(node) for node in region.nodes),
2598+
*forward_surfaces,
24992599
*_aot_backward_surfaces_for(region),
25002600
),
25012601
z3_sync=region.z3_sync,

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16041,10 +16041,21 @@ def plan_path_c_direct_fusion_chain_for_region(
1604116041
_path_c_default_target() == "metal"
1604216042
and forward_max_segment_nodes is not None
1604316043
)
16044+
# Build the symbolic backward graph when requested. A region whose nodes
16045+
# ALREADY include a synthesized (aot-derived) backward op is treated as
16046+
# already-built and left as-is. The chunked mamba3 brick appends its OWN
16047+
# explicit B2/B1/B0 ``_bwd`` surfaces (backward="owner_output") into the
16048+
# FORWARD node list; those must NOT short-circuit the backward build --
16049+
# ``build_path_c_aot_autograd_region`` relocates them to their correct
16050+
# reverse position AND derives the remaining non-mamba brick backward ops.
16051+
already_has_synthesized_backward = any(
16052+
node.op_name.endswith("_bwd")
16053+
and str(getattr(node, "backward", "")) != "owner_output"
16054+
for node in region.nodes
16055+
)
1604416056
working_region = (
1604516057
build_path_c_aot_autograd_region(region)
16046-
if include_backward
16047-
and not any(node.op_name.endswith("_bwd") for node in region.nodes)
16058+
if include_backward and not already_has_synthesized_backward
1604816059
else region
1604916060
)
1605016061
nodes = tuple(working_region.nodes)

scratch/MAMBA3-RUNTIME-WIRING-RESULTS.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,94 @@ the model gradient tree. Both are chain/grad-tree assembly — NOT a dtype/ABI b
189189
SAME 3 pre-existing failures (verified identical via `git stash` on clean base:
190190
test_fp8_..._blocks_missing_sparse_mla_producer + the 2 direct-chain
191191
value_and_grad bridge tests; unrelated to this change).
192+
193+
================================================================================
194+
## UPDATE (commit on `mamba3-grad-tree`) — NON-MAMBA BACKWARD SEGMENTS NOW EMIT
195+
## under flag ON; the mamba delta cotangent IS seeded. The PRIOR "NEXT GAP"
196+
## (missing non-mamba bwd segs) is RESOLVED. A DEEPER gap blocks 163-grad parity.
197+
================================================================================
198+
199+
### Root cause of the missing non-mamba backward segments (CORRECTS the premise above)
200+
The flag-ON `build_path_c_model_regions_from_model(include_backward=True)` region
201+
ALREADY emits ALL 9 backward nodes in correct reverse order (verified). The drop
202+
was in the DIRECT-CHAIN PLANNER: `plan_path_c_direct_fusion_chain_for_region`
203+
(path_c_fusion_schedules.py ~:16044) guarded the AOT-backward build with
204+
`not any(node.op_name.endswith("_bwd") for node in region.nodes)`. When the flag is
205+
ON the chunked brick appends its 3 explicit B2/B1/B0 `_bwd` surfaces INTO the
206+
forward node list, so that guard saw a `_bwd` node and SKIPPED
207+
`build_path_c_aot_autograd_region` ENTIRELY — so the OTHER bricks' backward
208+
(sparse_mla/attention/residual x2/m2rnn/entry) were never derived. The chain had
209+
only the 3 chunked mamba bwd, interleaved mid-forward.
210+
211+
### FIX (gated nowhere — both flag states share the same correct code path)
212+
1. `build_path_c_aot_autograd_region` (path_c_fusion.py) now: (a) drops explicit
213+
owner_output `_bwd` nodes from the forward list, (b) `_aot_backward_surfaces_for`
214+
walks `reversed(forward_nodes)` and, for each chunked-mamba FORWARD node, emits
215+
that brick's pre-built B2/B1/B0 `_bwd` surfaces AT THAT REVERSE POSITION (so the
216+
residual_rmsnorm_bwd that produces `{brick}_delta_grad` runs BEFORE the mamba
217+
backward). aot_autograd nodes derive their `_bwd` as before.
218+
2. The planner guard now only short-circuits on a SYNTHESIZED (non-owner_output)
219+
`_bwd` node, so the chunked explicit `_bwd` no longer suppresses the build.
220+
221+
### VERIFIED (probe + live route)
222+
- Flag-OFF chain UNCHANGED: 14 segments (7 fwd + 7 bwd), byte-identical order.
223+
- Flag-ON chain NOW: 18 segments (9 fwd + 9 bwd). Backward order:
224+
sparse_mla_bwd, attention_qkv_bwd, residual_rmsnorm_bwd(R), m2rnn_bwd,
225+
residual_rmsnorm_bwd(M -> emits brick_10_M_delta_grad), B2, B1, B0,
226+
entry_rmsnorm_bwd. seg13 residual_rmsnorm_bwd OUT=brick_10_M_delta_grad;
227+
seg14 B2 IN=brick_10_M_delta_grad (cotangent SEEDED).
228+
- Live flag-ON route end-to-end: loss FINITE (5.65–5.78), grads jumped 2 -> 18
229+
(m2rnn layer-11 + attention layer-12 + suffix param grads now ALL flow because
230+
the non-mamba backward runs). `brick_10_M_delta_grad` absmax=5.64e-3 (NONZERO).
231+
- New tests (test_mamba3_chunked_backward_b0b1b2.py): full-backward-chain OFF +
232+
ON ordering + delta_grad seam — 13 passed (11 prior + 2 new).
233+
234+
### THE DEEPER REMAINING GAP — chunked path has NO eager projection fwd/bwd bridge
235+
163-grad PARITY is STILL NOT achieved, but the cause is NOT the backward chain.
236+
The chunked region only covers the SSD CORE. Its projected inputs
237+
(`{brick}_x/B/C/A/dt/z/h0`) are caller-owned and the pre-step owner ALLOCATES THEM
238+
AS ZEROS (m04 `make_path_c_direct_chain_pre_step_runtime_owner` ~:8699) — there is
239+
NO eager `in_proj/conv/norm` forward that computes them from the residual hidden,
240+
and NO eager projection backward that folds the SSD-core input-grads back into the
241+
model params. Consequences, both verified at flag-ON:
242+
(a) The chunked FORWARD runs on ZERO x/B/C/A/dt, so the cached boundary states
243+
(cb/dA_cumsum/prev_states) are degenerate. The B2/B1/B0 backward multiplies
244+
the (now NONZERO) `delta_grad` by zero forward activations -> EVERY chunked
245+
grad output is ZERO: brick_10_M {x,B,C,A,dt,z,D,h0}_grad all absmax=0.
246+
(b) Even if nonzero, those are grads w.r.t. the PROJECTED SSD tensors, NOT the
247+
model params. The serial `mamba3_mimo_bwd` emits `{brick}_mamba3_in_proj_
248+
weight_grad`/`_conv_weight_grad`/... directly (it backprops proj+conv+norm
249+
internally). The grad-tree alias (`path_c_parameter_gradient_aliases`,
250+
hybrid_lm.py:991) maps the mamba PARAMS to those `mamba3_*_weight_grad`
251+
buffers — which the chunked chain never produces — so the coverage gate
252+
(`_path_c_direct_chain_full_gradient_coverage_payload`) is INCOMPLETE for
253+
layer-10 and the full-grad install path is bypassed (18 grads, not 163).
254+
Only `{brick}_D_grad` would map 1:1 to a model param (mamba3_D); the projection/
255+
conv/norm params have no chunked producer at all.
256+
257+
RULE #1: I did NOT fake parity by aliasing the mismatched SSD-input grads onto the
258+
projection-param grad names (that would be a silent wrong-gradient fallback). The
259+
honest remaining work to reach 163-grad parity is a SEPARATE feature:
260+
1. Eager (MLX-differentiable) mamba in_proj/conv/B-C-norm forward that POPULATES
261+
`{brick}_x/B/C/A/dt/z/h0` from the residual hidden + the real block weights
262+
(replacing the zero-seed), staged ahead of the chunked region.
263+
2. Eager projection BACKWARD: take the chunked B0/B1/B2 SSD-input grads
264+
(`x_grad/B_grad/.../z_grad/h0_grad`) and VJP them through that eager
265+
projection to produce `{brick}_mamba3_*_weight_grad` model-param grads, then
266+
register those under the existing grad-tree aliases so coverage completes.
267+
This is analogous to the existing `path_c_prefix_gradient_tree_from_hidden_
268+
cotangent` bridge but for the mamba projection sub-graph. It is NOT a chain-
269+
assembly or grad-name-mapping change; it is a missing differentiable sub-step.
270+
271+
### wall-time (Metal, local_gb10_quarter smoke, seq=128, batch=1)
272+
flag OFF (serial): 163 grads, loss 5.7134, fwd+bwd 6.106 s.
273+
flag ON (chunked): 18 grads (partial), loss ~5.65–5.78 finite, fwd+bwd ~26 s
274+
(the verify harness builds+compiles 18 segments incl. the now-present 9 bwd;
275+
the chunked GPU kernels themselves remain ~3x faster — the wall-time here is
276+
dominated by first-call compile of the 4 extra backward segments).
277+
278+
### Merge-safety re-confirmed with THIS change (flag default OFF)
279+
- test_path_c_fusion_ir.py + test_path_c_autosplit_metal_parity.py: 124 passed.
280+
- test_mamba3_chunked_backward_b0b1b2.py: 13 passed (incl. 2 new chain tests).
281+
- test_m04_train_step.py -k direct_chain|path_c|chain: 82 passed, SAME 3
282+
pre-existing failures (re-verified identical via `git stash` on clean base).

scratch/probe_chain_segs.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Probe the DIRECT CHAIN segments (planner output) for flag OFF vs ON."""
2+
from __future__ import annotations
3+
import os, sys
4+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, "scripts"))
5+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
6+
7+
import mlx.core as mx
8+
import numpy as np
9+
import m04_train_step as m04 # type: ignore
10+
from cppmega_mlx.recipes.model_factory import build_local_gb10_quarter_tiny_smoke_model
11+
from cppmega_mlx.runtime.path_c_fusion import path_c_mamba3_chunked_scan_enabled
12+
13+
flag_on = path_c_mamba3_chunked_scan_enabled()
14+
mode = "ON" if flag_on else "OFF"
15+
print(f"=== flag {mode} ===")
16+
17+
mx.random.seed(0)
18+
model = build_local_gb10_quarter_tiny_smoke_model(
19+
hidden_size=64, num_attention_heads=1, mamba_expand=1, mamba_head_dim=64,
20+
mamba_state_dim=16, mamba_groups=1, mamba_chunk_size=64,
21+
)
22+
profile_name = str(getattr(model, "path_c_profile_name", "HybridTinyLM"))
23+
prefix = m04._path_c_direct_chain_region_prefix(model, profile_name)
24+
seq = 128
25+
26+
direct_chains = m04.plan_path_c_direct_fusion_chains_for_model(
27+
model, region_prefix=prefix, include_backward=True, max_segment_nodes=1,
28+
sequence_length=seq,
29+
)
30+
regions = m04.build_path_c_model_regions_from_model(
31+
model, region_prefix=prefix, include_backward=False, sequence_length=seq,
32+
)
33+
sel_region = m04._select_path_c_model_route_region(regions)
34+
chain = m04._select_path_c_direct_chain_for_region(direct_chains, sel_region)
35+
print(f"chain status={getattr(chain,'status',None)} n_segments={len(chain.segments)}")
36+
for seg in chain.segments:
37+
region = seg.region
38+
ops = []
39+
for n in getattr(region, "nodes", ()):
40+
ops.append(n.op_name)
41+
phase = str(getattr(seg, "execution_phase", "?"))
42+
print(f" seg[{seg.index}] status={seg.status} phase={phase} region={region.name} ops={ops}")

tests/test_mamba3_chunked_backward_b0b1b2.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,87 @@ def test_region_flip_on_emits_three_chunked_backward_surfaces(monkeypatch):
243243
assert "mamba3_scan_cb" in b2.inputs
244244

245245

246+
def _build_mra_full_backward_region(env, *, include_backward):
247+
"""Build a 3-brick (M mamba3 / R m2rnn / A attention) region.
248+
249+
This mirrors the real model route span the direct-chain planner discovers so
250+
the FULL reverse backward chain (non-mamba ``_bwd`` + chunked mamba B2/B1/B0)
251+
can be asserted.
252+
"""
253+
from cppmega_mlx.runtime.path_c_fusion import (
254+
PathCModelBrick,
255+
build_path_c_model_region_from_bricks,
256+
)
257+
258+
bricks = (
259+
PathCModelBrick(name="brick_M", kind="mamba3", route_symbol="M"),
260+
PathCModelBrick(name="brick_R", kind="m2rnn", route_symbol="R"),
261+
PathCModelBrick(name="brick_A", kind="attention", route_symbol="A"),
262+
)
263+
return build_path_c_model_region_from_bricks(
264+
region_name="mra_chain",
265+
bricks=bricks,
266+
shape_env=env,
267+
include_backward=include_backward,
268+
)
269+
270+
271+
def test_full_backward_chain_off_emits_serial_mamba_bwd(monkeypatch):
272+
"""Flag OFF: the full (fwd+bwd) region emits the 7 serial backward ops in
273+
reverse order, ending with mamba3_mimo_bwd then entry_rmsnorm_bwd. Merge-safe
274+
baseline for the flag-ON assertion below."""
275+
monkeypatch.delenv("CPPMEGA_PATH_C_MAMBA3_CHUNKED_SCAN", raising=False)
276+
region = _build_mra_full_backward_region(_env(), include_backward=True)
277+
bwd = [n.op_name for n in region.nodes if n.op_name.endswith("_bwd")]
278+
assert bwd == [
279+
"sparse_mla_fp8_apply_bwd",
280+
"attention_qkv_projection_bwd",
281+
"residual_rmsnorm_bwd",
282+
"m2rnn_bwd",
283+
"residual_rmsnorm_bwd",
284+
"mamba3_mimo_bwd",
285+
"entry_rmsnorm_bwd",
286+
], bwd
287+
288+
289+
def test_full_backward_chain_on_emits_all_segments_in_reverse_order(monkeypatch):
290+
"""Flag ON (the unlock fix): the full (fwd+bwd) region emits the NON-mamba
291+
backward segments (sparse_mla/attention/residual x2/m2rnn/entry) IN ADDITION
292+
TO the 3 chunked mamba ``_bwd`` (B2/B1/B0), all in correct reverse-of-forward
293+
order. The residual_rmsnorm_bwd producing the mamba ``brick_M_delta_grad``
294+
cotangent MUST run BEFORE the chunked mamba backward so the cotangent is
295+
seeded (the root cause of the prior zero-mamba-grad gap)."""
296+
monkeypatch.setenv("CPPMEGA_PATH_C_MAMBA3_CHUNKED_SCAN", "1")
297+
region = _build_mra_full_backward_region(_env(), include_backward=True)
298+
bwd = [n.op_name for n in region.nodes if n.op_name.endswith("_bwd")]
299+
assert bwd == [
300+
"sparse_mla_fp8_apply_bwd",
301+
"attention_qkv_projection_bwd",
302+
"residual_rmsnorm_bwd",
303+
"m2rnn_bwd",
304+
"residual_rmsnorm_bwd",
305+
"mamba3_chunk_scan_combine_bwd",
306+
"mamba3_inter_chunk_recur_bwd",
307+
"mamba3_chunk_precompute_bwd",
308+
"entry_rmsnorm_bwd",
309+
], bwd
310+
311+
# The mamba cotangent seam: a residual_rmsnorm_bwd must OUTPUT the brick's
312+
# delta_grad and run strictly BEFORE the chunked mamba B2 reads it.
313+
nodes = list(region.nodes)
314+
delta_grad = "brick_M_delta_grad"
315+
seed_idx = next(
316+
i for i, n in enumerate(nodes)
317+
if n.op_name == "residual_rmsnorm_bwd" and delta_grad in n.outputs
318+
)
319+
b2_idx = next(
320+
i for i, n in enumerate(nodes)
321+
if n.op_name == "mamba3_chunk_scan_combine_bwd"
322+
)
323+
assert seed_idx < b2_idx, (seed_idx, b2_idx)
324+
assert delta_grad in nodes[b2_idx].inputs, nodes[b2_idx].inputs
325+
326+
246327
def test_backward_handoff_buffers_fp32_and_force_spilled():
247328
"""The 6 backward grad-handoff buffers resolve fp32 and are in the force-spill
248329
ABI set (mirror of the forward handoff registration)."""

0 commit comments

Comments
 (0)