Buckets:
| """cwm3/train/train_kp_residual.py — LANE F1 trainer: keypoint RESIDUAL forecaster on BATON (R2 WP4 batch 2; pre-registration docs/decisions.md 2026-09-17T19:33:12Z). | |
| python train/train_kp_residual.py --arm mb_full|mb_dense|mb_1hz|cwm_full --seed k [--out results/runs/KPF_<arm>_s<k>] [--epochs 12] [--rows-per-epoch 400000] | |
| [--max-anchors N --cpu (smoke only, never a result)] | |
| -> results/runs/KPF_<arm>_s<k>/ config.json history.jsonl ckpt_best.pt BEST TRAIN_DONE kpf_{regime}_{split}.npz (train / val / test) | |
| pred_kp_{regime}_{split}.npz (val / test) PRED_DONE run_*.json | |
| Data: results/kp_residual/store.npz (the 10 Hz causal RTMW upper-body pose of every s12 route, data/build_kp_residual_store.py) + anchors_{regime}_{split}.npz; the | |
| observed frames (<= t) and the target frames (+0.5 / +1 / +2 / +3 s) are gathered from the route tick table on the GPU per batch — nothing about the future is | |
| ever an input (models/kp_forecaster.causal_normalise reads the observed frames only; tests/test_kp_forecaster.py). TRAIN anchors of both regimes with an anchor | |
| frame; each epoch samples --rows-per-epoch anchors; AdamW 1e-3 / wd 1e-4, batch 1024, bf16 autocast, grad clip 1, <= --epochs, early stop patience 3 on the | |
| VALIDATION masked L1 of the most probable mode (`val_l1_top1`, normalised units — the selection objective); seeds 0 / 1 / 2. | |
| Arms: mb_full (MotionBERT architecture, dense 2 s + sparse 1 Hz history), mb_dense (dense only), mb_1hz (1 Hz only), cwm_full (CockpitWM part-token form, parameter-matched). | |
| Outputs per anchor (row order = the s12 grid, uid / t witness): | |
| kpf_*.npz route i32 [N] + route_names (uid = route_names[route]), t, feat f16 [N,10] = the DETACHED distillation channel (6 state probabilities, 3 mode probabilities, valid bit; all 0 when the anchor has no frame or no | |
| valid reference joint), valid bool[N] | |
| pred_kp_*.npz res_top1 f16 [N,4,11,2] (residual of the most probable mode in FRAME units = normalised residual x scale), ref f16 [N,11,2], ref_ok bool[N,11], | |
| scale f16[N], p_state f16 [N,3,2], p_mode f16 [N,3], valid bool[N] | |
| No CAN column, label or openpilot DMS field is read (the state targets are the pose-derived kin-v2 states). | |
| """ | |
| from __future__ import annotations | |
| import os | |
| os.environ.setdefault("OMP_NUM_THREADS", "4") | |
| os.environ.setdefault("MKL_NUM_THREADS", "4") | |
| import argparse # noqa: E402 | |
| import json # noqa: E402 | |
| import sys # noqa: E402 | |
| import time # noqa: E402 | |
| from pathlib import Path # noqa: E402 | |
| import numpy as np # noqa: E402 | |
| import torch # noqa: E402 | |
| torch.set_num_threads(4) | |
| CWM3 = Path(__file__).resolve().parents[1] | |
| for p in (CWM3, CWM3 / "common"): | |
| if str(p) not in sys.path: | |
| sys.path.insert(0, str(p)) | |
| from models import kp_forecaster as kf # noqa: E402 | |
| from runmeta import write_run_json # noqa: E402 | |
| STORE = CWM3 / "results" / "kp_residual" | |
| REGIMES, SPLITS = ("manual", "engaged"), ("train", "val", "test") | |
| ARMS = {"mb_full": dict(backbone="mb", obs="full", d=112, depth=2, heads=4, mlp_ratio=2), | |
| "mb_dense": dict(backbone="mb", obs="dense", d=112, depth=2, heads=4, mlp_ratio=2), | |
| "mb_1hz": dict(backbone="mb", obs="1hz", d=112, depth=2, heads=4, mlp_ratio=2), | |
| "cwm_full": dict(backbone="cwm", obs="full", d=128, depth=4, heads=4, ffn=512)} | |
| HT = torch.tensor(kf.HORIZON_TICKS, dtype=torch.long) | |
| def log(msg, out=None): | |
| line = f"[{time.strftime('%H:%M:%S')}] {msg}" | |
| print(line, flush=True) | |
| if out: | |
| with open(Path(out) / "train.log", "a") as f: | |
| f.write(line + "\n") | |
| class Store: | |
| """the pose store on `device`; gather(pos, route, offsets) -> xy [B,T,11,2], conf [B,T,11], valid [B,T,11].""" | |
| def __init__(self, device): | |
| z = np.load(STORE / "store.npz", allow_pickle=True) | |
| self.kp = torch.from_numpy(z["kp"]).to(device) # f16 [F,11,3] | |
| self.valid = torch.from_numpy(z["valid"]).to(device) | |
| self.lut = torch.from_numpy(z["lut"].astype(np.int64)).to(device) | |
| self.lut_off = torch.from_numpy(z["route_lut_off"]).to(device) | |
| self.lut_len = torch.from_numpy(z["route_lut_len"]).to(device) | |
| self.L = int(self.lut.numel()) | |
| self.device = device | |
| self.n_frames = int(self.kp.shape[0]) | |
| def gather(self, pos, route, offsets): | |
| idx = pos[:, None] + offsets.to(self.device)[None] | |
| lo = self.lut_off[route][:, None] | |
| inside = (idx >= lo) & (idx < lo + self.lut_len[route][:, None]) | |
| fi = self.lut[idx.clamp(0, self.L - 1)] | |
| fi = torch.where(inside, fi, torch.full_like(fi, -1)) | |
| ok = fi >= 0 | |
| k = self.kp[fi.clamp_min(0)].float() # [B,T,11,3] | |
| v = self.valid[fi.clamp_min(0)] & ok[..., None] | |
| xy = torch.nan_to_num(k[..., :2]) * v[..., None].float() | |
| return xy, torch.nan_to_num(k[..., 2]) * v.float(), v | |
| ROUTE_NAMES = None | |
| def route_names(): | |
| """store.npz route_names: uid == route name, so anchors carry the route index only (disk: the 2.6 M uid strings were 1.4 GB).""" | |
| global ROUTE_NAMES | |
| if ROUTE_NAMES is None: | |
| ROUTE_NAMES = np.load(STORE / "store.npz", allow_pickle=True)["route_names"].astype(str) | |
| return ROUTE_NAMES | |
| def load_anchors(regime, split, max_anchors=None, seed=0): | |
| z = np.load(STORE / f"anchors_{regime}_{split}.npz", allow_pickle=True) | |
| d = {k: z[k] for k in ("t", "dongle", "mode", "route", "lut_pos", "S", "S0", "kin_row_ok")} | |
| d["uid"] = route_names()[d["route"]] | |
| if max_anchors and len(d["t"]) > max_anchors: | |
| sel = np.sort(np.random.default_rng(seed).choice(len(d["t"]), max_anchors, replace=False)) | |
| d = {k: v[sel] for k, v in d.items()} | |
| return d | |
| def to_dev(d, device): | |
| return {"route": torch.from_numpy(d["route"].astype(np.int64)).to(device), "pos": torch.from_numpy(d["lut_pos"].astype(np.int64)).to(device), | |
| "S": torch.from_numpy(d["S"]).to(device)} | |
| def run_batch(model, store, a, idx, need_loss=True): | |
| pos, route = a["pos"][idx], a["route"][idx] | |
| has = pos >= 0 | |
| posc = pos.clamp_min(0) | |
| xy, conf, valid = store.gather(posc, route, model.offsets) | |
| valid = valid & has[:, None, None] | |
| out = model(xy, conf, valid) | |
| if not need_loss: | |
| return out, has | |
| fxy, _, fvalid = store.gather(posc, route, HT) | |
| fvalid = fvalid & has[:, None, None] | |
| total, parts, err = kf.kp_loss(out, fxy, fvalid, a["S"][idx]) | |
| return out, has, total, parts, err, fxy, fvalid | |
| def evaluate(model, store, a, batch, amp): | |
| """validation: masked L1 (normalised units) of the most probable mode and of the oracle mode, mean over anchors with a target.""" | |
| model.eval() | |
| N = a["pos"].numel() | |
| s_top, s_wta, n = 0.0, 0.0, 0 | |
| for i in range(0, N, batch): | |
| idx = torch.arange(i, min(N, i + batch), device=store.device) | |
| with torch.autocast("cuda", dtype=torch.bfloat16, enabled=amp): | |
| out, has, total, parts, err, fxy, fvalid = run_batch(model, store, a, idx) | |
| m = (fvalid & out["ref_ok"][:, None]).float()[:, :, None, :, None] | |
| hasrow = (m.sum((1, 2, 3, 4)) > 0) | |
| top = out["mode_logits"].float().argmax(1) | |
| e_top = err.float().gather(1, top[:, None]).squeeze(1) | |
| s_top += float((e_top * hasrow.float()).sum()); s_wta += float((err.float().min(1).values * hasrow.float()).sum()); n += int(hasrow.sum()) | |
| model.train() | |
| return {"val_l1_top1": s_top / max(n, 1), "val_l1_oracle": s_wta / max(n, 1), "n_val_rows": n} | |
| def predict(model, store, a_np, a, batch, amp, out_dir, regime, split, full): | |
| model.eval() | |
| N = a["pos"].numel() | |
| feat = np.zeros((N, kf.KPF_DIM), np.float16) | |
| valid_row = np.zeros(N, bool) | |
| if full: | |
| res = np.zeros((N, kf.H, kf.NJ, 2), np.float16); ref = np.zeros((N, kf.NJ, 2), np.float16); ref_ok = np.zeros((N, kf.NJ), bool) | |
| scale = np.zeros(N, np.float16); p_state = np.zeros((N, kf.N_STATE_H, kf.N_STATES), np.float16); p_mode = np.zeros((N, kf.N_MODES), np.float16) | |
| for i in range(0, N, batch): | |
| idx = torch.arange(i, min(N, i + batch), device=store.device) | |
| with torch.autocast("cuda", dtype=torch.bfloat16, enabled=amp): | |
| out, has = run_batch(model, store, a, idx, need_loss=False) | |
| ok = has & out["ref_ok"].any(1) | |
| ch = kf.kpf_channel({k: v.float() for k, v in out.items()}, ok) | |
| sl = slice(i, i + len(idx)) | |
| feat[sl] = ch.cpu().numpy().astype(np.float16); valid_row[sl] = ok.cpu().numpy() | |
| if full: | |
| top = out["mode_logits"].float().argmax(1) | |
| r = out["res"].float()[torch.arange(len(idx), device=store.device), :, top] # [B,H,J,2] | |
| res[sl] = (r * out["scale"].float()[:, None, None, None]).cpu().numpy().astype(np.float16) | |
| ref[sl] = out["ref"].float().cpu().numpy().astype(np.float16); ref_ok[sl] = out["ref_ok"].cpu().numpy(); scale[sl] = out["scale"].float().cpu().numpy().astype(np.float16) | |
| p_state[sl] = torch.sigmoid(out["state_logits"].float()).cpu().numpy().astype(np.float16); p_mode[sl] = torch.softmax(out["mode_logits"].float(), -1).cpu().numpy().astype(np.float16) | |
| rn = route_names() # uid = route_names[route] (reconstructed by the readers; no 2.6 M-string array per file) | |
| np.savez(out_dir / f"kpf_{regime}_{split}.npz", route=a_np["route"].astype(np.int32), route_names=rn, t=a_np["t"], feat=feat, valid=valid_row, | |
| names=np.asarray([f"p_{s}_{h:g}s" for h in (1, 2, 3) for s in ("hands_on", "head_turned")] + [f"p_mode{m}" for m in range(kf.N_MODES)] + ["valid"])) | |
| if full: | |
| np.savez(out_dir / f"pred_kp_{regime}_{split}.npz", route=a_np["route"].astype(np.int32), route_names=rn, t=a_np["t"], res_top1=res, ref=ref, ref_ok=ref_ok, scale=scale, p_state=p_state, p_mode=p_mode, | |
| valid=valid_row, horizons_s=np.asarray(kf.HORIZONS_S), state_horizons_s=np.asarray([1.0, 2.0, 3.0]), joints=np.load(STORE / "store.npz", allow_pickle=True)["joints"]) | |
| # (uid strings are not stored: readers rebuild uid = route_names[route] — data/cwm3_dataset.load_split "kpf", eval/kp_residual_eval.py) | |
| model.train() | |
| return float(valid_row.mean()) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--arm", required=True, choices=list(ARMS)) | |
| ap.add_argument("--seed", type=int, default=0) | |
| ap.add_argument("--out", default="") | |
| ap.add_argument("--epochs", type=int, default=12) | |
| ap.add_argument("--rows-per-epoch", type=int, default=400_000) | |
| ap.add_argument("--batch", type=int, default=1024) | |
| ap.add_argument("--lr", type=float, default=1e-3) | |
| ap.add_argument("--weight-decay", type=float, default=1e-4) | |
| ap.add_argument("--patience", type=int, default=3) | |
| ap.add_argument("--minutes", type=float, default=90) | |
| ap.add_argument("--max-anchors", type=int, default=0, help="smoke only") | |
| ap.add_argument("--cpu", action="store_true", help="smoke only") | |
| ap.add_argument("--no-amp", action="store_true") | |
| a = ap.parse_args() | |
| out = Path(a.out) if a.out else CWM3 / "results" / "runs" / f"KPF_{a.arm}_s{a.seed}" + ("_smoke" if a.max_anchors else "") | |
| out.mkdir(parents=True, exist_ok=True) | |
| t_start = time.time() | |
| device = torch.device("cpu" if a.cpu else "cuda") | |
| amp = (not a.no_amp) and device.type == "cuda" | |
| torch.manual_seed(a.seed); np.random.seed(a.seed) | |
| cfg = {"arm": a.arm, **ARMS[a.arm], "seed": a.seed, "epochs": a.epochs, "rows_per_epoch": a.rows_per_epoch, "batch": a.batch, "lr": a.lr, "weight_decay": a.weight_decay, | |
| "patience": a.patience, "selection": "val_l1_top1 (masked L1 of the most probable mode, normalised units)", "max_anchors": a.max_anchors, "amp": amp, | |
| "loss": "wta_l1 + 0.1 mode_ce + 0.5 state_bce", "horizons_s": list(kf.HORIZONS_S), "state_horizons_s": [1.0, 2.0, 3.0]} | |
| (out / "config.json").write_text(json.dumps(cfg, indent=1)) | |
| store = Store(device) | |
| log(f"=== train_kp_residual {a.arm} s{a.seed} out={out} store frames={store.n_frames:,} device={device} ===", out) | |
| tr_np = {r: load_anchors(r, "train", a.max_anchors or None, a.seed) for r in REGIMES} | |
| va_np = {r: load_anchors(r, "val", (a.max_anchors // 4 if a.max_anchors else None), a.seed) for r in REGIMES} | |
| tr = {k: torch.cat([to_dev(tr_np[r], device)[k] for r in REGIMES]) for k in ("route", "pos", "S")} | |
| va = {k: torch.cat([to_dev(va_np[r], device)[k] for r in REGIMES]) for k in ("route", "pos", "S")} | |
| tr_idx_all = torch.nonzero(tr["pos"] >= 0).squeeze(1) | |
| # scale fallback = TRAIN median shoulder width (observed frames) over a 50k-anchor sample: recorded, used identically at prediction time | |
| with torch.no_grad(): | |
| smp = tr_idx_all[torch.randperm(len(tr_idx_all), generator=torch.Generator().manual_seed(0))[:50_000].to(device)] | |
| xy, _, v = store.gather(tr["pos"][smp], tr["route"][smp], kf.obs_offsets("full")) | |
| sw = (xy[:, :, kf.L_SHO] - xy[:, :, kf.R_SHO]).norm(dim=-1)[v[:, :, kf.L_SHO] & v[:, :, kf.R_SHO]] | |
| scale_fb = float(sw.median()) if sw.numel() else kf.SCALE_FALLBACK | |
| cfg["scale_fallback"] = scale_fb | |
| model = kf.KpForecaster(**{k: v for k, v in ARMS[a.arm].items()}, scale_fallback=scale_fb).to(device) | |
| cfg["params"] = {"total": kf.n_params(model), "backbone": kf.n_params(model.backbone), "heads": kf.n_params(model) - kf.n_params(model.backbone)} | |
| (out / "config.json").write_text(json.dumps(cfg, indent=1)) | |
| log(f"params {cfg['params']} train anchors {len(tr_idx_all):,} val anchors {int((va['pos'] >= 0).sum()):,} scale_fallback {scale_fb:.4f}", out) | |
| opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=a.weight_decay) | |
| g = torch.Generator(device="cpu").manual_seed(a.seed) | |
| best, best_ep, bad, hist = float("inf"), -1, 0, [] | |
| for ep in range(a.epochs): | |
| t_ep = time.time() | |
| perm = tr_idx_all[torch.randperm(len(tr_idx_all), generator=g)[:a.rows_per_epoch].to(device)] | |
| sums, nb = {}, 0 | |
| for i in range(0, len(perm), a.batch): | |
| idx = perm[i:i + a.batch] | |
| with torch.autocast("cuda", dtype=torch.bfloat16, enabled=amp): | |
| out_, has, total, parts, err, _, _ = run_batch(model, store, tr, idx) | |
| opt.zero_grad(set_to_none=True) | |
| total.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| opt.step() | |
| for k, v in parts.items(): | |
| sums[k] = sums.get(k, 0.0) + v | |
| nb += 1 | |
| if (time.time() - t_start) / 60 > a.minutes: | |
| break | |
| ev = evaluate(model, store, va, 4096, amp) | |
| rec = {"epoch": ep, **{f"train_{k}": v / max(nb, 1) for k, v in sums.items()}, **ev, "sec": round(time.time() - t_ep, 1)} | |
| hist.append(rec) | |
| with open(out / "history.jsonl", "a") as f: | |
| f.write(json.dumps(rec) + "\n") | |
| improved = ev["val_l1_top1"] < best - 1e-6 | |
| if improved: | |
| best, best_ep, bad = ev["val_l1_top1"], ep, 0 | |
| torch.save({"model": model.state_dict(), "cfg": cfg, "epoch": ep, "val": ev}, out / "ckpt_best.pt") | |
| (out / "BEST").write_text("ckpt_best.pt") | |
| else: | |
| bad += 1 | |
| log(f"ep {ep} " + " ".join(f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}" for k, v in rec.items() if k != "epoch") + (" *" if improved else ""), out) | |
| if bad >= a.patience or (time.time() - t_start) / 60 > a.minutes: | |
| break | |
| (out / "TRAIN_DONE").write_text(json.dumps({"best_epoch": best_ep, "best_val_l1_top1": best, "epochs_run": len(hist)})) | |
| model.load_state_dict(torch.load(out / "ckpt_best.pt", map_location=device, weights_only=False)["model"]) | |
| cover = {} | |
| for r in REGIMES: | |
| for s in SPLITS: | |
| a_np = tr_np[r] if s == "train" else (va_np[r] if s == "val" else load_anchors(r, "test", (a.max_anchors // 2 if a.max_anchors else None), a.seed)) | |
| cover[f"{r}_{s}"] = predict(model, store, a_np, to_dev(a_np, device), 4096, amp, out, r, s, full=(s != "train")) | |
| log(f"predicted {r}/{s}: valid share {cover[f'{r}_{s}']:.4f}", out) | |
| (out / "PRED_DONE").write_text(json.dumps(cover)) | |
| write_run_json(out, f"train_kp_residual_{a.arm}_s{a.seed}", [Path(__file__), CWM3 / "models" / "kp_forecaster.py", CWM3 / "data" / "build_kp_residual_store.py"], | |
| [STORE / "store.npz"] + [STORE / f"anchors_{r}_{s}.npz" for r in REGIMES for s in SPLITS], seed=a.seed, t_start=t_start, | |
| extra={"cfg": cfg, "history": hist, "best_epoch": best_ep, "best_val_l1_top1": best, "epochs_run": len(hist), "gpu_wall_s": round(time.time() - t_start, 1), | |
| "coverage": cover, "smoke": bool(a.max_anchors or a.cpu), "dms_fields_used": [], "inputs_note": "own causal RTMW pose (results/pose_causal_routes[_v2]); kin-v2 states as targets"}) | |
| log("DONE", out) | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 17.4 kB
- Xet hash:
- 0c724917d50d3b4ec9773d5128318126ccfbb57cbb7502dee2158fce71f71d42
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.