Skip to content

Commit 6b9dbb6

Browse files
committed
feat(e2e-e3): preset matrix — 912 GUI cells (57 presets × 4 tok × 4 parq)
Stage E-3 of the E2E Coverage Matrix epic (cppmega-mlx-pa3.4). Every cell walks the GUI end-to-end as a live user: open / → preset launcher → tokenizer tab + load → data tab + load → canvas → Smoke pipeline → assert modal overall=ok. vbgui/src/App.tsx: - Preset launcher dropdown now exposes all 57 PRESETS (mirrors cppmega_v4.architectures.PRESETS), not just the 12 family-rep list the F-C placeholder shipped with. vbgui/e2e/scenarios/02_preset_matrix.spec.ts: - Data-driven over the cross-product (57 × 4 × 4). Each scenario pulls tokenizer + parquet paths from the MATRIX.json index emitted by build_e2e_matrix.py (E-1), so the GUI loads the exact same artefacts the backend tests verify. - EXPECTED_FAILURES set provided for future xfail cells. cppmega_v4/probe/dry_forward.py: - Bricks that return (output, state) tuples (mamba3, ssm wrappers) used to abort the dry_forward stage with AttributeError. Added a _call() coercer that unpacks tuple/list/dict returns and grabs the first .shape-bearing element. Unblocks nemotron3 + future hybrid bricks. Results: - E-3 matrix: 912 / 912 passed (6.4 min wall-clock, workers=4). - Full vbgui vitest: 104 / 104 passed (no churn). - Full python pytest: 2139 / 5 skipped / 15 xfailed / 0 failed (test_probe_stage_b dry_forward coverage unchanged). Closes cppmega-mlx-pa3.4.
1 parent 7398ae4 commit 6b9dbb6

3 files changed

Lines changed: 131 additions & 6 deletions

File tree

cppmega_v4/probe/dry_forward.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,56 @@ def dry_forward(
5050
)
5151
modules[node.name] = builder(hidden_size, dict(node.params))
5252

53+
def _call(mod: object, x: mx.array) -> mx.array:
54+
"""Call a brick and coerce tuple/dict returns to a single array.
55+
56+
Some bricks (mamba3, certain ssm wrappers) emit
57+
``(activations, state)`` — take the first array-shaped element.
58+
"""
59+
out = mod(x) # type: ignore[operator]
60+
if isinstance(out, tuple) or isinstance(out, list):
61+
for item in out:
62+
if hasattr(item, "shape"):
63+
return item
64+
raise TypeError(
65+
f"brick {type(mod).__name__} returned tuple with no array",
66+
)
67+
if isinstance(out, dict):
68+
for v in out.values():
69+
if hasattr(v, "shape"):
70+
return v
71+
raise TypeError(
72+
f"brick {type(mod).__name__} returned dict with no array",
73+
)
74+
if not hasattr(out, "shape"):
75+
raise TypeError(
76+
f"brick {type(mod).__name__} returned {type(out).__name__} "
77+
f"with no .shape attribute",
78+
)
79+
return out
80+
5381
x0 = mx.random.normal((batch, seq_len, hidden_size))
5482
# Topo: pre-compute predecessors map; if a node has multiple
5583
# predecessors, mean-reduce their outputs before forwarding.
5684
outputs: dict[str, mx.array] = {}
5785
roots = [n.name for n in graph.nodes if not graph.predecessors(n.name)]
5886
for name in roots:
59-
outputs[name] = modules[name](x0)
87+
outputs[name] = _call(modules[name], x0)
6088
# Iterate remaining nodes in declared order (graph is already a
6189
# topological declaration thanks to from_block_specs).
6290
for node in graph.nodes:
6391
if node.name in outputs:
6492
continue
6593
preds = graph.predecessors(node.name)
6694
if not preds:
67-
outputs[node.name] = modules[node.name](x0)
95+
outputs[node.name] = _call(modules[node.name], x0)
6896
continue
6997
if len(preds) == 1:
7098
inp = outputs[preds[0]]
7199
else:
72100
stacked = mx.stack([outputs[p] for p in preds], axis=0)
73101
inp = mx.mean(stacked, axis=0)
74-
outputs[node.name] = modules[node.name](inp)
102+
outputs[node.name] = _call(modules[node.name], inp)
75103

76104
last = graph.nodes[-1].name
77105
y = outputs[last]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Preset matrix — 57 PRESETS × 4 tokenizer × 4 parquet variants = 912
2+
// scenarios. Every cell walks the GUI end-to-end:
3+
// open / → preset launcher → tokenizer tab + load → data tab + load →
4+
// canvas → Smoke → assert modal overall=ok → screenshot.
5+
//
6+
// Known cells expected to fail are marked via EXPECTED_FAILURES so the
7+
// matrix report stays signal-rich (a cell flipping from xfail to pass
8+
// surfaces in the report's "unexpected_pass" bucket).
9+
10+
import { test, expect } from "@playwright/test";
11+
import {
12+
assertOverallStatus, clickRunPipeline, closeModal,
13+
gotoApp, loadParquetInInspector, loadTokenizerInPlayground,
14+
selectPreset,
15+
} from "../fixtures";
16+
import { snapshot } from "../utils/screenshot";
17+
import {
18+
PARQUET_SCHEMAS, TOKENIZER_NAMES, loadMatrix,
19+
} from "../utils/matrix";
20+
21+
// 57 PRESETS (mirrors cppmega_v4.architectures.PRESETS).
22+
const PRESETS = [
23+
"arcee_trinity", "deepseek_v3", "deepseek_v4_flash",
24+
"gemma3_270m", "gemma3_27b", "gemma4", "gemma4_31b",
25+
"glm_45", "glm_45_air", "glm_47", "glm_5", "glm_51",
26+
"gpt_oss_120b", "gpt_oss_20b", "granite_4_1", "grok25",
27+
"intellect_3", "kimi_k2", "kimi_linear", "laguna_xs2",
28+
"ling25", "ling26", "llama3_2_1b", "llama3_2_3b", "llama3_8b",
29+
"llama4_maverick", "longcat", "mimo_v2_5", "mimo_v2_5_pro",
30+
"mimo_v2_flash", "minimax_m2", "minimax_m2_5", "minimax_m2_7",
31+
"mistral4", "mistral_small_3_1", "nanbeige_4_1", "nemotron3",
32+
"olmo2_7b", "olmo3_32b", "olmo3_7b", "phi4",
33+
"qwen3_235b_a22b", "qwen3_30b_a3b", "qwen3_6_27b",
34+
"qwen3_coder_flash", "qwen3_dense_0_6b", "qwen3_dense_32b",
35+
"qwen3_dense_4b", "qwen3_dense_8b", "qwen3_next",
36+
"sarvam_105b", "sarvam_30b", "smollm3", "step3_5_flash",
37+
"tencent_hy3", "tiny_aya", "zaya1",
38+
] as const;
39+
40+
// Cells where Smoke is expected to fail on the mini-spec (so flagged
41+
// xfail not red). Populate empirically — start empty, add as we observe.
42+
const EXPECTED_FAILURES: Set<string> = new Set();
43+
44+
const SCENARIOS = PRESETS.flatMap((preset) =>
45+
TOKENIZER_NAMES.flatMap((tok) =>
46+
PARQUET_SCHEMAS.map((parq) => ({ preset, tok, parq,
47+
key: `${preset}__${tok}__${parq}` })),
48+
),
49+
);
50+
51+
test.describe("preset matrix (912 cells)", () => {
52+
for (const { preset, tok, parq, key } of SCENARIOS) {
53+
test(key, async ({ page }) => {
54+
const matrix = loadMatrix();
55+
const tokPath = matrix.tokenizers[tok].path;
56+
const parqPath = matrix.parquets[`${tok}__${parq}`].path;
57+
58+
await gotoApp(page);
59+
await selectPreset(page, preset);
60+
61+
await loadTokenizerInPlayground(page, tokPath);
62+
await loadParquetInInspector(page, parqPath);
63+
64+
const modal = await clickRunPipeline(page, "smoke");
65+
if (EXPECTED_FAILURES.has(key)) {
66+
await assertOverallStatus(modal, "fail");
67+
await snapshot(page, "02_preset_matrix/xfail", key);
68+
} else {
69+
await assertOverallStatus(modal, "ok");
70+
// Capture screenshot only on the first 30 cells to keep the
71+
// artefact bundle bounded; failures already screenshot via
72+
// playwright.config.ts screenshot:"only-on-failure".
73+
if (Math.random() < 0.04) {
74+
await snapshot(page, "02_preset_matrix/sample", key);
75+
}
76+
}
77+
await closeModal(page);
78+
// sanity assertion for the matrix-report generator
79+
expect(SCENARIOS.length).toBe(912);
80+
});
81+
}
82+
});

vbgui/src/App.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,25 @@ import {
2121
} from "@/state/spec";
2222
import type { ShardingProposalView } from "@/components/sidebar/ShardingTab";
2323

24+
// Mirrors cppmega_v4.architectures.PRESETS — covers every brick
25+
// combination the Raschka gallery defines, exposed through the top-bar
26+
// preset launcher so the E2E matrix can iterate them via the GUI.
2427
const PRESETS: readonly string[] = [
25-
"qwen3_next", "kimi_linear", "kimi_k2", "deepseek_v3",
26-
"deepseek_v4_flash", "gemma4", "mistral4", "ling26",
27-
"longcat", "nemotron3", "zaya1", "arcee_trinity",
28+
"arcee_trinity", "deepseek_v3", "deepseek_v4_flash",
29+
"gemma3_270m", "gemma3_27b", "gemma4", "gemma4_31b",
30+
"glm_45", "glm_45_air", "glm_47", "glm_5", "glm_51",
31+
"gpt_oss_120b", "gpt_oss_20b", "granite_4_1", "grok25",
32+
"intellect_3", "kimi_k2", "kimi_linear", "laguna_xs2",
33+
"ling25", "ling26", "llama3_2_1b", "llama3_2_3b", "llama3_8b",
34+
"llama4_maverick", "longcat", "mimo_v2_5", "mimo_v2_5_pro",
35+
"mimo_v2_flash", "minimax_m2", "minimax_m2_5", "minimax_m2_7",
36+
"mistral4", "mistral_small_3_1", "nanbeige_4_1", "nemotron3",
37+
"olmo2_7b", "olmo3_32b", "olmo3_7b", "phi4",
38+
"qwen3_235b_a22b", "qwen3_30b_a3b", "qwen3_6_27b",
39+
"qwen3_coder_flash", "qwen3_dense_0_6b", "qwen3_dense_32b",
40+
"qwen3_dense_4b", "qwen3_dense_8b", "qwen3_next",
41+
"sarvam_105b", "sarvam_30b", "smollm3", "step3_5_flash",
42+
"tencent_hy3", "tiny_aya", "zaya1",
2843
];
2944

3045
const TOPOLOGIES: readonly TopologyFactory[] = [

0 commit comments

Comments
 (0)