Skip to content

Commit 999136f

Browse files
committed
feat(v7-f56b,f53): DimEnvEditor for live H/nh/head_dim mutation
vbgui/src/components/DimEnvEditor.tsx: - Five-input row (H, nh, head_dim, B, S) + Apply button, rendered above the canvas. Each input has data-testid='dim-env-{key}'. - Inline mismatch indicator (data-testid='dim-env-inline-mismatch') appears live as the user types — without waiting for Apply — when nh*head_dim ≠ H. Renders "nh*head_dim = N ≠ H = M". - Numeric inputs; non-finite drafts are ignored on Apply. vbgui/src/App.tsx: - MINI_DIM_ENV constant was previously inlined into 3 RPC call sites (verify, suggest_sharding, buildVerifyParams default). Promoted to dimEnv useState, fed back into wireSpecRef and into the verify debouncer dep list (dimEnvKey) so a user mismatch immediately re-runs verify and surfaces the F56b warning badge. - Canvas wrapper switched to flexDirection='column' so the editor stacks above FlowCanvas without occluding it. - DimEnvEditor and the existing F56b badge co-render: the editor's inline indicator surfaces *during* typing; the badge surfaces *after* verify roundtrip completes. vbgui/tests/DimEnvEditor.test.tsx — 5 unit tests: - Inputs populate from value prop. - Apply forwards parsed numeric draft. - Mismatch indicator visible when nh*head_dim ≠ H. - Mismatch indicator hidden when consistent. - Indicator re-evaluates live (before Apply) on edit. App.integration tests still pass 7/7; vitest total 12/12. Unlocks F56b visual e2e (type mismatch into editor → assert badge) and F53 scaling sweep (programmatic H ∈ {64,128,256,512} via Apply). Ref: cppmega-mlx-j3qa.2, cppmega-mlx-j3qa.4
1 parent c970384 commit 999136f

3 files changed

Lines changed: 163 additions & 7 deletions

File tree

vbgui/src/App.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from "@xyflow/react";
55

66
import { FlowCanvas } from "@/components/FlowCanvas";
7+
import { DimEnvEditor } from "@/components/DimEnvEditor";
78
import { Palette } from "@/components/Palette";
89
import { Sidebar } from "@/components/Sidebar";
910
import { TopBar, type RunMode } from "@/components/TopBar";
@@ -147,11 +148,19 @@ export function App(): JSX.Element {
147148
// list that was missing 5 architectures.
148149
const PRESETS = usePresets(rpc);
149150

151+
// V7-F56b / V7-F53: editable dim_env (was a constant MINI_DIM_ENV).
152+
// Threaded into the verify trigger so changes to H/nh/head_dim
153+
// re-run verify and surface the F56b mismatch warning.
154+
const [dimEnv, setDimEnv] = useState<Record<string, number>>(
155+
{ ...MINI_DIM_ENV });
156+
150157
// Keep one stable spec snapshot for the verify debouncer to read.
151-
const wireSpecRef = useRef({ nodes, edges, spec, availableSideChannels });
158+
const wireSpecRef = useRef({ nodes, edges, spec, availableSideChannels,
159+
dimEnv });
152160
useEffect(() => {
153-
wireSpecRef.current = { nodes, edges, spec, availableSideChannels };
154-
}, [nodes, edges, spec, availableSideChannels]);
161+
wireSpecRef.current = { nodes, edges, spec, availableSideChannels,
162+
dimEnv };
163+
}, [nodes, edges, spec, availableSideChannels, dimEnv]);
155164

156165
useEffect(() => {
157166
const available = new Set(availableSideChannels);
@@ -163,6 +172,7 @@ export function App(): JSX.Element {
163172
if (snap.nodes.length === 0) return;
164173
const params = buildVerifyParams(
165174
snap.nodes, snap.edges, snap.spec, snap.availableSideChannels,
175+
snap.dimEnv,
166176
);
167177
try {
168178
const r = await rpc.call<{
@@ -227,10 +237,14 @@ export function App(): JSX.Element {
227237
const rewriterKey = spec.rewriters.map((r) => r.name).join(",");
228238
const sideChannelKey = JSON.stringify(spec.side_channels);
229239
const availableSideChannelKey = availableSideChannels.join(",");
240+
// V7-F56b/F53: re-verify when the dim_env editor mutates H, nh, or
241+
// head_dim — otherwise the F56b badge wouldn't appear after a user
242+
// types a mismatch.
243+
const dimEnvKey = JSON.stringify(dimEnv);
230244
useEffect(() => { scheduleVerify(); },
231245
[nodesKey, edgesKey, lossKey, optimKey, shardingKey,
232246
rewriterKey, sideChannelKey, availableSideChannelKey,
233-
scheduleVerify]);
247+
dimEnvKey, scheduleVerify]);
234248

235249
// V7-H03: snapshot the (nodes, edges, spec) triple every time a
236250
// structural key changes — that's the same set of user-meaningful
@@ -343,7 +357,7 @@ export function App(): JSX.Element {
343357
}[] } }[];
344358
}>("suggest_sharding", {
345359
graph: nodesToGraph(snap.nodes, snap.edges),
346-
dim_env: MINI_DIM_ENV,
360+
dim_env: snap.dimEnv,
347361
loss: { kind: snap.spec.loss.kind,
348362
head_outputs: snap.spec.loss.head_outputs },
349363
optim: { kind: snap.spec.optim.kind,
@@ -626,8 +640,10 @@ export function App(): JSX.Element {
626640
{activeTab === "canvas" && (
627641
<>
628642
<Palette />
629-
<div style={{ flex: 1, position: "relative", display: "flex",
643+
<div style={{ flex: 1, position: "relative",
644+
display: "flex", flexDirection: "column",
630645
minHeight: 0 }}>
646+
<DimEnvEditor value={dimEnv} onApply={setDimEnv} />
631647
{spec.gotchas.some(
632648
(g) => g.id === "v7_f56b_dim_env_mismatch") && (
633649
<div
@@ -821,10 +837,11 @@ function buildVerifyParams(
821837
edges: Edge[],
822838
spec: SpecState,
823839
availableSideChannels: string[] = ["doc_ids", "token_ids"],
840+
dimEnv: Record<string, number> = MINI_DIM_ENV,
824841
) {
825842
return {
826843
graph: nodesToGraph(nodes, edges),
827-
dim_env: MINI_DIM_ENV,
844+
dim_env: dimEnv,
828845
loss: { kind: spec.loss.kind, head_outputs: spec.loss.head_outputs,
829846
params: spec.loss.params },
830847
optim: { kind: spec.optim.kind,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Minimal dim_env editor surfaced above the canvas. Unlocks the F56b
2+
// visual-warning flow (mismatch surfaces a banner in the canvas) and
3+
// the F53 dimension-scaling sweep (sweeps H ∈ {64,128,256,512} by
4+
// calling onApply per H).
5+
6+
import { useState } from "react";
7+
8+
export interface DimEnvEditorProps {
9+
value: Record<string, number>;
10+
onApply: (next: Record<string, number>) => void;
11+
}
12+
13+
const EDITABLE_KEYS = ["H", "nh", "head_dim", "B", "S"] as const;
14+
type EditableKey = typeof EDITABLE_KEYS[number];
15+
16+
export function DimEnvEditor({ value, onApply }: DimEnvEditorProps): JSX.Element {
17+
const [draft, setDraft] = useState<Record<string, string>>(() => {
18+
const out: Record<string, string> = {};
19+
for (const k of EDITABLE_KEYS) out[k] = String(value[k] ?? "");
20+
return out;
21+
});
22+
23+
const mismatch = (() => {
24+
const H = Number(draft.H);
25+
const nh = Number(draft.nh);
26+
const hd = Number(draft.head_dim);
27+
if (!Number.isFinite(H) || !Number.isFinite(nh) ||
28+
!Number.isFinite(hd)) return null;
29+
if (nh * hd === H) return null;
30+
return `nh*head_dim = ${nh * hd} ≠ H = ${H}`;
31+
})();
32+
33+
return (
34+
<div data-testid="dim-env-editor"
35+
style={{ display: "flex", alignItems: "center", gap: 8,
36+
padding: "4px 8px", background: "#f9fafb",
37+
borderBottom: "1px solid #e5e7eb",
38+
fontFamily: "system-ui, sans-serif", fontSize: 12 }}>
39+
<strong data-testid="dim-env-editor-label">dim_env:</strong>
40+
{EDITABLE_KEYS.map((k: EditableKey) => (
41+
<label key={k} style={{ display: "inline-flex", gap: 4 }}>
42+
{k}
43+
<input
44+
data-testid={`dim-env-${k}`}
45+
type="number"
46+
value={draft[k]}
47+
onChange={(e) => setDraft({ ...draft, [k]: e.target.value })}
48+
style={{ width: 64 }}
49+
/>
50+
</label>
51+
))}
52+
<button
53+
data-testid="dim-env-apply"
54+
onClick={() => {
55+
const next: Record<string, number> = { ...value };
56+
for (const k of EDITABLE_KEYS) {
57+
const n = Number(draft[k]);
58+
if (Number.isFinite(n)) next[k] = n;
59+
}
60+
onApply(next);
61+
}}
62+
style={{ padding: "2px 8px" }}
63+
>
64+
Apply
65+
</button>
66+
{mismatch && (
67+
<span data-testid="dim-env-inline-mismatch"
68+
style={{ color: "#92400e", marginLeft: 8 }}>
69+
{mismatch}
70+
</span>
71+
)}
72+
</div>
73+
);
74+
}

vbgui/tests/DimEnvEditor.test.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { render, screen, fireEvent } from "@testing-library/react";
3+
import { DimEnvEditor } from "@/components/DimEnvEditor";
4+
5+
describe("V7-F56b/F53 DimEnvEditor", () => {
6+
it("populates inputs from the value prop", () => {
7+
render(<DimEnvEditor
8+
value={{ H: 128, nh: 2, head_dim: 64, B: 1, S: 8 }}
9+
onApply={() => {}}
10+
/>);
11+
expect((screen.getByTestId("dim-env-H") as HTMLInputElement).value).toBe("128");
12+
expect((screen.getByTestId("dim-env-nh") as HTMLInputElement).value).toBe("2");
13+
expect((screen.getByTestId("dim-env-head_dim") as HTMLInputElement).value).toBe("64");
14+
});
15+
16+
it("Apply calls onApply with the parsed numeric draft", () => {
17+
const onApply = vi.fn();
18+
render(<DimEnvEditor
19+
value={{ H: 128, nh: 2, head_dim: 64, B: 1, S: 8 }}
20+
onApply={onApply}
21+
/>);
22+
fireEvent.change(screen.getByTestId("dim-env-H"),
23+
{ target: { value: "256" } });
24+
fireEvent.change(screen.getByTestId("dim-env-nh"),
25+
{ target: { value: "4" } });
26+
fireEvent.click(screen.getByTestId("dim-env-apply"));
27+
expect(onApply).toHaveBeenCalledTimes(1);
28+
const next = onApply.mock.calls[0]?.[0];
29+
expect(next.H).toBe(256);
30+
expect(next.nh).toBe(4);
31+
expect(next.head_dim).toBe(64);
32+
});
33+
34+
it("renders an inline mismatch indicator when nh*head_dim != H", () => {
35+
render(<DimEnvEditor
36+
value={{ H: 128, nh: 3, head_dim: 50, B: 1, S: 8 }}
37+
onApply={() => {}}
38+
/>);
39+
const warn = screen.getByTestId("dim-env-inline-mismatch");
40+
expect(warn).toBeDefined();
41+
expect(warn.textContent).toContain("150");
42+
expect(warn.textContent).toContain("128");
43+
});
44+
45+
it("hides the mismatch indicator when nh*head_dim == H", () => {
46+
render(<DimEnvEditor
47+
value={{ H: 128, nh: 2, head_dim: 64, B: 1, S: 8 }}
48+
onApply={() => {}}
49+
/>);
50+
expect(screen.queryByTestId("dim-env-inline-mismatch")).toBeNull();
51+
});
52+
53+
it("re-evaluates the mismatch live as the user types (before Apply)", () => {
54+
render(<DimEnvEditor
55+
value={{ H: 128, nh: 2, head_dim: 64, B: 1, S: 8 }}
56+
onApply={() => {}}
57+
/>);
58+
expect(screen.queryByTestId("dim-env-inline-mismatch")).toBeNull();
59+
fireEvent.change(screen.getByTestId("dim-env-nh"),
60+
{ target: { value: "3" } });
61+
// 3 * 64 = 192 ≠ 128 → mismatch surfaces without Apply.
62+
const warn = screen.getByTestId("dim-env-inline-mismatch");
63+
expect(warn.textContent).toContain("192");
64+
});
65+
});

0 commit comments

Comments
 (0)