Skip to content

Commit 7945625

Browse files
committed
feat(mamba3-pathc): Stage 3 — LIVE region-build backward 1->3 surface flip (B2/B1/B0)
Mirrors the forward Stage-2 region flip for the backward (design §2/§3.1/§7): - _emit_mamba3_chunked_bwd_model_brick_surfaces emits the 3 _bwd surfaces (B2->B1->B0, the transpose of F2->F1->F0) when the flag is ON, wired by the per-brick grad-handoff buffers (dchunk_states B2->B1, dstates B1->B0, dinp_diag/dA_cumsum_y B2->B0, dA_cumsum_tail B1->B0). REUSES the forward- materialized cb/dA_cumsum/prev_states (no 8x checkpoint-replay). Invoked from the flag-ON _emit_mamba3_chunked_model_brick_surfaces (the forward owner_output surfaces carry no AOT autograd, so the backward is emitted explicitly). - 6 backward grad-handoff buffers registered: _buffer_shape (fp32 region shapes), _buffer_dtype (fp32 recurrent-grad precision), and the force-spill set DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS. - FIX: _path_c_schedule_node_execution_phase classified ANY op in the (now-shared) delegation set as forward; excluded *_bwd so the 3 backward ops classify as BACKWARD phase. The per-node backward stage planner already isolates each into its own single-node stage (interpose requires len(nodes)==1). Verified (tests/test_mamba3_chunked_backward_b0b1b2.py, 11 tests): - flag OFF: no chunked _bwd surfaces, serial mamba3_mimo unchanged (merge-safe). - flag ON: 3 _bwd surfaces, all backward-phase, each own backward stage, correct B2->B1->B0 grad-handoff + prev_states reuse, fp32 force-spilled handoff buffers. - region-build flip inspection: 7 nodes (entry_rmsnorm + 3 fwd + 3 bwd), stages g0..g3 forward / b0..b2 backward single-node each. Flag-OFF unchanged: 151 forward + path_c template/IR tests green (1 Stage-2 test updated to count forward-only chunked nodes). Flag default OFF (merge-safe).
1 parent 7cc15a4 commit 7945625

4 files changed

Lines changed: 265 additions & 8 deletions

File tree

cppmega_mlx/runtime/path_c_fusion.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,6 +1936,25 @@ def _path_c_model_brick_surface_lowerer_for(
19361936
"prev_states",
19371937
"final_state",
19381938
)
1939+
# Caller-owned BACKWARD grad-handoff buffer suffixes that wire B2 -> B1 -> B0
1940+
# (Stage 3; the analytic transpose of the forward F2 -> F1 -> F0 handoff). They
1941+
# are NEW logical region buffers (registered in _buffer_shape / _buffer_dtype and
1942+
# the schedules force-spill set DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS):
1943+
# dchunk_states : B2 -> B1 grad of prev_states (per-chunk entry state)
1944+
# dstates : B1 -> B0 grad of summary_states
1945+
# dinp_diag : B2 -> B0 diag input-outer grad
1946+
# dA_cumsum_y : B2 -> B0 Y-path dA_cumsum grad
1947+
# dA_cumsum_tail: B1 -> B0 chunk-tail dA_cumsum grad
1948+
# dh_last : (region input) cotangent of final_state (zero by default)
1949+
# All fp32 (recurrent-state precision; resolved in _buffer_dtype). The final input
1950+
# grads (dx/dB/dlog_decay/ddt/dh0/dC/dz/dD) are this brick's grad outputs.
1951+
_MAMBA3_CHUNKED_BWD_HANDOFF_SUFFIXES = (
1952+
"dchunk_states",
1953+
"dstates",
1954+
"dinp_diag",
1955+
"dA_cumsum_y",
1956+
"dA_cumsum_tail",
1957+
)
19391958

19401959

19411960
def _emit_mamba3_model_brick_surfaces(
@@ -2042,9 +2061,112 @@ def _emit_mamba3_chunked_model_brick_surfaces(
20422061
backward="owner_output",
20432062
)
20442063
)
2064+
# When the flag is ON the forward surfaces use ``owner_output`` (no AOT
2065+
# autograd derivation), so the 3 backward segments are emitted EXPLICITLY here
2066+
# with the cross-segment grad-handoff wiring (B2 -> B1 -> B0), the analytic
2067+
# transpose of the forward F2 -> F1 -> F0. This replaces the serial
2068+
# ``mamba3_mimo_bwd`` (which is not emitted when the flag is ON).
2069+
_emit_mamba3_chunked_bwd_model_brick_surfaces(brick, context)
20452070
return _PathCBrickSurfaceLoweringResult(delta_output=delta)
20462071

20472072

2073+
def _emit_mamba3_chunked_bwd_model_brick_surfaces(
2074+
brick: _ResolvedPathCModelBrick,
2075+
context: _PathCBrickSurfaceLoweringContext,
2076+
) -> None:
2077+
"""Emit the chunked B2/B1/B0 SSD-core BACKWARD as 3 region surfaces.
2078+
2079+
The analytic transpose of the forward F2/F1/F0 (design §2/§7 Stage 3), wired by
2080+
the per-brick caller-owned grad-handoff buffers (``dchunk_states`` B2->B1,
2081+
``dstates`` B1->B0, ``dinp_diag``/``dA_cumsum_y`` B2->B0, ``dA_cumsum_tail``
2082+
B1->B0). REUSES the forward-materialized boundary states
2083+
(``cb / dA_cumsum / prev_states / summary_states``) instead of the 8x
2084+
checkpoint-replay (design §3/§6).
2085+
2086+
Inputs are the brick's delta cotangent (``{name}_delta_grad``), the forward
2087+
cache (the same projected SSD tensors + handoff buffers the forward staged),
2088+
and ``{name}_dh_last`` (final-state cotangent, zero by default). Outputs are the
2089+
brick's grad outputs for the SSD inputs (``{name}_x_grad`` / ``B`` / ``C`` /
2090+
``dt`` / ``A`` via dlog_decay+ddt / ``z`` / ``D`` / ``h0``).
2091+
2092+
Each ``_bwd`` op-node compiles to its proven ``build_*_bwd_metal`` grid kernel
2093+
via the compile-site delegation interpose. RULE #1: on compile/parity failure
2094+
the builder RAISES (no silent serial fallback).
2095+
"""
2096+
name = brick.name
2097+
proj = {
2098+
suffix: f"{name}_{suffix}"
2099+
for suffix in _MAMBA3_CHUNKED_PROJECTED_SSD_INPUT_SUFFIXES
2100+
}
2101+
hand = {
2102+
suffix: f"{name}_{suffix}"
2103+
for suffix in _MAMBA3_CHUNKED_HANDOFF_SUFFIXES
2104+
}
2105+
bwd = {
2106+
suffix: f"{name}_{suffix}"
2107+
for suffix in _MAMBA3_CHUNKED_BWD_HANDOFF_SUFFIXES
2108+
}
2109+
h0 = f"{name}_h0"
2110+
skip_d = f"{name}_D"
2111+
delta_grad = f"{name}_delta_grad"
2112+
dh_last = f"{name}_dh_last"
2113+
y = f"{name}_y"
2114+
z = f"{name}_z"
2115+
2116+
# B2 — output/Y transpose. Reads delta cotangent + forward cache; writes the
2117+
# output-side grads (dC/dz/dD final; dchunk_states/dinp_diag/dA_cumsum_y handoff;
2118+
# dx D-skip partial accumulated by B0).
2119+
context.surfaces.append(
2120+
FusionKernelSurface.path_c(
2121+
name=f"{name}_chunk_scan_combine_bwd",
2122+
op_name="mamba3_chunk_scan_combine_bwd",
2123+
inputs=(
2124+
delta_grad, hand["cb"], proj["x"], z, proj["dt"],
2125+
hand["dA_cumsum"], proj["C"], proj["B"], hand["prev_states"],
2126+
skip_d, y,
2127+
),
2128+
outputs=(
2129+
f"{name}_C_grad", f"{name}_x_grad", f"{name}_z_grad",
2130+
bwd["dchunk_states"], bwd["dinp_diag"], bwd["dA_cumsum_y"],
2131+
f"{name}_D_grad",
2132+
),
2133+
backward="owner_output",
2134+
)
2135+
)
2136+
# B1 — REVERSE inter-chunk combiner. Reads dchunk_states + prev_states + the
2137+
# final-state cotangent; writes dstates/dA_cumsum_tail handoff + dh0 (final).
2138+
context.surfaces.append(
2139+
FusionKernelSurface.path_c(
2140+
name=f"{name}_inter_chunk_recur_bwd",
2141+
op_name="mamba3_inter_chunk_recur_bwd",
2142+
inputs=(
2143+
bwd["dchunk_states"], hand["dA_cumsum"], dh_last,
2144+
hand["prev_states"],
2145+
),
2146+
outputs=(bwd["dstates"], f"{name}_h0_grad", bwd["dA_cumsum_tail"]),
2147+
backward="owner_output",
2148+
)
2149+
)
2150+
# B0 — precompute transpose. Folds dstates + dinp_diag + the dA_cumsum grads
2151+
# into the final input grads dx (accumulated)/dB/dlog_decay/ddt.
2152+
context.surfaces.append(
2153+
FusionKernelSurface.path_c(
2154+
name=f"{name}_chunk_precompute_bwd",
2155+
op_name="mamba3_chunk_precompute_bwd",
2156+
inputs=(
2157+
bwd["dstates"], bwd["dinp_diag"], bwd["dA_cumsum_y"],
2158+
bwd["dA_cumsum_tail"], hand["dA_cumsum"], proj["x"], proj["B"],
2159+
proj["dt"], proj["A"],
2160+
),
2161+
outputs=(
2162+
f"{name}_x_grad", f"{name}_B_grad", f"{name}_dt_grad",
2163+
f"{name}_A_grad",
2164+
),
2165+
backward="owner_output",
2166+
)
2167+
)
2168+
2169+
20482170
def _emit_m2rnn_model_brick_surfaces(
20492171
brick: _ResolvedPathCModelBrick,
20502172
context: _PathCBrickSurfaceLoweringContext,

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,20 @@
265265
"mamba3_final_state",
266266
}
267267
)
268+
# Backward grad-handoff buffers (B2 -> B1 -> B0; Stage 3). Force-spilled to named
269+
# ABI scratch (mirror of the forward set) so they land as stable named params
270+
# across the producer/consumer backward segments. All fp32 (resolved in
271+
# _buffer_dtype). Flag-gated (default OFF) — never appear in the serial backward.
272+
DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS = frozenset(
273+
{
274+
"mamba3_dh_last",
275+
"mamba3_dchunk_states",
276+
"mamba3_dstates",
277+
"mamba3_dinp_diag",
278+
"mamba3_dA_cumsum_y",
279+
"mamba3_dA_cumsum_tail",
280+
}
281+
)
268282
DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_CANONICALS = frozenset(
269283
{
270284
"hidden",
@@ -725,13 +739,18 @@ class _ScheduleNodeView:
725739
def _path_c_schedule_node_execution_phase(node: Any) -> str:
726740
backward = str(getattr(node, "backward", ""))
727741
op_name = str(getattr(node, "op_name", ""))
728-
# The chunked F0/F1/F2 SSD-core ops are FORWARD producers that carry NO
729-
# synthesized AOT backward (they own their output gradient: backward=
730-
# "owner_output"), but they must run in the FORWARD execution stage so the
731-
# compile-site delegation interpose substitutes their grid kernel during the
732-
# forward pass (RULE #1: the chunked forward is never grouped as a backward
733-
# fragment). Backward stays serial/unchanged.
734-
if op_name in _MAMBA3_CHUNKED_GRID_DELEGATION_OPS:
742+
# The chunked F0/F1/F2 SSD-core FORWARD ops carry NO synthesized AOT backward
743+
# (they own their output gradient: backward="owner_output"), but they must run
744+
# in the FORWARD execution stage so the compile-site delegation interpose
745+
# substitutes their grid kernel during the forward pass (RULE #1: the chunked
746+
# forward is never grouped as a backward fragment). The chunked B0/B1/B2
747+
# BACKWARD ops (``*_bwd``) are explicitly EXCLUDED here so they classify as
748+
# backward below (they too are in _MAMBA3_CHUNKED_GRID_DELEGATION_OPS for the
749+
# shared interpose, but they belong to the backward execution stage).
750+
if (
751+
op_name in _MAMBA3_CHUNKED_GRID_DELEGATION_OPS
752+
and not op_name.endswith("_bwd")
753+
):
735754
return "forward"
736755
if backward == "owner_output" or op_name.endswith("_bwd"):
737756
return "backward"
@@ -4663,6 +4682,8 @@ def _is_row_phased_bwd_scratch_abi_buffer(buffer_name: str) -> bool:
46634682
return True
46644683
if name in DESCRIPTOR_MAMBA3_CHUNKED_FWD_HANDOFF_ABI_BUFFERS:
46654684
return True
4685+
if name in DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS:
4686+
return True
46664687
if not name.endswith("_grad"):
46674688
return False
46684689
canonical = _canonical_buffer_name(name)
@@ -14375,6 +14396,14 @@ def _buffer_dtype(
1437514396
"mamba3_summary_states",
1437614397
"mamba3_prev_states",
1437714398
"mamba3_final_state",
14399+
# Mamba3 chunked-scan B2->B1->B0 backward grad-handoff buffers (Stage 3):
14400+
# fp32 for recurrent grad-state precision.
14401+
"mamba3_dh_last",
14402+
"mamba3_dchunk_states",
14403+
"mamba3_dstates",
14404+
"mamba3_dinp_diag",
14405+
"mamba3_dA_cumsum_y",
14406+
"mamba3_dA_cumsum_tail",
1437814407
"m2rnn_h_state",
1437914408
"m2rnn_h_checkpoint",
1438014409
"m2rnn_h0_grad",
@@ -14502,6 +14531,13 @@ def _is_mamba3_state_like_buffer(buffer_name: str, canonical_name: str) -> bool:
1450214531
"summary_states",
1450314532
"prev_states",
1450414533
"final_state",
14534+
# backward grad-handoff (B2 -> B1 -> B0; Stage 3)
14535+
"dh_last",
14536+
"dchunk_states",
14537+
"dstates",
14538+
"dinp_diag",
14539+
"dA_cumsum_y",
14540+
"dA_cumsum_tail",
1450514541
)
1450614542

1450714543

@@ -14594,6 +14630,13 @@ def _mamba3_chunked_fwd_region_buffer_shape(
1459414630
"summary_states": (batch, nchunks, nheads, headdim, dstate),
1459514631
"prev_states": (batch, nchunks, nheads, headdim, dstate),
1459614632
"final_state": (batch, nheads, headdim, dstate),
14633+
# BACKWARD grad-handoff buffers (B2 -> B1 -> B0; Stage 3, fp32)
14634+
"dh_last": (batch, nheads, headdim, dstate),
14635+
"dchunk_states": (batch, nchunks, nheads, headdim, dstate),
14636+
"dstates": (batch, nchunks, nheads, headdim, dstate),
14637+
"dinp_diag": (batch, seqlen, nheads, headdim, dstate),
14638+
"dA_cumsum_y": (batch, nheads, nchunks, chunk),
14639+
"dA_cumsum_tail": (batch, nheads, nchunks, chunk),
1459714640
}
1459814641
return shapes[suffix]
1459914642

tests/test_mamba3_chained_forward_f0f1f2.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,13 @@ def test_region_flip_on_full_scale_region_parity(monkeypatch, capsys):
599599
assert env.mamba_num_heads == H
600600

601601
region = _build_mamba_direct_chain_region(env)
602+
# Forward-only chunked nodes (Stage 3 also emits 3 chunked _bwd nodes when the
603+
# flag is ON; this forward-parity check exercises just F0/F1/F2).
602604
chunked_nodes = [
603-
n for n in region.nodes if n.op_name in _MAMBA3_CHUNKED_GRID_DELEGATION_OPS
605+
n
606+
for n in region.nodes
607+
if n.op_name in _MAMBA3_CHUNKED_GRID_DELEGATION_OPS
608+
and not n.op_name.endswith("_bwd")
604609
]
605610
assert len(chunked_nodes) == 3
606611

tests/test_mamba3_chunked_backward_b0b1b2.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,93 @@ def test_backward_interpose_on_emits_real_grid_kernel(op_name, monkeypatch):
170170
# --------------------------------------------------------------------------- #
171171

172172

173+
# --------------------------------------------------------------------------- #
174+
# Verification 1b: the LIVE REGION-BUILD backward 1->3 surface flip. #
175+
# --------------------------------------------------------------------------- #
176+
177+
178+
def _build_mamba_direct_chain_region(env):
179+
from cppmega_mlx.runtime.path_c_fusion import (
180+
PathCModelBrick,
181+
build_path_c_model_region_from_bricks,
182+
)
183+
184+
bricks = (PathCModelBrick(name="mamba3_scan", kind="mamba3", route_symbol="M"),)
185+
return build_path_c_model_region_from_bricks(
186+
region_name="mamba3_direct_chain", bricks=bricks, shape_env=env
187+
)
188+
189+
190+
def test_region_flip_off_no_chunked_backward_surfaces(monkeypatch):
191+
"""Flag OFF (default): the direct-chain region has NO chunked _bwd surfaces —
192+
the serial mamba3_mimo (with aot_autograd) is unchanged (merge-safe)."""
193+
monkeypatch.delenv("CPPMEGA_PATH_C_MAMBA3_CHUNKED_SCAN", raising=False)
194+
region = _build_mamba_direct_chain_region(_env())
195+
ops = [n.op_name for n in region.nodes]
196+
assert "mamba3_mimo" in ops
197+
assert not any(o in _BWD_OPS for o in ops), ops
198+
199+
200+
def test_region_flip_on_emits_three_chunked_backward_surfaces(monkeypatch):
201+
"""Flag ON: the region emits the 3 chunked _bwd segments (B2->B1->B0), each
202+
classified as a BACKWARD-phase node isolated into its own backward stage, wired
203+
by the per-brick grad-handoff buffers."""
204+
monkeypatch.setenv("CPPMEGA_PATH_C_MAMBA3_CHUNKED_SCAN", "1")
205+
from cppmega_mlx.runtime.path_c_fusion_schedules import (
206+
_path_c_schedule_node_execution_phase,
207+
plan_path_c_descriptor_stage_groups,
208+
)
209+
210+
region = _build_mamba_direct_chain_region(_env())
211+
bwd_nodes = {n.op_name: n for n in region.nodes if n.op_name in _BWD_OPS}
212+
assert set(bwd_nodes) == set(_BWD_OPS), list(bwd_nodes)
213+
# all 3 classify as backward phase
214+
for n in region.nodes:
215+
if n.op_name in _BWD_OPS:
216+
assert _path_c_schedule_node_execution_phase(n) == "backward", n.op_name
217+
218+
# each _bwd op is isolated into its own backward stage group
219+
op_by_node = {n.name: n.op_name for n in region.nodes}
220+
bwd_stage_ops = []
221+
for g in plan_path_c_descriptor_stage_groups(region):
222+
ops = [op_by_node.get(x) for x in g.active_node_names]
223+
for op in ops:
224+
if op in _BWD_OPS:
225+
assert g.execution_stage == "backward", g.execution_stage
226+
assert len(g.active_node_names) == 1, list(g.active_node_names)
227+
bwd_stage_ops.append(op)
228+
assert bwd_stage_ops == list(_BWD_OPS), bwd_stage_ops
229+
230+
# grad-handoff wiring: B2 -> B1 -> B0 (the transpose of F2 -> F1 -> F0)
231+
b2 = bwd_nodes["mamba3_chunk_scan_combine_bwd"]
232+
b1 = bwd_nodes["mamba3_inter_chunk_recur_bwd"]
233+
b0 = bwd_nodes["mamba3_chunk_precompute_bwd"]
234+
assert "mamba3_scan_dchunk_states" in b2.outputs
235+
assert "mamba3_scan_dchunk_states" in b1.inputs
236+
assert "mamba3_scan_dstates" in b1.outputs
237+
assert "mamba3_scan_dstates" in b0.inputs
238+
assert "mamba3_scan_dinp_diag" in b2.outputs and "mamba3_scan_dinp_diag" in b0.inputs
239+
assert "mamba3_scan_dA_cumsum_tail" in b1.outputs and "mamba3_scan_dA_cumsum_tail" in b0.inputs
240+
# B2/B1/B0 REUSE the forward-materialized boundary states (no replay)
241+
assert "mamba3_scan_prev_states" in b2.inputs
242+
assert "mamba3_scan_prev_states" in b1.inputs
243+
assert "mamba3_scan_cb" in b2.inputs
244+
245+
246+
def test_backward_handoff_buffers_fp32_and_force_spilled():
247+
"""The 6 backward grad-handoff buffers resolve fp32 and are in the force-spill
248+
ABI set (mirror of the forward handoff registration)."""
249+
from cppmega_mlx.runtime.path_c_fusion_schedules import (
250+
DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS,
251+
_buffer_dtype,
252+
)
253+
254+
for nm in DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS:
255+
assert _buffer_dtype(nm) == "float32", nm
256+
assert "mamba3_dchunk_states" in DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS
257+
assert "mamba3_dstates" in DESCRIPTOR_MAMBA3_CHUNKED_BWD_HANDOFF_ABI_BUFFERS
258+
259+
173260
@pytest.mark.skipif(
174261
not _torch_mps_available(), reason="requires torch + Metal (mps) backend"
175262
)

0 commit comments

Comments
 (0)