changh95's picture
tt-model push transfusion-p150 (container)
8dc9457 verified
Raw History Blame Contribute Delete
11.1 kB
# SPDX-License-Identifier: Apache-2.0
"""Python API: TransFusion-L (Autoware lidar_transfusion) on one Tenstorrent Blackhole p150.
from tt_transfusion import TransFusion
with TransFusion.from_pretrained(device_id=0) as model: # weights -> HF cache, device open, traces captured
out = model("code/tt_transfusion/samples/test_pcd.npz") # see PYTHON.md for every input form
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_transfusion.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_transfusion.host`` (shared with the CPU reference ``tt_transfusion.reference``, so device and reference see
identical voxel and feature tensors), the ttnn graph in ``tt_transfusion.tt`` (one trace, run by a
``ttaw.trace.TraceRunner``). Importing it has no side effects: ttnn / torch are imported inside the hooks.
Inputs (``points``, plus optional ``sweeps`` or ``stream``; host/preprocess.py has the full contract):
- a cloud in ``base_link`` with fields x, y, z, intensity (0-255, Autoware's uint8 value; float values are clipped
and truncated like a uint8 cast);
- a column named ``time_lag`` marks an already densified cloud (Autoware's 2-sweep input), used as is;
- ``sweeps=[{"points", "time_lag_s", "T_current_from_sweep"}]``: past sweeps densified on the host (stateless);
- ``stream={"id", "timestamp_s", "T_world_from_ego", "reset"}``: Autoware's stateful densification per stream id
(``densification_num_past_frames: 1``), a missing pose refuses the frame without caching it (Autoware's TF failure);
- ``sampling="clean"`` (default: every point, first 20 per pillar in input order) or ``"autoware_compat"`` /
``{"mode": "autoware_compat", "offset": int}`` (the deployed node's capacity-wide shuffle emulated, and its skips of
frames with < 5,000 or > 60,000 pillars); an input keyword (``EXTRA_INPUTS``), also a top-level field of
``/predict``.
Output: ``Detections3D`` with Autoware ``DetectedObjects`` semantics: ObjectClassification label ids (``LABELS``),
``existence_probability`` as the score, the network yaw (counter-clockwise from +x, z at the box centre),
(length, width, height); ``meta`` holds the pillar counts (overflow above 65,536 pillars, Autoware's skip verdict),
the sampling, the per-stage detection counts, the per-object orientation availability and proposal index.
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from . import io as tio
from .device import DEVICE_DEFAULTS
from .host.postprocess import LABELS, RUNTIME_PARAMS, TransFusionPostprocessor
from .host.preprocess import Prepared, TransFusionPreprocessor
from .reference.config import DEFAULT_VARIANT, VARIANTS, TransFusionConfig
from .ttaw.api_base import ModelBase
from .ttaw.outputs import Detections3D
__all__ = ["TransFusion", "Output"]
# The result class of this model (ttaw.outputs: Detections3D, Detections2D, Segmentation3D, Mask2D, Trajectory).
Output = Detections3D
class TransFusion(ModelBase):
"""TransFusion-L (Autoware lidar_transfusion) on one Blackhole p150. Create it with :meth:`from_pretrained`."""
MODEL_NAME = "transfusion-p150"
ENV_PREFIX = "TRANSFUSION" # prefix of the environment knobs (SERVING.md section 3.4)
DEFAULT_REPO = "AutowareFoundation/lidar_transfusion"
DEFAULT_TAG = "v2.1" # the Autoware ansible artifacts pin
# the commit DEFAULT_TAG points to (pinned: tags can move)
DEFAULT_REVISION = "8f07eb93038bfd62e35d1f78e56a6b3fb76b12aa"
ALLOW_PATTERNS = ["transfusion.onnx", "transfusion_ml_package.param.yaml", "detection_class_remapper.param.yaml",
"deploy_metadata.yaml"]
VARIANTS = list(VARIANTS) # one network; sampling is a per-request host knob, not a variant
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 # default column names of an unnamed (N, 4) array
# output labels: autoware_perception_msgs ObjectClassification (label_id = the message value); the network's five
# classes (CAR, TRUCK, BUS, BICYCLE, PEDESTRIAN) map onto them, the remapper adds TRAILER
LABELS = list(LABELS)
# Per-request knobs (host post-processing only; defaults are Autoware's): host/postprocess.py
RUNTIME_PARAMS = RUNTIME_PARAMS
# Per-request pre-processing keyword (handed to _prepare): host/preprocess.py parse_sampling
EXTRA_INPUTS = ("sampling",)
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, pre- / post-processors), then the ttnn graph of ``tt_transfusion.tt``
(``TtTransFusion``: PFN, gather-form scatter, SECOND, SECONDFPN, dense head, local max, query head) as the
``"default"`` variant of a ``ttaw.trace.TraceRunner`` with two persistent inputs (no capture here;
``_warm_one`` captures), plus the persistent host buffers of those inputs (``ttaw.models.pillars.FrameStaging``)."""
from .reference.weights import load_weights
from .tt.model import FrameStaging, TtTransFusion, build_runner, stage_inputs
self._build_host()
# precision policy and knobs (TRANSFUSION_PRECISION, TRANSFUSION_BACKBONE_WEIGHT_TERMS) read once here
self.graph = TtTransFusion(self.device, load_weights(self.weights_path, self.config), self.config)
self.runner = build_runner(self.graph, variants=("default",), name=f"{self.MODEL_NAME}/{self.variant}")
self.staging = self.graph.make_staging() # C24 FrameStaging (slot) or tt/pfn.PairFrameStaging (pair)
self._stage = stage_inputs
def _build_host(self, *, max_streams: int = 16) -> None:
"""The host half of ``_build`` (no device): usable alone by host tests and tools."""
self.config = TransFusionConfig.from_weights(self.weights_path, self.variant)
from .tt.model import KNOBS
k = KNOBS.read()
self.pre = TransFusionPreprocessor(self.config, max_streams=max_streams, voxelizer=str(k.HOST_VOXELIZER),
densify=str(k.HOST_DENSIFY))
if "numba" in (k.HOST_VOXELIZER, k.HOST_DECORATE, k.HOST_DENSIFY, k.HOST_INDEX, k.HOST_NMS):
from .host import fast
fast.DECORATE_THREADS = max(1, int(k.HOST_THREADS)) # H1b: parallel decoration (bit-identical)
fast.warm_up() # compile the host twins at load, not on frame 1
self.post = TransFusionPostprocessor(self.config, nms=str(k.HOST_NMS))
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, stream: Optional[Any] = None,
images: Any = None, calibration: Any = None, inputs: Any = None, sampling: Any = None,
**_: Any) -> Prepared:
"""Host pre-processing (``host/preprocess.py``): densification, sampling, deterministic voxelization, the
float32 decoration and the canvas gather index. Client mistakes raise ``tio.InputError`` (HTTP 400)."""
if points is None:
raise tio.InputError("this model needs 'points'")
if images or inputs is not None:
raise tio.InputError("TransFusion takes a point cloud only (no 'images' / 'inputs')")
return self.pre(points, sweeps=sweeps, stream=stream, sampling=sampling)
def _forward(self, prepared: Prepared) -> Any:
"""Upload the frame's decorated features and canvas index into the persistent device inputs, replay the
trace, read the packed outputs back (one D2H) and unpack them into ``{cls_score0, bbox_pred0,
dir_cls_pred0}``. A frame Autoware skips (``autoware_compat``) runs nothing. Runs under the model lock."""
if prepared.skipped is not None:
return None
inputs = self._stage(self.staging, prepared)
variant = getattr(self.staging, "variant", "default") # PFN_BUCKETS: the frame's pillar capacity
return self.graph.assemble(self.runner(variant, inputs=inputs))
def _postprocess(self, raw: Any, prepared: Prepared, params: Dict[str, Any]) -> Output:
"""Host post-processing (``host/postprocess.py``): decode, score threshold, circle NMS, ROS conversion,
IoU-BEV NMS, class remapper -> ``Detections3D``; ``raw`` = ``{cls_score0, bbox_pred0, dir_cls_pred0}`` or None
for a skipped frame (no objects, as the node publishes nothing)."""
meta = {"variant": self.variant, **prepared.stats()}
if raw is None:
return self.post.empty(model=self.MODEL_NAME, frame_id=prepared.frame_id, meta={**meta, "counts": {}})
return self.post(raw, 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 config, the host state and the runner."""
info: Dict[str, Any] = {}
if getattr(self, "config", None) is not None:
info["config"] = self.config.describe()
if getattr(self, "pre", None) is not None:
info["preprocess"] = self.pre.describe()
runner = getattr(self, "runner", None)
if runner is not None:
info["trace"] = runner.describe()
graph = getattr(self, "graph", None)
if graph is not None:
info["graph"] = graph.describe()
return info