AsianPlayer's picture
download
raw
21.5 kB
"""cwm3/train/batch.py — compact raw arrays (data/cwm3_dataset.py) -> per-branch model inputs + targets, built ON THE GPU.
The child process (after the fork under the GPU lock) moves the stage's compact arrays to the device once (GpuStore) and every
batch is an index + standardisation on the device (no per-batch numpy work; the host raw dict is never standardised).
Standardisation statistics are TRAIN-only (both regimes pooled, one model): xmu/xsd (wm3 rule over all 40 steps of X — the same
statistics apply to the 8 s history, same signals), kmu/ksd (kin_predictability.feat_stats nan-aware rule over Kh),
ymu/ysd = s12 target_stats.npz (manual train; the s12 / cwm2 convention for the MDN channels).
Δt channels are real step offsets in seconds: X / Kh -> -(39-k)*0.1, X8 (8 s @5 Hz) -> -(39-k)*0.2, tokens -> t_bank - t_anchor.
Targets follow the cwm2 grid_batch conventions (chain z_prev = anchor state then z[:-1]; masks AND the s12 censoring M and the
validity of both z_prev and z). No openpilot DMS field is read.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import torch
CWM3 = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(CWM3))
sys.path.insert(0, str(CWM3 / "data"))
from models import heads as hd # noqa: E402
HIST, H, DK = 40, 25, 11
IDX_MIN_DIST, IDX_HEAD_CX = 10, 0
X_CLIP, K_CLIP = 8.0, 8.0 # K_CLIP 5 -> 8 (= X_CLIP) for NEW runs (coordinator 2026-09-11, K4 v2 stats arm: the ±5 clamp truncated 1.3 % of head_cx
K_CLIP_LEGACY = 5.0 # under the correct v2 SDs). The base trainer writes the value it used into cfg["kclip"] (run.json); prep_U falls back to the
# LEGACY 5 when a cfg has no `kclip` (every run before this date, joint / OOF / twin evaluation of those runs) -> old runs reproduce.
KEYS = {"V": ["X", "Xpre", "x8_ok", "Y", "M", "phase0", "phase", "Resp", "RespValid", "Resp0", "Resp0valid", "mode", "slice_tag"],
"U": ["X", "Kh", "K", "Kvalid", "S", "S0", "EM", "M", "mode", "slice_tag", "vc256", "vc256_ok", "crop256", "crop256_ok",
"Sg", "Sg0", "EMg", "Fh", "Fhvalid", "kpf"], # kpf: LANE F1 forecaster channel (2026-09-17; absent -> skipped); Sg* / EMg: gaze chain targets, Fh*: face-landmark inputs (opt-in U variants 2026-09-12; GpuStore skips absent keys)
"E": ["tok_idx", "t", "vf256", "vf256_ok", "D", "Dvalid", "D0", "D0valid", "M", "F", "Fok", "mode", "slice_tag",
"regionpca256", "regionpca256_ok", "P", "Pvalid", "P0", "P0valid",
"vfcan256", "vfcan256_ok", "can256", "can256_ok", "vfcan256s", "vfcan256s_ok"]} # regionpca256: side vector (pack v2); P*: road-pressure chains (E0 `*_P`); vfcan256 / can256: F5b CAN side streams (2026-09-16; GpuStore skips absent keys)
# ----------------------------------------------------------------------------- statistics (train only)
STATS_VERSION = 2 # 2 (2026-09-11): float64 accumulation. v1 accumulated 58 M float32 values with np.mean/np.std -> stored vEgo mean 11.90 vs 17.01
# true, kinematics SDs 2-3x off (docs/latent_quality.md §9). Models trained with v1 stats are internally consistent (same affine
# map on train / val / test) but their standardised MAEs are not in SD units; every table before this date is "stats v1".
def compute_stats_v1(tr, target_stats):
"""the PRE-fix rule verbatim (every run before 2026-09-11): float32 arrays reduced along axis 0 with np.mean / np.std (X) and the
kin_predictability nan-aware np.nanmean / np.nanstd (Kh) — sequential float32 accumulation over ~58 M values (vEgo mean 11.90 vs 17.01
true, kinematics SDs 2–3x off; results/tables/stats_fix.md). Kept ONLY for the stats-version disentangling arm (`--stats-version 1`,
coordinator 2026-09-11); never the default."""
st = {"ymu": np.asarray(target_stats["mu"], np.float32), "ysd": np.asarray(target_stats["sd"], np.float32)}
if "X" in tr:
X = tr["X"].reshape(-1, 5).astype(np.float32)
st["xmu"], st["xsd"] = X.mean(0).astype(np.float32), (X.std(0) + 1e-6).astype(np.float32)
if "Kh" in tr:
Kh = tr["Kh"].reshape(-1, tr["Kh"].shape[-1]).astype(np.float32) # width = the pack's Kh (11; 14 for kin v2p, 2026-09-18)
st["kmu"], st["ksd"] = np.nanmean(Kh, 0).astype(np.float32), (np.nanstd(Kh, 0) + 1e-6).astype(np.float32)
st["stats_version"] = np.int64(1)
if "Resp0" in tr:
v = tr["Resp0valid"]
st["resp0_base"] = np.float32(tr["Resp0"][v].mean() if v.any() else 0.3)
if "D0" in tr:
st["d0_base"] = np.array([tr["D0"][tr["D0valid"][:, j], j].mean() if tr["D0valid"][:, j].any() else 0.0 for j in range(6)], np.float32)
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)
return st
def compute_stats(tr, target_stats, version=STATS_VERSION):
if int(version) == 1:
return compute_stats_v1(tr, target_stats)
assert int(version) == 2, version
st = {"ymu": np.asarray(target_stats["mu"], np.float32), "ysd": np.asarray(target_stats["sd"], np.float32)}
if "X" in tr:
X = tr["X"].reshape(-1, 5)
mu = np.zeros(5, np.float64)
var = np.zeros(5, np.float64)
for i in range(0, len(X), 1_000_000): # chunked float64 accumulation (exact to ~1e-12 relative)
xb = X[i:i + 1_000_000].astype(np.float64)
mu += xb.sum(0)
var += (xb ** 2).sum(0)
mu /= len(X)
var = var / len(X) - mu ** 2
st["xmu"], st["xsd"] = mu.astype(np.float32), (np.sqrt(np.maximum(var, 0)) + 1e-6).astype(np.float32)
if "Kh" in tr:
DKh = tr["Kh"].shape[-1] # 11, or 14 for kin v2p (2026-09-18: 3 keypoint proxies appended; identical rule per column)
Kh = tr["Kh"].reshape(-1, DKh)
sm = np.zeros(DKh, np.float64)
sq = np.zeros(DKh, np.float64)
cnt = np.zeros(DKh, np.float64)
for i in range(0, len(Kh), 1_000_000):
kb = Kh[i:i + 1_000_000].astype(np.float64)
ok = np.isfinite(kb)
sm += np.where(ok, kb, 0.0).sum(0)
sq += np.where(ok, kb ** 2, 0.0).sum(0)
cnt += ok.sum(0)
mu = sm / np.maximum(cnt, 1)
var = sq / np.maximum(cnt, 1) - mu ** 2
st["kmu"], st["ksd"] = mu.astype(np.float32), (np.sqrt(np.maximum(var, 0)) + 1e-6).astype(np.float32)
if "Fh" in tr: # face-landmark inputs (u_face, 2026-09-12): the same nan-aware float64 rule as Kh
DF = tr["Fh"].shape[-1]
Fh = tr["Fh"].reshape(-1, DF)
sm = np.zeros(DF, np.float64)
sq = np.zeros(DF, np.float64)
cnt = np.zeros(DF, np.float64)
for i in range(0, len(Fh), 1_000_000):
fb = Fh[i:i + 1_000_000].astype(np.float64)
ok = np.isfinite(fb)
sm += np.where(ok, fb, 0.0).sum(0)
sq += np.where(ok, fb ** 2, 0.0).sum(0)
cnt += ok.sum(0)
mu = sm / np.maximum(cnt, 1)
var = sq / np.maximum(cnt, 1) - mu ** 2
st["fmu"], st["fsd"] = mu.astype(np.float32), (np.sqrt(np.maximum(var, 0)) + 1e-6).astype(np.float32)
st["stats_version"] = np.int64(STATS_VERSION)
if "Resp0" in tr:
v = tr["Resp0valid"]
st["resp0_base"] = np.float32(tr["Resp0"][v].mean() if v.any() else 0.3)
if "D0" in tr:
st["d0_base"] = np.array([tr["D0"][tr["D0valid"][:, j], j].mean() if tr["D0valid"][:, j].any() else 0.0 for j in range(6)], np.float32)
if "P0" in tr: # road-pressure anchor base rates (fill for invalid anchors), as d0_base
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)
return st
# ----------------------------------------------------------------------------- device store
class GpuStore:
"""Compact arrays of a raw dict (subset of keys) as tensors on `device`; the token bank lives here too (E)."""
def __init__(self, raw, keys, device, bank=None):
self.device = device
self.N = len(raw["t"])
self.t = {}
for k in keys:
if k in raw:
a = raw[k]
if a.dtype == np.float16:
self.t[k] = torch.from_numpy(a).to(device) # f16 stays f16 (cast per batch)
elif a.dtype == np.bool_:
self.t[k] = torch.from_numpy(a).to(device)
else:
self.t[k] = torch.from_numpy(np.ascontiguousarray(a)).to(device)
if bank is not None:
for k in ("tok", "pooled256"):
if k in bank:
self.t["bank_" + k] = torch.from_numpy(bank[k]).to(device)
self.t["bank_t"] = torch.from_numpy(bank["t"].astype(np.float32)).to(device)
def has(self, k):
return k in self.t
def bytes(self):
return int(sum(v.numel() * v.element_size() for v in self.t.values()))
def _prev_valid(v0, v):
"""[B] valid at anchor, [B,T] valid per step -> validity of z_prev per step [B,T]."""
return torch.cat([v0[:, None], v[:, :-1]], 1)
# ----------------------------------------------------------------------------- V
def prep_V(store, idx, st, cfg, need_targets=True):
d = store.device
X = store.t["X"][idx].float() # [B,40,5] raw
mode = store.t["mode"][idx].long()
xmu, xsd = torch.as_tensor(st["xmu"], device=d), torch.as_tensor(st["xsd"], device=d)
if cfg.get("can_hist", "4s") == "8s":
X8 = torch.cat([store.t["Xpre"][idx].float(), X], 1)[:, 1::2] # 80 @10 Hz -> 40 @5 Hz, ends at t
dt = -(39 - torch.arange(40, device=d).float()) * 0.2
Xin = torch.clamp((X8 - xmu) / xsd, -X_CLIP, X_CLIP)
else:
dt = -(39 - torch.arange(40, device=d).float()) * 0.1
Xin = torch.clamp((X - xmu) / xsd, -X_CLIP, X_CLIP)
b = {"X": torch.cat([Xin, dt[None, :, None].expand(len(idx), -1, -1)], -1), "mode": mode}
tg = None
if need_targets:
Y = store.t["Y"][idx].float()
M = store.t["M"][idx]
ymu, ysd = torch.as_tensor(st["ymu"], device=d), torch.as_tensor(st["ysd"], device=d)
Ystd = torch.nan_to_num((Y[..., [0, 3, 4]] - ymu) / ysd)
gas, brake = (Y[..., 1] > 0.5).float(), (Y[..., 2] > 0.5).float()
phase = store.t["phase"][idx].float()
resp = store.t["Resp"][idx].float()
gas0, brake0 = (X[:, -1, 1] > 0.5).float(), (X[:, -1, 2] > 0.5).float()
phase0 = store.t["phase0"][idx].float()
r0v = store.t["Resp0valid"][idx]
resp0 = torch.where(r0v, store.t["Resp0"][idx].float(), torch.full_like(gas0, float(st["resp0_base"])))
zV = torch.stack([gas, brake, phase, resp], -1)
z0 = torch.stack([gas0, brake0, phase0, resp0], -1)
zV_prev = torch.cat([z0[:, None], zV[:, :-1]], 1)
rv = store.t["RespValid"][idx]
respm = rv & M & _prev_valid(r0v, rv)
zVm = torch.stack([M, M, M, respm], -1)
tg = {"Ystd": Ystd, "M": M, "zV": zV, "zV_prev": zV_prev, "zVm": zVm, "z0": z0}
return b, tg
# ----------------------------------------------------------------------------- U
def std_kh(Kh, st, d, clip=K_CLIP_LEGACY):
kmu, ksd = torch.as_tensor(st["kmu"], device=d), torch.as_tensor(st["ksd"], device=d)
valid = torch.isfinite(Kh[..., IDX_MIN_DIST]) & torch.isfinite(Kh[..., IDX_HEAD_CX])
Z = torch.nan_to_num(torch.clamp((Kh - kmu) / ksd, -clip, clip))
return Z, valid
def k_clip(cfg):
"""the kinematics clamp of a run: cfg["kclip"] when recorded (new runs: K_CLIP = 8), else the legacy 5 of every older run."""
return float(cfg.get("kclip", K_CLIP_LEGACY))
def prep_U(store, idx, st, cfg, need_targets=True):
d = store.device
clip = k_clip(cfg)
Kh = store.t["Kh"][idx].float() # [B,40,11] NaN = invalid ([B,40,14] for kin v2p: + 3 keypoint proxies, 2026-09-18)
X = store.t["X"][idx].float()
xmu, xsd = torch.as_tensor(st["xmu"], device=d), torch.as_tensor(st["xsd"], device=d)
Khs, kvalid = std_kh(Kh, st, d, clip)
Xs = torch.clamp((X - xmu) / xsd, -X_CLIP, X_CLIP)
if cfg.get("recipe", {}).get("u_nocan"):
# CAN ablation (2026-09-14, recipe `u_nocan`, plan Part A §A.5 arm (a); decisions.md 2026-09-15T01:48:39Z): the 5 standardised CAN columns are EXACTLY zero on
# every history step; the valid bit and Δt are kept and Kx stays [B,40,18], so the GRU (and its paired seed init) is the parent's. Default off -> the old path bit-for-bit.
Xs = torch.zeros_like(Xs)
dt = (-(39 - torch.arange(40, device=d).float()) * 0.1)[None, :, None].expand(len(idx), -1, -1)
Kx = torch.cat([Khs, kvalid[..., None].float(), Xs, dt], -1) # [B,40,18] ([B,40,21] for kin v2p: n_kin 14 -> DriverBranch(n_kin=14))
if cfg.get("recipe", {}).get("u_face") and store.has("Fh"):
# face-landmark inputs (2026-09-12, recipe `u_face`): F standardised channels (train fmu / fsd, same clamp, NaN -> 0 = mean) + the per-step valid bit,
# appended AFTER the 18 baseline channels (the input LN keeps covering the first 12 kinematics channels only) -> [B,40,18+F+1]
fmu, fsd = torch.as_tensor(st["fmu"], device=d), torch.as_tensor(st["fsd"], device=d)
Fh = store.t["Fh"][idx].float()
fvalid = store.t["Fhvalid"][idx] & torch.isfinite(Fh[..., 1]) # lm_yaw_dev finite = the landmark geometry of the step is defined
Fs = torch.nan_to_num(torch.clamp((Fh - fmu) / fsd, -clip, clip)) * fvalid[..., None].float()
Kx = torch.cat([Kx, Fs, fvalid[..., None].float()], -1)
b = {"Kx": Kx, "mode": store.t["mode"][idx].long()}
if store.has("kpf"):
# LANE F1 (2026-09-17): the detached keypoint-forecaster channel [B,10] (results/runs/KPF_*/kpf_{regime}_{split}.npz feat) for models/kpf_inject.KpfInject
b["kpf"] = store.t["kpf"][idx].float()
cab = cfg.get("cabin", "none")
if cab != "none":
b["cab"] = store.t[cab][idx].float()
b["cab_ok"] = store.t[cab + "_ok"][idx]
tg = None
if need_targets:
M = store.t["M"][idx]
kmu, ksd = torch.as_tensor(st["kmu"], device=d)[:DK], torch.as_tensor(st["ksd"], device=d)[:DK] # the 11 K TARGET columns (kin v2p stats carry 14 entries: 3 input-only proxies)
K = store.t["K"][idx].float()
Km = store.t["Kvalid"][idx][:, :, None] & torch.isfinite(K) & M[:, :, None]
Kstd = torch.nan_to_num(torch.clamp((K - kmu) / ksd, -clip, clip)) * Km
K0std = Khs[:, -1, :DK] * kvalid[:, -1:].float() # K_0 = last history step (0 = mean when invalid); the 11 K TARGET columns only (v2p carries 3 extra input columns)
S = store.t["S"][idx].float() # [B,25,2] NaN censored
S0 = store.t["S0"][idx].float()
zU = torch.nan_to_num(S)
zU_prev = torch.cat([torch.nan_to_num(S0)[:, None], zU[:, :-1]], 1)
Sprev = torch.cat([S0[:, None], S[:, :-1]], 1)
EM = store.t["EM"][idx]
zUm = torch.stack([EM[:, :, 0], EM[:, :, 2]], -1) & M[:, :, None] & torch.isfinite(S) & torch.isfinite(Sprev)
tg = {"Kstd": Kstd, "Km": Km, "K0std": K0std, "zU": zU, "zU_prev": zU_prev, "zUm": zUm, "z0": torch.nan_to_num(S0),
"z0valid": torch.isfinite(S0)}
if cfg.get("recipe", {}).get("u_gaze_chain") and store.has("Sg"):
# gaze_off chain targets (2026-09-12): the kin convention for a third driver state — z_prev = anchor state then z[:-1]; mask = onset mask (both
# states finite) AND the s12 M AND finite z / z_prev (train/batch conventions of zUm)
Sg = store.t["Sg"][idx].float() # [B,25] NaN censored
Sg0 = store.t["Sg0"][idx].float()
zG = torch.nan_to_num(Sg)
zG_prev = torch.cat([torch.nan_to_num(Sg0)[:, None], zG[:, :-1]], 1)
Sgprev = torch.cat([Sg0[:, None], Sg[:, :-1]], 1)
EMg = store.t["EMg"][idx]
tg.update(zG=zG, zG_prev=zG_prev, zGm=EMg[:, :, 0] & M & torch.isfinite(Sg) & torch.isfinite(Sgprev), zG0=torch.nan_to_num(Sg0), zG0valid=torch.isfinite(Sg0))
return b, tg
# ----------------------------------------------------------------------------- E
def prep_E(store, idx, st, cfg, need_targets=True):
d = store.device
n_slots = int(cfg.get("n_slots", 16))
ti = store.t["tok_idx"][idx][:, -n_slots:].long() # [B,L]
ok = ti >= 0
safe = torch.where(ok, ti, torch.zeros_like(ti))
tokens = cfg.get("tokens", "region")
if tokens == "region":
tok = store.t["bank_tok"][safe].float() # [B,L,4,64]
elif tokens == "spat256":
tok = store.t["bank_tok"][safe].float().reshape(len(idx), n_slots, 1, 256) # [B,L,1,256]: the 4 region PCA-64 tokens FLATTENED (spatial order kept)
elif tokens == "none":
# 2026-09-16 (WP4 F5b sensor-only control, E0 `pooled_P_canonly` = `tokens: none` + `side: can256`): NO vision tokens — every slot is marked missing
# (tok_ok False -> the model sees only its learned `missing` embedding + slot / dt embeddings; the bank is never touched), so the E trunk output is
# input-independent and ALL road information enters through the side vector. Reached only with `tokens: none`; every other value follows the old path bit-for-bit.
tok = torch.zeros(len(idx), n_slots, 1, 256, device=d)
ok = torch.zeros_like(ok)
else:
tok = store.t["bank_pooled256"][safe].float()[:, :, None, :] # [B,L,1,256]
tok = tok * ok[:, :, None, None].float()
t_anchor = store.t["t"][idx].float()
nominal = -(n_slots - 1 - torch.arange(n_slots, device=d).float()) * 0.5
dt = torch.where(ok, store.t["bank_t"][safe] - t_anchor[:, None], nominal[None, :].expand(len(idx), -1))
side = cfg.get("side", "vf256") # per-anchor side vector: vf256 [256] (default) | regionpca256 [512]
b = {"tok": tok, "tok_ok": ok, "tok_dt": dt, "vf": store.t[side][idx].float(), "vf_ok": store.t[side + "_ok"][idx],
"mode": store.t["mode"][idx].long()}
tg = None
if need_targets:
M = store.t["M"][idx]
D, Dv = store.t["D"][idx].float(), store.t["Dvalid"][idx]
D0, D0v = store.t["D0"][idx].float(), store.t["D0valid"][idx]
d0b = torch.as_tensor(st["d0_base"], device=d)
z0 = torch.where(D0v, D0, d0b[None, :].expand_as(D0))
zD_prev = torch.cat([z0[:, None], D[:, :-1]], 1)
zDm = Dv & M[:, :, None] & torch.cat([D0v[:, None], Dv[:, :-1]], 1)
tg = {"zD": D, "zD_prev": zD_prev, "zDm": zDm, "F": store.t["F"][idx].float(), "Fok": store.t["Fok"][idx], "z0": z0, "z0valid": D0v}
if store.has("P") and "p0_base" in st: # road-pressure chains (E0 `*_P`): the joint trainer's prep_aux convention
Pz, Pv = store.t["P"][idx].float(), store.t["Pvalid"][idx]
P0v = store.t["P0valid"][idx]
p0b = torch.as_tensor(st["p0_base"], device=d)
zP0 = torch.where(P0v, store.t["P0"][idx].float(), p0b[None, :].expand_as(Pv[:, 0]))
tg.update(zP=Pz, zP_prev=torch.cat([zP0[:, None], Pz[:, :-1]], 1), zPm=Pv & M[:, :, None] & torch.cat([P0v[:, None], Pv[:, :-1]], 1),
zP0=zP0, zP0valid=P0v)
return b, tg
def loss_U(out, tg, lam):
"""heads.driver_loss (+ lambda G x the gaze_off chain NLL when the branch emits "G" and the batch carries the gaze targets — opt-in `u_gaze_chain`,
2026-09-12; its part chain_gaze_off enters the selection objective like the hands_on / head_turned chains). Old recipes: exactly driver_loss.
"HG" (recipe `u_can_gate`, 2026-09-14, plan Part A §A.5 arm (b)): heads.driver_loss_gated — the same chain BCE on the gated hazards λ' = clamp(λ·2g)."""
if "HG" in out:
# CAN release gate: identical arithmetic to driver_loss and bitwise the same loss while a = b = 0 (2g == 1); the K term is unchanged
total, parts = hd.driver_loss_gated(out["U"], tg, out["HG"], lam_K=lam.get("K", 0.5))
else:
total, parts = hd.driver_loss(out["U"], tg, lam_K=lam.get("K", 0.5))
if "G" in out and tg is not None and "zG" in tg:
lg, pg = hd.gaze_loss(out["G"], tg)
parts.update(pg)
total = total + float(lam.get("G", 1.0)) * lg
return total, parts
PREP = {"V": prep_V, "U": prep_U, "E": prep_E}
LOSS = {"V": lambda out, tg, lam: hd.vehicle_loss(out["V"], tg, lam_hz=lam.get("hz", 0.5)),
"U": loss_U,
"E": lambda out, tg, lam: hd.env_loss(out["E"], out["F"], tg, lam_F=lam.get("F", 0.1), P=out.get("P"), lam_P=lam.get("P", 1.0))}
def train_rows(raw, branch, cfg):
"""rows eligible for training / val objective: V can8s needs a full 8 s history (x8_ok)."""
N = len(raw["t"])
if branch == "V" and cfg.get("can_hist", "4s") == "8s":
return np.flatnonzero(raw["x8_ok"])
return np.arange(N)

Xet Storage Details

Size:
21.5 kB
·
Xet hash:
c0513a2c41584ec9e8a2dd24001f2ef417884315dea247329ca9233b34aa107f

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