Skip to content

Commit 91000dc

Browse files
committed
HybridTinyLM: own Path C physical ABI banks so fused train-block route binds
The m04 fp8_path_c training route's auto-install path is gated on the model exposing a callable make_path_c_physical_abi_bank_owner(sequence_length=...) factory. Without one, the route reports FP8_PATH_C_FUSED_TRAIN_BLOCK_BANKS_MISSING_STATUS, the artifact never gets wrapped into a PathCFusedTrainBlockTrainingRuntime, and every cell in the 1B Path C matrix records 'fused_train_block_runtime_available=False'. This change adds two model methods: - path_c_fused_train_block_prim_func(...) -> PrimFunc | None Drives the existing route discovery + plan_path_c_fusion_schedule_for_region pipeline and returns the generated PrimFunc that carries the physical-ABI attrs. Cached by caller only; no implicit reuse. - make_path_c_physical_abi_bank_owner(...) -> PathCPhysicalAbiBankOwner | None Reads _cppmega_path_c_physical_buffer_abi_{map,shapes} off the prim_func, allocates each declared bank as a zero-init mx.array sized to the exact physical-ABI dtype/shape, and returns a validated owner via make_physical_abi_bank_owner (no hidden packing, no implicit copy). After this commit, fp8_path_c_training_route_payload_for_model on a tiny HybridTinyLM reports binding.status='ok', physical_abi_binding_ready=True, fused_artifact_bound=True, and runtime_uses_fused_train_block=True with all three banks (float32, uint8, int32) bound to the model owner. Behaviour change checked into the existing test_fp8_path_c_training_route_reports_model_owned_physical_bank_plan: the binding status assertion flipped from BANKS_MISSING to 'ok'; bank-plan content invariants unchanged. Verified: pytest -q on every existing path_c m04 test plus the new tests/test_hybrid_lm_path_c_physical_abi_bank_owner.py (4 cases) all pass; ruff and pyright clean on the touched module.
1 parent e4047e4 commit 91000dc

3 files changed

Lines changed: 245 additions & 4 deletions

File tree

cppmega_mlx/models/hybrid_lm.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@
4343
from cppmega_mlx.nn.structure_embedding import CppMegaStructureEmbedding
4444
from cppmega_mlx.recipes.pattern import ExpandedNamPattern, NamLayer, expand_nam_pattern
4545
from cppmega_mlx.runtime.kernel_policy import KernelPath, selected_path
46-
from cppmega_mlx.runtime.path_c_physical_abi import PathCLogicalBufferOwner
46+
from cppmega_mlx.runtime.path_c_physical_abi import (
47+
PathCLogicalBufferOwner,
48+
PathCPhysicalAbiBankOwner,
49+
make_physical_abi_bank_owner,
50+
)
4751
from cppmega_mlx.runtime.path_c_taps import emit_and_tap_path_c_tensor
4852
from cppmega_mlx.training.mtp import MinimalMTPHead, MTPLossConfig
4953

@@ -74,6 +78,33 @@
7478

7579
HybridAttentionMode = Literal["mla", "dsa", "full", "gqa"]
7680

81+
_PATH_C_BANK_DTYPES: dict[str, mx.Dtype] = {
82+
"bool": mx.bool_,
83+
"uint8": mx.uint8,
84+
"int8": mx.int8,
85+
"uint16": mx.uint16,
86+
"int16": mx.int16,
87+
"uint32": mx.uint32,
88+
"int32": mx.int32,
89+
"uint64": mx.uint64,
90+
"int64": mx.int64,
91+
"float16": mx.float16,
92+
"bfloat16": mx.bfloat16,
93+
"float32": mx.float32,
94+
}
95+
96+
97+
def _path_c_bank_dtype(name: str) -> mx.Dtype:
98+
"""Translate a Path C physical-ABI dtype string into an ``mx.Dtype``."""
99+
try:
100+
return _PATH_C_BANK_DTYPES[name]
101+
except KeyError as exc:
102+
raise ValueError(
103+
f"unsupported Path C physical ABI bank dtype {name!r}"
104+
) from exc
105+
106+
107+
77108

78109
class StructureEmbeddingConfigKwargs(TypedDict):
79110
hidden_size: int
@@ -1061,6 +1092,110 @@ def path_c_fusion_regions(
10611092
sequence_length=sequence_length,
10621093
)
10631094

1095+
def path_c_fused_train_block_prim_func(
1096+
self,
1097+
*,
1098+
sequence_length: int | None = None,
1099+
) -> Any | None:
1100+
"""Materialise the fused-train-block PrimFunc for this model.
1101+
1102+
This drives the same schedule planner the production runtime uses and
1103+
returns the generated PrimFunc (carrying the physical-ABI attrs), or
1104+
``None`` when no Path C route region exists. The model never caches the
1105+
artifact — callers that need to reuse it across calls should hold the
1106+
returned reference themselves.
1107+
"""
1108+
from cppmega_mlx.runtime.path_c_fusion_schedules import (
1109+
plan_path_c_fusion_schedule_for_region,
1110+
)
1111+
1112+
regions = self.path_c_fusion_regions(
1113+
include_backward=True,
1114+
sequence_length=sequence_length,
1115+
)
1116+
if not regions:
1117+
return None
1118+
selected = max(
1119+
regions,
1120+
key=lambda region: (
1121+
len(region.nodes),
1122+
len(region.edges),
1123+
region.name,
1124+
),
1125+
)
1126+
planned = plan_path_c_fusion_schedule_for_region(
1127+
selected,
1128+
include_backward=True,
1129+
)
1130+
schedule_target = getattr(planned, "schedule_target", None)
1131+
if schedule_target is None:
1132+
return None
1133+
return schedule_target.schedule_template(planned.region)
1134+
1135+
def make_path_c_physical_abi_bank_owner(
1136+
self,
1137+
*,
1138+
sequence_length: int | None = None,
1139+
) -> PathCPhysicalAbiBankOwner | None:
1140+
"""Allocate the physical ABI banks the generated fused PrimFunc needs.
1141+
1142+
Returns a validated :class:`PathCPhysicalAbiBankOwner` whose ``buffers``
1143+
are freshly zero-initialised MLX arrays sized exactly to the generated
1144+
``_cppmega_path_c_physical_buffer_abi_shapes`` map (dtype is taken from
1145+
the corresponding logical-buffer placement so every bank reflects the
1146+
generated kernel ABI). The owner never repacks or copies model tensors;
1147+
it only owns the bank arrays so the runtime can bind kernel arguments
1148+
in declared order.
1149+
1150+
Returns ``None`` when no Path C route region exists for the model.
1151+
"""
1152+
prim_func = self.path_c_fused_train_block_prim_func(
1153+
sequence_length=sequence_length,
1154+
)
1155+
if prim_func is None:
1156+
return None
1157+
abi_map = dict(
1158+
getattr(prim_func, "_cppmega_path_c_physical_buffer_abi_map", {})
1159+
or {}
1160+
)
1161+
abi_shapes = dict(
1162+
getattr(prim_func, "_cppmega_path_c_physical_buffer_abi_shapes", {})
1163+
or {}
1164+
)
1165+
if not abi_map or not abi_shapes:
1166+
return None
1167+
bank_dtypes: dict[str, str] = {}
1168+
for placement in abi_map.values():
1169+
bank = str(placement["bank"])
1170+
dtype = str(placement["dtype"])
1171+
existing = bank_dtypes.setdefault(bank, dtype)
1172+
if existing != dtype:
1173+
raise ValueError(
1174+
f"conflicting bank dtype for {bank!r}: "
1175+
f"{existing!r} vs {dtype!r}"
1176+
)
1177+
bank_buffers: dict[str, mx.array] = {}
1178+
for bank, shape in abi_shapes.items():
1179+
dtype_name = bank_dtypes.get(str(bank))
1180+
if dtype_name is None:
1181+
raise ValueError(
1182+
f"no logical buffer is placed inside physical bank {bank!r}"
1183+
)
1184+
mx_dtype = _path_c_bank_dtype(dtype_name)
1185+
bank_buffers[str(bank)] = mx.zeros(
1186+
tuple(int(dim) for dim in tuple(shape)),
1187+
dtype=mx_dtype,
1188+
)
1189+
profile_name = str(
1190+
getattr(self, "path_c_profile_name", "HybridTinyLM")
1191+
)
1192+
return make_physical_abi_bank_owner(
1193+
f"{profile_name}.path_c_physical_abi_banks",
1194+
abi_map,
1195+
abi_shapes,
1196+
bank_buffers,
1197+
)
1198+
10641199
def __call__(
10651200
self,
10661201
input_ids: mx.array,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""HybridTinyLM.make_path_c_physical_abi_bank_owner.
2+
3+
Verifies that the model exposes the model-owned physical ABI bank factory
4+
the m04 Path C auto-install path requires. The check exercises the actual
5+
production schedule (no monkeypatching) and asserts that the resulting
6+
owner satisfies the structural contract enforced by
7+
``cppmega_mlx.runtime.path_c_physical_abi.make_physical_abi_bank_owner``.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import mlx.core as mx
13+
import pytest
14+
15+
from cppmega_mlx.recipes.model_factory import (
16+
build_local_gb10_quarter_tiny_smoke_model,
17+
)
18+
from cppmega_mlx.runtime.path_c_physical_abi import (
19+
PathCPhysicalAbiBankOwner,
20+
)
21+
22+
23+
@pytest.fixture(scope="module")
24+
def model():
25+
return build_local_gb10_quarter_tiny_smoke_model()
26+
27+
28+
def test_make_path_c_physical_abi_bank_owner_returns_validated_owner(model) -> None:
29+
owner = model.make_path_c_physical_abi_bank_owner(sequence_length=64)
30+
assert isinstance(owner, PathCPhysicalAbiBankOwner)
31+
assert owner.owner_name == "local_gb10_quarter.path_c_physical_abi_banks"
32+
assert owner.binding_payload["status"] == "ok"
33+
assert owner.hidden_packing_performed is False
34+
assert owner.no_hidden_allocation_policy is True
35+
# Bank specs must be non-empty and every bank buffer must already be
36+
# allocated as an mx.array with the spec's shape and dtype.
37+
assert owner.bank_specs, "expected at least one physical ABI bank"
38+
required = set(owner.required_bank_buffers)
39+
assert set(owner.buffers) == required
40+
for spec in owner.bank_specs:
41+
buf = owner.buffers[spec.name]
42+
assert isinstance(buf, mx.array)
43+
assert tuple(buf.shape) == spec.shape
44+
assert str(buf.dtype).rsplit(".", 1)[-1] == spec.dtype
45+
46+
47+
def test_make_path_c_physical_abi_bank_owner_buffers_are_zero_initialised(model) -> None:
48+
owner = model.make_path_c_physical_abi_bank_owner(sequence_length=64)
49+
for spec in owner.bank_specs:
50+
buf = owner.buffers[spec.name]
51+
sample = buf.astype(mx.float32)
52+
mx.eval(sample)
53+
assert float(sample.sum().item()) == 0.0
54+
55+
56+
def test_path_c_fused_train_block_prim_func_exposes_physical_abi_attrs(model) -> None:
57+
prim_func = model.path_c_fused_train_block_prim_func(sequence_length=64)
58+
assert prim_func is not None
59+
abi_map = dict(
60+
getattr(prim_func, "_cppmega_path_c_physical_buffer_abi_map", {}) or {}
61+
)
62+
abi_shapes = dict(
63+
getattr(prim_func, "_cppmega_path_c_physical_buffer_abi_shapes", {}) or {}
64+
)
65+
# The route region for local_gb10_quarter must emit a banked physical ABI.
66+
assert abi_map, "physical ABI map must not be empty"
67+
assert abi_shapes, "physical ABI bank shapes must not be empty"
68+
# Bank names referenced by the placements must all appear in the shape map.
69+
placement_banks = {str(info["bank"]) for info in abi_map.values()}
70+
assert placement_banks <= set(abi_shapes)
71+
72+
73+
def test_owner_unblocks_m04_fp8_path_c_runtime_binding(model) -> None:
74+
"""The owner must let the m04 route_payload reach a bound runtime."""
75+
76+
import importlib.util
77+
from pathlib import Path
78+
79+
spec = importlib.util.spec_from_file_location(
80+
"m04_train_step", str(Path("scripts/m04_train_step.py").resolve())
81+
)
82+
assert spec is not None and spec.loader is not None
83+
m04 = importlib.util.module_from_spec(spec)
84+
spec.loader.exec_module(m04)
85+
86+
args = m04.build_parser().parse_args([
87+
"--synthetic", "--dtype", "fp8_path_c",
88+
"--model-profile", "local_gb10_quarter",
89+
"--dry-run-json", "--output", "/tmp/test_owner_route.json",
90+
"--data-path", "/dev/null", "--data-format", "npz",
91+
])
92+
route = m04.fp8_path_c_training_route_payload_for_model(args, model)
93+
binding = route.get("path_c_fusion", {}).get("runtime_training_binding", {})
94+
assert binding.get("runtime_uses_fused_train_block") is True, route
95+
assert binding.get("physical_abi_binding_ready") is True, binding
96+
assert binding.get("fused_artifact_bound") is True, binding
97+
assert binding.get("missing_bank_buffers") == [], binding
98+
assert binding.get("bank_buffer_owner") == (
99+
"local_gb10_quarter.path_c_physical_abi_banks"
100+
)

tests/test_m04_train_step.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3871,9 +3871,15 @@ def test_fp8_path_c_training_route_reports_model_owned_physical_bank_plan(
38713871
)
38723872

38733873
binding = route["path_c_fusion"]["runtime_training_binding"]
3874-
assert binding["status"] == (
3875-
m04_train_step.FP8_PATH_C_FUSED_TRAIN_BLOCK_BANKS_MISSING_STATUS
3876-
)
3874+
# HybridTinyLM now provides ``make_path_c_physical_abi_bank_owner`` so
3875+
# the production auto-install path resolves the banks before the route
3876+
# payload is computed; the runtime training binding reports ``ok``.
3877+
assert binding["status"] == "ok", binding
3878+
assert binding["runtime_uses_fused_train_block"] is True
3879+
assert binding["physical_abi_binding_ready"] is True
3880+
assert binding["fused_artifact_bound"] is True
3881+
assert binding["missing_bank_buffers"] == []
3882+
assert binding["provided_bank_buffers"] == binding["required_bank_buffers"]
38773883
bank_plan = binding["model_owned_physical_abi_bank_plan"]
38783884
assert bank_plan["status"] == "model_owned_physical_abi_banks_required"
38793885
assert bank_plan["owner_attribute"] == "path_c_physical_abi_bank_owner"

0 commit comments

Comments
 (0)