Download code/tt_transfusion/api.py from changh95/transfusion-p150: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/changh95/transfusion-p150/resolve/main/code/tt_transfusion/api.py
- Command line
-
hf download hf://changh95/transfusion-p150/code/tt_transfusion/api.py
-
curl -L -o api.py https://huggingface.co/changh95/transfusion-p150/resolve/main/code/tt_transfusion/api.py
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 | |