Download code/tt_transfusion/reference/weights.py from changh95/transfusion-p150: direct link, hf CLI and curl.
- Browser
- Download file 30.1 kB
-
https://huggingface.co/changh95/transfusion-p150/resolve/main/code/tt_transfusion/reference/weights.py
- Command line
-
hf download hf://changh95/transfusion-p150/code/tt_transfusion/reference/weights.py
-
curl -L -o weights.py https://huggingface.co/changh95/transfusion-p150/resolve/main/code/tt_transfusion/reference/weights.py
30.1 kB
| # SPDX-License-Identifier: Apache-2.0 | |
| """The deployed ``transfusion.onnx`` of AutowareFoundation/lidar_transfusion@v2.1 read as DATA, the way the TT port | |
| reads it: the vendored ``ttaw.weights.OnnxWeights`` (initializers and Constant tensors addressed by the node that | |
| consumes them; nothing is executed), the module structure checked against the graph topology, BatchNorm folded in | |
| float64 with one final rounding where the port folds it. | |
| The export (mmdeploy, opset 11, PyTorch 2.2) names its nodes after the mmdet3d modules (``/pts_voxel_encoder/...``, | |
| ``/pts_backbone/blocks.1/blocks.1.3/Conv``, ``/pts_bbox_head/decoder.0/cross_attn/attn/MatMul_1``), so layers are | |
| addressed by node name and every assumption the reference makes about the wiring is asserted from the graph | |
| (S:transfusion:130-208): | |
| - PFN (``/pts_voxel_encoder``): the in-graph decoration constants (``Mul`` = ``(voxel_z, voxel_y, voxel_x)``, | |
| ``Add`` = ``voxel / 2 + min``, z-y-x order, reversed by ``Slice_3``) and the two | |
| ``MatMul -> Transpose -> BatchNormalization(eps 1e-3) -> Transpose -> Relu`` layers, BN **folded here** into | |
| ``W' = W * s``, ``b' = beta - mean * s`` (what the port's ``ttnn.linear`` weights are); layer 1 consumes | |
| ``concat[h, tile(max over the 20 slots of h)]``; | |
| - SECOND (``/pts_backbone/blocks.{0,1,2}``): Conv(+Relu) chains with the exporter's BN fold already in the biases; | |
| - SECONDFPN (``/pts_neck``): deblock0 = Conv 2x2 stride 2 (+Relu), deblock1 = Conv 1x1 (+Relu), deblock2 = | |
| ConvTranspose 2x2 stride 2 (no bias) + BatchNormalization(eps 1e-3) **folded here** + Relu; their concat; | |
| - head (``/pts_bbox_head``): shared conv 3x3 (no activation), heatmap head (3x3 + Relu, 3x3 -> 5), the 3x3 / stride | |
| 1 / pad 0 MaxPool with a zero border, TopK ``K``, the class encoding (Conv1d 5 -> 128 of a one-hot), the constant | |
| ``bev_pos``, one decoder layer (position-embedding MLPs, 8-head self- and cross-attention with a ``Div`` pre-scale, | |
| post-LayerNorms, FFN 128 -> 256 -> 128) and the six prediction heads (Conv1d 128 -> 64 + Relu, Conv1d 64 -> C); | |
| the three graph outputs are assembled as ``bbox_pred0 = [center + query_pos, height, dim, vel]``, | |
| ``dir_cls_pred0 = rot``, ``cls_score0 = query_heat * sigmoid(heatmap)``. | |
| :attr:`TransFusionWeights.onnx_taps` maps every reference tap name to the ONNX tensor that holds the same value, so | |
| the ONNX Runtime golden can expose matching intermediates (``reference/ort.py``). | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional, Sequence, Tuple | |
| import numpy as np | |
| from ..ttaw.weights import OnnxWeights, file_sha256, fold_bn_conv_transpose, fold_bn_linear | |
| from .config import NUM_CLASSES, ONNX_FILE, OUT_SIZE_FACTOR, PILLAR_FEATURE_SIZE, TransFusionConfig | |
| __all__ = ["Layer", "PfnParams", "AttentionParams", "DecoderParams", "HeadParams", "TransFusionWeights", | |
| "load_weights", "PREDICTION_HEADS", "VE", "BB", "NK", "HB", "DEC"] | |
| VE, BB, NK, HB = "/pts_voxel_encoder", "/pts_backbone", "/pts_neck", "/pts_bbox_head" | |
| DEC = f"{HB}/decoder.0" | |
| # output order of the prediction heads in the ONNX (``common_heads`` + heatmap), and their channel counts | |
| PREDICTION_HEADS = (("center", 2), ("height", 1), ("dim", 3), ("rot", 2), ("vel", 2), ("heatmap", NUM_CLASSES)) | |
| class Layer: | |
| """One conv layer, BN folded: ``y = relu?(conv(x, weight) + bias)`` (2-D, or 1-D for the head's 1x1 Conv1d).""" | |
| name: str # tap name ("backbone.block0.conv0", "neck.deblock2", "heads.center.hidden", ...) | |
| kind: str # "conv" | "conv_transpose" | "conv1d" | |
| weight: np.ndarray # float32: conv [Cout, Cin, k, k]; conv_transpose [Cin, Cout, k, k]; conv1d [Cout, Cin, 1] | |
| bias: np.ndarray # float32 [Cout] | |
| stride: int | |
| padding: int | |
| relu: bool | |
| onnx_node: str | |
| onnx_output: str # ONNX tensor equal to this layer's output (after the Relu when there is one) | |
| bn_node: Optional[str] = None # BatchNormalization folded here (None: none, or exporter-folded) | |
| def kernel(self) -> int: | |
| return int(self.weight.shape[-1]) | |
| def in_channels(self) -> int: | |
| return int(self.weight.shape[0] if self.kind == "conv_transpose" else self.weight.shape[1]) | |
| def out_channels(self) -> int: | |
| return int(self.weight.shape[1] if self.kind == "conv_transpose" else self.weight.shape[0]) | |
| def num_params(self) -> int: | |
| return int(self.weight.size + self.bias.size) | |
| class PfnParams: | |
| """PillarFeatureNet (``feat_channels=[64, 64]``, legacy decoration) with BN folded. ``w*`` are ``[in, out]``. | |
| Layer 1 consumes ``concat[h, tile(max_slots h)]``: rows 0-31 of ``w1`` act on ``h`` (``w1a``), rows 32-63 on the | |
| slot max (``w1b``).""" | |
| w0: np.ndarray # (11, 32) float32 | |
| b0: np.ndarray # (32,) | |
| w1: np.ndarray # (64, 64) | |
| b1: np.ndarray # (64,) | |
| scale_zyx: np.ndarray # (3,) float32 decoration constants (ONNX /pts_voxel_encoder/Mul) | |
| offset_zyx: np.ndarray # (3,) float32 (ONNX /pts_voxel_encoder/Add) | |
| nodes: Dict[str, str] | |
| def w1a(self) -> np.ndarray: | |
| return self.w1[: self.w0.shape[1]] | |
| def w1b(self) -> np.ndarray: | |
| return self.w1[self.w0.shape[1]:] | |
| class AttentionParams: | |
| """``nn.MultiheadAttention`` as exported: ``q = x_q @ wq + bq`` (``[in, out]`` layout), likewise k / v, per-head | |
| ``softmax((q / sqrt(d)) @ k^T) @ v``, ``out = concat_heads @ wo + bo`` (``wo`` stored ``[in, out]`` here; the | |
| ONNX Gemm keeps ``[out, in]`` with transB=1).""" | |
| wq: np.ndarray | |
| bq: np.ndarray | |
| wk: np.ndarray | |
| bk: np.ndarray | |
| wv: np.ndarray | |
| bv: np.ndarray | |
| wo: np.ndarray | |
| bo: np.ndarray | |
| num_heads: int | |
| head_dim: int | |
| q_divisor: float # the ONNX pre-scale Div constant (sqrt(head_dim) = 4.0) | |
| def scale(self) -> float: | |
| return 1.0 / self.q_divisor | |
| class DecoderParams: | |
| """One TransFusion decoder layer (post-norm): self-attention on ``query + query_pos_embed`` (Q = K = V), | |
| cross-attention ``query = x + query_pos_embed`` over ``key = value = lidar_feat + key_pos_embed``, FFN.""" | |
| self_posembed: Tuple[Layer, Layer] # Conv1d 2 -> 128 + Relu, Conv1d 128 -> 128 | |
| cross_posembed: Tuple[Layer, Layer] | |
| self_attn: AttentionParams | |
| cross_attn: AttentionParams | |
| norms: Tuple[Tuple[np.ndarray, np.ndarray, float], ...] # (gamma, beta, eps) x 3 | |
| ffn_w1: np.ndarray # (128, 256) [in, out] | |
| ffn_b1: np.ndarray | |
| ffn_w2: np.ndarray # (256, 128) | |
| ffn_b2: np.ndarray | |
| class HeadParams: | |
| shared: Layer # 3x3 384 -> 128, no activation (lidar_feat) | |
| heatmap_hidden: Layer # 3x3 128 -> 128 + Relu (BN exporter-folded) | |
| heatmap_out: Layer # 3x3 128 -> 5 (logits) | |
| nms_kernel: int # 3 (MaxPool kernel; stride 1, pad 0 + zero border) | |
| num_proposals: int # TopK K = 500 | |
| class_encoding_w: np.ndarray # (128, 5) = Conv1d weight[:, :, 0] | |
| class_encoding_b: np.ndarray # (128,) | |
| bev_pos: np.ndarray # (36864, 2) float32 constant: (x + 0.5, y + 0.5), k = y * 192 + x | |
| decoder: DecoderParams | |
| prediction_heads: Tuple[Tuple[str, Layer, Layer], ...] # (name, Conv1d 128 -> 64 + Relu, Conv1d 64 -> C) | |
| def class_table(self) -> np.ndarray: | |
| """``(5, 128)`` float32: row c = ``class_encoding(one_hot(c))`` = ``W[:, c] + b`` (one fp32 rounding, as the | |
| Conv1d of a one-hot input computes it).""" | |
| return (self.class_encoding_w.T.astype(np.float32) + self.class_encoding_b.astype(np.float32)).astype( | |
| np.float32) | |
| class TransFusionWeights: | |
| pfn: PfnParams | |
| blocks: Tuple[Tuple[Layer, ...], ...] # SECOND: 3 blocks of 3x3 convs (4 / 6 / 6) | |
| deblocks: Tuple[Layer, ...] # SECONDFPN: conv 2x2 s2, conv 1x1, convT 2x2 s2 (BN folded) | |
| head: HeadParams | |
| onnx_taps: Dict[str, str] # reference tap name -> ONNX tensor name | |
| sha256: str | |
| path: str | |
| grid: Tuple[int, int] # canvas (W, H) = 768 x 768 (the scatter's row stride and size) | |
| def conv_layers(self) -> List[Layer]: | |
| """Every 2-D conv layer (backbone, neck, head) in forward order.""" | |
| h = self.head | |
| return ([layer for block in self.blocks for layer in block] + list(self.deblocks) | |
| + [h.shared, h.heatmap_hidden, h.heatmap_out]) | |
| def num_params(self) -> int: | |
| """Learned parameters after the folds (the ONNX has 5,270,580; the PFN and deblock2 BNs fold into biases).""" | |
| p = self.pfn | |
| d = self.head.decoder | |
| n = sum(x.size for x in (p.w0, p.b0, p.w1, p.b1)) | |
| n += sum(layer.num_params for layer in self.conv_layers()) | |
| n += self.head.class_encoding_w.size + self.head.class_encoding_b.size | |
| for pe in (d.self_posembed, d.cross_posembed): | |
| n += sum(layer.num_params for layer in pe) | |
| for a in (d.self_attn, d.cross_attn): | |
| n += sum(x.size for x in (a.wq, a.bq, a.wk, a.bk, a.wv, a.bv, a.wo, a.bo)) | |
| n += sum(g.size + b.size for g, b, _ in d.norms) | |
| n += sum(x.size for x in (d.ffn_w1, d.ffn_b1, d.ffn_w2, d.ffn_b2)) | |
| for _, hid, fin in self.head.prediction_heads: | |
| n += hid.num_params + fin.num_params | |
| return int(n) | |
| # ------------------------------------------------------------------------------------------------- helpers | |
| def _f32(a: Any) -> np.ndarray: | |
| return np.asarray(a, dtype=np.float32) | |
| def _single(found: Sequence[Any], what: str) -> Any: | |
| if len(found) != 1: | |
| raise ValueError(f"expected exactly one {what}, found {len(found)}") | |
| return found[0] | |
| def _consumer(w: OnnxWeights, tensor: str, op_type: str, through: Sequence[str] = ()) -> Any: | |
| """The single ``op_type`` consumer of ``tensor``, looking through single-consumer ``through`` ops.""" | |
| node = _single([n for n in w.consumers(tensor) if n.op_type not in ("Shape",)], f"consumer of {tensor!r}") | |
| while node.op_type in through: | |
| node = _single([n for n in w.consumers(node.outputs[0]) if n.op_type not in ("Shape",)], | |
| f"consumer of {node.outputs[0]!r}") | |
| if node.op_type != op_type: | |
| raise ValueError(f"{tensor!r} feeds {node.name} ({node.op_type}), expected {op_type}") | |
| return node | |
| def _producer(w: OnnxWeights, tensor: str, op_type: str) -> Any: | |
| node = w.producer(tensor) | |
| if node is None or node.op_type != op_type: | |
| raise ValueError(f"{tensor!r} is produced by {getattr(node, 'op_type', None)}, expected {op_type}") | |
| return node | |
| def _expect(cond: bool, msg: str) -> None: | |
| if not cond: | |
| raise ValueError(f"transfusion.onnx structure: {msg}") | |
| def _scalar(w: OnnxWeights, node: str, slot: int) -> float: | |
| a = np.asarray(w.param(node, slot)).reshape(-1) | |
| _expect(a.size == 1, f"{node} input {slot} is not a scalar constant") | |
| return float(a[0]) | |
| def _conv_layer(w: OnnxWeights, name: str, node_name: str, *, relu_node: Optional[str] = None, | |
| kind: str = "conv") -> Layer: | |
| node = w.node(node_name) | |
| c = w.conv(node_name) | |
| k = c.kernel_shape | |
| _expect(c.group == 1 and all(d == 1 for d in c.dilations) and len(set(c.strides)) == 1 | |
| and len(set(c.pads)) == 1 and len(set(k)) == 1 and c.auto_pad == "NOTSET", | |
| f"{node_name}: unexpected attributes {c}") | |
| out = node.outputs[0] | |
| if relu_node is not None: | |
| relu = w.node(relu_node) | |
| _expect(relu.op_type == "Relu" and relu.inputs[0] == out, f"{relu_node} is not the Relu of {node_name}") | |
| out = relu.outputs[0] | |
| bias = np.zeros(c.weight.shape[0], np.float32) if c.bias is None else _f32(c.bias) | |
| return Layer(name, kind, _f32(c.weight), bias, int(c.strides[0]), int(c.pads[0]), relu_node is not None, | |
| node_name, out) | |
| # ------------------------------------------------------------------------------------------------------ PFN | |
| def _load_pfn(w: OnnxWeights, cfg: Optional[TransFusionConfig]) -> Tuple[PfnParams, Dict[str, str]]: | |
| folded, nodes, relu_out = [], {}, [] | |
| for i in range(2): | |
| mm = w.node(f"{VE}/pfn_layers.{i}/linear/MatMul") | |
| wt = np.asarray(w.matmul_weight(mm.name), np.float64) # [in, out] | |
| bn = _consumer(w, mm.outputs[0], "BatchNormalization", through=("Transpose",)) | |
| relu = _consumer(w, bn.outputs[0], "Relu", through=("Transpose",)) | |
| wf, bf = fold_bn_linear(wt, None, w.batchnorm(bn.name), layout="in_out") | |
| folded.append((_f32(wf), _f32(bf))) | |
| nodes.update({f"matmul{i}": mm.name, f"bn{i}": bn.name}) | |
| relu_out.append(relu.outputs[0]) | |
| (w0, b0), (w1, b1) = folded | |
| _expect(w0.shape == (11, 32) and w1.shape == (64, 64), f"PFN shapes {w0.shape} / {w1.shape}") | |
| # decoration: concat[f_center, intensity / time_lag, f_cluster, f_center] * mask -> MatMul 0 | |
| concat = w.node(f"{VE}/Concat") | |
| sub, sub1 = w.node(f"{VE}/Sub").outputs[0], w.node(f"{VE}/Sub_1").outputs[0] | |
| _expect(list(concat.inputs) == [sub1, w.node(f"{VE}/Slice_4").outputs[0], sub, sub1], | |
| f"decoration concat inputs {concat.inputs} (legacy: [f_center, i/t, f_cluster, f_center])") | |
| mask_mul = w.node(f"{VE}/Mul_1") | |
| _expect(mask_mul.inputs[0] == concat.outputs[0] and w.node(nodes["matmul0"]).inputs[0] == mask_mul.outputs[0], | |
| "masked decoration does not feed PFN layer 0") | |
| scale = _f32(w.param(f"{VE}/Mul", 1)).reshape(-1) | |
| offset = _f32(w.param(f"{VE}/Add", 1)).reshape(-1) | |
| _expect(scale.shape == (3,) and offset.shape == (3,), "decoration constants are not 3-vectors") | |
| if cfg is not None: | |
| _expect(np.array_equal(scale, cfg.decoration_scale_zyx) and np.array_equal(offset, cfg.decoration_offset_zyx), | |
| f"decoration constants {scale} / {offset} disagree with the ml_package range / voxel size " | |
| f"({cfg.decoration_scale_zyx} / {cfg.decoration_offset_zyx})") | |
| # layer 1 input is concat[h, tile(max over slots h)]; the output is the slot max of layer 1 | |
| c2 = w.node(f"{VE}/pfn_layers.0/Concat_2") | |
| _expect(c2.inputs[0] == relu_out[0] and int(c2.attrs.get("axis", -1)) == 2 | |
| and w.node(nodes["matmul1"]).inputs[0] == c2.outputs[0], "PFN layer 1 input is not concat[h, max h]") | |
| rmax0 = w.node(f"{VE}/pfn_layers.0/ReduceMax") | |
| rmax1 = w.node(f"{VE}/pfn_layers.1/ReduceMax") | |
| _expect(rmax0.inputs[0] == relu_out[0] and list(rmax0.attrs.get("axes", [])) == [1] | |
| and rmax1.inputs[0] == relu_out[1] and list(rmax1.attrs.get("axes", [])) == [1], | |
| "PFN maxima are not over the point slots") | |
| squeeze = w.node(f"{VE}/Squeeze") | |
| _expect(squeeze.inputs[0] == rmax1.outputs[0], "PFN output is not the squeezed slot max of layer 1") | |
| taps = {"pfn.input": mask_mul.outputs[0], "pfn.layer0": relu_out[0], "pfn": squeeze.outputs[0]} | |
| return PfnParams(w0, b0, w1, b1, scale, offset, nodes), taps | |
| # ------------------------------------------------------------------------------------------ SECOND / SECONDFPN | |
| def _load_backbone_neck(w: OnnxWeights, canvas: str) -> Tuple[tuple, tuple, Dict[str, str]]: | |
| taps: Dict[str, str] = {} | |
| blocks, prev = [], canvas | |
| for b in range(3): | |
| convs = w.nodes("Conv", f"{BB}/blocks.{b}/*") | |
| layers = [] | |
| for j, conv in enumerate(convs): | |
| _expect(conv.inputs[0] == prev, f"{conv.name} does not consume the previous layer output") | |
| relu = _consumer(w, conv.outputs[0], "Relu") | |
| layer = _conv_layer(w, f"backbone.block{b}.conv{j}", conv.name, relu_node=relu.name) | |
| _expect(layer.kernel == 3 and layer.padding == 1 and layer.stride == (2 if j == 0 else 1), | |
| f"{conv.name}: SECOND conv must be 3x3 pad 1, stride 2 first") | |
| layers.append(layer) | |
| taps[layer.name] = layer.onnx_output | |
| prev = layer.onnx_output | |
| blocks.append(tuple(layers)) | |
| taps[f"backbone.block{b}"] = prev | |
| _expect([len(b) for b in blocks] == [4, 6, 6], f"blocks have {[len(b) for b in blocks]} convs, expected 4/6/6") | |
| concat = w.node(f"{NK}/Concat") | |
| _expect(int(concat.attrs.get("axis", -1)) == 1 and len(concat.inputs) == 3, "neck concat") | |
| deblocks = [] | |
| for i, t in enumerate(concat.inputs): | |
| relu = _producer(w, t, "Relu") | |
| src = w.producer(relu.inputs[0]) | |
| if src.op_type == "Conv": | |
| layer = _conv_layer(w, f"neck.deblock{i}", src.name, relu_node=relu.name) | |
| elif src.op_type == "BatchNormalization": | |
| convt = _producer(w, src.inputs[0], "ConvTranspose") | |
| c = w.conv(convt.name) | |
| _expect(c.group == 1 and c.kernel_shape == tuple(c.strides) and not any(c.pads) and c.bias is None | |
| and c.output_padding in ((), (0, 0)), f"{convt.name}: expected ConvTranspose k == s, no pad/bias") | |
| wf, bf = fold_bn_conv_transpose(np.asarray(c.weight, np.float64), None, w.batchnorm(src.name)) | |
| layer = Layer(f"neck.deblock{i}", "conv_transpose", _f32(wf), _f32(bf), int(c.strides[0]), 0, True, | |
| convt.name, relu.outputs[0], bn_node=src.name) | |
| else: | |
| raise ValueError(f"transfusion.onnx structure: neck concat input {i} comes from {src.op_type}") | |
| _expect(w.node(layer.onnx_node).inputs[0] == taps[f"backbone.block{i}"], | |
| f"deblock {i} does not read backbone block {i}") | |
| deblocks.append(layer) | |
| taps[layer.name] = layer.onnx_output | |
| kinds = [(d.kind, d.kernel, d.stride, d.padding) for d in deblocks] | |
| _expect(kinds == [("conv", 2, 2, 0), ("conv", 1, 1, 0), ("conv_transpose", 2, 2, 0)], | |
| f"deblocks {kinds} (expected conv 2x2 s2, conv 1x1, convT 2x2 s2)") | |
| taps["neck"] = concat.outputs[0] | |
| return tuple(blocks), tuple(deblocks), taps | |
| # ---------------------------------------------------------------------------------------------------- head | |
| def _attention(w: OnnxWeights, prefix: str) -> AttentionParams: | |
| a = f"{prefix}/attn" | |
| mats = [np.asarray(w.matmul_weight(f"{a}/{n}"), np.float32) for n in ("MatMul", "MatMul_1", "MatMul_2")] | |
| biases = [] | |
| for n, mm in (("Add", "MatMul"), ("Add_1", "MatMul_1"), ("Add_2", "MatMul_2")): | |
| add = w.node(f"{a}/{n}") | |
| _expect(add.inputs[1] == w.node(f"{a}/{mm}").outputs[0], f"{a}/{n} does not add the bias of {mm}") | |
| biases.append(_f32(w.param(add.name, 0))) | |
| g = w.gemm(f"{a}/Gemm") | |
| _expect(g.trans_b and not g.trans_a and g.alpha == 1.0 and g.beta == 1.0, f"{a}/Gemm attributes") | |
| shape = np.asarray(w.param(f"{a}/Reshape", 1)).reshape(-1) | |
| heads, hd = int(shape[1]), int(shape[2]) | |
| _expect(heads * hd == mats[0].shape[1], f"{a}: {heads} heads x {hd} != {mats[0].shape[1]}") | |
| div = _scalar(w, f"{a}/Div", 1) | |
| _expect(abs(div - np.sqrt(hd)) < 1e-6, f"{a}: Q pre-scale divisor {div} is not sqrt({hd})") | |
| return AttentionParams(mats[0], biases[0], mats[1], biases[1], mats[2], biases[2], _f32(g.weight).T.copy(), | |
| _f32(g.bias), heads, hd, div) | |
| def _posembed(w: OnnxWeights, which: str) -> Tuple[Layer, Layer]: | |
| p = f"{DEC}/{which}/position_embedding_head" | |
| l0 = _conv_layer(w, f"decoder.{which}.0", f"{p}/position_embedding_head.0/Conv", | |
| relu_node=f"{p}/position_embedding_head.2/Relu", kind="conv1d") | |
| l1 = _conv_layer(w, f"decoder.{which}.1", f"{p}/position_embedding_head.3/Conv", kind="conv1d") | |
| _expect(w.node(l1.onnx_node).inputs[0] == l0.onnx_output and l0.weight.shape == (128, 2, 1) | |
| and l1.weight.shape == (128, 128, 1), f"{which} MLP shape / wiring") | |
| return l0, l1 | |
| def _load_head(w: OnnxWeights, cfg: Optional[TransFusionConfig], neck_out: str) -> Tuple[HeadParams, Dict[str, str]]: | |
| taps: Dict[str, str] = {} | |
| shared = _conv_layer(w, "head.shared", f"{HB}/shared_conv/Conv") | |
| _expect(w.node(shared.onnx_node).inputs[0] == neck_out and (shared.in_channels, shared.out_channels, | |
| shared.kernel) == (384, 128, 3), "shared conv") | |
| cast1 = w.node(f"{HB}/Cast_1") | |
| _expect(cast1.inputs[0] == shared.onnx_output, "heatmap head input is not the shared conv") | |
| hh = _conv_layer(w, "head.heatmap.hidden", f"{HB}/heatmap_head/heatmap_head.0/conv/Conv", | |
| relu_node=f"{HB}/heatmap_head/heatmap_head.0/activate/Relu") | |
| ho = _conv_layer(w, "head.heatmap", f"{HB}/heatmap_head/heatmap_head.1/Conv") | |
| _expect(w.node(hh.onnx_node).inputs[0] == cast1.outputs[0] and w.node(ho.onnx_node).inputs[0] == hh.onnx_output | |
| and ho.out_channels == NUM_CLASSES and hh.kernel == ho.kernel == 3, "heatmap head") | |
| sig = w.node(f"{HB}/Sigmoid") | |
| pool = w.node(f"{HB}/MaxPool") | |
| _expect(sig.inputs[0] == ho.onnx_output and pool.inputs[0] == sig.outputs[0], "sigmoid / MaxPool wiring") | |
| k = list(pool.attrs.get("kernel_shape", [])) | |
| _expect(len(set(k)) == 1 and list(pool.attrs.get("strides", [1, 1])) == [1, 1] | |
| and not any(pool.attrs.get("pads", [0, 0, 0, 0])), f"MaxPool attributes {pool.attrs}") | |
| nms_mul = w.node(f"{HB}/Mul") | |
| eq = w.node(f"{HB}/Equal") | |
| _expect(eq.inputs[0] == sig.outputs[0] and sig.outputs[0] in nms_mul.inputs, "local-max mask wiring") | |
| topk = w.node(f"{HB}/TopK") | |
| K = int(np.asarray(w.param(topk.name, 1)).reshape(-1)[0]) | |
| _expect(int(topk.attrs.get("largest", 1)) == 1 and int(topk.attrs.get("sorted", 1)) == 1, "TopK attributes") | |
| grid_size = int(round(_scalar(w, f"{HB}/Div", 1))) | |
| enc = w.conv(f"{HB}/class_encoding/Conv") | |
| _expect(enc.weight.shape == (128, NUM_CLASSES, 1), f"class encoding {enc.weight.shape}") | |
| onehot = w.node(f"{HB}/OneHot") | |
| _expect(int(np.asarray(w.param(onehot.name, 1)).reshape(-1)[0]) == NUM_CLASSES, "OneHot depth") | |
| bev_pos = _f32(w.array(w.node(f"{HB}/Expand").inputs[0])).reshape(-1, 2) | |
| fw = int(round(np.sqrt(bev_pos.shape[0]))) | |
| _expect(fw * fw == bev_pos.shape[0] == grid_size, f"bev_pos {bev_pos.shape} vs the feature grid {grid_size}") | |
| kk = np.arange(grid_size) | |
| _expect(np.array_equal(bev_pos[:, 0], (kk % fw).astype(np.float32) + np.float32(0.5)) | |
| and np.array_equal(bev_pos[:, 1], (kk // fw).astype(np.float32) + np.float32(0.5)), | |
| "bev_pos is not (x + 0.5, y + 0.5) with k = y * W + x") | |
| if cfg is not None: | |
| _expect(K == cfg.num_proposals, f"ONNX TopK K={K} != ml_package num_proposals {cfg.num_proposals}") | |
| _expect(fw == cfg.feature_x == cfg.feature_y, f"feature grid {fw} != {cfg.feature_x}") | |
| # decoder | |
| dec = DecoderParams( | |
| self_posembed=_posembed(w, "self_posembed"), cross_posembed=_posembed(w, "cross_posembed"), | |
| self_attn=_attention(w, f"{DEC}/self_attn"), cross_attn=_attention(w, f"{DEC}/cross_attn"), | |
| norms=tuple((_f32(w.param(f"{DEC}/norms.{i}/Mul", 1)), _f32(w.param(f"{DEC}/norms.{i}/Add_1", 1)), | |
| _scalar(w, f"{DEC}/norms.{i}/Add", 1)) for i in range(3)), | |
| ffn_w1=_f32(w.matmul_weight(f"{DEC}/ffn/layers/layers.0/layers.0.0/MatMul")), | |
| ffn_b1=_f32(w.param(f"{DEC}/ffn/layers/layers.0/layers.0.0/Add", 0)), | |
| ffn_w2=_f32(w.matmul_weight(f"{DEC}/ffn/layers/layers.1/MatMul")), | |
| ffn_b2=_f32(w.param(f"{DEC}/ffn/layers/layers.1/Add", 0))) | |
| _expect(all(abs(eps - 1e-5) < 1e-9 for _, _, eps in dec.norms), "LayerNorm eps != 1e-5") | |
| _expect(dec.ffn_w1.shape == (128, 256) and dec.ffn_w2.shape == (256, 128), "FFN shapes") | |
| # wiring of the decoder inputs (S:transfusion:177-182): Q = K = V = query + pos in self-attention | |
| q_plus = w.node(f"{DEC}/self_attn/Add") | |
| _expect(set(w.node(f"{DEC}/self_attn/Add_1").inputs) == set(q_plus.inputs) == set(w.node(f"{DEC}/Add").inputs), | |
| "self-attention Q / K / V are not all query + query_pos_embed") | |
| kv = w.node(f"{DEC}/Add_1") | |
| kpe = w.node(f"{DEC}/Transpose_1").outputs[0] | |
| _expect(kpe in kv.inputs, "cross-attention key / value are not lidar_feat + key_pos_embed") | |
| heads = [] | |
| for name, ch in PREDICTION_HEADS: | |
| p = f"{HB}/prediction_heads.0/{name}" | |
| hid = _conv_layer(w, f"heads.{name}.hidden", f"{p}/{name}.0/conv/Conv", | |
| relu_node=f"{p}/{name}.0/activate/Relu", kind="conv1d") | |
| fin = _conv_layer(w, f"heads.{name}", f"{p}/{name}.1/Conv", kind="conv1d") | |
| _expect(hid.weight.shape == (64, 128, 1) and fin.weight.shape == (ch, 64, 1) | |
| and w.node(fin.onnx_node).inputs[0] == hid.onnx_output, f"prediction head {name}") | |
| heads.append((name, hid, fin)) | |
| taps[fin.name] = fin.onnx_output | |
| head = HeadParams(shared=shared, heatmap_hidden=hh, heatmap_out=ho, nms_kernel=int(k[0]), num_proposals=K, | |
| class_encoding_w=_f32(enc.weight[:, :, 0]), class_encoding_b=_f32(enc.bias), bev_pos=bev_pos, | |
| decoder=dec, prediction_heads=tuple(heads)) | |
| taps.update({ | |
| "head.shared": shared.onnx_output, "head.heatmap.hidden": hh.onnx_output, "head.heatmap": ho.onnx_output, | |
| "head.heat_nms": nms_mul.outputs[0], "head.topk_values": topk.outputs[0], "head.topk_indices": topk.outputs[1], | |
| "query.feat": w.node(f"{HB}/Add").outputs[0], "query.pos": w.node(f"{HB}/GatherElements_1").outputs[0], | |
| "query.heat": w.node(f"{HB}/GatherElements_2").outputs[0], "decoder.key_pos_embed": kpe, | |
| "decoder.query_pos_embed": w.node(f"{DEC}/Transpose").outputs[0], | |
| "decoder.self_attn": w.node(f"{DEC}/norms.0/Add_1").outputs[0], | |
| "decoder.cross_attn": w.node(f"{DEC}/norms.1/Add_1").outputs[0], | |
| "decoder.out": w.node(f"{DEC}/norms.2/Add_1").outputs[0], | |
| }) | |
| _check_outputs(w, heads) | |
| return head, taps | |
| def _check_outputs(w: OnnxWeights, heads: Sequence[Tuple[str, Layer, Layer]]) -> None: | |
| """``bbox_pred0 = concat[center + query_pos, height, dim, vel]``, ``dir_cls_pred0 = rot``, | |
| ``cls_score0 = query_heat * sigmoid(heatmap)`` (each head's output passes through 1-input Concats).""" | |
| out = {name: fin.onnx_output for name, _, fin in heads} | |
| def through(t: str) -> str: # follow single-input Concat / Identity wrappers forward | |
| while True: | |
| cons = [n for n in w.consumers(t) if n.op_type in ("Concat", "Identity") and len(n.inputs) == 1] | |
| if not cons: | |
| return t | |
| t = cons[0].outputs[0] | |
| center_add = w.node(f"{HB}/Add_1") | |
| qpos_t = w.node(f"{HB}/Transpose_2") | |
| _expect(list(center_add.inputs) == [out["center"], qpos_t.outputs[0]] | |
| and qpos_t.inputs[0] == w.node(f"{HB}/GatherElements_1").outputs[0], "center is not offset by query_pos") | |
| bbox = w.producer("bbox_pred0") | |
| _expect(bbox.op_type == "Concat" and list(bbox.inputs) == [through(center_add.outputs[0]), through(out["height"]), | |
| through(out["dim"]), through(out["vel"])], | |
| f"bbox_pred0 = {bbox.inputs}") | |
| _expect(through(out["rot"]) == "dir_cls_pred0", "dir_cls_pred0 is not rot") | |
| query_heat = w.node(f"{HB}/GatherElements_2").outputs[0] | |
| mul = w.producer(w.producer("cls_score0").inputs[0]) | |
| _expect(mul.op_type == "Mul" and mul.inputs[0] == query_heat, "cls_score0 is not query_heat * ...") | |
| sig = w.producer(mul.inputs[1]) | |
| _expect(sig.op_type == "Sigmoid" and sig.inputs[0] == through(out["heatmap"]), "cls_score0 sigmoid input") | |
| gather = w.node(f"{HB}/GatherElements_2") | |
| _expect(gather.inputs[0] == w.node(f"{HB}/Reshape_1").outputs[0] | |
| and w.node(f"{HB}/Reshape_1").inputs[0] == w.node(f"{HB}/Mul").outputs[0], | |
| "query_heat is not gathered from the local-max-masked heatmap (all classes)") | |
| # ---------------------------------------------------------------------------------------------------- load | |
| def load_weights(weights_dir: Any, config: Optional[TransFusionConfig] = None) -> TransFusionWeights: | |
| """``<weights_dir>/transfusion.onnx`` (or the ``.onnx`` path itself) -> :class:`TransFusionWeights`, checked | |
| against ``config`` (range / voxel / K / feature grid) when given.""" | |
| p = Path(weights_dir) | |
| path = p if p.suffix == ".onnx" else p / ONNX_FILE | |
| w = OnnxWeights(path) | |
| _expect(tuple(w.input_names) == ("voxels", "num_points", "coors"), f"inputs {w.input_names}") | |
| _expect(tuple(w.output_names) == ("cls_score0", "bbox_pred0", "dir_cls_pred0"), f"outputs {w.output_names}") | |
| pfn, taps = _load_pfn(w, config) | |
| scatter = w.node("/pts_middle_encoder/ScatterElements") | |
| canvas_shape = tuple(int(v) for v in np.asarray(w.array(scatter.inputs[0]).shape)) | |
| row = int(round(_scalar(w, "/pts_middle_encoder/Mul", 1))) | |
| reshape = w.node("/pts_middle_encoder/Reshape") | |
| full = tuple(int(v) for v in np.asarray(w.param(reshape.name, 1)).reshape(-1)) | |
| _expect(canvas_shape == (PILLAR_FEATURE_SIZE, row * row) and full == (1, PILLAR_FEATURE_SIZE, row, row), | |
| f"scatter canvas {canvas_shape} / {full} with row stride {row}") | |
| if config is not None: | |
| _expect((row, row) == (config.grid_x, config.grid_y), f"canvas {row}^2 != grid {config.grid_x}") | |
| _expect(config.feature_x * OUT_SIZE_FACTOR == row, "feature grid") | |
| taps["canvas"] = reshape.outputs[0] | |
| blocks, deblocks, t2 = _load_backbone_neck(w, reshape.outputs[0]) | |
| taps.update(t2) | |
| head, t3 = _load_head(w, config, taps["neck"]) | |
| taps.update(t3) | |
| taps.update({"cls_score0": "cls_score0", "bbox_pred0": "bbox_pred0", "dir_cls_pred0": "dir_cls_pred0"}) | |
| return TransFusionWeights(pfn=pfn, blocks=blocks, deblocks=deblocks, head=head, onnx_taps=taps, | |
| sha256=file_sha256(path), path=str(path), grid=(row, row)) | |