changh95's picture
tt-model push frnet-p150 (container)
fe51a63 verified
Raw History Blame Contribute Delete
11.5 kB
# SPDX-License-Identifier: Apache-2.0
"""Python API: FRNet (Autoware lidar_frnet) on one Tenstorrent Blackhole p150.
from tt_frnet import FRNet
with FRNet.from_pretrained(device_id=0) as model: # weights -> HF cache, device open, traces captured
out = model("code/tt_frnet/samples/synthetic_ot128.npz", calibration={"preset": "synthetic_ot128"})
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_frnet.server.app``) calls this class, so ``/predict`` and ``model(...)`` agree bit for bit.
This module holds the identity of the port and its hooks: host pre- and post-processing ported from Autoware live in
``tt_frnet.host`` (shared with the CPU reference ``tt_frnet.reference``, so device and reference see identical
network inputs), the ttnn graph in ``tt_frnet.tt`` (run by a ``ttaw.trace.TraceRunner``; milestones M1-M3 of
PORT_LOG.md). Importing it has no side effects: ttnn / torch are imported inside the hooks.
Inputs (``host/frame.py`` has the full contract): ``points`` -- ONE sweep in the **sensor frame**, fields x, y, z,
intensity (raw 0-255); ``calibration`` -- ``{"T_base_link_from_sensor": ...}`` (or ``{"preset": name}`` of
``calib/``) for the node's ego crop box, ``ego_crop_box`` to override or disable the box (``None``).
``variant``: ``ot128`` (Autoware's default sensor model) or ``qt128``, a load-time choice (weights + grid).
Output: :class:`FRNetSegmentation` (a ``ttaw.outputs.Segmentation3D``) in the node's output order: the kept raw
points (input order) then Autoware's interpolated points; ``label_ids`` = class_id, ``scores`` = probability,
``points``, ``filter_keep`` (the filtered cloud), ``input_index``. A frame outside the node's profiles gives an empty
output whose ``meta["skipped"]`` is the node's message (the node publishes nothing).
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from . import io as tio
from .device import DEVICE_DEFAULTS
from .host.postprocess import RUNTIME_PARAMS, FRNetSegmentation
from .reference.config import DEFAULT_VARIANT, VARIANTS
from .ttaw.api_base import ModelBase
__all__ = ["FRNet", "Output", "FRNetSegmentation", "LABELS"]
# The result class of this model (ttaw.outputs: Detections3D, Detections2D, Segmentation3D, Mask2D, Trajectory).
Output = FRNetSegmentation
# ml_package_frnet_{ot128,qt128}.param.yaml class_names (identical in both variants; _build_host checks the yaml).
# Indices 22 / 23 read "debris" / "stroller" here, but the weights were trained with stroller = 22, debris = 23
# (AWML frnet_1xb8_t4dataset-ot128-seg.py:60-61; S:frnet:314-317): Autoware's names are kept for parity.
LABELS = ("drivable_surface", "other_flat_surface", "sidewalk", "manmade", "vegetation", "car", "bus",
"emergency_vehicle", "train", "truck", "tractor_unit", "semi_trailer", "construction_vehicle", "forklift",
"kart", "motorcycle", "bicycle", "pedestrian", "personal_mobility", "animal", "pushable_pullable",
"traffic_cone", "debris", "stroller", "other_stuff", "noise+ghost_point", "unknown")
def _given(value: Any) -> bool:
"""A real input (``None`` and empty containers are 'not given'); never the truth value of an array."""
if value is None:
return False
if isinstance(value, (list, tuple, dict, str)):
return len(value) > 0
return True
class FRNet(ModelBase):
"""FRNet (Autoware lidar_frnet) on one Blackhole p150. Create it with :meth:`from_pretrained`."""
MODEL_NAME = "frnet-p150"
ENV_PREFIX = "FRNET" # prefix of the environment knobs (SERVING.md section 3.4)
DEFAULT_REPO = "AutowareFoundation/lidar_frnet"
DEFAULT_TAG = "v2.0" # the Autoware ansible artifacts pin
# the commit DEFAULT_TAG points to (pinned: tags can move)
DEFAULT_REVISION = "a53a1c11b0b28d31fd50a03abef5db0e830a5b3c"
ALLOW_PATTERNS = ["frnet_*.onnx", "ml_package_frnet_*.param.yaml", "deploy_metadata.yaml"]
VARIANTS = list(VARIANTS) # load-time: selects the ONNX, the frustum grid and the trace shapes
DEFAULT_VARIANT = DEFAULT_VARIANT
INPUT_KIND = "lidar" # lidar | camera | multicam | lidar+multicam | planner
CAMERA_ORDER = () # multicam models: the fixed camera order of the network
POINT_FIELDS = tio.DEFAULT_POINT_FIELDS # columns the network consumes, in order (x, y, z, intensity)
LABELS = list(LABELS)
# Per-request knobs (host post-processing only; defaults are Autoware's): host/postprocess.py
RUNTIME_PARAMS = RUNTIME_PARAMS
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:
"""Host side (config, weights, pre- / post-processors), then the ttnn graph of ``tt_frnet.tt``
(``TtFRNet``: point encoder, K1 segmented max, gathers, convs, decoder, head) as the ``"default"`` variant of
a ``ttaw.trace.TraceRunner`` with the per-frame tables as persistent inputs (no capture here; ``_warm_one``
captures). Warm-up runs an empty frame (every index valid)."""
from .tt.model import FrameStaging, TtFRNet, build_runner, default_policy
self._build_host()
self.graph = TtFRNet(self.device, self.frnet_weights, self.config, policy=default_policy())
self.runner = build_runner(self.graph, variants=("default",), name=f"{self.MODEL_NAME}/{self.variant}")
self.staging = FrameStaging(self.graph) # persistent host buffers of the inputs (written in place)
def _build_host(self) -> None:
"""The host half of ``_build`` (no device): usable alone by host tests and tools."""
from .host.frame import FRNetPreprocessor
from .host.postprocess import FRNetPostprocessor
from .reference.config import FRNetConfig
from .reference.weights import check_variant_grid, load_weights
self.config = FRNetConfig.from_model_dir(self.weights_path, self.variant)
if tuple(self.config.class_names) != tuple(self.LABELS):
raise ValueError(f"ml_package class_names differ from FRNet.LABELS: {self.config.class_names}")
self.frnet_weights = load_weights(self.weights_path, self.variant)
check_variant_grid(self.frnet_weights, self.config.frustum_h, self.config.frustum_w)
self.pre = FRNetPreprocessor(self.config, pre_norm=(self.frnet_weights.pre_norm_scale,
self.frnet_weights.pre_norm_shift))
self.post = FRNetPostprocessor(self.config)
def _warm_one(self, variant: Dict[str, Any]) -> None:
"""Warm up and capture the traces of one entry of ``default_warmup_variants()``. ``TraceRunner.capture``
runs every pending variant eagerly (kernel JIT, program cache) before any capture, then captures with
program-cache misses forbidden; it is idempotent."""
self.runner.capture()
def _prepare(self, points: Any = None, *, sweeps: Optional[Any] = None, images: Any = None,
calibration: Any = None, stream: Any = None, inputs: Any = None, **_: Any):
"""Host pre-processing (``host/frame.py``): decoding, the ego crop, float32 projection, fp16 interpolation,
unique cells, the node's profile checks and the device plan. Client mistakes raise ``tio.InputError``
(HTTP 400); an out-of-profile frame is ``prepared.skipped`` (no device run, empty output)."""
if points is None:
raise tio.InputError("this model needs 'points'")
extra = [name for name, value in (("sweeps", sweeps), ("images", images), ("inputs", inputs),
("stream", stream)) if _given(value)]
if extra:
raise tio.InputError(f"FRNet takes one sensor-frame sweep ('points') and its 'calibration' only; got "
f"{extra}")
return self.pre(points, calibration)
def _forward(self, prepared: Any) -> Any:
"""Upload the frame plan (features, CSR offsets, gather tables) into the persistent device inputs, replay
the trace, read ``pred_probs`` back (one D2H) and return it as ``(N, 27)`` float32 in the network-input order
(``plan.unsort``). A skipped frame runs nothing. Runs under the model lock."""
if prepared.skipped is not None:
return None
return self.device_probs(prepared.plan)
def device_probs(self, plan: Any) -> Any:
"""``pred_probs`` ``(N, 27)`` float32 (network-input order) of one ``host.preprocess.FramePlan`` (the device
half of ``_forward``; tests and tools feed stored network inputs through it). Call it under the model
lock (``model(...)`` does)."""
import numpy as np
raw = self.runner("default", inputs=self.staging.write(plan))
probs = np.asarray(raw, np.float32).reshape(-1, raw.shape[-1])
return plan.unsort(probs[:, : self.config.num_classes])
def _postprocess(self, raw: Any, prepared: Any, params: Dict[str, Any]) -> Output:
"""Host post-processing (``host/postprocess.py``, ``fill_cloud_kernel``): first-max argmax, probability,
the class-probability filter -> ``FRNetSegmentation``; ``raw`` = ``pred_probs`` or None (skipped)."""
meta = {"variant": self.variant, **prepared.stats()}
self.post.filter_indices(params["filter_classes"]) # an unknown class name is a 400 either way
if raw is None:
return self.post.empty(model=self.MODEL_NAME, frame_id=prepared.frame_id, meta=meta)
inp = prepared.inputs
return self.post(raw, points=inp.points, num_points_raw=inp.num_points_raw,
input_index=prepared.input_index, params=params, model=self.MODEL_NAME,
frame_id=prepared.frame_id, meta=meta)
def _release(self) -> None:
"""Release the traces, persistent tensors and device weights; also called when ``from_pretrained`` fails
half-way (tolerates a partial build)."""
runner = getattr(self, "runner", None)
if runner is not None:
runner.release()
self.runner = None
graph = getattr(self, "graph", None)
if graph is not None:
graph.release()
self.graph = None
def extra_info(self) -> Dict[str, Any]:
"""Additions to ``model.info`` and ``/info``: the node parameters and the runner's traces."""
out: Dict[str, Any] = {}
config = getattr(self, "config", None)
if config is not None:
out["frnet"] = config.describe()
runner = getattr(self, "runner", None)
if runner is not None:
out["trace"] = runner.describe()
graph = getattr(self, "graph", None)
if graph is not None:
out["graph"] = graph.describe()
return out