Skip to content

Commit 9b7cadb

Browse files
committed
refactor: implement continuous snake graph alignment in vbgui and enforce buffer shape safety in compilation runtime.
1 parent c6579ff commit 9b7cadb

23 files changed

Lines changed: 5500 additions & 1675 deletions

cppmega_mlx/nn/_tilelang/mamba3_path_c.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@
108108
MSLDispatchUnsupported,
109109
can_run_metal,
110110
)
111+
from cppmega_mlx.nn._tilelang._mlx_runtime import (
112+
NativeTileLangRuntimeError,
113+
_validate_owner_result,
114+
)
111115
from cppmega_mlx.nn._tilelang.mamba3 import _validate_inputs
112116

113117

@@ -2789,14 +2793,17 @@ def mamba3_mimo_fwd_path_c(
27892793
if not isinstance(out_list, (list, tuple)) or len(out_list) != 2:
27902794
raise RuntimeError("Mamba3 Path C fwd tvm-ffi returned an invalid output tuple")
27912795
if owner_outputs is not None:
2792-
y, h_last = owner_outputs
2793-
if not all(
2794-
got is expected
2795-
for got, expected in zip(out_list, (y, h_last), strict=True)
2796-
):
2796+
try:
2797+
y, h_last = cast(
2798+
tuple[mx.array, mx.array],
2799+
_validate_owner_result(out_list, owner_outputs),
2800+
)
2801+
except NativeTileLangRuntimeError as exc:
27972802
raise RuntimeError(
27982803
"Mamba3 Path C fwd tvm-ffi did not return caller-owned outputs"
2799-
)
2804+
) from exc
2805+
del lowering
2806+
return y, h_last
28002807
y, h_last = cast(tuple[mx.array, mx.array], tuple(out_list))
28012808
del lowering
28022809
return y, h_last

cppmega_mlx/runtime/path_c_fusion_launcher.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ class PathCAbiManifest:
114114
row_subchunk_index_param: str | None
115115
row_chunk_index_buffer: str | None
116116
backward_gate_param: str | None
117+
backward_stage_count: int | None
118+
backward_stage_index_param: str | None
117119

118120

119121
def _optional_positive_int_attr(prim_func: Any, key: str) -> int | None:
@@ -241,6 +243,12 @@ def load_path_c_abi_manifest(prim_func: Any) -> PathCAbiManifest:
241243
backward_gate_param=_optional_string_attr(
242244
prim_func, "tl.fusion.backward_gate_param"
243245
),
246+
backward_stage_count=_optional_positive_int_attr(
247+
prim_func, "tl.fusion.backward_stage_count"
248+
),
249+
backward_stage_index_param=_optional_string_attr(
250+
prim_func, "tl.fusion.backward_stage_index_param"
251+
),
244252
)
245253

246254

@@ -534,6 +542,7 @@ def __call__(
534542
row_chunk_param_index: int | None = None
535543
row_chunk_param_is_scalar = False
536544
row_subchunk_param_index: int | None = None
545+
backward_stage_param_index: int | None = None
537546
backward_gate_param_index: int | None = None
538547
for param_name in manifest.param_order:
539548
logical_name = param_name[: -len("_handle")] if param_name.endswith("_handle") else param_name
@@ -554,6 +563,9 @@ def __call__(
554563
elif logical_name == manifest.row_subchunk_index_param:
555564
row_subchunk_param_index = len(positional)
556565
positional.append(0)
566+
elif logical_name == manifest.backward_stage_index_param:
567+
backward_stage_param_index = len(positional)
568+
positional.append(0)
557569
elif logical_name == manifest.backward_gate_param:
558570
backward_gate_param_index = len(positional)
559571
positional.append(_BACKWARD_GATE if run_backward else _FORWARD_ONLY_GATE)
@@ -608,6 +620,8 @@ def apply_returned_outputs(returned: Any) -> None:
608620
continue
609621
elif logical_name == manifest.row_subchunk_index_param:
610622
continue
623+
elif logical_name == manifest.backward_stage_index_param:
624+
continue
611625
elif logical_name == manifest.backward_gate_param:
612626
continue
613627
positional[param_index] = value
@@ -678,26 +692,52 @@ def restore_logical_buffers(snapshots: Mapping[str, mx.array]) -> None:
678692
"compiled PrimFunc declares row subchunk dispatch metadata "
679693
"but has no row subchunk index parameter"
680694
)
681-
def launch_chunk(chunk_index: int, subchunk_index: int) -> None:
695+
if (
696+
manifest.backward_stage_index_param is not None
697+
and backward_stage_param_index is None
698+
):
699+
raise ValueError(
700+
"compiled PrimFunc declares backward stage dispatch metadata "
701+
"but has no backward stage index parameter"
702+
)
703+
704+
def launch_chunk(
705+
chunk_index: int,
706+
subchunk_index: int,
707+
backward_stage_index: int = 0,
708+
) -> None:
682709
positional[row_chunk_param_index] = (
683710
int(chunk_index)
684711
if row_chunk_param_is_scalar
685712
else mx.array([chunk_index], dtype=mx.int32)
686713
)
687714
if row_subchunk_param_index is not None:
688715
positional[row_subchunk_param_index] = int(subchunk_index)
716+
if backward_stage_param_index is not None:
717+
positional[backward_stage_param_index] = int(backward_stage_index)
689718
apply_returned_outputs(self._kernel(*positional))
690719

691720
if run_backward and backward_gate_param_index is not None:
692721
positional[backward_gate_param_index] = _BACKWARD_FORWARD_PREPASS_GATE
722+
prepass_subchunk_count = int(manifest.row_subchunk_count or 1)
693723
for chunk_index in range(chunk_count):
694-
launch_chunk(chunk_index, 0)
724+
for subchunk_index in range(prepass_subchunk_count):
725+
launch_chunk(chunk_index, subchunk_index)
695726
final_forward_states = snapshot_logical_buffers(
696727
_STATE_AFTER_LOGICAL_BUFFERS
697728
)
698729
restore_logical_buffers(initial_backward_states)
699730
positional[backward_gate_param_index] = _BACKWARD_GATE
700-
launch_chunk(0, 0)
731+
backward_subchunk_count = int(manifest.row_subchunk_count or 1)
732+
backward_stage_count = int(manifest.backward_stage_count or 1)
733+
for backward_stage_index in range(backward_stage_count):
734+
for chunk_index in range(chunk_count):
735+
for subchunk_index in range(backward_subchunk_count):
736+
launch_chunk(
737+
chunk_index,
738+
subchunk_index,
739+
backward_stage_index,
740+
)
701741
restore_logical_buffers(final_forward_states)
702742
else:
703743
for chunk_index in range(chunk_count):

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
PATH_C_DESCRIPTOR_SCHEDULE_GENERATOR = "dynamic_brick_descriptor_generator"
114114
DESCRIPTOR_DEFAULT_BUFFER_EXTENT = 4
115115
DESCRIPTOR_DEFAULT_THREADS = 256
116-
DESCRIPTOR_ROW_PHASED_THREADS = 128
116+
DESCRIPTOR_ROW_PHASED_THREADS = 1024
117117
DESCRIPTOR_INTERNAL_BUFFER_POLICY_SCALAR_LOCAL = "scalar_local"
118118
DESCRIPTOR_INTERNAL_BUFFER_POLICY_ROW_LOCAL_HIDDEN = "row_local_hidden"
119119
DESCRIPTOR_LOOP_POLICY_FLAT = "flat"
@@ -123,7 +123,7 @@
123123
DESCRIPTOR_PHYSICAL_ABI_POLICY_BANKED_BY_ROLE = "banked_by_role"
124124
DESCRIPTOR_PORTABLE_KERNEL_BUFFER_LIMIT = 31
125125
DESCRIPTOR_SHARED_SCRATCH_SPILL_THRESHOLD_BYTES = 1024
126-
DESCRIPTOR_SHARED_SCRATCH_BUDGET_BYTES = 12 * 1024
126+
DESCRIPTOR_SHARED_SCRATCH_BUDGET_BYTES = 8 * 1024
127127
MAMBA3_BWD_REPLAY_CHECKPOINT_INTERVAL = 8
128128
M2RNN_BWD_REPLAY_CHECKPOINT_INTERVAL = 1
129129
_DESCRIPTOR_ROOT_READS_MARKER = "cppmega_path_c_root_reads"
@@ -1780,6 +1780,7 @@ def build_path_c_descriptor_prim_func(
17801780
internal_buffers=internal_buffers,
17811781
internal_buffer_shapes=internal_buffer_shapes,
17821782
physical_abi_plan=physical_abi_plan,
1783+
physical_abi_policy=validated_physical_abi_policy,
17831784
internal_buffer_policy=validated_internal_buffer_policy,
17841785
loop_policy=validated_loop_policy,
17851786
max_rows_per_launch=validated_max_rows_per_launch,
@@ -2408,6 +2409,7 @@ def _descriptor_prim_func_source(
24082409
internal_buffers: Sequence[str],
24092410
internal_buffer_shapes: Mapping[str, tuple[int, ...]],
24102411
physical_abi_plan: _PhysicalAbiPlan,
2412+
physical_abi_policy: str,
24112413
internal_buffer_policy: str,
24122414
loop_policy: str,
24132415
max_rows_per_launch: int | None,
@@ -2667,6 +2669,9 @@ def _descriptor_prim_func_source(
26672669
existing_parameter_count=len(param_lines),
26682670
internal_buffer_names=frozenset(active_internal_buffers),
26692671
force_spill_names=frozenset(force_spilled_internal_buffers),
2672+
force_builtin_spill_names=(
2673+
physical_abi_policy != DESCRIPTOR_PHYSICAL_ABI_POLICY_DIRECT
2674+
),
26702675
)
26712676

26722677

@@ -3861,6 +3866,7 @@ def _spill_large_shared_scratch_to_abi(
38613866
existing_parameter_count: int,
38623867
internal_buffer_names: frozenset[str] = frozenset(),
38633868
force_spill_names: frozenset[str] = frozenset(),
3869+
force_builtin_spill_names: bool = True,
38643870
) -> tuple[str, Mapping[str, Mapping[str, Any]]]:
38653871
candidates: list[dict[str, Any]] = []
38663872
total_shared_bytes = 0
@@ -3881,7 +3887,10 @@ def _spill_large_shared_scratch_to_abi(
38813887
force_scratch_abi = (
38823888
scratch_name in force_spill_names
38833889
or _is_row_phased_bwd_scratch_abi_buffer(scratch_name)
3884-
or _force_spill_shared_scratch_name(scratch_name)
3890+
or (
3891+
force_builtin_spill_names
3892+
and _force_spill_shared_scratch_name(scratch_name)
3893+
)
38853894
)
38863895
if (
38873896
byte_count >= DESCRIPTOR_SHARED_SCRATCH_SPILL_THRESHOLD_BYTES
@@ -6363,6 +6372,7 @@ def _append_row_phased_mamba3_body(
63636372
f"({out_inner}[{feature}] + ({d_skip} * {x_val})) * "
63646373
f"{z_val} * (1.0 / (1.0 + T.exp(-{z_val})))"
63656374
)
6375+
body.append(f"{indent * 3}T.sync_threads()")
63666376
body.append(f"{indent * 3}if ((row + 1) % {checkpoint_interval}) == 0:")
63676377
body.append(f"{indent * 4}{checkpoint_idx} = (row + 1) // {checkpoint_interval}")
63686378
body.append(
@@ -9447,14 +9457,17 @@ def _append_row_phased_m2rnn_bwd_body(
94479457
)
94489458
body.append(f"{indent * 5}for {state_idx} in T.serial(0, {full_state_extent}):")
94499459
body.append(f"{indent * 6}{dh}[{state_idx}] = 0.0")
9450-
body.append(f"{indent * 3}if lane == 0:")
9451-
body.append(f"{indent * 4}for {time_rev} in T.serial({time_rev_range}):")
9452-
body.append(f"{indent * 6}{time_idx} = {sequence_length - 1} - {time_rev}")
94539460
if hidden_grad is not None:
9454-
body.append(f"{indent * 6}for {hidden_loop} in T.serial(0, {hidden_size}):")
94559461
body.append(
9456-
f"{indent * 7}{_indexed_buffer_ref(hidden_grad, access_by_buffer, f'{time_idx} * {hidden_size} + {hidden_loop}')} = 0.0"
9462+
f"{indent * 5}for {grad_flat} in T.serial(0, "
9463+
f"{sequence_length * hidden_size}):"
94579464
)
9465+
body.append(
9466+
f"{indent * 6}{_indexed_buffer_ref(hidden_grad, access_by_buffer, grad_flat)} = 0.0"
9467+
)
9468+
body.append(f"{indent * 3}if lane == 0:")
9469+
body.append(f"{indent * 4}for {time_rev} in T.serial({time_rev_range}):")
9470+
body.append(f"{indent * 6}{time_idx} = {sequence_length - 1} - {time_rev}")
94589471
body.append(f"{indent * 6}{checkpoint_idx} = {time_idx} // {checkpoint_interval}")
94599472
body.append(f"{indent * 6}{checkpoint_start} = {checkpoint_idx} * {checkpoint_interval}")
94609473
body.append(f"{indent * 6}for {state_idx} in T.serial(0, {full_state_extent}):")
@@ -9734,10 +9747,7 @@ def _append_row_phased_m2rnn_bwd_body(
97349747
body.append(f"{indent * 6}for {conv_ch} in T.serial(0, {conv_dim}):")
97359748
body.append(f"{indent * 7}{conv_grad}[{conv_ch}] = 0.0")
97369749
body.append(f"{indent * 6}T.sync_threads()")
9737-
body.append(
9738-
f"{indent * 6}for {proj_dim} in T.serial(lane, {in_proj_dim}, "
9739-
f"step={thread_count}):"
9740-
)
9750+
body.append(f"{indent * 6}for {proj_dim} in T.serial(0, {in_proj_dim}):")
97419751
body.append(f"{indent * 7}{project_grad}[{proj_dim}] = 0.0")
97429752
body.append(f"{indent * 6}for {feature} in T.serial(0, {features}):")
97439753
body.append(f"{indent * 7}{head} = {feature} // {v_dim}")
@@ -9838,13 +9848,7 @@ def _append_row_phased_m2rnn_bwd_body(
98389848
f"({h_prev}[{head} * {k_dim * v_dim} + {kk} * {v_dim} + {vv_inner}] * "
98399849
f"{state_weight_expr_inner})"
98409850
)
9841-
body.append(
9842-
f"{indent * 9}{tanh_val}[0] = "
9843-
f"({h_next}[{head} * {k_dim * v_dim} + {kk} * {v_dim} + {vv}] - "
9844-
f"({decay}[0] * "
9845-
f"{h_prev}[{head} * {k_dim * v_dim} + {kk} * {v_dim} + {vv}])) / "
9846-
f"(1.0 - {decay}[0])"
9847-
)
9851+
body.append(f"{indent * 9}{tanh_val}[0] = T.tanh({accum}[0] + ({k_val} * {v_val}))")
98489852
body.append(
98499853
f"{indent * 9}{scalar0}[0] = {scalar0}[0] + "
98509854
f"({dh}[{head} * {k_dim * v_dim} + {kk} * {v_dim} + {vv}] * "
@@ -10048,10 +10052,7 @@ def _append_row_phased_m2rnn_bwd_body(
1004810052
f"{project_grad}[{conv_ch}] + ({scalar1}[0] * {current_conv_weight_expr})"
1004910053
)
1005010054
body.append(f"{indent * 6}T.sync_threads()")
10051-
body.append(
10052-
f"{indent * 6}for {proj_dim} in T.serial(lane, {in_proj_dim}, "
10053-
f"step={thread_count}):"
10054-
)
10055+
body.append(f"{indent * 6}for {proj_dim} in T.serial(0, {in_proj_dim}):")
1005510056
body.append(f"{indent * 7}for {hidden_loop} in T.serial(0, {hidden_size}):")
1005610057
hidden_expr = _node_indexed_canonical_or_positional_input_expr(
1005710058
node,
@@ -10766,7 +10767,6 @@ def _mamba3_emit_recompute_row(
1076610767
)
1076710768
if (
1076810769
hidden_grad is not None
10769-
and not launcher_chunked_rows
1077010770
and not _is_full_sequence_bank_slot(
1077110771
hidden_grad,
1077210772
access_by_buffer,
@@ -10819,14 +10819,6 @@ def _mamba3_emit_recompute_row(
1081910819
body.append(f"{indent * 5}T.sync_threads()")
1082010820
body.append(f"{indent * 5}for {time_rev} in T.serial({time_rev_range}):")
1082110821
body.append(f"{indent * 6}{time_idx} = {sequence_length - 1} - {time_rev}")
10822-
if launcher_chunked_rows and hidden_grad is not None:
10823-
body.append(
10824-
f"{indent * 6}for {hidden_loop} in T.serial(lane, {hidden_size}, "
10825-
f"step={thread_count}):"
10826-
)
10827-
body.append(
10828-
f"{indent * 7}{_indexed_buffer_ref(hidden_grad, access_by_buffer, f'{time_idx} * {hidden_size} + {hidden_loop}')} = 0.0"
10829-
)
1083010822
body.append(f"{indent * 6}{checkpoint_idx} = {time_idx} // {checkpoint_interval}")
1083110823
body.append(
1083210824
f"{indent * 6}{checkpoint_start} = {checkpoint_idx} * {checkpoint_interval}"
@@ -13728,6 +13720,12 @@ def _physical_abi_policy_for_region(
1372813720
external_buffer_count += len(_TRAIN_STEP_SCALAR_OUTPUT_ABI_NAMES)
1372913721
external_buffer_count += len(_TRAIN_STEP_SUFFIX_LOSS_INPUT_ABI_NAMES)
1373013722
external_buffer_count += len(_TRAIN_STEP_SUFFIX_LOSS_PARAMETER_GRAD_ABI_NAMES)
13723+
if (
13724+
shape_env is not None
13725+
and internal_buffer_policy == DESCRIPTOR_INTERNAL_BUFFER_POLICY_ROW_LOCAL_HIDDEN
13726+
and loop_policy == DESCRIPTOR_LOOP_POLICY_ROW_PHASED_HIDDEN
13727+
):
13728+
return DESCRIPTOR_PHYSICAL_ABI_POLICY_BANKED_BY_ROLE
1373113729
if external_buffer_count > DESCRIPTOR_PORTABLE_KERNEL_BUFFER_LIMIT:
1373213730
return DESCRIPTOR_PHYSICAL_ABI_POLICY_BANKED_BY_ROLE
1373313731
return DESCRIPTOR_PHYSICAL_ABI_POLICY_DIRECT

cppmega_mlx/training/compiled.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,12 @@ def _kernel_exact_buffers(self, buffers: Mapping[str, Any]) -> Mapping[str, Any]
332332
expected_size *= int(dim)
333333
actual_size = int(getattr(value, "size", 0) or 0)
334334
if actual_size < expected_size:
335-
continue
335+
raise ValueError(
336+
"physical ABI runtime bindings are not executable: "
337+
f"{name}: caller-owned kernel buffer shape {actual_shape} "
338+
f"has {actual_size} elements, expected at least "
339+
f"{expected_shape} ({expected_size} elements)"
340+
)
336341
view = mx.reshape(value, (-1,))[:expected_size]
337342
if expected_shape != (expected_size,):
338343
view = mx.reshape(view, expected_shape)

cppmega_v4/_tilelang/_path_d_deps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def path_d_imports_allowed(mode: str = "frontend") -> tuple[bool, str]:
361361
def unsafe_triton_frontend_import_disabled_reason(root: str | None) -> str:
362362
root_text = f" root={root}" if root else " root=<not found>"
363363
return (
364-
"unsafe triton frontend import disabled because "
364+
"unsafe triton frontend import disabled (not importable by default) because "
365365
f"{TRITON_FRONTEND_UNSAFE_IMPORT_ENV}=1 is not set; native import "
366366
"preflight did not authorize in-process import; Path D runtime "
367367
f"adapter not reached;{root_text}"
@@ -371,7 +371,7 @@ def unsafe_triton_frontend_import_disabled_reason(root: str | None) -> str:
371371
def unsafe_fla_import_disabled_reason(root: str | None) -> str:
372372
root_text = f" root={root}" if root else " root=<not found>"
373373
return (
374-
"unsafe FLA import disabled because "
374+
"unsafe FLA import disabled (not importable by default) because "
375375
f"{TRITON_FRONTEND_UNSAFE_IMPORT_ENV}=1 is not set; native import "
376376
"preflight did not authorize in-process import; Path D runtime "
377377
f"adapter not reached;{root_text}"

cppmega_v4/fusion/auto_planner.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
77
- Eligibility check delegates entirely to
88
:func:`cppmega_v4.fusion.compatibility.can_fuse_pair` (table-driven).
9-
- A region is **extended** when the trailing brick can fuse with the
10-
next candidate AND no hard limit is breached.
9+
- A region is **extended** when every existing brick in the open region can
10+
fuse with the next candidate AND no hard limit is breached.
1111
- A region is **closed** (and a new one started at the next brick)
1212
when eligibility says ``can_fuse=False``, when adding the next brick
1313
would exceed ``max_region_size`` bricks, or when the estimated
@@ -197,7 +197,9 @@ def plan_fusion_regions(
197197
198198
The decision is:
199199
200-
1. If ``can_fuse_pair(tail, next).can_fuse`` is False → close.
200+
1. If any existing brick in the open region cannot fuse with ``next``
201+
→ close. This prevents light ops such as RMSNorm from bridging two
202+
hard-incompatible heavy blocks into one invalid region.
201203
2. If extending would put the region above ``max_region_size`` → close.
202204
3. If extending would push estimated threadgroup memory above
203205
``max_shared_mem_bytes`` → close.
@@ -234,12 +236,33 @@ def plan_fusion_regions(
234236
if not elig.can_fuse:
235237
close = True
236238
reason = elig.reason
237-
elif len(open_region.nodes) + 1 > max_region_size:
239+
else:
240+
for existing in open_region.nodes[:-1]:
241+
region_elig = can_fuse_pair(existing, current)
242+
if not region_elig.can_fuse:
243+
close = True
244+
reason = (
245+
"region incompatibility: "
246+
f"{existing.kind}->{current.kind}: {region_elig.reason}"
247+
)
248+
break
249+
if region_elig.backend != elig.backend:
250+
close = True
251+
reason = (
252+
"region backend mismatch: "
253+
f"{existing.kind}->{current.kind} uses "
254+
f"{region_elig.backend!r}, tail pair uses {elig.backend!r}"
255+
)
256+
break
257+
if not close and len(open_region.nodes) + 1 > max_region_size:
238258
close = True
239259
reason = (
240260
f"region would exceed max_region_size={max_region_size}"
241261
)
242-
elif open_region.shared_mem_bytes + next_shared > max_shared_mem_bytes:
262+
elif (
263+
not close
264+
and open_region.shared_mem_bytes + next_shared > max_shared_mem_bytes
265+
):
243266
close = True
244267
reason = (
245268
"region would exceed shared-mem budget "

0 commit comments

Comments
 (0)