Skip to content

Commit eed4032

Browse files
committed
feat(v7-h33): loss_surface.run dispatcher route + LossSurfaceModal UI
Closes the orphan-backend half of item 15 (loss_surface): the loss_surface_method.py file existed but no dispatcher route, so the UI got -32601 method_not_found. UI also had no consumer. - Register loss_surface.run in dispatcher._ROUTES. - New LossSurfaceModal: N×M heatmap with viridis-style coloring, best cell outlined yellow, Apply-best button commits lr/wd multipliers back into spec.optim.groups via dispatch. - App.tsx wires a floating 'Loss surface…' launcher button. bd: cppmega-mlx-sahc 2/2 pytest (dispatcher route + grid matrix) + 4/4 vitest.
1 parent 9283299 commit eed4032

5 files changed

Lines changed: 402 additions & 0 deletions

File tree

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
SideChannelApplyParams,
6363
apply_side_channels,
6464
)
65+
from cppmega_v4.jsonrpc.loss_surface_method import (
66+
LossSurfaceParams,
67+
loss_surface_run,
68+
)
6569
from cppmega_v4.jsonrpc.schema import (
6670
BuildPresetSpecsParams,
6771
CatalogExplainParams,
@@ -182,6 +186,10 @@
182186
SideChannelApplyParams,
183187
lambda p, c: apply_side_channels(p, cache=c),
184188
),
189+
"loss_surface.run": (
190+
LossSurfaceParams,
191+
lambda p, c: loss_surface_run(p, cache=c),
192+
),
185193
}
186194

187195

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""V7-H33: loss_surface.run RPC dispatcher route registration."""
2+
3+
from __future__ import annotations
4+
5+
from cppmega_v4.jsonrpc.dispatcher import dispatch
6+
7+
8+
_SPEC = {
9+
"graph": {
10+
"nodes": [
11+
{"id": "attn", "kind": "attention",
12+
"params": {"num_heads": 4, "head_dim": 64}},
13+
{"id": "mlp", "kind": "mlp", "params": {}},
14+
],
15+
"edges": [{"src": "attn", "dst": "mlp"}],
16+
},
17+
"dim_env": {"B": 1, "S": 8, "H": 128,
18+
"nh": 2, "nkv": 1, "head_dim": 64},
19+
"loss": {"kind": "cross_entropy", "head_outputs": ["mlp"]},
20+
"optim": {"kind": "adamw",
21+
"groups": [{"matcher": "all", "lr": 1e-3,
22+
"weight_decay": 0.01,
23+
"betas": [0.9, 0.95]}]},
24+
}
25+
26+
27+
def test_v7_h33_loss_surface_run_method_is_routed():
28+
"""Before this fix loss_surface_method.py existed but the route
29+
was orphan — UI got -32601 method_not_found."""
30+
response = dispatch({
31+
"jsonrpc": "2.0", "id": "T1", "method": "loss_surface.run",
32+
"params": {
33+
"spec": _SPEC,
34+
"lr_deltas": [1.0],
35+
"wd_deltas": [1.0],
36+
"k_steps": 2,
37+
},
38+
})
39+
assert response.error is None, response.error
40+
assert response.result is not None
41+
assert "rows" in response.result
42+
assert len(response.result["rows"]) == 1
43+
assert response.result["lr_deltas"] == [1.0]
44+
assert response.result["wd_deltas"] == [1.0]
45+
assert response.result["best_lr_mult"] == 1.0
46+
assert response.result["best_wd_mult"] == 1.0
47+
assert response.result["best_loss"] is not None
48+
49+
50+
def test_v7_h33_loss_surface_run_grid_returns_full_matrix():
51+
response = dispatch({
52+
"jsonrpc": "2.0", "id": "T2", "method": "loss_surface.run",
53+
"params": {
54+
"spec": _SPEC,
55+
"lr_deltas": [0.5, 1.0],
56+
"wd_deltas": [0.5, 1.0, 2.0],
57+
"k_steps": 2,
58+
},
59+
})
60+
assert response.error is None
61+
rows = response.result["rows"]
62+
assert len(rows) == 2
63+
assert all(len(r) == 3 for r in rows)
64+
for row in rows:
65+
for cell in row:
66+
assert cell["status"] in {"ok"} or cell["status"].startswith("fail")

vbgui/src/App.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { TopBar, type RunMode } from "@/components/TopBar";
2121
import { BottomStrip } from "@/components/BottomStrip";
2222
import { AppTabs, type AppTab } from "@/components/AppTabs";
2323
import { RunResultModal, type RunReport } from "@/components/RunResultModal";
24+
import { LossSurfaceModal } from "@/components/LossSurfaceModal";
2425
import { TokenizerPlayground } from "@/components/TokenizerPlayground";
2526
import { DataInspector } from "@/components/DataInspector";
2627
import { BrickContextPanel } from "@/components/BrickContextPanel";
@@ -138,6 +139,8 @@ export function App(): JSX.Element {
138139
setRunErrorRaw({ code, message, data });
139140
}, []);
140141
const [trainInFlight, setTrainInFlight] = useState(false);
142+
// V7-H33: loss-surface explorer modal open flag.
143+
const [lossSurfaceOpen, setLossSurfaceOpen] = useState<boolean>(false);
141144
// V7-I03: synchronous lock. React's setTrainInFlight schedules an
142145
// async commit, so two button clicks within the same microtask
143146
// both read trainInFlight=false and both call rpc.call. The ref
@@ -1323,6 +1326,34 @@ export function App(): JSX.Element {
13231326
<RunResultModal report={runReport} error={runError}
13241327
onClose={() => { setRunReport(null);
13251328
setRunError(null); }} />
1329+
{/* V7-H33: loss-surface explorer launcher + modal. */}
1330+
<button data-testid="loss-surface-open"
1331+
onClick={() => setLossSurfaceOpen(true)}
1332+
style={{ position: "fixed", bottom: 56, right: 18,
1333+
padding: "6px 10px", fontSize: 12,
1334+
background: "#eef2ff",
1335+
border: "1px solid #c7d2fe", borderRadius: 6,
1336+
fontFamily: "system-ui, sans-serif" }}>
1337+
Loss surface…
1338+
</button>
1339+
<LossSurfaceModal
1340+
rpc={rpc}
1341+
spec={buildVerifyParams(
1342+
nodes, edges, spec, availableSideChannels)}
1343+
open={lossSurfaceOpen}
1344+
onClose={() => setLossSurfaceOpen(false)}
1345+
onApplyBest={(lrMult, wdMult) => {
1346+
// V7-H33: scale every optim group's lr+wd by the chosen
1347+
// multipliers and commit through dispatch so verify+history
1348+
// see the change.
1349+
const nextGroups = spec.optim.groups.map((g) => ({
1350+
...g,
1351+
lr: g.lr * lrMult,
1352+
weight_decay: (g.weight_decay ?? 0) * wdMult,
1353+
}));
1354+
dispatch({ type: "optim.set",
1355+
optim: { ...spec.optim, groups: nextGroups } });
1356+
}} />
13261357
</div>
13271358
</ReactFlowProvider>
13281359
);
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// V7-H33: LossSurfaceModal — N×M heatmap of (lr_mult × wd_mult)
2+
// triggered by RunResultModal "Explore loss surface" button. Calls
3+
// loss_surface.run RPC, renders the best cell + Apply-best button that
4+
// commits the chosen lr/wd multipliers back to the optim spec.
5+
6+
import { useState } from "react";
7+
import type { RpcClient } from "@/lib/rpc";
8+
import { HelpIcon } from "@/components/HelpIcon";
9+
10+
export interface LossSurfaceCell {
11+
lr_mult: number;
12+
wd_mult: number;
13+
status: string;
14+
final_loss?: number | null;
15+
throughput_tok_s?: number | null;
16+
mem_mb?: number | null;
17+
elapsed_ms?: number;
18+
}
19+
20+
export interface LossSurfaceResult {
21+
rows: LossSurfaceCell[][];
22+
lr_deltas: number[];
23+
wd_deltas: number[];
24+
best_lr_mult: number | null;
25+
best_wd_mult: number | null;
26+
best_loss: number | null;
27+
}
28+
29+
export interface LossSurfaceModalProps {
30+
rpc: RpcClient | null;
31+
spec: unknown;
32+
open: boolean;
33+
onClose: () => void;
34+
onApplyBest: (lrMult: number, wdMult: number) => void;
35+
}
36+
37+
const DEFAULT_LR_DELTAS = [0.5, 1.0, 2.0];
38+
const DEFAULT_WD_DELTAS = [0.5, 1.0, 2.0];
39+
40+
function cellColor(loss: number | null | undefined,
41+
min: number | null, max: number | null): string {
42+
if (loss == null || min == null || max == null || max <= min) {
43+
return "#f3f4f6";
44+
}
45+
const t = (loss - min) / (max - min);
46+
const r = Math.round(40 + 215 * t);
47+
const g = Math.round(180 - 120 * t);
48+
return `rgb(${r}, ${g}, 60)`;
49+
}
50+
51+
export function LossSurfaceModal({
52+
rpc, spec, open, onClose, onApplyBest,
53+
}: LossSurfaceModalProps): JSX.Element | null {
54+
const [running, setRunning] = useState(false);
55+
const [result, setResult] = useState<LossSurfaceResult | null>(null);
56+
const [error, setError] = useState<string | null>(null);
57+
const [kSteps, setKSteps] = useState<number>(2);
58+
59+
if (!open) return null;
60+
61+
async function run() {
62+
setRunning(true);
63+
setError(null);
64+
setResult(null);
65+
if (!rpc) {
66+
setError("no backend connection");
67+
setRunning(false);
68+
return;
69+
}
70+
try {
71+
const r = await rpc.call<LossSurfaceResult>("loss_surface.run", {
72+
spec,
73+
lr_deltas: DEFAULT_LR_DELTAS,
74+
wd_deltas: DEFAULT_WD_DELTAS,
75+
k_steps: kSteps,
76+
});
77+
setResult(r);
78+
} catch (e) {
79+
setError(e instanceof Error ? e.message : String(e));
80+
} finally {
81+
setRunning(false);
82+
}
83+
}
84+
85+
const allLosses = result?.rows.flat()
86+
.map((c) => c.final_loss)
87+
.filter((v): v is number => v != null) ?? [];
88+
const minLoss = allLosses.length ? Math.min(...allLosses) : null;
89+
const maxLoss = allLosses.length ? Math.max(...allLosses) : null;
90+
91+
return (
92+
<div data-testid="loss-surface-modal"
93+
role="dialog" aria-modal="true"
94+
style={{ position: "fixed", inset: 0, zIndex: 1000,
95+
background: "rgba(0,0,0,0.45)", display: "flex",
96+
alignItems: "center", justifyContent: "center",
97+
padding: 24, fontFamily: "system-ui, sans-serif" }}>
98+
<div style={{ background: "white", borderRadius: 8, padding: 18,
99+
minWidth: 460, maxWidth: 720, maxHeight: "85vh",
100+
overflow: "auto" }}>
101+
<div style={{ display: "flex", justifyContent: "space-between",
102+
alignItems: "center", marginBottom: 8 }}>
103+
<h3 data-testid="loss-surface-title"
104+
style={{ margin: 0, fontSize: 16 }}>
105+
Loss surface · lr_mult × wd_mult
106+
<HelpIcon topic="loss_surface_explorer" />
107+
</h3>
108+
<button data-testid="loss-surface-close"
109+
onClick={onClose}>×</button>
110+
</div>
111+
112+
<div style={{ fontSize: 12, marginBottom: 8 }}>
113+
<label style={{ marginRight: 8 }}>k_steps:</label>
114+
<input data-testid="loss-surface-k-steps"
115+
type="number" min={1} max={64} value={kSteps}
116+
onChange={(e) =>
117+
setKSteps(Math.max(1, Math.min(64,
118+
Number(e.target.value) || 2)))}
119+
style={{ width: 60 }} />
120+
<button data-testid="loss-surface-run"
121+
onClick={() => void run()}
122+
disabled={running}
123+
style={{ marginLeft: 10 }}>
124+
{running ? "Running…" : "Run sweep"}
125+
</button>
126+
</div>
127+
128+
{error && (
129+
<div data-testid="loss-surface-error"
130+
style={{ color: "#b91c1c", fontSize: 12 }}>
131+
{error}
132+
</div>
133+
)}
134+
135+
{result && (
136+
<div data-testid="loss-surface-result">
137+
<table style={{ borderCollapse: "collapse",
138+
margin: "8px 0" }}>
139+
<thead>
140+
<tr>
141+
<th style={{ padding: "4px 8px", fontSize: 11 }}>
142+
lr↓ / wd→
143+
</th>
144+
{result.wd_deltas.map((wd) => (
145+
<th key={wd}
146+
data-testid={`loss-surface-col-${wd}`}
147+
style={{ padding: "4px 8px", fontSize: 11 }}>
148+
×{wd}
149+
</th>
150+
))}
151+
</tr>
152+
</thead>
153+
<tbody>
154+
{result.rows.map((row, ri) => (
155+
<tr key={ri}>
156+
<th data-testid={`loss-surface-row-${result.lr_deltas[ri]}`}
157+
style={{ padding: "4px 8px", fontSize: 11 }}>
158+
×{result.lr_deltas[ri]}
159+
</th>
160+
{row.map((cell, ci) => {
161+
const isBest = result.best_lr_mult === cell.lr_mult
162+
&& result.best_wd_mult === cell.wd_mult;
163+
return (
164+
<td key={ci}
165+
data-testid={`loss-surface-cell-${ri}-${ci}`}
166+
style={{
167+
padding: "10px 14px",
168+
background: cellColor(cell.final_loss,
169+
minLoss, maxLoss),
170+
color: "white", fontSize: 11,
171+
border: isBest
172+
? "3px solid #facc15" : "1px solid #e5e7eb",
173+
textAlign: "center",
174+
minWidth: 56,
175+
}}>
176+
{cell.status === "ok" && cell.final_loss != null
177+
? cell.final_loss.toFixed(3)
178+
: cell.status}
179+
</td>
180+
);
181+
})}
182+
</tr>
183+
))}
184+
</tbody>
185+
</table>
186+
187+
<div data-testid="loss-surface-best"
188+
style={{ fontSize: 12, marginTop: 4 }}>
189+
{result.best_loss != null ? (
190+
<>
191+
best: lr×{result.best_lr_mult}, wd×{result.best_wd_mult}
192+
{" → loss "}{result.best_loss.toFixed(4)}
193+
</>
194+
) : (
195+
"no successful cells"
196+
)}
197+
</div>
198+
199+
{result.best_lr_mult != null && result.best_wd_mult != null && (
200+
<button data-testid="loss-surface-apply-best"
201+
onClick={() => {
202+
onApplyBest(result.best_lr_mult!,
203+
result.best_wd_mult!);
204+
onClose();
205+
}}
206+
style={{ marginTop: 8 }}>
207+
Apply best to optim
208+
</button>
209+
)}
210+
</div>
211+
)}
212+
</div>
213+
</div>
214+
);
215+
}

0 commit comments

Comments
 (0)