changh95's picture
tt-model push meteor-p150 (container)
51defdc verified
Raw History Blame Contribute Delete
33.7 kB
# SPDX-License-Identifier: Apache-2.0
"""Pure-PyTorch fp32 re-implementation of ``meteor_v157c3Z.onnx`` with per-module taps.
A node-by-node transliteration of the deployed graph (2,021 ONNX nodes; ``research/meteor/SPEC.md`` sections 4.2-4.4,
cited ``S:<line>``), written from the graph itself (no upstream code is imported or executed): every convolution runs
with the weights and the attributes of its ONNX node (``reference.weights``), every non-conv constant (attention
pools, LayerNorm, gates, pooling matrices, lift ground points) is read from its consuming node, and the arithmetic
keeps the graph's operation order where it decides a rounding or a discrete choice (lift projection, validity and
depth-bin selection, risk sampling, decoupled decoder, depth_mean). Shape glue (Shape / Gather / Concat / Reshape of
shapes, ~470 nodes) disappears. ``MeteorNet.forward`` returns the 19 graph outputs with the ONNX names, dtypes and
shapes; ``taps=`` records the intermediates below (names shared with ``reference.ort.ORT_TAPS``, the device gates
and the goldens).
Module boundaries (what a TT stage computes) and taps:
========================== =========================== ====================================================
tap shape graph tensor / meaning
========================== =========================== ====================================================
``img.input`` [8, 3, 432, 768] uint8 / 255 (+ ImageNet mean/std with input_norm=imagenet)
``img.stem`` [8, 64, 108, 192] 7x7 s2 + ReLU + maxpool 3x3 s2
``img.layer1..4`` [8, 64..512, ...] ResNet-34 stages (BasicBlocks, BN folded)
``img.fpn`` [8, 160, 108, 192] lat1..4 + bilinear (x2, x4, 14x24 -> 108x192) + fuse 3x3 ReLU
``seg2d.logits`` [8, 21, 108, 192] SegHeadED raw logits (the PointPainting input, unclamped)
``seg2d.clamped`` [8, 21, 108, 192] nan_to_num + clamp +-30 (the seg2d argmax input only)
``depth.logits`` / ``.prob`` [8, 64, 108, 192] depth head 1x1 / softmax over the 64 linear bins
``ctx.painted`` [8, 96, 108, 192] ctx 1x1 + paint_proj(softmax(seg)[2,3,4,5,6,7,8,13])
``lift.grid`` [8, 100000, 1, 2] normalised (u, v) of the 400x250 ground points per camera
``lift.valid`` [8, 100000] bool z > 0.5, inside the image, range < 90 m
``lift.weight`` [8, 1, 100000] (interpolated bin probability + 0.05) * valid
``lift.bev`` [1, 96, 400, 250] sum_n ctx_s w / max(sum_n w, 1e-4)
``bev.raw`` [1, 96, 800, 500] bilinear x2 of lift.bev
``bev.gate`` [1, 1, 800, 500] 4 softmax(tgate(raw))[0]
``bev.fused`` [1, 96, 800, 500] raw + tfuse(raw * gate)
``lane.pre`` [1, 9, 800, 500] lane decoder + lane_branch on channels 4..6
``det.feat`` [1, 128, 400, 250] det stem (ED) output
``det.hm_pre/.reg_pre`` [1, 2 | 6, 400, 250] heads before the box refiner
``occ.feat`` [1, 192, 200, 200] occ stem on raw rows 200:600, cols 50:450
``traj.stem / .feat`` [1, 128 | 256, 400, 250] traj stem; cat(stem, det.feat) + agent_delta
``traj.agent_delta`` [1, 256, 1, 1] agent attention pool -> 1x1 conv
``stat.feat`` [1, 48, 400, 250] delta_stat convs on avgpool2(|raw|)
``risk.feat`` [1, 64, 400, 250] risk ConvBlocks on fused rows 200:600, cols 125:375
``ego.*`` [1, 42] (and others) planner chain: stem, mlp, attn, plus_attn, sem_in, sem,
plus_sem, kin, plus_kin, risk_sample, after_risk,
dec_head, dec_wp, after_dec
``bev.fused_mean`` [1, 96] global mean of the fused BEV (computed once; S:368-369)
``refiner.seg_res`` [1, 9, 800, 500] seg refiner residual (before 8 tanh(x / 8))
``refiner.box_res`` [1, 8, 400, 250] box refiner residual (before 6 tanh(x / 6))
``refiner.e2e_ln / _res`` [1, 139] / [1, 36] e2e LayerNorm output / 3 tanh(mlp)
========================== =========================== ====================================================
The 19 outputs are tapped under their ONNX names (``lane``, ``depth``, ``seg2d``, ``hm`` ... ``depth_mean``).
"""
from __future__ import annotations
from typing import Any, Dict, Optional
import numpy as np
from ..ttaw.golden import NULL_TAPS, TapRegistry
from . import config as C
from .config import MeteorConfig, default_config
from .weights import MeteorWeights
__all__ = ["MeteorNet", "OUTPUT_NAMES", "child"]
OUTPUT_NAMES = ("lane", "depth", "seg2d", "hm", "reg", "hm2d_s0", "hm2d_s1", "hm2d_s2", "reg2d_s0", "reg2d_s1",
"reg2d_s2", "ego", "occ", "traj", "stationary", "tl", "risk", "lane_logit", "depth_mean")
def _torch():
import torch
return torch
def child(module: str, index: int) -> str:
"""ONNX scope of child ``index`` of a Sequential: ``child("seg_head/d1/d1.3", 0)`` = ``seg_head/d1/d1.3/d1.3.0``."""
return f"{module}/{module.rsplit('/', 1)[-1]}.{index}"
class MeteorNet:
"""The network as fp32 torch functions over the ONNX weights (no autograd, no nn.Module state)."""
def __init__(self, weights: MeteorWeights, cfg: Optional[MeteorConfig] = None, *, check_graph: bool = True):
torch = _torch()
self.cfg = (cfg or default_config()).check()
self.weights = weights
if check_graph:
weights.check_graph()
f32 = lambda a: torch.from_numpy(np.array(a, dtype=np.float32)) # own writable copy
self._conv: Dict[str, Any] = {}
for module, p in weights.iter_convs():
pads = tuple(int(v) for v in p.pads)
if len(pads) != 4 or pads[:2] != pads[2:] or p.group != 1 or tuple(p.dilations) != (1, 1):
raise ValueError(f"{module}: unsupported conv attributes {p}")
self._conv[module] = (f32(p.weight), None if p.bias is None else f32(p.bias),
tuple(int(s) for s in p.strides), pads[:2])
self._gemm: Dict[str, Any] = {}
for module in weights.gemm_modules():
g = weights.gemm(module)
if g.trans_a or not g.trans_b or g.alpha != 1.0 or g.beta != 1.0:
raise ValueError(f"{module}: unsupported Gemm attributes")
self._gemm[module] = (f32(g.weight), f32(g.bias))
self._pool = {name: f32(weights.pool_matrix(name)) for name in
("ego_stem_w", "ego_stem_h", "tl_w", "tl_h", "fused_pool_w", "fused_pool_h", "sem_w", "sem_h",
"det_pool_w", "det_pool_h", "fused_mean_w", "fused_mean_h", "e2e_mean_w", "e2e_mean_h")}
self._mha = {name: weights.mha(name) for name in ("ego_attn", "agent_attn")}
self._mha_t = {name: (f32(m.in_weight), f32(m.in_bias), f32(m.out_weight), f32(m.out_bias))
for name, m in self._mha.items()}
self._queries = {"ego": f32(weights.queries("ego")), "agent": f32(weights.queries("agent"))}
g, b, eps = weights.layer_norm()
self._ln = (f32(g), f32(b), float(eps))
self._risk_gate = f32(np.asarray(weights.risk_gate(), np.float32).reshape(1))
self._dec_gate = f32(np.asarray(weights.dec_gate(), np.float32).reshape(1))
self._ground = f32(weights.lift_ground_points()) # [1, 4, 100000]
zc = weights.zero_constants()
self._hist = f32(zc["hist"]) # [1, 96, 800, 500] zeros (baked history)
self._mask = f32(zc["mask"]) # [1, 1, 800, 500] zeros
self._paint = torch.tensor(weights.paint_classes(), dtype=torch.long)
self._depth_centres = f32(C.depth_bin_centres(self.cfg.depth_mean_bins)).reshape(1, 1, C.DEPTH_BINS, 1, 1)
# ---- primitives ------------------------------------------------------------------------------------------------
def conv(self, x, module: str, relu: bool = False):
F = _torch().nn.functional
w, b, stride, pad = self._conv[module]
y = F.conv2d(x, w, b, stride=stride, padding=pad)
return F.relu(y) if relu else y
def block(self, x, module: str):
"""ConvBlock = (3x3 conv, BN (folded), ReLU) x 2: children ``.0`` and ``.3`` (S:318)."""
return self.conv(self.conv(x, child(module, 0), True), child(module, 3), True)
def down(self, x, module: str):
"""stride-2 3x3 conv + ReLU (child ``.0``) then a ConvBlock (child ``.3``): the encoder steps of the EDs."""
return self.block(self.conv(x, child(module, 0), True), child(module, 3))
def gemm(self, x, module: str):
w, b = self._gemm[module]
return _torch().addmm(b, x, w.t())
@staticmethod
def resize(x, hw):
"""ONNX Resize linear / half_pixel to a target size = F.interpolate(bilinear, align_corners=False)."""
F = _torch().nn.functional
return F.interpolate(x, size=tuple(int(v) for v in hw), mode="bilinear", align_corners=False)
def pool2(self, x, w_name: str, h_name: str):
"""The exported adaptive pool / mean: (H_mat @ (x @ W_mat)) on the last two dims (S:356-366)."""
torch = _torch()
return torch.matmul(self._pool[h_name], torch.matmul(x, self._pool[w_name]))
@staticmethod
def nan_to_num(x):
"""The graph's IsNaN / IsInf / Where chain (ONNX nodes 190-213 and 233-255): NaN -> 0, +inf -> 30,
-inf -> -30; the identity for finite values."""
torch = _torch()
x = torch.where(torch.isnan(x), torch.zeros_like(x), x)
x = torch.where(torch.isinf(x) & (x > 0), torch.full_like(x, 30.0), x)
return torch.where(torch.isinf(x) & (x < 0), torch.full_like(x, -30.0), x)
def mha_pool(self, name: str, keys, queries):
"""``nn.MultiheadAttention(q=learned queries, k=v=keys)`` as exported (torch 2.1, need_weights=False):
q and k|v projections, q / sqrt(head_dim), softmax over the 400 keys, out-projection.
``keys`` [L, 1, E] (sequence first), ``queries`` [1, Q, E] -> [1, Q, E]."""
torch = _torch()
m = self._mha[name]
w_in, b_in, w_out, b_out = self._mha_t[name]
e, h = m.embed_dim, m.num_heads
hd = e // h
q_in = queries.transpose(0, 1) # [Q, 1, E]
q = b_in[:e] + torch.matmul(q_in, w_in[:e].t()) # /Add_4 = bias + MatMul
kv = b_in[e:] + torch.matmul(keys, w_in[e:].t()) # [L, 1, 2E]
k, v = kv[..., :e], kv[..., e:]
nq, nk = q.shape[0], k.shape[0]
qh = q.reshape(nq, h, hd).transpose(0, 1) # [H, Q, hd]
kh = k.reshape(nk, h, hd).transpose(0, 1) # [H, L, hd]
vh = v.reshape(nk, h, hd).transpose(0, 1)
att = torch.softmax(torch.matmul(qh / m.scale_divisor, kh.transpose(1, 2)), dim=-1)
o = torch.matmul(att, vh).transpose(0, 1).reshape(nq, e) # [Q, E]
o = torch.addmm(b_out, o, w_out.t()) # out-projection (Gemm)
return o.reshape(nq, 1, e).transpose(0, 1) # [1, Q, E]
# ---- stages ----------------------------------------------------------------------------------------------------
def normalize(self, imgs_u8, present=None):
"""uint8 [1, 8, 3, 432, 768] -> [8, 3, 432, 768] float32: /255 (``onnx``) or (x/255 - mean)/std with absent
cameras zeroed AFTER normalisation (``imagenet``, SPEC section 2 imgs row)."""
torch = _torch()
x = imgs_u8.to(torch.float32) / 255.0
if self.cfg.input_norm == "imagenet":
mean = torch.tensor(C.IMAGENET_MEAN, dtype=torch.float32).reshape(1, 1, 3, 1, 1)
std = torch.tensor(C.IMAGENET_STD, dtype=torch.float32).reshape(1, 1, 3, 1, 1)
x = (x - mean) / std
if present is not None:
keep = torch.as_tensor(np.asarray(present, np.float32)).reshape(1, -1, 1, 1, 1)
x = x * keep
return x.reshape(C.N_CAMS, 3, C.IMG_H, C.IMG_W)
def image_encoder(self, x, taps: TapRegistry = NULL_TAPS):
"""ResNet-34 (torchvision layout, BN folded) + FPN -> f [8, 160, 108, 192] (S:301-302)."""
F = _torch().nn.functional
y = F.max_pool2d(self.conv(x, "stem/stem.0", True), kernel_size=3, stride=2, padding=1)
taps.tap("img.stem", y)
feats = []
for li, nb in enumerate((3, 4, 6, 3), start=1):
for bi in range(nb):
p = f"layer{li}/layer{li}.{bi}"
ds = f"{p}/downsample/downsample.0"
idt = self.conv(y, ds) if ds in self._conv else y
y = F.relu(self.conv(self.conv(y, f"{p}/conv1", True), f"{p}/conv2") + idt)
feats.append(taps.tap(f"img.layer{li}", y))
c1, c2, c3, c4 = feats
hw = (C.FEAT_H, C.FEAT_W)
s = self.conv(c1, "lat1") + self.resize(self.conv(c2, "lat2"), hw)
s = s + self.resize(self.conv(c3, "lat3"), hw)
s = s + self.resize(self.conv(c4, "lat4"), hw) # 14x24 -> 108x192: non-integer (S:302)
return taps.tap("img.fpn", self.conv(s, "fuse/fuse.0", True))
def seg2d_head(self, f, taps: TapRegistry = NULL_TAPS):
"""SegHeadED -> raw logits [8, 21, 108, 192] (S:303)."""
d1 = self.down(f, "seg_head/d1")
d2 = self.down(d1, "seg_head/d2")
m1 = self.block(d1 + self.resize(self.conv(d2, "seg_head/u1"), d1.shape[-2:]), "seg_head/m1")
u2 = self.resize(self.conv(m1, "seg_head/u2"), f.shape[-2:])
y = self.conv(self.conv(self.conv(f, "seg_head/skip") + u2, "seg_head/out/out.0", True), "seg_head/out/out.3")
return taps.tap("seg2d.logits", y)
def depth_head(self, f, taps: TapRegistry = NULL_TAPS):
"""4 ConvBlocks 160->128->128->96->64, 1x1 -> 64 bins, softmax over the bins (S:304)."""
torch = _torch()
y = f
for i in range(4):
y = self.block(y, f"depth_head/depth_head.{i}")
logits = taps.tap("depth.logits", self.conv(y, "depth_head/depth_head.4"))
return logits, taps.tap("depth.prob", torch.softmax(logits, dim=1))
def det2d_heads(self, f):
"""det2d stem + d8 / d16 pyramid, 1x1 heads at strides 4 / 8 / 16 (S:305) -> the six [1, 8, k, h, w] outputs."""
s = self.block(f, "det2d_stem")
d8 = self.down(s, "det2d_d8")
d16 = self.down(d8, "det2d_d16")
out = {}
for i, (x, sfx) in enumerate(((s, ""), (d8, "8"), (d16, "16"))):
for kind, ch in (("hm2d", 10), ("reg2d", 4)):
y = self.conv(x, f"{kind}_head{sfx}")
out[f"{kind}_s{i}"] = y.reshape(1, C.N_CAMS, ch, y.shape[-2], y.shape[-1])
return out
def tl_head(self, f):
"""cat(f[cam 0], f[cam 6]) -> s2 conv + CB -> s2 conv + CB -> global mean (MatMul consts) -> Linear (S:306)."""
torch = _torch()
x = torch.cat([f[0:1], f[6:7]], dim=1)
x = self.block(self.conv(x, "tl_head/tl_head.0", True), "tl_head/tl_head.3")
x = self.block(self.conv(x, "tl_head/tl_head.4", True), "tl_head/tl_head.7")
x = self.pool2(x, "tl_w", "tl_h").flatten(1)
return self.gemm(x, "tl_fc")
def context(self, f, seg_logits, taps: TapRegistry = NULL_TAPS):
"""ctx 1x1 + paint_proj(softmax(nan_to_num(raw seg logits))[paint classes]) (S:307; UNCLAMPED logits)."""
torch = _torch()
p = torch.softmax(self.nan_to_num(seg_logits), dim=1).index_select(1, self._paint)
return taps.tap("ctx.painted", self.conv(f, "ctx") + self.conv(p, "paint_proj"))
def lift(self, ctx, prob, K, T_cam_ego, taps: TapRegistry = NULL_TAPS):
"""Depth-gated IPM lift to the 0.4 m grid, then bilinear x2 to the raw BEV (S:333-351, ONNX nodes 261-421)."""
torch = _torch()
F = torch.nn.functional
n = C.N_CAMS
T = T_cam_ego.reshape(n, 4, 4).to(torch.float32)
Km = K.reshape(n, 3, 3).to(torch.float32)
pc = torch.matmul(T, self._ground) # [8, 4, 100000]
x, y, z = pc[:, 0], pc[:, 1], pc[:, 2]
zc = torch.clamp(z, min=C.LIFT_MIN_Z)
u = Km[:, 0, 0].unsqueeze(-1) * x / zc + Km[:, 0, 2].unsqueeze(-1)
v = Km[:, 1, 1].unsqueeze(-1) * y / zc + Km[:, 1, 2].unsqueeze(-1)
dist = torch.sqrt(x * x + y * y + z * z)
valid = ((z > C.LIFT_MIN_Z) & (u >= 0.0) & (u < float(C.IMG_W)) & (v >= 0.0) & (v < float(C.IMG_H))
& (dist < C.LIFT_MAX_DIST))
taps.tap("lift.valid", valid)
gu = torch.clamp(u / float(C.IMG_W - 1) * 2.0 - 1.0, -2.0, 2.0)
gv = torch.clamp(v / float(C.IMG_H - 1) * 2.0 - 1.0, -2.0, 2.0)
grid = taps.tap("lift.grid", torch.stack([gu, gv], dim=-1).unsqueeze(2)) # [8, 100000, 1, 2]
ctx_s = F.grid_sample(ctx, grid, mode="bilinear", padding_mode="zeros", align_corners=False).squeeze(3)
prob_s = F.grid_sample(prob, grid, mode="bilinear", padding_mode="zeros", align_corners=False).squeeze(3)
b = torch.clamp((dist - C.DEPTH_MIN) / C.DEPTH_STEP, 0.0, float(np.float32(C.LIFT_BIN_CLIP)))
b0 = torch.floor(b).to(torch.int64)
fr = (b - b0.to(torch.float32)).unsqueeze(1) # [8, 1, 100000]
g0 = torch.gather(prob_s, 1, b0.unsqueeze(1))
g1 = torch.gather(prob_s, 1, torch.clamp(b0 + 1, max=C.DEPTH_BINS - 1).unsqueeze(1))
w = (g0 * (1.0 - fr) + g1 * fr + C.LIFT_W_EPS) * valid.unsqueeze(1).to(torch.float32)
taps.tap("lift.weight", w)
wn = w.reshape(1, n, 1, -1)
num = (ctx_s.reshape(1, n, C.CTX_C, -1) * wn).sum(dim=1) # [1, 96, 100000]
den = torch.clamp(wn.sum(dim=1), min=C.LIFT_DEN_EPS) # [1, 1, 100000]
bev04 = taps.tap("lift.bev", (num / den).reshape(1, C.CTX_C, C.LIFT_H, C.LIFT_W))
return taps.tap("bev.raw", self.resize(bev04, (C.BEV_H, C.BEV_W)))
def temporal_fuse(self, raw, taps: TapRegistry = NULL_TAPS):
"""No-history temporal fuse: fused = raw + tfuse(raw * 4 softmax(tgate(raw))[0]) (S:308)."""
torch = _torch()
gate = taps.tap("bev.gate", torch.softmax(self.conv(raw, "tgate_slim"), dim=1)[:, 0:1] * C.TGATE_SCALE)
t = self.block(self.conv(raw * gate, "tfuse3_slim/tfuse3_slim.0", True), "tfuse3_slim/tfuse3_slim.3")
return taps.tap("bev.fused", raw + t)
def lane_decoder(self, raw, taps: TapRegistry = NULL_TAPS):
"""LaneDecED + lane_branch added to channels 4..6 -> lane_pre [1, 9, 800, 500] (S:309)."""
torch = _torch()
skip = self.block(raw, "dec/skip")
d1 = self.down(raw, "dec/d1")
d2 = self.down(d1, "dec/d2")
m1 = self.block(d1 + self.resize(self.conv(d2, "dec/u1"), d1.shape[-2:]), "dec/m1")
u2 = self.resize(self.conv(m1, "dec/u2"), skip.shape[-2:])
out = self.conv(self.conv(skip + u2, "dec/out/out.0", True), "dec/out/out.3")
lb = self.conv(self.conv(self.conv(raw, "lane_branch/lane_branch.0", True), "lane_branch/lane_branch.3", True),
"lane_branch/lane_branch.6")
return taps.tap("lane.pre", torch.cat([out[:, :4], out[:, 4:7] + lb, out[:, 7:]], dim=1))
def det_stem(self, raw, taps: TapRegistry = NULL_TAPS):
"""_DetStemED on the raw BEV -> det_feat [1, 128, 400, 250], hm_pre, reg_pre (S:310)."""
s = self.down(raw, "det_stem/stem")
d8 = self.down(s, "det_stem/d8")
feat = self.conv(s + self.resize(self.conv(d8, "det_stem/u"), s.shape[-2:]), "det_stem/m/m.0", True)
taps.tap("det.feat", feat)
hm = taps.tap("det.hm_pre", self.conv(feat, "hm_head"))
return feat, hm, taps.tap("det.reg_pre", self.conv(feat, "reg_head"))
def occupancy(self, raw, taps: TapRegistry = NULL_TAPS):
"""occ stem on the raw BEV crop rows 200:600, cols 50:450 -> 1x1 160 -> view [1, 10, 16, 200, 200] (S:311)."""
(r0, r1), (c0, c1) = C.OCC_CROP
x = self.down(raw[:, :, r0:r1, c0:c1], "occ_stem")
taps.tap("occ.feat", x)
y = self.conv(x, "occ_head")
return y.reshape(1, len(C.OCC_CLASSES), C.OCC_Z_BINS, y.shape[-2], y.shape[-1])
def agent_forecast(self, raw, fused, det_feat, reg_pre, taps: TapRegistry = NULL_TAPS):
"""traj_stem on cat(fused, mot = (raw - hist) * mask = 0), agent attention pool, traj head (S:312)."""
torch = _torch()
mot = (raw - self._hist) * self._mask # the baked-out history: all zeros
stem = self.block(self.conv(torch.cat([fused, mot], dim=1), "traj_stem/traj_stem.0", True),
"traj_stem/traj_stem.3")
del mot
taps.tap("traj.stem", stem)
reg46 = reg_pre[:, 4:6]
keys = self.pool2(det_feat, "det_pool_w", "det_pool_h").reshape(1, det_feat.shape[1], -1).permute(2, 0, 1)
att = self.mha_pool("agent_attn", keys, self._queries["agent"]) # [1, 4, 128]
delta = self.conv(att.mean(dim=1).reshape(1, -1, 1, 1), "agent_delta")
taps.tap("traj.agent_delta", delta)
feat = taps.tap("traj.feat", torch.cat([stem, det_feat], dim=1) + delta)
return self.conv(torch.cat([feat, reg46], dim=1), "traj_head")
def stationary(self, raw, taps: TapRegistry = NULL_TAPS):
"""delta_stat on avgpool2(|raw - 0|) (S:313)."""
F = _torch().nn.functional
x = F.avg_pool2d((raw - self._hist).abs(), kernel_size=2, stride=2)
x = self.conv(self.conv(x, "delta_stat/delta_stat.1", True), "delta_stat/delta_stat.4", True)
taps.tap("stat.feat", x)
return self.conv(x, "delta_stat/delta_stat.7")
def risk(self, fused, taps: TapRegistry = NULL_TAPS):
"""risk head on the fused BEV crop rows 200:600, cols 125:375 (S:314)."""
(r0, r1), (c0, c1) = C.RISK_CROP
x = self.block(self.block(fused[:, :, r0:r1, c0:c1], "risk_head/risk_head.0"), "risk_head/risk_head.1")
taps.tap("risk.feat", x)
return self.conv(x, "risk_head/risk_head.2")
def planner(self, fused, lane_pre, hm_pre, risk, v0, taps: TapRegistry = NULL_TAPS):
"""Ego planner up to the e2e refiner (S:315): ego_stem + MLP, + ego_attn, + sem_ego, + kin, risk-aware
mode logits, decoupled decoder. Returns ``(ego_after_dec [1, 42], fused_mean [1, 96])``."""
torch = _torch()
F = torch.nn.functional
with taps.scope("ego"):
e = self.block(self.conv(fused, "ego_stem/ego_stem.0", True), "ego_stem/ego_stem.3") # s4
e = self.block(self.conv(e, "ego_stem/ego_stem.4", True), "ego_stem/ego_stem.7")
e = self.block(self.conv(e, "ego_stem/ego_stem.8", True), "ego_stem/ego_stem.11")
e = self.conv(e, "ego_stem/ego_stem.12", True) # [1, 256, 25, 16]
e = taps.tap("stem", self.pool2(e, "ego_stem_w", "ego_stem_h").flatten(1))
v0 = v0.reshape(1, 1).to(torch.float32)
h = torch.cat([e, v0], dim=1)
for i in (0, 2, 4):
h = F.relu(self.gemm(h, f"ego_mlp/ego_mlp.{i}"))
ego = taps.tap("mlp", self.gemm(h, "ego_mlp/ego_mlp.6"))
keys = self.pool2(fused, "fused_pool_w", "fused_pool_h").reshape(1, fused.shape[1], -1).permute(2, 0, 1)
att = self.mha_pool("ego_attn", keys, self._queries["ego"]) # [1, 3, 96]
ego = taps.tap("plus_attn", ego + taps.tap("attn", self.gemm(att.flatten(1), "ego_delta")))
sem_in = torch.cat([F.avg_pool2d(torch.softmax(lane_pre, dim=1), kernel_size=2, stride=2),
torch.sigmoid(hm_pre)], dim=1)
taps.tap("sem_in", sem_in)
s = self.conv(self.conv(self.conv(sem_in, "sem_ego/sem_ego.0", True), "sem_ego/sem_ego.3", True),
"sem_ego/sem_ego.6", True)
s = self.pool2(s, "sem_w", "sem_h").flatten(1)
s = self.gemm(F.relu(self.gemm(s, "sem_ego/sem_ego.11")), "sem_ego/sem_ego.13")
ego = taps.tap("plus_sem", ego + taps.tap("sem", s))
kin_in = torch.zeros(1, C.KIN_DIM, dtype=torch.float32)
kin_in[0, 0] = v0.reshape(()) / C.V0_SCALE
ego = taps.tap("plus_kin", ego + taps.tap("kin", self.gemm(kin_in, "kin_delta")))
# risk-aware mode logits: sample sigmoid(risk) (border padding) at each path's 6 waypoints (S:315)
paths = ego[:, :36].reshape(1, C.EGO_MODES, C.EGO_STEPS, 2)
gx = -paths[..., 1] / C.RISK_Y_HALF
gy = (C.RISK_X_HALF - paths[..., 0]) / C.RISK_X_HALF - 1.0
samp = F.grid_sample(torch.sigmoid(risk), torch.stack([gx, gy], dim=-1), mode="bilinear",
padding_mode="border", align_corners=False) # [1, 1, 3, 6]
samp = taps.tap("risk_sample", samp.mean(dim=3).squeeze(1)) # [1, 3]
ego = torch.cat([ego[:, :36], ego[:, 36:39] - self._risk_gate * samp, ego[:, 39:]], dim=1)
taps.tap("after_risk", ego)
# decoupled decoder on mean(fused): heading phi and speed v = min(softplus, 25) -> cumulative waypoints
fmean = self.pool2(fused, "fused_mean_w", "fused_mean_h").flatten(1)
dh = taps.tap("dec_head", self.gemm(fmean, "dec_head")).reshape(1, C.EGO_MODES, 12)
phi, vr = dh[..., :6], dh[..., 6:]
sp = torch.clamp(vr, min=0.0) + torch.log(1.0 + torch.exp(-vr.abs()))
step = torch.clamp(sp, max=C.DEC_SPEED_MAX) * 0.5
wp = taps.tap("dec_wp", torch.stack([torch.cumsum(step * torch.cos(phi), dim=2),
torch.cumsum(step * torch.sin(phi), dim=2)], dim=-1))
e36 = ego[:, :36].reshape(1, C.EGO_MODES, C.EGO_STEPS, 2)
e36 = e36 + torch.tanh(self._dec_gate) * (wp - e36)
ego = taps.tap("after_dec", torch.cat([e36.reshape(1, 36), ego[:, 36:]], dim=1))
return ego, fmean
@staticmethod
def _range_channel(h: int, w: int):
"""The refiners' constant range channel: row i -> (80 - (i + 0.5) * (160 / h)) / 80, float32 in graph order
(Range + 0.5, Reciprocal(h) * 160, Mul, 80 - ., / 80)."""
torch = _torch()
step = (1.0 / torch.tensor(float(h), dtype=torch.float32)) * 160.0
rows = (80.0 - (torch.arange(h, dtype=torch.float32) + 0.5) * step) / 80.0
return rows.reshape(1, 1, h, 1).expand(1, 1, h, w)
def seg_refiner(self, lane_pre, taps: TapRegistry = NULL_TAPS):
"""BEVSegRefiner: lane_logit = clamp(lane_pre, +-20) + 8 tanh(res / 8) (S:316)."""
torch = _torch()
lc = torch.clamp(lane_pre, -C.SEG_REFINE_CLAMP, C.SEG_REFINE_CLAMP)
x = torch.cat([lc, self._range_channel(lc.shape[-2], lc.shape[-1])], dim=1)
p = "refiner/seg"
stem = self.conv(x, f"{p}/stem/stem.0", True)
d1 = self.down(stem, f"{p}/d1")
d2 = self.down(d1, f"{p}/d2")
d3 = self.down(d2, f"{p}/d3")
d4 = self.down(d3, f"{p}/d4")
m3 = self.block(d3 + self.resize(self.conv(d4, f"{p}/u4"), d3.shape[-2:]), f"{p}/m3")
m2 = self.block(d2 + self.resize(self.conv(m3, f"{p}/u3"), d2.shape[-2:]), f"{p}/m2")
m1 = self.block(d1 + self.resize(self.conv(m2, f"{p}/u2"), d1.shape[-2:]), f"{p}/m1")
o = stem + self.resize(self.conv(m1, f"{p}/u1"), stem.shape[-2:])
res = taps.tap("refiner.seg_res", self.conv(self.conv(o, f"{p}/out/out.0", True), f"{p}/out/out.3"))
return lc + torch.tanh(res / C.SEG_REFINE_SCALE) * C.SEG_REFINE_SCALE
def box_refiner(self, hm_pre, reg_pre, taps: TapRegistry = NULL_TAPS):
"""BEVBoxRefiner: hm, reg = clamp(.) + 6 tanh(res / 6) (S:317)."""
torch = _torch()
hc = torch.clamp(hm_pre, -C.BOX_HM_CLAMP, C.BOX_HM_CLAMP)
rc = torch.clamp(reg_pre, -C.BOX_REG_CLAMP, C.BOX_REG_CLAMP)
x = torch.cat([hc, rc, self._range_channel(hc.shape[-2], hc.shape[-1])], dim=1)
p = "refiner/box"
stem = self.conv(x, f"{p}/stem/stem.0", True)
d1 = self.down(stem, f"{p}/d1")
d2 = self.down(d1, f"{p}/d2")
d3 = self.down(d2, f"{p}/d3")
m2 = self.block(d2 + self.resize(self.conv(d3, f"{p}/u3"), d2.shape[-2:]), f"{p}/m2")
m1 = self.block(d1 + self.resize(self.conv(m2, f"{p}/u2"), d1.shape[-2:]), f"{p}/m1")
o = stem + self.resize(self.conv(m1, f"{p}/u1"), stem.shape[-2:])
res = taps.tap("refiner.box_res", self.conv(self.conv(o, f"{p}/out/out.0", True), f"{p}/out/out.3"))
r = torch.tanh(res / C.BOX_REFINE_SCALE) * C.BOX_REFINE_SCALE
return hc + r[:, :2], rc + r[:, 2:]
def e2e_refiner(self, ego, v0, fused_mean, taps: TapRegistry = NULL_TAPS):
"""E2ERefiner: ego[:36] += 3 tanh(mlp(LN([ego, v0, mean(fused)]))) (S:318; LN at the logical width 139)."""
torch = _torch()
F = torch.nn.functional
g, b, eps = self._ln
x = torch.cat([ego, v0.reshape(1, 1).to(torch.float32), fused_mean], dim=1)
h = taps.tap("refiner.e2e_ln", F.layer_norm(x, (x.shape[-1],), g, b, eps))
h = F.relu(self.gemm(h, "refiner/e2e/mlp/mlp.0"))
h = F.relu(self.gemm(h, "refiner/e2e/mlp/mlp.2"))
r = taps.tap("refiner.e2e_res", torch.tanh(self.gemm(h, "refiner/e2e/mlp/mlp.4")) * C.E2E_SCALE)
return torch.cat([ego[:, :36] + r, ego[:, 36:]], dim=1)
def depth_mean(self, depth_logits):
"""avgpool2 of the 64 logits -> softmax -> argmax -> +-2-bin window -> sum p c / max(sum p, 1e-6) -> fp16
(ONNX nodes 1946-2014; bin centres per ``cfg.depth_mean_bins``)."""
torch = _torch()
F = torch.nn.functional
lp = F.avg_pool2d(depth_logits, kernel_size=2, stride=2)
p = torch.softmax(lp.reshape(1, C.N_CAMS, C.DEPTH_BINS, lp.shape[-2], lp.shape[-1]), dim=2)
am = torch.argmax(p, dim=2, keepdim=True)
r = torch.arange(C.DEPTH_BINS, dtype=torch.int64).reshape(1, 1, C.DEPTH_BINS, 1, 1)
pm = p * ((r - am).abs() <= 2).to(torch.float32)
num = (pm * self._depth_centres).sum(dim=2)
den = torch.clamp(pm.sum(dim=2), min=1e-6)
return (num / den).to(torch.float16)
# ---- whole graph ------------------------------------------------------------------------------------------------
def forward(self, imgs, K, T_cam_ego, v0, *, present=None, taps: TapRegistry = NULL_TAPS) -> Dict[str, Any]:
"""The 19 outputs of the graph (torch tensors, ONNX dtypes) for one frame.
``imgs`` uint8 [1, 8, 3, 432, 768] RGB, ``K`` [1, 8, 3, 3] at 768x432, ``T_cam_ego`` [1, 8, 4, 4], ``v0`` [1];
numpy or torch. ``present`` (8 flags) only matters for ``input_norm="imagenet"``."""
torch = _torch()
t = lambda a, dt: a.to(dt) if isinstance(a, torch.Tensor) else torch.from_numpy(np.ascontiguousarray(a)).to(dt)
imgs = t(imgs, torch.uint8)
K = t(K, torch.float32)
T = t(T_cam_ego, torch.float32)
v0 = t(v0, torch.float32).reshape(1)
out: Dict[str, Any] = {}
with torch.inference_mode():
x = taps.tap("img.input", self.normalize(imgs, present))
f = self.image_encoder(x, taps)
del x
seg = self.seg2d_head(f, taps)
seg_c = taps.tap("seg2d.clamped", torch.clamp(self.nan_to_num(seg), -C.SEG2D_CLAMP, C.SEG2D_CLAMP))
dlog, dprob = self.depth_head(f, taps)
ctx = self.context(f, seg, taps)
out.update(self.det2d_heads(f))
out["tl"] = self.tl_head(f)
del f
raw = self.lift(ctx, dprob, K, T, taps)
del ctx, dprob
fused = self.temporal_fuse(raw, taps)
lane_pre = self.lane_decoder(raw, taps)
det_feat, hm_pre, reg_pre = self.det_stem(raw, taps)
out["occ"] = self.occupancy(raw, taps)
out["traj"] = self.agent_forecast(raw, fused, det_feat, reg_pre, taps)
out["stationary"] = self.stationary(raw, taps)
del raw, det_feat
out["risk"] = self.risk(fused, taps)
ego, fmean = self.planner(fused, lane_pre, hm_pre, out["risk"], v0, taps)
taps.tap("bev.fused_mean", fmean)
out["lane_logit"] = self.seg_refiner(lane_pre, taps)
out["hm"], out["reg"] = self.box_refiner(hm_pre, reg_pre, taps)
e2e_mean = self.pool2(fused, "e2e_mean_w", "e2e_mean_h").flatten(1)
out["ego"] = self.e2e_refiner(ego, v0, e2e_mean, taps)
del fused
out["lane"] = torch.argmax(out["lane_logit"], dim=1).to(torch.uint8)
dl5 = dlog.reshape(1, C.N_CAMS, C.DEPTH_BINS, C.FEAT_H, C.FEAT_W)
out["depth"] = torch.argmax(dl5, dim=2).to(torch.uint8)
out["seg2d"] = torch.argmax(seg_c.reshape(1, C.N_CAMS, len(C.SEG2D_CLASSES), C.FEAT_H, C.FEAT_W),
dim=2).to(torch.uint8)
out["depth_mean"] = self.depth_mean(dlog)
for name in OUTPUT_NAMES:
taps.tap(name, out[name])
return {name: out[name] for name in OUTPUT_NAMES}
__call__ = forward