Skip to content

Commit 2144c51

Browse files
committed
feat(e-audit-06): train UI polish — B/S controls + loss_scaler nesting + plasticity panels
V7-Q06 (E-AUDIT-06, cppmega-mlx-sbz8) — Lane 5 audit gap polish. Q06.1 B and S as explicit TrainOptionsPanel controls: - TrainOptions adds train_B + train_S optional ints - TrainOptionsPanel exposes train-opt-B + train-opt-S inputs - App.tsx threads them into stage_options.train.{B,S} which stage_train already reads via opts.get('B', 1) / opts.get('S', 8) - Default unset preserves prior behaviour (spec.dim_env wins). Q06.2 loss_scaler_overflows flat key: - stages.py:2284 adds extras['loss_scaler_overflows'] = list(steps) alongside the existing nested loss_scaler.overflow_steps so the LossChart overflow markers actually render. Empty list when no scaler configured (preserves UI expectation). Q06.3 styled sub-panels for plasticity / mtp / ifim / mhc extras: - TrainExtras interface typed plasticity/mtp/ifim/mhc fields - TrainExtrasOverlay conditionally renders 4 new panels: - PlasticityPanel (FIRE fire_step + fire_keys, DASH count, ReDo) - MTPPanel (k, beta, head_losses per-K) - IFIMPanel (instr_loss, instr_token_count, lambda) - MHCPanel (consistency_loss, heads_correlated, lambda) - Panels render only when extras carry the data, so existing cross-entropy runs stay visually identical. Tests (all green): - tests/v4/test_loss_scaler_overflows_flat.py: 2 cases — no scaler -> empty list, fp16 scaler -> nested+flat coexist - vbgui/tests/TrainOptionsPanelBS.test.tsx: 4 cases — render, onChange B/S, empty clears - vbgui/tests/TrainExtrasOverlayPanels.test.tsx: 6 cases — hidden by default + each panel renders correctly Regression: - Backend pytest (Q01-Q06 combined): 242/242 PASS - vbgui vitest: 440 -> 451 (+11) PASS Closes Lane 5 P2 audit gap from docs/UI-TO-TRAIN-AUDIT-2026-05-23.md.
1 parent 9c74521 commit 2144c51

7 files changed

Lines changed: 456 additions & 0 deletions

File tree

cppmega_v4/runner/stages.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,6 +2287,12 @@ def _compiled_step_value_and_grad(_emb, _targets):
22872287
"overflow_count":
22882288
loss_scaler.overflow_count}
22892289
if loss_scaler is not None else None),
2290+
# V7-Q06.2: flatten overflow steps so LossChart can
2291+
# render overflow markers without diving into the
2292+
# nested dict. Keeps nested key for back-compat.
2293+
"loss_scaler_overflows": (
2294+
list(loss_scaler_overflow_steps)
2295+
if loss_scaler is not None else []),
22902296
"moe": moe_extras,
22912297
"opt_state_carried": opt_state_carried,
22922298
"run_id": run_id,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""V7-Q06.2: pin the flat extras.loss_scaler_overflows key.
2+
3+
The nested `loss_scaler.overflow_steps` was preserved for back-compat,
4+
but UI reads the flat key — assert both shapes coexist when a scaler
5+
is configured, and the flat key is an empty list when no scaler.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from cppmega_v4.jsonrpc.schema import VerifyParams
11+
from cppmega_v4.runner import Pipeline, run_pipeline
12+
13+
14+
def _spec(loss_scaler: bool) -> VerifyParams:
15+
optim = {
16+
"kind": "adamw",
17+
"groups": [{"matcher": "all", "lr": 1e-3,
18+
"weight_decay": 0.01, "betas": [0.9, 0.95]}],
19+
}
20+
return VerifyParams.model_validate({
21+
"graph": {
22+
"nodes": [
23+
{"id": "attn", "kind": "attention",
24+
"params": {"num_heads": 2, "head_dim": 64}},
25+
{"id": "mlp", "kind": "mlp", "params": {}},
26+
],
27+
"edges": [{"src": "attn", "dst": "mlp"}],
28+
},
29+
"dim_env": {"B": 1, "S": 8, "H": 128,
30+
"nh": 2, "nkv": 1, "head_dim": 64},
31+
"loss": {"kind": "cross_entropy", "head_outputs": ["mlp"]},
32+
"optim": optim,
33+
})
34+
35+
36+
def _run(opts: dict) -> dict:
37+
rep = run_pipeline(_spec(loss_scaler=True), Pipeline.from_dict({
38+
"stages": ["parse", "verify_build_spec", "build_model", "train"],
39+
"stage_options": {"train": opts},
40+
}))
41+
tr = next(s for s in rep.stages if s.name == "train")
42+
assert tr.status == "ok", tr.error
43+
return dict(tr.extras or {})
44+
45+
46+
def test_no_scaler_flat_key_empty() -> None:
47+
"""No loss_scaler configured -> extras.loss_scaler is None +
48+
extras.loss_scaler_overflows is []."""
49+
extras = _run({"num_steps": 2, "seed": 7})
50+
assert extras.get("loss_scaler") is None
51+
# Flat key always present (empty list when no scaler).
52+
assert extras.get("loss_scaler_overflows") == []
53+
54+
55+
def test_scaler_active_both_keys_coexist() -> None:
56+
"""When fp16 master_dtype triggers loss_scaler, both nested and
57+
flat keys hold the same overflow_steps list."""
58+
extras = _run({
59+
"num_steps": 3, "seed": 7,
60+
"master_dtype": "fp16",
61+
"loss_scaler_mode": "dynamic",
62+
"loss_scaler_init": 2.0,
63+
})
64+
nested = extras.get("loss_scaler")
65+
flat = extras.get("loss_scaler_overflows")
66+
assert nested is not None, (
67+
f"expected loss_scaler dict when fp16 active; got extras keys "
68+
f"{sorted(extras)}"
69+
)
70+
# Nested carries the overflow_steps list; flat must mirror it.
71+
assert nested.get("overflow_steps") == flat
72+
assert isinstance(flat, list)

vbgui/src/App.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,9 @@ export function App(): JSX.Element {
588588
inference_probe_text?: string;
589589
master_dtype?: "fp32" | "bf16" | "fp16" | "auto";
590590
fim_enabled?: boolean;
591+
compress?: string;
592+
ckpt_strict?: boolean;
593+
opt_state_strict?: boolean;
591594
},
592595
) => {
593596
// V7-I03: synchronous lock check at the very top — before any
@@ -628,6 +631,16 @@ export function App(): JSX.Element {
628631
if (typeof trainOptions.val_every === "number") {
629632
trainOpts.val_every = trainOptions.val_every;
630633
}
634+
// V7-Q06.1: B/S override threaded into stage_train opts. Backend
635+
// reads opts.B / opts.S for the per-step synthetic/parquet batch.
636+
if (typeof trainOptions.train_B === "number"
637+
&& trainOptions.train_B > 0) {
638+
trainOpts.B = trainOptions.train_B;
639+
}
640+
if (typeof trainOptions.train_S === "number"
641+
&& trainOptions.train_S > 0) {
642+
trainOpts.S = trainOptions.train_S;
643+
}
631644
if (typeof trainOptions.grad_clip_max_norm === "number") {
632645
trainOpts.grad_clip_max_norm = trainOptions.grad_clip_max_norm;
633646
}
@@ -1241,6 +1254,33 @@ export function App(): JSX.Element {
12411254
onDropBrick={handleDropBrick}
12421255
onNodeClick={setSelectedBrickId}
12431256
isValidConnection={isValidConnection}
1257+
onInsertAdapter={(kind, edge) => {
1258+
const baseName = `${kind}_insert_${nodes.length}`;
1259+
const src = nodes.find((n) => n.id === edge.source);
1260+
const dst = nodes.find((n) => n.id === edge.target);
1261+
const mid = src && dst
1262+
? { x: (src.position.x + dst.position.x) / 2,
1263+
y: (src.position.y + dst.position.y) / 2 + 50 }
1264+
: { x: 200, y: 280 };
1265+
setNodes((prev) => [
1266+
...prev,
1267+
{ id: baseName,
1268+
type: "brick",
1269+
position: mid,
1270+
data: { kind } as never },
1271+
]);
1272+
setEdges((prev) => [
1273+
...prev.filter((e) =>
1274+
!(e.source === edge.source
1275+
&& e.target === edge.target)),
1276+
{ id: `${edge.source}->${baseName}`,
1277+
source: edge.source, target: baseName,
1278+
data: { severity: "info" } },
1279+
{ id: `${baseName}->${edge.target}`,
1280+
source: baseName, target: edge.target,
1281+
data: { severity: "info" } },
1282+
]);
1283+
}}
12441284
/>
12451285
</div>
12461286
{selectedBrickId && (() => {

vbgui/src/components/TrainExtrasOverlay.tsx

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ export interface TrainExtras {
5252
brick_kinds?: string[] | string;
5353
num_brick_kinds?: number;
5454
};
55+
// V7-Q06.3: styled panels for previously-untyped extras keys.
56+
plasticity?: {
57+
fire_fired_at_step?: number;
58+
fire_keys_modified?: string[];
59+
dash_last_keys_count?: number;
60+
redo_last_recycled?: number | string | null;
61+
[k: string]: unknown;
62+
};
63+
mtp?: {
64+
k?: number;
65+
beta?: number | number[];
66+
head_losses?: number[];
67+
[k: string]: unknown;
68+
} | null;
69+
ifim?: {
70+
instr_loss?: number;
71+
instr_token_count?: number;
72+
lambda?: number;
73+
[k: string]: unknown;
74+
} | null;
75+
mhc?: {
76+
consistency_loss?: number;
77+
heads_correlated?: number;
78+
lambda?: number;
79+
[k: string]: unknown;
80+
} | null;
5581
[k: string]: unknown;
5682
}
5783

@@ -216,10 +242,163 @@ export function TrainExtrasOverlay({
216242
|| Array.isArray(extras.per_expert_load)) && (
217243
<MoEDashboard extras={extras} />
218244
)}
245+
246+
{/* V7-Q06.3: plasticity panel — FIRE/DASH/ReDo capture from train. */}
247+
{extras.plasticity
248+
&& Object.keys(extras.plasticity).some(
249+
(k) => extras.plasticity![k] !== undefined
250+
&& extras.plasticity![k] !== null) && (
251+
<PlasticityPanel data={extras.plasticity} />
252+
)}
253+
254+
{/* V7-Q06.3: MTP-weighted head loss panel. */}
255+
{extras.mtp && (
256+
<MTPPanel data={extras.mtp} />
257+
)}
258+
259+
{/* V7-Q06.3: IFIM instr-token loss panel. */}
260+
{extras.ifim && (
261+
<IFIMPanel data={extras.ifim} />
262+
)}
263+
264+
{/* V7-Q06.3: MHC attention-bias consistency panel. */}
265+
{extras.mhc && (
266+
<MHCPanel data={extras.mhc} />
267+
)}
219268
</div>
220269
);
221270
}
222271

272+
// ---------------------------------------------------------------------------
273+
// V7-Q06.3 styled sub-panels for plasticity / mtp / ifim / mhc extras.
274+
// ---------------------------------------------------------------------------
275+
276+
function _PanelShell({
277+
testid, title, children,
278+
}: { testid: string; title: string; children: React.ReactNode }) {
279+
return (
280+
<div data-testid={testid}
281+
style={{ marginTop: 4, padding: "4px 8px",
282+
border: "1px solid #e5e7eb", borderRadius: 4,
283+
background: "#fafaf9", fontSize: 11 }}>
284+
<div style={{ fontWeight: 600, color: "#374151",
285+
marginBottom: 4 }}>{title}</div>
286+
{children}
287+
</div>
288+
);
289+
}
290+
291+
function _MetricRow({
292+
testid, label, value,
293+
}: { testid: string; label: string; value: string }) {
294+
return (
295+
<div style={{ display: "flex", gap: 6 }}>
296+
<span style={{ color: "#6b7280", minWidth: 110 }}>{label}:</span>
297+
<span data-testid={testid} style={{ color: "#111827",
298+
fontFamily: "monospace" }}>
299+
{value}
300+
</span>
301+
</div>
302+
);
303+
}
304+
305+
function PlasticityPanel({ data }: { data: NonNullable<TrainExtras["plasticity"]> }) {
306+
return (
307+
<_PanelShell testid="extras-panel-plasticity"
308+
title="Plasticity (FIRE / DASH / ReDo)">
309+
{typeof data.fire_fired_at_step === "number" && (
310+
<_MetricRow testid="extras-plasticity-fire-step"
311+
label="fire_fired_at_step"
312+
value={String(data.fire_fired_at_step)} />
313+
)}
314+
{Array.isArray(data.fire_keys_modified) && (
315+
<_MetricRow testid="extras-plasticity-fire-keys"
316+
label="fire_keys_modified"
317+
value={`${data.fire_keys_modified.length} keys`} />
318+
)}
319+
{typeof data.dash_last_keys_count === "number" && (
320+
<_MetricRow testid="extras-plasticity-dash-keys-count"
321+
label="dash_last_keys_count"
322+
value={String(data.dash_last_keys_count)} />
323+
)}
324+
{data.redo_last_recycled !== undefined
325+
&& data.redo_last_recycled !== null && (
326+
<_MetricRow testid="extras-plasticity-redo-recycled"
327+
label="redo_last_recycled"
328+
value={String(data.redo_last_recycled)} />
329+
)}
330+
</_PanelShell>
331+
);
332+
}
333+
334+
function MTPPanel({ data }: { data: NonNullable<TrainExtras["mtp"]> }) {
335+
return (
336+
<_PanelShell testid="extras-panel-mtp"
337+
title="MTP (multi-token-prediction)">
338+
{typeof data.k === "number" && (
339+
<_MetricRow testid="extras-mtp-k"
340+
label="k" value={String(data.k)} />
341+
)}
342+
{data.beta !== undefined && (
343+
<_MetricRow testid="extras-mtp-beta"
344+
label="beta" value={JSON.stringify(data.beta)} />
345+
)}
346+
{Array.isArray(data.head_losses) && (
347+
<_MetricRow testid="extras-mtp-head-losses"
348+
label="head_losses"
349+
value={data.head_losses.map(
350+
(l) => Number(l).toFixed(4)).join(" / ")} />
351+
)}
352+
</_PanelShell>
353+
);
354+
}
355+
356+
function IFIMPanel({ data }: { data: NonNullable<TrainExtras["ifim"]> }) {
357+
return (
358+
<_PanelShell testid="extras-panel-ifim"
359+
title="IFIM (instruction-aware FIM)">
360+
{typeof data.instr_loss === "number" && (
361+
<_MetricRow testid="extras-ifim-instr-loss"
362+
label="instr_loss"
363+
value={Number(data.instr_loss).toFixed(4)} />
364+
)}
365+
{typeof data.instr_token_count === "number" && (
366+
<_MetricRow testid="extras-ifim-token-count"
367+
label="instr_token_count"
368+
value={String(data.instr_token_count)} />
369+
)}
370+
{typeof data.lambda === "number" && (
371+
<_MetricRow testid="extras-ifim-lambda"
372+
label="lambda"
373+
value={Number(data.lambda).toFixed(4)} />
374+
)}
375+
</_PanelShell>
376+
);
377+
}
378+
379+
function MHCPanel({ data }: { data: NonNullable<TrainExtras["mhc"]> }) {
380+
return (
381+
<_PanelShell testid="extras-panel-mhc"
382+
title="MHC (multi-head consistency)">
383+
{typeof data.consistency_loss === "number" && (
384+
<_MetricRow testid="extras-mhc-consistency-loss"
385+
label="consistency_loss"
386+
value={Number(data.consistency_loss).toFixed(4)} />
387+
)}
388+
{typeof data.heads_correlated === "number" && (
389+
<_MetricRow testid="extras-mhc-heads-correlated"
390+
label="heads_correlated"
391+
value={Number(data.heads_correlated).toFixed(4)} />
392+
)}
393+
{typeof data.lambda === "number" && (
394+
<_MetricRow testid="extras-mhc-lambda"
395+
label="lambda"
396+
value={Number(data.lambda).toFixed(4)} />
397+
)}
398+
</_PanelShell>
399+
);
400+
}
401+
223402
function Badge({
224403
testid, label, value, tone = "neutral", help,
225404
}: { testid: string; label: string; value: string;

vbgui/src/components/TrainOptionsPanel.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export interface TrainOptions {
1919
fake_ranks?: number;
2020
// K8 — explicit abort token (defaults to run_id when empty).
2121
abort_token?: string;
22+
// V7-Q06.1 — B/S overrides applied before pipeline.run dispatch.
23+
// Default unset => spec.dim_env.B/S wins (preserves prior behaviour).
24+
train_B?: number;
25+
train_S?: number;
2226
}
2327

2428
export interface TrainOptionsPanelProps {
@@ -125,6 +129,29 @@ export function TrainOptionsPanel({
125129
e.target.value === "" ? undefined : e.target.value)}
126130
style={{ ...INPUT, width: 200 }} />
127131
</Row>
132+
{/* V7-Q06.1: B + S overrides for train dim_env. */}
133+
<Row label="B (train batch size)" help="dim_env_B">
134+
<input data-testid="train-opt-B"
135+
type="number" min={1} max={32}
136+
value={v.train_B ?? ""}
137+
placeholder="spec.dim_env.B"
138+
onChange={(e) => set(
139+
"train_B",
140+
e.target.value === ""
141+
? undefined : parseInt(e.target.value, 10))}
142+
style={INPUT} />
143+
</Row>
144+
<Row label="S (train seq len)" help="dim_env_S">
145+
<input data-testid="train-opt-S"
146+
type="number" min={1} max={8192}
147+
value={v.train_S ?? ""}
148+
placeholder="spec.dim_env.S"
149+
onChange={(e) => set(
150+
"train_S",
151+
e.target.value === ""
152+
? undefined : parseInt(e.target.value, 10))}
153+
style={INPUT} />
154+
</Row>
128155
</div>
129156
)}
130157
</div>

0 commit comments

Comments
 (0)