File size: 13,783 Bytes
fe51a63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 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"