Download code/tt_meteor/reference/weights.py from changh95/meteor-p150: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/changh95/meteor-p150/resolve/main/code/tt_meteor/reference/weights.py
- Command line
-
hf download hf://changh95/meteor-p150/code/tt_meteor/reference/weights.py
-
curl -L -o weights.py https://huggingface.co/changh95/meteor-p150/resolve/main/code/tt_meteor/reference/weights.py
12.7 kB
| # SPDX-License-Identifier: Apache-2.0 | |
| """Weights of ``meteor_v157c3Z.onnx`` (AutowareFoundation/meteor@v1.0), read the way the TT port reads them. | |
| The ONNX 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, so convolution attributes (strides, pads, dilations, | |
| groups) come from the deployed graph itself, never from re-typed constants. Module names are the ONNX scopes without | |
| the ``/net/`` prefix and the op suffix, e.g. ``seg_head/d1/d1.3/d1.3.0`` for ``/net/seg_head/d1/d1.3/d1.3.0/Conv``. | |
| BatchNorm: the export folded every BatchNorm2d of the network into its convolution (SPEC section 4.2, "Every BN is | |
| folded into the conv in the ONNX"; the stem equals ``meteor_v157.pt`` conv1 + BN to 2e-8, S:113), and the graph has | |
| no BatchNormalization node left (checked by :meth:`MeteorWeights.check_graph`). The port therefore uses the conv | |
| weights as stored and folds nothing itself: BN folding happens exactly where the TT port folds it, i.e. nowhere. | |
| The exact rewrites the port applies (the /255 or ImageNet scale into the stem, merged sibling heads, the paint | |
| gather as a 21->96 1x1 conv, tgate + tfuse merge, the zero ``mot`` channels of traj_stem dropped) are proven on the | |
| CPU in ``reference.port_form``. | |
| Besides convolutions and Gemms the graph carries a few non-conv constants the reference reads by consuming node: | |
| the attention-pool parameters (``ego_attn``, ``agent_attn``: packed in-projections sliced by ``Slice`` nodes, learned | |
| queries expanded by ``Expand``), the e2e LayerNorm, the two learned gates (``risk_gate`` -> ``/net/Mul_19``, | |
| ``dec_gate`` -> ``/net/Tanh``), the constant adaptive-pooling matrices (``MatMul`` operands), the 0.4 m lift ground | |
| points (``onnx::Expand_1643`` [1, 4, 100000]) and the painted seg2d classes (``/net/Gather`` indices). | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Dict, Iterator, List, Optional, Tuple | |
| import numpy as np | |
| from ..ttaw.weights import ConvParams, GemmParams, OnnxWeights, file_sha256 | |
| from . import config as C | |
| __all__ = ["MeteorWeights", "MHAParams", "find_onnx", "conv_node", "gemm_node", "POOL_NODES"] | |
| NET = "/net/" | |
| def conv_node(module: str) -> str: | |
| return f"{NET}{module}/Conv" | |
| def gemm_node(module: str) -> str: | |
| return f"{NET}{module}/Gemm" | |
| # constant adaptive-pool / mean matrices: name -> (MatMul node, constant slot). Windows follow | |
| # adaptive_avg_pool2d: [floor(i n / m), ceil((i + 1) n / m)) (export_onnx.py:53-104; S:356-366) | |
| POOL_NODES: Dict[str, Tuple[str, int]] = { | |
| "ego_stem_w": ("/net/ego_stem/ego_stem.15/MatMul", 1), # [16, 1] global mean over 25x16 | |
| "ego_stem_h": ("/net/ego_stem/ego_stem.15/MatMul_1", 0), # [1, 25] | |
| "tl_w": ("/net/tl_head/tl_head.8/MatMul", 1), # [48, 1] global mean over 27x48 | |
| "tl_h": ("/net/tl_head/tl_head.8/MatMul_1", 0), # [1, 27] | |
| "fused_pool_w": ("/net/MatMul_1", 1), # [500, 16] fused 800x500 -> 25x16 (ego_attn keys) | |
| "fused_pool_h": ("/net/MatMul_2", 0), # [25, 800] | |
| "sem_w": ("/net/sem_ego/sem_ego.9/MatMul", 1), # [16, 4] 25x16 -> 5x4 | |
| "sem_h": ("/net/sem_ego/sem_ego.9/MatMul_1", 0), # [5, 25] | |
| "det_pool_w": ("/net/MatMul_3", 1), # [250, 16] det_feat 400x250 -> 25x16 (agent keys) | |
| "det_pool_h": ("/net/MatMul_4", 0), # [25, 400] | |
| "fused_mean_w": ("/net/MatMul_5", 1), # [500, 1] mean(fused) for dec_head | |
| "fused_mean_h": ("/net/MatMul_6", 0), # [1, 800] | |
| "e2e_mean_w": ("/net/refiner/e2e/MatMul", 1), # [500, 1] the same mean for the e2e refiner | |
| "e2e_mean_h": ("/net/refiner/e2e/MatMul_1", 0), # [1, 800] | |
| } | |
| class MHAParams: | |
| """``nn.MultiheadAttention`` exported by torch 2.1: packed in-projection (q | k | v rows), out-projection.""" | |
| in_weight: np.ndarray # [3E, E] | |
| in_bias: np.ndarray # [3E] | |
| out_weight: np.ndarray # [E, E] (Gemm transB=1: y = x W^T + b) | |
| out_bias: np.ndarray # [E] | |
| num_heads: int | |
| scale_divisor: float # q is divided by sqrt(E / heads) (the graph's Div constant) | |
| def embed_dim(self) -> int: | |
| return int(self.out_weight.shape[0]) | |
| def find_onnx(weights_dir: Optional[os.PathLike] = None) -> Path: | |
| """The ONNX file in ``weights_dir`` (a snapshot of AutowareFoundation/meteor) or, for workspace runs, | |
| ``$METEOR_WEIGHTS_DIR`` or ``assets/meteor/hf_meteor`` of the workspace.""" | |
| cands: List[Path] = [] | |
| if weights_dir is not None: | |
| cands.append(Path(weights_dir)) | |
| if os.environ.get("METEOR_WEIGHTS_DIR"): | |
| cands.append(Path(os.environ["METEOR_WEIGHTS_DIR"])) | |
| here = Path(__file__).resolve() | |
| for parent in here.parents: | |
| if (parent / "assets" / "meteor" / "hf_meteor").is_dir(): | |
| cands.append(parent / "assets" / "meteor" / "hf_meteor") | |
| break | |
| for c in cands: | |
| p = c if c.suffix == ".onnx" else c / C.ONNX_FILE | |
| if p.is_file(): | |
| return p | |
| raise FileNotFoundError(f"{C.ONNX_FILE} not found in {[str(c) for c in cands]}") | |
| class MeteorWeights: | |
| """The deployed graph's parameters, addressed by consuming node (see the module docstring).""" | |
| def __init__(self, path: Optional[os.PathLike] = None, *, verify_sha256: bool = False): | |
| self.path = find_onnx(path) if (path is None or Path(path).is_dir()) else Path(path) | |
| if verify_sha256 and file_sha256(self.path) != C.ONNX_SHA256: | |
| raise ValueError(f"{self.path} is not the pinned {C.ONNX_FILE} (sha256 {C.ONNX_SHA256[:12]}...)") | |
| self.onnx = OnnxWeights(self.path) | |
| # ---- generic access ------------------------------------------------------------------------------------------ | |
| def sha256(self) -> str: | |
| return self.onnx.sha256 | |
| def conv(self, module: str) -> ConvParams: | |
| return self.onnx.conv(conv_node(module)) | |
| def gemm(self, module: str) -> GemmParams: | |
| return self.onnx.gemm(gemm_node(module)) | |
| def conv_modules(self) -> List[str]: | |
| """Every Conv of the graph by module name, in graph order (190).""" | |
| return [n.name[len(NET):-len("/Conv")] for n in self.onnx.nodes("Conv")] | |
| def gemm_modules(self) -> List[str]: | |
| return [n.name[len(NET):-len("/Gemm")] for n in self.onnx.nodes("Gemm")] | |
| def iter_convs(self) -> Iterator[Tuple[str, ConvParams]]: | |
| for m in self.conv_modules(): | |
| yield m, self.conv(m) | |
| def const(self, node: str, slot: int) -> np.ndarray: | |
| """The constant feeding input ``slot`` of ``node`` (a full node name, e.g. ``/net/Mul_19``).""" | |
| return self.onnx.param(node, slot) | |
| # ---- named non-conv parameters --------------------------------------------------------------------------------- | |
| def pool_matrix(self, name: str) -> np.ndarray: | |
| node, slot = POOL_NODES[name] | |
| return self.const(node, slot) | |
| def mha(self, name: str) -> MHAParams: | |
| """``ego_attn`` (E 96, 4 heads, 3 queries) or ``agent_attn`` (E 128, 4 heads, 4 queries).""" | |
| p = f"{NET}{name}/" | |
| w_q = self.const(p + "Slice_1", 0) # the packed [3E, E] in_proj_weight, sliced into q and k|v | |
| w_kv = self.const(p + "Slice_2", 0) | |
| b_q = self.const(p + "Slice_3", 0) | |
| b_kv = self.const(p + "Slice_4", 0) | |
| if w_q is not w_kv and not np.array_equal(w_q, w_kv): | |
| raise ValueError(f"{name}: the q and k|v slices do not read one packed in-projection") | |
| if not np.array_equal(b_q, b_kv): | |
| raise ValueError(f"{name}: the q and k|v bias slices do not read one packed bias") | |
| out = self.gemm(name) | |
| div = float(self.const(p + "Div_1", 1)) | |
| heads = int(self.const(p + "Div", 1)) # head dim = E // heads (the graph's /Div by 4) | |
| e = int(out.weight.shape[0]) | |
| if abs(div - np.float32(np.sqrt(e / heads))) > 1e-6: | |
| raise ValueError(f"{name}: q scale divisor {div} != sqrt({e}/{heads})") | |
| return MHAParams(np.asarray(w_q, np.float32), np.asarray(b_q, np.float32), np.asarray(out.weight, np.float32), | |
| np.asarray(out.bias, np.float32), heads, div) | |
| def queries(self, name: str) -> np.ndarray: | |
| """Learned attention-pool queries: ``ego`` [1, 3, 96] (``/net/Expand_1``), ``agent`` [1, 4, 128] | |
| (``/net/Expand_2``).""" | |
| node = {"ego": "/net/Expand_1", "agent": "/net/Expand_2"}[name] | |
| return self.const(node, 0) | |
| def layer_norm(self) -> Tuple[np.ndarray, np.ndarray, float]: | |
| node = self.onnx.node(f"{NET}refiner/e2e/norm/LayerNormalization") | |
| return (self.const(node.name, 1), self.const(node.name, 2), float(node.attrs.get("epsilon", 1e-5))) | |
| def risk_gate(self) -> float: | |
| """``net.risk_gate`` (2.2981195, S:120): mode logits -= risk_gate * mean_t sigmoid(risk)(path_t).""" | |
| return float(self.const("/net/Mul_19", 0).reshape(())) | |
| def dec_gate(self) -> float: | |
| """``net.dec_gate`` (0.0336921, S:120): ego[:36] += tanh(dec_gate) * (wp_dec - ego[:36]).""" | |
| return float(self.const("/net/Tanh", 0).reshape(())) | |
| def lift_ground_points(self) -> np.ndarray: | |
| """The constant [1, 4, 100000] homogeneous ground points (x, y, 0, 1) of the 400 x 250 lift grid.""" | |
| return self.const("/net/Expand", 0) | |
| def paint_classes(self) -> Tuple[int, ...]: | |
| return tuple(int(i) for i in self.const("/net/Gather", 1).reshape(-1)) | |
| def zero_constants(self) -> Dict[str, np.ndarray]: | |
| """The baked-out history (``mot``) constants that the graph subtracts / multiplies (both all-zero).""" | |
| return {"hist": self.const("/net/Sub_5", 1), "mask": self.const("/net/Mul_14", 1)} | |
| # ---- graph check ---------------------------------------------------------------------------------------------- | |
| def check_graph(self) -> Dict[str, int]: | |
| """Fail loudly unless the file is the graph this port reproduces (ops, attributes, constants).""" | |
| o = self.onnx | |
| problems: List[str] = [] | |
| ops: Dict[str, int] = {} | |
| for n in o.nodes(): | |
| ops[n.op_type] = ops.get(n.op_type, 0) + 1 | |
| expect = {"Conv": 190, "Gemm": 15, "GridSample": 3, "Resize": 16, "Softmax": 7, "ArgMax": 4, | |
| "LayerNormalization": 1, "AveragePool": 3, "MaxPool": 1, "GatherElements": 2, "CumSum": 2} | |
| for op, cnt in expect.items(): | |
| if ops.get(op, 0) != cnt: | |
| problems.append(f"{ops.get(op, 0)} {op} nodes, expected {cnt}") | |
| if ops.get("BatchNormalization", 0): | |
| problems.append("BatchNormalization nodes present (the reference assumes a fully folded export)") | |
| for n in o.nodes("Resize"): | |
| a = n.attrs | |
| if a.get("mode") != "linear" or a.get("coordinate_transformation_mode") != "half_pixel": | |
| problems.append(f"{n.name}: Resize {a.get('mode')}/{a.get('coordinate_transformation_mode')}") | |
| for n in o.nodes("GridSample"): | |
| want = "border" if n.name == "/net/GridSample_2" else "zeros" | |
| a = n.attrs | |
| if (a.get("mode"), a.get("padding_mode"), int(a.get("align_corners", 0))) != ("bilinear", want, 0): | |
| problems.append(f"{n.name}: GridSample {a}") | |
| for n in o.nodes("ArgMax"): | |
| if int(n.attrs.get("select_last_index", 0)) != 0: | |
| problems.append(f"{n.name}: select_last_index") | |
| if self.paint_classes() != C.PAINT_CLASSES: | |
| problems.append(f"paint classes {self.paint_classes()} != {C.PAINT_CLASSES}") | |
| g, b, eps = self.layer_norm() | |
| if g.shape != (139,) or abs(eps - C.E2E_LN_EPS) > 1e-9: | |
| problems.append(f"e2e LayerNorm {g.shape} eps {eps}") | |
| z = self.zero_constants() | |
| if any(np.any(v != 0) for v in z.values()): | |
| problems.append("the baked history constants are not all zero") | |
| pts = self.lift_ground_points() | |
| if pts.shape != (1, 4, C.LIFT_H * C.LIFT_W): | |
| problems.append(f"lift points {pts.shape}") | |
| for name, (node, slot) in POOL_NODES.items(): | |
| if not o.has(o.node(node).inputs[slot]): | |
| problems.append(f"pool matrix {name}: {node} slot {slot} is not constant") | |
| if problems: | |
| raise ValueError(f"{self.path.name} is not the graph this port reproduces: " + "; ".join(problems)) | |
| return ops | |