File size: 25,738 Bytes
ddaad99 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | # SPDX-License-Identifier: Apache-2.0
"""BEVFormer-small in torch fp32: the deployed ``forward_trt`` graph (DerryHub/BEVFormer_tensorrt, the export behind
Autoware's ``bevformer_small.onnx``), built from :class:`~.weights.BEVFormerWeights` with the backbone BatchNorms
folded exactly where the port folds them. Ported from the verified research reference
(``research/bevformer/scripts/bevformer_ref.py``; research/bevformer/SPEC.md sections 4 and 6).
Two entry points with the same outputs ``(bev_embed [22500, 1, 256], outputs_classes [6, 1, 900, 10],
outputs_coords [6, 1, 900, 10])``:
- :meth:`BEVFormerNet.forward` takes the five ONNX inputs (``image``, ``prev_bev``, ``use_prev_bev``, ``can_bus``,
``lidar2img``) and computes the geometry INSIDE the graph in fp32, as the deployed graph does: the oracle form,
compared with ONNX Runtime on the derived pure ONNX (``tests/test_reference_cpu.py``).
- :meth:`BEVFormerNet.forward_port` takes the geometry the TT port computes on the HOST (``host.geometry``:
``ref_cam`` / SCA tables from ``lidar2img`` in float64, the TSA shift, the prev_bev rotation index map) and runs the
port's exact rewrites: the rotation as a row gather with a zero row, the spatial cross-attention REBATCHED per
camera into ``SCA_CAPACITY`` rows and scatter-added back by a gather-form table (no dense 6 x 22,500 pass). Proven
equal to :meth:`forward` on CPU before any device work (``tests/test_reference_host.py``).
Per-module taps (``self.taps = {}`` before a call; ``None`` disables them): ``backbone.stem``, ``backbone.layer1-4``,
``neck``, ``enc.can_bus_mlp``, ``enc.bev_queries``, ``enc.value``, ``enc.prev_bev_rot``, ``enc.shift``, ``ref_cam``,
``bev_mask_n``, ``encoder.layer{i}.{tsa,norm0,sca,norm1,ffn}``, ``encoder.layer{i}`` (layer output),
``decoder.layer{i}.{self_attn,norm0,cross_attn,norm1,ffn,ref}``, ``decoder.layer{i}``, ``head.cls{i}``,
``head.reg{i}``. The research goldens use the same names for the ones they hold.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple
import numpy as np
import torch
import torch.nn.functional as F
from . import config as C
from . import ops
from .weights import BEVFormerWeights
__all__ = ["BEVFormerNet", "PortGeometry", "reference_points_3d", "ref_2d", "point_sampling", "can_bus_shift",
"inverse_sigmoid_mmdet", "inverse_sigmoid_dec"]
E = C.EMBED
Q = C.NUM_QUERY_BEV
# ------------------------------------------------------------------------------------------------ geometry (fp32)
def reference_points_3d() -> torch.Tensor:
"""``[1, 4, Q, 3]`` normalised (x, y, z) of the 4 pillar points of every BEV cell (DH encoder.py:169-195)."""
Z = C.PC_RANGE[5] - C.PC_RANGE[2]
zs = torch.linspace(0.5, Z - 0.5, C.PILLAR_POINTS).view(-1, 1, 1).expand(C.PILLAR_POINTS, C.BEV_H, C.BEV_W) / Z
xs = torch.linspace(0.5, C.BEV_W - 0.5, C.BEV_W).view(1, 1, C.BEV_W).expand(C.PILLAR_POINTS, C.BEV_H, C.BEV_W)
ys = torch.linspace(0.5, C.BEV_H - 0.5, C.BEV_H).view(1, C.BEV_H, 1).expand(C.PILLAR_POINTS, C.BEV_H, C.BEV_W)
return torch.stack((xs / C.BEV_W, ys / C.BEV_H, zs), -1).reshape(1, C.PILLAR_POINTS, -1, 3)
def ref_2d() -> torch.Tensor:
"""``[1, Q, 1, 2]`` normalised BEV cell centres (x, y), row-major over (y, x)."""
return reference_points_3d()[0, 0, :, :2].reshape(1, -1, 1, 2)
def point_sampling(ref_3d: torch.Tensor, lidar2img: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""``point_sampling_trt`` (DH encoder.py:197-259) in fp32 -> ``ref_cam [6, Q, 4, 2]``, ``bev_mask_n [6, Q, 1]``
(the any-of-4 camera mask normalised by the number of cameras that see the cell, clamp 1e-4)."""
pr = C.PC_RANGE
rp = ref_3d * torch.tensor([pr[3] - pr[0], pr[4] - pr[1], pr[5] - pr[2]]).view(1, 1, 1, 3) + torch.tensor(pr[:3])
rp = torch.cat((rp, torch.ones_like(rp[..., :1])), -1).view(C.PILLAR_POINTS, 1, 1, -1, 4, 1)
l2i = lidar2img.view(1, 1, C.NUM_CAMS, 1, 4, 4)
cam = torch.matmul(l2i, rp).squeeze(-1) # [4, 1, 6, Q, 4]
eps = C.MSDA_EPS
mask = (cam[..., 2:3] > eps).float()
uv = cam[..., 0:2] / torch.maximum(cam[..., 2:3], torch.ones_like(cam[..., 2:3]) * eps)
uv = uv / torch.tensor([C.IMG_HW[1], C.IMG_HW[0]], dtype=uv.dtype)
mask = mask * (uv[..., 1:2] > 0.0) * (uv[..., 1:2] < 1.0) * (uv[..., 0:1] < 1.0) * (uv[..., 0:1] > 0.0)
ref_cam = uv.permute(2, 1, 3, 0, 4).reshape(C.NUM_CAMS, -1, C.PILLAR_POINTS, 2)
m = (1 - (1 - mask).prod(0)).view(C.NUM_CAMS, -1, 1)
return ref_cam, m / torch.clamp(m.sum(0, keepdim=True), min=1e-4)
def can_bus_shift(can_bus: torch.Tensor) -> torch.Tensor:
"""TSA reference shift ``[1, 2]`` (x, y) in BEV units from can_bus[0], [1], [16], with DerryHub's atan2 surrogate
(``get_bev_features_trt``, DH transformer.py:262-294), in fp32 as the deployed graph."""
dx, dy = can_bus[0:1], can_bus[1:2]
ego_angle = can_bus[16:17] / math.pi * 180
tl = torch.sqrt(dx ** 2 + dy ** 2)
ta = (torch.atan(dy / (dx + 1e-8)) + ((1 - torch.sign(dx)) / 2) * torch.sign(dy) * math.pi) / math.pi * 180
ba = ego_angle - ta
sy = tl * torch.cos(ba / 180 * math.pi) / C.GRID_LENGTH / C.BEV_H
sx = tl * torch.sin(ba / 180 * math.pi) / C.GRID_LENGTH / C.BEV_W
return torch.cat([sx, sy]).unsqueeze(0)
def inverse_sigmoid_mmdet(x: torch.Tensor, eps: float = C.INV_SIG_EPS) -> torch.Tensor:
"""mmdet ``inverse_sigmoid`` (the head's coordinate decode)."""
x = x.clamp(min=0, max=1)
return torch.log(x.clamp(min=eps) / (1 - x).clamp(min=eps))
def inverse_sigmoid_dec(x: torch.Tensor, eps: float = C.INV_SIG_EPS) -> torch.Tensor:
"""The decoder's reference update (DH decoder.py:24-37)."""
x = x.clamp(min=eps, max=1 - eps)
return torch.log(x / (1 - x))
@dataclass
class PortGeometry:
"""The per-frame host geometry of the TT port (``host.geometry`` builds it; all torch, fp32 / int64):
``ref_cam [6, Q, 4, 2]``; ``sca_rows [6, CAP]`` (BEV query index of every rebatched row, ``Q`` = padding);
``sca_counts [6]``; ``sca_gather [Q, K]`` (row of the stacked ``[6 * CAP + 1]`` SCA output table for each of the
up to K cameras that see a query, ``6 * CAP`` = the zero row); ``sca_scale [Q, 1]`` (1 / cameras seeing it, 0 if
none); ``shift [1, 2]``; ``rot_index [Q]`` (source row of the rotated prev_bev, ``Q`` = the zero row)."""
ref_cam: torch.Tensor
sca_rows: torch.Tensor
sca_counts: torch.Tensor
sca_gather: torch.Tensor
sca_scale: torch.Tensor
shift: torch.Tensor
rot_index: torch.Tensor
@classmethod
def from_numpy(cls, d: Dict[str, np.ndarray]) -> "PortGeometry":
def t(name, dtype):
return torch.from_numpy(np.ascontiguousarray(d[name])).to(dtype)
return cls(t("ref_cam", torch.float32), t("sca_rows", torch.int64), t("sca_counts", torch.int64),
t("sca_gather", torch.int64), t("sca_scale", torch.float32), t("shift", torch.float32),
t("rot_index", torch.int64))
# ------------------------------------------------------------------------------------------------------- model
class BEVFormerNet:
"""The network as torch fp32 functions over the checkpoint tensors (BN folded in the backbone)."""
def __init__(self, weights: BEVFormerWeights, *, q_chunk: int = 4096):
self.weights = weights
self.q_chunk = int(q_chunk)
sd = weights.sd
self.p: Dict[str, torch.Tensor] = {k: torch.from_numpy(np.array(sd[k], np.float32)) for k in sd
if not k.startswith("img_backbone") and sd[k].dtype != np.int64}
self.conv: Dict[str, Tuple[torch.Tensor, torch.Tensor, int, int]] = {}
for module, _, _, _, _ in C.backbone_conv_specs():
cp = weights.conv(module)
self.conv[module] = (torch.from_numpy(cp.weight), torch.from_numpy(cp.bias), cp.stride[0],
cp.padding[0])
h = "pts_bbox_head.positional_encoding"
x_embed, y_embed = self.p[f"{h}.col_embed.weight"], self.p[f"{h}.row_embed.weight"]
pos = torch.cat([x_embed.unsqueeze(0).expand(C.BEV_H, C.BEV_W, -1),
y_embed.unsqueeze(1).expand(C.BEV_H, C.BEV_W, -1)], dim=-1)
self.bev_pos = pos.reshape(Q, E).contiguous() # [Q, 256], constant
self.taps: Optional[Dict[str, torch.Tensor]] = None
# ---- helpers ------------------------------------------------------------------------------------------------
def _tap(self, name: str, value: torch.Tensor) -> None:
if self.taps is not None:
self.taps[name] = value.detach().clone()
def _lin(self, name: str, x: torch.Tensor) -> torch.Tensor:
return F.linear(x, self.p[f"{name}.weight"], self.p[f"{name}.bias"])
def _ln(self, name: str, x: torch.Tensor) -> torch.Tensor:
return F.layer_norm(x, (E,), self.p[f"{name}.weight"], self.p[f"{name}.bias"], eps=C.LN_EPS)
def _conv(self, module: str, x: torch.Tensor, relu: bool) -> torch.Tensor:
w, b, s, pad = self.conv[module]
y = F.conv2d(x, w, b, stride=s, padding=pad)
return F.relu(y) if relu else y
# ---- image branch -------------------------------------------------------------------------------------------
def _dcn(self, module: str, x: torch.Tensor) -> torch.Tensor:
"""DCNv2 conv2 + folded bn2 + ReLU: offset conv (3x3 -> 27: dy/dx of 9 taps as o1 | o2, mask logits),
sigmoid mask, deformable sampling + GEMM with the BN-folded weight and bias."""
out = self._conv(f"{module}.conv_offset", x, False)
o1, o2, m = torch.chunk(out, 3, dim=1)
w, b, _, _ = self.conv[module]
return F.relu(ops.mdcn(x, torch.cat((o1, o2), dim=1), torch.sigmoid(m), w, b))
def backbone(self, x: torch.Tensor) -> torch.Tensor:
"""ResNet-101 (caffe style, DCNv2 in stages 3-4) on ``[6, 3, 736, 1280]`` -> C5 ``[6, 2048, 23, 40]``."""
y = F.max_pool2d(self._conv("img_backbone.conv1", x, True), 3, 2, 1)
self._tap("backbone.stem", y)
for s, (n, dcn) in enumerate(zip(C.RESNET_LAYERS, C.RESNET_DCN)):
for b in range(n):
p = f"img_backbone.layer{s + 1}.{b}"
t = self._conv(f"{p}.conv1", y, True)
t = self._dcn(f"{p}.conv2", t) if dcn else self._conv(f"{p}.conv2", t, True)
t = self._conv(f"{p}.conv3", t, False)
idt = self._conv(f"{p}.downsample.0", y, False) if b == 0 else y
y = F.relu(t + idt)
self._tap(f"backbone.layer{s + 1}", y)
return y
def neck(self, c5: torch.Tensor) -> torch.Tensor:
"""FPN on C5 only: lateral 1x1 (2048 -> 256) + 3x3 (256 -> 256), biases, no activation."""
n = "img_neck"
lat = F.conv2d(c5, self.p[f"{n}.lateral_convs.0.conv.weight"], self.p[f"{n}.lateral_convs.0.conv.bias"])
out = F.conv2d(lat, self.p[f"{n}.fpn_convs.0.conv.weight"], self.p[f"{n}.fpn_convs.0.conv.bias"], padding=1)
self._tap("neck", out)
return out
def extract_feat(self, image: torch.Tensor) -> torch.Tensor:
return self.neck(self.backbone(image.reshape(C.NUM_CAMS, 3, C.IMG_HW[0], C.IMG_HW[1])))
# ---- encoder pieces -----------------------------------------------------------------------------------------
def bev_queries(self, can_bus: torch.Tensor) -> torch.Tensor:
"""``bev_embedding + can_bus_mlp(can_bus)`` -> ``[Q, 256]`` (Linear 18 -> 128, ReLU, Linear -> 256, ReLU,
LayerNorm; one vector broadcast to every query)."""
t = "pts_bbox_head.transformer.can_bus_mlp"
cb = F.relu(self._lin(f"{t}.2", F.relu(self._lin(f"{t}.0", can_bus.view(1, 18)))))
cb = self._ln(f"{t}.norm", cb)
self._tap("enc.can_bus_mlp", cb.view(E))
q = self.p["pts_bbox_head.bev_embedding.weight"] + cb
self._tap("enc.bev_queries", q)
return q
def encoder_value(self, feat: torch.Tensor) -> torch.Tensor:
"""``[6, 256, 23, 40]`` -> SCA value ``[6, 920, 256]`` (+ cams_embeds[cam] + level_embeds[0])."""
t = "pts_bbox_head.transformer"
n, c, fh, fw = feat.shape
f = feat.reshape(n, c, fh * fw).permute(0, 2, 1)
v = f + self.p[f"{t}.cams_embeds"].view(n, 1, E) + self.p[f"{t}.level_embeds"][0].view(1, 1, E)
self._tap("enc.value", v)
return v
def tsa(self, i: int, query: torch.Tensor, value2: torch.Tensor, hybrid_ref: torch.Tensor) -> torch.Tensor:
"""TemporalSelfAttention (+ residual): ``query [Q, 256]``, ``value2 [2, Q, 256]`` (queue: [prev; current]),
``hybrid_ref [2, Q, 1, 2]``."""
a = f"pts_bbox_head.transformer.encoder.layers.{i}.attentions.0"
q = query + self.bev_pos
qcat = torch.cat([value2[0], q], -1).view(1, Q, 2 * E) # [1, Q, 512]
v = self._lin(f"{a}.value_proj", value2).view(C.TSA_QUEUE, Q, C.NUM_HEADS, C.HEAD_DIM)
off = self._lin(f"{a}.sampling_offsets", qcat).view(1, Q, C.NUM_HEADS, C.TSA_QUEUE, 1, C.TSA_POINTS, 2)
off = off.permute(0, 3, 1, 2, 4, 5, 6).reshape(C.TSA_QUEUE, Q, C.NUM_HEADS, C.TSA_POINTS * 2)
aw = self._lin(f"{a}.attention_weights", qcat).view(1, Q, C.NUM_HEADS, C.TSA_QUEUE, 1, C.TSA_POINTS)
aw = aw.permute(0, 3, 1, 2, 4, 5).reshape(C.TSA_QUEUE, Q, C.NUM_HEADS, C.TSA_POINTS)
out = ops.msda(v, [(C.BEV_H, C.BEV_W)], hybrid_ref, off, aw, self.q_chunk).flatten(2) # [2, Q, 256]
return self._lin(f"{a}.output_proj", out.mean(0)) + query
def _sca_proj(self, i: int, value: torch.Tensor, q_rows: torch.Tensor):
a = f"pts_bbox_head.transformer.encoder.layers.{i}.attentions.1.deformable_attention"
v = self._lin(f"{a}.value_proj", value).view(C.NUM_CAMS, C.FEAT_LEN, C.NUM_HEADS, C.HEAD_DIM)
off = self._lin(f"{a}.sampling_offsets", q_rows)
aw = self._lin(f"{a}.attention_weights", q_rows)
return v, off, aw
def sca_dense(self, i: int, query: torch.Tensor, value: torch.Tensor, ref_cam: torch.Tensor,
mask_n: torch.Tensor) -> torch.Tensor:
"""SpatialCrossAttention as the TRT graph runs it: dense over all 6 x 22,500 (camera, query) pairs, then the
normalised-mask sum over cameras (+ output_proj + residual)."""
qr = query.unsqueeze(0).expand(C.NUM_CAMS, Q, E)
v, off, aw = self._sca_proj(i, value, qr)
ref = ref_cam.reshape(C.NUM_CAMS, Q, 1, C.PILLAR_POINTS * 2)
out = ops.msda(v, [C.FEAT_HW], ref, off.view(C.NUM_CAMS, Q, C.NUM_HEADS, C.SCA_POINTS * 2),
aw.view(C.NUM_CAMS, Q, C.NUM_HEADS, C.SCA_POINTS), self.q_chunk).flatten(2)
slots = (out * mask_n).sum(0)
a = f"pts_bbox_head.transformer.encoder.layers.{i}.attentions.1"
return self._lin(f"{a}.output_proj", slots) + query
def sca_rebatched(self, i: int, query: torch.Tensor, value: torch.Tensor, geo: PortGeometry) -> torch.Tensor:
"""The port's SCA: per camera, gather its ``SCA_CAPACITY`` rows (valid queries + padding), run the linears
and the MSDA on those rows only, stack the six outputs with a zero row, and sum every query's rows through
the gather table, times 1 / cameras (identical to :meth:`sca_dense`: a query seen by no camera gets 0)."""
cap = geo.sca_rows.shape[1]
q_pad = torch.cat([query, torch.zeros(1, E, dtype=query.dtype)], 0) # row Q = padding
rows = q_pad[geo.sca_rows.reshape(-1)].view(C.NUM_CAMS, cap, E)
v, off, aw = self._sca_proj(i, value, rows)
ref_pad = torch.cat([geo.ref_cam, torch.zeros(C.NUM_CAMS, 1, C.PILLAR_POINTS, 2)], 1)
ref = torch.stack([ref_pad[c, geo.sca_rows[c]] for c in range(C.NUM_CAMS)]) # [6, cap, 4, 2]
out = ops.msda(v, [C.FEAT_HW], ref.reshape(C.NUM_CAMS, cap, 1, C.PILLAR_POINTS * 2),
off.view(C.NUM_CAMS, cap, C.NUM_HEADS, C.SCA_POINTS * 2),
aw.view(C.NUM_CAMS, cap, C.NUM_HEADS, C.SCA_POINTS), self.q_chunk).flatten(2)
table = torch.cat([out.reshape(C.NUM_CAMS * cap, E), torch.zeros(1, E, dtype=out.dtype)], 0)
slots = table[geo.sca_gather].sum(1) * geo.sca_scale # [Q, 256]
a = f"pts_bbox_head.transformer.encoder.layers.{i}.attentions.1"
return self._lin(f"{a}.output_proj", slots) + query
def ffn(self, prefix: str, x: torch.Tensor) -> torch.Tensor:
return x + self._lin(f"{prefix}.ffns.0.layers.1", F.relu(self._lin(f"{prefix}.ffns.0.layers.0.0", x)))
def encoder(self, bev_q: torch.Tensor, value: torch.Tensor, prev_rot: torch.Tensor, use_prev_bev: torch.Tensor,
shift: torch.Tensor, sca) -> torch.Tensor:
"""3 x (TSA -> LN -> SCA -> LN -> FFN -> LN). ``sca(i, query) -> query`` is the dense or rebatched SCA.
The TSA value is ``use * [rot(prev_bev); bev_queries] + (1 - use) * [query; query]`` with the CURRENT
layer's query in the second term (DH encoder.py:305, 587-589)."""
use = use_prev_bev.reshape(())
r2 = ref_2d()
hybrid = torch.cat([r2 + shift.view(1, 1, 1, 2) * use, r2], 0) # [2, Q, 1, 2]
prev = torch.stack([prev_rot, bev_q]) # [2, Q, 256]
query = bev_q
for i in range(C.ENC_LAYERS):
pre = f"pts_bbox_head.transformer.encoder.layers.{i}"
value2 = use * prev + (1 - use) * query.unsqueeze(0).expand(2, Q, E)
query = self.tsa(i, query, value2, hybrid)
self._tap(f"encoder.layer{i}.tsa", query)
query = self._ln(f"{pre}.norms.0", query)
self._tap(f"encoder.layer{i}.norm0", query)
query = sca(i, query)
self._tap(f"encoder.layer{i}.sca", query)
query = self._ln(f"{pre}.norms.1", query)
self._tap(f"encoder.layer{i}.norm1", query)
query = self.ffn(pre, query)
self._tap(f"encoder.layer{i}.ffn", query)
query = self._ln(f"{pre}.norms.2", query)
self._tap(f"encoder.layer{i}", query)
return query
# ---- decoder + heads ----------------------------------------------------------------------------------------
def _mha(self, i: int, x: torch.Tensor, pos: torch.Tensor) -> torch.Tensor:
"""mmcv MultiheadAttention (q = k = x + pos, v = x, 8 heads, batch_first False) + residual."""
a = f"pts_bbox_head.transformer.decoder.layers.{i}.attentions.0.attn"
wq, wk, wv = self.p[f"{a}.in_proj_weight"].chunk(3, 0)
bq, bk, bv = self.p[f"{a}.in_proj_bias"].chunk(3, 0)
qk = x + pos
n = x.shape[0]
q = F.linear(qk, wq, bq).view(n, C.NUM_HEADS, C.HEAD_DIM).transpose(0, 1)
k = F.linear(qk, wk, bk).view(n, C.NUM_HEADS, C.HEAD_DIM).transpose(0, 1)
v = F.linear(x, wv, bv).view(n, C.NUM_HEADS, C.HEAD_DIM).transpose(0, 1)
att = torch.softmax(torch.matmul(q, k.transpose(1, 2)) * (1.0 / math.sqrt(C.HEAD_DIM)), dim=-1)
o = torch.matmul(att, v).transpose(0, 1).reshape(n, E)
return x + self._lin(f"{a}.out_proj", o)
def _cross(self, i: int, x: torch.Tensor, pos: torch.Tensor, bev: torch.Tensor, ref_xy: torch.Tensor):
"""CustomMSDeformableAttention over the BEV (150 x 150, 4 points at ``ref_xy``) + residual."""
a = f"pts_bbox_head.transformer.decoder.layers.{i}.attentions.1"
q = (x + pos).view(1, -1, E)
n = q.shape[1]
v = self._lin(f"{a}.value_proj", bev.view(1, Q, E)).view(1, Q, C.NUM_HEADS, C.HEAD_DIM)
off = self._lin(f"{a}.sampling_offsets", q).view(1, n, C.NUM_HEADS, C.DEC_POINTS * 2)
aw = self._lin(f"{a}.attention_weights", q).view(1, n, C.NUM_HEADS, C.DEC_POINTS)
out = ops.msda(v, [(C.BEV_H, C.BEV_W)], ref_xy.view(1, n, 1, 2), off, aw, self.q_chunk).flatten(2)
return x + self._lin(f"{a}.output_proj", out.view(n, E))
def _cls(self, i: int, h: torch.Tensor) -> torch.Tensor:
b = f"pts_bbox_head.cls_branches.{i}"
h = F.relu(self._ln(f"{b}.1", self._lin(f"{b}.0", h)))
h = F.relu(self._ln(f"{b}.4", self._lin(f"{b}.3", h)))
return self._lin(f"{b}.6", h)
def _reg(self, i: int, h: torch.Tensor) -> torch.Tensor:
b = f"pts_bbox_head.reg_branches.{i}"
h = F.relu(self._lin(f"{b}.2", F.relu(self._lin(f"{b}.0", h))))
return self._lin(f"{b}.4", h)
def decode(self, bev: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""6 decoder layers (MHA -> LN -> deformable cross-attention -> LN -> FFN -> LN, box refinement) + the six
cls / reg heads -> ``outputs_classes``, ``outputs_coords`` ``[6, 1, 900, 10]`` (cx, cy, cz in metres)."""
qe = self.p["pts_bbox_head.query_embedding.weight"]
pos, x = qe[:, :E], qe[:, E:]
ref = torch.sigmoid(self._lin("pts_bbox_head.transformer.reference_points", pos)) # [900, 3]
init_ref = ref
cls_all, crd_all = [], []
pr = C.PC_RANGE
for i in range(C.DEC_LAYERS):
pre = f"pts_bbox_head.transformer.decoder.layers.{i}"
ref_in = ref
x = self._mha(i, x, pos)
self._tap(f"decoder.layer{i}.self_attn", x)
x = self._ln(f"{pre}.norms.0", x)
self._tap(f"decoder.layer{i}.norm0", x)
x = self._cross(i, x, pos, bev, ref[:, :2])
self._tap(f"decoder.layer{i}.cross_attn", x)
x = self._ln(f"{pre}.norms.1", x)
self._tap(f"decoder.layer{i}.norm1", x)
x = self.ffn(pre, x)
self._tap(f"decoder.layer{i}.ffn", x)
x = self._ln(f"{pre}.norms.2", x)
self._tap(f"decoder.layer{i}", x)
t = self._reg(i, x) # the head's reg == the update's
ref = torch.sigmoid(torch.cat([t[:, 0:2] + inverse_sigmoid_dec(ref[:, :2]),
t[:, 4:5] + inverse_sigmoid_dec(ref[:, 2:3])], -1))
self._tap(f"decoder.layer{i}.ref", ref)
r = inverse_sigmoid_mmdet(init_ref if i == 0 else ref_in)
cls = self._cls(i, x)
xy = torch.sigmoid(t[:, 0:2] + r[:, 0:2])
z = torch.sigmoid(t[:, 4:5] + r[:, 2:3])
crd = torch.cat([xy[:, 0:1] * (pr[3] - pr[0]) + pr[0], xy[:, 1:2] * (pr[4] - pr[1]) + pr[1], t[:, 2:4],
z * (pr[5] - pr[2]) + pr[2], t[:, 5:]], -1)
self._tap(f"head.cls{i}", cls)
self._tap(f"head.reg{i}", t)
cls_all.append(cls)
crd_all.append(crd)
return torch.stack(cls_all).unsqueeze(1), torch.stack(crd_all).unsqueeze(1)
# ---- entry points -------------------------------------------------------------------------------------------
@torch.no_grad()
def forward(self, image, prev_bev, use_prev_bev, can_bus, lidar2img):
"""The deployed graph: the five ONNX inputs (any array-likes, float32) -> the three ONNX outputs (torch)."""
image, prev_bev, use_prev_bev, can_bus, lidar2img = (torch.as_tensor(np.asarray(a, np.float32)) for a in
(image, prev_bev, use_prev_bev, can_bus, lidar2img))
feat = self.extract_feat(image)
value = self.encoder_value(feat)
bev_q = self.bev_queries(can_bus)
shift = can_bus_shift(can_bus)
self._tap("enc.shift", shift.view(2))
pb = prev_bev.reshape(C.BEV_H, C.BEV_W, E).permute(2, 0, 1)
pb = ops.rotate(pb, can_bus[17:18], C.ROTATE_CENTER).permute(1, 2, 0).reshape(Q, E)
self._tap("enc.prev_bev_rot", pb)
ref_cam, mask_n = point_sampling(reference_points_3d(), lidar2img)
self._tap("ref_cam", ref_cam)
self._tap("bev_mask_n", mask_n)
bev = self.encoder(bev_q, value, pb, use_prev_bev, shift,
lambda i, q: self.sca_dense(i, q, value, ref_cam, mask_n))
cls, crd = self.decode(bev)
return bev.view(Q, 1, E), cls, crd
@torch.no_grad()
def forward_port(self, image, prev_bev, use_prev_bev, can_bus, geo: PortGeometry):
"""The TT port's decomposition with host geometry ``geo`` (see :class:`PortGeometry`)."""
image, prev_bev, use_prev_bev, can_bus = (torch.as_tensor(np.asarray(a, np.float32)) for a in
(image, prev_bev, use_prev_bev, can_bus))
feat = self.extract_feat(image)
value = self.encoder_value(feat)
bev_q = self.bev_queries(can_bus)
self._tap("enc.shift", geo.shift.view(2))
table = torch.cat([prev_bev.reshape(Q, E), torch.zeros(1, E)], 0)
pb = table[geo.rot_index] # row gather, zero row
self._tap("enc.prev_bev_rot", pb)
self._tap("ref_cam", geo.ref_cam)
bev = self.encoder(bev_q, value, pb, use_prev_bev, geo.shift,
lambda i, q: self.sca_rebatched(i, q, value, geo))
cls, crd = self.decode(bev)
return bev.view(Q, 1, E), cls, crd
def run(self, *args: Any, taps: bool = False, port: bool = False):
"""``forward`` / ``forward_port`` returning numpy plus the taps (numpy) when ``taps``."""
self.taps = {} if taps else None
try:
outs = (self.forward_port if port else self.forward)(*args)
tp = {k: v.numpy() for k, v in (self.taps or {}).items()}
finally:
self.taps = None
return tuple(o.numpy() for o in outs), tp
|