frnet-p150 / tt-model.yaml
changh95's picture
tt-model push frnet-p150 (container)
fe51a63 verified
Raw History Blame
13.8 kB
# SPDX-License-Identifier: Apache-2.0
# tt-model-manager container manifest (schema 5.1): FRNet (Autoware lidar_frnet) on one Blackhole p150.
# Conventions: research/BUNDLE_CONVENTIONS.md (workspace) -- section 3 explains every field.
#
# Run EVERY tt-model command FROM THIS DIRECTORY: source.tt_metal and extra_code[].root resolve against the
# process CWD, not this file; --out must point OUTSIDE this directory (package deletes <out>/<name> first).
# python -c "from tt_kernel.container_manifest import load_container_manifest as L; L('tt-model.yaml', check_sources=True)"
# sg docker -c "tt-model package --container tt-model.yaml --out /home/ubuntu/experiments/tt-models/build"
# (serve + smoke + stop under bin/devrun, once per serve profile: code/scripts/container_smoke.sh, SERVING.md 2)
# HF_TOKEN=<from env> tt-model push /home/ubuntu/experiments/tt-models/build/frnet-p150 --publish
schema: "5.1"
repo: "changh95/frnet-p150"
name: "frnet-p150"
# A POINTER to the network Autoware deploys (ansible artifacts role: lidar_frnet @ v2.0), never baked into
# the image. Pinned to the COMMIT the tag points to -- HfApi().model_info(repo, revision="v2.0").sha, not
# list_repo_refs().tags[].target_commit (that is the annotated-tag object) -- because tags can move and `package`
# does not resolve weights refs. `serve` / `pull --with-weights` pre-download exactly these files at this sha; the
# launcher exports the sha as TT_MODEL_WEIGHTS_REVISION (and serve.env repeats it as TT_WEIGHTS_REVISION).
weights:
repo: "AutowareFoundation/lidar_frnet"
revision: "a53a1c11b0b28d31fd50a03abef5db0e830a5b3c"
allow_patterns: ["frnet_*.onnx", "ml_package_frnet_*.param.yaml", "deploy_metadata.yaml"]
kind: tt-dit-server # the model's own FastAPI app under uvicorn (no vLLM); see the conventions doc
arch: blackhole
source:
# tt-metal main 44d6650 (v0.80.0-dev20261006) WITH patches/tt-metal-eth-dispatch.patch applied: a dirty tree,
# packaged as-is (the image is built from it, so it carries ETH dispatch). torch 2.11.0 pin read from it.
tt_metal: "/home/ubuntu/experiments/tt-models/tt-metal"
# Schema minimum (>= 1 path relative to the tt-metal tree). The port imports nothing from tt-metal's models/.
code:
- models/common/lightweightmodule.py
# The port itself. code/ on the Hub becomes EXACTLY this set (+ the file above) at every push, and it is
# COPY'd over /opt/tt-metal/<path> in the image (PYTHONPATH=/opt/tt-metal) BEFORE tt-metal's C++ build and its
# `uv pip install -e /opt/tt-metal`. So never list a file that tt-metal's root also has and its build reads
# (pyproject.toml, setup.py, setup.cfg, MANIFEST.in, README.md, uv.toml, CMakeLists.txt, build_metal.sh, ...) or
# anything under its build trees (tt_metal/, ttnn/, cmake/, tools/, ...): check_bundle.py refuses them. The pip
# project is the repo-ROOT pyproject.toml (`pip install -e .`), published as an overlay file, never from here.
# conftest.py is deliberate: it replaces tt-metal's root conftest.py in the builder stage, which no build step
# reads, and the runtime stage never copies tt-metal's, so in the image it is the only root conftest.
extra_code:
- root: code
paths:
- "tt_frnet"
- scripts
- conftest.py
- PYTHON.md
ubuntu: "22.04"
python: "3.12"
runtime:
app: "tt_frnet.server.app:app"
mesh_shape_env: TT_MESH_SHAPE
# On top of the kind defaults (fastapi, uvicorn, pydantic>=2, pillow) and the auto-pinned torch==2.11.0+cpu.
# numpy stays < 2 (ttnn). Add per model: scipy, opencv-python-headless>=4.9,<4.12, ...
packages:
- "numpy>=1.24.4,<2"
- huggingface_hub
- safetensors
- pyyaml
- "onnx>=1.17,<2"
# After the first green build: copy <out>/frnet-p150/requirements.lock next to this file, check it, and
# uncomment (resolved relative to THIS file):
# lock: requirements.lock
serve:
port: 20000
hardware: p150
mesh_device: P150
env:
TT_WEIGHTS_REVISION: "a53a1c11b0b28d31fd50a03abef5db0e830a5b3c" # older tt-model exports no revision; the app also reads TT_MODEL_WEIGHTS_REVISION
TT_METAL_VISIBLE_DEVICES: "0"
# The weights repos are public and ungated: never send a token. serve mounts the host HF cache (which may hold a
# stored token file) at /hf, so tell huggingface_hub not to pick one up implicitly (PACKAGING_PILOT.md problem 10).
HF_HUB_DISABLE_IMPLICIT_TOKEN: "1"
# Pinned dispatch: ETH, the 12x10 grid every published number uses (D14). Profiles must not override it:
# check_bundle.py refuses any *_DISPATCH other than eth, and the container smoke fails unless /info reports
# dispatch=eth, grid=12x10 and these pins.
"FRNET_DISPATCH": "eth"
"FRNET_NUM_CQS": "1"
"FRNET_VARIANT": "ot128"
# Knob defaults of tt_frnet.tt.model.KNOBS (asserted by tests/test_bundle_host.py): the two-term bf16 point island
# (FRNET_POINT_TERMS=1, fp32 x fp32, misses the OT128 e2e gates on nuScenes frames: PORT_LOG.md "M1 device results")
"FRNET_POINT_TERMS": "2"
# fp32 image-branch maps (conv operands bf16): with bf16 maps every conv / HardSwish / add output is rounded and
# OT128 misses the e2e argmax gate on nuScenes 0916 kf40 (98.28 %; PORT_LOG.md "M3 precision follow-up")
"FRNET_MAP_DTYPE": "float32"
"FRNET_CONV_TERMS": "1"
# OPTIONAL: load-time variants of ONE image as serve profiles (`tt-model serve <repo> --profile <name>`), e.g. the
# CenterPoint base / tiny pair. Each profile deep-merges over serve: (hardware / mesh_device are inherited) and is
# smoke-tested on its own (`container_smoke.sh <staged> <port> --profile <name>`).
serve_profiles:
- name: ot128
description: FRNet for the Hesai OT128 (Autoware's default sensor_model; 1024x128 frustum grid)
env:
"FRNET_VARIANT": ot128
- name: qt128
description: FRNet for the Hesai QT128 (256x128 frustum grid, +-52.6 deg FOV)
env:
"FRNET_VARIANT": qt128
default_profile: ot128
# Build-time assertions run INSIDE the finished image as uid 1000, no device, no weights.
verify:
# the image must carry the ETH-dispatch patch (12x10 grid): fail the build otherwise (PLAN.md 6.2 step 1)
- "assert 'single_chip_arch_1cq_no_dispatch_s' in open('/opt/tt-metal/tt_metal/impl/dispatch/topology.cpp').read()"
- "from tt_frnet.ttaw.device import eth_dispatch_patch_present as p; assert p(), 'ttaw does not see the ETH patch'"
- "import tt_frnet.server.app as a; assert a.app"
- "from tt_frnet.server.app import parse_mesh_shape as p; assert p('1x1') == p('(1, 1)') == p('1,1') == (1, 1)"
- "import numpy; assert int(numpy.__version__.split('.')[0]) < 2, numpy.__version__"
- "import onnx, yaml, huggingface_hub, safetensors, PIL"
- "from tt_frnet.api import FRNet; assert FRNet.DEFAULT_REVISION == 'a53a1c11b0b28d31fd50a03abef5db0e830a5b3c'"
- "import ttnn; assert hasattr(ttnn, 'DispatchCoreConfig') and hasattr(ttnn, 'begin_trace_capture') and hasattr(ttnn, 'execute_trace') and hasattr(ttnn, 'release_trace')"
# the shipped samples (synthetic scans, code/scripts/make_sample.py), their calibration presets and stored CPU
# references: the container smoke posts the sample of the served profile and compares with its reference
- "from pathlib import Path; d = Path('/opt/tt-metal/tt_frnet'); assert all((d / 'samples' / f'synthetic_{v}.npz').is_file() and (d / 'samples' / f'synthetic_{v}.{v}.reference.json').is_file() and (d / 'calib' / f'synthetic_{v}.json').is_file() for v in ('ot128', 'qt128'))"
# the K1 segment_reduce kernel source (ttaw.ops.segment; JIT-compiled at run time)
- "from pathlib import Path; assert Path('/opt/tt-metal/tt_frnet/ttaw/ops/kernels/segment_reduce_dm.cpp').is_file()"
- "from tt_frnet.ttaw.ops import kernel_path; kernel_path('segment_reduce_dm.cpp')"
card:
# Lede under the title of the GENERATED card (tt-model package). The published README.md is the hand-written
# card of this directory (overlaid before push); keep the facts here and there identical.
description: >
FRNet (Autoware lidar_frnet): the network Autoware deploys (autoware_lidar_frnet, weights AutowareFoundation/lidar_frnet
v2.0) running on one Tenstorrent Blackhole p150 via tt-nn: one LiDAR sweep in the sensor frame (x, y, z, raw 0-255 intensity) in, a class id and probability for every point (27 T4 classes; Autoware's interpolated points included) out.
architecture: "FRNet: frustum feature encoder (point MLP 8->64->128->256->256, per-cell max, 256->16) + frustum-range 2D CNN (stem, 4 residual stages with point-pixel fusion and attention, align-corners decoder) + per-point head (27-class softmax); one ONNX graph per sensor profile"
status: Community port (first release, baseline; optimization pending), validated against the fp32 CPU reference on public driving data
intended_use: >
LiDAR 3D semantic segmentation for Autoware-style sensor setups (one Hesai OT128 or QT128 sweep in the sensor frame) on one Blackhole p150, as a drop-in for the TensorRT node's network.
out_of_scope_use: >
Safety-critical driving decisions (this is not a certified Autoware component); multi-chip meshes; batch > 1;
inputs outside the Autoware sensor configuration the weights were trained for.
quickstart: |
The first start compiles the kernels and captures the traces (minutes on a cold cache, seconds later); the
server is ready when its log shows `Application startup complete`. Build a request from a file with
`python3 code/tt_frnet/server/client.py --points code/tt_frnet/samples/synthetic_ot128.npz --calib-preset synthetic_ot128 --out req.json` (your own cloud: `--calib my_calib.json`); the QT128 network is the `qt128` profile (`tt-model serve --profile qt128 <repo>`), then
`curl -s localhost:20000/predict -H 'Content-Type: application/json' -d @req.json`.
usage: |
`POST /predict` (JSON): `points` (base64 `.bin` / `.npy` / `.npz` / `.pcd`, sensor frame, fields x, y, z, intensity 0-255); `calibration` (`T_base_link_from_sensor` for the ego crop box, or `{"preset": name}`; optional `ego_crop_box`, `null` disables the box); optional `params` (`class_probability_threshold` 0.05, `filter_classes` "drivable_surface"), `output_format`. `GET /health`, `GET /info`, `GET /v1/models` (stub).
Full request / response contract: `SERVING.md` section 3.
performance: |
| Metric | Performance |
|---|---:|
| Class agreement with the fp32 CPU reference, shipped sample | OT128 99.55 % 路 QT128 99.62 % of the points |
| Class agreement, 113 public frames per profile (PandaSet 32, nuScenes mini_val 81) | OT128 99.66 % / 99.19 % pooled (worst frame 98.37 %) 路 QT128 99.29 % / 99.44 % (worst 98.10 %) |
| Frozen device gates (17 OT128 / 5 QT128 frames; PCC >= 0.998, agreement >= 98.5 %) | all pass; OT128 min agreement 98.907 % (nuScenes 0916 kf40, 0.41 pt margin) |
| Python `model()` call, shipped sample | OT128 292.6 ms 路 QT128 221.1 ms p50 |
| Served `/predict` `timing_ms.total`, shipped sample | OT128 310.4 ms 路 QT128 228.9 ms median |
| Device trace, one forward / back-to-back | OT128 210.02 / 209.95 ms 路 QT128 133.53 / 133.43 ms |
| `from_pretrained` load, empty / warm JIT cache | OT128 156.6 / 7.5 s 路 QT128 151.9 / 6.6 s |
ETH dispatch, 1 CQ, 12x10 grid, batch 1, 2026-10-09; details in README.md "Demo & Performances".
limitations: |
- First release: baseline port, optimization pending (OPT_REPORT.md); the point-wise layers kept at two-term bf16 precision are 63 % (OT128) / 75 % (QT128) of the device time.
- Narrow accuracy margin: 98.907 % agreement on the hardest gated frame (gate 98.5 %); 3 of 226 frame-profile pairs of the wider public set fall to 98.10-98.499 % (nuScenes scene-0916, out of domain).
- FRNet is an opt-in, standalone Autoware node; this bundle is not a ROS 2 node and not a certified Autoware component.
- Trained on T4 data from Hesai OT128 / QT128 sensors (not public); other sensors (PandaSet Pandar64, nuScenes HDL-32E) show a domain gap in the reference itself.
- Batch 1, one sweep per request, one variant per process; fixed device shapes per profile (160,000 / 120,000 points, 60,000 cells).
- Single p150 only (ETH dispatch patch; no multi-chip mesh); dispatch=worker is an A/B opt-in the numbers do not cover.
- Not an OpenAI-compatible API; p150 power not measured.
risks: >
Device numerics: per-point probabilities differ slightly from the fp32 reference (see the agreement figures);
points near a class boundary can change class. Validated against the fp32 reference on public driving data
(no public OT128 / QT128 data exists); the mapped-label mIoU is a sanity check, not an accuracy claim.
licensing: |
- Weights: [AutowareFoundation/lidar_frnet](https://huggingface.co/AutowareFoundation/lidar_frnet) at tag `v2.0` (commit `a53a1c11b0b28d31fd50a03abef5db0e830a5b3c`), Apache-2.0 per its model card; not redistributed here. Trained by TIER IV with AWML on about 16,000 frames of the T4Dataset (Hesai OT128 / QT128 sensors; not public).
- Pre-/post-processing ported from autoware_universe `perception/autoware_lidar_frnet` @ `9ceaccf026c31ffc5319bc9eeb4bd7bede0af3fd` (Apache-2.0).
- Port and serving code (`code/`): Apache-2.0. Shipped samples: synthetic scans generated by `code/scripts/make_sample.py` (Apache-2.0).
- Demo media: PandaSet renders (CC BY 4.0 + PandaSet Dataset Terms, attribution in media/ATTRIBUTION.md) and nuScenes renders (`*_NC`, CC BY-NC-SA 4.0, non-commercial).
related: |
Other Autoware models on Blackhole: the "Autoware" collection of https://huggingface.co/changh95
license:
id: apache-2.0
pipeline_tag: "image-segmentation"
base_model:
- "AutowareFoundation/lidar_frnet"