Skip to content

Commit 4f8473a

Browse files
committed
feat: implement neural debugger frontend and backend support for brick node interaction
1 parent e4bcf77 commit 4f8473a

7 files changed

Lines changed: 1851 additions & 15 deletions

File tree

backend.log

Lines changed: 1690 additions & 0 deletions
Large diffs are not rendered by default.

cppmega_v4/jsonrpc/methods.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,32 @@ def _make_schedule(g) -> ScheduleSpec | None:
173173
schedule = g.schedule
174174
if schedule is None:
175175
return None
176+
kind = schedule.kind
177+
warmup_steps = schedule.warmup_steps if schedule.warmup_steps is not None else 0
178+
total_steps = schedule.total_steps
179+
min_lr_ratio = schedule.min_lr_ratio if schedule.min_lr_ratio is not None else 0.0
180+
decay_steps = schedule.decay_steps
181+
power = schedule.power if schedule.power is not None else 2.0
182+
183+
if kind in ("cosine", "wsd", "polynomial"):
184+
if total_steps is None or total_steps <= 0:
185+
total_steps = 100_000
186+
if total_steps < warmup_steps:
187+
warmup_steps = total_steps
188+
189+
if kind == "wsd":
190+
if decay_steps is None or decay_steps < 1:
191+
decay_steps = 10_000
192+
if warmup_steps + decay_steps > total_steps:
193+
decay_steps = max(1, total_steps - warmup_steps)
194+
176195
return ScheduleSpec(
177-
kind=schedule.kind,
178-
warmup_steps=schedule.warmup_steps,
179-
total_steps=schedule.total_steps,
180-
min_lr_ratio=schedule.min_lr_ratio,
181-
decay_steps=schedule.decay_steps,
182-
power=schedule.power,
196+
kind=kind,
197+
warmup_steps=warmup_steps,
198+
total_steps=total_steps,
199+
min_lr_ratio=min_lr_ratio,
200+
decay_steps=decay_steps,
201+
power=power,
183202
)
184203

185204
groups = tuple(

frontend.log

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@
77

88
➜ Local: http://localhost:5173/
99
➜ Network: use --host to expose
10+
2:48:34 AM [vite] hmr update /src/App.tsx
11+
2:50:20 AM [vite] hmr update /src/components/BrickNode.tsx
12+
2:50:22 AM [vite] hmr update /src/components/BrickNode.tsx
13+
2:50:28 AM [vite] hmr update /src/App.tsx
14+
2:50:30 AM [vite] hmr update /src/App.tsx
15+
2:50:37 AM [vite] hmr update /src/App.tsx
16+
2:50:43 AM [vite] hmr update /src/App.tsx
17+
2:50:54 AM [vite] hmr update /src/App.tsx
18+
2:50:56 AM [vite] hmr update /src/App.tsx
19+
2:50:59 AM [vite] hmr update /src/App.tsx
20+
2:55:07 AM [vite] hmr update /src/App.tsx
21+
2:55:20 AM [vite] hmr update /src/App.tsx

vbgui/src/App.tsx

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,19 @@ export function App(): JSX.Element {
192192
const [tabs, setTabs] = useState<WorkspaceTab[]>(getSavedTabs);
193193
const [activeTabId, setActiveTabId] = useState<string>(getSavedActiveTabId);
194194
const isSwitchingRef = useRef<boolean>(false);
195-
const debugState = useNeuralDebugger();
196195

197196
const [nodes, setNodes] = useState<Node[]>(() => {
198197
const savedTabs = getSavedTabs();
199198
const savedActiveId = getSavedActiveTabId();
200199
const active = savedTabs.find((t) => t.id === savedActiveId) || savedTabs[0];
201200
return active.nodes;
202201
});
202+
203+
const modelNodesCount = useMemo(() => {
204+
return nodes.filter(n => n.type === "brick" || n.type === "adapter").length;
205+
}, [nodes]);
206+
207+
const maxStep = modelNodesCount + 1;
203208
const [edges, setEdges] = useState<Edge[]>(() => {
204209
const savedTabs = getSavedTabs();
205210
const savedActiveId = getSavedActiveTabId();
@@ -303,6 +308,7 @@ export function App(): JSX.Element {
303308
// hardcoding 65536 — so dropping a GPT-2 / SmolLM / cppmega tokenizer
304309
// gives the BrickContextPanel the right embedding_table param count.
305310
const tokenizerVocabSize = useTokenizerVocab(rpc, trainTokenizerPath);
311+
const debugState = useNeuralDebugger(rpc, trainTokenizerPath);
306312

307313
// V7-I07: poll the JsonRPC LRU cache hit-rate so the BottomStrip
308314
// chip surfaces hit_rate / size / evictions live.
@@ -1001,6 +1007,7 @@ export function App(): JSX.Element {
10011007
...(firstGroup.schedule ?? {}),
10021008
kind: scheduleKind,
10031009
warmup_steps: d.warmup_steps,
1010+
total_steps: firstGroup.schedule?.total_steps ?? 100000,
10041011
},
10051012
},
10061013
...spec.optim.groups.slice(1),
@@ -1088,6 +1095,7 @@ export function App(): JSX.Element {
10881095
...(firstGroup.schedule ?? {}),
10891096
kind: scheduleKind,
10901097
warmup_steps: d.warmup_steps,
1098+
total_steps: firstGroup.schedule?.total_steps ?? 100000,
10911099
},
10921100
},
10931101
...spec.optim.groups.slice(1),
@@ -1520,6 +1528,46 @@ export function App(): JSX.Element {
15201528
if (trainInFlight && trainRunId) liveTrainResetRef.current();
15211529
}, [trainInFlight, trainRunId]);
15221530

1531+
// V7-F03-ANIMATE: Trigger forward-backward canvas stepping animation on actual WebSocket step updates
1532+
const lastAnimatedStepRef = useRef<number>(-1);
1533+
const animateStepRef = useRef(debugState.animateFullTrainStep);
1534+
useEffect(() => {
1535+
animateStepRef.current = debugState.animateFullTrainStep;
1536+
}, [debugState.animateFullTrainStep]);
1537+
1538+
useEffect(() => {
1539+
if (!trainInFlight) {
1540+
lastAnimatedStepRef.current = -1;
1541+
return;
1542+
}
1543+
const lastEvent = liveTrain.events.length > 0 ? liveTrain.events[liveTrain.events.length - 1] : null;
1544+
if (lastEvent && lastEvent.step !== lastAnimatedStepRef.current) {
1545+
lastAnimatedStepRef.current = lastEvent.step;
1546+
// Trigger full forward-backward canvas pass
1547+
animateStepRef.current(maxStep);
1548+
}
1549+
}, [liveTrain.events, trainInFlight, maxStep]);
1550+
1551+
// V7-F04-SYNC: Sync real Loss and LR values from active WebSocket stream to the debugger
1552+
const setLrRef = useRef(debugState.setLr);
1553+
const setLossValRef = useRef(debugState.setLossVal);
1554+
useEffect(() => {
1555+
setLrRef.current = debugState.setLr;
1556+
setLossValRef.current = debugState.setLossVal;
1557+
}, [debugState.setLr, debugState.setLossVal]);
1558+
1559+
useEffect(() => {
1560+
const lastEvent = liveTrain.events.length > 0 ? liveTrain.events[liveTrain.events.length - 1] : null;
1561+
if (lastEvent) {
1562+
if (typeof lastEvent.lr === "number") {
1563+
setLrRef.current(lastEvent.lr);
1564+
}
1565+
if (typeof lastEvent.loss === "number") {
1566+
setLossValRef.current(lastEvent.loss);
1567+
}
1568+
}
1569+
}, [liveTrain.events]);
1570+
15231571
const handleShardingAccept = useCallback((idx: number) => {
15241572
const chosen = proposals[idx];
15251573
if (!chosen) return;
@@ -1538,11 +1586,6 @@ export function App(): JSX.Element {
15381586
setTimeout(() => setRunError(null), 2000);
15391587
}, [proposals, scheduleVerify, spec.sharding]);
15401588

1541-
const modelNodesCount = useMemo(() => {
1542-
return nodes.filter(n => n.type === "brick" || n.type === "adapter").length;
1543-
}, [nodes]);
1544-
1545-
const maxStep = modelNodesCount + 1;
15461589

15471590
const mappedNodes = useMemo(() => {
15481591
const head = spec.loss.head_outputs?.[0];
@@ -1555,6 +1598,8 @@ export function App(): JSX.Element {
15551598

15561599
const K = modelNodes.length;
15571600

1601+
const lastEvent = liveTrain.events.length > 0 ? liveTrain.events[liveTrain.events.length - 1] : null;
1602+
15581603
let enriched = nodes.map((n) => {
15591604
const isModel = n.type === "brick" || n.type === "adapter";
15601605
const idx = isModel ? modelNodes.findIndex((m) => m.id === n.id) : -1;
@@ -1566,6 +1611,31 @@ export function App(): JSX.Element {
15661611
(n.id === "__loss_ghost__" && debugState.activeStep === K);
15671612
}
15681613

1614+
let gradNorm: number | undefined;
1615+
if (lastEvent?.grad_norms && isModel) {
1616+
const normKeys = Object.keys(lastEvent.grad_norms);
1617+
const nameVal = typeof (n.data as any)?.name === "string" ? (n.data as any).name.toLowerCase() : "";
1618+
const idVal = n.id.toLowerCase();
1619+
1620+
// Find best key match in grad_norms map
1621+
const matchedKey = normKeys.find((k) => {
1622+
const lk = k.toLowerCase();
1623+
if (nameVal && (lk.includes(nameVal) || nameVal.includes(lk))) return true;
1624+
if (lk.includes(idVal) || idVal.includes(lk)) return true;
1625+
// check if index matches e.g. key contains "layers.3." or "layer_3" etc.
1626+
const idxPattern1 = `layers.${idx}.`;
1627+
const idxPattern2 = `layers.${idx}`;
1628+
const idxPattern3 = `layer_${idx}`;
1629+
const idxPattern4 = `.${idx}.`;
1630+
const idxPattern5 = `.${idx}`;
1631+
if (lk.includes(idxPattern1) || lk.includes(idxPattern2) || lk.includes(idxPattern3) || lk.includes(idxPattern4) || lk.endsWith(idxPattern5)) return true;
1632+
return false;
1633+
});
1634+
if (matchedKey !== undefined) {
1635+
gradNorm = lastEvent.grad_norms[matchedKey];
1636+
}
1637+
}
1638+
15691639
let additionalData: any = {};
15701640
if (n.type === "tokenizer_virtual") {
15711641
isActiveNode = debugState.debuggerMode && debugState.activeStep === -1;
@@ -1581,6 +1651,10 @@ export function App(): JSX.Element {
15811651
direction: debugState.direction,
15821652
isActiveNode,
15831653
};
1654+
} else if (isModel && gradNorm !== undefined) {
1655+
additionalData = {
1656+
gradNorm,
1657+
};
15841658
}
15851659

15861660
return {
@@ -1659,7 +1733,7 @@ export function App(): JSX.Element {
16591733
result.push(detokenizerNode);
16601734
}
16611735
return result;
1662-
}, [nodes, debugState.debuggerMode, debugState.activeStep, debugState.direction, debugState.prompt, debugState.tokens, debugState.isWeightUpdated, spec.loss.head_outputs, spec.loss.kind, spec.loss.params]);
1736+
}, [nodes, debugState.debuggerMode, debugState.activeStep, debugState.direction, debugState.prompt, debugState.tokens, debugState.isWeightUpdated, spec.loss.head_outputs, spec.loss.kind, spec.loss.params, liveTrain.events]);
16631737

16641738
const mappedEdges = useMemo(() => {
16651739
const head = spec.loss.head_outputs?.[0];

vbgui/src/components/BrickNode.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface BrickNodeData {
1717
debuggerMode?: boolean;
1818
isActiveNode?: boolean;
1919
isWeightUpdated?: boolean;
20+
gradNorm?: number;
2021
}
2122

2223
export function BrickNode({ data, id, selected }: NodeProps): JSX.Element {
@@ -80,7 +81,7 @@ export function BrickNode({ data, id, selected }: NodeProps): JSX.Element {
8081
</div>
8182
</div>
8283

83-
{(d.shape || typeof d.memory_mb === "number") && (
84+
{(d.shape || typeof d.memory_mb === "number" || typeof d.gradNorm === "number") && (
8485
<div style={{ display: "flex", flexWrap: "wrap", gap: 6,
8586
marginTop: 10 }}>
8687
{d.shape && (
@@ -93,6 +94,20 @@ export function BrickNode({ data, id, selected }: NodeProps): JSX.Element {
9394
{d.memory_mb.toFixed(1)} MB
9495
</span>
9596
)}
97+
{typeof d.gradNorm === "number" && (
98+
<span
99+
data-testid="brick-grad-norm"
100+
style={{
101+
...statPill,
102+
color: "var(--vb-success)",
103+
borderColor: "rgba(52, 211, 153, 0.4)",
104+
boxShadow: "0 0 8px rgba(52, 211, 153, 0.2)",
105+
fontWeight: 600,
106+
}}
107+
>
108+
‖g‖: {d.gradNorm.toFixed(4)}
109+
</span>
110+
)}
96111
</div>
97112
)}
98113

vbgui/src/hooks/useNeuralDebugger.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export interface Token {
55
text: string;
66
}
77

8-
export function useNeuralDebugger() {
8+
export function useNeuralDebugger(rpc?: any, tokenizerPath?: string | null) {
99
const [debuggerMode, setDebuggerMode] = useState(false);
1010
const [activeStep, setActiveStep] = useState(-1); // -1 = tokenizer, 0..N-1 = layer nodes, N = loss, N+1 = de-tokenizer
1111
const [direction, setDirection] = useState<"forward" | "backward">("forward");
@@ -23,6 +23,27 @@ export function useNeuralDebugger() {
2323
const [lossVal, setLossVal] = useState(2.34);
2424
const [isWeightUpdated, setIsWeightUpdated] = useState(false);
2525

26+
// V7-F01-REAL: fetch real segmented tokens from the active tokenizer via RPC
27+
useEffect(() => {
28+
if (!rpc || !tokenizerPath || !prompt) return;
29+
let cancelled = false;
30+
(async () => {
31+
try {
32+
const r = await rpc.call(
33+
"tokenizer.encode_visualize",
34+
{ text: prompt, tokenizer_source: tokenizerPath }
35+
);
36+
if (cancelled) return;
37+
if (r && r.tokens) {
38+
setTokens(r.tokens.map((t: any) => ({ id: t.id, text: t.text })));
39+
}
40+
} catch (e) {
41+
console.error("Debugger tokenizer encode failed:", e);
42+
}
43+
})();
44+
return () => { cancelled = true; };
45+
}, [rpc, tokenizerPath, prompt]);
46+
2647
// Use refs for intervals to prevent state staleness
2748
const stepTimerRef = useRef<NodeJS.Timeout | null>(null);
2849

vbgui/tests/BrickNode.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ describe("BrickNode", () => {
6262
expect(screen.getByTestId("brick-side-channel-warn")).toBeTruthy();
6363
});
6464

65+
it("shows gradNorm when provided", () => {
66+
renderBrick({ kind: "mlp", gradNorm: 0.123456 });
67+
expect(screen.getByTestId("brick-grad-norm").textContent).toContain("‖g‖: 0.1235");
68+
});
69+
6570
it("falls back to kind when meta is unknown", () => {
6671
renderBrick({ kind: "totally_made_up" });
6772
expect(screen.getAllByText("totally_made_up").length).toBeGreaterThan(0);

0 commit comments

Comments
 (0)