AsianPlayer/TriDrive / code /models /branches_alt.py
AsianPlayer's picture
download
raw
9.31 kB
"""cwm3/models/branches_alt.py — architecture-swap variants of the base trunks (GPU-baselines experiment B; results/tables/branch_arch_ablation.md).
Same input contract, same output contract, same flat head and mode embedding as models/branches.py; ONLY the sequence encoder of the trunk is
swapped, and its width is SOLVED so that the encoder's parameter count matches the reference trunk within ±10 % (U: GRU(18 -> 128) = 56,832;
V: TCN(6 -> 256) = 1,313,536). Selected through the stages.yaml variant key `arch` (train_cwm3.model_kwargs forwards it; branches.build_branch
dispatches here); every existing variant has no `arch` key and builds the reference modules bit for bit.
U arch tcn : causal TCN (branches.TCN form: k 5, dilations 1,2,4,8, width d*) -> Linear(d* -> 128) [d* solved]
U arch tfm : 2-layer pre-norm causal TransformerEncoder (d*, 4 heads, d_ff 2 d*) over the 40 history steps + learned step embedding, last state -> Linear(d* -> 128)
V arch gru : GRU(6 -> h*), last state -> Linear(h* -> 256) [h* solved]
V arch tfm : 2-layer pre-norm causal TransformerEncoder (256, 4 heads, d_ff*) + step embedding, last state (256) [d_ff* solved]
The alternative modules subclass the reference branch, so the input LayerNorm (u_in_ln), mode embedding, flat head, cabin / adversarial arms keep the
reference code path AND the reference's seeded initial values (they are constructed before the encoder is replaced). The joint model never uses
these variants (branch-level ablation only). No openpilot DMS field is read anywhere (inputs: CAN whitelist, YOLO-pose kinematics).
"""
from __future__ import annotations
import torch
import torch.nn as nn
from . import heads as hd
from .branches import TCN, DriverBranch, VehicleBranch
from .identity import grad_reverse
H = hd.H
TOL = 0.10
L_HIST = 40
def n_params(m):
return sum(p.numel() for p in m.parameters())
# ----------------------------------------------------------------------------- encoders
class CausalTransformerEncoder(nn.Module):
"""Linear(cin -> d) + learned step embedding -> n_layers pre-norm TransformerEncoder layers with a causal (upper-triangular) mask -> LN(last step)."""
def __init__(self, cin, d, n_layers=2, n_heads=4, d_ff=None, L=L_HIST, dropout=0.0, d_out=None):
super().__init__()
d_ff = d_ff or 2 * d
self.inp = nn.Linear(cin, d)
self.pos = nn.Parameter(torch.zeros(L, d))
nn.init.normal_(self.pos, std=0.02)
layer = nn.TransformerEncoderLayer(d, n_heads, d_ff, dropout, activation="gelu", batch_first=True, norm_first=True)
self.enc = nn.TransformerEncoder(layer, n_layers, enable_nested_tensor=False)
self.norm = nn.LayerNorm(d)
self.proj = nn.Linear(d, d_out) if (d_out is not None and d_out != d) else nn.Identity()
self.register_buffer("mask", torch.triu(torch.ones(L, L, dtype=torch.bool), 1), persistent=False)
def forward(self, x): # [B,L,C] -> [B,d_out]
L = x.shape[1]
h = self.inp(x) + self.pos[None, :L]
h = self.enc(h, mask=self.mask[:L, :L])
return self.proj(self.norm(h[:, -1]))
class GRUEncoder(nn.Module):
def __init__(self, cin, hid, d_out):
super().__init__()
self.gru = nn.GRU(cin, hid, batch_first=True)
self.proj = nn.Linear(hid, d_out) if hid != d_out else nn.Identity()
def forward(self, x):
_, h = self.gru(x)
return self.proj(h[-1])
class TCNEncoder(nn.Module):
def __init__(self, cin, d, d_out, dropout=0.0):
super().__init__()
self.tcn = TCN(cin, d, dropout)
self.proj = nn.Linear(d, d_out) if d != d_out else nn.Identity()
def forward(self, x):
return self.proj(self.tcn(x))
# ----------------------------------------------------------------------------- analytic parameter counts (for the width solver)
def p_gru(cin, h, d_out):
return 3 * (cin * h + h * h + 2 * h) + (h * d_out + d_out if h != d_out else 0)
def p_tcn(cin, d, d_out):
return (cin * d + d) + 4 * (d * d * 5 + d) + (d * d_out + d_out if d != d_out else 0)
def p_tfm(cin, d, n_layers, d_ff, L, d_out):
layer = (3 * d * d + 3 * d) + (d * d + d) + (d * d_ff + d_ff) + (d_ff * d + d) + 4 * d
return (cin * d + d) + L * d + n_layers * layer + 2 * d + (d * d_out + d_out if d != d_out else 0)
def solve(fn, target, lo, hi, step=1):
"""the width in [lo, hi] (multiples of step) whose analytic count is closest to target; asserts ±TOL."""
best, best_n = None, None
for w in range(lo, hi + 1, step):
n = fn(w)
if best is None or abs(n - target) < abs(best_n - target):
best, best_n = w, n
assert abs(best_n / target - 1) <= TOL, f"no width within ±{TOL:.0%} of {target:,} (closest {best}: {best_n:,})"
return best, best_n
# ----------------------------------------------------------------------------- branches
class DriverBranchAlt(DriverBranch):
"""DriverBranch with the GRU trunk replaced by a parameter-matched causal TCN (`tcn`) or causal Transformer (`tfm`)."""
def __init__(self, arch, cin=18, hid=128, d_mode=16, cabin="none", d_cab=64, hidden=128, dropout=0.0, in_ln=False, adv_devices=0):
super().__init__(cin, hid, d_mode, cabin, d_cab, hidden, dropout, in_ln, adv_devices)
target = n_params(self.gru)
del self.gru
if arch == "tcn":
w, n_an = solve(lambda d: p_tcn(cin, d, hid), target, 8, 256)
self.enc = TCNEncoder(cin, w, hid, 0.0)
elif arch == "tfm":
w, n_an = solve(lambda d: p_tfm(cin, d, 2, 2 * d, L_HIST, hid), target, 8, 256, step=4)
self.enc = CausalTransformerEncoder(cin, w, 2, 4, 2 * w, L_HIST, 0.0, d_out=hid)
else:
raise ValueError(f"U arch {arch!r}")
n_enc = n_params(self.enc)
assert abs(n_enc / target - 1) <= TOL, (arch, n_enc, target)
self.arch_info = {"branch": "U", "arch": arch, "width": int(w), "encoder_params": int(n_enc), "reference_encoder_params": int(target),
"ratio": round(n_enc / target, 4), "reference": "GRU(18 -> 128)"}
def forward(self, b):
kx = b["Kx"].float()
if self.in_ln is not None:
kx = torch.cat([self.in_ln(kx[..., :self.N_KIN_IN]), kx[..., self.N_KIN_IN:]], -1)
h = self.enc(kx)
parts = [h, self.mode(b["mode"].long())]
if self.cab is not None:
parts.append(self.cab(b["cab"].float()) * b["cab_ok"].float()[:, None])
out = {"U": self.head(torch.cat(parts, 1)).view(-1, H, hd.D_U)}
if self.adv is not None:
out["ADV"] = self.adv(grad_reverse(h))
return out
class VehicleBranchAlt(VehicleBranch):
"""VehicleBranch with the TCN trunk replaced by a parameter-matched GRU (`gru`) or causal Transformer (`tfm`)."""
def __init__(self, arch, d=256, d_mode=16, hidden=256, dropout=0.1, cin=6):
super().__init__(d, d_mode, hidden, dropout, cin)
target = n_params(self.tcn)
del self.tcn
if arch == "gru":
w, n_an = solve(lambda h: p_gru(cin, h, d), target, 16, 1024)
self.enc = GRUEncoder(cin, w, d)
elif arch == "tfm":
w, n_an = solve(lambda ff: p_tfm(cin, d, 2, ff, L_HIST, d), target, 16, 4096, step=16)
self.enc = CausalTransformerEncoder(cin, d, 2, 4, w, L_HIST, dropout, d_out=d)
else:
raise ValueError(f"V arch {arch!r}")
n_enc = n_params(self.enc)
assert abs(n_enc / target - 1) <= TOL, (arch, n_enc, target)
self.arch_info = {"branch": "V", "arch": arch, "width": int(w), "encoder_params": int(n_enc), "reference_encoder_params": int(target),
"ratio": round(n_enc / target, 4), "reference": "TCN(6 -> 256), k 5, dilations 1,2,4,8"}
def forward(self, b):
z = self.enc(b["X"].float())
z = torch.cat([z, self.mode(b["mode"].long())], 1)
return {"V": self.head(z).view(-1, H, hd.D_V)}
def build_alt(branch, arch, **kw):
"""called by branches.build_branch AFTER torch.manual_seed(seed) when the cfg carries `arch`."""
if branch == "U" and arch == "driverwm":
# 2026-09-17 (owner experiment C, pre-registration docs/decisions.md 2026-09-17T07:00:22Z): the Driver-WM temporal encoder at its published width
# (models/u_driverwm.py; NOT parameter-matched) as the U trunk; variant kin_can_noK_clip8_ln_udwm. Every other arch value is unchanged.
from .u_driverwm import DriverBranchDWM
return DriverBranchDWM(**kw)
if branch == "U":
return DriverBranchAlt(arch, **kw)
if branch == "V":
return VehicleBranchAlt(arch, **kw)
raise ValueError(f"no architecture-swap variants for branch {branch!r} (E depth is a plain `model: {{n_layers: ...}}` variant)")
if __name__ == "__main__":
for br, archs, kw in (("U", ("tcn", "tfm"), dict(hid=128, hidden=128, dropout=0.0, cabin="none", in_ln=True)), ("V", ("gru", "tfm"), dict(d=256, hidden=256, dropout=0.1))):
for ar in archs:
torch.manual_seed(0)
m = build_alt(br, ar, **kw)
print(br, ar, m.arch_info, "total", n_params(m))

Xet Storage Details

Size:
9.31 kB
·
Xet hash:
0b0df3cbca712cee61db5d68e1334f046d94bebbd430bb8abfd5b41e533d45bf

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