Skip to content

Commit 847257c

Browse files
committed
feat(v7-f55): tokenizer × preset compatibility matrix tab
Closes the F-block gap "Tokenizer compatibility matrix через UI" (cppmega-mlx-j3qa.5): the architect can see at a glance which preset × tokenizer pairs are usable, which silently produce empty streams, and which throw — without having to launch four trial train runs to discover incompat the hard way. vbgui/src/components/TokenizerMatrixTab.tsx: - 2D grid (rows = presets, cols = tokenizers from fixtures). Cells start 'idle' and drive a status pill — 'ok' / 'incompat' / 'error'. - 'Probe all cells' button runs sequential tokenizer.encode_visualize RPCs against a canonical probe text (a short Python def). - Per-cell click on an idle pill probes that cell alone. Per-cell click on a populated pill toggles an inline expand panel showing the first 10 token ids + token_count + bytes_per_token_avg. - testid contract: tokmatrix-{preset}-{tokLabel} (with data-status attribute), tokmatrix-{preset}-{tokLabel}-pill (the button), tokmatrix-{preset}-{tokLabel}-expand (the inline ids panel). vbgui/src/components/HelpIcon.tsx: - New HELP_TOPICS entry 'tokenizer_matrix' explaining what counts as incompat (empty token stream, often FIM-only fixtures against non-code probes) and why pre-validating saves the architect a training-time crash. vbgui/src/components/AppTabs.tsx: - New 'tokmatrix' top-level tab labelled "Tokenizer Matrix", placed next to the existing TokenizerPlayground. vbgui/src/App.tsx: - Tab wires TokenizerMatrixTab to the four tests/fixtures/tokenizers T1..T4 JSONs and the {llama3_8b, mistral_small_3_1} pair (matches the test_tokenizer_preset_matrix.py backend fixture). vbgui/tests/TokenizerMatrixTab.test.tsx — 6 unit tests: - Grid renders with row/cell testids. - Probe-all fires RPCs in order, all pills end 'ok'. - Per-cell probe only triggers one RPC. - token_count=0 → 'incompat' status. - RPC rejection → 'error' status. - Second pill click on a populated cell toggles the expand panel with token ids + count + bpt. vbgui/e2e/scenarios/55_tokenizer_matrix.spec.ts — Playwright visual e2e: - Real backend tokenizer.encode_visualize for 8 cells (2×4). - Initial state assertion (all 'idle'). - Probe-all click → poll until at least one cell per row is 'ok', then assert no cell remains 'idle'. - Pill click expands the inline ids panel; assert it contains 'ids:' text (real token ids visible, not a mock). 13/13 vitest (TokenizerMatrixTab + App.integration). Ref: cppmega-mlx-j3qa.5
1 parent e5c30bb commit 847257c

6 files changed

Lines changed: 427 additions & 6 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// V7-F55 visual e2e — tokenizer × preset compatibility matrix.
2+
// Click "Probe all cells" → each cell makes a real
3+
// tokenizer.encode_visualize RPC against the fixtures shipped at
4+
// tests/fixtures/tokenizers/T{1..4}_*.json. Asserts the pills land
5+
// on visible 'ok' / 'incompat' / 'error' statuses (via data-status
6+
// attribute) and the inline expand panel shows real token ids.
7+
8+
import { test, expect } from "@playwright/test";
9+
import { gotoApp } from "../fixtures";
10+
11+
test("F55: tokenizer matrix probes 8 cells with real RPC", async ({
12+
page,
13+
}) => {
14+
test.setTimeout(120_000);
15+
await gotoApp(page);
16+
await page.getByTestId("app-tab-tokmatrix").click();
17+
await expect(page.getByTestId("tokenizer-matrix-tab")).toBeVisible();
18+
19+
// 2 presets × 4 tokenizers = 8 cells. All start in 'idle'.
20+
const presets = ["llama3_8b", "mistral_small_3_1"];
21+
const toks = ["T1_cppmega_v3", "T2_gpt2_small",
22+
"T3_minimal_no_fim", "T4_fim_only"];
23+
24+
// Initial pills are all 'idle'.
25+
for (const p of presets) {
26+
for (const t of toks) {
27+
await expect(page.getByTestId(`tokmatrix-${p}-${t}`))
28+
.toHaveAttribute("data-status", "idle");
29+
}
30+
}
31+
32+
// Probe every cell.
33+
await page.getByTestId("tokmatrix-probe-all").click();
34+
35+
// Wait until at least one cell on every row settles into 'ok'.
36+
for (const p of presets) {
37+
await expect.poll(async () => {
38+
let any_ok = false;
39+
for (const t of toks) {
40+
const s = await page.getByTestId(`tokmatrix-${p}-${t}`)
41+
.getAttribute("data-status");
42+
if (s === "ok") { any_ok = true; break; }
43+
}
44+
return any_ok;
45+
}, { timeout: 60_000 }).toBe(true);
46+
}
47+
48+
// All 8 cells must have left 'idle' (either ok / incompat / error).
49+
for (const p of presets) {
50+
for (const t of toks) {
51+
const status = await page.getByTestId(`tokmatrix-${p}-${t}`)
52+
.getAttribute("data-status");
53+
expect(status, `${p}/${t} status`).not.toBe("idle");
54+
}
55+
}
56+
57+
// Click a known-ok cell pill twice to expand the inline ids panel.
58+
// First click (after probe) toggles expand.
59+
const pill = page.getByTestId(`tokmatrix-${presets[0]}-${toks[1]}-pill`);
60+
// ensure ok then expand.
61+
await expect(page.getByTestId(`tokmatrix-${presets[0]}-${toks[1]}`))
62+
.toHaveAttribute("data-status", "ok");
63+
await pill.click();
64+
await expect(page.getByTestId(
65+
`tokmatrix-${presets[0]}-${toks[1]}-expand`)).toBeVisible();
66+
await expect(page.getByTestId(
67+
`tokmatrix-${presets[0]}-${toks[1]}-expand`)).toContainText("ids:");
68+
});

vbgui/src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FlowCanvas } from "@/components/FlowCanvas";
77
import { DimEnvEditor } from "@/components/DimEnvEditor";
88
import { GalleryTab } from "@/components/GalleryTab";
99
import { SweepPanel } from "@/components/SweepPanel";
10+
import { TokenizerMatrixTab } from "@/components/TokenizerMatrixTab";
1011
import { useGalleryCache } from "@/hooks/useGalleryCache";
1112
import { Palette } from "@/components/Palette";
1213
import { Sidebar } from "@/components/Sidebar";
@@ -922,6 +923,18 @@ export function App(): JSX.Element {
922923
}}
923924
/>
924925
)}
926+
{activeTab === "tokmatrix" && (
927+
<TokenizerMatrixTab
928+
rpc={rpc}
929+
presets={["llama3_8b", "mistral_small_3_1"]}
930+
tokenizers={[
931+
"/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T1_cppmega_v3.json",
932+
"/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T2_gpt2_small.json",
933+
"/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T3_minimal_no_fim.json",
934+
"/Volumes/external/sources/cppmega.mlx/tests/fixtures/tokenizers/T4_fim_only.json",
935+
]}
936+
/>
937+
)}
925938
{activeTab === "sweep" && (
926939
<SweepPanel
927940
runner={async (H: number) => {

vbgui/src/components/AppTabs.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22
// Data vs Gallery (V7-F58 per-preset sortable report).
33

44
export type AppTab = "canvas" | "tokenizer" | "data"
5-
| "gallery" | "sweep";
5+
| "gallery" | "sweep" | "tokmatrix";
66

77
export interface AppTabsProps {
88
active: AppTab;
99
onChange: (t: AppTab) => void;
1010
}
1111

1212
const TABS: { key: AppTab; label: string }[] = [
13-
{ key: "canvas", label: "Canvas" },
14-
{ key: "tokenizer", label: "Tokenizer Playground" },
15-
{ key: "data", label: "Data Inspector" },
16-
{ key: "gallery", label: "Gallery" },
17-
{ key: "sweep", label: "Scaling Sweep" },
13+
{ key: "canvas", label: "Canvas" },
14+
{ key: "tokenizer", label: "Tokenizer Playground" },
15+
{ key: "tokmatrix", label: "Tokenizer Matrix" },
16+
{ key: "data", label: "Data Inspector" },
17+
{ key: "gallery", label: "Gallery" },
18+
{ key: "sweep", label: "Scaling Sweep" },
1819
];
1920

2021
export function AppTabs({ active, onChange }: AppTabsProps): JSX.Element {

vbgui/src/components/HelpIcon.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ export const HELP_TOPICS: Record<string, HelpTopic> = {
7272
"Attention memory scales O(B*S^2) for vanilla SDPA. The " +
7373
"GUI default S=64 keeps mini runs interactive.",
7474
},
75+
// ----- tokenizer matrix -----
76+
tokenizer_matrix: {
77+
title: "Tokenizer × preset compatibility matrix",
78+
what:
79+
"A grid showing each preset paired with each available " +
80+
"tokenizer. Cells are 'ok' (encodes cleanly), 'incompat' " +
81+
"(empty token stream — the tokenizer has no vocabulary for " +
82+
"the probe text), or 'error' (file missing / corrupt).",
83+
why:
84+
"Mixing a preset (which expects a particular vocab_size + " +
85+
"special-token contract) with the wrong tokenizer is the " +
86+
"single most common training failure mode in practice. " +
87+
"Validating the pair *before* train tells the architect " +
88+
"instantly whether their fixture is even self-consistent. " +
89+
"Click any cell to inspect the first 10 token ids it " +
90+
"produced for the canonical probe text.",
91+
example:
92+
"T1_cppmega_v3.json + llama3_8b → ok (vocab=32000 fits). " +
93+
"T4_fim_only.json + mistral_small_3_1 → incompat (no " +
94+
"non-FIM tokens emitted).",
95+
},
7596
// ----- F56b convention -----
7697
symbolic_dim_mismatch: {
7798
title: "Why nh*head_dim != H is allowed (but warned)",
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// V7-F55 — preset × tokenizer compatibility matrix.
2+
// Each cell shows a status pill driven by a real tokenizer.encode_visualize
3+
// roundtrip. Cells start in 'idle'; user clicks "Probe all" to fill
4+
// the grid (sequential, ~5-50ms each) or clicks individual cells to
5+
// fetch one. Click any populated cell to expand the inline panel with
6+
// the first 10 token ids it produced.
7+
8+
import { useState } from "react";
9+
import type { RpcClient } from "@/lib/rpc";
10+
import { HelpIcon } from "@/components/HelpIcon";
11+
12+
const PROBE_TEXT = "def hello():\n print('hello, world')\n";
13+
14+
export interface TokenizerMatrixTabProps {
15+
rpc: RpcClient | null;
16+
presets: readonly string[];
17+
tokenizers: readonly string[];
18+
}
19+
20+
type Status = "idle" | "ok" | "incompat" | "error";
21+
22+
interface CellState {
23+
status: Status;
24+
token_count?: number;
25+
bytes_per_token_avg?: number;
26+
first_token_ids?: number[];
27+
error?: string;
28+
}
29+
30+
function cellKey(preset: string, tokenizer: string): string {
31+
return `${preset}::${tokenizer}`;
32+
}
33+
34+
const PILL: Record<Status, { bg: string; fg: string; label: string }> = {
35+
idle: { bg: "#f3f4f6", fg: "#6b7280", label: "—" },
36+
ok: { bg: "#dcfce7", fg: "#166534", label: "ok" },
37+
incompat: { bg: "#fef3c7", fg: "#92400e", label: "incompat" },
38+
error: { bg: "#fee2e2", fg: "#991b1b", label: "error" },
39+
};
40+
41+
export function TokenizerMatrixTab({
42+
rpc, presets, tokenizers,
43+
}: TokenizerMatrixTabProps): JSX.Element {
44+
const [cells, setCells] = useState<Map<string, CellState>>(new Map());
45+
const [running, setRunning] = useState(false);
46+
const [expanded, setExpanded] = useState<string | null>(null);
47+
48+
async function probeOne(preset: string, tokenizer: string) {
49+
setCells((prev) => {
50+
const next = new Map(prev);
51+
next.set(cellKey(preset, tokenizer), { status: "idle" });
52+
return next;
53+
});
54+
try {
55+
if (!rpc) throw new Error("rpc unavailable");
56+
const r = await rpc.call<{
57+
tokens: { id: number }[];
58+
token_count: number;
59+
bytes_per_token_avg: number;
60+
}>("tokenizer.encode_visualize", {
61+
tokenizer_source: tokenizer,
62+
text: PROBE_TEXT,
63+
});
64+
const status: Status = r.token_count > 0 ? "ok" : "incompat";
65+
const ids = r.tokens.slice(0, 10).map((t) => t.id);
66+
setCells((prev) => {
67+
const next = new Map(prev);
68+
next.set(cellKey(preset, tokenizer), {
69+
status,
70+
token_count: r.token_count,
71+
bytes_per_token_avg: r.bytes_per_token_avg,
72+
first_token_ids: ids,
73+
});
74+
return next;
75+
});
76+
} catch (e) {
77+
setCells((prev) => {
78+
const next = new Map(prev);
79+
next.set(cellKey(preset, tokenizer), {
80+
status: "error", error: (e as Error).message,
81+
});
82+
return next;
83+
});
84+
}
85+
}
86+
87+
async function probeAll() {
88+
setRunning(true);
89+
for (const p of presets) {
90+
for (const t of tokenizers) {
91+
await probeOne(p, t);
92+
}
93+
}
94+
setRunning(false);
95+
}
96+
97+
return (
98+
<div data-testid="tokenizer-matrix-tab"
99+
style={{ padding: 12, fontFamily: "system-ui, sans-serif",
100+
fontSize: 12, overflowY: "auto", flex: 1 }}>
101+
<div style={{ display: "flex", alignItems: "center", gap: 8,
102+
marginBottom: 8 }}>
103+
<h3 style={{ margin: 0, fontSize: 14 }}>
104+
Tokenizer × preset compatibility matrix
105+
</h3>
106+
<HelpIcon topic="tokenizer_matrix" />
107+
<button data-testid="tokmatrix-probe-all"
108+
disabled={running}
109+
onClick={probeAll}
110+
style={{ padding: "4px 10px", border: "none",
111+
borderRadius: 4,
112+
background: running ? "#e5e7eb" : "#2563eb",
113+
color: running ? "#6b7280" : "white",
114+
cursor: running ? "wait" : "pointer" }}>
115+
{running ? "Probing…" : "Probe all cells"}
116+
</button>
117+
</div>
118+
<table data-testid="tokenizer-matrix-table"
119+
style={{ borderCollapse: "collapse", width: "100%" }}>
120+
<thead>
121+
<tr style={{ borderBottom: "1px solid #e5e7eb" }}>
122+
<th style={th}>preset \ tokenizer</th>
123+
{tokenizers.map((t) => (
124+
<th key={t} style={th}
125+
title={t}
126+
data-testid={`tokmatrix-col-${tokFileLabel(t)}`}>
127+
{tokFileLabel(t)}
128+
</th>
129+
))}
130+
</tr>
131+
</thead>
132+
<tbody>
133+
{presets.map((p) => (
134+
<tr key={p} data-testid={`tokmatrix-row-${p}`}
135+
style={{ borderBottom: "1px solid #f3f4f6" }}>
136+
<td style={{ ...td, fontWeight: 600 }}>{p}</td>
137+
{tokenizers.map((t) => {
138+
const key = cellKey(p, t);
139+
const c = cells.get(key) ?? { status: "idle" as Status };
140+
const pill = PILL[c.status];
141+
const isExpanded = expanded === key;
142+
const tokLabel = tokFileLabel(t);
143+
return (
144+
<td key={t} style={td}
145+
data-testid={`tokmatrix-${p}-${tokLabel}`}
146+
data-status={c.status}>
147+
<button
148+
data-testid={`tokmatrix-${p}-${tokLabel}-pill`}
149+
onClick={() => {
150+
if (c.status === "idle") probeOne(p, t);
151+
else setExpanded(isExpanded ? null : key);
152+
}}
153+
style={{ background: pill.bg, color: pill.fg,
154+
border: "none", padding: "2px 8px",
155+
borderRadius: 9999, cursor: "pointer",
156+
fontWeight: 600, fontSize: 11 }}>
157+
{pill.label}
158+
</button>
159+
{isExpanded && c.first_token_ids && (
160+
<div data-testid={`tokmatrix-${p}-${tokLabel}-expand`}
161+
style={{ marginTop: 4, fontFamily: "monospace",
162+
fontSize: 11, color: "#374151" }}>
163+
ids: [{c.first_token_ids.join(", ")}]
164+
<br />
165+
count: {c.token_count}, bpt: {c.bytes_per_token_avg?.toFixed(2)}
166+
</div>
167+
)}
168+
{isExpanded && c.error && (
169+
<div data-testid={`tokmatrix-${p}-${tokLabel}-expand-error`}
170+
style={{ marginTop: 4, color: "#991b1b",
171+
fontSize: 11 }}>
172+
{c.error}
173+
</div>
174+
)}
175+
</td>
176+
);
177+
})}
178+
</tr>
179+
))}
180+
</tbody>
181+
</table>
182+
</div>
183+
);
184+
}
185+
186+
function tokFileLabel(path: string): string {
187+
// Strip directory + extension so cell testids stay readable.
188+
const base = path.split("/").pop() ?? path;
189+
return base.replace(/\.json$/, "");
190+
}
191+
192+
const th: React.CSSProperties = {
193+
textAlign: "left", padding: "4px 6px",
194+
color: "#374151", fontWeight: 600,
195+
};
196+
const td: React.CSSProperties = { padding: "4px 6px",
197+
verticalAlign: "top" };

0 commit comments

Comments
 (0)