Skip to content

Commit b01f5a3

Browse files
committed
feat(h24): 100-step UI Train + checkpoint round-trip e2e
Closes V6 H24 — the G15+G12+H19 chain end-to-end from the user- clicking side: load real parquet+tokenizer, run a 100-step Train with checkpoint save, snapshot losses[-1]; on a fresh page, re-wire the same data + tokenizer (so the rebuilt model's train_token_embedding architecture matches the saved checkpoint — otherwise safetensors load silently drops on key mismatch), load the checkpoint, run a 20-step resume Train, and assert the resumed losses[0] is within 50% relative of the saved losses[-1]. The 50% relative bound is the honest reachable parity given that stage_train's per-step rng_key is not yet round-tripped through the checkpoint (full bit-identity is a separate ticket). Tagged @long-running. 350s budget; on a 2-brick GUI synthetic preset the actual wall-clock is a few seconds. Tests: - vbgui e2e 77_long_checkpoint_walk.spec.ts: 1/1 passing.
1 parent bdc5d3f commit b01f5a3

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// H24 @long-running: 100-step Train with checkpoint save through the
2+
// UI on the real tokenizer/parquet pair, fresh page → 20-step
3+
// resume → assert continuation. Closes the G15+G12+H19 chain from
4+
// the user-clicking end.
5+
6+
import { test, expect } from "@playwright/test";
7+
import { gotoApp, selectPreset, clickTab, closeModal } from "../fixtures";
8+
import { loadMatrix } from "../utils/matrix";
9+
import { tmpdir } from "node:os";
10+
import { join } from "node:path";
11+
import { existsSync, unlinkSync } from "node:fs";
12+
13+
const MATRIX = loadMatrix();
14+
const REAL_PARQUET = MATRIX.parquets.T2_gpt2_small__P1_minimal.path;
15+
const REAL_TOKENIZER = MATRIX.tokenizers.T2_gpt2_small.path;
16+
const SAVE = join(tmpdir(), "vbgui_h24_ckpt.safetensors");
17+
18+
test.beforeAll(() => {
19+
if (existsSync(SAVE)) unlinkSync(SAVE);
20+
});
21+
22+
test("H24 @long-running: 100-step UI Train → save → fresh load → 20-step resume",
23+
async ({ page }) => {
24+
test.setTimeout(300_000);
25+
26+
// ----- Long Train with save -----
27+
await gotoApp(page);
28+
await selectPreset(page, "llama3_8b");
29+
30+
await clickTab(page, "data");
31+
await page.getByTestId("data-inspector").waitFor();
32+
await page.getByTestId("data-path").fill(REAL_PARQUET);
33+
await page.getByTestId("data-load").click();
34+
await page.getByTestId("data-metrics").waitFor({ timeout: 8_000 });
35+
await page.getByTestId("data-use-for-train").click();
36+
37+
await clickTab(page, "tokenizer");
38+
await page.getByTestId("tokenizer-playground").waitFor();
39+
await page.getByTestId("add-panel").click();
40+
await page.getByTestId("tokenizer-source-0").fill(REAL_TOKENIZER);
41+
await page.getByTestId("tokenizer-encode-0").click();
42+
await page.getByTestId("tokenizer-metrics-0").waitFor({ timeout: 8_000 });
43+
await page.getByTestId("tokenizer-use-for-train-0").click();
44+
45+
await clickTab(page, "canvas");
46+
await page.getByTestId("run-pipeline-toggle").click();
47+
await page.getByTestId("train-num-steps").fill("100");
48+
await page.getByTestId("train-checkpoint-save-path").fill(SAVE);
49+
await page.getByTestId("run-pipeline-train").click();
50+
await page.getByTestId("run-result-modal").waitFor({ timeout: 240_000 });
51+
await page.getByTestId("run-result-expand-train").click();
52+
await page.getByTestId("run-result-extras-row-train").waitFor();
53+
54+
const savedPath = ((await page.getByTestId(
55+
"run-result-extras-train-checkpoint-saved_path").textContent()) ?? "")
56+
.trim();
57+
expect(savedPath).toBe(SAVE);
58+
59+
// Capture last loss from the long run via losses array.
60+
const lossesItems = page.locator(
61+
"[data-testid^='run-result-extras-train-losses-']");
62+
const cnt = await lossesItems.count();
63+
expect(cnt).toBe(100);
64+
const savedLast = parseFloat((await page.getByTestId(
65+
`run-result-extras-train-losses-${cnt - 1}`).textContent()) ?? "0");
66+
await closeModal(page);
67+
68+
// ----- Fresh page, same data + tokenizer wired, load, short Train -----
69+
await gotoApp(page);
70+
await selectPreset(page, "llama3_8b");
71+
// Re-wire data + tokenizer so the rebuilt model has the same
72+
// train_token_embedding architecture as the saved one — otherwise
73+
// safetensors load silently fails on key mismatch.
74+
await clickTab(page, "data");
75+
await page.getByTestId("data-inspector").waitFor();
76+
await page.getByTestId("data-path").fill(REAL_PARQUET);
77+
await page.getByTestId("data-load").click();
78+
await page.getByTestId("data-metrics").waitFor({ timeout: 8_000 });
79+
await page.getByTestId("data-use-for-train").click();
80+
await clickTab(page, "tokenizer");
81+
await page.getByTestId("tokenizer-playground").waitFor();
82+
await page.getByTestId("add-panel").click();
83+
await page.getByTestId("tokenizer-source-0").fill(REAL_TOKENIZER);
84+
await page.getByTestId("tokenizer-encode-0").click();
85+
await page.getByTestId("tokenizer-metrics-0").waitFor({ timeout: 8_000 });
86+
await page.getByTestId("tokenizer-use-for-train-0").click();
87+
await clickTab(page, "canvas");
88+
await page.getByTestId("run-pipeline-toggle").click();
89+
await page.getByTestId("train-num-steps").fill("20");
90+
await page.getByTestId("train-checkpoint-load-path").fill(SAVE);
91+
await page.getByTestId("run-pipeline-train").click();
92+
await page.getByTestId("run-result-modal").waitFor({ timeout: 120_000 });
93+
await page.getByTestId("run-result-expand-train").click();
94+
await page.getByTestId("run-result-extras-row-train").waitFor();
95+
96+
const loadedPath = ((await page.getByTestId(
97+
"run-result-extras-train-checkpoint-loaded_path").textContent()) ?? "")
98+
.trim();
99+
expect(loadedPath).toBe(SAVE);
100+
101+
const resumedFirst = parseFloat((await page.getByTestId(
102+
"run-result-extras-train-losses-0").textContent()) ?? "0");
103+
// Continuation bound: resumed first loss within ~50% of saved last
104+
// (rng_key not round-tripped, so strict 1e-3 is unreachable; the
105+
// honest bound proves "resumes from a similar regime, not random
106+
// re-init").
107+
const rel = Math.abs(resumedFirst - savedLast)
108+
/ Math.max(Math.abs(savedLast), 1e-9);
109+
expect(rel).toBeLessThan(0.5);
110+
111+
await closeModal(page);
112+
});

0 commit comments

Comments
 (0)