|
| 1 | +# VisualBuilderPlan-v7 — UI → API → Training End-to-End Closure |
| 2 | + |
| 3 | +**Status**: planned 2026-05-23 (epic `cppmega-mlx-v7-q`) |
| 4 | + |
| 5 | +**Driver**: 8-lane parallel audit (Lanes 1–8: Gallery, Canvas, Tokenizer, Data, |
| 6 | +Training, Checkpoints, Optim, Distributed) against the operator promise from |
| 7 | +the Raschka LLM gallery walkthrough. Audit report at |
| 8 | +`docs/UI-TO-TRAIN-AUDIT-2026-05-23.md`. Found 5 production-blocking gaps + 3 |
| 9 | +polish + 2 coverage; this plan closes them. |
| 10 | + |
| 11 | +## bd ticket ID mapping (post-creation) |
| 12 | + |
| 13 | +| Q# | bd id | Title | |
| 14 | +|------|-----------------|-------------------------------------------------------| |
| 15 | +| Q01 | cppmega-mlx-zcbs | DataInspector file picker (P0 BLOCKING, Lane 4) | |
| 16 | +| Q02 | cppmega-mlx-ofd2 | Edge validation in FlowCanvas (P0 BLOCKING, Lane 2) | |
| 17 | +| Q03 | cppmega-mlx-9avx | Checkpoint history UI + compress/strict (P1, Lane 6) | |
| 18 | +| Q04 | cppmega-mlx-2d47 | Close 5 Raschka gallery preset gaps (P1, Lane 1) | |
| 19 | +| Q05 | cppmega-mlx-bfbi | smoke_zero1 CLI implementation (P1, Lane 8) | |
| 20 | +| Q06 | cppmega-mlx-sbz8 | Train UI polish — B/S + nesting + plasticity (P2) | |
| 21 | +| Q07 | cppmega-mlx-x4vd | E2E coverage gaps — extras + edge + adapter (P2) | |
| 22 | +| Q08 | cppmega-mlx-bpge | Late warnings + multi-node receipt (P3) | |
| 23 | + |
| 24 | +V7-Q is intentionally smaller than V5/V6 (8 epics, ~24 sub-tickets vs V6's |
| 25 | +25 gaps / 190 sub-tickets) because the closure surface is narrower — |
| 26 | +V5/V6 wired the *math*; V7-Q wires the *operator UX* on top. |
| 27 | + |
| 28 | +## 1. Why v7-Q exists |
| 29 | + |
| 30 | +V6 closed the backend-ready → UI-wired → real-math triangle. The honest |
| 31 | +audit pass that followed (`docs/UI-TO-TRAIN-AUDIT-2026-05-23.md`) walked |
| 32 | +the Raschka-gallery operator promise step-by-step and found that the |
| 33 | +*math* paths work end-to-end, but several *operator surfaces* are stub |
| 34 | +or text-only: |
| 35 | + |
| 36 | +### Block A — File system access (P0 BLOCKING, 1 gap) |
| 37 | +The promise "user uploads a parquet file (or picks one from the |
| 38 | +matrix)" is half-shipped: matrix-pick works, upload doesn't. |
| 39 | + |
| 40 | +| Q# | Audit lane | UI work needed | |
| 41 | +|------|-----------|----------------------------------------------------------| |
| 42 | +| Q01 | Lane 4 | `<input type="file">` + `POST /upload/parquet` endpoint | |
| 43 | + |
| 44 | +### Block B — Compatibility-at-edit (P0 BLOCKING, 1 gap) |
| 45 | +`FlowCanvas` accepts `isValidConnection?: IsValidConnection` (line 20) |
| 46 | +but `App.tsx:1215` never passes it. Incompatible drops succeed silently |
| 47 | +and fail at server verify. Client-side rejection contract exists in the |
| 48 | +React Flow prop but is unused. |
| 49 | + |
| 50 | +| Q# | Audit lane | UI work needed | |
| 51 | +|------|-----------|----------------------------------------------------------| |
| 52 | +| Q02 | Lane 2 | Pass `isValidConnection` from App, derive set from `catalog.list_options('compatible_edges')` | |
| 53 | + |
| 54 | +### Block C — Operator ergonomics (P1, 3 gaps) |
| 55 | +Backend supports save/load with arch-hash, compression, strict modes, |
| 56 | +mid-run trigger; UI surfaces only the two raw text paths. 5 of 71 |
| 57 | +Raschka entries have bricks but no preset factory. `smoke_zero1` CLI |
| 58 | +documented but unimplemented. |
| 59 | + |
| 60 | +| Q# | Audit lane | UI/CLI work needed | |
| 61 | +|------|-----------|-----------------------------------------------------------| |
| 62 | +| Q03 | Lane 6 | `ckpt.list_history` RPC + dropdown + 3 toggles + V7-H05 WS | |
| 63 | +| Q04 | Lane 1 | 4 preset factories: abs_pos_embed / mlstm / per_layer_embed; tiny_aya wire-in | |
| 64 | +| Q05 | Lane 8 | `cppmega_mlx.cli.smoke_zero1` + mlx.launch wrapper + receipt regression | |
| 65 | + |
| 66 | +### Block D — UX polish (P2, 2 gaps) |
| 67 | +B and S not exposed as train controls. `loss_scaler.overflow_steps` |
| 68 | +nests under `loss_scaler` dict in backend but UI reads flat |
| 69 | +`extras.loss_scaler_overflows` — silent mismatch, overflow markers |
| 70 | +don't render. Plasticity / MTP / IFIM / MHC extras flow through but |
| 71 | +have no styled panel. |
| 72 | + |
| 73 | +| Q# | Audit lane | Work | |
| 74 | +|------|-----------|-----------------------------------------------------------| |
| 75 | +| Q06 | Lane 5 | B + S inputs; fix nesting at `stages.py:2245`; add 4 styled sub-panels | |
| 76 | + |
| 77 | +### Block E — Coverage gaps (P2, 1 gap) |
| 78 | +`readTrainExtras` reads 7/16 promised keys; no e2e for manual edge |
| 79 | +connect or GotchasTab adapter suggestions. |
| 80 | + |
| 81 | +| Q# | Audit lane | Work | |
| 82 | +|------|-----------|-----------------------------------------------------------| |
| 83 | +| Q07 | Lane 2+5 | Extend readTrainExtras + 2 new Playwright specs | |
| 84 | + |
| 85 | +### Block F — Defer / external blocker (P3, 1 gap) |
| 86 | +Tokenizer compatibility check at preview time, peak-RSS regression at |
| 87 | +real >1 GiB checkpoint, multi-node throughput receipt. The last one is |
| 88 | +**BLOCKED** on peer-48 hardware (external). |
| 89 | + |
| 90 | +| Q# | Audit lane | Work | |
| 91 | +|------|-----------|-----------------------------------------------------------| |
| 92 | +| Q08 | Lane 4+6+8 | tokenizer-at-preview, >1GB peak-RSS test, multi-node note | |
| 93 | + |
| 94 | +## 2. Goal |
| 95 | + |
| 96 | +After v7-Q every step of the operator promise from |
| 97 | +`docs/UI-TO-TRAIN-AUDIT-2026-05-23.md §"happy path"` works without |
| 98 | +copy-paste of paths, without silent shape-contract failures, and with |
| 99 | +71/71 Raschka gallery coverage. The honest audit is re-runnable and |
| 100 | +returns zero P0/P1 gaps. |
| 101 | + |
| 102 | +## 3. Stages — 8 Q-epics × 3–5 sub-tasks ≈ 24 sub-tickets |
| 103 | + |
| 104 | +``` |
| 105 | +cppmega-mlx-v7-q (epic) |
| 106 | +├── Q01 file picker (P0, 3 sub) |
| 107 | +├── Q02 edge validation (P0, 3 sub) |
| 108 | +├── Q03 ckpt history (P1, 4 sub) |
| 109 | +├── Q04 5 gallery presets (P1, 4 sub) |
| 110 | +├── Q05 smoke_zero1 CLI (P1, 3 sub) |
| 111 | +├── Q06 train UI polish (P2, 3 sub) |
| 112 | +├── Q07 e2e coverage (P2, 3 sub) |
| 113 | +└── Q08 late warnings (P3, 3 sub) |
| 114 | +
|
| 115 | +Total: ~24 sub-tickets (much smaller than V6 because closure surface |
| 116 | +is narrow — V7-Q is the "polish on top of V6's wiring" pass) |
| 117 | +``` |
| 118 | + |
| 119 | +## 4. Per-gap acceptance + sub-task breakdown |
| 120 | + |
| 121 | +### Q01 — DataInspector file picker (P0 BLOCKING, 3) |
| 122 | +- Q01.1 Backend `POST /upload/parquet` endpoint in `cppmega_v4/jsonrpc/server.py` with size-cap 2 GiB, suffix-validate, write to `${VBGUI_UPLOAD_DIR:-/tmp/vbgui_uploads}/<uuid>.parquet`, return `{"path": str}`. TTL cleanup 24 h. |
| 123 | +- Q01.2 UI: `<input type="file" accept=".parquet">` in `DataInspector.tsx:194-198` with testid `data-inspector-file-upload`. On change: `FileReader.readAsArrayBuffer` → POST → set text field to returned path. |
| 124 | +- Q01.3 Tests: vitest mock fetch + path-set; pytest `TestClient` POST a 1 KiB synthetic parquet; Playwright `page.setInputFiles` on the testid, assert preview RPC fires with the returned path. |
| 125 | + |
| 126 | +### Q02 — Edge validation in FlowCanvas (P0 BLOCKING, 3) |
| 127 | +- Q02.1 Backend `catalog.list_options("compatible_edges")` returns `[{src_kind, dst_kind}]` derived from `cppmega_v4/buildspec/shape_contract.SHAPE_CONTRACTS`. |
| 128 | +- Q02.2 UI: `App.tsx` on preset/brick changes pulls compatible-edges set once; closures `validateEdge({source, target})` → `contractSet.has(`${srcKind}→${dstKind}`)`. Pass `isValidConnection={validateEdge}` to FlowCanvas at line 1215. |
| 129 | +- Q02.3 Tests: vitest renders FlowCanvas with `isValidConnection=()=>false`, attempts onConnect, asserts edge not added; pytest catalog endpoint coverage for all 25 bricks; Playwright drops `attention` + `embed_lookup`, attempts connect, asserts no React Flow edge node appears. |
| 130 | + |
| 131 | +### Q03 — Checkpoint history UI + compress/strict (P1, 4) |
| 132 | +- Q03.1 RPC `ckpt.list_history(directory: str = ".") → list[CkptEntry]`. Scans recursively for `*.safetensors`, reads metadata via `read_ckpt_metadata` (header-only, no full load), sorts by mtime desc, caps at 100. CkptEntry: `{path, mtime, size_bytes, arch_hash, opt_kind, global_step, has_opt_sidecar}`. |
| 133 | +- Q03.2 UI: new `CheckpointHistoryDropdown.tsx` component in `vbgui/src/components/`, wire into `TopBar.tsx` next to ckpt path input. Each row: `<basename> · arch_hash[:8] · step N · mtime`. Click → fills `ckpt_load_path` text field. |
| 134 | +- Q03.3 UI: `TrainOptionsPanel.tsx` adds 3 controls — `compress` dropdown (none / weights-int8 / opt-fp16 / both), `ckpt_strict` checkbox, `opt_state_strict` checkbox. Forward via `stage_options.train`. |
| 135 | +- Q03.4 V7-H05 mid-run trigger: backend adds `_TRIGGER_CHECKPOINT_TOKENS` set + WS message handler `ckpt.trigger {token, path}` → train loop checks each step → saves via existing `_save_compressed` + sidecar. UI: existing `TrainLiveControls` button's `onScheduleCheckpoint` callback rewired to send the WS message. |
| 136 | + |
| 137 | +### Q04 — 5 Raschka gallery preset gaps (P1, 4) |
| 138 | +- Q04.1 `gpt2_xl_specs(...)` in `cppmega_v4/architectures/presets.py` using `abs_pos_embed` brick. Closes Raschka #1. |
| 139 | +- Q04.2 Wire existing `tiny_aya_parallel_specs()` into `PRESETS` dict with key `"tiny_aya"`. Closes Raschka #44. |
| 140 | +- Q04.3 `xlstm_7b_specs(...)` using `mlstm` brick. Closes Raschka #50. |
| 141 | +- Q04.4 `gemma_4_e2b_specs(...)` + `gemma_4_e4b_specs(...)` using `per_layer_embed` brick. Closes Raschka #57+58. Remove 5 xfail markers from `tests/v4/test_galcov_stage_d.py:204-211`. Acceptance: 71/71 (100%). |
| 142 | + |
| 143 | +### Q05 — smoke_zero1 CLI (P1, 3) |
| 144 | +- Q05.1 `cppmega_mlx/cli/smoke_zero1.py` entrypoint with argparse `--hosts H1,H2,...`, `--world-size W`, `--num-steps N`, `--out path`. Wraps `mlx.launch -n W --hosts H1,H2,...` around the `bench/bench_zero1_loopback` payload. |
| 145 | +- Q05.2 Each rank runs ZeRO-1 wrapper over a tiny MLP; captures per-rank loss + `gradient_reduce_ms`. Rank 0 collects + writes receipt JSON `{world_size, hosts, losses, reduce_ms, parity_max_delta}`. |
| 146 | +- Q05.3 Regression: receipt regeneratable via `python -m cppmega_mlx.cli.smoke_zero1 --hosts 127.0.0.1,127.0.0.1` matches existing `bench/baselines/zero1_loopback_2proc_m4.json` within tolerances. pytest hook for loopback receipt. |
| 147 | + |
| 148 | +### Q06 — Train UI polish (P2, 3) |
| 149 | +- Q06.1 `TrainOptionsPanel.tsx` adds `train-opt-B` (number 1-32) and `train-opt-S` (number 1-8192) inputs. App overrides `spec.dim_env.B/S` before `pipeline.run`. |
| 150 | +- Q06.2 Fix nesting at `cppmega_v4/runner/stages.py:2245`: also flatten `extras["loss_scaler_overflows"] = loss_scaler_overflow_steps` (keep nested `loss_scaler.overflow_steps` for back-compat). vitest pins both. |
| 151 | +- Q06.3 `TrainExtrasOverlay.tsx`: add conditional sub-panels — `PlasticityPanel` (FIRE/DASH/ReDo), `MTPPanel` (k-head losses), `IFIMPanel` (instr-FIM loss), `MHCPanel` (multi-head consistency). Each renders only when its key present in extras. |
| 152 | + |
| 153 | +### Q07 — E2E coverage (P2, 3) |
| 154 | +- Q07.1 Extend `vbgui/e2e/utils/train_extras.ts:readTrainExtras` to all 16 promised keys (losses, losses_smoothed, val_losses, lr_trajectory, perplexity, bpb, dtype_state, fp8_active, fim_active, sharding_applied, side_channels_observed, per_brick_grad_norms, moe.*, optimizer_kind, gradient_reduce_ms, weight_delta_norm). `12_train_convergence.spec.ts` asserts all 16 present + finite where applicable. |
| 155 | +- Q07.2 New `vbgui/e2e/scenarios/74_manual_edge_connect.spec.ts`: drop two compatible bricks, manually draw edge via React Flow `connectionLine`, verify edge appears, assert verify RPC accepts. |
| 156 | +- Q07.3 New `vbgui/e2e/scenarios/75_adapter_suggestions.spec.ts`: load preset with intentional shape mismatch, navigate to GotchasTab, click "Suggest adapters", assert chain renders and "Apply" inserts adapter nodes on canvas. |
| 157 | + |
| 158 | +### Q08 — Late warnings + perf + external blocker note (P3, 3) |
| 159 | +- Q08.1 Extend `data.preview_parquet` RPC: accept optional `tokenizer_source` arg → returns `roundtrip_pass_rate: float` field → DataInspector shows badge inline with preview, not waiting until train. |
| 160 | +- Q08.2 pytest `test_checkpoint_streaming_peak_rss.py` generates 1.5 GiB synthetic checkpoint → `load_auto` → asserts `mx.metal.get_peak_memory()` < 200 MiB during load. |
| 161 | +- Q08.3 Multi-node throughput receipt: file a `BLOCKED` note in bd ticket — peer-48 TB5 + macOS 26.2 + M4 family hardware required (per `docs/multimac_training.md:114-129`). Carry hardware-blocker notes; not implementable without external resource. |
| 162 | + |
| 163 | +## 5. Workflow per gap (same as v5/v6) |
| 164 | + |
| 165 | +1. `bd update <id> --claim` |
| 166 | +2. Implement (each sub-task atomic) |
| 167 | +3. Code review (self if <100 LOC, gsd-code-reviewer otherwise) |
| 168 | +4. Perf check (no >5% regression on affected suite) |
| 169 | +5. Regression (pytest + vitest + e2e affected) |
| 170 | +6. `git add` specific files → commit → push → verify on origin/main |
| 171 | +7. `bd close <id>` only after push verified |
| 172 | + |
| 173 | +## 6. Acceptance counters |
| 174 | + |
| 175 | +| Surface | Before v7-Q | Target v7-Q | |
| 176 | +|----------------------------------------|-------------|-------------| |
| 177 | +| pytest | ~2540 | ≥2570 | |
| 178 | +| vitest | ~428 | ≥440 | |
| 179 | +| Playwright deep e2e cells | ~1170 | ≥1175 | |
| 180 | +| Raschka gallery coverage | 66/71 (93%) | 71/71 (100%) | |
| 181 | +| extras keys read by e2e | 7/16 | 16/16 | |
| 182 | +| P0 audit gaps | 2 | 0 | |
| 183 | +| P1 audit gaps | 3 | 0 | |
| 184 | +| Hardware-blocked gaps | 1 | 1 (external) | |
| 185 | + |
| 186 | +## 7. Out of scope (defer to v8+) |
| 187 | + |
| 188 | +- Real cross-Mac ZeRO-1 receipt (BLOCKED on peer-48 hardware) |
| 189 | +- 1B+ param training matrix on Mac |
| 190 | +- A/B hyperparameter search UI |
| 191 | +- LoRA / PEFT adapter wiring |
| 192 | +- Tokenizer mutation UI (read-only by design per Lane 3) |
| 193 | +- Sophia / Adafactor / Tiger / AdEMAMix optimizers (deferred per Lane 7) |
| 194 | +- Live inference serving from trained checkpoint |
| 195 | +- Model weight quantisation post-train |
| 196 | + |
| 197 | +## 8. Done definition |
| 198 | + |
| 199 | +All 8 Q-gaps closed (Q08.3 carried as hardware-blocker note). Zero P0/P1 |
| 200 | +audit findings on re-run. Closure report at |
| 201 | +`tests/fixtures/e2e_matrix_v7_q_report.md`. Re-run audit |
| 202 | +(`docs/UI-TO-TRAIN-AUDIT-2026-05-23.md` re-execution) returns 0 BLOCKING |
| 203 | +gaps. |
0 commit comments