changh95's picture
tt-model push meteor-p150 (container)
51defdc verified
Raw History Blame Contribute Delete
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]
}
@dataclass(frozen=True)
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)
@property
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 ------------------------------------------------------------------------------------------
@property
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