Skip to content

Commit 6969a64

Browse files
committed
feat(path_c): device-resident gridded BACKWARD (B0/B1/B2) — CUDA pass_configs + delegation wire + RULE #1 no-numpy-fallback + gb10 probe
Make the MR region's backward device-resident gridded (mirrors the forward 77cb4a6+b961c6d), removing the abstract numpy host backward (the §15 79.6%@8l / 91.4%@28l wall) as a SILENT fallback. 1. CUDA pass_configs (mamba3_chunked_backward_core.py): thread pass_configs={tl.disable_tma_lower:True, tl.disable_warp_specialized:True} into each of build_chunk_scan_combine_bwd_metal / build_inter_chunk_recur_bwd_metal / build_chunk_precompute_bwd_metal ONLY on the resolved CUDA branch, mirroring compile_chunk_scan_fwd_metal exactly. The B0/B1/B2 prim BODIES have ZERO T.gemm / shared.dyn / make_swizzled_layout (grep-confirmed) so NO new CUDA sibling prim is needed. Metal default path is byte-identical (else branch unchanged). 2. Backward delegation: the existing interpose already routes the 3 bwd ops (_MAMBA3_CHUNKED_GRID_DELEGATION_OPS, descriptors, production_source, fragment_emitters, BWD handoff ABI buffers) via the direct-chain region path with the flag ON. Added register_real_backward_driver + make_real_bank_backward_driver (path_c_relax_train_step.py), symmetric to register_real_forward_driver, to drive the real gridded backward MR kernel (run_backward=1) over the bank ABI for the bank-SSA train_step path (device-resident, no host bounce). 3. RULE #1 (no silent numpy fallback): register_bank_drivers (path_c_relax_step_banks.py) now RAISES with WHERE+WHAT when the chunked flag is ON and the real gridded bank backward is NOT installed — instead of silently binding pathc.bank_bwd_* to the numpy _region_bwd_driver. set_real_bank_bwd_installed clears the guard once register_real_backward_driver runs (and stops the numpy bwd from clobbering the real binding). Explicit CPU self-check escape: CPPMEGA_PATH_C_ALLOW_NUMPY_BANK_BWD=1. Flag OFF (default) = byte-identical to before. 4. Probe scratch/probe_chunked_backward_cuda_gb10.py: prod shape (b=1,S=4096,c=64, G=8,H=112,P=64,N=64); seeds deterministic inputs + cotangent; builds the gridded forward cache (F0/F1, target=cuda) + serial y; runs B2->B1->B0 (pre-zeroed fp32 out, dx accumulated B2 D-skip then B0 inp-path); reference = MLX-free torch.autograd VJP of OUR exact serial recurrence (GOLD oracle); asserts all 8 model-facing grads max|abs|<1e-3 over ALL elements + NaN/inf guard; times B0/B1/B2 per-call; prints a machine-parseable RESULT json. RULE #1: any grad over gate or NaN RAISES. NO GPU on this Mac: edits + py_compile/AST + cross-module symbol checks only; gb10 compile/run is the Profile agent's single-run step.
1 parent 820c25e commit 6969a64

4 files changed

Lines changed: 819 additions & 5 deletions

File tree

cppmega_mlx/nn/_tilelang/mamba3_chunked_backward_core.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,31 @@ def build_chunk_scan_combine_bwd_metal(
419419
prim = chunk_scan_combine_bwd_metal_prim(
420420
batch, seqlen, chunk_size, ngroups, nheads, headdim, dstate, **kwargs
421421
)
422+
resolved_target = _resolve_chunked_compile_target(target)
423+
# CUDA (sm_121): mirror the forward F2 compile-site EXACTLY — thread the same
424+
# two pass_configs so the CUDA codegen surface is identical to the validated
425+
# forward. The B2 prim BODY has NO T.gemm / TMA-eligible copy (grep-confirmed:
426+
# zero T.gemm / shared.dyn / make_swizzled_layout), so there is no tensormap
427+
# descriptor to mis-align; disabling the TMA + warp-specialized lowering is a
428+
# no-op-or-safer escape hatch that keeps the compile path byte-identical to
429+
# the forward. The Metal branch is UNCHANGED (no pass_configs). RULE #1:
430+
# explicit per-target codegen choice, never a silent fallback.
431+
kind = str(getattr(getattr(resolved_target, "kind", None), "name", "")).lower()
432+
if "cuda" in kind:
433+
pass_configs = {
434+
"tl.disable_tma_lower": True,
435+
"tl.disable_warp_specialized": True,
436+
}
437+
return tilelang.compile(
438+
prim,
439+
out_idx=[11, 12, 13, 14, 15, 16, 17],
440+
target=resolved_target,
441+
pass_configs=pass_configs,
442+
)
422443
return tilelang.compile(
423444
prim,
424445
out_idx=[11, 12, 13, 14, 15, 16, 17],
425-
target=_resolve_chunked_compile_target(target),
446+
target=resolved_target,
426447
)
427448

428449

@@ -596,10 +617,31 @@ def build_inter_chunk_recur_bwd_metal(
596617
prim = inter_chunk_recur_bwd_metal_prim(
597618
batch, seqlen, chunk_size, ngroups, nheads, headdim, dstate, **kwargs
598619
)
620+
resolved_target = _resolve_chunked_compile_target(target)
621+
# CUDA (sm_121): mirror the forward F2 compile-site EXACTLY — thread the same
622+
# two pass_configs. The B1 prim BODY has NO T.gemm / TMA-eligible copy
623+
# (grep-confirmed: zero T.gemm / shared.dyn / make_swizzled_layout — it does
624+
# not even alloc_shared), so there is no tensormap descriptor to mis-align;
625+
# the pass_configs are a no-op-or-safer escape hatch keeping the CUDA compile
626+
# path byte-identical to the forward. The Metal branch is UNCHANGED (no
627+
# pass_configs). RULE #1: explicit per-target codegen choice, never a silent
628+
# fallback.
629+
kind = str(getattr(getattr(resolved_target, "kind", None), "name", "")).lower()
630+
if "cuda" in kind:
631+
pass_configs = {
632+
"tl.disable_tma_lower": True,
633+
"tl.disable_warp_specialized": True,
634+
}
635+
return tilelang.compile(
636+
prim,
637+
out_idx=[4, 5, 6],
638+
target=resolved_target,
639+
pass_configs=pass_configs,
640+
)
599641
return tilelang.compile(
600642
prim,
601643
out_idx=[4, 5, 6],
602-
target=_resolve_chunked_compile_target(target),
644+
target=resolved_target,
603645
)
604646

605647

@@ -852,8 +894,29 @@ def build_chunk_precompute_bwd_metal(
852894
prim = chunk_precompute_bwd_metal_prim(
853895
batch, seqlen, chunk_size, ngroups, nheads, headdim, dstate, **kwargs
854896
)
897+
resolved_target = _resolve_chunked_compile_target(target)
898+
# CUDA (sm_121): mirror the forward F2 compile-site EXACTLY — thread the same
899+
# two pass_configs. The B0 prim BODY has NO T.gemm / TMA-eligible copy
900+
# (grep-confirmed: zero T.gemm / shared.dyn / make_swizzled_layout; all
901+
# alloc_shared are plain default scope), so there is no tensormap descriptor
902+
# to mis-align; the pass_configs are a no-op-or-safer escape hatch keeping the
903+
# CUDA compile path byte-identical to the forward. The Metal branch is
904+
# UNCHANGED (no pass_configs). RULE #1: explicit per-target codegen choice,
905+
# never a silent fallback.
906+
kind = str(getattr(getattr(resolved_target, "kind", None), "name", "")).lower()
907+
if "cuda" in kind:
908+
pass_configs = {
909+
"tl.disable_tma_lower": True,
910+
"tl.disable_warp_specialized": True,
911+
}
912+
return tilelang.compile(
913+
prim,
914+
out_idx=[9, 10, 11, 12],
915+
target=resolved_target,
916+
pass_configs=pass_configs,
917+
)
855918
return tilelang.compile(
856919
prim,
857920
out_idx=[9, 10, 11, 12],
858-
target=_resolve_chunked_compile_target(target),
921+
target=resolved_target,
859922
)

cppmega_mlx/runtime/path_c_relax_step_banks.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@
8484
parse_physical_bank_shapes,
8585
)
8686
from cppmega_mlx.runtime.path_c_fusion import (
87+
MAMBA3_CHUNKED_SCAN_ENV,
8788
build_path_c_aot_autograd_region,
8889
build_path_c_model_region_from_route_symbols,
90+
path_c_mamba3_chunked_scan_enabled,
8991
)
9092
from cppmega_mlx.runtime.path_c_fusion_schedules import path_c_fusion_schedule_template
9193
from cppmega_mlx.recipes.model_factory import local_gb10_quarter_profile
@@ -295,12 +297,87 @@ def packed(actg_in, param, state_ckpt, paramg_in, actg_out, paramg_out):
295297
return packed
296298

297299

300+
# Env that EXPLICITLY acknowledges the abstract numpy bank backward driver is the
301+
# intended path for THIS bank-SSA build (the CPU self-check / non-gridded path).
302+
# Default OFF. When the gridded chunked-scan backward (B0/B1/B2) is enabled but the
303+
# real backward driver has NOT been installed, register_bank_drivers RAISES rather
304+
# than silently binding the numpy host backward (RULE #1: no silent fallback). Set
305+
# this truthy ONLY for the deliberate numpy self-check, never in production.
306+
ALLOW_NUMPY_BANK_BWD_ENV = "CPPMEGA_PATH_C_ALLOW_NUMPY_BANK_BWD"
307+
308+
# Set truthy by register_real_backward_driver once the REAL gridded bank backward is
309+
# installed, so a subsequent register_bank_drivers (e.g. re-entry) does NOT clobber
310+
# the real bwd binding with the numpy one (and the RULE #1 guard knows the real path
311+
# is live). This is a process-local in-memory flag, NOT an env read.
312+
_REAL_BANK_BWD_INSTALLED = False
313+
314+
315+
def _numpy_bank_bwd_allowed() -> bool:
316+
import os as _os
317+
318+
raw = _os.environ.get(ALLOW_NUMPY_BANK_BWD_ENV, "")
319+
return raw.strip().lower() in {"1", "true", "yes", "on"}
320+
321+
322+
def set_real_bank_bwd_installed(value: bool) -> None:
323+
"""Mark whether the REAL gridded bank backward driver has been installed.
324+
325+
Called by ``register_real_backward_driver`` so a later ``register_bank_drivers``
326+
does NOT clobber the real gridded ``pathc.bank_bwd_*`` binding with the abstract
327+
numpy one, and so the RULE #1 guard treats the gridded backward as live (it does
328+
NOT raise — the real path is installed, not the numpy fallback).
329+
"""
330+
global _REAL_BANK_BWD_INSTALLED
331+
_REAL_BANK_BWD_INSTALLED = bool(value)
332+
333+
334+
def real_bank_bwd_installed() -> bool:
335+
"""Return whether the REAL gridded bank backward driver is currently installed."""
336+
return _REAL_BANK_BWD_INSTALLED
337+
338+
298339
def register_bank_drivers(numels: dict[str, int], n_layers: int) -> None:
340+
# RULE #1 (NO SILENT FALLBACK): when the gridded chunked-scan backward is
341+
# ENABLED, the bank-SSA train_step path's per-region `pathc.bank_bwd_i` is the
342+
# ABSTRACT NUMPY host backward (`_region_bwd_driver`) — a host round-trip of the
343+
# ~2028 MB region banks that does NOT route through the gridded B0/B1/B2 region
344+
# surfaces. Binding it while the gridded backward is enabled would be a SILENT
345+
# numpy-host fallback masquerading as the gridded path. So:
346+
# * If the real gridded bank backward is already installed
347+
# (`register_real_backward_driver` set `_REAL_BANK_BWD_INSTALLED`), keep it —
348+
# bind ONLY the forward here, never clobber the real bwd with numpy.
349+
# * Else if the flag is ON and numpy-bwd was NOT explicitly acknowledged
350+
# (`ALLOW_NUMPY_BANK_BWD_ENV`), RAISE with WHERE+WHAT — direct the caller to
351+
# the direct-chain region path (build_path_c_model_region_from_route_symbols
352+
# + compile_path_c_region, flag ON) OR register_real_backward_driver, which
353+
# actually execute the gridded B0/B1/B2.
354+
# * Else (flag OFF, or numpy explicitly acknowledged for the CPU self-check):
355+
# the numpy bank backward is the legitimate path — bind it.
356+
chunked_on = path_c_mamba3_chunked_scan_enabled()
357+
if chunked_on and not _REAL_BANK_BWD_INSTALLED and not _numpy_bank_bwd_allowed():
358+
raise RuntimeError(
359+
"register_bank_drivers (path_c_relax_step_banks): the gridded chunked "
360+
"backward is ENABLED ("
361+
f"{MAMBA3_CHUNKED_SCAN_ENV}=1) but this bank-SSA "
362+
"build_train_step path would bind `pathc.bank_bwd_*` to the ABSTRACT "
363+
"NUMPY host backward (_region_bwd_driver), which does NOT execute the "
364+
"gridded B0/B1/B2 region surfaces. Binding it would be a SILENT "
365+
"numpy-host backward fallback (RULE #1 forbidden). Drive the gridded "
366+
"backward via EITHER the direct-chain region path "
367+
"(build_path_c_model_region_from_route_symbols + compile_path_c_region "
368+
"with the flag ON, which routes through the chunked region surfaces and "
369+
"the delegation interpose) OR call register_real_backward_driver(...) to "
370+
"install the real gridded bank backward. To DELIBERATELY run the numpy "
371+
"self-check with the flag on, set "
372+
f"{ALLOW_NUMPY_BANK_BWD_ENV}=1 (CPU reference only, never production)."
373+
)
299374
for i in range(n_layers):
300375
tvm_ffi.register_global_func(
301376
f"pathc.bank_fwd_{i}", _region_fwd_driver(numels), override=True)
302-
tvm_ffi.register_global_func(
303-
f"pathc.bank_bwd_{i}", _region_bwd_driver(numels), override=True)
377+
# Do NOT overwrite a real gridded bwd binding with the numpy one.
378+
if not _REAL_BANK_BWD_INSTALLED:
379+
tvm_ffi.register_global_func(
380+
f"pathc.bank_bwd_{i}", _region_bwd_driver(numels), override=True)
304381

305382

306383
def build_bank_chain(numels: dict[str, int], n_layers: int) -> tvm.IRModule:

cppmega_mlx/runtime/path_c_relax_train_step.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
bank_writeback,
7878
real_bank_numels,
7979
register_bank_drivers,
80+
set_real_bank_bwd_installed,
8081
)
8182
from cppmega_mlx.runtime.path_c_dps_adapter import (
8283
alloc_device_banks,
@@ -246,6 +247,149 @@ def register_real_forward_driver(leaf, real_driver, bank_numels: dict[str, int],
246247
tvm_ffi.register_global_func(f"pathc.bank_fwd_{i}", fwd, override=True)
247248

248249

250+
# --------------------------------------------------------------------------- #
251+
# (b2) The REAL-kernel BACKWARD driver — symmetric to make_real_bank_forward_driver.
252+
#
253+
# This is what flips the §15-profiled `pathc.bank_bwd_i` from the ABSTRACT NUMPY host
254+
# backward (path_c_relax_step_banks._region_bwd_driver, the 79.6%/91.4% wall — a host
255+
# round-trip of the ~2028 MB region banks) to the REAL GRIDDED backward MR kernel
256+
# (run_backward=1), which behind the call_dps_packed boundary runs the gridded
257+
# B0/B1/B2 chunked SSD backward on the physical banks. The bank-SSA, remat, optimizer,
258+
# and loss structure are UNCHANGED — only the per-region backward compute becomes the
259+
# real device-resident kernel.
260+
#
261+
# The bank-backward call_dps_packed ABI (from _region_bwd_driver) is
262+
# (actg_in, param, state_ckpt, paramg_in, actg_out, paramg_out):
263+
# IN : actg_in = incoming activation grad (from the next layer)
264+
# param = the model weights (read-only)
265+
# state_ckpt= checkpoint i (the forward-saved activation snapshot)
266+
# paramg_in = running parameter-grad accumulator
267+
# OUT: actg_out = grad propagated to the previous layer (the bwd kernel writes
268+
# BANK_ACTG in place)
269+
# paramg_out= updated parameter-grad accumulator (BANK_PARAMG in place)
270+
#
271+
# `leaf` is a BACKWARD PathCRegionLeaf (run_backward=1) carrying the real kernel;
272+
# `real_driver` is make_real_kernel_driver(leaf, device) from the driver phase.
273+
# --------------------------------------------------------------------------- #
274+
def make_real_bank_backward_driver(leaf, real_driver, bank_numels: dict[str, int],
275+
*, device=None):
276+
"""Build a `pathc.bank_bwd_*` packed func that runs the REAL gridded MR backward.
277+
278+
Mirrors make_real_bank_forward_driver exactly (same DEVICE-RESIDENT vs
279+
NUMPY-STAGED gate, RULE #1: clear device-vs-host gate, no silent fallback), but
280+
over the BACKWARD bank ABI. The real kernel is the SAME compiled artifact as the
281+
forward, specialized to run_backward=1 (the gridded B0/B1/B2 transpose). It seeds
282+
BANK_ACTG/BANK_PARAM/BANK_STATE/BANK_PARAMG from actg_in/param/state_ckpt/
283+
paramg_in, runs the kernel (writes BANK_ACTG + BANK_PARAMG in place), then writes
284+
actg_out = BANK_ACTG and paramg_out = BANK_PARAMG.
285+
286+
RULE #1: no numpy host backward is reachable from this driver; the only paths are
287+
(a) DEVICE-RESIDENT real kernel and (b) NUMPY-STAGED real kernel (the same real
288+
compiled kernel, just host-staged for the CPU self-check). Any failure RAISES.
289+
"""
290+
291+
actg_n = bank_numels[BANK_ACTG]
292+
state_n = bank_numels[BANK_STATE]
293+
param_n = bank_numels[BANK_PARAM]
294+
paramg_n = bank_numels[BANK_PARAMG]
295+
296+
# Build the device-resident kernel driver + driver-owned device physical banks
297+
# ONCE (lazily, first device call); persists across every backward call.
298+
dev_state: dict[str, object] = {}
299+
300+
def _ensure_device_state(dev):
301+
if "driver" not in dev_state:
302+
dev_state["driver"] = make_device_resident_kernel_driver(leaf, dev)
303+
dev_state["banks"] = alloc_device_banks(leaf.bank_shapes, dev)
304+
return dev_state["driver"], dev_state["banks"]
305+
306+
def packed(actg_in, param, state_ckpt, paramg_in, actg_out, paramg_out):
307+
if bank_arg_is_device(actg_in):
308+
# ---- DEVICE-RESIDENT PATH (no numpy host bounce) ----
309+
dev = actg_in.device
310+
dev_driver, dbanks = _ensure_device_state(dev)
311+
# Seed the backward kernel's physical banks from the SSA banks
312+
# (device->device view copies; tail-zeroed where the SSA bank is shorter).
313+
bank_copy_prefix_device(
314+
dbanks[BANK_ACTG], actg_in,
315+
min(actg_n, int(np.prod([int(d) for d in actg_in.shape]))))
316+
bank_copy_prefix_device(
317+
dbanks[BANK_PARAM], param,
318+
min(param_n, int(np.prod([int(d) for d in param.shape]))))
319+
bank_copy_prefix_device(
320+
dbanks[BANK_STATE], state_ckpt,
321+
min(state_n, int(np.prod([int(d) for d in state_ckpt.shape]))))
322+
bank_copy_prefix_device(
323+
dbanks[BANK_PARAMG], paramg_in,
324+
min(paramg_n, int(np.prod([int(d) for d in paramg_in.shape]))))
325+
# Run the REAL gridded backward kernel (run_backward=1) on the device
326+
# banks (writes BANK_ACTG + BANK_PARAMG in place).
327+
dev_driver(leaf, dbanks, dev)
328+
# actg_out <- new activation grad (device->device).
329+
bank_copy_prefix_device(
330+
actg_out, dbanks[BANK_ACTG],
331+
min(actg_n, int(np.prod([int(d) for d in actg_out.shape]))))
332+
# paramg_out <- updated parameter-grad accumulator (device->device).
333+
bank_copy_prefix_device(
334+
paramg_out, dbanks[BANK_PARAMG],
335+
min(paramg_n, int(np.prod([int(d) for d in paramg_out.shape]))))
336+
return
337+
338+
# ---- NUMPY-STAGED REFERENCE PATH (CPU self-test; STILL the real kernel) ----
339+
ag = bank_arg_to_host(actg_in)
340+
p = bank_arg_to_host(param)
341+
ck = bank_arg_to_host(state_ckpt)
342+
pgi = bank_arg_to_host(paramg_in)
343+
banks = {
344+
BANK_ACT: np.zeros(bank_numels[BANK_ACT], np.float32),
345+
BANK_ACTG: np.ascontiguousarray(ag, np.float32).reshape(-1)[:actg_n].copy(),
346+
BANK_PARAM: np.ascontiguousarray(p, np.float32).reshape(-1)[:param_n].copy(),
347+
BANK_PARAMG: np.ascontiguousarray(pgi, np.float32).reshape(-1)[:paramg_n].copy(),
348+
BANK_STATE: np.ascontiguousarray(ck, np.float32).reshape(-1)[:state_n].copy(),
349+
}
350+
for bank, n in ((BANK_ACTG, actg_n), (BANK_PARAM, param_n),
351+
(BANK_PARAMG, paramg_n), (BANK_STATE, state_n)):
352+
if banks[bank].size < n:
353+
pad = np.zeros(n, np.float32)
354+
pad[: banks[bank].size] = banks[bank]
355+
banks[bank] = pad
356+
real_driver(leaf, banks)
357+
bank_writeback(actg_out, banks[BANK_ACTG].reshape(-1)[:actg_n])
358+
bank_writeback(paramg_out, banks[BANK_PARAMG].reshape(-1)[:paramg_n])
359+
360+
return packed
361+
362+
363+
def register_real_backward_driver(leaf, real_driver, bank_numels: dict[str, int],
364+
n_layers: int, *, device=None) -> None:
365+
"""Install the REAL gridded path_c backward kernel as EVERY `pathc.bank_bwd_i`.
366+
367+
This REPLACES the abstract numpy host backward (the §15 79.6%/91.4% wall) with the
368+
real device-resident gridded backward MR kernel (run_backward=1 -> gridded
369+
B0/B1/B2). It also flips ``set_real_bank_bwd_installed(True)`` so that:
370+
* the RULE #1 guard in register_bank_drivers does NOT raise (the gridded path is
371+
live, not the forbidden numpy fallback), and
372+
* a subsequent register_bank_drivers re-entry does NOT clobber this real bwd
373+
binding with the numpy one.
374+
375+
``leaf`` MUST be a BACKWARD leaf (run_backward=1); ``device`` enables the
376+
DEVICE-RESIDENT backward path (banks stay device tensors, no host bounce). Pass the
377+
CUDA device on gb10. RULE #1: a forward leaf here is a wiring bug — RAISE."""
378+
if int(getattr(leaf, "run_backward", 0)) != 1:
379+
raise RuntimeError(
380+
"register_real_backward_driver: leaf.run_backward must be 1 (a BACKWARD "
381+
f"leaf), got run_backward={getattr(leaf, 'run_backward', None)!r}. "
382+
"Pass the bwd-specialized PathCRegionLeaf (RULE #1: no silent forward "
383+
"leaf binding as the backward region)."
384+
)
385+
bwd = make_real_bank_backward_driver(leaf, real_driver, bank_numels, device=device)
386+
for i in range(n_layers):
387+
tvm_ffi.register_global_func(f"pathc.bank_bwd_{i}", bwd, override=True)
388+
# Mark the real gridded backward as installed so the RULE #1 guard knows the
389+
# numpy bank backward is NOT the live path (and is never re-bound over this).
390+
set_real_bank_bwd_installed(True)
391+
392+
249393
# --------------------------------------------------------------------------- #
250394
# (c) The whole-step IRModule: fwd (remat) + bwd + in-place Adam + LOSS.
251395
#

0 commit comments

Comments
 (0)