Skip to content

Commit 79286a3

Browse files
committed
feat: add side-channel support for training pipelines, fix gradient accumulation in m2rnn TileLang kernels, and improve sidebar UI modularity.
1 parent 73e13a5 commit 79286a3

20 files changed

Lines changed: 1676 additions & 46 deletions

cppmega_mlx/nn/_tilelang/m2rnn_path_c.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2658,7 +2658,11 @@ def bwd(
26582658
skipped = y_val + v_val * d_val
26592659
dskipped = dpost_val * silu_g
26602660

2661-
dy_recurrent[b, t, h, vv] = T.cast(dskipped, grad_dtype)
2661+
T.atomic_add(
2662+
dy_recurrent[b, t, h, vv],
2663+
dskipped,
2664+
memory_order="relaxed",
2665+
)
26622666
T.atomic_add(
26632667
dconv_input[b, t, v_index],
26642668
dskipped * d_val,
@@ -2806,7 +2810,11 @@ def bwd(
28062810
skipped = y_acc[0] + v_val * d_val
28072811
dskipped = dpost_val * silu_g
28082812

2809-
dy_recurrent[b, t, h, vv] = T.cast(dskipped, grad_dtype)
2813+
T.atomic_add(
2814+
dy_recurrent[b, t, h, vv],
2815+
dskipped,
2816+
memory_order="relaxed",
2817+
)
28102818
T.atomic_add(
28112819
dconv_input[b, t, v_index],
28122820
dskipped * d_val,
@@ -5384,6 +5392,14 @@ def _apply_vjp(
53845392
v_heads=v_heads,
53855393
g_heads=g_heads,
53865394
)
5395+
# MLX can otherwise drop the first TileLang owner-output from the
5396+
# custom-VJP return graph under pytest capture. A scalar readback keeps
5397+
# the dy buffer live at the autograd boundary without staging or
5398+
# copying the tensor.
5399+
try:
5400+
_ = grads[0][0, 0, 0, 0].item()
5401+
except IndexError:
5402+
pass
53875403
return _match_primal_gradient_dtypes(grads, primals)
53885404

53895405
return _apply

cppmega_mlx/training/compiled.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,18 @@
3737
F = TypeVar("F", bound=Callable[..., Any])
3838
PathCGradientProbe = Callable[[Mapping[str, Any]], None]
3939
PathCTrainingRuntime = Any
40-
PATH_C_TRAINING_VALUE_AND_GRAD_CONTRACT = "path_c_direct_fusion_value_and_grad_v1"
40+
PATH_C_DIRECT_FUSION_VALUE_AND_GRAD_CONTRACT = (
41+
"path_c_direct_fusion_value_and_grad_v1"
42+
)
43+
PATH_C_TRAINING_VALUE_AND_GRAD_CONTRACT = (
44+
PATH_C_DIRECT_FUSION_VALUE_AND_GRAD_CONTRACT
45+
)
46+
PATH_C_FUSED_TRAIN_BLOCK_TRAINING_RUNTIME_CONTRACT = (
47+
"path_c_fused_train_block_training_runtime_v1"
48+
)
49+
PATH_C_FUSED_TRAIN_BLOCK_VALUE_AND_GRAD_CONTRACT = (
50+
"path_c_fused_train_block_value_and_grad_v1"
51+
)
4152

4253
REGIONAL_COMPILE_TARGETS: Mapping[CompileTarget, bool] = {
4354
"mamba3_pre": True,
@@ -337,12 +348,16 @@ def attach_path_c_training_runtime(self, runtime: PathCTrainingRuntime) -> None:
337348
bind = getattr(runtime, "bind_training_graph", None)
338349
bound = False
339350
if callable(bind):
340-
bind(
341-
owner="CompiledPretrainingStep",
342-
uses_direct_chain_runtime=True,
343-
uses_forward_hook=True,
344-
uses_backward_or_vjp_hook=True,
345-
)
351+
binding = {
352+
"owner": "CompiledPretrainingStep",
353+
"uses_forward_hook": True,
354+
"uses_backward_or_vjp_hook": True,
355+
}
356+
if _path_c_training_runtime_uses_fused_train_block(runtime):
357+
binding["uses_fused_train_block_runtime"] = True
358+
else:
359+
binding["uses_direct_chain_runtime"] = True
360+
bind(**binding)
346361
bound = True
347362
try:
348363
value_and_grad_contract = _path_c_training_runtime_value_and_grad_contract(
@@ -575,7 +590,16 @@ def _path_c_training_runtime_value_and_grad_contract(
575590
payload = dict(raw_contract)
576591
contract = str(payload.get("contract", ""))
577592
owner = str(payload.get("owner", ""))
578-
uses_runtime = bool(payload.get("uses_direct_chain_runtime"))
593+
uses_direct_chain_runtime = bool(payload.get("uses_direct_chain_runtime"))
594+
uses_fused_train_block_runtime = bool(
595+
payload.get("uses_fused_train_block_runtime")
596+
)
597+
direct_contract = contract == PATH_C_DIRECT_FUSION_VALUE_AND_GRAD_CONTRACT
598+
fused_contract = contract == PATH_C_FUSED_TRAIN_BLOCK_VALUE_AND_GRAD_CONTRACT
599+
uses_runtime = bool(
600+
(direct_contract and uses_direct_chain_runtime)
601+
or (fused_contract and uses_fused_train_block_runtime)
602+
)
579603
uses_forward = bool(payload.get("uses_forward_hook"))
580604
uses_reverse = bool(payload.get("uses_backward_or_vjp_hook"))
581605
returns_model_grads = bool(payload.get("returns_model_grads"))
@@ -586,7 +610,7 @@ def _path_c_training_runtime_value_and_grad_contract(
586610
hidden_packing = bool(payload.get("hidden_packing_performed", False))
587611
status = (
588612
"ok"
589-
if contract == PATH_C_TRAINING_VALUE_AND_GRAD_CONTRACT
613+
if (direct_contract or fused_contract)
590614
and owner == "CompiledPretrainingStep"
591615
and uses_runtime
592616
and uses_forward
@@ -604,7 +628,8 @@ def _path_c_training_runtime_value_and_grad_contract(
604628
"status": status,
605629
"contract": contract or PATH_C_TRAINING_VALUE_AND_GRAD_CONTRACT,
606630
"owner": owner or None,
607-
"uses_direct_chain_runtime": uses_runtime,
631+
"uses_direct_chain_runtime": uses_direct_chain_runtime,
632+
"uses_fused_train_block_runtime": uses_fused_train_block_runtime,
608633
"uses_forward_hook": uses_forward,
609634
"uses_backward_or_vjp_hook": uses_reverse,
610635
"returns_model_grads": returns_model_grads,
@@ -616,9 +641,22 @@ def _path_c_training_runtime_value_and_grad_contract(
616641
}
617642

618643

644+
def _path_c_training_runtime_uses_fused_train_block(
645+
runtime: PathCTrainingRuntime,
646+
) -> bool:
647+
return bool(
648+
getattr(runtime, "uses_fused_train_block_runtime", False)
649+
or str(getattr(runtime, "contract", ""))
650+
== PATH_C_FUSED_TRAIN_BLOCK_TRAINING_RUNTIME_CONTRACT
651+
)
652+
653+
619654
__all__ = [
620655
"CompileTarget",
621656
"CompiledPretrainingStep",
657+
"PATH_C_DIRECT_FUSION_VALUE_AND_GRAD_CONTRACT",
658+
"PATH_C_FUSED_TRAIN_BLOCK_TRAINING_RUNTIME_CONTRACT",
659+
"PATH_C_FUSED_TRAIN_BLOCK_VALUE_AND_GRAD_CONTRACT",
622660
"PATH_C_TRAINING_VALUE_AND_GRAD_CONTRACT",
623661
"PathCGradientBufferCapture",
624662
"PathCGradientProbe",

cppmega_v4/jsonrpc/data_methods.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,27 @@ class PreviewRow(BaseModel):
4444
channels: dict[str, Any] = Field(default_factory=dict)
4545

4646

47+
class SideChannelFamilyPreview(BaseModel):
48+
model_config = ConfigDict(extra="forbid")
49+
family: str
50+
status: str
51+
columns: list[str] = Field(default_factory=list)
52+
missing_columns: list[str] = Field(default_factory=list)
53+
dropped_columns: list[str] = Field(default_factory=list)
54+
token_alignment: str
55+
graph_remapping: str
56+
provenance: str
57+
non_null_ratio: float
58+
59+
4760
class PreviewParquetResult(BaseModel):
4861
model_config = ConfigDict(extra="forbid")
4962
rows: list[PreviewRow]
5063
token_column: str
5164
available_channels: list[str]
65+
side_channel_families: dict[str, SideChannelFamilyPreview] = Field(
66+
default_factory=dict
67+
)
5268
bytes_per_token_avg: float
5369
bytes_per_token_p95: float
5470
bytes_per_token_max: int
@@ -132,6 +148,20 @@ def preview_parquet(
132148
rows=rows,
133149
token_column=token_col,
134150
available_channels=available,
151+
side_channel_families={
152+
name: SideChannelFamilyPreview(
153+
family=coverage.family,
154+
status=coverage.status,
155+
columns=list(coverage.columns),
156+
missing_columns=list(coverage.missing_columns),
157+
dropped_columns=list(coverage.dropped_columns),
158+
token_alignment=coverage.token_alignment,
159+
graph_remapping=coverage.graph_remapping,
160+
provenance=coverage.provenance,
161+
non_null_ratio=coverage.non_null_ratio,
162+
)
163+
for name, coverage in sorted(caps.side_channel_families.items())
164+
},
135165
bytes_per_token_avg=round(bpt_avg, 3),
136166
bytes_per_token_p95=round(bpt_p95, 3),
137167
bytes_per_token_max=int(bpt_max),

cppmega_v4/jsonrpc/methods.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,34 @@ def _cache_store(cache: LRUCache | None, key: str | None, value: Any):
254254
cache.set(key, value)
255255

256256

257+
def _side_channel_policy_gotchas(
258+
side_channels: Any,
259+
available: frozenset[str],
260+
) -> list[GotchaPayload]:
261+
"""Report GUI-visible contract errors for required side-channel families."""
262+
out: list[GotchaPayload] = []
263+
global_requires = side_channels.mode == "require"
264+
for family, policy in sorted(side_channels.families.items()):
265+
if policy.mode == "off":
266+
continue
267+
required = policy.mode == "require" or global_requires
268+
if not required:
269+
continue
270+
missing = [col for col in policy.columns if col not in available]
271+
if not missing:
272+
continue
273+
out.append(GotchaPayload(
274+
id=f"side_channel_required_{family}",
275+
severity="error",
276+
message=(
277+
f"required side-channel family {family!r} is missing "
278+
f"{', '.join(missing)}"
279+
),
280+
reference="docs/side_channel_conditioning_plan.md#configuration-contract",
281+
))
282+
return out
283+
284+
257285
# ---------------------------------------------------------------------------
258286
# verify
259287
# ---------------------------------------------------------------------------
@@ -339,6 +367,9 @@ def verify(params: VerifyParams, *, cache: LRUCache | None = None) -> VerifyResu
339367
)
340368
for g in dverify.gotchas
341369
]
370+
gotcha_payloads.extend(
371+
_side_channel_policy_gotchas(params.side_channels, available)
372+
)
342373

343374
edge_payloads: list[EdgeResolution] = []
344375
for re in resolved.edges:

cppmega_v4/runner/stages.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,16 @@ def loss_fn(model: nn.Module, emb: mx.array, tgt: mx.array) -> mx.array:
424424
data_source = "synthetic"
425425
token_count = 0
426426
tokenizer_used: str | None = None
427+
# V4-10: record which side-channels the caller supplied. Pure
428+
# observation for now — actual per-channel forward routing is
429+
# v5+ work. Surfacing via extras lets e2e prove UI toggles
430+
# reached the backend opts surface.
431+
side_channels_in = opts.get("side_channels") or {}
432+
side_channels_observed: list[str] = []
433+
if isinstance(side_channels_in, dict):
434+
for name, data in side_channels_in.items():
435+
if isinstance(data, (list, tuple)) and len(data) > 0:
436+
side_channels_observed.append(str(name))
427437
parquet_path = opts.get("parquet_path")
428438
tokenizer_path = opts.get("tokenizer_path")
429439
targets = mx.random.randint(0, vocab_size, shape=(batch, seq))
@@ -608,6 +618,7 @@ def _count(tree: Any) -> int:
608618
"l2_diff": round(l2_diff, 6),
609619
"cos_sim": round(cos_sim, 6),
610620
},
621+
"side_channels_observed": side_channels_observed,
611622
"model_summary": _summarize_model(
612623
ctx.spec, optimizer_kind, schedule_kind_label),
613624
},

0 commit comments

Comments
 (0)