AsianPlayer/TriDrive / code /train /train_cwm3_joint.py
AsianPlayer's picture
download
raw
77.1 kB
"""cwm3 joint-model stage trainer (I1 interactions / F1 decoders; plan §3, claims B2 / B3) — both regimes in ONE model.
python train_cwm3_joint.py --stage I1 --variant all --seed 0 [--base-v can4s|can8s] [--max-anchors N] [--synthetic N] [--minutes 18] [--out DIR]
Assembles models/cwm3.CWM3Joint from the seed-k BEST checkpoints of the three base stages (train/stages_joint.yaml `base`; the V base is a
CONFIG FIELD — V0/can4s or V0/can8s — never hard-coded), adds the zero-initialised interaction modules of the variant, the detached K head and
the decoder kind, and fine-tunes: base branches at lr * lr_base_mult (0.1; 0 = frozen), new modules at lr. Standardisation statistics are the
base checkpoints' (train-only), never recomputed. Selection objective (BEST / early stop): validation sum of the chain NLLs of all three
branches + 0.1 L_F (models/heads.selection_objective over the merged parts); V terms are masked to rows with the V history available.
Execution: `--no-lock` (default in train/run_queue_joint.sh since the owner lifted the lock requirement on 2026-09-10 13:58) runs training and
prediction INLINE in one process — no fork, no lock; the queue script gates launches on free VRAM / RAM / the number of live trainers. Lock mode
is train/train_cwm3.py's (imported, unchanged): the parent loads all data BEFORE the lock and never touches CUDA — nor any
torch op (a CPU model build before os.fork() left the child deadlocked on the OMP pool for 31 min on 2026-09-10; base cfg / stats now come from
the base runs' config.json / stats.npz, label rows from a subprocess); flock(/tmp/s13_gpu.lock) -> fork -> child trains until the wall budget
(<= 22 min) / epoch budget / early stop -> exit 0 | 3; the same child predicts when >= 4 min of the chunk are left. Watchdog: faulthandler
dumps every thread's stack and exits (releasing the lock) if the first batch has not completed 5 min after the lock, then guards the chunk.
E base is a config field too (E0/pooled default; `--base-e`); with `tok_source: region` the region token bank feeds ONLY the road -> driver
cross-attention (RegionTokenEmbed), the E chains / F stay those of the E base.
The `indep` variant (`no_train`) skips training: the assembled base branches predict as-is — the exact independent-branch comparator whose
ep_p / w blocks are composed identically to the trained variants. 4-thread CPU cap; F targets stay in host RAM (gathered per batch).
Detached auxiliary heads (no trunk gradient): P road-pressure chains (results/targets_pressure, 6 families) on the E trunk and the HARD response
chain R >= 2 (results/targets_response) on the V trunk — extra outputs for the warning head; their NLL (pchain_* / rchain_*) is excluded from the
selection objective. Outputs under results/runs/{stage}_{variant}_s{seed}[_v8 when --base-v overrides the yaml]: ckpt_*.pt, BEST, history.jsonl, TRAIN_DONE,
stats.npz, pred_{regime}_{split}.npz (hazards of all 12 chains, K̂, per-row means), pred_layout_{regime}_{split}.npz
(eval/write_pred_layout_joint.py: onset / ep / w / nr blocks), pred_{regime}_train_labelrows.npz (rows the warning head is trained on;
data/labels.py), metrics_quick_*.json, run_*.json, PRED_DONE; rows appended to results/cwm3_quick_metrics.csv.
No openpilot DMS field is read anywhere (inputs: CAN whitelist, YOLO-pose kinematics, V-JEPA2 latents).
"""
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 csv # noqa: E402
import json # noqa: E402
import shutil # noqa: E402
import sys # noqa: E402
import time # noqa: E402
from pathlib import Path # noqa: E402
import numpy as np # noqa: E402
import yaml # noqa: E402
CWM3 = Path(__file__).resolve().parents[1]
EXPLORE = CWM3.parent
for p in (CWM3, CWM3 / "data", CWM3 / "common", CWM3 / "train", CWM3 / "eval", EXPLORE / "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 (lock loop, checkpoint helpers, log — unchanged)
from models import heads as hd # noqa: E402
from models.cwm3 import build_joint, joint_loss, P_CHAINS, R_CHAINS, base_cfg_summary, branch_stats, merge_stats, stats_per_branch # noqa: E402
from runmeta import write_run_json # noqa: E402
import write_pred_layout_joint as wplj # noqa: E402
from mtl import MTL, KINDS as MTL_KINDS # noqa: E402 (WP3 MTL block 2026-09-16: --mtl none|uw|famo|pcgrad; none = the original path)
from models import latent_aux as LA # noqa: E402 (R2 WP4 batch 2 C4 2026-09-17: variant key `latent_aux`; lam 0 -> nothing is built, the original path)
STAGES = yaml.safe_load((CWM3 / "train" / "stages_joint.yaml").read_text())
CHAINS = list(hd.V_CHAINS) + list(hd.U_CHAINS) + list(hd.E_CHAINS)
AUX_KEYS = ["P", "Pvalid", "P0", "P0valid", "Rz", "Rvalid", "R0z", "R0valid"] # detached auxiliary chains (results/targets_{pressure,response})
LAT_KEYS = ["LYf", "LYv"] # C4 future road quantities at the latent horizons (results/targets_road_dyn Yf / Yvalid sliced to [N,K,4]); present only when latent_enabled
KEYS = sorted(set(bt.KEYS["V"]) | set(bt.KEYS["U"]) | set(bt.KEYS["E"]) | set(AUX_KEYS) | set(LAT_KEYS))
HOST_KEYS = ("F",) # largest stream (2.5 KB/row): host RAM, gathered per batch
PRESSURE = CWM3 / "results" / "targets_pressure"
RESPONSE = CWM3 / "results" / "targets_response"
ROAD_DYN = CWM3 / "results" / "targets_road_dyn"
log = T.log
def latent_enabled(cfg):
"""C4 (2026-09-17): the variant's `latent_aux` dict with lam > 0. lam == 0 (the pre-registered control) or no key -> False: the module is never built, no target is
attached, no loss term is added — the parent variant's code path, bit for bit (tests/test_latent_aux.py)."""
la = cfg.get("latent_aux") or {}
return bool(la) and float(la.get("lam", 0.0)) > 0.0
def latent_lam(cfg):
return float((cfg.get("latent_aux") or {}).get("lam", 0.0)) if latent_enabled(cfg) else 0.0
# ----------------------------------------------------------------------------- config
def stage_cfg(stage, variant, base_v=None, base_e=None, base_u=None):
s = STAGES["stages"][stage]
cfg = dict(STAGES["defaults"])
cfg.update({"stage": stage, "variant": variant, "recipe": dict(s["recipe"]), "lambda": dict(s.get("lambda", {})), "decoder": s.get("decoder", "direct"),
"experiment": s.get("experiment", ""), "interactions": [], "naive": False, "no_train": False})
cfg.setdefault("tok_source", "region")
v = dict(s["variants"][variant])
if "recipe" in v:
cfg["recipe"].update(v.pop("recipe"))
cfg.update(v)
cfg["base"] = {k: list(x) for k, x in cfg["base"].items()}
if base_v:
cfg["base"]["V"] = ["V0", base_v]
if base_e:
cfg["base"]["E"] = ["E0", base_e]
if base_u:
cfg["base"]["U"] = ["U0", base_u]
return cfg
def resolve_run_dir(x):
"""--base-dir-* value: an absolute path, a path relative to cwm3/, or a bare run-dir name under results/runs (2026-09-11)."""
q = Path(str(x))
if q.is_absolute():
return q
if (CWM3 / q).exists():
return CWM3 / q
return CWM3 / "results" / "runs" / q.name
def base_dirs(cfg, seed):
"""{branch: run dir}: cfg["base_dirs"][branch] (explicit, --base-dir-{v,u,e}; 2026-09-11 final assembly) else <stage>_<variant>_s<seed>[suffix]."""
suf = {"E": cfg.get("base_suffix_e", "")}
explicit = cfg.get("base_dirs", {}) or {}
return {b: (resolve_run_dir(explicit[b]) if explicit.get(b) else CWM3 / "results" / "runs" / f"{st}_{va}_s{seed}{suf.get(b, '')}")
for b, (st, va) in cfg["base"].items()}
def need_streams(cfg):
need = {"X", "Y", "demand", "Kh", "kin", "F", "vf256", "tokens"}
if cfg["base"]["V"][1] == "can8s":
need.add("X8")
if cfg.get("e_side", "vf256") != "vf256": # the E base's side vector (regionpca256, pack v2 only); set by main from the E base config
need.add(cfg["e_side"])
if cfg.get("u_gaze_chain"): # gaze U bases (2026-09-12 appendix variant): Sg / Sg0 / EMg chain targets; set by main from the U base config
need.add("gaze")
if cfg.get("u_face"): # Fh / Fhvalid causal face-landmark inputs of the U base
need.add("face")
if cfg.get("u_kpf"): # LANE F1 (2026-09-17): the detached keypoint-forecaster channel (--u-kpf <KPF run dir>)
need.add("kpf")
return frozenset(need)
def prep_cfgs(info):
"""per-branch batch.py configs from the base runs' configs (never hard-coded); "R" = the region token bank for the cross-attention.
2026-09-11: the U cfg carries the base's `kclip` (batch.k_clip -> 8 for the clip8 recipes; absent -> legacy 5, old runs reproduce) and the E cfg
its `side` vector name (regionpca256 bases)."""
bc = info["base_cfgs"]
u = {"cabin": bc["U"].get("cabin", "none")}
if "kclip" in bc["U"]:
u["kclip"] = float(bc["U"]["kclip"])
rec_u = {k: bool(bc["U"][k]) for k in ("u_face", "u_gaze_chain") if k in bc["U"]}
if rec_u: # gaze U bases (2026-09-12): batch.prep_U appends the face channels / builds the gaze targets
u["recipe"] = rec_u
e = {"tokens": bc["E"].get("tokens", "region"), "n_slots": int(bc["E"].get("n_slots", 16))}
if "side" in bc["E"]:
e["side"] = bc["E"]["side"]
return {"V": {"can_hist": bc["V"].get("can_hist", "4s")}, "U": u, "E": e, "R": {"tokens": "region", "n_slots": 16}}
def base_info_numpy(cfg, seed):
"""PARENT-side assembly info WITHOUT torch (a torch op before os.fork() left the child deadlocked on the OMP pool, 2026-09-10): per base
run dir -> config.json (cfg), stats.npz (train-only statistics, merged with equality asserted on overlapping keys), BEST name.
2026-09-11: base_cfgs = models.cwm3.base_cfg_summary (kclip / u_in_ln / kin_source / kin_version / stats_version / p_head / select_LF / side);
cfg["stats_mode"] per_branch keeps every base's own statistics ('<branch>/<key>', models.cwm3.stats_per_branch) instead of merging."""
names, cfgs, stats, dirs = {}, {}, [], {}
for b, d in base_dirs(cfg, seed).items():
assert (d / "BEST").exists() and (d / "PRED_DONE").exists(), f"base checkpoint for {b} missing or not finished: {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)
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)
mode = cfg.get("stats_mode", "merged")
merged = merge_stats(stats) if mode == "merged" else stats_per_branch(stats, tuple(base_dirs(cfg, seed)))
return {"base_ckpts": names, "base_cfgs": cfgs, "stats": merged, "stats_mode": mode, "base_dirs": dirs}
# ----------------------------------------------------------------------------- data
class HostTensor:
"""numpy array kept in host RAM; `[idx]` with a device index tensor gathers the rows and moves them to the device."""
def __init__(self, a, device):
self.a, self.device = a, device
def __getitem__(self, idx):
import torch
i = idx.cpu().numpy() if torch.is_tensor(idx) else np.asarray(idx)
return torch.from_numpy(np.ascontiguousarray(self.a[i])).to(self.device)
def numel(self):
return 0
def element_size(self):
return 0
def make_store(raw, device, bank):
store = bt.GpuStore(raw, [k for k in KEYS if k not in HOST_KEYS], device, bank)
for k in HOST_KEYS:
if k in raw:
store.t[k] = HostTensor(raw[k], device)
return store
def v_valid_np(raw, pc):
if pc["V"].get("mask_V", False):
return np.zeros(len(raw["t"]), bool) # variant `ue_noV` (2026-09-18): the vehicle branch is a MISSING modality on every row
return raw["x8_ok"].astype(bool) if pc["V"].get("can_hist") == "8s" else np.ones(len(raw["t"]), bool)
def apply_mask_v(pc, cfg):
"""variant key `mask_V` (2026-09-18, pre-registration docs/decisions.md 2026-09-18T03:34:28Z): mark the V batch config so that prep_joint / v_valid_np treat the
vehicle branch as missing (X zeroed, V-valid mask all False, valid_V = 0 in every pack). Absent / false -> pc unchanged (every older run bit for bit)."""
if cfg.get("mask_V", False):
pc["V"]["mask_V"] = True
return pc
def aux_stats(tr):
"""train base rates of the auxiliary anchor states (fill for invalid anchors), as batch.compute_stats does for resp0 / d0."""
st = {}
if "P0" in tr:
st["p0_base"] = np.array([tr["P0"][tr["P0valid"][:, j], j].mean() if tr["P0valid"][:, j].any() else 0.0 for j in range(6)], np.float32)
if "R0z" in tr:
v = tr["R0valid"][:, 0]
st["r0_base"] = np.array([tr["R0z"][v, 0].mean() if v.any() else 0.05], np.float32)
return st
def aux_stats_from_files(regimes):
"""the same base rates read directly from the TRAIN pressure / response files (both regimes pooled; all rows) — no full-split load."""
P0, P0v, R0, R0v = [], [], [], []
for r in regimes:
pf, rf = PRESSURE / f"pressure_{r}_train.npz", RESPONSE / f"response_{r}_train.npz"
if not (pf.exists() and rf.exists()):
return {}
with np.load(pf, allow_pickle=True) as z:
P0.append(z["P0"].astype(bool)); P0v.append(z["P0valid"].astype(bool))
with np.load(rf, allow_pickle=True) as z:
R0.append((z["R0"] >= 2)[:, None]); R0v.append(z["R0valid"].astype(bool)[:, None])
return aux_stats({"P0": np.concatenate(P0), "P0valid": np.concatenate(P0v), "R0z": np.concatenate(R0), "R0valid": np.concatenate(R0v)})
def prep_aux(store, idx, st, M, key, valid_key, k0, k0valid, base_key):
"""2-state chain targets for a detached auxiliary head: z, z_prev (anchor state, base rate where invalid), mask (valid & M & prev valid)."""
import torch
d = store.device
z = store.t[key][idx].float()
zv = store.t[valid_key][idx]
z0v = store.t[k0valid][idx]
base = torch.as_tensor(st[base_key], device=d)
z0 = torch.where(z0v, store.t[k0][idx].float(), base[None, :].expand(len(idx), -1))
zp = torch.cat([z0[:, None], z[:, :-1]], 1)
m = zv & M[:, :, None] & torch.cat([z0v[:, None], zv[:, :-1]], 1)
return {"z": z, "z_prev": zp, "m": m, "z0": z0, "z0valid": z0v}
def prep_joint(store, idx, st, pc, need_targets=True, region_keys=False):
import torch
bV, tV = bt.prep_V(store, idx, branch_stats(st, "V"), pc["V"], need_targets)
bU, tU = bt.prep_U(store, idx, branch_stats(st, "U"), pc["U"], need_targets)
bE, tE = bt.prep_E(store, idx, branch_stats(st, "E"), pc["E"], need_targets)
bR = bt.prep_E(store, idx, branch_stats(st, "E"), pc["R"], False)[0] if region_keys else None
if pc["V"].get("mask_V", False):
# missing vehicle modality (variant `ue_noV`, 2026-09-18): the standardised CAN history is zeroed and NO row counts as V-valid -> the V chain / MDN losses,
# the detached R-head loss and the V selection terms are masked out; the model's trunk_V returns exact zeros regardless of X (models/cwm3.py)
bV["X"] = torch.zeros_like(bV["X"])
vv = torch.zeros(len(idx), dtype=torch.bool, device=store.device)
elif pc["V"].get("can_hist") == "8s":
vv = store.t["x8_ok"][idx]
else:
vv = torch.ones(len(idx), dtype=torch.bool, device=store.device)
tg = {"V": tV, "U": tU, "E": tE}
b = {"V": bV, "U": bU, "E": bE}
if bR is not None:
b["R"] = {k: bR[k] for k in ("tok", "tok_ok", "tok_dt")}
if need_targets and store.has("LYf") and "lat_mu" in st:
# C4 latent targets (2026-09-17): future road quantities at the horizons + P occupancy + driver-state occupancy, validity-masked; only when latent_enabled
tg_lat = LA.build_targets(store.t["LYf"][idx], store.t["LYv"][idx], store.t["P"][idx], store.t["Pvalid"][idx], store.t["S"][idx].float(), store.t["M"][idx],
st["lat_mu"], st["lat_sd"], horizons=tuple(int(k) for k in np.asarray(st["lat_horizons"]).tolist()))
else:
tg_lat = None
if need_targets:
tV["M"] = tV["M"] & vv[:, None]
tV["zVm"] = tV["zVm"] & vv[:, None, None]
M = store.t["M"][idx]
if store.has("P") and "p0_base" in st:
tg["P"] = prep_aux(store, idx, st, M, "P", "Pvalid", "P0", "P0valid", "p0_base")
if store.has("Rz") and "r0_base" in st:
tg["R"] = prep_aux(store, idx, st, M & vv[:, None], "Rz", "Rvalid", "R0z", "R0valid", "r0_base")
if tg_lat is not None:
tg["LAT"] = tg_lat
return b, tg, vv
def load_raw(args, cfg, split, subset=True, regimes=None):
regimes = regimes or cfg["regimes"]
need = need_streams(cfg)
if args.synthetic:
n = {"train": args.synthetic, "val": max(args.synthetic // 4, 64), "test": max(args.synthetic // 2, 64)}[split]
parts = [ds.synthetic_raw(n, seed=args.seed + 10 * i + {"train": 0, "val": 1, "test": 2}[split], mode=ds.MODE[r], need=need)
for i, r in enumerate(regimes)]
raw = synthetic_aux(ds.concat_regimes(parts) if len(parts) > 1 else dict(parts[0], regime_row=parts[0]["idx"]), args.seed)
return synthetic_latent(raw, cfg, args.seed) if latent_enabled(cfg) else raw
mx = (args.max_anchors or None) if subset else None
raw = ds.load_regimes(regimes, split, need, max_anchors=mx, seed=args.seed + 7, verbose=lambda m: log(m, args.out))
attach_aux_targets(raw, regimes, split)
if latent_enabled(cfg):
attach_latent_targets(raw, regimes, split, latent_horizons(cfg))
return raw
def latent_horizons(cfg):
return tuple(int(k) for k in (cfg.get("latent_aux") or {}).get("horizons", LA.HORIZONS))
def attach_latent_targets(raw, regimes, split, horizons=LA.HORIZONS):
"""C4 (2026-09-17): row-aligned FUTURE road quantities of results/targets_road_dyn/road_dyn_{regime}_{split}.npz (Yf [N,25,4] lead_dRel / lead_vRel / lead_vLead /
lane_center_offset, Yvalid) for the rows of `raw`, sliced to the latent horizons -> raw["LYf"] [N,K,4] f32, raw["LYv"] [N,K,4] bool (uid / t witness asserted, as
attach_aux_targets). Strict future: Yf at index k-1 is the quantity at t + 0.2 k s (data/build_road_dynamics_targets.py contract, tests/test_road_dynamics_targets.py).
Missing file -> assertion (the lane requires the targets; never silently skipped)."""
idx = np.asarray([k - 1 for k in horizons], np.int64)
Yf, Yv = [], []
for r in regimes:
sel = raw["regime_id"] == ds.MODE[r]
rows = raw["regime_row"][sel]
f = ROAD_DYN / f"road_dyn_{r}_{split}.npz"
assert f.exists(), f"C4 latent targets need {f}"
with np.load(f, allow_pickle=True) as z:
assert np.array_equal(z["uid"][rows], raw["uid"][sel]) and np.allclose(z["t"][rows], raw["t"][sel]), f"{f}: uid/t witness"
assert [str(c) for c in z["target_channels"]] == list(LA.CONT[:4]), f"{f}: target_channels {list(z['target_channels'])}"
Yf.append(np.ascontiguousarray(z["Yf"][rows][:, idx, :].astype(np.float32)))
Yv.append(np.ascontiguousarray(z["Yvalid"][rows][:, idx, :].astype(bool)))
raw["LYf"], raw["LYv"] = np.concatenate(Yf), np.concatenate(Yv)
return raw
def latent_stats_from_files(regimes, horizons=LA.HORIZONS):
"""TRAIN mean / sd of the 5 continuous latent channels (both regimes pooled, valid entries at the horizons, float64) -> lat_mu / lat_sd / lat_horizons."""
idx = np.asarray([k - 1 for k in horizons], np.int64)
Yf, Yv = [], []
for r in regimes:
with np.load(ROAD_DYN / f"road_dyn_{r}_train.npz", allow_pickle=True) as z:
Yf.append(z["Yf"][:, idx, :].astype(np.float32)); Yv.append(z["Yvalid"][:, idx, :].astype(bool))
st = LA.latent_stats(np.concatenate(Yf), np.concatenate(Yv))
st["lat_horizons"] = np.asarray(horizons, np.int64)
return st
def synthetic_latent(raw, cfg, seed=0):
rng = np.random.default_rng(seed + 5)
N, K = len(raw["t"]), len(latent_horizons(cfg))
raw["LYf"] = np.stack([rng.uniform(5, 120, (N, K)), rng.normal(0, 3, (N, K)), rng.uniform(0, 30, (N, K)), rng.normal(0, 0.4, (N, K))], -1).astype(np.float32)
raw["LYv"] = rng.random((N, K, 4)) < 0.8
return raw
def attach_aux_targets(raw, regimes, split):
"""row-aligned road-pressure (P) and graded-response (R) targets for the rows of `raw` (per regime: regime_row), uid/t witness asserted.
Rz = hard response (R >= 2) per step, R0z at the anchor. Missing files -> the auxiliary heads are skipped (keys absent)."""
parts = {k: [] for k in AUX_KEYS}
for r in regimes:
rows = raw["regime_row"][raw["regime_id"] == ds.MODE[r]]
pf, rf = PRESSURE / f"pressure_{r}_{split}.npz", RESPONSE / f"response_{r}_{split}.npz"
if not (pf.exists() and rf.exists()):
return raw
with np.load(pf, allow_pickle=True) as z:
assert np.array_equal(z["uid"][rows], raw["uid"][raw["regime_id"] == ds.MODE[r]]) and np.allclose(z["t"][rows], raw["t"][raw["regime_id"] == ds.MODE[r]]), f"{pf}: uid/t witness"
assert [str(f) for f in z["families"]] == list(P_CHAINS), f"{pf}: families {list(z['families'])}"
parts["P"].append(z["P"][rows].astype(bool)); parts["Pvalid"].append(z["Pvalid"][rows].astype(bool))
parts["P0"].append(z["P0"][rows].astype(bool)); parts["P0valid"].append(z["P0valid"][rows].astype(bool))
with np.load(rf, allow_pickle=True) as z:
assert np.array_equal(z["uid"][rows], raw["uid"][raw["regime_id"] == ds.MODE[r]]), f"{rf}: uid witness"
parts["Rz"].append((z["R"][rows] >= 2)[:, :, None]); parts["Rvalid"].append(z["Rvalid"][rows].astype(bool)[:, :, None])
parts["R0z"].append((z["R0"][rows] >= 2)[:, None]); parts["R0valid"].append(z["R0valid"][rows].astype(bool)[:, None])
for k, v in parts.items():
raw[k] = np.concatenate(v)
return raw
def synthetic_aux(raw, seed=0):
rng = np.random.default_rng(seed)
N = len(raw["t"])
raw["P"], raw["Pvalid"] = rng.random((N, 25, 6)) < 0.03, rng.random((N, 25, 6)) < 0.9
raw["P0"], raw["P0valid"] = rng.random((N, 6)) < 0.03, rng.random((N, 6)) < 0.9
raw["Rz"], raw["Rvalid"] = rng.random((N, 25, 1)) < 0.05, rng.random((N, 25, 1)) < 0.95
raw["R0z"], raw["R0valid"] = rng.random((N, 1)) < 0.05, rng.random((N, 1)) < 0.95
return raw
def load_bank(args, cfg, info):
if args.synthetic:
return ds.synthetic_bank()
e_tok = info["base_cfgs"]["E"].get("tokens", "region")
need_region = e_tok == "region" or (cfg.get("tok_source", "region") == "region" and "road2drv" in cfg.get("interactions", []))
return ds.load_bank(need_tok=need_region, need_pooled=(e_tok == "pooled"))
def label_rows(args, cfg):
"""{regime: sorted int rows of the TRAIN split} the warning head is trained on (data/labels.py); {} when unavailable / synthetic."""
if args.synthetic or args.no_label_rows:
return {}
try:
import subprocess
dump = Path(args.out) / "label_rows.npz"
if not dump.exists():
subprocess.run([sys.executable, str(CWM3 / "data" / "labels.py"), "--dump-rows", str(dump), "--sources", "consensus,legacy_v1"], check=True,
capture_output=True, timeout=1800) # separate process: the parent must not import torch-heavy modules before forking
with np.load(dump) as z:
return {r: z[r].astype(np.int64) for r in cfg["regimes"] if r in z.files}
except Exception as ex: # noqa: BLE001
log(f"label rows unavailable: {ex!r}", args.out)
return {}
# ----------------------------------------------------------------------------- model / optimiser
def build_model(args, cfg, device):
"""CWM3Joint on `device` from the base checkpoints (synthetic: fresh branches with the base configs)."""
import torch
from models.cwm3 import CWM3Joint, merge_stats
from models import build_branch
kw = dict(interactions=tuple(cfg.get("interactions", [])), naive=bool(cfg.get("naive", False)), decoder=cfg.get("decoder", "direct"),
k_head=True, i_hidden=int(cfg.get("i_hidden", 640)), dropout=float(cfg.get("dropout", 0.1)), tok_source=cfg.get("tok_source", "region"))
if cfg.get("u_kpf"):
kw["kpf_dim"] = 10 # LANE F1 injector (models/kpf_inject.py); the kwarg is absent otherwise (older configs unchanged)
if cfg.get("mask_V"):
kw["mask_V"] = True # variant `ue_noV` (2026-09-18): vehicle branch = missing modality; the kwarg is absent otherwise
if latent_enabled(cfg):
kw["latent_aux"] = dict(cfg["latent_aux"]) # C4 (2026-09-17): built only with lam > 0; the kwarg is absent otherwise (older configs unchanged)
if args.synthetic:
canh = "8s" if cfg["base"]["V"][1] == "can8s" else "4s"
e_tok = "pooled" if cfg["base"]["E"][1] == "pooled" else "region"
V = build_branch("V", args.seed, d=256, hidden=256, dropout=0.1)
U = build_branch("U", args.seed, hid=128, hidden=128, dropout=0.0, cabin="none")
E = build_branch("E", args.seed, tokens=e_tok, n_slots=16, d=128, n_layers=2, n_heads=4, d_ff=256, hidden=256, dropout=0.1)
model = CWM3Joint(V, U, E, seed=args.seed, **kw)
info = {"base_ckpts": {"V": "synthetic", "U": "synthetic", "E": "synthetic"}, "stats": {},
"base_cfgs": {"V": {"can_hist": canh}, "U": {"cabin": "none"}, "E": {"tokens": e_tok, "n_slots": 16}}, "params": model.param_report()}
return model.to(device), info
base = {b: tuple(x) for b, x in cfg["base"].items()}
dirs = {b: str(d) for b, d in base_dirs(cfg, args.seed).items()} # explicit dirs (suffixed bases such as _v2)
if cfg.get("fresh_trunks"):
# naive_fresh control (coordinator 2026-09-10): the SAME architecture with every trunk / head randomly initialised (build_branch with the base
# runs' model kwargs and this seed); statistics stay the data-derived train statistics of the base runs
from models.cwm3 import _model_kwargs
info = base_info_numpy(cfg, args.seed)
mods = {}
for b, d in base_dirs(cfg, args.seed).items():
c = json.loads((d / "config.json").read_text())["cfg"]
mods[b] = build_branch(b, args.seed, **_model_kwargs(c))
model = CWM3Joint(mods["V"], mods["U"], mods["E"], seed=args.seed, **kw)
info["base_ckpts"] = {b: f"FRESH(random init, seed {args.seed}; arch of {n})" for b, n in info["base_ckpts"].items()}
return model.to(device), info
model, info = build_joint(args.seed, base=base, device="cpu", dirs=dirs, stats_mode=cfg.get("stats_mode", "merged"), **kw)
for b in ("V", "U", "E"):
assert info["base_ckpts"][b].split(":")[0] == Path(dirs[b]).name, (b, info["base_ckpts"], dirs)
return model.to(device), info
def synthetic_info(args, cfg):
"""parent-side info for --synthetic without touching torch (mirrors build_model's synthetic branch)."""
canh = "8s" if cfg["base"]["V"][1] == "can8s" else "4s"
e_tok = "pooled" if cfg["base"]["E"][1] == "pooled" else "region"
return {"base_ckpts": {"V": "synthetic", "U": "synthetic", "E": "synthetic"}, "stats": {},
"base_cfgs": {"V": {"can_hist": canh}, "U": {"cabin": "none"}, "E": {"tokens": e_tok, "n_slots": 16}}}
def write_params_once(args, model, info):
p = Path(args.out) / "params.json"
if not p.exists():
rep = model.param_report()
p.write_text(json.dumps(rep, indent=1, default=str))
info["params"] = rep
rep["p_head_source"] = getattr(model, "p_head_source", "")
log(f"model: total {rep['total']:,} params, new {rep['new_total']:,} (I {rep['interaction_total']:,} incl. tok_embed {rep['tok_embed']:,}; "
f"K {rep['k_head']:,} P {rep['p_head']:,} R {rep['r_head']:,}); P head {rep['p_head_source']}", args.out)
else:
info["params"] = json.loads(p.read_text())
def make_optim(model, rec, epochs, mtl=None):
"""mtl (2026-09-16 WP3 block): the MTL object's own parameters (uw: 3 log-variances) join the optimiser as a last group at the new-module lr with
weight decay 0; every other kind (none / famo / pcgrad) adds nothing, so the optimiser is exactly the original one."""
import torch
mult = float(rec.get("lr_base_mult", 0.1))
base, new = list(model.base_parameters()), model.new_parameters()
groups = [{"params": new, "lr": float(rec["lr"])}]
if mult > 0:
groups.append({"params": base, "lr": float(rec["lr"]) * mult})
else:
for p in base:
p.requires_grad_(False)
if mtl is not None and mtl.optim_params():
groups.append({"params": mtl.optim_params(), "lr": float(rec["lr"]), "weight_decay": 0.0})
opt = torch.optim.AdamW(groups, lr=float(rec["lr"]), weight_decay=float(rec["weight_decay"]))
if rec.get("sched", "none") == "warmcos":
warm = torch.optim.lr_scheduler.LinearLR(opt, 0.1, total_iters=3)
cosn = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=max(epochs - 3, 1))
sched = torch.optim.lr_scheduler.SequentialLR(opt, [warm, cosn], [3])
else:
sched = torch.optim.lr_scheduler.LambdaLR(opt, lambda e: 1.0)
return opt, sched
# ----------------------------------------------------------------------------- child: training chunk
WATCHDOG_S = 300 # coordinator rule (2026-09-10): no progress within 5 min of the lock -> dump stacks, exit, release the lock
def arm_watchdog(seconds, out):
import faulthandler
faulthandler.cancel_dump_traceback_later()
faulthandler.dump_traceback_later(seconds, repeat=False, exit=True)
log(f"watchdog armed: {seconds:.0f}s", out)
def add_latent_loss(model, lat_lam, out_, tg, loss, parts):
"""C4 (2026-09-17): loss + lam * L_aux when the model carries the latent module and the batch its targets; parts lat_* (never chain_* -> selection unchanged).
Without the module (every older variant and the lam = 0 control) returns loss / parts untouched."""
if getattr(model, "latent_aux", None) is None or "LAT" not in out_ or "LAT" not in tg:
return loss, parts
la, pl = model.latent_aux.loss(out_["LAT"], tg["LAT"])
parts.update(pl)
parts["lat_total"] = la
return loss + float(lat_lam) * la, parts
def val_objective(model, store, rows, st, pc, lam, device, amp, bs=8192, region_keys=False, select_LF=True, lat_lam=0.0):
"""select_LF (2026-09-11 fix): the recipe flag is passed in explicitly — the earlier edit referenced `cfg`, which is not in this scope, so every
TRAINED joint variant crashed at its first validation (NameError; the no_train `indep` variant never validates and survived)."""
import torch
model.eval()
tot_sel, tot, n, acc = 0.0, 0.0, 0, {}
with torch.no_grad():
for i in range(0, len(rows), bs):
idx = torch.as_tensor(rows[i:i + bs], device=device)
b, tg, _ = prep_joint(store, idx, st, pc, True, region_keys)
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=amp):
out = model(b)
loss, parts = joint_loss(out, tg, lam)
loss, parts = add_latent_loss(model, lat_lam, out, tg, loss, parts) # C4: val_loss_full includes lam * L_aux; the selection objective does not
tot_sel += hd.selection_objective(parts, bool(select_LF)) * len(idx)
tot += float(loss) * len(idx)
for k, v in parts.items():
acc[k] = acc.get(k, 0.0) + float(v) * len(idx)
n += len(idx)
return tot_sel / max(n, 1), tot / max(n, 1), {k: v / max(n, 1) for k, v in acc.items()}
def run_chunk(args, cfg, tr, va, st, bank, info):
import torch
torch.set_num_threads(4)
t0 = time.time()
out = Path(args.out)
arm_watchdog(WATCHDOG_S, out)
device = "cuda" if torch.cuda.is_available() and not args.cpu else "cpu"
rec = cfg["recipe"]
epochs = args.epochs or int(rec["max_epochs"])
amp = device == "cuda" and cfg.get("amp", True) and not args.no_amp
model, _ = build_model(args, cfg, device)
write_params_once(args, model, info)
pb = info["params"]
pc = apply_mask_v(prep_cfgs(info), cfg)
region_keys = model.tok_embed is not None
mtl = MTL(cfg.get("mtl", "none"), device=device, seed=args.seed) # WP3 MTL block (train/mtl.py); "none" = the original training path
lat_lam = latent_lam(cfg)
assert not (lat_lam > 0 and mtl.kind != "none"), "C4 latent_aux is defined on the plain-sum path only"
opt, sched = make_optim(model, rec, epochs, mtl)
state = {"epoch": 0, "batch_pos": 0, "best": 1e18, "best_epoch": -1, "patience": 0, "acq": 0, "done": False}
ck = T.latest_ckpt(out)
if ck is not None:
z = torch.load(ck, map_location=device, weights_only=False)
model.load_state_dict(z["model"])
state = z["state"]
opt.load_state_dict(z["opt"])
sched.load_state_dict(z["sched"])
if z.get("mtl") is not None:
mtl.load_state_dict(z["mtl"])
torch.set_rng_state(z["rng_torch"].cpu())
if device == "cuda" and z.get("rng_cuda") is not None:
torch.cuda.set_rng_state(z["rng_cuda"].cpu())
log(f"resumed {ck.name}: epoch {state['epoch']} batch_pos {state['batch_pos']} best {state['best']:.4f}", out)
else:
torch.manual_seed(args.seed)
state["acq"] += 1
if state["done"]:
return 0
rows_tr, rows_va = np.arange(len(tr["t"])), np.arange(len(va["t"]))
store_tr, store_va = make_store(tr, device, bank), make_store(va, device, bank)
log(f"chunk {state['acq']} start: {cfg['stage']}/{cfg['variant']} s{args.seed} device={device} amp={amp} params={pb['total']:,} "
f"(new {pb['new_total']:,}: I {pb['interaction_total']:,} K {pb['k_head']:,}) train rows {len(rows_tr):,} val rows {len(rows_va):,} "
f"gpu store {(store_tr.bytes() + store_va.bytes()) / 2**30:.2f} GiB epochs={epochs} rows/epoch={rec['rows_per_epoch'] or 'all'} "
f"batch={rec['batch']} lr={rec['lr']} base_mult={rec.get('lr_base_mult', 0.1)} decoder={cfg.get('decoder')} inter={cfg.get('interactions')} "
f"naive={cfg.get('naive')} mtl={mtl.kind} latent_aux={cfg.get('latent_aux') if lat_lam > 0 else None} (params {pb.get('latent_aux', 0):,}) V={cfg['base']['V']}"
f"{' MASK_V (vehicle branch = missing modality)' if cfg.get('mask_V') else ''} ({time.time() - t0:.0f}s)", out)
tag_tr = tr["slice_tag"][rows_tr]
cnt = np.array([(tag_tr == v).sum() for v in range(4)], float)
p_s = (1.0 / np.maximum(cnt, 1))[tag_tr]
p_s /= p_s.sum()
bs, rows_per_epoch = int(rec["batch"]), int(rec["rows_per_epoch"])
def save():
free_gb = shutil.disk_usage(out).free / 2**30
assert free_gb >= 15.0, f"free disk {free_gb:.1f} GB < 15 GB: refusing to checkpoint"
p = out / f"ckpt_ep{state['epoch']:03d}_b{state['batch_pos']:05d}.pt"
assert not p.exists()
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "sched": sched.state_dict(), "state": state,
"rng_torch": torch.get_rng_state(), "rng_cuda": torch.cuda.get_rng_state() if device == "cuda" else None,
"cfg": cfg, "seed": args.seed, "stats": st, "params": pb, "base_ckpts": info["base_ckpts"],
"mtl": mtl.state_dict() if mtl.kind != "none" else None}, p)
return p
def over_budget():
return (time.time() - t0) >= args.minutes * 60
while state["epoch"] < epochs and not state["done"]:
ep_ = state["epoch"]
rng = np.random.default_rng(args.seed * 1000 + ep_)
if rows_per_epoch > 0:
idx_all = rows_tr[rng.choice(len(rows_tr), size=min(len(rows_tr), rows_per_epoch), replace=False, p=p_s)]
else:
idx_all = rows_tr[rng.permutation(len(rows_tr))]
model.train()
nb = (len(idx_all) + bs - 1) // bs
tot, nbatch = 0.0, 0
while state["batch_pos"] < nb:
i = state["batch_pos"]
idx = torch.as_tensor(idx_all[i * bs:(i + 1) * bs], device=device)
b, tg, _ = prep_joint(store_tr, idx, st, pc, True, region_keys)
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=amp):
out_ = model(b)
if mtl.kind == "none":
loss, parts = joint_loss(out_, tg, cfg["lambda"])
loss, parts = add_latent_loss(model, lat_lam, out_, tg, loss, parts) # C4 (2026-09-17): + lam * L_aux; identity when the module is absent
opt.zero_grad(set_to_none=True)
loss.backward()
else:
# WP3 MTL block (train/mtl.py): uw / famo re-weight the three branch losses, pcgrad projects their conflicting gradients on the shared
# parameters (probe on the first batch of every chunk); `loss` stays the PLAIN sum for the history / logs, clipping + step unchanged
loss, parts, br = joint_loss(out_, tg, cfg["lambda"], return_branches=True)
opt.zero_grad(set_to_none=True)
if mtl.kind == "pcgrad" and (mtl.shared is None or nbatch == 0):
trainable = [p for p in model.parameters() if p.requires_grad]
mtl.probe_shared(br, trainable)
(out / "mtl_probe.json").write_text(json.dumps(mtl.probe_info, indent=1))
log(f"pcgrad probe: {mtl.probe_info}", out)
mtl.backward(loss, br)
torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], float(cfg.get("grad_clip", 1.0)))
opt.step()
if getattr(model, "latent_aux", None) is not None:
model.latent_aux.ema_update() # C4: EMA target encoder follows the online encoder after every step
if mtl.kind == "famo":
with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16, enabled=amp):
_, _, br_after = joint_loss(model(b), tg, cfg["lambda"], return_branches=True)
mtl.famo_update(br_after)
tot += float(loss)
nbatch += 1
state["batch_pos"] += 1
if nbatch == 1:
arm_watchdog(args.minutes * 60 + WATCHDOG_S, out) # first batch done: guard the whole chunk instead
if nbatch % 100 == 0:
log(f" ep {ep_} b {i + 1}/{nb} loss {tot / nbatch:.4f} " + " ".join(f"{k}={float(v):.3f}" for k, v in parts.items() if k.startswith("chain_") or k == "L_F" or k.startswith("lat_"))
+ (f" mtl[{mtl.kind}] w={['%.3g' % w for w in mtl.weights()]}" if mtl.kind != "none" else ""), out)
if mtl.kind != "none":
assert mtl.finite(), f"MTL weights not finite: {mtl.state_dict()}"
if over_budget() and state["batch_pos"] < nb:
p = save()
log(f"wall budget reached mid-epoch -> saved {p.name}; exit 3", out)
return 3
sched.step()
v, v_full, vparts = val_objective(model, store_va, rows_va, st, pc, cfg["lambda"], device, amp, region_keys=region_keys,
select_LF=bool(cfg["recipe"].get("select_LF", True)), lat_lam=lat_lam)
state["epoch"] += 1
state["batch_pos"] = 0
improved = v < state["best"] - 1e-5
if improved:
state["best"], state["best_epoch"], state["patience"] = v, ep_, 0
else:
state["patience"] += 1
per_branch = {"V": sum(vparts.get(f"chain_{c}", 0.0) for c in hd.V_CHAINS), "U": sum(vparts.get(f"chain_{c}", 0.0) for c in hd.U_CHAINS),
"E": sum(vparts.get(f"chain_{c}", 0.0) for c in hd.E_CHAINS) + vparts.get("L_F", 0.0)}
rec_h = {"epoch": ep_, "train_loss": tot / max(nbatch, 1), "val_objective": v, "val_objective_kind": "sum of chain NLLs (V masked to V-valid rows) + 0.1 L_F",
"val_loss_full": v_full, "val_parts": vparts, "val_per_branch": per_branch, "best": state["best"], "best_epoch": state["best_epoch"],
"lr": [g["lr"] for g in opt.param_groups], "acq": state["acq"], "t_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
**({"mtl": mtl.kind, "mtl_weights": mtl.weights()} if mtl.kind != "none" else {})}
with open(out / "history.jsonl", "a") as f:
f.write(json.dumps(rec_h) + "\n")
log(f"ep {ep_}: train {rec_h['train_loss']:.4f} val_select {v:.4f} (V {per_branch['V']:.4f} U {per_branch['U']:.4f} E {per_branch['E']:.4f}) "
f"val_full {v_full:.4f} best {state['best']:.4f}@{state['best_epoch']} ({time.time() - t0:.0f}s)", out)
if state["patience"] >= int(rec["patience"]) or state["epoch"] >= epochs:
state["done"] = True
p = save()
if improved:
(out / "BEST").write_text(p.name + "\n")
T.prune_ckpts(out, keep={p.name, (out / "BEST").read_text().strip() if (out / "BEST").exists() else ""})
if state["done"]:
(out / "TRAIN_DONE").write_text(json.dumps({"best_epoch": state["best_epoch"], "best": state["best"], "epochs_run": state["epoch"],
"acq": state["acq"], "params": pb}))
log("training done", out)
import faulthandler
faulthandler.cancel_dump_traceback_later()
return 0
if over_budget():
log("wall budget reached at epoch end; exit 3", out)
return 3
return 0
# ----------------------------------------------------------------------------- child: prediction
def load_best_model(args, cfg, device, info):
import torch
model, _ = build_model(args, cfg, device)
best = (Path(args.out) / "BEST").read_text().strip() if (Path(args.out) / "BEST").exists() else "base"
if best != "base":
z = torch.load(Path(args.out) / best, map_location=device, weights_only=False)
model.load_state_dict(z["model"])
model.eval()
return model, best
def predict_split(args, cfg, raw, st, bank, info, regime, split, rows=None):
"""all rows of one (regime, split) pack -> pred_{regime}_{split}.npz; with `rows` (train label rows) -> pred_{regime}_train_labelrows.npz."""
import torch
import eval_s13 as e13
torch.set_num_threads(4)
t0 = time.time()
out = Path(args.out)
arm_watchdog(WATCHDOG_S, out)
device = "cuda" if torch.cuda.is_available() and not args.cpu else "cpu"
amp = device == "cuda" and cfg.get("amp", True) and not args.no_amp
model, best = load_best_model(args, cfg, device, info)
write_params_once(args, model, info)
pc = apply_mask_v(prep_cfgs(info), cfg)
region_keys = model.tok_embed is not None
N = len(raw["t"])
store = make_store(raw, device, bank)
C = len(CHAINS)
valid_U = raw["kin_row_ok"].astype(bool) if "kin_row_ok" in raw else np.ones(N, bool) # causal kin: rows without a causal box are not scored (2026-09-11)
P = {"hz_on": np.zeros((N, 25, C), np.float16), "hz_off": np.zeros((N, 25, C), np.float16), "z0": np.zeros((N, C), np.float16),
"z0valid": np.ones((N, C), bool), "valid_V": v_valid_np(raw, pc), "valid_U": valid_U, "valid_E": np.ones(N, bool),
"y_mae": np.full((N, 3), np.nan, np.float32), "y_crps": np.full((N, 3), np.nan, np.float32), "pi_resp": np.zeros((N, 25), np.float16),
"f_cos": np.full((N, 5), np.nan, np.float32), "k_mae": np.full(N, np.nan, np.float32), "K_hat": np.zeros((N, 3, 11), np.float16),
"K_sig": np.zeros((N, 3, 11), np.float16)}
has_aux = store.has("P") and "p0_base" in st and model.p_head is not None
if has_aux:
P.update(hz_on_P=np.zeros((N, 25, 6), np.float16), hz_off_P=np.zeros((N, 25, 6), np.float16), z0_P=np.zeros((N, 6), np.float16), z0valid_P=np.ones((N, 6), bool),
hz_on_R=np.zeros((N, 25, 1), np.float16), hz_off_R=np.zeros((N, 25, 1), np.float16), z0_R=np.zeros((N, 1), np.float16), z0valid_R=np.ones((N, 1), bool))
has_g = getattr(model.U, "gaze_head", None) is not None and store.has("Sg")
if has_g: # gaze_off chain of the gaze U bases (2026-09-12 appendix variant): the base trainer's extra blocks, same names / shapes
CG = len(hd.G_CHAINS)
P.update(hz_on_G=np.zeros((N, 25, CG), np.float16), hz_off_G=np.zeros((N, 25, CG), np.float16), z0_G=np.zeros((N, CG), np.float16), z0valid_G=np.zeros((N, CG), bool))
TGG = {"zp": [], "z": [], "m": []}
TGA = {"P": {"zp": [], "z": [], "m": []}, "R": {"zp": [], "z": [], "m": []}}
TG = {"zp": [], "z": [], "m": []}
stV = branch_stats(st, "V")
ymu, ysd = torch.as_tensor(stV["ymu"], device=device), torch.as_tensor(stV["ysd"], device=device)
gen = torch.Generator(device=device).manual_seed(args.seed + 99)
bs = 4096
with torch.no_grad():
for i in range(0, N, bs):
idx = torch.arange(i, min(i + bs, N), device=device)
sl = slice(i, i + len(idx))
b, tg, vv = prep_joint(store, idx, st, pc, True, region_keys)
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=amp):
o = model(b)
if i == 0:
arm_watchdog(args.minutes * 60 + WATCHDOG_S, out)
v, u, e = hd.split_V(o["V"]), hd.split_U(o["U"]), hd.split_E(o["E"])
hon_v, hoff_v = hd.hazards(v["chains"])
hon_u, hoff_u = hd.hazards(u["chains"])
hon_e, hoff_e = hd.hazards(e["chains"])
h_on, h_off = torch.cat([hon_v, hon_u, hon_e], -1), torch.cat([hoff_v, hoff_u, hoff_e], -1)
z0 = torch.cat([tg["V"]["z0"], tg["U"]["z0"], tg["E"]["z0"]], -1)
zp = torch.cat([tg["V"]["zV_prev"], tg["U"]["zU_prev"], tg["E"]["zD_prev"]], -1)
zz = torch.cat([tg["V"]["zV"], tg["U"]["zU"], tg["E"]["zD"]], -1)
mm = torch.cat([tg["V"]["zVm"], tg["U"]["zUm"], tg["E"]["zDm"]], -1)
P["hz_on"][sl], P["hz_off"][sl], P["z0"][sl] = h_on.cpu().numpy(), h_off.cpu().numpy(), z0.cpu().numpy()
P["z0valid"][sl, 3] = store.t["Resp0valid"][idx].cpu().numpy()
P["z0valid"][sl, 4:6] = tg["U"]["z0valid"].cpu().numpy()
P["z0valid"][sl, 6:] = tg["E"]["z0valid"].cpu().numpy()
# vehicle continuous emissions (rows with the V history)
mean = torch.stack([hd.mdn_mean(v["steer"]), hd.mdn_mean(v["vEgo"]), hd.mdn_mean(v["aEgo"])], -1) * ysd + ymu
Y = store.t["Y"][idx].float()[..., [0, 3, 4]]
M = tg["V"]["M"].float()[..., None]
P["y_mae"][sl] = ((mean - Y).abs() * M).sum(1).div(M.sum(1).clamp_min(1)).cpu().numpy()
smp = torch.stack([hd.mdn_sample(v[c], 32, gen) for c in ("steer", "vEgo", "aEgo")], -1) * ysd + ymu
t1 = (smp - Y[None]).abs().mean(0)
t2 = (smp[:, None] - smp[None]).abs().mean((0, 1))
P["y_crps"][sl] = ((t1 - 0.5 * t2) * M).sum(1).div(M.sum(1).clamp_min(1)).cpu().numpy()
P["pi_resp"][sl] = hd.forward_marginal(hon_v[..., 3], hoff_v[..., 3], tg["V"]["z0"][:, 3]).cpu().numpy()
# driver kinematics (detached K head): K̂ = K_0 + ΔK̂, standardised
K_mu = tg["U"]["K0std"][:, None, :] + u["dK_mu"]
Km = tg["U"]["Km"].float()
P["k_mae"][sl] = (((K_mu - tg["U"]["Kstd"]).abs() * Km).sum((1, 2)) / Km.sum((1, 2)).clamp_min(1)).cpu().numpy()
P["K_hat"][sl] = K_mu[:, [4, 14, 24], :].cpu().numpy()
P["K_sig"][sl] = u["K_logsig"][:, [4, 14, 24], :].clamp(-7, 5).exp().cpu().numpy()
cos = torch.nn.functional.cosine_similarity(o["F"].float(), tg["E"]["F"].float(), dim=-1)
P["f_cos"][sl] = torch.where(tg["E"]["Fok"], cos, torch.full_like(cos, float("nan"))).cpu().numpy()
TG["zp"].append(zp.cpu().numpy().astype(np.float32)); TG["z"].append(zz.cpu().numpy().astype(np.float32)); TG["m"].append(mm.cpu().numpy())
if has_aux:
for key, nm in (("P", "P"), ("R", "R")):
c = o[key].float().reshape(len(idx), 25, -1, 2)
hon, hoff = hd.hazards(c)
P[f"hz_on_{nm}"][sl], P[f"hz_off_{nm}"][sl] = hon.cpu().numpy(), hoff.cpu().numpy()
P[f"z0_{nm}"][sl], P[f"z0valid_{nm}"][sl] = tg[key]["z0"].cpu().numpy(), tg[key]["z0valid"].cpu().numpy()
TGA[key]["zp"].append(tg[key]["z_prev"].cpu().numpy().astype(np.float32)); TGA[key]["z"].append(tg[key]["z"].cpu().numpy().astype(np.float32)); TGA[key]["m"].append(tg[key]["m"].cpu().numpy())
if has_g and "G" in o and "zG" in tg["U"]:
tgu = tg["U"]
hg_on, hg_off = hd.hazards(o["G"].float().reshape(len(idx), 25, 1, 2))
P["hz_on_G"][sl], P["hz_off_G"][sl] = hg_on.cpu().numpy(), hg_off.cpu().numpy()
P["z0_G"][sl], P["z0valid_G"][sl] = tgu["zG0"][:, None].cpu().numpy(), tgu["zG0valid"][:, None].cpu().numpy()
TGG["zp"].append(tgu["zG_prev"][:, :, None].cpu().numpy().astype(np.float32)); TGG["z"].append(tgu["zG"][:, :, None].cpu().numpy().astype(np.float32)); TGG["m"].append(tgu["zGm"][:, :, None].cpu().numpy())
if (i // bs) % 25 == 0:
log(f" predict {regime}/{split} {i + len(idx)}/{N} ({time.time() - t0:.0f}s)", out)
TG = {k: np.concatenate(v) for k, v in TG.items()}
vV = P["valid_V"]
vU = P["valid_U"]
P["y_mae"][~vV] = np.nan
P["y_crps"][~vV] = np.nan
P["k_mae"][~vU] = np.nan
tag = f"{regime}_{split}" if rows is None else f"{regime}_train_labelrows"
qm = {"n_rows": int(N), "n_rows_predicted": int(N), "n_rows_V": int(vV.sum()), "n_rows_U": int(vU.sum()), "regime": regime, "split": split, "branch": "joint"}
if rows is None:
hz_on, hz_off = P["hz_on"].astype(np.float64), P["hz_off"].astype(np.float64)
events = [(c, c, "on") for c in hd.V_CHAINS] + [("hand_return", "hands_on", "on"), ("hand_leave", "hands_on", "off"), ("head_turn_onset", "head_turned", "on"),
("head_return", "head_turned", "off")] + [(c, c, "on") for c in hd.E_CHAINS]
for name, ch, kind in events:
c = CHAINS.index(ch)
keep = vV if ch in hd.V_CHAINS else vU if ch in hd.U_CHAINS else np.ones(N, bool)
zp, zz, mm = TG["zp"][keep][..., c], TG["z"][keep][..., c], TG["m"][keep][..., c].astype(bool)
if kind == "off":
zp, zz, h = 1 - zp, 1 - zz, hz_off[keep][..., c]
else:
h = hz_on[keep][..., c]
try:
for r in e13.transition_metrics(h, zp, zz, mm, name):
qm[f"{name}:{r['metric']}"] = r["value"]
if "base_rate" in r:
qm[f"{name}:{r['metric']}:base_rate"], qm[f"{name}:{r['metric']}:n"] = r["base_rate"], r["n"]
except Exception as ex: # noqa: BLE001 (too few rows at smoke scale)
qm[f"{name}:error"] = str(ex)[:120]
if has_aux:
for key, names, keep in (("P", [f"P_{c}" for c in P_CHAINS], np.ones(N, bool)), ("R", ["R_hard"], vV)):
A = {k: np.concatenate(v) for k, v in TGA[key].items()}
for c, name in enumerate(names):
try:
for r in e13.transition_metrics(P[f"hz_on_{key}"].astype(np.float64)[keep][..., c], A["zp"][keep][..., c], A["z"][keep][..., c], A["m"][keep][..., c].astype(bool), name):
qm[f"{name}:{r['metric']}"] = r["value"]
if "base_rate" in r:
qm[f"{name}:{r['metric']}:base_rate"], qm[f"{name}:{r['metric']}:n"] = r["base_rate"], r["n"]
except Exception as ex: # noqa: BLE001
qm[f"{name}:error"] = str(ex)[:120]
if has_g and TGG["z"]:
# gaze_off_on / gaze_off_off quick metrics on the U-valid rows — the base trainer's form (train_cwm3.predict_split), so the identity check can compare them
G = {k: np.concatenate(v) for k, v in TGG.items()}
for name, kind in (("gaze_off_on", "on"), ("gaze_off_off", "off")):
zp, zz, mm = G["zp"][vU][..., 0], G["z"][vU][..., 0], G["m"][vU][..., 0].astype(bool)
if kind == "off":
zp, zz, h = 1 - zp, 1 - zz, P["hz_off_G"].astype(np.float64)[vU][..., 0]
else:
h = P["hz_on_G"].astype(np.float64)[vU][..., 0]
try:
for r in e13.transition_metrics(h, zp, zz, mm, name):
qm[f"{name}:{r['metric']}"] = r["value"]
if "base_rate" in r:
qm[f"{name}:{r['metric']}:base_rate"], qm[f"{name}:{r['metric']}:n"] = r["base_rate"], r["n"]
except Exception as ex: # noqa: BLE001
qm[f"{name}:error"] = str(ex)[:120]
for k in ("y_mae", "y_crps", "f_cos", "k_mae"):
vals = P[k]
qm[k + ":mean"] = np.nanmean(vals, 0).tolist() if vals.ndim == 2 else float(np.nanmean(vals))
qm["wall_s"] = round(time.time() - t0, 1)
(out / f"metrics_quick_{regime}_{split}.json").write_text(json.dumps(qm, indent=1))
extra = {} if rows is None else {"rows": np.asarray(rows, np.int64), "uid": raw["uid"], "t": raw["t"]}
np.savez_compressed(out / f"pred_{tag}.npz", **P, **extra, regime=np.array(regime), split=np.array(split), branch=np.array("joint"), seed=np.array(args.seed),
stage=np.array(cfg["stage"]), variant=np.array(cfg["variant"]), best_ckpt=np.array(best), chain_names=np.array(CHAINS),
p_chain_names=np.array(list(P_CHAINS)), r_chain_names=np.array(list(R_CHAINS)),
hazard_kind=np.array("head"), n_rows=np.array(N), base_ckpts=np.array(json.dumps(info["base_ckpts"])),
**({"g_chain_names": np.array(list(hd.G_CHAINS))} if has_g else {}))
log(f"pred_{tag}.npz written ({N} rows, V rows {int(vV.sum())}) in {time.time() - t0:.0f}s; " +
" ".join(f"{k}={v:.3f}" for k, v in qm.items() if k.endswith("onset_prauc@3s")), out)
import faulthandler
faulthandler.cancel_dump_traceback_later()
return 0
def predict_many(args, cfg, raws, st, bank, info):
for key, raw in raws.items():
regime, split = key[0], key[1]
rows = raw.get("_label_rows")
tag = f"{regime}_{split}" if rows is None else f"{regime}_train_labelrows"
if (Path(args.out) / f"pred_{tag}.npz").exists():
continue
code = predict_split(args, cfg, raw, st, bank, info, regime, split, rows)
if code:
return code
return 0
def load_pred_raws(args, cfg, lrows):
raws = {}
for split in cfg["pred_splits"]:
for regime in cfg["regimes"]:
if not (Path(args.out) / f"pred_{regime}_{split}.npz").exists():
raws[(regime, split)] = load_raw(args, cfg, split, subset=False, regimes=[regime])
for regime, rows in lrows.items():
if len(rows) and not (Path(args.out) / f"pred_{regime}_train_labelrows.npz").exists():
full = 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
def train_then_predict(args, cfg, tr, va, st, bank, info, raws):
t0 = time.time()
code = run_chunk(args, cfg, tr, va, st, bank, info)
if code != 0:
return code
left = args.minutes * 60 - (time.time() - t0)
if raws and left >= T.PREDICT_MIN_LEFT_S:
log(f"training finished with {left / 60:.1f} min of the chunk budget left -> predicting in the same acquisition", args.out)
return predict_many(args, cfg, raws, st, bank, info)
return 0
def append_quick_rows(args, cfg, stamp, td):
rows = []
for regime in cfg["regimes"]:
for split in cfg["pred_splits"]:
p = Path(args.out) / f"metrics_quick_{regime}_{split}.json"
if not p.exists():
continue
qm = json.loads(p.read_text())
for k, v in qm.items():
if ":" not in k or k.endswith(":base_rate") or k.endswith(":n") or k.endswith(":error"):
continue
chain, metric = k.split(":", 1)
rows.append({"run_stamp": stamp, "stage": cfg["stage"], "variant": cfg["variant"], "seed": args.seed, "regime": regime, "split": split,
"branch": "joint", "chain": chain, "metric": metric, "value": v if not isinstance(v, list) else json.dumps(v),
"base_rate": qm.get(k + ":base_rate", ""), "n": qm.get(k + ":n", ""), "n_rows_predicted": qm["n_rows_predicted"],
"best_epoch": td["best_epoch"], "val_objective": td["best"], "run_dir": str(args.out)})
if args.synthetic or not rows:
return 0
new = not T.QUICK_CSV.exists()
with open(T.QUICK_CSV, "a", newline="") as f:
w = csv.DictWriter(f, fieldnames=T.QUICK_COLS)
if new:
w.writeheader()
for r in rows:
w.writerow(r)
return len(rows)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--stage", required=True, choices=list(STAGES["stages"]))
ap.add_argument("--variant", required=True)
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--base-v", default="", choices=[""] + list(T.STAGES["stages"]["V0"]["variants"]), help="override the V base variant of stages_joint.yaml")
ap.add_argument("--base-e", default="", choices=[""] + list(T.STAGES["stages"]["E0"]["variants"]), help="override the E base variant of stages_joint.yaml (e.g. pooled_P)")
ap.add_argument("--base-u", default="", choices=[""] + list(T.STAGES["stages"]["U0"]["variants"]), help="override the U base variant of stages_joint.yaml (e.g. kin_can_noK_clip8_ln)")
ap.add_argument("--base-dir-v", default="", help="explicit V base run dir (absolute, cwm3-relative or a name under results/runs); 2026-09-11 final assembly")
ap.add_argument("--base-dir-u", default="", help="explicit U base run dir")
ap.add_argument("--base-dir-e", default="", help="explicit E base run dir")
ap.add_argument("--kin-source", default="auto", choices=["auto", "cwm2", "causal_routes", "oldin_ctgt"],
help="driver kinematics source of the data layer (ds.set_kin_source); auto (default) = the U base's config.json (cwm2 for bases without the key)")
ap.add_argument("--kin-version", default="auto", choices=["auto", "v1", "v2", "v3", "v2p"], help="causal kin pack build; auto = the U base's config.json (v3 = 2026-09-16 wheel-reference labels; v2p = 2026-09-18 v2 + 3 keypoint input proxies)")
ap.add_argument("--run-suffix", default="", help="appended to the default run dir name (e.g. _final)")
ap.add_argument("--stats-mode", default="merged", choices=["merged", "per_branch"],
help="merged (default): the bases' statistics are merged with equality asserted on overlapping keys (1e-5); per_branch: each branch uses its own base's stats.npz "
"('<branch>/<key>' keys) — an explicit choice when the merged assert fires, never a loosened assert (roadmap §三 3)")
ap.add_argument("--pack-version", default="v1", choices=["v1", "v2"], help="v2 = results/pack_v2 vf256 / vc256 + aligned F (2026-09-10 latent audit); the E base dir then needs the _v2 suffix")
ap.add_argument("--base-suffix-e", default="", help="suffix of the E base run dir (e.g. _v2 -> results/runs/E0_<variant>_s<k>_v2)")
ap.add_argument("--max-anchors", type=int, default=0)
ap.add_argument("--synthetic", type=int, default=0)
ap.add_argument("--epochs", type=int, default=0)
ap.add_argument("--minutes", type=float, default=0)
ap.add_argument("--regimes", default="")
ap.add_argument("--out", default="")
ap.add_argument("--pred-splits", default="")
ap.add_argument("--no-label-rows", action="store_true", help="skip the train label-row prediction pack")
ap.add_argument("--no-lock", action="store_true", help="run inline (no fork, no /tmp/s13_gpu.lock) — owner directive 2026-09-10 13:58: the exclusive lock is no longer required; "
"the queue script gates on free VRAM / RAM / trainer count instead")
ap.add_argument("--no-amp", action="store_true")
ap.add_argument("--cpu", action="store_true", help="tests only (smoke); never a result")
ap.add_argument("--u-kpf", default="", help="LANE F1 (2026-09-17, pre-reg 2026-09-17T19:33:12Z): keypoint-forecaster run dir (results/runs/KPF_<arm>_s<k>) whose detached "
"kpf_{regime}_{split}.npz channel enters the driver state through the zero-init KpfInject; empty = the unchanged model")
ap.add_argument("--mtl", default=None, choices=list(MTL_KINDS),
help="WP3 reviewer control (2026-09-16, train/mtl.py): multi-task balancing of the three branch losses — none (default: plain sum, the original path), "
"uw (Kendall 2018 learned log-variances), famo (Liu 2023), pcgrad (Yu 2020 on the shared parameters); unset -> the variant's `mtl` key (none)")
args = ap.parse_args()
ds.set_pack_version(args.pack_version)
cfg = stage_cfg(args.stage, args.variant, args.base_v or None, args.base_e or None, args.base_u or None)
cfg["mtl"] = args.mtl if args.mtl is not None else str(cfg.get("mtl", "none"))
assert cfg["mtl"] in MTL_KINDS, cfg["mtl"]
cfg["pack_version"], cfg["base_suffix_e"] = args.pack_version, args.base_suffix_e or ("_v2" if args.pack_version == "v2" else "")
cfg["base_dirs"] = {b: v for b, v in (("V", args.base_dir_v), ("U", args.base_dir_u), ("E", args.base_dir_e)) if v}
cfg["stats_mode"], cfg["run_suffix"] = args.stats_mode, args.run_suffix
# driver kinematics source: explicit or inherited from the U base's config (the joint model must read the inputs its U base was trained on)
ks, kv = args.kin_source, args.kin_version
if not args.synthetic:
ucfg_p = base_dirs(cfg, args.seed)["U"] / "config.json"
if ucfg_p.exists():
uc = json.loads(ucfg_p.read_text())["cfg"]
ks = uc.get("kin_source", "cwm2") if ks == "auto" else ks
kv = uc.get("kin_version", "v1") if kv == "auto" else kv
assert ks == uc.get("kin_source", "cwm2") and kv == uc.get("kin_version", "v1"), f"kin source {ks}/{kv} != the U base's {uc.get('kin_source', 'cwm2')}/{uc.get('kin_version', 'v1')}"
urec = uc.get("recipe", {}) or {} # gaze U bases (2026-09-12 appendix variant): opt-in streams (need_streams) / heads follow the U base's recipe
cfg["u_gaze_chain"], cfg["u_face"] = bool(urec.get("u_gaze_chain", False)), bool(urec.get("u_face", False))
ks, kv = ("cwm2" if ks == "auto" else ks), ("v1" if kv == "auto" else kv)
ds.set_kin_source(ks, kv)
cfg["u_kpf"] = str(resolve_run_dir(args.u_kpf)) if args.u_kpf else "" # LANE F1 (2026-09-17)
if cfg["u_kpf"]:
assert (Path(cfg["u_kpf"]) / "PRED_DONE").exists(), f"--u-kpf {cfg['u_kpf']}: PRED_DONE missing"
ds.set_kpf_dir(cfg["u_kpf"])
cfg["kin_source"], cfg["kin_version"] = ks, kv
if args.regimes:
cfg["regimes"] = args.regimes.split(",")
if args.pred_splits:
cfg["pred_splits"] = args.pred_splits.split(",")
args.minutes = args.minutes or float(cfg["minutes"])
assert args.minutes <= 22 or args.no_lock, "GPU-lock chunks must be <= 22 min (unlimited only inline with --no-lock)"
if not args.out:
suffix = ("_synthetic" if args.synthetic else "") + (f"_max{args.max_anchors}" if args.max_anchors else "")
if args.base_v and args.base_v != STAGES["defaults"]["base"]["V"][1]:
suffix += "_v" + args.base_v[3]
if args.base_e and args.base_e != STAGES["defaults"]["base"]["E"][1]:
suffix += "_e" + args.base_e[:4]
if args.pack_version == "v2":
suffix += "_v2"
suffix += args.run_suffix
args.out = str(CWM3 / "results" / "runs" / f"{args.stage}_{args.variant}_s{args.seed}{suffix}")
Path(args.out).mkdir(parents=True, exist_ok=True)
t_start = time.time()
(Path(args.out) / "config.json").write_text(json.dumps({"cfg": cfg, "args": vars(args)}, indent=1, default=str))
log(f"=== train_cwm3_joint {args.stage}/{args.variant} s{args.seed} out={args.out} base={cfg['base']} synthetic={args.synthetic} "
f"max_anchors={args.max_anchors} regimes={cfg['regimes']} need={sorted(need_streams(cfg))} ===", args.out)
# PARENT: numpy / json only — no torch op before os.fork() (the OMP thread pool does not survive fork; 31-min deadlock on 2026-09-10)
if args.synthetic:
info = synthetic_info(args, cfg)
tr0 = load_raw(args, cfg, "train")
info["stats"] = bt.compute_stats(tr0, {"mu": np.array([0.0, 12.0, 0.0], np.float32), "sd": np.array([70.0, 8.0, 0.7], np.float32)})
info["stats"].update(aux_stats(tr0))
if latent_enabled(cfg):
info["stats"].update(LA.latent_stats(tr0["LYf"], tr0["LYv"]))
info["stats"]["lat_horizons"] = np.asarray(latent_horizons(cfg), np.int64)
del tr0
else:
info = base_info_numpy(cfg, args.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"] = 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
cfg["base_stats_versions"] = sv
(Path(args.out) / "config.json").write_text(json.dumps({"cfg": cfg, "args": vars(args)}, indent=1, default=str)) # re-written with the derived fields
log(f"stats_mode {info['stats_mode']}; base stats versions {sv}; kin_source {ks}/{kv}; base dirs {info['base_dirs']}", args.out)
st = {k: np.asarray(v, np.float32) for k, v in info["stats"].items()}
st_path = Path(args.out) / "stats.npz"
if st_path.exists():
st = {k: v for k, v in np.load(st_path).items()}
else:
if not args.synthetic:
st.update(aux_stats_from_files(cfg["regimes"])) # auxiliary anchor base rates (TRAIN files only)
if latent_enabled(cfg):
st.update(latent_stats_from_files(cfg["regimes"], latent_horizons(cfg))) # C4: TRAIN statistics of the continuous latent channels
np.savez(st_path, **st)
(Path(args.out) / "base_ckpts.json").write_text(json.dumps({"base_ckpts": info["base_ckpts"], "base_cfgs": info["base_cfgs"], "tok_source": cfg.get("tok_source"),
"base_dirs": info.get("base_dirs", {}), "stats_mode": info.get("stats_mode", "merged"),
"kin_source": ks, "kin_version": kv}, indent=1, default=str))
log(f"base {info['base_ckpts']}; tok_source {cfg.get('tok_source')} (the child prints the parameter report)", args.out)
bank = load_bank(args, cfg, info)
lrows = label_rows(args, cfg)
n_acq, mem = 0, {}
if cfg.get("no_train"):
if not (Path(args.out) / "TRAIN_DONE").exists():
(Path(args.out) / "TRAIN_DONE").write_text(json.dumps({"best_epoch": -1, "best": float("nan"), "epochs_run": 0, "acq": 0, "params": {}, "no_train": True}))
(Path(args.out) / "BEST").write_text("base\n")
with open(Path(args.out) / "history.jsonl", "a") as f:
f.write(json.dumps({"epoch": -1, "note": "no_train variant: base checkpoints predict as assembled"}) + "\n")
if not (Path(args.out) / "TRAIN_DONE").exists():
r0 = ds.rss_gb()
tr, va = load_raw(args, cfg, "train"), load_raw(args, cfg, "val")
mem = {"rss_after_load_gb": round(ds.rss_gb(), 2), "rss_before_gb": round(r0, 2), "arrays_train_gb": round(ds.nbytes(tr) / 2**30, 3),
"arrays_val_gb": round(ds.nbytes(va) / 2**30, 3), "bank_gb": round(sum(v.nbytes for v in bank.values() if isinstance(v, np.ndarray)) / 2**30, 3)}
log(f"loaded train {len(tr['t']):,} val {len(va['t']):,} rows; host arrays {mem['arrays_train_gb'] + mem['arrays_val_gb']:.2f} GiB, RSS {mem['rss_after_load_gb']:.2f} GiB", args.out)
raws = {} if args.no_lock else load_pred_raws(args, cfg, lrows) # inline mode: predict after training (no lock wait to save; ~8 GB less RSS)
mem["rss_with_pred_packs_gb"] = round(ds.rss_gb(), 2)
(Path(args.out) / "memory.json").write_text(json.dumps(mem, indent=1))
log(f"prediction packs loaded: {sum(len(r['t']) for r in raws.values()):,} rows; RSS {mem['rss_with_pred_packs_gb']:.2f} GiB", args.out)
while True:
n_acq += 1
code = (train_then_predict(args, cfg, tr, va, st, bank, info, raws) if args.no_lock
else T.run_under_lock(args, train_then_predict, args, cfg, tr, va, st, bank, info, raws))
if code == 0:
break
if code != 3:
raise SystemExit(f"child exited with {code}")
time.sleep(5)
del tr, va, raws
import gc
gc.collect()
if not (Path(args.out) / "PRED_DONE").exists():
raws = load_pred_raws(args, cfg, lrows)
if raws:
log(f"prediction packs loaded: {sum(len(r['t']) for r in raws.values()):,} rows, RSS {ds.rss_gb():.2f} GiB", args.out)
n_acq += 1
code = predict_many(args, cfg, raws, st, bank, info) if args.no_lock else T.run_under_lock(args, predict_many, args, cfg, raws, st, bank, info)
if code != 0:
raise SystemExit(f"prediction child exited with {code}")
del raws
import gc
gc.collect()
code_files = sorted((CWM3 / "models").glob("*.py")) + [CWM3 / "train" / "train_cwm3_joint.py", CWM3 / "train" / "train_cwm3.py", CWM3 / "train" / "batch.py",
CWM3 / "eval" / "write_pred_layout_joint.py", CWM3 / "data" / "cwm3_dataset.py", EXPLORE / "s12_wm/eval_s13.py"]
run_cfg = {"stage": cfg["stage"], "variant": cfg["variant"], "cfg": cfg}
layouts = []
if not args.synthetic:
for regime in cfg["regimes"]:
for split in cfg["pred_splits"]:
if (Path(args.out) / f"pred_{regime}_{split}.npz").exists() and not (Path(args.out) / f"pred_layout_{regime}_{split}.npz").exists():
layouts.append(str(wplj.write_layout(args.out, regime, split, run_cfg, code_files, log=lambda m: log(m, args.out))))
hist = [json.loads(l) for l in (Path(args.out) / "history.jsonl").read_text().splitlines()]
td = json.loads((Path(args.out) / "TRAIN_DONE").read_text())
pj = Path(args.out) / "params.json"
info["params"] = json.loads(pj.read_text()) if pj.exists() else {}
if not td.get("params"):
td["params"] = info["params"]
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
n_rows = append_quick_rows(args, cfg, stamp, td)
qm = {f"{r}_{s}": json.loads((Path(args.out) / f"metrics_quick_{r}_{s}.json").read_text()) for r in cfg["regimes"] for s in cfg["pred_splits"]
if (Path(args.out) / f"metrics_quick_{r}_{s}.json").exists()}
inputs = [] if args.synthetic else sorted({p for d in base_dirs(cfg, args.seed).values() for p in [d / (d / "BEST").read_text().strip()]}
| set(T.stage_input_files({"regimes": cfg["regimes"], "branch": "V", "can_hist": "8s" if "X8" in need_streams(cfg) else "4s"}))
| set(T.stage_input_files({"regimes": cfg["regimes"], "branch": "U", "cabin": "none"}))
| set(T.stage_input_files({"regimes": cfg["regimes"], "branch": "E"}))
| {p for r in cfg["regimes"] for sp in ("train", "val", "test") for p in (PRESSURE / f"pressure_{r}_{sp}.npz", RESPONSE / f"response_{r}_{sp}.npz") if p.exists()})
mem_p = Path(args.out) / "memory.json"
write_run_json(args.out, f"train_{args.stage}_{args.variant}_s{args.seed}", code_files, inputs, seed=args.seed, t_start=t_start, extra={
"arm": wplj.ARM_OF.get(cfg["stage"], "cwm3_I"), "stage": args.stage, "variant": args.variant, "cfg": cfg, "synthetic": args.synthetic, "max_anchors": args.max_anchors,
"params": info["params"], "base_ckpts": info["base_ckpts"], "base_cfgs": info["base_cfgs"], "tok_source": cfg.get("tok_source"), "pack_version": args.pack_version,
"lock_acquisitions_this_invocation": n_acq,
"epochs_run": td["epochs_run"], "best_epoch": td["best_epoch"], "val_objective": td["best"], "history": hist, "quick_metrics": qm, "quick_rows_appended": n_rows,
"run_stamp": stamp, "memory": json.loads(mem_p.read_text()) if mem_p.exists() else {}, "roi": wplj.roi_of(ks), "roi_streams": ["Kh", "K", "S"],
"external_priors": wplj.external_priors(ks, kv), "dms_fields_used": [], "label_rows": {r: int(len(v)) for r, v in lrows.items()},
"kin_source": ks, "kin_version": kv, "kin_dir": (str(ds.kin_dir()) if ks != "cwm2" else ""), "stats_mode": cfg.get("stats_mode", "merged"),
"stats_version": cfg.get("stats_version"), "base_stats_versions": cfg.get("base_stats_versions", {}), "base_dirs": info.get("base_dirs", {}), "run_suffix": args.run_suffix,
"u_gaze_chain": bool(cfg.get("u_gaze_chain", False)), "u_face": bool(cfg.get("u_face", False)), # gaze U bases (2026-09-12 appendix variant)
"u_kpf": cfg.get("u_kpf", ""), # LANE F1 (2026-09-17): forecaster run dir of the kpf channel
"mask_V": bool(cfg.get("mask_V", False)), # variant `ue_noV` (2026-09-18): vehicle branch masked as missing
"p_head_source": info["params"].get("p_head_source", ""),
"latent_aux": (dict(cfg["latent_aux"]) if latent_enabled(cfg) else None), "latent_aux_lam": latent_lam(cfg), # C4 (2026-09-17)
"mtl": cfg.get("mtl", "none"), "mtl_probe": (json.loads((Path(args.out) / "mtl_probe.json").read_text()) if (Path(args.out) / "mtl_probe.json").exists() else {}),
"selection_objective": "validation sum of chain NLLs (V terms on V-valid rows)" + (" + 0.1 L_F" if cfg["recipe"].get("select_LF", True) else " (L_F excluded: select_LF false)") + "; MDN / Gaussian-K excluded",
"pred_layouts": layouts,
"note_kh_causality": (f"Kh / K / S from the CAUSAL route-level pose ({ds.kin_dir().name}); rows without a causal box are not scored (valid_U)" if ks == "causal_routes" else
"Kh / K / S come from the route-median crop pose cache (NON-causal ROI): driver-leg numbers are diagnostic (K4 pending)")})
(Path(args.out) / "PRED_DONE").write_text(json.dumps({"splits": cfg["pred_splits"], "regimes": cfg["regimes"], "label_rows": {r: int(len(v)) for r, v in lrows.items()}}))
log("run.json written; DONE", args.out)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
77.1 kB
·
Xet hash:
819ee36363e8695dcc9fd0b590450e0d2e4d72796dcb08581b6c9926cd2519d1

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