AsianPlayer/TriDrive / code /train /train_cwm3_oof.py
AsianPlayer's picture
download
raw
18.8 kB
"""cwm3/train/train_cwm3_oof.py — OUT-OF-FOLD joint-model predictions for the warning head's TRAIN label rows (coordinator, 2026-09-10).
Problem: the warning head is trained on features the joint model produced on TRAIN rows it was fitted on (in-sample predictions), which
inflates the device-grouped CV of the flexible heads on the predicted-future features. Fix: K device folds over the labelled TRAIN devices
(91 devices, 979 consensus + 1,250 legacy rows); for fold k the three base branches (V0/can4s, U0/kin_can_noK, E0/pooled — the same stage
recipes, `train/train_cwm3.py` code called as a library, no file edited) and then the joint model (`train/train_cwm3_joint.py` as a library,
same I1 recipe / variant) are trained on every train row EXCEPT the rows of the fold's devices, and the fold's label rows are predicted by
that model. Merged: results/runs/<joint run>/pred_{regime}_train_labelrows_oof.npz (same layout as the in-sample pack + `fold` per row).
Devices without labels stay in every training set (they are never held out). Test / val features stay those of the full models.
python train_cwm3_oof.py --seed 0 [--variant all] [--folds 5] [--fold k] [--out-root results/runs/oof]
2026-09-12 (GPU-baselines agent, roadmap E8 on the FINAL model; every flag opt-in, defaults = the command above):
python train_cwm3_oof.py --seed 0 --variant all_minus_road2drv --bases V0:can4s_stats2,U0:kin_can_noK_clip8_ln,E0:pooled_P --kin-source causal_routes --kin-version v2 \
--pack-version v2 --out-root results/runs/oof_final --target-run I1_all_minus_road2drv_s0_final --gate-each --gate-all-trainers --max-others 2
(--bases: the three base variants (stage:variant); the fold bases inherit the variants' recipes incl. `kclip` / `u_in_ln` / `p_head` / `select_LF` and the data
flags; base_cfgs = models.cwm3.base_cfg_summary so the joint's batch prep uses the base's clamp / side vector; --target-run: the run dir that receives
pred_{regime}_train_labelrows_oof.npz; --gate-each: wait for the launch gates before EVERY training (not only per fold); --gate-all-trainers: count
train_kp / pretrain processes too (project rule: < 4 trainers alive across agents).)
Runs inline (no lock), one fold at a time; ≈ 10 min of GPU for the three bases + ≈ 8 min for the joint fine-tune per fold. Resumable per
stage (TRAIN_DONE / pred file present -> skipped). The launcher waits for the run_queue_joint.sh gates (< 2 trainers, RAM, VRAM).
No openpilot DMS field is read.
"""
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 importlib.util # noqa: E402
import json # noqa: E402
import subprocess # noqa: E402
import sys # noqa: E402
import time # noqa: E402
from pathlib import Path # noqa: E402
from types import SimpleNamespace # noqa: E402
import numpy as np # noqa: E402
CWM3 = Path(__file__).resolve().parents[1]
for p in (CWM3, CWM3 / "data", CWM3 / "common", CWM3 / "train", CWM3 / "eval", CWM3.parent / "s12_wm"):
sys.path.insert(0, str(p))
import cwm3_dataset as ds # noqa: E402
import batch as bt # noqa: E402
import train_cwm3 as T # noqa: E402
import train_cwm3_joint as J # noqa: E402
log = T.log
BASES = [("V0", "can4s"), ("U0", "kin_can_noK"), ("E0", "pooled")]
BASES_DEFAULT = "V0:can4s,U0:kin_can_noK,E0:pooled"
DFLAGS = {"pack_version": "v1", "kin_source": "cwm2", "kin_version": "v1"} # module-level data flags (set by main from the opt-in CLI flags)
def label_table():
spec = importlib.util.spec_from_file_location("cwm3_labels", CWM3 / "data" / "labels.py")
LB = importlib.util.module_from_spec(spec)
spec.loader.exec_module(LB)
return LB.train_label_table(sources=("consensus", "legacy_v1"))
def device_folds(tab, K):
dev = np.array(sorted(tab.dongle.unique()))
return {d: i % K for i, d in enumerate(dev)}
def gates_ok(min_avail_gb=26, min_vram_gb=12, max_others=1, pattern="train_cwm3"): # measured joint-trainer VRAM 7.6 GB + 4 GB headroom (owner rule)
"""same gates as train/run_queue_joint.sh; this process is excluded from the trainer count (it is not training while it waits).
max_others / pattern (2026-09-12, opt-in): the trainer-count limit and the process pattern (default = the original train_cwm3-only count <= 1)."""
me = os.getpid()
n = int(subprocess.run(["bash", "-c", f'pids=$(pgrep -f "^[^ ]*python[^ ]* [^ ]*({pattern})" | grep -vx {me} || true); n=0; for p in $pids; do pp=$(awk \'/^PPid/{{print $2}}\' /proc/$p/status 2>/dev/null); '
'grep -qx -- "$pp" <<< "$pids" || n=$((n+1)); done; echo $n'], capture_output=True, text=True).stdout.strip() or 0)
avail = int(open("/proc/meminfo").read().split("MemAvailable:")[1].split()[0]) // 1048576
vram = int(subprocess.run(["nvidia-smi", "--query-gpu=memory.free", "--format=csv,noheader,nounits"], capture_output=True, text=True).stdout.split()[0]) // 1024
return n <= max_others and avail >= min_avail_gb and vram >= min_vram_gb, (n, avail, vram)
GATE = {"each": False, "kw": {}} # --gate-each: wait for gates_ok(**kw) before every training (default off = the original per-fold gate)
def wait_gates(log_out, what):
if not GATE["each"]:
return
while True:
ok, stat = gates_ok(**GATE["kw"])
if ok:
log(f"[oof] gates ok for {what} (others, availGB, vramGB) = {stat}", log_out)
return
time.sleep(60)
# ----------------------------------------------------------------------------- base branches (train_cwm3.py as a library)
def train_base_oof(stage, variant, seed, held, out, log_out):
if (out / "TRAIN_DONE").exists() and (out / "BEST").exists():
return
out.mkdir(parents=True, exist_ok=True)
cfg = T.stage_cfg(stage, variant)
cfg["pack_version"], cfg["kin_source"], cfg["kin_version"], cfg["mask_routes"] = DFLAGS["pack_version"], DFLAGS["kin_source"], DFLAGS["kin_version"], ""
cfg["stats_version"] = int(bt.STATS_VERSION)
if cfg["branch"] == "U" and "kclip" in cfg["recipe"]: # recipes that pin the clamp (clip8 survivors) record it, as train_cwm3.main does; others keep the legacy path
cfg["kclip"] = float(cfg["recipe"]["kclip"])
args = SimpleNamespace(seed=seed, out=str(out), epochs=0, minutes=600, cpu=False, no_amp=False, synthetic=0, max_anchors=0, no_lock=True)
(out / "config.json").write_text(json.dumps({"cfg": cfg, "args": vars(args), "oof": {"held_devices": sorted(held), "data_flags": DFLAGS}}, indent=1, default=str))
wait_gates(log_out, f"base {stage}/{variant} s{seed}")
bank = T.load_bank(args, cfg)
tr, va = T.load_raw(args, cfg, "train"), T.load_raw(args, cfg, "val")
keep = np.flatnonzero(~np.isin(tr["dongle"], list(held)))
n_all = len(tr["t"])
tr = ds.subset(tr, keep)
st = bt.compute_stats(tr, T.target_stats(args))
if not (out / "stats.npz").exists():
np.savez(out / "stats.npz", **st)
log(f"[oof] base {stage}/{variant} s{seed}: train rows {len(keep):,}/{n_all:,} after holding out {len(held)} devices", log_out)
code = T.run_chunk(args, cfg, tr, va, st, bank)
assert code == 0 and (out / "TRAIN_DONE").exists(), f"base {stage}/{variant} fold training failed ({code})"
del tr, va, bank
# ----------------------------------------------------------------------------- joint fine-tune + fold label rows (train_cwm3_joint.py as a library)
def train_joint_oof(seed, variant, held, fold_rows, base_root, suffix, out, log_out, bases=None):
if all((out / f"pred_{r}_train_labelrows.npz").exists() for r in fold_rows):
return
out.mkdir(parents=True, exist_ok=True)
bases = bases or dict(BASES)
cfg = J.stage_cfg("I1", variant, base_v=bases.get("V0"), base_e=bases.get("E0"), base_u=bases.get("U0"))
cfg["pack_version"], cfg["kin_source"], cfg["kin_version"] = DFLAGS["pack_version"], DFLAGS["kin_source"], DFLAGS["kin_version"]
cfg["stats_mode"], cfg["base_suffix_e"], cfg["run_suffix"], cfg["base_dirs"] = "merged", "", "", {}
args = SimpleNamespace(stage="I1", variant=variant, seed=seed, out=str(out), epochs=0, minutes=600, cpu=False, no_amp=False, synthetic=0, max_anchors=0,
no_lock=True, no_label_rows=False, base_v=bases.get("V0", ""), base_e=bases.get("E0", ""), base_u=bases.get("U0", ""), regimes="", pred_splits="",
kin_source=DFLAGS["kin_source"], kin_version=DFLAGS["kin_version"], pack_version=DFLAGS["pack_version"], stats_mode="merged", run_suffix="", base_suffix_e="")
# library monkey-patches (module-level, this process only): OOF base dirs, no PRED_DONE requirement, device exclusion, fold label rows only
J.base_dirs = lambda cfg_, seed_: {b: base_root / f"{st_}_{va_}_s{seed_}{suffix}" for b, (st_, va_) in cfg_["base"].items()}
J.base_info_numpy = _base_info_numpy_oof
orig_load_raw = J.load_raw
def load_raw_oof(a, c, split, subset=True, regimes=None):
raw = orig_load_raw(a, c, split, subset, regimes)
if split == "train" and subset:
keep = np.flatnonzero(~np.isin(raw["dongle"], list(held)))
raw = ds.subset(raw, keep)
raw["regime_row"], raw["regime_id"] = raw["regime_row"], raw["regime_id"]
return raw
J.load_raw = load_raw_oof
J.label_rows = lambda a, c: {r: np.asarray(v, np.int64) for r, v in fold_rows.items()}
J.load_pred_raws = lambda a, c, lrows: _label_raws_only(a, c, lrows, orig_load_raw)
info = J.base_info_numpy(cfg, seed)
cfg["e_side"] = info["base_cfgs"]["E"].get("side", "vf256")
sv = {b: c.get("stats_version") for b, c in info["base_cfgs"].items()}
cfg["stats_version"], cfg["base_stats_versions"] = (int(min(v for v in sv.values() if v is not None)) if any(v is not None for v in sv.values()) else 1), sv
(out / "config.json").write_text(json.dumps({"cfg": cfg, "args": vars(args), "oof": {"held_devices": sorted(held), "base_root": str(base_root), "suffix": suffix, "data_flags": DFLAGS}}, indent=1, default=str))
wait_gates(log_out, f"joint {variant} s{seed}")
st = {k: np.asarray(v, np.float32) for k, v in info["stats"].items()}
st.update(J.aux_stats_from_files(cfg["regimes"]))
np.savez(out / "stats.npz", **st)
(out / "base_ckpts.json").write_text(json.dumps({"base_ckpts": info["base_ckpts"], "base_cfgs": info["base_cfgs"], "tok_source": cfg.get("tok_source")}, indent=1))
bank = J.load_bank(args, cfg, info)
if not (out / "TRAIN_DONE").exists():
tr, va = J.load_raw(args, cfg, "train"), J.load_raw(args, cfg, "val")
log(f"[oof] joint {variant} s{seed}: train rows {len(tr['t']):,} (fold devices held out)", log_out)
code = J.run_chunk(args, cfg, tr, va, st, bank, info)
assert code == 0, f"joint fold training failed ({code})"
del tr, va
raws = J.load_pred_raws(args, cfg, fold_rows)
code = J.predict_many(args, cfg, raws, st, bank, info)
assert code == 0
(out / "PRED_DONE").write_text(json.dumps({"label_rows_only": True, "regimes": list(fold_rows)}))
def _base_info_numpy_oof(cfg, seed):
from models.cwm3 import merge_stats, base_cfg_summary
names, cfgs, stats, dirs = {}, {}, [], {}
for b, d in J.base_dirs(cfg, seed).items():
assert (d / "BEST").exists() and (d / "TRAIN_DONE").exists(), f"OOF base for {b} missing: {d}"
c = json.loads((d / "config.json").read_text())["cfg"]
names[b] = f"{d.name}:{(d / 'BEST').read_text().strip()}"
cfgs[b] = base_cfg_summary(c) # 2026-09-12: kclip / u_in_ln / p_head / select_LF / side / stats_version reach the joint's batch prep (as train_cwm3_joint.base_info_numpy)
st = {k: v for k, v in np.load(d / "stats.npz").items()}
if "stats_version" in st and "stats_version" not in cfgs[b]:
cfgs[b]["stats_version"] = int(st["stats_version"])
stats.append(st)
dirs[b] = str(d)
return {"base_ckpts": names, "base_cfgs": cfgs, "stats": merge_stats(stats), "stats_mode": "merged", "base_dirs": dirs}
def _label_raws_only(args, cfg, lrows, orig_load_raw):
raws = {}
for regime, rows in lrows.items():
if len(rows) and not (Path(args.out) / f"pred_{regime}_train_labelrows.npz").exists():
full = orig_load_raw(args, cfg, "train", subset=False, regimes=[regime])
sub = ds.subset(full, rows)
sub["_label_rows"] = rows
raws[(regime, "train")] = sub
del full
return raws
# ----------------------------------------------------------------------------- merge
def merge(seed, variant, K, out_root, target_run, log_out):
for regime in ("manual", "engaged"):
packs = [np.load(out_root / f"I1_{variant}_s{seed}_f{k}" / f"pred_{regime}_train_labelrows.npz", allow_pickle=True) for k in range(K)
if (out_root / f"I1_{variant}_s{seed}_f{k}" / f"pred_{regime}_train_labelrows.npz").exists()]
if len(packs) < K:
log(f"[oof] merge {regime}: only {len(packs)}/{K} folds present — skipped", log_out)
continue
rows = np.concatenate([p["rows"] for p in packs])
order = np.argsort(rows, kind="mergesort")
fold = np.concatenate([np.full(len(p["rows"]), k, np.int8) for k, p in enumerate(packs)])[order]
out = {}
for key in packs[0].files:
a0 = packs[0][key]
if a0.ndim >= 1 and a0.shape[0] == len(packs[0]["rows"]) and key != "rows":
out[key] = np.concatenate([p[key] for p in packs])[order]
elif key == "rows":
out[key] = rows[order]
else:
out[key] = a0
out["fold"] = fold
out["oof"] = np.array(json.dumps({"K": K, "folds": [str(out_root / f"I1_{variant}_s{seed}_f{k}") for k in range(K)]}))
target = CWM3 / "results" / "runs" / target_run / f"pred_{regime}_train_labelrows_oof.npz"
if target.exists():
log(f"[oof] {target} exists — not overwritten", log_out)
continue
np.savez_compressed(target, **out)
log(f"[oof] merged {regime}: {len(rows)} label rows from {K} folds -> {target}", log_out)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--variant", default="all")
ap.add_argument("--folds", type=int, default=5)
ap.add_argument("--fold", type=int, default=-1, help="single fold (default: all)")
ap.add_argument("--out-root", default=str(CWM3 / "results" / "runs" / "oof"))
ap.add_argument("--no-gate", action="store_true")
ap.add_argument("--bases", default=BASES_DEFAULT, help="stage:variant triple of the base branches (default = the original V0/can4s, U0/kin_can_noK, E0/pooled)")
ap.add_argument("--kin-source", default="cwm2", choices=["cwm2", "causal_routes", "oldin_ctgt"], help="data layer kin source for the fold bases AND the joint (default cwm2 = original)")
ap.add_argument("--kin-version", default="v1", choices=["v1", "v2"])
ap.add_argument("--pack-version", default="v1", choices=["v1", "v2"])
ap.add_argument("--target-run", default="", help="run dir under results/runs that receives pred_{regime}_train_labelrows_oof.npz (default I1_<variant>_s<seed>)")
ap.add_argument("--gate-each", action="store_true", help="wait for the launch gates before EVERY training (default: once per fold, original behaviour)")
ap.add_argument("--gate-all-trainers", action="store_true", help="count train_kp / pretrain processes in the trainer gate too (project rule: < 4 trainers alive)")
ap.add_argument("--max-others", type=int, default=1, help="trainer-count gate: other trainers alive <= this (default 1 = original)")
ap.add_argument("--min-avail-gb", type=int, default=26)
ap.add_argument("--min-vram-gb", type=int, default=12)
a = ap.parse_args()
global BASES
BASES = [tuple(x.split(":")) for x in a.bases.split(",")]
assert [b[0] for b in BASES] == ["V0", "U0", "E0"], a.bases
DFLAGS.update(pack_version=a.pack_version, kin_source=a.kin_source, kin_version=a.kin_version)
ds.set_pack_version(a.pack_version)
ds.set_kin_source(a.kin_source, a.kin_version)
gate_kw = dict(min_avail_gb=a.min_avail_gb, min_vram_gb=a.min_vram_gb, max_others=a.max_others, pattern=("train_cwm3|train_kp|pretrain" if a.gate_all_trainers else "train_cwm3"))
GATE["each"], GATE["kw"] = bool(a.gate_each and not a.no_gate), gate_kw
t0 = time.time()
out_root = Path(a.out_root)
out_root.mkdir(parents=True, exist_ok=True)
log_out = str(out_root)
tab = label_table()
folds = device_folds(tab, a.folds)
(out_root / f"folds_s{a.seed}.json").write_text(json.dumps({"K": a.folds, "device_fold": folds, "n_label_rows": int(len(tab))}, indent=1))
log(f"[oof] {len(folds)} labelled devices -> {a.folds} folds; {len(tab)} label rows; bases {BASES}; data flags {DFLAGS}; target run {a.target_run or f'I1_{a.variant}_s{a.seed}'}", log_out)
for k in range(a.folds):
if a.fold >= 0 and k != a.fold:
continue
held = {d for d, f in folds.items() if f == k}
fold_rows = {r: np.sort(tab[(tab.regime == r) & tab.dongle.isin(held)].row.unique()).astype(np.int64) for r in ("manual", "engaged")}
while not a.no_gate:
ok, stat = gates_ok(**gate_kw)
if ok:
break
time.sleep(60)
log(f"[oof] === fold {k}: {len(held)} devices, label rows manual {len(fold_rows['manual'])} engaged {len(fold_rows['engaged'])} ({time.time() - t0:.0f}s)", log_out)
for stage, variant in BASES:
train_base_oof(stage, variant, a.seed, held, out_root / f"{stage}_{variant}_s{a.seed}_f{k}", log_out)
train_joint_oof(a.seed, a.variant, held, fold_rows, out_root, f"_f{k}", out_root / f"I1_{a.variant}_s{a.seed}_f{k}", log_out, bases=dict(BASES))
if a.fold < 0:
merge(a.seed, a.variant, a.folds, out_root, a.target_run or f"I1_{a.variant}_s{a.seed}", log_out)
log(f"[oof] DONE in {(time.time() - t0) / 60:.1f} min", log_out)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
18.8 kB
·
Xet hash:
6077934b0abb8473266c3516652f96bc0387e1cab2ed7f4102d0c20f9c44895a

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.