|
| 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