File size: 13,873 Bytes
aec3f16 | 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 | # SPDX-License-Identifier: Apache-2.0
"""Weights of SceneSeg, read as DATA the way the TT port reads them.
:func:`load_weights` takes the deployed ``SceneSeg_FP32.onnx``: the vendored ``ttaw.weights.OnnxWeights`` addresses
every initializer by its consuming node (Conv / ConvTranspose / Gemm), and BatchNormalization is folded (fp64, one
rounding) exactly where a BN node follows a conv. The deployed graph has no BN node (PyTorch folded them at export,
eps 1e-5, S:sceneseg:249,374), so it folds nothing and returns the ONNX tensors unchanged: the numbers Autoware's
TensorRT engine was built from. The TT port calls the same function.
:func:`load_state_dict_weights` builds the same table from the upstream ``.pth`` (or its safetensors copy re-hosted
in ``weights/``, PLAN D10): it folds every torchvision ``Conv2dNormActivation`` BatchNorm with
``ttaw.weights.fold_bn_conv`` (fp64, one rounding). It is the independent cross-check of the ONNX path (tests:
equal to ~1e-6, S:sceneseg 6.2) and documents the BN fold the export did.
:data:`ARCH` is the expected layer list (100 convs, 5 transposed convs, 3 linears), written from the architecture
(torchvision ``efficientnet_b0().features`` with its stage table, SceneContext, SceneNeck, SceneSegHead;
S:sceneseg 4.2) rather than read from the graph: the loaders check every layer against it (kernel, stride, padding,
groups, channels, activation) and that nothing else is in the graph. Keys are the torch module paths of the conv /
linear (the ``.pth`` prefixes), e.g. ``Backbone.encoder.2.1.block.1.0`` (the depthwise conv of stage 2, block 1).
Importing this module needs neither onnx nor torch.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterator, List, Optional, Tuple, Union
import numpy as np
from . import config
__all__ = ["LayerSpec", "LayerWeights", "ARCH", "ARCH_BY_KEY", "WeightsError", "load_weights",
"load_state_dict_weights", "mbconv_specs", "param_count", "iter_specs"]
ACTIVATIONS = ("silu", "gelu", "sigmoid", "none")
class WeightsError(ValueError):
"""The file does not hold the network this port reproduces."""
@dataclass(frozen=True)
class LayerSpec:
"""One weighted layer of the deployed graph.
``kind``: ``"conv"`` (weight ``[cout, cin / groups, k, k]``, padding ``k // 2``), ``"conv_transpose"`` (weight
``[cin, cout, k, k]``, stride ``k``, no padding) or ``"linear"`` (ONNX Gemm ``transB=1``: weight ``[cout, cin]``).
``act``: the activation that follows in the graph (``silu`` = x * sigmoid(x), ``gelu`` = the exact erf form,
``sigmoid``, ``none``). ``bn``: the ``.pth`` prefix of the BatchNorm folded into the conv (torchvision
``Conv2dNormActivation``), None if the layer has a bias of its own."""
key: str
node: str
kind: str
cin: int
cout: int
k: int = 1
stride: int = 1
groups: int = 1
act: str = "none"
bn: Optional[str] = None
@property
def pad(self) -> int:
return self.k // 2 if self.kind == "conv" else 0
@property
def weight_shape(self) -> Tuple[int, ...]:
if self.kind == "conv":
return (self.cout, self.cin // self.groups, self.k, self.k)
if self.kind == "conv_transpose":
return (self.cin, self.cout, self.k, self.k)
return (self.cout, self.cin)
@property
def depthwise(self) -> bool:
return self.kind == "conv" and self.groups > 1 and self.groups == self.cin == self.cout
@dataclass(frozen=True)
class LayerWeights:
"""Float32 weight and bias of one layer, with its spec and source (ONNX node or ``.pth`` prefix)."""
spec: LayerSpec
weight: np.ndarray
bias: np.ndarray
source: str
folded_bn: Optional[str] = None
# ----------------------------------------------------------------------------------------------- architecture
def _conv(key: str, node: str, cin: int, cout: int, k: int = 1, stride: int = 1, groups: int = 1, act: str = "none",
bn: Optional[str] = None) -> LayerSpec:
return LayerSpec(key, node, "conv", cin, cout, k, stride, groups, act, bn)
def mbconv_specs(stage: int, block: int, expand: int, k: int, stride: int, cin: int, cout: int) -> List[LayerSpec]:
"""torchvision ``MBConv`` (``efficientnet.py``): [expand 1x1 + BN + SiLU] (absent when expand == 1), depthwise kxk
+ BN + SiLU, SqueezeExcitation (fc1 1x1 + SiLU, fc2 1x1 + Sigmoid, squeeze = max(1, cin // 4) of the BLOCK input),
project 1x1 + BN (no activation). Residual (StochasticDepth = identity at eval) when stride 1 and cin == cout."""
cexp = cin * expand
key = f"Backbone.encoder.{stage}.{block}.block"
node = f"/Backbone/encoder.{stage}/encoder.{stage}.{block}/block"
out: List[LayerSpec] = []
i = 0
if expand != 1:
out.append(_conv(f"{key}.0.0", f"{node}/block.0/block.0.0/Conv", cin, cexp, 1, act="silu", bn=f"{key}.0.1"))
i = 1
out.append(_conv(f"{key}.{i}.0", f"{node}/block.{i}/block.{i}.0/Conv", cexp, cexp, k, stride, cexp, "silu",
f"{key}.{i}.1"))
csq = max(1, cin // 4)
out.append(_conv(f"{key}.{i + 1}.fc1", f"{node}/block.{i + 1}/fc1/Conv", cexp, csq, 1, act="silu"))
out.append(_conv(f"{key}.{i + 1}.fc2", f"{node}/block.{i + 1}/fc2/Conv", csq, cexp, 1, act="sigmoid"))
out.append(_conv(f"{key}.{i + 2}.0", f"{node}/block.{i + 2}/block.{i + 2}.0/Conv", cexp, cout, 1,
bn=f"{key}.{i + 2}.1"))
return out
def _build_arch() -> List[LayerSpec]:
a: List[LayerSpec] = [_conv("Backbone.encoder.0.0", "/Backbone/encoder.0/encoder.0.0/Conv", 3,
config.STEM_CHANNELS, 3, 2, act="silu", bn="Backbone.encoder.0.1")]
for s, (e, k, stride, cin, cout, n) in enumerate(config.B0_SETTINGS, start=1):
for b in range(n):
a += mbconv_specs(s, b, e, k, stride if b == 0 else 1, cin if b == 0 else cout, cout)
a.append(_conv("Backbone.encoder.8.0", "/Backbone/encoder.8/encoder.8.0/Conv", config.B0_SETTINGS[-1][4],
config.HEAD_CHANNELS, 1, act="silu", bn="Backbone.encoder.8.1"))
mlp = config.CONTEXT_MLP
for i in range(3):
a.append(LayerSpec(f"SceneContext.context_layer_{i}", f"/SceneContext/context_layer_{i}/Gemm", "linear",
mlp[i], mlp[i + 1], act="gelu" if i < 2 else "sigmoid"))
ch = config.CONTEXT_CONVS
for i in range(4):
a.append(_conv(f"SceneContext.context_layer_{i + 3}", f"/SceneContext/context_layer_{i + 3}/Conv", ch[i],
ch[i + 1], 3, act="gelu"))
for mod, blocks in (("SceneNeck", config.NECK_BLOCKS), ("SceneSegHead", config.HEAD_BLOCKS)):
for up, cup, skip, cskip, decodes in blocks:
a.append(LayerSpec(f"{mod}.{up}", f"/{mod}/{up}/ConvTranspose", "conv_transpose", cup, cup, 2, 2))
if skip is not None:
a.append(_conv(f"{mod}.{skip}", f"/{mod}/{skip}/Conv", cskip, cup, 1))
for name, ci, co in decodes:
a.append(_conv(f"{mod}.{name}", f"/{mod}/{name}/Conv", ci, co, 3, act="gelu"))
name, ci, co = config.FINAL_CONV
a.append(_conv(f"SceneSegHead.{name}", f"/SceneSegHead/{name}/Conv", ci, co, 3))
return a
ARCH: Tuple[LayerSpec, ...] = tuple(_build_arch())
ARCH_BY_KEY: Dict[str, LayerSpec] = {s.key: s for s in ARCH}
assert len(ARCH_BY_KEY) == len(ARCH) == 108
assert sum(s.kind == "conv" for s in ARCH) == 100 and sum(s.kind == "conv_transpose" for s in ARCH) == 5
assert sum(s.depthwise for s in ARCH) == 16 and sum(s.bn is not None for s in ARCH) == 1 + 16 + 15 + 16 + 1
def iter_specs(prefix: str = "") -> Iterator[LayerSpec]:
"""The :data:`ARCH` entries under a key prefix, in graph order."""
return (s for s in ARCH if s.key.startswith(prefix))
def param_count(weights: Dict[str, LayerWeights]) -> int:
return int(sum(lw.weight.size + lw.bias.size for lw in weights.values()))
# ------------------------------------------------------------------------------------------------- ONNX path
def _activation(w, node) -> str:
"""The activation pattern that consumes ``node``'s output in the exported graph: SiLU = Sigmoid + Mul, exact
GELU = Div (by sqrt 2) + Mul, Sigmoid alone, else none (Add / Conv / graph output)."""
ops = sorted(c.op_type for c in w.consumers(node.outputs[0]))
if ops == ["Mul", "Sigmoid"]:
return "silu"
if ops == ["Div", "Mul"]:
return "gelu"
if ops == ["Sigmoid"]:
return "sigmoid"
return "none"
def load_weights(onnx_path: Union[str, Path], *, check_sha256: bool = True) -> Dict[str, LayerWeights]:
"""Every weighted layer of ``SceneSeg_FP32.onnx`` as float32 :class:`LayerWeights`, keyed like :data:`ARCH`
and checked against it. Raises :class:`WeightsError` if the file is not the expected network."""
from ..ttaw.weights import OnnxWeights
w = OnnxWeights(onnx_path)
if check_sha256 and w.sha256 != config.ONNX_SHA256:
raise WeightsError(f"{onnx_path}: sha256 {w.sha256} is not {config.ONNX_FILE} ({config.ONNX_SHA256}); pass "
"check_sha256=False for another export of the same graph")
weighted = {n.name: n for n in w.nodes() if n.op_type in ("Conv", "ConvTranspose", "Gemm")}
expected = {s.node for s in ARCH}
missing = sorted(expected - set(weighted))
extra = sorted(set(weighted) - expected)
if missing or extra:
raise WeightsError(f"layer layout differs from ARCH: missing {missing[:4]}, unexpected {extra[:4]}")
out: Dict[str, LayerWeights] = {}
for spec in ARCH:
node = weighted[spec.node]
folded = None
if spec.kind == "linear":
g = w.gemm(node.name)
if g.trans_a or not g.trans_b or g.alpha != 1.0 or g.beta != 1.0:
raise WeightsError(f"{node.name}: Gemm attributes {g}")
weight, bias = g.weight, g.bias
else:
p = w.conv(node.name)
if spec.kind == "conv":
expect = dict(kernel_shape=(spec.k, spec.k), strides=(spec.stride, spec.stride), pads=(spec.pad,) * 4,
dilations=(1, 1), group=spec.groups)
else:
expect = dict(kernel_shape=(spec.k, spec.k), strides=(spec.stride, spec.stride), pads=(0,) * 4,
dilations=(1, 1), group=1)
got = dict(kernel_shape=tuple(p.kernel_shape), strides=tuple(p.strides), pads=tuple(p.pads or (0,) * 4),
dilations=tuple(p.dilations or (1, 1)), group=p.group)
bad_pad = p.auto_pad not in ("", "NOTSET", None) or tuple(p.output_padding or ()) not in ((), (0, 0))
if got != expect or bad_pad:
raise WeightsError(f"{node.name}: attributes {got} (auto_pad {p.auto_pad}), expected {expect}")
weight, bias = p.weight, p.bias
bn = [c for c in w.consumers(node.outputs[0]) if c.op_type == "BatchNormalization"]
if bn: # not in the deployed export; folded like the TT port would
weight, bias = w.fold_conv_bn(node.name, bn[0].name)
folded = bn[0].name
node = bn[0]
if tuple(weight.shape) != spec.weight_shape:
raise WeightsError(f"{spec.node}: weight {tuple(weight.shape)}, expected {spec.weight_shape}")
if bias is None:
bias = np.zeros(spec.cout, np.float32)
if tuple(np.shape(bias)) != (spec.cout,):
raise WeightsError(f"{spec.node}: bias {np.shape(bias)}, expected ({spec.cout},)")
act = _activation(w, node)
if act != spec.act:
raise WeightsError(f"{spec.node}: activation {act}, ARCH says {spec.act}")
out[spec.key] = LayerWeights(spec, np.ascontiguousarray(weight, np.float32),
np.ascontiguousarray(bias, np.float32), spec.node, folded)
return out
# ------------------------------------------------------------------------------------- .pth / safetensors path
def load_state_dict_weights(path: Union[str, Path], *, eps: float = config.BN_EPS) -> Dict[str, LayerWeights]:
"""The same table from the upstream state_dict (``.pth`` via ``torch.load(weights_only=True)`` or the re-hosted
``.safetensors``): BatchNorms folded in fp64 with one rounding (``ttaw.weights.fold_bn_conv``). Raises
:class:`WeightsError` on a missing or unexpected tensor (``num_batches_tracked`` is ignored)."""
from ..ttaw.weights import fold_bn_conv, load_state_dict
sd = load_state_dict(path)
used = set()
out: Dict[str, LayerWeights] = {}
for spec in ARCH:
try:
weight = np.asarray(sd[f"{spec.key}.weight"])
used.add(f"{spec.key}.weight")
if spec.bn is None:
bias = np.asarray(sd[f"{spec.key}.bias"])
used.add(f"{spec.key}.bias")
folded = None
else:
bn = sd.bn(spec.bn, eps)
used.update(f"{spec.bn}.{n}" for n in ("weight", "bias", "running_mean", "running_var"))
weight, bias = fold_bn_conv(weight, None, bn)
folded = spec.bn
except KeyError as e:
raise WeightsError(f"{path}: {e}") from None
if tuple(weight.shape) != spec.weight_shape:
raise WeightsError(f"{spec.key}: weight {tuple(weight.shape)}, expected {spec.weight_shape}")
out[spec.key] = LayerWeights(spec, np.ascontiguousarray(weight, np.float32),
np.ascontiguousarray(bias, np.float32), spec.key, folded)
extra = sorted(k for k in sd if k not in used and not k.endswith("num_batches_tracked"))
if extra:
raise WeightsError(f"{path}: tensors not in ARCH: {extra[:4]}")
return out
|