Skip to content

Commit 0f1e950

Browse files
committed
feat: wire path c fused train-block runtime contract
1 parent 4c9f87c commit 0f1e950

7 files changed

Lines changed: 584 additions & 43 deletions

File tree

cppmega_mlx/runtime/path_c_fusion_schedules.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@
112112
DESCRIPTOR_SHARED_SCRATCH_SPILL_THRESHOLD_BYTES = 4 * 1024
113113
DESCRIPTOR_SHARED_SCRATCH_BUDGET_BYTES = 24 * 1024
114114
_GENERIC_MODEL_REAL_ABI_INPUT_SUFFIXES = ("residual_norm_weight",)
115+
_TRAIN_STEP_SCALAR_OUTPUT_ABI_NAMES = ("loss", "ntokens")
116+
_TRAIN_STEP_SCALAR_OUTPUT_ABI_REASON = (
117+
"train-step scalar ABI slots are declared, but suffix loss codegen is not "
118+
"fused into the descriptor body yet"
119+
)
115120
_DTYPE_NBYTES = {
116121
"bool": 1,
117122
"uint8": 1,
@@ -820,6 +825,7 @@ def make_path_c_descriptor_schedule_template(
820825
internal_buffer_policy: str = DESCRIPTOR_INTERNAL_BUFFER_POLICY_SCALAR_LOCAL,
821826
loop_policy: str = DESCRIPTOR_LOOP_POLICY_FLAT,
822827
physical_abi_policy: str = DESCRIPTOR_PHYSICAL_ABI_POLICY_DIRECT,
828+
train_step_output_abi: bool = False,
823829
) -> Callable[[Any], Any]:
824830
"""Return a schedule template generated from brick descriptors."""
825831

@@ -845,6 +851,7 @@ def descriptor_schedule_template(template_region: Any) -> Any:
845851
internal_buffer_policy=validated_internal_buffer_policy,
846852
loop_policy=validated_loop_policy,
847853
physical_abi_policy=validated_physical_abi_policy,
854+
train_step_output_abi=bool(train_step_output_abi),
848855
)
849856

850857
descriptor_schedule_template._cppmega_path_c_schedule_generator = (
@@ -867,6 +874,9 @@ def descriptor_schedule_template(template_region: Any) -> Any:
867874
if _descriptor_chain_uses_kv_history_workspace(descriptors)
868875
else ()
869876
)
877+
descriptor_schedule_template._cppmega_path_c_train_step_output_abi_enabled = (
878+
bool(train_step_output_abi)
879+
)
870880
return descriptor_schedule_template
871881

872882

@@ -918,6 +928,7 @@ def build_path_c_descriptor_prim_func(
918928
internal_buffer_policy: str = DESCRIPTOR_INTERNAL_BUFFER_POLICY_SCALAR_LOCAL,
919929
loop_policy: str = DESCRIPTOR_LOOP_POLICY_FLAT,
920930
physical_abi_policy: str = DESCRIPTOR_PHYSICAL_ABI_POLICY_DIRECT,
931+
train_step_output_abi: bool = False,
921932
) -> Any:
922933
"""Generate a single-entry TileLang PrimFunc from a descriptor chain."""
923934

@@ -949,6 +960,18 @@ def build_path_c_descriptor_prim_func(
949960
for name in (*node.inputs, *node.outputs)
950961
}
951962
external_buffers = _external_buffers_for_nodes(nodes, internal_buffers)
963+
train_step_output_abi_payload = _train_step_output_abi_payload(
964+
declared=bool(train_step_output_abi)
965+
)
966+
train_step_output_buffers = tuple(
967+
train_step_output_abi_payload["logical_outputs"]
968+
if train_step_output_abi_payload["declared"]
969+
else ()
970+
)
971+
external_buffers_for_abi = _append_unique_names(
972+
external_buffers,
973+
train_step_output_buffers,
974+
)
952975
extent = _validated_buffer_extent(buffer_extent)
953976
entry_name = _safe_identifier(
954977
entry_symbol or getattr(region, "entry_symbol", None) or getattr(region, "name", None)
@@ -963,13 +986,17 @@ def build_path_c_descriptor_prim_func(
963986
name: _buffer_shape(name, extent, resolved_shape_env)
964987
for name in external_buffers
965988
}
989+
dtype_by_buffer = dict(dtype_by_buffer)
990+
for name in train_step_output_buffers:
991+
dtype_by_buffer[name] = "float32"
992+
shape_by_buffer[name] = (1,)
966993
loop_extent = _descriptor_loop_extent(
967994
external_buffers,
968995
extent,
969996
resolved_shape_env,
970997
)
971998
physical_abi_plan = _physical_abi_plan(
972-
external_buffers=external_buffers,
999+
external_buffers=external_buffers_for_abi,
9731000
shape_by_buffer=shape_by_buffer,
9741001
dtype_by_buffer=dtype_by_buffer,
9751002
buffer_extent=extent,
@@ -986,7 +1013,7 @@ def build_path_c_descriptor_prim_func(
9861013
physical_abi_plan=physical_abi_plan,
9871014
internal_buffer_policy=validated_internal_buffer_policy,
9881015
loop_policy=validated_loop_policy,
989-
external_buffers=external_buffers,
1016+
external_buffers=external_buffers_for_abi,
9901017
shape_by_buffer=shape_by_buffer,
9911018
dtype_by_buffer=dtype_by_buffer,
9921019
buffer_extent=extent,
@@ -1033,6 +1060,9 @@ def build_path_c_descriptor_prim_func(
10331060
).with_attr(
10341061
"tl.fusion.physical_abi.physical_buffer_shapes",
10351062
json.dumps(physical_abi_plan.physical_buffer_shapes, sort_keys=True),
1063+
).with_attr(
1064+
"tl.fusion.train_step_output_abi",
1065+
json.dumps(train_step_output_abi_payload, sort_keys=True),
10361066
)
10371067
compile_pass_configs = _descriptor_tilelang_compile_pass_configs(
10381068
descriptors,
@@ -1056,8 +1086,8 @@ def build_path_c_descriptor_prim_func(
10561086
prim_func._cppmega_path_c_physical_abi_policy = validated_physical_abi_policy
10571087
prim_func._cppmega_path_c_internal_buffer_shapes = internal_buffer_shapes
10581088
prim_func._cppmega_path_c_buffer_abi_shapes = {
1059-
name: _buffer_shape(name, extent, resolved_shape_env)
1060-
for name in external_buffers
1089+
name: shape_by_buffer[name]
1090+
for name in external_buffers_for_abi
10611091
}
10621092
prim_func._cppmega_path_c_physical_buffer_abi_shapes = dict(
10631093
physical_abi_plan.physical_buffer_shapes
@@ -1074,6 +1104,9 @@ def build_path_c_descriptor_prim_func(
10741104
prim_func._cppmega_path_c_loop_extent = loop_extent
10751105
prim_func._cppmega_path_c_generated_source = source
10761106
prim_func._cppmega_path_c_compile_pass_configs = compile_pass_configs
1107+
prim_func._cppmega_path_c_train_step_output_abi = dict(
1108+
train_step_output_abi_payload
1109+
)
10771110
return prim_func
10781111

10791112

@@ -1090,6 +1123,43 @@ def _descriptor_tilelang_compile_pass_configs(
10901123
return {}
10911124

10921125

1126+
def _append_unique_names(
1127+
names: Sequence[str],
1128+
extra_names: Sequence[str],
1129+
) -> tuple[str, ...]:
1130+
values = [str(name) for name in names]
1131+
seen = set(values)
1132+
for raw_name in extra_names:
1133+
name = str(raw_name)
1134+
if name in seen:
1135+
continue
1136+
values.append(name)
1137+
seen.add(name)
1138+
return tuple(values)
1139+
1140+
1141+
def _train_step_output_abi_payload(
1142+
*,
1143+
declared: bool,
1144+
outputs_computed: bool = False,
1145+
) -> dict[str, Any]:
1146+
return {
1147+
"declared": bool(declared),
1148+
"outputs_computed": bool(outputs_computed),
1149+
"logical_outputs": _TRAIN_STEP_SCALAR_OUTPUT_ABI_NAMES
1150+
if declared
1151+
else (),
1152+
"reason": _TRAIN_STEP_SCALAR_OUTPUT_ABI_REASON
1153+
if declared and not outputs_computed
1154+
else (
1155+
"train-step scalar ABI slots are generated and populated by fused "
1156+
"suffix loss codegen"
1157+
if declared
1158+
else "train-step scalar ABI slots are not required for this descriptor"
1159+
),
1160+
}
1161+
1162+
10931163
def _descriptor_prim_func_source(
10941164
*,
10951165
entry_name: str,
@@ -7412,6 +7482,7 @@ def _dynamic_descriptor_target_for_region(
74127482
internal_buffer_policy=internal_buffer_policy,
74137483
loop_policy=loop_policy,
74147484
)
7485+
train_step_output_abi = _region_requires_train_step_output_abi(region, nodes)
74157486
schedule_name = (
74167487
acceptance_profile.schedule_name
74177488
if acceptance_profile is not None
@@ -7430,6 +7501,8 @@ def _dynamic_descriptor_target_for_region(
74307501
extra_steps.append("z3_sync_async_schedule_points")
74317502
if shape_env is not None:
74327503
extra_steps.append("cache_key_shape_specialization_audit")
7504+
if train_step_output_abi:
7505+
extra_steps.append("train_step_scalar_output_abi")
74337506
if extra_steps:
74347507
seen = set(required_codegen_steps)
74357508
required_codegen_steps = (
@@ -7493,6 +7566,7 @@ def _dynamic_descriptor_target_for_region(
74937566
internal_buffer_policy=internal_buffer_policy,
74947567
loop_policy=loop_policy,
74957568
physical_abi_policy=physical_abi_policy,
7569+
train_step_output_abi=train_step_output_abi,
74967570
),
74977571
required_real_abi_inputs=required_real_abi_inputs,
74987572
brick_descriptors=descriptors,
@@ -7503,6 +7577,20 @@ def _dynamic_descriptor_target_for_region(
75037577
)
75047578

75057579

7580+
def _region_requires_train_step_output_abi(
7581+
region: Any,
7582+
nodes: Sequence[_ScheduleNodeView],
7583+
) -> bool:
7584+
metadata = getattr(region, "metadata", {}) or {}
7585+
if bool(metadata.get("path_c_acceptance_fixture_abi", False)):
7586+
return False
7587+
if metadata.get("path_c_chain_source_region"):
7588+
return False
7589+
if not metadata.get("path_c_bricks"):
7590+
return False
7591+
return any(str(node.op_name).endswith("_bwd") for node in nodes)
7592+
7593+
75067594
def _physical_abi_policy_for_region(
75077595
nodes: Sequence[_ScheduleNodeView],
75087596
*,

cppmega_mlx/training/compiled.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,94 @@ def clear(self) -> None:
108108
self.events.clear()
109109

110110

111+
class PathCFusedTrainBlockTrainingRuntime:
112+
"""Training runtime wrapper for a contracted fused Path C train artifact.
113+
114+
The wrapper binds an already-compiled fused artifact to caller/model-owned
115+
physical ABI banks. It never allocates, packs, casts, or reshapes tensors;
116+
artifacts that need those steps must fail closed before reaching this seam.
117+
"""
118+
119+
contract = PATH_C_FUSED_TRAIN_BLOCK_TRAINING_RUNTIME_CONTRACT
120+
training_critical_path = True
121+
hidden_packing_performed = False
122+
no_hidden_allocation_policy = True
123+
uses_fused_train_block_runtime = True
124+
125+
def __init__(
126+
self,
127+
*,
128+
artifact: Any,
129+
bank_owner: Any,
130+
owner_name: str,
131+
) -> None:
132+
if not callable(artifact):
133+
raise TypeError("fused train-block artifact must be callable")
134+
if not callable(getattr(artifact, "forward", None)):
135+
raise TypeError("fused train-block artifact must define forward")
136+
if not (
137+
callable(getattr(artifact, "backward", None))
138+
or callable(getattr(artifact, "vjp", None))
139+
):
140+
raise TypeError("fused train-block artifact must define backward or vjp")
141+
if not callable(getattr(artifact, "value_and_grad", None)):
142+
raise TypeError("fused train-block artifact must define value_and_grad")
143+
if not callable(getattr(artifact, "value_and_grad_contract", None)):
144+
raise TypeError(
145+
"fused train-block artifact must define value_and_grad_contract"
146+
)
147+
self.artifact = artifact
148+
self.bank_owner = bank_owner
149+
self.owner_name = owner_name
150+
self._binding: dict[str, Any] | None = None
151+
152+
def _with_bank_owner(self, kwargs: Mapping[str, Any]) -> dict[str, Any]:
153+
payload = dict(kwargs)
154+
payload.setdefault("bank_owner", self.bank_owner)
155+
return payload
156+
157+
def forward(self, *args: Any, **kwargs: Any) -> Any:
158+
return self.artifact.forward(*args, **self._with_bank_owner(kwargs))
159+
160+
def backward(self, *args: Any, **kwargs: Any) -> Any:
161+
backward = getattr(self.artifact, "backward", None)
162+
if callable(backward):
163+
return backward(*args, **self._with_bank_owner(kwargs))
164+
return self.artifact.vjp(*args, **self._with_bank_owner(kwargs))
165+
166+
def vjp(self, *args: Any, **kwargs: Any) -> Any:
167+
vjp = getattr(self.artifact, "vjp", None)
168+
if callable(vjp):
169+
return vjp(*args, **self._with_bank_owner(kwargs))
170+
return self.backward(*args, **kwargs)
171+
172+
def value_and_grad(
173+
self,
174+
model: nn.Module,
175+
batch: Mapping[str, mx.array],
176+
loss_and_grad: Any,
177+
) -> tuple[tuple[mx.array, mx.array], Any]:
178+
del loss_and_grad
179+
return self.artifact.value_and_grad(
180+
model,
181+
batch,
182+
bank_owner=self.bank_owner,
183+
)
184+
185+
def bind_training_graph(self, **binding: Any) -> None:
186+
self._binding = dict(binding)
187+
188+
def unbind_training_graph(self, *, owner: str) -> None:
189+
if self._binding is not None and self._binding.get("owner") == owner:
190+
self._binding = None
191+
192+
def training_graph_binding(self) -> dict[str, Any]:
193+
return dict(self._binding or {})
194+
195+
def value_and_grad_contract(self) -> Mapping[str, Any]:
196+
return self.artifact.value_and_grad_contract()
197+
198+
111199
def _path_c_capture_event_metadata(event: Mapping[str, Any]) -> Mapping[str, Any]:
112200
metadata: dict[str, Any] = {}
113201
for key, value in event.items():
@@ -660,6 +748,7 @@ def _path_c_training_runtime_uses_fused_train_block(
660748
"PATH_C_TRAINING_VALUE_AND_GRAD_CONTRACT",
661749
"PathCGradientBufferCapture",
662750
"PathCGradientProbe",
751+
"PathCFusedTrainBlockTrainingRuntime",
663752
"PathCTrainingRuntime",
664753
"REGIONAL_COMPILE_TARGETS",
665754
"maybe_compile_region",

0 commit comments

Comments
 (0)