Download code/tt_streampetr/reference/weights.py from changh95/streampetr-p150: direct link, hf CLI and curl.
- Browser
- Download file 16.6 kB
-
https://huggingface.co/changh95/streampetr-p150/resolve/main/code/tt_streampetr/reference/weights.py
- Command line
-
hf download hf://changh95/streampetr-p150/code/tt_streampetr/reference/weights.py
-
curl -L -o weights.py https://huggingface.co/changh95/streampetr-p150/resolve/main/code/tt_streampetr/reference/weights.py
16.6 kB
| # SPDX-License-Identifier: Apache-2.0 | |
| """Weights of the three StreamPETR graphs (AutowareFoundation/camera_streampetr@v1.0), read the way the TT port | |
| reads them. | |
| Every file is parsed as data with the vendored ``ttaw.weights.OnnxWeights`` (C04): nothing is executed, and every | |
| parameter is addressed through the graph node that consumes it (``/img_backbone/stage4/OSA4_2/concat/.../Conv``, | |
| ``/transformer/decoder/layers.3/attentions.1/attn/MatMul_1``, ...), so attributes (strides, pads) come from the | |
| deployed graph itself. The exporter folded every BatchNorm into its convolution (S:4.1: "BN folded into conv weight + | |
| bias"), so there is **no BN fold in the port**: conv weights and biases are used as stored. Linear layers are | |
| ``MatMul(x, W[in, out]) + Add(b)`` pairs (``linear``), the attention output projections ``Gemm`` with ``transB=1`` | |
| (``gemm`` returns ``W[in, out]`` like ``linear``). The head's six cls / reg branch and post-norm instances share their | |
| initializers (the branch weights of the training code are shared by all decoder layers, S:4.3 step 8), which | |
| :meth:`StreamPETRWeights.check_graph` verifies. | |
| :meth:`StreamPETRWeights.check_graph` verifies that the files are the networks this port was written for (I/O names | |
| and shapes, op histograms, the 118 convolutions mapped 1:1 to modules, MaxPool ceil mode, nearest Resize, LayerNorm | |
| eps, the attention scale, TopK k, pc_range / position_range constants) and fails loudly otherwise. | |
| """ | |
| from __future__ import annotations | |
| import collections | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any, Dict, Iterator, List, Optional, Tuple | |
| import numpy as np | |
| from .config import ONNX_FILES, StreamPETRConfig, default_config | |
| __all__ = ["Conv", "StreamPETRWeights", "backbone_modules", "find_weights_dir", "OSA_STAGES"] | |
| # VoVNet-99-eSE (vovnet.py:80-88): stage -> (OSA blocks, stage channels, concat output channels) | |
| OSA_STAGES: Dict[int, Tuple[int, int, int]] = {2: (1, 128, 256), 3: (3, 160, 512), 4: (9, 192, 768), | |
| 5: (3, 224, 1024)} | |
| # op histograms of the deployed graphs (S:4.1-4.3; verify_onnx.py) | |
| _HIST = { | |
| "backbone": {"Conv": 118, "Relu": 99, "Add": 29, "Concat": 16, "GlobalAveragePool": 16, "Clip": 16, "Div": 16, | |
| "Mul": 16, "MaxPool": 3, "Squeeze": 2, "Resize": 1, "Reshape": 1}, | |
| "position_embedding": {"Reshape": 11, "Unsqueeze": 6, "Gather": 6, "Slice": 6, "Div": 5, "Concat": 4, "Tile": 4, | |
| "ScatterND": 4, "Squeeze": 3, "Expand": 3, "Mul": 3, "MatMul": 3, "Clip": 3, "Sub": 2, | |
| "Add": 2, "Split": 1, "Abs": 1, "Max": 1, "Log": 1, "Relu": 1}, | |
| "head": {"Add": 148, "MatMul": 128, "Reshape": 67, "Transpose": 58, "LayerNormalization": 40, "Slice": 37, | |
| "Unsqueeze": 36, "Relu": 36, "Concat": 27, "Mul": 24, "ScatterND": 19, "Gather": 14, "Softmax": 12, | |
| "Gemm": 12, "Sin": 10, "Cos": 10, "Sigmoid": 8, "Div": 6, "Tile": 5, "GatherElements": 5, "Clip": 3, | |
| "Squeeze": 2, "Sub": 2, "TopK": 2, "Flatten": 2, "Log": 1, "Cast": 1}, | |
| } | |
| _IO = { | |
| "backbone": ((("img", (1, 5, 3, 480, 640)),), (("img_feats", (1, 5, 256, 30, 40)),)), | |
| "position_embedding": ((("img_metas_pad", (3,)), ("intrinsics", (1, 5, 4, 4)), ("img2lidar", (1, 5, 4, 4))), | |
| (("pos_embed", (1, 6000, 256)), ("cone", (1, 6000, 8)))), | |
| "head": ((("x", (1, 5, 256, 30, 40)), ("pos_embed", (1, 6000, 256)), ("cone", (1, 6000, 8)), | |
| ("data_ego_pose", (1, 4, 4)), ("data_ego_pose_inv", (1, 4, 4)), ("pre_memory_embedding", (1, 1024, 256)), | |
| ("pre_memory_reference_point", (1, 1024, 3)), ("pre_memory_timestamp", (1, 1024, 1)), | |
| ("pre_memory_egopose", (1, 1024, 4, 4)), ("pre_memory_velo", (1, 1024, 2))), | |
| (("all_cls_scores", (1, 5, 5400)), ("all_bbox_preds", (1, 10, 5400)), | |
| ("post_memory_embedding", (1, 1280, 256)), ("post_memory_reference_point", (1, 1280, 3)), | |
| ("post_memory_timestamp", (1, 1280, 1)), ("post_memory_egopose", (1, 1280, 4, 4)), | |
| ("post_memory_velo", (1, 1280, 2)), ("reference_points", (1, 900, 3)), ("tgt", (1, 900, 256)), | |
| ("temp_memory", (1, 768, 256)), ("temp_pos", (1, 768, 256)), ("query_pos", (1, 900, 256)), | |
| ("query_pos_in", (1, 644, 256)), ("outs_dec", (6, 1, 900, 256)))), | |
| } | |
| def backbone_modules() -> Dict[str, List[str]]: | |
| """Conv node names of the backbone graph by stage, in graph order (118 = 3 + 7 + 21 + 63 + 21 + 3).""" | |
| out: Dict[str, List[str]] = {"stem": [f"/img_backbone/stem/stem_{i}/conv/Conv" for i in (1, 2, 3)]} | |
| for st, (nb, _, _) in OSA_STAGES.items(): | |
| names = [] | |
| for k in range(1, nb + 1): | |
| pre = f"/img_backbone/stage{st}/OSA{st}_{k}" | |
| names += [f"{pre}/layers.{i}/OSA{st}_{k}_{i}/conv/Conv" for i in range(5)] | |
| names += [f"{pre}/concat/OSA{st}_{k}_concat/conv/Conv", f"{pre}/ese/fc/Conv"] | |
| out[f"stage{st}"] = names | |
| out["neck"] = ["/img_neck/lateral_convs.0/conv/Conv", "/img_neck/lateral_convs.1/conv/Conv", | |
| "/img_neck/fpn_convs.0/conv/Conv"] | |
| return out | |
| class Conv: | |
| """One convolution as the deployed graph runs it (float32 weights as stored; BN already folded by the exporter).""" | |
| node: str | |
| weight: np.ndarray # [Cout, Cin, kh, kw] | |
| bias: Optional[np.ndarray] # [Cout] | |
| stride: int | |
| padding: int | |
| class StreamPETRWeights: | |
| """Parameters of the three deployed graphs, addressed by consuming node. ``weights_dir`` holds the HF files.""" | |
| def __init__(self, weights_dir: Any, *, check: bool = True, verify_sha256: bool = False): | |
| from ..ttaw.weights import OnnxWeights | |
| self.dir = Path(weights_dir) | |
| self.onnx: Dict[str, Any] = {} | |
| for key, (name, sha) in ONNX_FILES.items(): | |
| p = self.dir / name | |
| if not p.is_file(): | |
| raise FileNotFoundError(f"{p} (the HF camera_streampetr v1.0 files are needed)") | |
| self.onnx[key] = OnnxWeights(p) | |
| if verify_sha256 and self.onnx[key].sha256 != sha: | |
| raise ValueError(f"{name}: sha256 {self.onnx[key].sha256} != pinned {sha}") | |
| self._convs: Dict[str, Conv] = {} | |
| if check: | |
| self.check_graph() | |
| def backbone(self): | |
| return self.onnx["backbone"] | |
| def pe(self): | |
| return self.onnx["position_embedding"] | |
| def head(self): | |
| return self.onnx["head"] | |
| def sha256(self) -> Dict[str, str]: | |
| return {ONNX_FILES[k][0]: w.sha256 for k, w in self.onnx.items()} | |
| # ---- addressing --------------------------------------------------------------------------------------- | |
| def conv(self, node: str) -> Conv: | |
| """A backbone convolution by node name (symmetric pads, group 1, dilation 1: checked).""" | |
| if node not in self._convs: | |
| p = self.backbone.conv(node) | |
| pads = tuple(p.pads) | |
| if len(set(pads)) != 1 or len(set(p.strides)) != 1 or set(p.dilations) != {1} or p.group != 1 \ | |
| or p.auto_pad not in ("NOTSET", ""): | |
| raise ValueError(f"{node}: unsupported attributes {p}") | |
| self._convs[node] = Conv(node, np.asarray(p.weight, np.float32), | |
| None if p.bias is None else np.asarray(p.bias, np.float32), | |
| int(p.strides[0]), int(pads[0])) | |
| return self._convs[node] | |
| def iter_convs(self) -> Iterator[Tuple[str, Conv]]: | |
| for names in backbone_modules().values(): | |
| for n in names: | |
| yield n, self.conv(n) | |
| def _const_operand(w: Any, node: str) -> np.ndarray: | |
| ps = w.params(node) | |
| if len(ps) != 1: | |
| raise ValueError(f"{node}: expected one constant operand, got slots {sorted(ps)}") | |
| return next(iter(ps.values())) | |
| def linear(self, prefix: str, graph: str = "head") -> Tuple[np.ndarray, np.ndarray]: | |
| """``<prefix>/MatMul`` + ``<prefix>/Add`` -> (W [in, out], b [out]) float32.""" | |
| w = self.onnx[graph] | |
| W = np.asarray(w.matmul_weight(prefix + "/MatMul"), np.float32) | |
| b = np.asarray(self._const_operand(w, prefix + "/Add"), np.float32) | |
| if W.ndim != 2 or b.shape != (W.shape[1],): | |
| raise ValueError(f"{prefix}: weight {W.shape} / bias {b.shape}") | |
| return W, b | |
| def attn_proj(self, prefix: str, which: str) -> Tuple[np.ndarray, np.ndarray]: | |
| """Q / K / V projection of ``<prefix>/attn`` (``MatMul`` / ``MatMul_1`` / ``MatMul_2`` + ``Add*``).""" | |
| suffix = {"q": "", "k": "_1", "v": "_2"}[which] | |
| w = self.head | |
| W = np.asarray(w.matmul_weight(f"{prefix}/attn/MatMul{suffix}"), np.float32) | |
| b = np.asarray(self._const_operand(w, f"{prefix}/attn/Add{suffix}"), np.float32) | |
| return W, b | |
| def gemm(self, node: str) -> Tuple[np.ndarray, np.ndarray]: | |
| """A ``Gemm`` (``transB=1``, alpha = beta = 1) -> (W [in, out], b [out]) float32.""" | |
| g = self.head.gemm(node) | |
| if g.trans_a or not g.trans_b or g.alpha != 1.0 or g.beta != 1.0: | |
| raise ValueError(f"{node}: unexpected Gemm attributes {g}") | |
| return np.ascontiguousarray(np.asarray(g.weight, np.float32).T), np.asarray(g.bias, np.float32) | |
| def layer_norm(self, node: str) -> Tuple[np.ndarray, np.ndarray, float]: | |
| """``LayerNormalization`` -> (gamma, beta, eps) (axis -1 checked).""" | |
| n = self.head.node(node) | |
| if int(n.attrs.get("axis", -1)) != -1: | |
| raise ValueError(f"{node}: axis {n.attrs.get('axis')}") | |
| ps = self.head.params(node) | |
| return np.asarray(ps[1], np.float32), np.asarray(ps[2], np.float32), float(n.attrs.get("epsilon", 1e-5)) | |
| def const(self, name: str, graph: str = "head") -> np.ndarray: | |
| return np.asarray(self.onnx[graph].array(name), np.float32) | |
| def learned_queries(self) -> Dict[str, np.ndarray]: | |
| """The constant-folded tensors of the 644 learned queries (S:4.3 "Folded constants"): | |
| ``reference_points`` [1, 644, 3] (``/Tile_output_0``), ``tgt`` [1, 644, 256] = MLN_mem(0, identity motion) | |
| (``/ego_pose_memory/Add_output_0``), ``query_pos`` [1, 644, 256] (``/Add_output_0``) and the debug output | |
| ``query_pos_in``.""" | |
| return {"reference_points": self.const("/Tile_output_0"), "tgt": self.const("/ego_pose_memory/Add_output_0"), | |
| "query_pos": self.const("/Add_output_0"), "query_pos_in": self.const("query_pos_in")} | |
| def attention_scale(self) -> np.float32: | |
| """The ONNX ``/attn/Mul`` constant applied to Q after the projection (1/sqrt(32) in float32).""" | |
| vals = {float(self._const_operand(self.head, n.name)) for n in self.head.nodes("Mul", "*/attn/Mul")} | |
| if len(vals) != 1: | |
| raise ValueError(f"attention scales differ: {vals}") | |
| return np.float32(vals.pop()) | |
| # ---- graph checks -------------------------------------------------------------------------------------- | |
| def check_graph(self, cfg: Optional[StreamPETRConfig] = None) -> None: | |
| """The files are the deployed StreamPETR this port reproduces (module docstring); raises ``ValueError``.""" | |
| cfg = cfg or default_config() | |
| problems: List[str] = [] | |
| for key, w in self.onnx.items(): | |
| hist = dict(collections.Counter(n.op_type for n in w.nodes())) | |
| if hist != _HIST[key]: | |
| problems.append(f"{key}: op histogram {hist} != {_HIST[key]}") | |
| ins, outs = _IO[key] | |
| if tuple(w.input_names) != tuple(n for n, _ in ins) or tuple(w.output_names) != tuple(n for n, _ in outs): | |
| problems.append(f"{key}: I/O names {w.input_names} -> {w.output_names}") | |
| if set(w.opset) != {"ai.onnx"}: | |
| problems.append(f"{key}: custom op domains {w.opset}") | |
| # backbone: every Conv maps 1:1 to a module; pools, resize, eSE hard-sigmoid | |
| convs = [n for names in backbone_modules().values() for n in names] | |
| if sorted(convs) != sorted(n.name for n in self.backbone.nodes("Conv")): | |
| problems.append("backbone: Conv nodes do not map 1:1 to the VoVNet-99-eSE + CPFPN modules") | |
| for n in self.backbone.nodes("MaxPool"): | |
| a = n.attrs | |
| if (int(a.get("ceil_mode", 0)), tuple(a["kernel_shape"]), tuple(a["strides"]), tuple(a.get("pads", ()))) \ | |
| != (1, (3, 3), (2, 2), (0, 0, 0, 0)): | |
| problems.append(f"backbone: {n.name} {a}") | |
| r = self.backbone.find_node("*", "Resize").attrs | |
| if (r.get("mode"), r.get("coordinate_transformation_mode"), r.get("nearest_mode")) != \ | |
| ("nearest", "asymmetric", "floor"): | |
| problems.append(f"backbone: Resize {r}") | |
| clips = {tuple(float(v) for v in self.backbone.params(n.name).values()) for n in self.backbone.nodes("Clip")} | |
| if clips != {(0.0, 6.0)}: | |
| problems.append(f"backbone: eSE Clip bounds {clips}") | |
| divs = {float(next(iter(self.backbone.params(n.name).values()))) for n in self.backbone.nodes("Div")} | |
| if divs != {6.0}: | |
| problems.append(f"backbone: eSE Div constants {divs}") | |
| # position embedding: the position_range constants | |
| pr = cfg.position_range | |
| if not np.allclose(self.pe.params("/Sub")[1], np.float32(pr[:3]), rtol=0, atol=0) or not np.allclose( | |
| self.pe.params("/Div_3")[1], np.float32(np.subtract(pr[3:], pr[:3])), rtol=0, atol=0): | |
| problems.append("position_embedding: position_range constants differ from the configuration") | |
| # head: LN eps, attention scale, TopK k, pc_range, shared branches | |
| eps = {float(n.attrs.get("epsilon", 1e-5)) for n in self.head.nodes("LayerNormalization")} | |
| if eps != {float(np.float32(cfg.ln_eps))}: | |
| problems.append(f"head: LayerNorm eps {eps}") | |
| if self.attention_scale() != np.float32(1.0 / np.sqrt(cfg.head_dim)): | |
| problems.append(f"head: attention scale {self.attention_scale()}") | |
| ks = sorted(int(self.head.params(n.name)[1][0]) for n in self.head.nodes("TopK")) | |
| if ks != [1, cfg.num_propagated]: | |
| problems.append(f"head: TopK k {ks}") | |
| for n in self.head.nodes("TopK"): | |
| if int(n.attrs.get("largest", 1)) != 1 or int(n.attrs.get("sorted", 1)) != 1: | |
| problems.append(f"head: {n.name} {n.attrs}") | |
| pc = cfg.pc_range | |
| if not np.array_equal(self.head.params("/Sub")[1], np.float32(pc[:3])) or not np.array_equal( | |
| self.head.params("/Div_3")[1], np.float32(np.subtract(pc[3:], pc[:3]))): | |
| problems.append("head: pc_range constants (temporal alignment) differ from the configuration") | |
| if not np.array_equal(self.head.params("/Mul_69")[1], np.float32(np.subtract(pc[3:], pc[:3]))) or \ | |
| not np.array_equal(self.head.params("/Add_8")[1], np.float32(pc[:3])): | |
| problems.append("head: pc_range constants (box decode) differ from the configuration") | |
| for branch in ("/cls_branches.0/cls_branches.0.0", "/reg_branches.0/reg_branches.0.0"): | |
| inits = {self.head.node(f"{branch}{'' if i == 0 else f'_{i}'}/MatMul").inputs[1] | |
| for i in range(cfg.num_layers)} | |
| if len(inits) != 1: | |
| problems.append(f"head: {branch} is not shared by the {cfg.num_layers} layers") | |
| posts = {self.head.node(f"/transformer/decoder/post_norm{'' if i == 0 else f'_{i}'}/LayerNormalization") | |
| .inputs[1:] for i in range(cfg.num_layers)} | |
| if len(posts) != 1: | |
| problems.append("head: the post_norm instances do not share their initializers") | |
| q = self.learned_queries() | |
| if q["reference_points"].shape != (1, cfg.num_query, 3) or q["tgt"].shape != (1, cfg.num_query, 256): | |
| problems.append("head: learned-query constants have unexpected shapes") | |
| if problems: | |
| raise ValueError("the ONNX files are not the StreamPETR v1.0 graphs this port reproduces:\n " | |
| + "\n ".join(problems)) | |
| def find_weights_dir(weights_dir: Any = None) -> Optional[Path]: | |
| """``weights_dir`` if it holds the three ONNX files (``weights_dir`` may also name one of them), else None.""" | |
| if weights_dir is None: | |
| return None | |
| p = Path(weights_dir) | |
| if p.is_file(): | |
| p = p.parent | |
| return p if all((p / name).is_file() for name, _ in ONNX_FILES.values()) else None | |