Skip to content

Commit ca1cc7c

Browse files
committed
feat: implement fused train-block runtime and coalesced scratch bank allocation for FP8 path-c training
1 parent 44e5093 commit ca1cc7c

37 files changed

Lines changed: 6359 additions & 363 deletions

cppmega_mlx/nn/m2rnn.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from cppmega_mlx.nn.mamba3 import causal_depthwise_conv1d
1313

1414
DEFAULT_CHUNK_SIZE = 128
15+
PATH_B_REFERENCE_BASELINE_KERNEL = "reference_path_b_baseline"
1516

1617

1718
def _require_positive_int(name: str, value: int) -> None:
@@ -309,8 +310,9 @@ def _dispatch_m2rnn_scan(
309310
310311
AUTO uses the retired Path B status seam only when it is available,
311312
otherwise it falls back to the pure-MLX :func:`chunked_m2rnn_scan`.
312-
PATH_B and PATH_C are explicit and fail-closed when their native routes
313-
are unavailable.
313+
PATH_B keeps the benchmark baseline runnable by using the legacy
314+
differentiable compatibility wrapper when direct MSL is retired. PATH_C
315+
remains explicit and fail-closed when its native route is unavailable.
314316
"""
315317

316318
from cppmega_mlx.runtime.kernel_policy import (
@@ -369,11 +371,7 @@ def _dispatch_m2rnn_scan(
369371
)
370372

371373
status = m2rnn_metal_status(q)
372-
if path is KernelPath.PATH_B and not status.available:
373-
raise RuntimeError(
374-
f"m2rnn: Path B kernel unavailable ({status.reason})"
375-
)
376-
if status.available:
374+
if path is KernelPath.PATH_B or status.available:
377375
q_b, k_b, v_b, W_b, xf_b = broadcast_m2rnn_heads(q, k, v, W, xf)
378376
batch, _seq, heads, k_dim = q_b.shape
379377
v_dim = v_b.shape[-1]
@@ -386,7 +384,15 @@ def _dispatch_m2rnn_scan(
386384
dtype=q_b.dtype,
387385
)
388386
out, h = m2rnn_apply_with_state(q_b, k_b, v_b, W_b, xf_b, h0_full)
389-
record_dispatch("m2rnn", path, "metal_kernel_fwd_v1")
387+
record_dispatch(
388+
"m2rnn",
389+
path,
390+
(
391+
"metal_kernel_fwd_v1"
392+
if status.available
393+
else PATH_B_REFERENCE_BASELINE_KERNEL
394+
),
395+
)
390396
return out, h
391397

392398
record_dispatch("m2rnn", path, "reference_pure_mlx")
@@ -782,6 +788,7 @@ def __call__(
782788
"M2RNNConfig",
783789
"M2RNNMixer",
784790
"M2RNNMixerState",
791+
"PATH_B_REFERENCE_BASELINE_KERNEL",
785792
"broadcast_m2rnn_heads",
786793
"chunked_m2rnn_scan",
787794
"m2rnn_softplus_decay_gate",

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@
112112
DESCRIPTOR_PHYSICAL_ABI_POLICY_BANKED_BY_DTYPE = "banked_by_dtype"
113113
DESCRIPTOR_PHYSICAL_ABI_POLICY_BANKED_BY_ROLE = "banked_by_role"
114114
DESCRIPTOR_PORTABLE_KERNEL_BUFFER_LIMIT = 31
115-
DESCRIPTOR_SHARED_SCRATCH_SPILL_THRESHOLD_BYTES = 4 * 1024
116-
DESCRIPTOR_SHARED_SCRATCH_BUDGET_BYTES = 24 * 1024
115+
DESCRIPTOR_SHARED_SCRATCH_SPILL_THRESHOLD_BYTES = 1024
116+
DESCRIPTOR_SHARED_SCRATCH_BUDGET_BYTES = 12 * 1024
117117
MAMBA3_BWD_REPLAY_CHECKPOINT_INTERVAL = 64
118118
_DESCRIPTOR_ROOT_READS_MARKER = "cppmega_path_c_root_reads"
119119
_DESCRIPTOR_ROOT_WRITES_MARKER = "cppmega_path_c_root_writes"
@@ -139,6 +139,20 @@
139139
"mamba3_delta_grad",
140140
}
141141
)
142+
DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_CANONICALS = frozenset(
143+
{
144+
"hidden",
145+
"attention_hidden",
146+
"hidden_after_mamba3",
147+
"m2rnn_hidden",
148+
"m2rnn_delta",
149+
"mamba3_delta",
150+
"q_fp8",
151+
"q_scale",
152+
"kv_fp8",
153+
"kv_scale",
154+
}
155+
)
142156
DESCRIPTOR_DEFAULT_MAX_ROWS_PER_LAUNCH = 64
143157
DESCRIPTOR_ROW_DISPATCH_GRID_CHUNKS = "grid_chunks"
144158
DESCRIPTOR_ROW_DISPATCH_LAUNCHER_CHUNKS = "launcher_chunks"
@@ -3033,9 +3047,9 @@ def _spill_large_shared_scratch_to_abi(
30333047
byte_count = _flattened_extent(shape) * _DTYPE_NBYTES[dtype]
30343048
total_shared_bytes += byte_count
30353049
scratch_name = match.group("name")
3036-
force_scratch_abi = scratch_name in DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_BUFFERS
3050+
force_scratch_abi = _is_row_phased_bwd_scratch_abi_buffer(scratch_name)
30373051
if (
3038-
byte_count > DESCRIPTOR_SHARED_SCRATCH_SPILL_THRESHOLD_BYTES
3052+
byte_count >= DESCRIPTOR_SHARED_SCRATCH_SPILL_THRESHOLD_BYTES
30393053
or force_scratch_abi
30403054
):
30413055
candidates.append(
@@ -3178,6 +3192,21 @@ def rewrite_scratch_refs(line: str) -> str:
31783192
return "\n".join(rewritten) + "\n", spilled
31793193

31803194

3195+
def _is_row_phased_bwd_scratch_abi_buffer(buffer_name: str) -> bool:
3196+
"""Return True for internal bwd scratch that must be a device ABI buffer."""
3197+
3198+
name = str(buffer_name)
3199+
if name in DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_BUFFERS:
3200+
return True
3201+
if not name.endswith("_grad"):
3202+
return False
3203+
canonical = _canonical_buffer_name(name)
3204+
if canonical in DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_CANONICALS:
3205+
return True
3206+
base = name[: -len("_grad")]
3207+
return base.endswith(("_hidden", "_delta", "_out"))
3208+
3209+
31813210
def _validated_internal_buffer_policy(policy: str) -> str:
31823211
normalized = str(policy)
31833212
if normalized not in {
@@ -3594,6 +3623,27 @@ def _row_local_internal_buffer_shape(
35943623
shape_env: PathCModelShapeEnv,
35953624
) -> tuple[int, ...]:
35963625
canonical_name = _canonical_buffer_name(buffer_name)
3626+
if str(buffer_name).endswith("_grad") and (
3627+
canonical_name in DESCRIPTOR_ROW_PHASED_BWD_SCRATCH_ABI_CANONICALS
3628+
or str(buffer_name)[: -len("_grad")].endswith(("_hidden", "_delta", "_out"))
3629+
):
3630+
if canonical_name == "q_fp8":
3631+
return (
3632+
shape_env.sequence_length
3633+
* shape_env.attention_num_q_heads
3634+
* shape_env.attention_head_dim,
3635+
)
3636+
if canonical_name == "q_scale":
3637+
return (shape_env.sequence_length * shape_env.attention_num_q_heads,)
3638+
if canonical_name == "kv_fp8":
3639+
return (
3640+
shape_env.sequence_length
3641+
* shape_env.attention_num_kv_heads
3642+
* shape_env.attention_head_dim,
3643+
)
3644+
if canonical_name == "kv_scale":
3645+
return (shape_env.sequence_length * shape_env.attention_num_kv_heads,)
3646+
return (shape_env.sequence_length * shape_env.hidden_size,)
35973647
if canonical_name in {
35983648
"mamba3_delta",
35993649
"m2rnn_hidden",
@@ -3616,23 +3666,6 @@ def _row_local_internal_buffer_shape(
36163666
* shape_env.attention_num_kv_heads
36173667
* shape_env.attention_sparse_topk,
36183668
)
3619-
if str(buffer_name).endswith("_grad"):
3620-
if canonical_name == "q_fp8":
3621-
return (
3622-
shape_env.sequence_length
3623-
* shape_env.attention_num_q_heads
3624-
* shape_env.attention_head_dim,
3625-
)
3626-
if canonical_name == "q_scale":
3627-
return (shape_env.sequence_length * shape_env.attention_num_q_heads,)
3628-
if canonical_name == "kv_fp8":
3629-
return (
3630-
shape_env.sequence_length
3631-
* shape_env.attention_num_kv_heads
3632-
* shape_env.attention_head_dim,
3633-
)
3634-
if canonical_name == "kv_scale":
3635-
return (shape_env.sequence_length * shape_env.attention_num_kv_heads,)
36363669
if canonical_name == "kv_fp8":
36373670
return (shape_env.attention_num_kv_heads * shape_env.attention_head_dim,)
36383671
if canonical_name == "kv_scale":
@@ -12290,13 +12323,14 @@ def _dynamic_descriptor_target_for_region(
1229012323
acceptance_profile,
1229112324
descriptors,
1229212325
)
12326+
train_step_output_abi = _region_requires_train_step_output_abi(region, nodes)
1229312327
physical_abi_policy = _physical_abi_policy_for_region(
1229412328
nodes,
1229512329
shape_env=shape_env,
1229612330
internal_buffer_policy=internal_buffer_policy,
1229712331
loop_policy=loop_policy,
12332+
train_step_output_abi=train_step_output_abi,
1229812333
)
12299-
train_step_output_abi = _region_requires_train_step_output_abi(region, nodes)
1230012334
schedule_name = (
1230112335
acceptance_profile.schedule_name
1230212336
if acceptance_profile is not None
@@ -12412,6 +12446,7 @@ def _physical_abi_policy_for_region(
1241212446
shape_env: PathCModelShapeEnv | None,
1241312447
internal_buffer_policy: str,
1241412448
loop_policy: str,
12449+
train_step_output_abi: bool = False,
1241512450
) -> str:
1241612451
internal_buffers = _internal_buffers_for_nodes(
1241712452
nodes,
@@ -12420,6 +12455,10 @@ def _physical_abi_policy_for_region(
1242012455
loop_policy=loop_policy,
1242112456
)
1242212457
external_buffer_count = len(_external_buffers_for_nodes(nodes, internal_buffers))
12458+
if train_step_output_abi:
12459+
external_buffer_count += len(_TRAIN_STEP_SCALAR_OUTPUT_ABI_NAMES)
12460+
external_buffer_count += len(_TRAIN_STEP_SUFFIX_LOSS_INPUT_ABI_NAMES)
12461+
external_buffer_count += len(_TRAIN_STEP_SUFFIX_LOSS_PARAMETER_GRAD_ABI_NAMES)
1242312462
if external_buffer_count > DESCRIPTOR_PORTABLE_KERNEL_BUFFER_LIMIT:
1242412463
return DESCRIPTOR_PHYSICAL_ABI_POLICY_BANKED_BY_ROLE
1242512464
return DESCRIPTOR_PHYSICAL_ABI_POLICY_DIRECT

cppmega_mlx/training/compiled.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
from cppmega_mlx.data.batch import LMTokenBatch, ensure_lm_batch
2323
from cppmega_mlx.runtime.path_c_physical_abi import (
24-
physical_abi_full_runtime_kernel_args,
2524
physical_abi_runtime_kernel_args,
2625
)
2726
from cppmega_mlx.training.loss import next_token_cross_entropy
@@ -53,6 +52,11 @@
5352
PATH_C_FUSED_TRAIN_BLOCK_VALUE_AND_GRAD_CONTRACT = (
5453
"path_c_fused_train_block_value_and_grad_v1"
5554
)
55+
PATH_C_SCALAR_KERNEL_PARAM_DEFAULTS: Mapping[str, int] = {
56+
"path_c_run_backward": 1,
57+
"path_c_row_chunk_index": 0,
58+
"path_c_row_subchunk_index": 0,
59+
}
5660

5761
REGIONAL_COMPILE_TARGETS: Mapping[CompileTarget, bool] = {
5862
"mamba3_pre": True,
@@ -213,12 +217,35 @@ def _bank_buffers(self, bank_owner: Any) -> Mapping[str, Any]:
213217

214218
def _kernel_args_from_bank_owner(self, bank_owner: Any) -> tuple[Any, ...]:
215219
if self.kernel_buffer_order is not None:
216-
return physical_abi_full_runtime_kernel_args(
220+
buffers = self._bank_buffers(bank_owner)
221+
physical_abi_runtime_kernel_args(
217222
self.physical_abi_map,
218223
self.physical_abi_shapes,
219-
self.kernel_buffer_order,
220-
self._bank_buffers(bank_owner),
224+
{
225+
name: buffers[name]
226+
for name in self.physical_abi_shapes
227+
if name in buffers
228+
},
221229
)
230+
missing: list[str] = []
231+
args: list[Any] = []
232+
for name in self.kernel_buffer_order:
233+
if name in buffers:
234+
args.append(buffers[name])
235+
continue
236+
if name in PATH_C_SCALAR_KERNEL_PARAM_DEFAULTS:
237+
args.append(PATH_C_SCALAR_KERNEL_PARAM_DEFAULTS[name])
238+
continue
239+
missing.append(name)
240+
if missing:
241+
raise ValueError(
242+
"physical ABI runtime bindings are not executable: "
243+
+ "; ".join(
244+
f"{name}: missing caller-owned kernel buffer"
245+
for name in missing
246+
)
247+
)
248+
return tuple(args)
222249
return physical_abi_runtime_kernel_args(
223250
self.physical_abi_map,
224251
self.physical_abi_shapes,

cppmega_mlx/training/path_c_fused_suffix.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@
4040

4141

4242
FusedSuffixCallable = Callable[..., Any]
43+
_MX_DTYPE_BY_ABI_NAME = {
44+
"bool": mx.bool_,
45+
"uint8": mx.uint8,
46+
"int8": mx.int8,
47+
"float16": mx.float16,
48+
"bfloat16": mx.bfloat16,
49+
"uint16": mx.uint16,
50+
"int16": mx.int16,
51+
"float32": mx.float32,
52+
"uint32": mx.uint32,
53+
"int32": mx.int32,
54+
"float64": mx.float64,
55+
"uint64": mx.uint64,
56+
"int64": mx.int64,
57+
}
4358

4459

4560
def _zeros_like_with_cotangent_dtype(value: mx.array) -> mx.array:
@@ -56,6 +71,22 @@ def _zeros_like_with_cotangent_dtype(value: mx.array) -> mx.array:
5671
return mx.zeros_like(value)
5772

5873

74+
def _target_mask_for_abi(
75+
abi_map: Mapping[str, Any],
76+
logical_name: str,
77+
value: mx.array,
78+
) -> mx.array:
79+
info = abi_map.get(logical_name)
80+
if not isinstance(info, Mapping):
81+
return value
82+
expected_dtype = _MX_DTYPE_BY_ABI_NAME.get(str(info.get("dtype", "")))
83+
if expected_dtype is None or value.dtype == expected_dtype:
84+
return value
85+
if value.dtype not in (mx.float16, mx.bfloat16, mx.float32, mx.float64):
86+
return value
87+
return value.astype(expected_dtype)
88+
89+
5990
def build_fused_suffix_custom_function(
6091
*,
6192
artifact: Any,
@@ -115,6 +146,11 @@ def _write_runtime_state(
115146
write_into_bank_slot(
116147
abi_map, bank_buffers, target_ids_logical_name, target_ids
117148
)
149+
target_mask = _target_mask_for_abi(
150+
abi_map,
151+
target_mask_logical_name,
152+
target_mask,
153+
)
118154
write_into_bank_slot(
119155
abi_map, bank_buffers, target_mask_logical_name, target_mask
120156
)

cppmega_v4/_tilelang/_path_d_deps.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
TRITON_FRONTEND_PATH_ENV = "CPPMEGA_MLX_TRITON_FRONTEND_PATH"
1515
FLA_SOURCE_PATH_ENV = "CPPMEGA_MLX_FLA_SOURCE_PATH"
16+
TRITON_FRONTEND_UNSAFE_IMPORT_ENV = "CPPMEGA_V4_ENABLE_UNSAFE_TRITON_IMPORT"
1617

1718
_TRITON_FRONTEND_ROOTS = (
1819
Path("/Users/dave/sources/tilelang"),
@@ -62,9 +63,47 @@ def ensure_fla_root() -> str | None:
6263
return None
6364

6465

66+
def unsafe_triton_frontend_import_enabled() -> bool:
67+
"""Return True only when the caller explicitly accepts unsafe Triton imports.
68+
69+
Some local Triton checkouts abort the Python process during import instead
70+
of raising a Python exception. Path D status probes run in normal test and
71+
benchmark discovery, so they must fail closed unless the developer opts in.
72+
"""
73+
74+
return os.environ.get(TRITON_FRONTEND_UNSAFE_IMPORT_ENV, "").strip().lower() in {
75+
"1",
76+
"true",
77+
"yes",
78+
"on",
79+
}
80+
81+
82+
def unsafe_triton_frontend_import_disabled_reason(root: str | None) -> str:
83+
root_text = f" root={root}" if root else " root=<not found>"
84+
return (
85+
"triton frontend not importable by default: unsafe import disabled "
86+
f"({TRITON_FRONTEND_UNSAFE_IMPORT_ENV}=1 required); Path D runtime "
87+
f"adapter not reached;{root_text}"
88+
)
89+
90+
91+
def unsafe_fla_import_disabled_reason(root: str | None) -> str:
92+
root_text = f" root={root}" if root else " root=<not found>"
93+
return (
94+
"FLA not importable by default: unsafe Path D import disabled "
95+
f"({TRITON_FRONTEND_UNSAFE_IMPORT_ENV}=1 required); Path D runtime "
96+
f"adapter not reached;{root_text}"
97+
)
98+
99+
65100
__all__ = [
66101
"FLA_SOURCE_PATH_ENV",
67102
"TRITON_FRONTEND_PATH_ENV",
103+
"TRITON_FRONTEND_UNSAFE_IMPORT_ENV",
68104
"ensure_fla_root",
69105
"ensure_triton_frontend_root",
106+
"unsafe_fla_import_disabled_reason",
107+
"unsafe_triton_frontend_import_disabled_reason",
108+
"unsafe_triton_frontend_import_enabled",
70109
]

0 commit comments

Comments
 (0)