|
| 1 | +# VisualBuilderPlan-v3 — Deep E2E Verification (UI → API → Real Training Math) |
| 2 | + |
| 3 | +**Status**: in-progress 2026-05-21 (epic `cppmega-mlx-y7f`) |
| 4 | +**Driver**: honest audit of v2 closure (bb0). v2 declared "1188 cells, 0 hard failures" |
| 5 | +but the 5 "deep" UI→train scenarios used vacuous `expect(["ok","fail"]).toContain(status)` |
| 6 | +assertions. The status was returned by `stage_train`; nothing proved the UI mutation |
| 7 | +**affected the trained model**. v3 closes that gap. |
| 8 | + |
| 9 | +This plan supersedes v2 §8 acceptance criteria ONLY for the UI→training assertions; |
| 10 | +v2 stage delivery + 1188-cell breadth remains valid. |
| 11 | + |
| 12 | +## 1. Why v3 exists |
| 13 | + |
| 14 | +Audit on 2026-05-21 found 3 categories of unproven claims and 3 confirmed backend bugs: |
| 15 | + |
| 16 | +### Confirmed backend bugs (v2 ignored these) |
| 17 | + |
| 18 | +| ID | Bug | File | Effect | |
| 19 | +|-----|---------------------------------------------------------------|--------------------------------------------|-----------------------------------------------------------------------| |
| 20 | +| B1 | `stage_train` hardcodes `optim.AdamW` | `cppmega_v4/runner/stages.py:425` | UI OptimTab choice (Lion/Muon/Adam8bit) is silently ignored at train | |
| 21 | +| B2 | `stage_train` uses synthetic Gaussian embeddings | `cppmega_v4/runner/stages.py:447` | tokenizer + parquet selection in UI has zero effect on training input | |
| 22 | +| B3 | `RunResultModal` does not surface `extras` (losses, lr, delta)| `vbgui/src/components/RunResultModal.tsx` | UI hides training math from user; tests cannot assert against extras | |
| 23 | + |
| 24 | +### Vacuous assertions in `11_ui_to_train.spec.ts` |
| 25 | + |
| 26 | +| Test | Old assertion | Why vacuous | |
| 27 | +|--------------------------------|----------------------------------------------|------------------------------------------| |
| 28 | +| activation glu→swiglu | `expect(["ok","fail"]).toContain(status)` | passes even if activation ignored | |
| 29 | +| schedule linear_warmup w=4 | `await stage-train.waitFor()` (no content) | no check that lr_trajectory shape matches| |
| 30 | +| pre_norm rmsnorm→layernorm | `expect(["ok","fail"]).toContain(status)` | passes even if norm ignored | |
| 31 | +| Auto-group + Train | `expect(["ok","fail"]).toContain(status)` | passes even though B1 ignores optimizer | |
| 32 | +| AblationsTab Run | `rows >= 2` | passes even if all variants same loss | |
| 33 | + |
| 34 | +### Cross-cut gaps not tested at all |
| 35 | + |
| 36 | +- **Multi-step convergence**: matrix uses N=2 (proves non-NaN, not learning). |
| 37 | +- **Cross-architecture**: deep UI→train only run on `llama3_8b` (1/57 presets). |
| 38 | +- **Sharding application**: UI shows proposals, never asserted that train uses them. |
| 39 | +- **Memory match**: UI estimate vs real `mx.metal.get_peak_memory()` never compared. |
| 40 | +- **Gotcha gating**: critical gotcha should disable Train; not verified. |
| 41 | +- **Validator gating**: verify=error should block Train; not verified. |
| 42 | +- **Lifecycle**: WS reconnect mid-train, spec save/load roundtrip. |
| 43 | + |
| 44 | +## 2. Goal |
| 45 | + |
| 46 | +Every UI mutation that claims to change training behavior **must produce an observable |
| 47 | +difference in training math**, asserted from a Playwright test that reads the value |
| 48 | +out of UI-surfaced extras. |
| 49 | + |
| 50 | +Test pattern (the only one acceptable from v3 onward): |
| 51 | + |
| 52 | +```ts |
| 53 | +const { extras } = await trainResult(page); |
| 54 | + |
| 55 | +expect(extras.model_summary.mlp_activation).toBe("swiglu"); // propagated |
| 56 | +expect(extras.losses.length).toBeGreaterThanOrEqual(5); // multi-step |
| 57 | +expect(extras.losses[0]).toBeGreaterThan(extras.losses[N-1]); // learned |
| 58 | +expect(extras.weight_delta_norm).toBeGreaterThan(1e-3); // moved |
| 59 | +expect(extras.lr_trajectory[0]).toBeCloseTo(base_lr * 0.25, 4); // schedule shape |
| 60 | +expect(extras.optimizer_kind).toBe("lion"); // B1 fixed |
| 61 | +``` |
| 62 | + |
| 63 | +No more `["ok","fail"]`. Status check is a sanity gate only; the **content** is what |
| 64 | +proves the claim. |
| 65 | + |
| 66 | +## 3. Stages |
| 67 | + |
| 68 | +13 stages in epic `cppmega-mlx-y7f`, each with bd ticket. Run in dependency order; |
| 69 | +do not advance to next until current is green. |
| 70 | + |
| 71 | +| Stage | Ticket | Title | Type | Pri | Depends | |
| 72 | +|-------|-----------------|--------------------------------------------------------------------|---------|-----|---------| |
| 73 | +| V3-1 | cppmega-mlx-wly | stage_train honors OptimKind (B1 fix + extras.optimizer_kind) | bugfix | P0 | — | |
| 74 | +| V3-2 | cppmega-mlx-4pp | stage_train consumes tokenizer+parquet from spec (B2 fix) | bugfix | P0 | — | |
| 75 | +| V3-3 | cppmega-mlx-0js | stage_train extras includes model_summary | feature | P1 | V3-1 | |
| 76 | +| V3-4 | cppmega-mlx-xzp | RunResultModal surfaces extras with testids (B3 fix) | feature | P0 | — | |
| 77 | +| V3-5 | cppmega-mlx-7g9 | Rewrite 11_ui_to_train.spec.ts with strict content assertions | task | P0 | V3-1,3,4| |
| 78 | +| V3-6 | cppmega-mlx-9wj | Multi-step convergence (12_train_convergence.spec.ts) | task | P1 | V3-3,4 | |
| 79 | +| V3-7 | cppmega-mlx-bws | Ablation loss-divergence (13_ablation_math.spec.ts) | task | P1 | V3-5 | |
| 80 | +| V3-8 | cppmega-mlx-cwz | Critical gotcha disables Train + assertion | feature | P1 | V3-4 | |
| 81 | +| V3-9 | cppmega-mlx-7ut | verify=error disables Train + assertion | feature | P1 | V3-4 | |
| 82 | +| V3-10 | cppmega-mlx-e0t | Roundtrip FAIL warning, does not block train | task | P2 | V3-4 | |
| 83 | +| V3-11 | cppmega-mlx-2ar | Cross-architecture deep verify (14_cross_arch_deep.spec.ts) | task | P1 | V3-5 | |
| 84 | +| V3-12 | cppmega-mlx-1wm | Spec save/load roundtrip → identical train extras | task | P2 | V3-5 | |
| 85 | +| V3-13 | cppmega-mlx-7yd | Closure report `tests/fixtures/e2e_matrix_v3_report.md` | doc | P3 | all | |
| 86 | + |
| 87 | +Deferred (not in this epic, follow-up): |
| 88 | +- Sharding apply → real distributed train (needs multi-device, scope creep) |
| 89 | +- Memory peak comparison (requires actual `mx.metal.get_peak_memory` calls + tolerance) |
| 90 | +- WS reconnect mid-train (lifecycle, not "training math") |
| 91 | + |
| 92 | +## 4. Per-stage acceptance criteria |
| 93 | + |
| 94 | +### V3-1 stage_train honors OptimKind |
| 95 | + |
| 96 | +- `stages.py` reads `spec.optim.groups[0].kind` and instantiates corresponding optimizer |
| 97 | + from `cppmega_mlx.training.optimizers` (AdamW / Lion / Lion8bit / Adam8bit / Muon / |
| 98 | + MuonAdamWHybrid / SGD) |
| 99 | +- extras gains `optimizer_kind: str` field |
| 100 | +- pytest: 7 new tests in `tests/v4/test_stage_train_optimizers.py`, one per OptimKind, |
| 101 | + asserts loss is finite and weight_delta > 0 |
| 102 | +- regression: existing 2306+ pytest stays green |
| 103 | + |
| 104 | +### V3-2 stage_train consumes data |
| 105 | + |
| 106 | +- If `spec.data` has `tokenizer_path` + `parquet_path`, train loads first N tokens |
| 107 | + from parquet, tokenizes via the configured tokenizer, uses those token IDs as |
| 108 | + targets (and a learned `nn.Embedding(vocab, hidden)` as input embeds) |
| 109 | +- Falls back to synthetic embeds + random targets only when `spec.data` is None |
| 110 | + (backwards compat for matrix tests) |
| 111 | +- extras gains `data_source: "synthetic"|"parquet"` and `token_count: int` |
| 112 | +- pytest: 4 new tests in `tests/v4/test_stage_train_data.py` — synthetic path, |
| 113 | + parquet path, mismatched vocab raises clear error, tokenizer load failure surfaces |
| 114 | + |
| 115 | +### V3-3 model_summary in extras |
| 116 | + |
| 117 | +- `stage_train.extras["model_summary"]` is `{mlp_activation, attention_pre_norm, |
| 118 | + attention_post_norm, mlp_pre_norm, mlp_post_norm, optimizer_kind, schedule_kind, |
| 119 | + num_brick_kinds}` |
| 120 | +- Helper `_summarize_model(spec, optimizer)` in stages.py |
| 121 | +- pytest: 5 new tests asserting summary correctness for various configs |
| 122 | + |
| 123 | +### V3-4 RunResultModal extras display |
| 124 | + |
| 125 | +- For each stage with non-error extras, add a clickable disclosure row |
| 126 | +- Inside expand: data-testid'd cells for every extras key: |
| 127 | + - `run-result-extras-{stage}-{key}` for primitives |
| 128 | + - `run-result-extras-{stage}-losses-{i}` for arrays |
| 129 | + - `run-result-extras-{stage}-model-{field}` for model_summary |
| 130 | +- vitest: 6 new tests covering shape rendering |
| 131 | + |
| 132 | +### V3-5 Rewrite 11_ui_to_train.spec.ts |
| 133 | + |
| 134 | +Five scenarios, each with **at least 3 content assertions** beyond status row presence: |
| 135 | + |
| 136 | +1. **Activation glu→swiglu**: extras.model_summary.mlp_activation === "swiglu", |
| 137 | + losses[0] !== losses[-1] (learning happened with the new activation). |
| 138 | +2. **Schedule linear_warmup w=4**: extras.lr_trajectory[0] === base_lr * 0.25, |
| 139 | + extras.lr_trajectory[3] === base_lr (warmup completed), extras.schedule_kind === |
| 140 | + "linear_warmup". |
| 141 | +3. **pre_norm switch attention**: extras.model_summary.attention_pre_norm === |
| 142 | + "layernorm", weight_delta_norm > 0. |
| 143 | +4. **Auto-group + optimizer change to Lion**: extras.optimizer_kind === "lion", |
| 144 | + losses finite (B1 fix verified end-to-end). |
| 145 | +5. **Optimizer change to Muon**: extras.optimizer_kind === "muon", weight_delta > 0 |
| 146 | + (Muon != AdamW math regression). |
| 147 | + |
| 148 | +### V3-6 multi-step convergence |
| 149 | + |
| 150 | +- New test `12_train_convergence.spec.ts` |
| 151 | +- Drive N=8 step training via stage_options.train.num_steps in OptimTab |
| 152 | +- Assert `losses[0] > losses[7] * 1.05` (5% loss reduction floor) |
| 153 | +- 4 preset variants: llama3_8b, gpt2_small_124m, mistral_7b, tiny_aya_parallel |
| 154 | + |
| 155 | +### V3-7 ablation divergence |
| 156 | + |
| 157 | +- `13_ablation_math.spec.ts` |
| 158 | +- Activation axis: glu vs swiglu vs gelu, 4 steps each |
| 159 | +- Assert pairwise `|final_loss_a - final_loss_b| > 1e-3` for at least one pair |
| 160 | +- Optimizer axis: adamw vs lion → assert weight_delta_norm differs by >10% |
| 161 | + |
| 162 | +### V3-8 Critical gotcha gates Train |
| 163 | + |
| 164 | +- Force a gotcha-critical config (e.g., loss type incompatible with brick output) |
| 165 | +- UI: `top-bar-train` button becomes disabled, tooltip "critical gotcha blocks train" |
| 166 | +- Test asserts disabled state + tooltip |
| 167 | + |
| 168 | +### V3-9 verify=error gates Train |
| 169 | + |
| 170 | +- Same pattern with a validator error (e.g., parallel-block requires pre_norm) |
| 171 | +- UI: train disabled, banner shows verify error count |
| 172 | + |
| 173 | +### V3-10 roundtrip FAIL warning |
| 174 | + |
| 175 | +- Force a parquet/tokenizer roundtrip mismatch |
| 176 | +- UI shows yellow banner, but Train remains enabled (training is robust to roundtrip |
| 177 | + fail since synthetic targets bypass it) |
| 178 | +- Assert banner + train still works (status="ok") |
| 179 | + |
| 180 | +### V3-11 cross-architecture deep verify |
| 181 | + |
| 182 | +- 6 family-reps × 3 mutations = 18 scenarios in `14_cross_arch_deep.spec.ts` |
| 183 | +- Family reps: llama3_8b, mistral_7b, gpt2_small_124m, qwen3_8b, gemma2_2b, deepseek_v3 |
| 184 | +- Mutations: activation, schedule, optimizer |
| 185 | +- Assert same strict content as V3-5 |
| 186 | + |
| 187 | +### V3-12 save/load roundtrip |
| 188 | + |
| 189 | +- Build spec in UI → Save → Load → Train → assert extras identical to first run |
| 190 | +- Tests load → save → diff JSON should be empty |
| 191 | + |
| 192 | +### V3-13 Closure report |
| 193 | + |
| 194 | +- ≤150 lines markdown report mirroring v2 layout |
| 195 | +- Stage table with commits, before/after assertion counts, regression totals |
| 196 | + |
| 197 | +## 5. Workflow per stage (Goal directive) |
| 198 | + |
| 199 | +1. `bd update <id> --claim` → in_progress |
| 200 | +2. Implement (smallest atomic change) |
| 201 | +3. Code review pass (Self-review via diff; gsd-code-reviewer only if >100 LOC or |
| 202 | + touches >3 files) |
| 203 | +4. Perf check (no >5% regression in pytest wall-clock for affected suite) |
| 204 | +5. Regression (pytest + vitest + playwright affected files) |
| 205 | +6. `git add` specific files (no `-A`) → commit → push to origin/main |
| 206 | +7. `bd close <id>` only after push verified |
| 207 | + |
| 208 | +## 6. Pre-existing red (excluded from regression gates) |
| 209 | + |
| 210 | +- `tests/v4/test_path_d_runtime_adapter.py::test_gdn_chunk_o_metal_threadgroup_memory_fits_device_limit` |
| 211 | + (61440 > 32K limit, parallel agent's WIP in `path_c_fusion.py`, not owned by this work) |
| 212 | + |
| 213 | +## 7. Done definition |
| 214 | + |
| 215 | +All 13 V3 tickets closed. Playwright `11/12/13/14_*.spec.ts` green with strict |
| 216 | +assertions (no `["ok","fail"]` in `vbgui/e2e/scenarios/1[1-4]_*.spec.ts`). |
| 217 | +Closure doc at `tests/fixtures/e2e_matrix_v3_report.md`. |
0 commit comments