# SPDX-License-Identifier: Apache-2.0 """Python API: StreamPETR (Autoware camera_streampetr) on one Tenstorrent Blackhole p150. from tt_streampetr import StreamPETR, load_sample with StreamPETR.from_pretrained(device_id=0) as model: # weights -> HF cache, device open, traces captured out = model(**load_sample("code/tt_streampetr/samples/synthetic_5cam.json")) # 5 cameras + calibration # a sequence: one dict per frame with the same stream id (the temporal memory carries over), e.g. # model(images=..., calibration=..., stream={"id": "rig", "timestamp_s": t, "T_world_from_ego": T}) print(out.to_dict()) # the same JSON as POST /predict The contract shared by every bundle of the Autoware collection is the vendored ``ttaw.api_base.ModelBase`` (BUNDLE_CONVENTIONS.md section 8): ``from_pretrained`` resolves the pinned weights before it claims the chip, opens it (ETH dispatch, 12x10), builds the graph and captures every trace variant in ``warmup_variants``, so the first call is as fast as the later ones; calls are serialised by a lock (one chip, batch 1) and fill ``timing_ms``; ``close()`` is idempotent, also runs at interpreter exit, and closes the chip only if the model opened it. The HTTP server (``tt_streampetr.server.app``) calls this class, so ``/predict`` and ``model(...)`` agree bit for bit. Inputs per call (``_prepare``; host code in ``tt_streampetr.host``, exact Autoware semantics): - ``images``: the five cameras ``CAM_FRONT``, ``CAM_FRONT_LEFT``, ``CAM_BACK_LEFT``, ``CAM_FRONT_RIGHT``, ``CAM_BACK_RIGHT`` (any order, any size; resized / cropped / normalised like the node); - ``calibration``: per camera ``intrinsics`` (``CameraInfo.P`` or K of the image as sent; optional full 3x4 ``P``) and ``T_ref_from_camera`` (camera optical frame -> base_link), or ``{"preset": name}``; - ``stream``: ``{"id", "reset", "timestamp_s", "T_world_from_ego"}`` drives the temporal object memory: a new id or ``reset`` starts like a fresh node (zero memory), ``timestamp_s`` (the CAM_FRONT stamp) gives the memory ages and ``T_world_from_ego`` (map <- base_link) the ego-motion compensation (a stream frame without it is skipped, as the node skips a cycle whose TF lookup fails: ``meta.skipped = "no_ego_pose"``). Without ``stream``: one frame from zero memory. Per-camera ``timestamp_s`` feed the node's 0.15 s camera-sync check (a failure resets the stream and returns no objects, ``meta.skipped``). Device graph (``tt_streampetr.tt``, PORT_LOG.md 1.1): ONE trace ``frame`` = VoVNet-99-eSE + CPFPN, the head (tokens, temporal alignment, 6 decoder layers, shared branches), the top-256 selection and the memory update, with the memory (embedding bf16, geometry fp32 in the previous ego frame) kept in persistent device states. The host runs the node's pre-processing (C14), the position embedding and the spatial-alignment coefficients once per calibration, the stream clock / ego poses / ages, and the post-processing (all-layer decode, IoU-BEV NMS). Load-time knobs: :data:`KNOBS` (``STREAMPETR_*``). Importing this module has no side effects (torch / ttnn are imported in the hooks). """ from __future__ import annotations from collections import OrderedDict from typing import Any, Dict, Mapping, Optional import numpy as np from . import io as tio from .device import DEVICE_DEFAULTS from .reference.config import CAMERA_ORDER, CLASS_NAMES, NORMALIZATION_PRESETS from .ttaw.api_base import ModelBase from .ttaw.knobs import Knob, Knobs from .ttaw.outputs import Detections3D __all__ = ["StreamPETR", "Output", "KNOBS"] # The result class of this model (ttaw.outputs: Detections3D, Detections2D, Segmentation3D, Mask2D, Trajectory). Output = Detections3D # Load-time switches (read once at build; env STREAMPETR_). Each optimization adds its knob here (A/B switch). KNOBS = Knobs("STREAMPETR", [ Knob("NORMALIZATION", "autoware_main", "input normalisation preset (PLAN D7; SPEC 3.4): autoware_main = universe " "main RGB planes + RGB stats; autoware_0.52 = an rgb8 camera on 0.52.1; " "awml_training = BGR planes + BGR stats", choices=NORMALIZATION_PRESETS), Knob("USE_TEMPORAL", True, "carry the object memory between frames (the node's use_temporal; False wipes it)"), Knob("MAX_STREAMS", 1, "temporal streams kept (PLAN D16; another stream id evicts the least recent one)"), Knob("AUTOWARE_COMPAT", True, "reproduce the node's age quirks (default, PORT_LOG section 6 Q1): a sync-failure " "wipe keeps the stale ages, the 600 s clock-origin reset shifts them (SPEC 6 / risk " "11); False zeroes / rebases the ages instead (the SPEC's clean variant)"), Knob("PREPROCESS_FMA", False, "model nvcc's fmaf contraction in the resize kernel (ttaw.image_triangle; <= 3e-5 " "difference)", experiment=True), Knob("PREPROCESS_WORKERS", 4, "host threads of the five-camera resize / normalise"), Knob("HOST_PRE", "numba", "host pre-processing (OPT_PLAN item 1): numba = host.fast.preprocess_rows_bf16 (the C14 " "triangle resize, crop, normalisation and the bf16 cast of the upload rows fused in one " "pass per camera, written into a persistent ttnn host buffer; bit-identical upload); " "numpy = ttaw.image_triangle float32 NCHW + img_rows + bf16 conversion (the release " "path; also the automatic fallback without numba)", choices=("numba", "numpy")), Knob("HOST_POST", "numba", "host post-processing (OPT_PLAN item 4): numba = the compiled IoU-BEV NMS " "(host.fast.iou_bev_nms, the same keep decisions); numpy = ttaw.nms.iou_bev_nms (also " "the fallback without numba)", choices=("numba", "numpy")), Knob("STEM_IN", "kin", "device input layout (OPT_PLAN item 2): kin = the image as 3,840-byte bf16 rows " "[5*480, 640*3] and per camera a K-IN generic_op (tt/kin.py) writing stem conv 0's L1 " "shards (conv on the L1 path); rows = [5*480*640, 3] 6-byte pixel pages and the stem's " "DRAM-sliced conv (the release path)", choices=("kin", "rows")), Knob("CONV_L1", "s3r45n", "3x3 conv placement (OPT_PLAN item 3, tt/placement.py): the OSA 3x3 convs of the listed " "stages (n = the CPFPN 3x3) on ttnn's L1 path (the op moves its DRAM input into L1 shards; " "no TILE reshape pair), DRAM slicing as the first-call fallback. s3r45n (default) = stages " "4-5 + neck + stage 3 except each block's first conv (whose 256 / 512-channel input fits " "L1 only with a changed K split: s345* fails the frozen sample_stream.centre_err gate, " "OPT_REPORT R2.1); s3r45n / s45n are bit-identical to none = every 3x3 conv DRAM-sliced " "(the release path)", choices=("none", "s5", "s45", "s45n", "s3r45n", "s345", "s345n", "s2345n")), Knob("CONV_L1_ABH", "256,128,64,32", "with CONV_L1: activation block heights (comma list, multiples of 32) retried " "in order when a conv's L1 path does not fit with ttnn's own blocks"), Knob("ESE", "kernel", "eSE gate application (OPT_PLAN items 5 / 5b): kernel = one ESG generic_op per OSA block " "(tt/ese.py: the gate tiles built in L1 per camera run, the stock FPU multiply and the " "identity add fused; bit-identical); bcast = one row-broadcast multiply of the " "[1, N, HW, C] view by the per-camera gate rows in the stages whose H*W is a multiple of 32 " "(2, 3; exact: the same products); matmul = the gate broadcast to every row by the " "transposed camera-indicator matmul, then a multiply (the release path)", choices=("matmul", "bcast", "kernel")), Knob("ESE_MEAN", "kernel", "eSE per-camera sums (OPT_PLAN item 5b): kernel = ESM partial sums on the whole grid " "(tt/ese.py) + a fixed-order fp32 reduce (ESR; the fp32 sums are regrouped, " "closer to the exact sums than the matmul's; gated, the 8-frame stream outputs stayed " "bit-identical); matmul = the camera-indicator matmul (the release path)", choices=("matmul", "kernel")), Knob("OSA_CAT", "kmm", "OSA concatenation [x, y1..y5] (OPT_PLAN item 6b, tt/kcat.py): kernel = one KCAT " "data-movement generic_op (output row tiles over the whole grid; a copy: bit-identical); " "kmm = (OPT round 9, tt/kmm.py) stages 2-3 read the parts straight from the concat " "1x1 matmul's in0 reader (the stock 1D matmul program ported to a generic_op, same " "compute: bit-identical; no concat tensor), KCAT in stages 4-5; " "concat = ttnn.concat (the release path)", choices=("concat", "kernel", "kmm")), Knob("CONV_CFG", "adb,wdb", "data-movement Conv2dConfig flags of every backbone 3x3 conv (OPT round 4, " "tt/backbone.conv_flags): comma list of adb (activation double buffer, always " "applied), wdb (weights double buffer), split (forced split reader), reuse " "(activation reuse; NOT exact: it changes the stride-1 DRAM-sliced convs' outputs); " "wdb / split / reuse are tried on the first call and dropped where a conv rejects " "them (never by shrinking activation blocks). adb,wdb is bit-identical; none = " "ttnn's defaults (the release path)"), Knob("CONV_CHAIN", "kcat45", "L1 conv chaining in the OSA blocks (OPT_PLAN item 19, OPT round 7, " "tt/backbone.OSABlock._convs; kcat45 default, bit-identical): " "chain = a conv on ttnn's L1 path whose successor is on the L1 path too keeps its " "sharded output as the successor's input (no InterleavedToSharded; the concat part is " "its DRAM copy); kcat = chain, and every L1-path conv output stays sharded as its " "own concat part (the KCAT-SH reader reads it from L1: no ShardedToInterleaved " "either; needs OSA_CAT=kernel, else DRAM copies); none = every L1 conv returns DRAM " "interleaved (the round-6 path); kcat45 = kcat in stages 4-5, chain in stage 3 (whose " "five outputs do not fit L1 together with the conv buffers)", choices=("none", "chain", "kcat45", "kcat")), Knob("RESHAPE", "kernel", "input re-tiling of the DRAM-sliced 3x3 convs (OPT_PLAN item 20, OPT round 8, " "tt/krt.py): kernel = the flat [1, 1, N*H*W, C] map unflattened to [N, H, W, C] by one " "KRT data-movement generic_op (16-row half-tile copies, whole grid) so ttnn.conv2d's own " "ReshapeView is a no-op (stage-3 block-first convs, W = 80; a copy: bit-identical); " "stock = ttnn.conv2d's ReshapeView (the round-7 path)", choices=("stock", "kernel")), Knob("KMM_BUF", "auto", "KMM circular-buffer depth (OPT round 9, R9.2, tt/kmm.buffers; OSA_CAT=kmm only): auto = " "deeper in0 / in1 buffers (stage 2 12 / 12 blocks, stage 3 4 / 4; data movement only: " "bit-identical); 2 = the stock factory's double buffers (R9.1)", choices=("2", "auto")), Knob("OSA_MM", "rowblock", "OSA concat 1x1 matmul config (OPT_PLAN item 6a, tt/layers.row_block_config): rowblock = " "ttnn's 1D config with the output in row blocks (stage 2: 25 x 8 -> 5 x 8 fp32 L1 partials; " "the same K order: bit-identical); auto = ttnn's own config (the release path)", choices=("auto", "rowblock")), Knob("KV_EPI", True, "split-key K / V operands (OPT_PLAN item 7, tt/kvx.py): True = single-width K / V " "projections and one KVX generic_op per K / V pair writing the per-layer [hi | lo] / [v | v] " "head tensors (replaces the 5-program key split over the doubled width, the head split and " "the per-layer slices; bit-identical); False = the doubled projections + stock programs " "(the release path)"), Knob("SELF_KV", "buffer", "self-attention keys (OPT_PLAN item 9, with KV_EPI): buffer = KVX writes the memory and " "query K / V rows straight into per-layer [memory; queries] buffers (bit-identical); " "concat = untilize, ROW_MAJOR concat and tilize per layer (the release path)", choices=("concat", "buffer")), Knob("S26_KERNEL", True, "exact-product slicing of the temporal chain's sliced maps (OPT_PLAN item 8, tt/ksl.py): " "True = the x slices and their pair-block concatenation as one KSL generic_op per map " "input (the three maps on the sines share one; bit-identical); False = 5 S - 1 stock " "elementwise programs + ttnn.concat per map (the release path)"), Knob("SEL", "mk", "top-256 selection + memory update (OPT rounds 5 / 10): rank = (tt/sel.py) exact ranks from fp32 " "comparisons on the whole grid, a one-hot matrix and one-hot matmul gathers (the fp32 rows " "as three bf16 terms; the same set and order as the stable sort: bit-identical); sort = " "ttnn.sort on one core + ttnn.embedding / ttnn.gather (the release path); mk = MK-SEL " "(OPT round 10, tt/mksel.py): the selection + the whole memory update (gathers, ego identity, " "ring shift, state writes) as one generic_op on 72 cores (bit-identical; the taps path keeps rank)", choices=("sort", "rank", "mk")), Knob("PRECISION", "HiFi4+fp32+l1acc:w=fp32", "precision of every device conv / linear (ttaw.precision spec; " "PORT_LOG S10: fp32 weights and biases, HiFi4, fp32 accumulation)"), Knob("ATTN_EXP", "exact", "SDPA softmax exponential (OPT round 5): approx = the SFPU's approximate exp " "(exp_approx_mode + math_approx_mode) in every SDPA; exp = the approximate exp only; " "cross / self = the approximate exp in those SDPAs only. The exact exp makes the SDPAs " "SFPU-bound (cross 643 -> 207 us approximate) but the numerics move: gated (approx " "fails sample_stream); exact = the release path (PORT_LOG r3)", choices=("exact", "approx", "exp", "cross", "self")), Knob("XATTN", "x8", "SDPA implementation (OPT round 6, item 11b, tt/spx.py): sdpa = ttnn's SDPA; spx = the SPX " "generic_op (the stock SDPA program factory ported, stock reader / writer, a copy of the " "compute kernel) with the stock exponential (bit-identical); x8 = SPX whose softmax " "exponential runs the stock exp_fp32_accurate(x * scale) operations as one hand-scheduled " "TTI pass (26 SFPU instructions per row instead of a multiply pass + ~37; bit-identical). " "Needs ATTN_EXP=exact and ATTN_FP32_ACC (else sdpa)", choices=("sdpa", "spx", "x8")), Knob("ATTN_FP32_ACC", True, "SDPA with fp32 accumulation (probe P7: half the max error)"), Knob("HEAD_ACT", "float32", "dtype of the head's token / query / residual activations (PORT_LOG S25; SDPA " "operands stay bf16)", choices=("bfloat16", "float32")), Knob("EXACT_MAP", "gather", "fp32 maps of the temporal alignment (PORT_LOG S18): column gather (exact bf16 " "embedding of split terms) + fp32 elementwise, or the bf16-split matmul", choices=("gather", "matmul")), Knob("KEY_SPLIT", True, "SDPA keys as hi + lo bf16 terms in a doubled head dim (PORT_LOG S25: the bf16 rounding " "of the keys dominates the head's error otherwise)"), Knob("TEMPORAL_EXACT", True, "the temporal positional chain (query embedding MLP, MLN, time embedding, their " "LayerNorms) as exact-product sliced maps (PORT_LOG S26: its matrix-engine error " "dominates the self-attention error otherwise)"), ]) class StreamPETR(ModelBase): """StreamPETR (Autoware camera_streampetr) on one Blackhole p150. Create it with :meth:`from_pretrained`.""" MODEL_NAME = "streampetr-p150" ENV_PREFIX = "STREAMPETR" # prefix of the environment knobs (SERVING.md section 3.4) DEFAULT_REPO = "AutowareFoundation/camera_streampetr" DEFAULT_TAG = "v1.0" # the Autoware ansible artifacts pin # the commit DEFAULT_TAG points to (pinned: tags can move) DEFAULT_REVISION = "90f37d64186686ebc458509d69d3d4ca16d185c7" ALLOW_PATTERNS = ["simplify_extract_img_feat.onnx", "simplify_position_embedding.onnx", "simplify_pts_head_memory.onnx", "ml_package_camera_streampetr.param.yaml"] VARIANTS = ["default"] # load-time: selects weights files and trace shapes DEFAULT_VARIANT = "default" INPUT_KIND = "multicam" # lidar | camera | multicam | lidar+multicam | planner CAMERA_ORDER = CAMERA_ORDER # the network's camera slots (HF ml_package yaml:10) POINT_FIELDS = tio.DEFAULT_POINT_FIELDS # unused (camera model); kept for the shared /info schema # class names in network output order (HF ml_package yaml:4); they are also the Autoware labels the node publishes LABELS = list(CLASS_NAMES) # Per-request knobs: name -> (type, min, max, default). Host-side post-processing only (S:5, S:9.1). RUNTIME_PARAMS = { "score_threshold": (float, 0.0, 1.0, None), # None: the per-class node thresholds .36/.39/.38/.41/.43 "iou_nms_threshold": (float, 0.0, 1.0, 0.5), # IouBevNms iou_threshold (0 = off) "iou_nms_search_distance": (float, 0.0, 1000.0, 10.0), # IouBevNms search_distance_2d [m] "circle_nms_distance": (float, 0.0, 100.0, 0.0), # circle NMS radius [m] (0 = off, the node's default) "decode_layers": (int, 1, 6, 6), # last decoder layers decoded (6 = all, the node) "awml_decode": (bool, None, None, False), # AWML's evaluation decode (last layer, top-300) "max_detections": (int, 1, 5400, 500), } EXTRA_INPUTS = () DEVICE_DEFAULTS = DEVICE_DEFAULTS # validated open parameters (device.py) # ---- port-specific hooks (called by ModelBase; keep host work out of _forward) --------------------------- def _build(self) -> None: """Weights (``reference.weights.StreamPETRWeights``: the three ONNX files by consuming node), the host state and the device graph (``tt.model.TtStreamPETR``, the ``frame`` variant: backbone + head + in-trace memory update). The position embedding and the spatial-alignment coefficients run on the host once per calibration (PORT_LOG S5). No capture here (``_warm_one``).""" from .reference.model import Head, PositionEmbedding from .reference.weights import StreamPETRWeights, find_weights_dir from .tt.model import TtStreamPETR self._build_host() wd = find_weights_dir(self.weights_path) if wd is None: raise FileNotFoundError(f"{self.weights_path}: the three camera_streampetr v1.0 ONNX files are needed") self.net_weights = StreamPETRWeights(wd) self.ref_head = Head(self.net_weights, self.cfg) self.ref_pe = PositionEmbedding(self.net_weights, self.cfg) self.tt = TtStreamPETR(self.device, self.net_weights, self.cfg, variants=("frame",), precision=str(self.knobs.PRECISION), attn_fp32_acc=bool(self.knobs.ATTN_FP32_ACC), head_act=str(self.knobs.HEAD_ACT), key_split=bool(self.knobs.KEY_SPLIT), exact_map=str(self.knobs.EXACT_MAP), temporal_exact=bool(self.knobs.TEMPORAL_EXACT), state_banks=int(self.knobs.MAX_STREAMS), head=self.ref_head, stem_in=str(self.knobs.STEM_IN), conv_l1=str(self.knobs.CONV_L1), conv_l1_abh=str(self.knobs.CONV_L1_ABH), ese=str(self.knobs.ESE), osa_mm=str(self.knobs.OSA_MM), kv_epi=bool(self.knobs.KV_EPI), self_kv=str(self.knobs.SELF_KV), s26_kernel=bool(self.knobs.S26_KERNEL), ese_mean=str(self.knobs.ESE_MEAN), osa_cat=str(self.knobs.OSA_CAT), conv_cfg=str(self.knobs.CONV_CFG), sel=str(self.knobs.SEL), attn_exp=str(self.knobs.ATTN_EXP), xattn=str(self.knobs.XATTN), conv_chain=str(self.knobs.CONV_CHAIN), reshape=str(self.knobs.RESHAPE), kmm_buf=str(self.knobs.KMM_BUF)) self.runner = self.tt.runner self._img_stage = None if self._fast_pre(): from .host import fast as hfast from .ttaw.tensors import HostStaging stage = HostStaging(self.tt.img_shape, "bfloat16") self._img_stage = stage if stage.zero_copy else None hfast.warm() self.calib_fn = self._calibration_constants self._active_calib: Optional[str] = None # rig key whose PE / MLN coefficients are on the device self._device_owner: Optional[str] = None # stream id whose memory the device states hold self._banks: "OrderedDict[str, int]" = OrderedDict() # stream id -> state bank (PLAN D16) def _warm_one(self, variant: Any) -> None: """Capture the ``frame`` trace (warm-up runs of the eager graph first; ``TraceRunner.capture``).""" self.tt.capture() def _calibration_constants(self, rig: Any) -> Dict[str, Any]: """Host fp32 PE graph (``reference.model.PositionEmbedding``) and the spatial-alignment MLN coefficients of its ``cone`` (``Head.spatial_alignment_coeffs``) -> the device params of one calibration.""" import torch with torch.no_grad(): pos, cone = self.ref_pe(rig.img_metas_pad, rig.intrinsics, rig.img2lidar) gamma, beta = self.ref_head.spatial_alignment_coeffs(cone) params = self.tt.calibration_params(pos.numpy(), gamma.numpy(), beta.numpy()) return {"key": rig.key(), "params": params} def _build_host(self) -> None: """The host half of the model: configuration, load-time knobs, the per-calibration cache and the per-stream temporal states (no device, no weights; the host tests call it directly).""" from .reference.config import default_config params = {k.upper(): v for k, v in getattr(self, "compile_params", {}).items() if k.upper() in KNOBS.knobs} self.knobs = KNOBS.read(**params) self.cfg = default_config(normalization=str(self.knobs.NORMALIZATION), use_temporal=bool(self.knobs.USE_TEMPORAL)) self._streams: "OrderedDict[str, Any]" = OrderedDict() self._calib: "OrderedDict[str, Dict[str, Any]]" = OrderedDict() # rig key -> PE / cone / MLN coeffs self.calib_fn = None # rig -> {"pos_embed", "cone", "sa_gamma", "sa_beta"} (set by _build) self._img_bits: Optional[np.ndarray] = None # HOST_PRE=numba without a zero-copy staging buffer self._img_stage = getattr(self, "_img_stage", None) def _fast_pre(self) -> bool: """HOST_PRE=numba applies (numba importable; else the numpy path).""" from .host import fast as hfast return str(self.knobs.HOST_PRE) == "numba" and hfast.available() def _img_target(self) -> np.ndarray: """Where HOST_PRE=numba writes the bf16 upload rows: the persistent ttnn host buffer (zero copy) when the device model exists, else a reused numpy buffer. Calls are serialized by the model lock, and each frame's upload completes inside its ``_forward``, so one buffer suffices.""" if self._img_stage is not None: return self._img_stage.view n, (h, w) = self.cfg.num_cams, tuple(self.cfg.input_hw) if self._img_bits is None: self._img_bits = np.empty((n * h * w, 3), np.uint16) return self._img_bits def _img_input(self, prepared: Dict[str, Any]) -> Any: """The value of the ``img`` trace input: the staging host tensor, a ttnn host tensor of the bf16 bits, or (HOST_PRE=numpy) the float32 NHWC rows.""" if "img_bits" not in prepared: from .tt.model import img_rows return self.tt.img_input(img_rows(prepared["img"])) if self._img_stage is not None and prepared["img_bits"] is self._img_stage.view: return self._img_stage.tensor import torch import ttnn bits = np.ascontiguousarray(prepared["img_bits"]).reshape(self.tt.img_shape) t = torch.from_numpy(bits.view(np.int16)).view(torch.bfloat16) return ttnn.from_torch(t, dtype=ttnn.bfloat16, layout=ttnn.ROW_MAJOR_LAYOUT) def _stream_state(self, sid: str, reset: bool) -> Any: """The host temporal state of ``sid`` (``host.temporal.StreamState``); a new id or ``reset`` starts fresh; beyond ``MAX_STREAMS`` the least recently used stream is dropped (PLAN D16, first publish: one stream).""" from .host.temporal import StreamState if reset or sid not in self._streams: self._streams[sid] = StreamState(self.cfg, autoware_compat=bool(self.knobs.AUTOWARE_COMPAT), use_temporal=bool(self.knobs.USE_TEMPORAL)) self._streams.move_to_end(sid) while len(self._streams) > int(self.knobs.MAX_STREAMS): self._streams.popitem(last=False) return self._streams[sid] def _calibration(self, rig: Any) -> Optional[Dict[str, Any]]: """Once-per-calibration constants (PE, cone, spatial-alignment gamma / beta), cached by the rig's hash.""" if self.calib_fn is None: return None key = rig.key() if key not in self._calib: self._calib[key] = self.calib_fn(rig) while len(self._calib) > 8: self._calib.popitem(last=False) self._calib.move_to_end(key) return self._calib[key] def _prepare(self, points: Any = None, *, images: Any = None, calibration: Optional[Mapping[str, Any]] = None, stream: Optional[Mapping[str, Any]] = None, **_: Any) -> Dict[str, Any]: """Five images + calibration + stream -> the host values of one frame: the network input ``img`` [1, 5, 3, 480, 640] float32 (preset ``NORMALIZATION``), the rig calibration (+ its cached PE constants once the device model is built) and the stream's frame plan (ts, E / E^-1, the ages, fresh / skipped).""" from .host.inputs import prepare_frame from .host.preprocess import preprocess_images from .host.temporal import StreamState if points is not None: raise tio.InputError("this model takes five camera images, not a point cloud") frame = prepare_frame(images, calibration, stream, self.cfg) st = frame.stream if st.stateless: sid, state = None, StreamState(self.cfg, autoware_compat=bool(self.knobs.AUTOWARE_COMPAT), use_temporal=bool(self.knobs.USE_TEMPORAL)) else: sid, state = st.id, self._stream_state(st.id, st.reset) anchor = frame.anchor_stamp if frame.anchor_stamp is not None else 0.0 T = np.eye(4) if st.stateless else st.T_world_from_ego # None: no ego pose -> skipped (no_ego_pose) plan = state.begin(anchor, T, frame.camera_stamps) out: Dict[str, Any] = {"frame": frame, "plan": plan, "state": state, "stream_id": sid} if plan.skipped is None: if self._fast_pre(): from .host import fast as hfast out["img_bits"] = hfast.preprocess_rows_bf16( frame.images, preset=self.cfg.normalization, dst_hw=tuple(self.cfg.input_hw), fma=bool(self.knobs.PREPROCESS_FMA), threads=int(self.knobs.PREPROCESS_WORKERS), out=self._img_target()) else: out["img"] = preprocess_images(frame.images, self.cfg, fma=bool(self.knobs.PREPROCESS_FMA), workers=int(self.knobs.PREPROCESS_WORKERS)) out["calib"] = self._calibration(frame.rig) return out def _select_device_memory(self, prepared: Dict[str, Any]) -> None: """Bring the right temporal memory into the device states before the frame (PLAN D16, PORT_LOG S11): the memory of the stream whose frame runs (saved to / loaded from its DRAM bank when another stream or a stateless frame ran in between), zeros for a fresh stream or a stateless frame, and the node's per-frame wipe when ``USE_TEMPORAL`` is off.""" r, sid, plan, state = self.runner, prepared["stream_id"], prepared["plan"], prepared["state"] owner = self._device_owner if owner is not None and owner != sid and owner in self._streams and owner in self._banks: for name in ("emb", "geo"): r.save_state(name, self._banks[owner]) if sid is not None and sid not in self._banks: used = set(self._banks.values()) free_banks = [b for b in range(int(self.knobs.MAX_STREAMS)) if b not in used] if not free_banks: # the least recently used stream lost its memory old, bank = self._banks.popitem(last=False) free_banks = [bank] self._banks[sid] = free_banks[0] if sid is not None: self._banks.move_to_end(sid) for gone in [k for k in self._banks if k not in self._streams]: del self._banks[gone] if sid is None or plan.fresh: self.tt.set_memory() # a fresh node: zero memory elif not state.use_temporal: ref = state.wiped_device_memory() n = self.cfg.pre_memory_length self.tt.set_memory(None, np.tile(ref, (n, 1)), np.zeros((n, 4, 4), np.float32), np.zeros((n, 2), np.float32)) elif owner != sid: for name in ("emb", "geo"): r.load_state(name, self._banks[sid]) self._device_owner = sid def _forward(self, prepared: Dict[str, Any]) -> Any: """The traced device graph: upload ``img`` and the per-frame ages / relative motion (and the calibration constants when the calibration changed), replay ``frame`` (backbone + head + memory update), read the packed cls / bbox. A skipped frame (camera sync) runs nothing.""" import time from .tt.model import TtStreamPETR from .tt.params import age_input plan = prepared["plan"] if plan.skipped is not None: return None t0 = time.perf_counter() self._select_device_memory(prepared) params = self.tt.geometry_params(plan.M) calib = prepared["calib"] if calib["key"] != self._active_calib: params.update(calib["params"]) self._active_calib = calib["key"] inputs = {"img": self._img_input(prepared), "ages": age_input(plan.pre["pre_memory_timestamp"], device_shape=True)} t1 = time.perf_counter() packed = self.runner("frame", inputs=inputs, params=params) t2 = time.perf_counter() prepared["state"].end_device(plan) out = TtStreamPETR.unpack(packed["cls"], packed["reg"]) out["timing_ms"] = {"host_in": (t1 - t0) * 1e3, "trace": (t2 - t1) * 1e3} return out def _postprocess(self, raw: Any, prepared: Dict[str, Any], params: Dict[str, Any]) -> Output: """Head outputs (``raw``: ``all_cls_scores`` [1, 5, 5400], ``all_bbox_preds`` [1, 10, 5400]) -> the node's decode of all 6 layers, thresholds, NMS -> ``Detections3D``; a skipped frame (camera sync) -> no objects.""" from .host import postprocess as hpost from .reference.pipeline import to_detections plan = prepared["plan"] meta = {"stream": prepared["stream_id"], "fresh": bool(plan.fresh), "skipped": plan.skipped, "ts": float(plan.ts), "frame_index": int(plan.frame_index)} if plan.skipped is not None or raw is None: empty = {"objects": hpost.decode(np.zeros((5, 0), np.float32), np.zeros((10, 0), np.float32), self.cfg), "num_pre_nms": 0} return to_detections(empty, self.cfg, model=self.MODEL_NAME, meta=meta) res = hpost.autoware_postprocess(raw["all_cls_scores"], raw["all_bbox_preds"], self.cfg, hpost.params_from(params, self.cfg), fast_nms=str(self.knobs.HOST_POST) == "numba") out = to_detections(res, self.cfg, model=self.MODEL_NAME, meta=meta) out.timing_ms.update({k: round(v, 3) for k, v in (raw.get("timing_ms") or {}).items()}) return out def _release(self) -> None: """Release the traces and persistent device tensors; also called when ``from_pretrained`` fails half-way.""" runner = getattr(self, "runner", None) if runner is not None: runner.release() def extra_info(self) -> Dict[str, Any]: """Additions to ``model.info`` and ``/info``: the trace variants, CQs and persistent tensors of the runner, the load-time knobs.""" info: Dict[str, Any] = {} runner = getattr(self, "runner", None) if runner is not None: info["trace"] = runner.describe() knobs = getattr(self, "knobs", None) if knobs is not None: info["knobs"] = knobs.as_dict() return info