Skip to content

Commit ee094dd

Browse files
committed
feat(v4-1): DataInspector → stage_options.train.parquet_path threading
Closes V4-1 / cppmega-mlx-vsw. Fixes G1: V3-2 plumbed the backend to accept opts.parquet_path but the DataInspector selection never reached it, so user-picked parquet shards stayed decorative for training. DataInspector: - new prop onUseForTrain(parquetPath, tokenizerPath | null) - new prop trainParquetPath (drives ✓ Training label) - data-testid='data-use-for-train' button next to Load — disabled until a parquet has been loaded. App.tsx: - new state trainParquetPath / trainTokenizerPath - handleRunPipeline forwards both via stage_options.train.{parquet_path, tokenizer_path} when set (tokenizer path lands ahead of V4-2 backend consumer — harmless ignored extra now). TopBar: - new props trainParquetPath / trainTokenizerPath - data-testid='train-data-source' indicator reads 'synthetic' by default; upgrades to 'parquet: <basename>' or 'parquet: ... · tok: ...' when set. New e2e 28_parquet_train_threading.spec.ts (2 cells): + positive: load fixture parquet → Use-for-train → Train → assert extras.data_source === "parquet" + token_count > 0 + negative: skip Use-for-train → Train → extras.data_source === "synthetic" (V3-2 fallback still works) 7 new vitest (3 TopBar + 4 DataInspector). Regression 173/173 + 8/8 adjacent e2e (11_ui_to_train + 15_gating).
1 parent 6db1302 commit ee094dd

6 files changed

Lines changed: 225 additions & 4 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// V4-1: DataInspector → stage_options.train.parquet_path threading.
2+
//
3+
// Walks: load parquet in Data tab → click "Use for training" → switch
4+
// to canvas → Train → assert extras.data_source === "parquet" AND
5+
// extras.token_count > 0 (proves UI selection actually reached backend).
6+
//
7+
// Closes G1 from V4 audit (UI parquet decorative for training).
8+
9+
import { test, expect } from "@playwright/test";
10+
import { gotoApp, selectPreset, clickTab, closeModal } from "../fixtures";
11+
import { loadMatrix } from "../utils/matrix";
12+
import { readTrainExtras } from "../utils/train_extras";
13+
14+
test("V4-1: DataInspector parquet reaches stage_train via stage_options",
15+
async ({ page }) => {
16+
test.setTimeout(120_000);
17+
const matrix = loadMatrix();
18+
const parquet = matrix.parquets.T2_gpt2_small__P1_minimal.path;
19+
20+
await gotoApp(page);
21+
await selectPreset(page, "llama3_8b");
22+
23+
// Default indicator: synthetic
24+
await expect(page.getByTestId("train-data-source")).toContainText(
25+
"synthetic");
26+
27+
// Load parquet in Data tab
28+
await clickTab(page, "data");
29+
await page.getByTestId("data-inspector").waitFor();
30+
await page.getByTestId("data-path").fill(parquet);
31+
await page.getByTestId("data-load").click();
32+
await page.getByTestId("data-metrics").waitFor({ timeout: 8_000 });
33+
34+
// Use this parquet for training
35+
await page.getByTestId("data-use-for-train").click();
36+
37+
// Indicator now shows parquet basename
38+
const indicator = page.getByTestId("train-data-source");
39+
await expect(indicator).toContainText("parquet:");
40+
await expect(indicator).toContainText("T2_gpt2_small__P1_minimal");
41+
42+
// Train
43+
await clickTab(page, "canvas");
44+
await page.getByTestId("run-pipeline-toggle").click();
45+
await page.getByTestId("run-pipeline-train").click();
46+
const modal = page.getByTestId("run-result-modal");
47+
await modal.waitFor({ timeout: 60_000 });
48+
49+
const extras = await readTrainExtras(page);
50+
// Direct DOM check on new data_source / token_count primitives.
51+
const dataSource = await page.getByTestId(
52+
"run-result-extras-train-data_source").textContent();
53+
expect(dataSource?.trim()).toBe("parquet");
54+
const tokenCount = parseInt(
55+
(await page.getByTestId(
56+
"run-result-extras-train-token_count").textContent()) ?? "0", 10);
57+
expect(tokenCount).toBeGreaterThan(0);
58+
// Weights moved
59+
expect(extras.weight_delta_norm).toBeGreaterThan(0);
60+
61+
await closeModal(page);
62+
});
63+
64+
test("V4-1 negative: no Use-for-train click → stage_train falls back to synthetic",
65+
async ({ page }) => {
66+
test.setTimeout(120_000);
67+
await gotoApp(page);
68+
await selectPreset(page, "llama3_8b");
69+
70+
// Don't click Use-for-train; just run Train.
71+
await page.getByTestId("run-pipeline-toggle").click();
72+
await page.getByTestId("run-pipeline-train").click();
73+
const modal = page.getByTestId("run-result-modal");
74+
await modal.waitFor({ timeout: 60_000 });
75+
76+
await page.getByTestId("run-result-expand-train").click();
77+
const dataSource = await page.getByTestId(
78+
"run-result-extras-train-data_source").textContent();
79+
expect(dataSource?.trim()).toBe("synthetic");
80+
await closeModal(page);
81+
});

vbgui/src/App.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ export function App(): JSX.Element {
9797
{ brick: string; param: string; value: unknown;
9898
source: "user" | "auto"; reason: string }[]
9999
>([]);
100+
// V4-1: data source for stage_train. When set, handleRunPipeline
101+
// forwards via stage_options.train.parquet_path; UI shows indicator.
102+
const [trainParquetPath, setTrainParquetPath] =
103+
useState<string | null>(null);
104+
const [trainTokenizerPath, setTrainTokenizerPath] =
105+
useState<string | null>(null);
100106

101107
const rpc = useRpc({
102108
baseUrl: (import.meta.env.VITE_BACKEND_URL as string | undefined)
@@ -275,9 +281,16 @@ export function App(): JSX.Element {
275281
const stages = mode === "smoke" ? SMOKE_STAGES
276282
: mode === "full" ? FULL_STAGES : TRAIN_STAGES;
277283
// V3-6: TopBar exposes train_num_steps; thread it via stage_options.
284+
// V4-1: forward parquet_path + tokenizer_path picked in Data/Tokenizer tabs.
278285
const stage_options: Record<string, Record<string, unknown>> = {};
279-
if (mode === "train" && typeof opts?.num_steps === "number") {
280-
stage_options.train = { num_steps: opts.num_steps };
286+
if (mode === "train") {
287+
const trainOpts: Record<string, unknown> = {};
288+
if (typeof opts?.num_steps === "number") {
289+
trainOpts.num_steps = opts.num_steps;
290+
}
291+
if (trainParquetPath) trainOpts.parquet_path = trainParquetPath;
292+
if (trainTokenizerPath) trainOpts.tokenizer_path = trainTokenizerPath;
293+
if (Object.keys(trainOpts).length > 0) stage_options.train = trainOpts;
281294
}
282295
try {
283296
const r = await rpc.call<RunReport>("pipeline.run", {
@@ -288,7 +301,7 @@ export function App(): JSX.Element {
288301
} catch (e) {
289302
setRunError(String(e));
290303
}
291-
}, [rpc]);
304+
}, [rpc, trainParquetPath, trainTokenizerPath]);
292305

293306
const handleShardingAccept = useCallback((idx: number) => {
294307
const chosen = proposals[idx];
@@ -317,6 +330,8 @@ export function App(): JSX.Element {
317330
onCompileModeChange={(m) => dispatch({ type: "sharding.set",
318331
sharding: { ...spec.sharding, compile_mode: m } })}
319332
onRunPipeline={handleRunPipeline}
333+
trainParquetPath={trainParquetPath}
334+
trainTokenizerPath={trainTokenizerPath}
320335
trainDisabled={
321336
(() => {
322337
// V3-8/V3-9: gate Train on gotcha severity. The verify RPC
@@ -395,7 +410,12 @@ export function App(): JSX.Element {
395410
<TokenizerPlayground rpc={rpc} />
396411
)}
397412
{activeTab === "data" && (
398-
<DataInspector rpc={rpc} />
413+
<DataInspector rpc={rpc}
414+
onUseForTrain={(p, t) => {
415+
setTrainParquetPath(p);
416+
if (t !== null) setTrainTokenizerPath(t);
417+
}}
418+
trainParquetPath={trainParquetPath} />
399419
)}
400420
</div>
401421
<BottomStrip state={spec} fusedRegionCount={0} />

vbgui/src/components/DataInspector.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface DataInspectorProps {
2222
rpc: RpcClient;
2323
initialPath?: string;
2424
pageSize?: number;
25+
/** V4-1: callback when user picks the loaded parquet for training.
26+
* App stores the path and forwards via stage_options.train.parquet_path. */
27+
onUseForTrain?: (parquetPath: string, tokenizerPath: string | null) => void;
28+
/** V4-1: current path App is using for training (drives button label). */
29+
trainParquetPath?: string | null;
2530
}
2631

2732
const CHANNEL_COLORS = ["#fde68a", "#bfdbfe", "#bbf7d0", "#fecaca",
@@ -56,6 +61,7 @@ interface RoundtripCheckResult {
5661

5762
export function DataInspector({
5863
rpc, initialPath = "", pageSize = 16,
64+
onUseForTrain, trainParquetPath,
5965
}: DataInspectorProps): JSX.Element {
6066
const [path, setPath] = useState(initialPath);
6167
const [offset, setOffset] = useState(0);
@@ -118,6 +124,21 @@ export function DataInspector({
118124
<button data-testid="data-load" onClick={() => load(0)}>
119125
Load
120126
</button>
127+
<button data-testid="data-use-for-train"
128+
disabled={!result || !onUseForTrain}
129+
title={trainParquetPath === path
130+
? "Currently used for training"
131+
: "Send this parquet (and tokenizer if set) to stage_train"}
132+
onClick={() => onUseForTrain?.(path,
133+
tokenizerSource || null)}
134+
style={{
135+
background: trainParquetPath === path
136+
? "#dcfce7" : undefined,
137+
color: trainParquetPath === path
138+
? "#166534" : undefined,
139+
}}>
140+
{trainParquetPath === path ? "✓ Training" : "Use for training"}
141+
</button>
121142
</header>
122143
<header style={{ display: "flex", gap: 8, alignItems: "center" }}>
123144
<span style={{ fontSize: 11, color: "#6b7280" }}>Tokenizer:</span>

vbgui/src/components/TopBar.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export interface TopBarProps {
1717
/** V3-8/V3-9: when present, Train button is rendered disabled with
1818
* reason exposed via data-testid='top-bar-train-disabled-reason'. */
1919
trainDisabled?: { reason: string } | null;
20+
/** V4-1: parquet + tokenizer paths picked in Data/Tokenizer tabs.
21+
* Drives the data-testid='train-data-source' indicator next to
22+
* Train so the user can tell whether training will use real tokens
23+
* or fall back to synthetic. */
24+
trainParquetPath?: string | null;
25+
trainTokenizerPath?: string | null;
2026
}
2127

2228
export function TopBar(p: TopBarProps): JSX.Element {
@@ -59,6 +65,17 @@ export function TopBar(p: TopBarProps): JSX.Element {
5965

6066
<MemoryBar state={p.state} />
6167

68+
<span data-testid="train-data-source"
69+
style={{ fontSize: 10,
70+
color: p.trainParquetPath ? "#16a34a" : "#9ca3af",
71+
fontFamily: "monospace" }}>
72+
{p.trainParquetPath
73+
? `parquet: ${basename(p.trainParquetPath)}` +
74+
(p.trainTokenizerPath
75+
? ` · tok: ${basename(p.trainTokenizerPath)}` : "")
76+
: "synthetic"}
77+
</span>
78+
6279
<div style={{ position: "relative" }}>
6380
<button data-testid="run-pipeline"
6481
onClick={() => p.onRunPipeline("smoke")}>
@@ -113,6 +130,11 @@ export function TopBar(p: TopBarProps): JSX.Element {
113130
);
114131
}
115132

133+
function basename(p: string): string {
134+
const slash = p.lastIndexOf("/");
135+
return slash >= 0 ? p.slice(slash + 1) : p;
136+
}
137+
116138
const menuItem: React.CSSProperties = {
117139
display: "block", padding: "6px 12px", border: "none",
118140
background: "white", cursor: "pointer", textAlign: "left", width: "100%",

vbgui/tests/DataInspector.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,54 @@ describe("DataInspector", () => {
9191
});
9292
});
9393

94+
// V4-1: Use-for-train button
95+
it("data-use-for-train disabled until parquet loaded", () => {
96+
render(<DataInspector rpc={mockClient(SAMPLE)} initialPath="/x"
97+
onUseForTrain={() => {}} />);
98+
expect(screen.getByTestId("data-use-for-train")
99+
.hasAttribute("disabled")).toBe(true);
100+
});
101+
102+
it("data-use-for-train fires onUseForTrain(path, null) after load",
103+
async () => {
104+
const onUseForTrain = vi.fn();
105+
render(<DataInspector rpc={mockClient(SAMPLE)}
106+
initialPath="/tmp/sh.parquet"
107+
onUseForTrain={onUseForTrain} />);
108+
fireEvent.click(screen.getByTestId("data-load"));
109+
await waitFor(() => {
110+
expect(screen.getByTestId("data-metrics")).toBeTruthy();
111+
});
112+
fireEvent.click(screen.getByTestId("data-use-for-train"));
113+
expect(onUseForTrain).toHaveBeenCalledWith("/tmp/sh.parquet", null);
114+
});
115+
116+
it("data-use-for-train fires onUseForTrain(path, tokenizer) when both set",
117+
async () => {
118+
const onUseForTrain = vi.fn();
119+
render(<DataInspector rpc={mockClient(SAMPLE)}
120+
initialPath="/p.parquet"
121+
onUseForTrain={onUseForTrain} />);
122+
fireEvent.click(screen.getByTestId("data-load"));
123+
await waitFor(() =>
124+
expect(screen.getByTestId("data-metrics")).toBeTruthy());
125+
fireEvent.change(screen.getByTestId("data-tokenizer-path"),
126+
{ target: { value: "/t.json" } });
127+
fireEvent.click(screen.getByTestId("data-use-for-train"));
128+
expect(onUseForTrain).toHaveBeenCalledWith("/p.parquet", "/t.json");
129+
});
130+
131+
it("data-use-for-train shows ✓ Training when this path is active", async () => {
132+
render(<DataInspector rpc={mockClient(SAMPLE)} initialPath="/p"
133+
trainParquetPath="/p"
134+
onUseForTrain={() => {}} />);
135+
fireEvent.click(screen.getByTestId("data-load"));
136+
await waitFor(() =>
137+
expect(screen.getByTestId("data-metrics")).toBeTruthy());
138+
expect(screen.getByTestId("data-use-for-train").textContent)
139+
.toContain("Training");
140+
});
141+
94142
it("renders error envelope when backend fails", async () => {
95143
const failing = new RpcClient({
96144
baseUrl: "http://x",

vbgui/tests/TopBar.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,35 @@ describe("TopBar", () => {
3434
expect(screen.getByTestId("run-pipeline")).toBeTruthy();
3535
});
3636

37+
// V4-1: train-data-source indicator
38+
it("train-data-source reads 'synthetic' when no parquet selected", () => {
39+
render(<TopBar {...defaultTopProps()} />);
40+
expect(screen.getByTestId("train-data-source").textContent)
41+
.toBe("synthetic");
42+
});
43+
44+
it("train-data-source shows parquet basename when trainParquetPath set",
45+
() => {
46+
render(<TopBar {...defaultTopProps({
47+
trainParquetPath: "/tmp/fixtures/T2_gpt2_small__P1.parquet",
48+
})} />);
49+
const indicator = screen.getByTestId("train-data-source").textContent!;
50+
expect(indicator).toContain("parquet:");
51+
expect(indicator).toContain("T2_gpt2_small__P1.parquet");
52+
expect(indicator).not.toContain("/tmp/");
53+
});
54+
55+
it("train-data-source appends tokenizer basename when both set", () => {
56+
render(<TopBar {...defaultTopProps({
57+
trainParquetPath: "/a/b/foo.parquet",
58+
trainTokenizerPath: "/x/y/cppmega_tokenizer.json",
59+
})} />);
60+
const t = screen.getByTestId("train-data-source").textContent!;
61+
expect(t).toContain("foo.parquet");
62+
expect(t).toContain("tok:");
63+
expect(t).toContain("cppmega_tokenizer.json");
64+
});
65+
3766
it("preset launcher fires onPresetDrop when chosen", () => {
3867
const onPresetDrop = vi.fn();
3968
render(<TopBar {...defaultTopProps({ onPresetDrop })} />);

0 commit comments

Comments
 (0)