Skip to content

Commit f443aaf

Browse files
committed
feat(v3-1): stage_train honors OptimKind + extras.optimizer_kind + model_summary
Closes V3-1 / cppmega-mlx-wly. Fixes 3 confirmed UI→API→training bugs: B1: stage_train hardcoded optim.AdamW. Lion/Lion8bit/Adam8bit/Muon/ Muon_AdamW_Hybrid/SGD selected in UI OptimTab were silently ignored at train. Now _build_optimizer(spec_optim, lr) dispatches every OptimKind through cppmega_mlx.training.optimizers factories. B1-bis: build_model raised "unsupported optimizer kind" for Lion/Lion8bit/ Adam8bit. Pipeline orchestrator swallowed the BuildError and marked train "skipped" — UI showed superficial green. Now build_model wires these three through the same factories. B1-tris: _make_optim dropped ns_steps and schedule when converting wire payload to OptimSpec. Muon-from-UI failed validation; scheduled groups silently lost their lr curve. Now both fields thread through with kind-aware defaults. V3-3 (model_summary helper) lands alongside: _summarize_model surfaces mlp_activation, attention pre/post_norm, mlp pre/post_norm, optimizer_kind, schedule_kind, num_brick_kinds — enables UI-side assertions that a brick context change reached training. Also fixed: stage_train's forward chain silently zeroed gradients for every q/k/v after attention because attention's zero-init-out (o_proj= zeros for identity-at-init) killed downstream gradient flow. Real transformer blocks have residuals; now stage_train adds residual when shapes match so gradients propagate as in production. 19 new pytest in tests/v4/test_stage_train_optimizers.py. Regression 92/92 across pipeline + schedules + jsonrpc_methods + new file.
1 parent d71c482 commit f443aaf

4 files changed

Lines changed: 410 additions & 48 deletions

File tree

cppmega_v4/buildspec/api.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from __future__ import annotations
2121

2222
import time
23-
from collections.abc import Callable, Mapping, Sequence
24-
from dataclasses import dataclass, field
23+
from collections.abc import Callable, Mapping
24+
from dataclasses import dataclass
2525
from typing import Any
2626

2727
import mlx.core as mx
@@ -35,7 +35,7 @@
3535
from cppmega_v4.buildspec.loss_spec import LossKind, LossSpec
3636
from cppmega_v4.buildspec.model_build_spec import ModelBuildSpec
3737
from cppmega_v4.buildspec.optim_spec import OptimKind, OptimSpec, ParamGroup
38-
from cppmega_v4.fusion.brick_graph import BrickGraph, BrickNode
38+
from cppmega_v4.fusion.brick_graph import BrickGraph
3939
from cppmega_v4.models.unified_superblock_v4 import BLOCK_BUILDERS
4040

4141

@@ -48,6 +48,14 @@
4848
# adapter-prefixed bricks are pure-shape rewires; their forward is
4949
# a passthrough (or handled separately via a future Stage F)
5050
})
51+
_AUTOGRAD_SAFE_KERNEL_KINDS: frozenset[str] = frozenset({"gdn", "kda"})
52+
53+
54+
def _autograd_safe_params(kind: str, params: Mapping[str, Any]) -> dict[str, Any]:
55+
out = dict(params)
56+
if kind in _AUTOGRAD_SAFE_KERNEL_KINDS and "kernel_path" not in out:
57+
out["kernel_path"] = "path_a"
58+
return out
5159

5260

5361
class BuiltSequentialModel(nn.Module):
@@ -67,9 +75,12 @@ def __init__(self, graph: BrickGraph, hidden_size: int):
6775
# them up as sub-modules (for parameter registration).
6876
self._node_attrs: dict[str, str] = {}
6977
for node in graph.nodes:
78+
params = _autograd_safe_params(node.kind, node.params)
7079
module = node.module
80+
if node.kind in _AUTOGRAD_SAFE_KERNEL_KINDS and "kernel_path" not in node.params:
81+
module = None
7182
if module is None and node.kind in BLOCK_BUILDERS:
72-
module = BLOCK_BUILDERS[node.kind](hidden_size, dict(node.params))
83+
module = BLOCK_BUILDERS[node.kind](hidden_size, params)
7384
if module is None:
7485
continue
7586
attr = f"brick_{_safe_name(node.name)}"
@@ -235,8 +246,33 @@ def _build_optimizer(spec: OptimSpec) -> object:
235246
nn.Module params is performed by callers (Stage F has the full
236247
matcher loop)."""
237248
if spec.kind is OptimKind.ADAMW:
249+
from cppmega_mlx.training.optimizers import make_adamw
250+
g = spec.groups[0]
251+
return make_adamw(
252+
learning_rate=g.lr,
253+
betas=list(g.betas) if g.betas is not None else [0.9, 0.999],
254+
weight_decay=g.weight_decay,
255+
)
256+
if spec.kind is OptimKind.LION:
257+
from cppmega_mlx.training.optimizers import make_lion
258+
g = spec.groups[0]
259+
return make_lion(
260+
learning_rate=g.lr,
261+
betas=list(g.betas) if g.betas is not None else [0.9, 0.99],
262+
weight_decay=g.weight_decay,
263+
)
264+
if spec.kind is OptimKind.LION_8BIT:
265+
from cppmega_mlx.training.optimizers_quantized import make_lion8bit
266+
g = spec.groups[0]
267+
return make_lion8bit(
268+
learning_rate=g.lr,
269+
betas=list(g.betas) if g.betas is not None else [0.9, 0.99],
270+
weight_decay=g.weight_decay,
271+
)
272+
if spec.kind is OptimKind.ADAM_8BIT:
273+
from cppmega_mlx.training.optimizers_quantized import make_adam8bit
238274
g = spec.groups[0]
239-
return optim.AdamW(
275+
return make_adam8bit(
240276
learning_rate=g.lr,
241277
betas=list(g.betas) if g.betas is not None else [0.9, 0.999],
242278
weight_decay=g.weight_decay,

cppmega_v4/jsonrpc/methods.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
OptimSpec,
2828
ParamGroup,
2929
)
30+
from cppmega_v4.buildspec.schedules import ScheduleSpec
3031
from cppmega_v4.fusion import from_block_specs
3132
from cppmega_v4.parallelism import (
3233
AxisAssignment,
@@ -43,18 +44,6 @@
4344
tpu_v6e_8,
4445
verify_distributed_plan,
4546
)
46-
47-
48-
_TOPOLOGY_FACTORIES = {
49-
"h100_8x": h100_8x,
50-
"h200_8x": h200_8x,
51-
"a100_8x": a100_8x,
52-
"b100_8x": b100_8x,
53-
"gb10_quarter": gb10_quarter,
54-
"tpu_v5p_4": tpu_v5p_4,
55-
"tpu_v6e_8": tpu_v6e_8,
56-
"m3_ultra_solo": m3_ultra_solo,
57-
}
5847
from cppmega_v4.probe import contract_probe as _contract_probe
5948
from cppmega_v4.probe import to_dict as _probe_to_dict
6049
from cppmega_v4.spec import (
@@ -94,6 +83,18 @@
9483
)
9584

9685

86+
_TOPOLOGY_FACTORIES = {
87+
"h100_8x": h100_8x,
88+
"h200_8x": h200_8x,
89+
"a100_8x": a100_8x,
90+
"b100_8x": b100_8x,
91+
"gb10_quarter": gb10_quarter,
92+
"tpu_v5p_4": tpu_v5p_4,
93+
"tpu_v6e_8": tpu_v6e_8,
94+
"m3_ultra_solo": m3_ultra_solo,
95+
}
96+
97+
9798
# ---------------------------------------------------------------------------
9899
# Coercion helpers: wire payload → backend dataclasses.
99100
# ---------------------------------------------------------------------------
@@ -123,16 +124,46 @@ def _make_loss(payload: LossSpecPayload) -> LossSpec:
123124

124125

125126
def _make_optim(payload: OptimSpecPayload) -> OptimSpec:
127+
kind = OptimKind(payload.kind)
128+
129+
def _default_betas() -> tuple[float, float] | None:
130+
if kind in (OptimKind.LION, OptimKind.LION_8BIT):
131+
return (0.9, 0.99)
132+
if kind is OptimKind.ADAM_8BIT:
133+
return (0.9, 0.999)
134+
if kind in (OptimKind.ADAMW, OptimKind.MUON_ADAMW_HYBRID):
135+
return (0.9, 0.95)
136+
return None
137+
138+
def _make_schedule(g) -> ScheduleSpec | None:
139+
schedule = g.schedule
140+
if schedule is None:
141+
return None
142+
return ScheduleSpec(
143+
kind=schedule.kind,
144+
warmup_steps=schedule.warmup_steps,
145+
total_steps=schedule.total_steps,
146+
min_lr_ratio=schedule.min_lr_ratio,
147+
decay_steps=schedule.decay_steps,
148+
power=schedule.power,
149+
)
150+
126151
groups = tuple(
127152
ParamGroup(
128153
matcher=g.matcher,
129154
lr=g.lr,
130155
weight_decay=g.weight_decay,
131-
betas=g.betas if g.betas is not None else (0.9, 0.95),
156+
betas=g.betas if g.betas is not None else _default_betas(),
157+
ns_steps=(
158+
g.ns_steps
159+
if g.ns_steps is not None
160+
else (5 if kind is OptimKind.MUON else None)
161+
),
162+
schedule=_make_schedule(g),
132163
)
133164
for g in payload.groups
134165
)
135-
return OptimSpec(kind=OptimKind(payload.kind), groups=groups)
166+
return OptimSpec(kind=kind, groups=groups)
136167

137168

138169
def _make_topology(payload: TopologyPayload):

0 commit comments

Comments
 (0)