Skip to content

Commit f93fc58

Browse files
committed
test(v4-4): real-data convergence (full UI parquet+tokenizer → train)
Closes V4-4 / cppmega-mlx-jvt. Backend tokenize helper extended to recognise 'text', 'original_text', 'raw_text' columns so the existing E-1 matrix fixtures (which use 'original_text') feed straight through. New spec 18_real_data_convergence.spec.ts (2 cells, llama3_8b + mistral_small_3_1): - clickTab(data) → Load fixture parquet → Use-for-train - clickTab(tokenizer) → add-panel → Use-for-train - Train N=8 steps - Asserts extras.data_source === 'parquet_tokenized' (proves V4-2 tokenize path activated, not V3-2 raw-int fallback) - Asserts tokenizer_used contains '.json' (real basename) - Asserts 8 steps executed with finite losses - Convergence floor: last < first OR tailAvg < headAvg (real tokens give learning signal that synthetic doesn't) - weight_delta_norm > 1e-4 V3-6 used synthetic random targets; V4-4 is the real-corpus version. Regression: 5/5 V4-2 pytest still green with the 'original_text' alias.
1 parent 73b4715 commit f93fc58

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

cppmega_v4/runner/stages.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,11 +576,13 @@ def _tokenize_parquet_text(
576576
from tokenizers import Tokenizer
577577
from pathlib import Path
578578
table = pq.read_table(parquet_path)
579-
if "text" not in table.column_names:
579+
text_col = next((c for c in ("text", "original_text", "raw_text")
580+
if c in table.column_names), None)
581+
if text_col is None:
580582
return [], None
581583
tok = Tokenizer.from_file(str(tokenizer_path))
582584
out: list[int] = []
583-
col = table.column("text")
585+
col = table.column(text_col)
584586
for chunk in col.chunks:
585587
for cell in chunk.to_pylist():
586588
if cell is None:
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// V4-4: Real-data convergence — pick fixture parquet+tokenizer via
2+
// UI, set 8 steps, assert tokenized path activated AND losses fell.
3+
// Closes G10 from the V4 audit: V3-6 convergence used synthetic
4+
// random targets; this is the real-corpus version.
5+
6+
import { test, expect } from "@playwright/test";
7+
import { gotoApp, selectPreset, clickTab, closeModal } from "../fixtures";
8+
import { loadMatrix } from "../utils/matrix";
9+
import { readTrainExtras } from "../utils/train_extras";
10+
11+
const MATRIX = loadMatrix();
12+
13+
// Tokenizer + parquet pair from the existing E-1 matrix fixture
14+
// (parquet has an 'original_text' column the V4-2 helper recognises).
15+
const REAL_PARQUET = MATRIX.parquets.T2_gpt2_small__P1_minimal.path;
16+
const REAL_TOKENIZER = MATRIX.tokenizers.T2_gpt2_small.path;
17+
18+
// Deep presets that produced loss-fall in V3-6 synthetic test.
19+
const CONVERGENCE_PRESETS = ["llama3_8b", "mistral_small_3_1"];
20+
21+
for (const preset of CONVERGENCE_PRESETS) {
22+
test(`V4-4: real-data convergence on ${preset}`, async ({ page }) => {
23+
test.setTimeout(150_000);
24+
await gotoApp(page);
25+
await selectPreset(page, preset);
26+
27+
// Wire parquet
28+
await clickTab(page, "data");
29+
await page.getByTestId("data-inspector").waitFor();
30+
await page.getByTestId("data-path").fill(REAL_PARQUET);
31+
await page.getByTestId("data-load").click();
32+
await page.getByTestId("data-metrics").waitFor({ timeout: 8_000 });
33+
await page.getByTestId("data-use-for-train").click();
34+
35+
// Wire tokenizer
36+
await clickTab(page, "tokenizer");
37+
await page.getByTestId("tokenizer-playground").waitFor();
38+
await page.getByTestId("add-panel").click();
39+
await page.getByTestId("tokenizer-source-0").fill(REAL_TOKENIZER);
40+
await page.getByTestId("tokenizer-encode-0").click();
41+
await page.getByTestId("tokenizer-metrics-0").waitFor({ timeout: 8_000 });
42+
await page.getByTestId("tokenizer-use-for-train-0").click();
43+
44+
// Train N=8
45+
await clickTab(page, "canvas");
46+
await page.getByTestId("run-pipeline-toggle").click();
47+
await page.getByTestId("train-num-steps").fill("8");
48+
await page.getByTestId("run-pipeline-train").click();
49+
const modal = page.getByTestId("run-result-modal");
50+
await modal.waitFor({ timeout: 90_000 });
51+
52+
const extras = await readTrainExtras(page);
53+
54+
// Real-data path activated
55+
const dataSource = await page.getByTestId(
56+
"run-result-extras-train-data_source").textContent();
57+
expect(dataSource?.trim()).toBe("parquet_tokenized");
58+
59+
// tokenizer_used reports a path basename (not 'null')
60+
const tokUsed = await page.getByTestId(
61+
"run-result-extras-train-tokenizer_used").textContent();
62+
expect(tokUsed?.trim()).toContain(".json");
63+
64+
// 8 steps actually executed
65+
expect(extras.num_steps).toBe(8);
66+
expect(extras.losses.length).toBe(8);
67+
expect(extras.losses.every(l => Number.isFinite(l))).toBe(true);
68+
69+
// Convergence floor: at least one of {strict last<first,
70+
// tailAvg<headAvg} must hold. Real tokens give signal; if neither
71+
// direction holds the loss kernel is broken.
72+
const first = extras.losses[0];
73+
const last = extras.losses[extras.losses.length - 1];
74+
const head = extras.losses.slice(0, 3);
75+
const tail = extras.losses.slice(-3);
76+
const headAvg = head.reduce((a, b) => a + b, 0) / head.length;
77+
const tailAvg = tail.reduce((a, b) => a + b, 0) / tail.length;
78+
expect(last < first || tailAvg < headAvg).toBe(true);
79+
80+
// Weights actually moved
81+
expect(extras.weight_delta_norm).toBeGreaterThan(1e-4);
82+
83+
await closeModal(page);
84+
});
85+
}

0 commit comments

Comments
 (0)