Skip to content

Commit 93930b2

Browse files
committed
feat(v7-c03): ckpt.inspect RPC + TopBar metadata display on Load
UI honest-closure gap: ckpt metadata writer existed (stages.py:2020, write_ckpt_metadata) and reader existed (read_ckpt_metadata:2083), but the UI Load flow had no surface that exposed arch_hash / opt_kind / cppmega_version before the user committed to a warm-start. - cppmega_v4/jsonrpc/ckpt_inspect_method.py: thin Pydantic-typed wrapper around read_ckpt_metadata. Returns {exists, has_metadata, cppmega_version, arch_hash, opt_kind, opt_lr, global_step, raw}. - dispatcher.py + schema.py: registers 'ckpt.inspect' in _ROUTES and METHOD_REGISTRY. - TopBar: 300ms-debounced inspect on ckpt-load-path change, renders arch_hash (truncated 12+…), opt_kind+lr, global_step, version. Surfaces 'file not found' / 'no cppmega metadata' branches too. - App.tsx: passes onInspectCheckpoint callback wrapping rpc.call. - tests/v4/test_ckpt_inspect_method.py: round-trip via FastAPI TestClient — missing-file, no-metadata, full-metadata. - e2e 81: visual assertion — type path → block shows arch / opt / step / version testids without question-mark fallback values.
1 parent d7b6c33 commit 93930b2

7 files changed

Lines changed: 357 additions & 1 deletion

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""V7-C03 honest-closure: ckpt.inspect RPC.
2+
3+
Reads safetensors metadata at the given path and returns the parsed
4+
arch / train / opt sub-objects so the UI can render arch_hash,
5+
opt_kind, cppmega_version on Load before the user commits to the
6+
warm-start path. Pairs with stages.write_ckpt_metadata + read_ckpt_metadata
7+
(stages.py:2020-2099) so the round-trip stays in one place.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from pathlib import Path
13+
from typing import Any
14+
15+
from pydantic import BaseModel, ConfigDict, Field
16+
17+
from cppmega_v4.jsonrpc.cache import LRUCache
18+
19+
20+
class CkptInspectParams(BaseModel):
21+
model_config = ConfigDict(extra="forbid")
22+
23+
path: str
24+
25+
26+
class CkptInspectResult(BaseModel):
27+
model_config = ConfigDict(extra="allow")
28+
29+
exists: bool
30+
has_metadata: bool = False
31+
cppmega_version: str | None = None
32+
arch_hash: str | None = None
33+
opt_kind: str | None = None
34+
opt_lr: float | None = None
35+
global_step: int | None = None
36+
raw: dict[str, Any] = Field(default_factory=dict)
37+
error: str | None = None
38+
39+
40+
def ckpt_inspect(
41+
params: CkptInspectParams, *, cache: LRUCache | None = None,
42+
) -> CkptInspectResult:
43+
p = Path(params.path)
44+
if not p.exists():
45+
return CkptInspectResult(exists=False)
46+
47+
from cppmega_v4.runner.stages import read_ckpt_metadata
48+
49+
try:
50+
meta = read_ckpt_metadata(str(p))
51+
except Exception as exc:
52+
return CkptInspectResult(
53+
exists=True, has_metadata=False, error=str(exc),
54+
)
55+
if meta is None:
56+
return CkptInspectResult(exists=True, has_metadata=False)
57+
58+
arch = meta.get("arch") if isinstance(meta.get("arch"), dict) else {}
59+
train = meta.get("train") if isinstance(meta.get("train"), dict) else {}
60+
opt = meta.get("opt") if isinstance(meta.get("opt"), dict) else {}
61+
62+
return CkptInspectResult(
63+
exists=True,
64+
has_metadata=True,
65+
cppmega_version=meta.get("cppmega_version"),
66+
arch_hash=arch.get("config_hash"),
67+
opt_kind=opt.get("kind"),
68+
opt_lr=opt.get("lr"),
69+
global_step=train.get("global_step"),
70+
raw=meta,
71+
)
72+
73+
74+
__all__ = ["CkptInspectParams", "CkptInspectResult", "ckpt_inspect"]

cppmega_v4/jsonrpc/dispatcher.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
from cppmega_v4.jsonrpc.ablation_method import (
4343
AblationRunParams, ablation_run,
4444
)
45+
from cppmega_v4.jsonrpc.ckpt_inspect_method import (
46+
CkptInspectParams, ckpt_inspect,
47+
)
4548
from cppmega_v4.jsonrpc.schema import (
4649
BuildPresetSpecsParams,
4750
CatalogExplainParams,
@@ -124,6 +127,10 @@
124127
AblationRunParams,
125128
lambda p, c: ablation_run(p, cache=c),
126129
),
130+
"ckpt.inspect": (
131+
CkptInspectParams,
132+
lambda p, c: ckpt_inspect(p, cache=c),
133+
),
127134
}
128135

129136

cppmega_v4/jsonrpc/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,7 @@ class PipelineRunResult(BaseModel):
749749
"suggest_optim_groups",
750750
"data.roundtrip_check",
751751
"ablation.run",
752+
"ckpt.inspect",
752753
})
753754

754755

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""V7-C03: ckpt.inspect RPC round-trip.
2+
3+
Writes a tiny safetensors checkpoint with the cppmega metadata schema
4+
produced by stages.write_ckpt_metadata, hits POST /rpc with ckpt.inspect,
5+
and asserts the parsed sub-objects come back wired so the UI can render
6+
arch_hash / opt_kind / version on Load.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import json
12+
from pathlib import Path
13+
14+
import mlx.core as mx
15+
import pytest
16+
from fastapi.testclient import TestClient
17+
from safetensors.numpy import save_file as save_safetensors
18+
19+
from cppmega_v4.jsonrpc import create_app
20+
21+
22+
@pytest.fixture
23+
def client():
24+
return TestClient(create_app(cache_capacity=2))
25+
26+
27+
def _write_ckpt(path: Path, *, version: str = "v1",
28+
arch_hash: str = "deadbeef" * 8,
29+
opt_kind: str = "adamw", lr: float = 3e-4,
30+
step: int = 17) -> None:
31+
metadata = {
32+
"cppmega_version": version,
33+
"arch": json.dumps(
34+
{"config_hash": arch_hash, "config_json": {}}, sort_keys=True),
35+
"train": json.dumps({"global_step": step}, sort_keys=True),
36+
"opt": json.dumps({"kind": opt_kind, "lr": lr}, sort_keys=True),
37+
}
38+
# Smallest possible payload — one scalar weight.
39+
import numpy as np
40+
save_safetensors({"w": np.zeros((1,), dtype=np.float32)},
41+
str(path), metadata=metadata)
42+
43+
44+
def test_ckpt_inspect_missing_file(client, tmp_path):
45+
payload = {
46+
"jsonrpc": "2.0", "id": "m1", "method": "ckpt.inspect",
47+
"params": {"path": str(tmp_path / "nope.safetensors")},
48+
}
49+
r = client.post("/rpc", json=payload)
50+
assert r.status_code == 200
51+
body = r.json()
52+
assert body["result"]["exists"] is False
53+
54+
55+
def test_ckpt_inspect_no_metadata(client, tmp_path):
56+
import numpy as np
57+
p = tmp_path / "bare.safetensors"
58+
save_safetensors({"w": np.zeros((1,), dtype=np.float32)}, str(p))
59+
payload = {
60+
"jsonrpc": "2.0", "id": "m2", "method": "ckpt.inspect",
61+
"params": {"path": str(p)},
62+
}
63+
r = client.post("/rpc", json=payload)
64+
assert r.status_code == 200
65+
body = r.json()
66+
assert body["result"]["exists"] is True
67+
assert body["result"]["has_metadata"] is False
68+
69+
70+
def test_ckpt_inspect_round_trip(client, tmp_path):
71+
p = tmp_path / "ckpt.safetensors"
72+
_write_ckpt(p, opt_kind="muon_adamw_hybrid", lr=1e-3, step=42)
73+
74+
payload = {
75+
"jsonrpc": "2.0", "id": "m3", "method": "ckpt.inspect",
76+
"params": {"path": str(p)},
77+
}
78+
r = client.post("/rpc", json=payload)
79+
assert r.status_code == 200
80+
res = r.json()["result"]
81+
assert res["exists"] is True
82+
assert res["has_metadata"] is True
83+
assert res["opt_kind"] == "muon_adamw_hybrid"
84+
assert res["opt_lr"] == pytest.approx(1e-3)
85+
assert res["global_step"] == 42
86+
# arch_hash must be the 64-char sha256 we wrote.
87+
assert isinstance(res["arch_hash"], str)
88+
assert len(res["arch_hash"]) == 64
89+
90+
91+
# Silence the unused mlx warning — keep the import so future tests on
92+
# the same module can use mlx-backed safetensors without re-importing.
93+
_ = mx
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// V7-C03: ckpt.inspect RPC + TopBar metadata display on Load.
2+
// Scenario: run a Train with checkpoint_save_path, then a second
3+
// Train with checkpoint_load_path pointing at the same file —
4+
// before clicking Train, the TopBar must render arch_hash /
5+
// opt_kind / version parsed out of the just-written metadata.
6+
7+
import { test, expect } from "@playwright/test";
8+
import path from "node:path";
9+
import os from "node:os";
10+
import fs from "node:fs";
11+
import { gotoApp, selectPreset, closeModal } from "../fixtures";
12+
13+
function tmpCkptPath(): string {
14+
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "v7c03-"));
15+
return path.join(tmpdir, "ckpt.safetensors");
16+
}
17+
18+
test("V7-C03: ckpt.inspect renders arch_hash + opt_kind + version on Load",
19+
async ({ page }) => {
20+
test.setTimeout(90_000);
21+
const ckptPath = tmpCkptPath();
22+
await gotoApp(page);
23+
await selectPreset(page, "llama3_8b");
24+
25+
// Wave 1: train + save.
26+
await page.getByTestId("run-pipeline-toggle").click();
27+
await page.getByTestId("train-num-steps").fill("2");
28+
await page.getByTestId("train-checkpoint-save-path").fill(ckptPath);
29+
await page.getByTestId("run-pipeline-train").click();
30+
await page.getByTestId("run-result-modal").waitFor({ timeout: 60_000 });
31+
await closeModal(page);
32+
33+
// Wave 2: type the same path into ckpt-load. The TopBar's
34+
// debounced effect must fire ckpt.inspect and surface metadata.
35+
await page.getByTestId("run-pipeline-toggle").click();
36+
await page.getByTestId("train-checkpoint-load-path").fill(ckptPath);
37+
38+
// Wait for inspect block.
39+
await expect(page.getByTestId("ckpt-inspect-info"))
40+
.toBeVisible({ timeout: 5_000 });
41+
42+
const archHashEl = page.getByTestId("ckpt-inspect-arch-hash");
43+
await expect(archHashEl).toBeVisible();
44+
const archHashText = await archHashEl.textContent();
45+
// truncated to 12 chars + ellipsis, but must start with "arch: "
46+
expect(archHashText).toMatch(/^arch: [0-9a-f]{12}$/);
47+
48+
const optKindEl = page.getByTestId("ckpt-inspect-opt-kind");
49+
const optKindText = await optKindEl.textContent();
50+
expect(optKindText).toContain("opt: ");
51+
expect(optKindText).not.toContain("opt: ?");
52+
53+
const stepEl = page.getByTestId("ckpt-inspect-step");
54+
const stepText = await stepEl.textContent();
55+
expect(stepText).not.toContain("step: ?");
56+
57+
const versionEl = page.getByTestId("ckpt-inspect-version");
58+
const versionText = await versionEl.textContent();
59+
expect(versionText).not.toContain("v: ?");
60+
});
61+
62+
test("V7-C03: ckpt.inspect renders 'file not found' for missing path",
63+
async ({ page }) => {
64+
test.setTimeout(30_000);
65+
await gotoApp(page);
66+
await selectPreset(page, "llama3_8b");
67+
68+
await page.getByTestId("run-pipeline-toggle").click();
69+
await page.getByTestId("train-checkpoint-load-path")
70+
.fill("/tmp/definitely_does_not_exist_v7c03.safetensors");
71+
72+
await expect(page.getByTestId("ckpt-inspect-missing"))
73+
.toBeVisible({ timeout: 5_000 });
74+
});

vbgui/src/App.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,21 @@ export function App(): JSX.Element {
592592
};
593593
reader.readAsText(file);
594594
}}
595+
onInspectCheckpoint={async (path: string) => {
596+
// V7-C03: thin pass-through to ckpt.inspect RPC. Errors
597+
// surface as the TopBar's "no cppmega metadata" / error
598+
// branch — they should not blow up the surrounding menu.
599+
return rpc.call<{
600+
exists: boolean;
601+
has_metadata?: boolean;
602+
cppmega_version?: string | null;
603+
arch_hash?: string | null;
604+
opt_kind?: string | null;
605+
opt_lr?: number | null;
606+
global_step?: number | null;
607+
error?: string | null;
608+
}>("ckpt.inspect", { path });
609+
}}
595610
trainDisabled={
596611
(() => {
597612
// V3-8/V3-9: gate Train on gotcha severity. The verify RPC

0 commit comments

Comments
 (0)