File size: 14,959 Bytes
51defdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8eeb550
51defdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e91e989
51defdc
 
e91e989
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51defdc
 
 
 
e91e989
8eeb550
e91e989
 
51defdc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8eeb550
 
e91e989
 
8eeb550
 
51defdc
 
 
e91e989
 
51defdc
e91e989
8eeb550
e91e989
 
 
 
 
 
51defdc
e91e989
51defdc
 
 
 
 
8eeb550
51defdc
 
e91e989
 
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
183
184
185
186
# Python API: METEOR (TIER IV, AutowareFoundation/meteor) on Blackhole

Use this API from Python code (a pipeline, a notebook, a ROS 2 node wrapper). You do not need the HTTP server: the
API and the server share the decoders, the device graph and the post-processing, so the outputs and the speed are
the same.

## Install

Install the package on top of an environment that already has `ttnn`: a tt-metal `python_env` at `44d66500520`
with `patches/tt-metal-eth-dispatch.patch` and `patches/tt-metal-reshape-rm-sys1419.patch` applied, or the tt-model
container. From the root of the model repository (the directory that holds `pyproject.toml`, `README.md` and
`code/`):

```bash
pip install -e .                        # the Python API (numpy<2, pillow, pyyaml, onnx, huggingface_hub, safetensors, opencv-python-headless)
pip install -e ".[server,test]"         # + the HTTP server and the tests
```

The pip project is the repository's top-level `pyproject.toml`; it installs the package from `code/tt_meteor` (there
is no `pyproject.toml` inside `code/`, because the container build copies `code/` over the tt-metal tree). `ttnn`
and `torch` are not declared, so pip never replaces tt-metal's own build; tt-metal's `python_env` already has
OpenCV 4.8.1, which satisfies the `opencv-python-headless>=4.8,<4.12` requirement.

The package carries `tt_meteor.ttaw`, the shared code of the Autoware ports to Blackhole (device open, trace runner,
decoders, model base class, HTTP app), vendored at the version recorded in `code/tt_meteor/ttaw/VENDORED.json`
(0.24.1).

| You want to run | Extras |
|---|---|
| the Python API | none |
| the HTTP server (`tt_meteor.server.app`, see `SERVING.md`) | `server` |
| host tests (no device; the device tests are skipped): `TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_meteor/tests` | `server,test` |
| device tests: `python -m pytest -q -s code/tt_meteor/tests/test_pcc_device.py code/tt_meteor/tests/test_e2e_device.py code/tt_meteor/tests/test_variants_device.py` (most of them need the fp32 goldens of the development workspace and skip without them) | `test` |

## Quickstart

```python
from tt_meteor import METEOR, load_sample

with METEOR.from_pretrained(device_id=0) as model:
    out = model(**load_sample("code/tt_meteor/samples/synthetic_8cam.json"))   # 8 cameras + calibration + ego speed + stream
print(out.to_dict()["plan"]["mode"], [d["label"] for d in out.to_dicts()])
```

`examples/quickstart.py` runs the same snippet and writes the `/predict` JSON and a bird's-eye view
(`quickstart_bev.png`). The shipped sample is a synthetic test frame generated by this repository (see
`code/tt_meteor/samples/README.md`); feed your own rig's eight images and calibration for real use.

## `METEOR.from_pretrained(...)`

```python
METEOR.from_pretrained(
    model_id=None,          # HF repo or a local directory with the weights files; default AutowareFoundation/meteor
    *,
    revision=None,          # default for the default repo: the validated commit 01a5f6d71df (tag v1.0)
    variant=None,           # load-time variant: "default" (the only one); default $METEOR_VARIANT
    device_id=None,         # chip to open; default $TT_DEVICE_ID or 0
    device=None,            # an already-opened ttnn device (tt_meteor.device.open_device); close() does not close it
    dispatch=None,          # "eth" (p150 target, 12x10 grid) | "worker" (A/B only, 11x10) | "auto"; default $METEOR_DISPATCH or "eth"
    num_command_queues=None,  # default $METEOR_NUM_CQS or 1 (D14: a second queue gains nothing for synchronous calls)
    weights_dir=None,       # explicit local weights directory; no Hub access
    warmup_variants="default",  # trace variants to capture now; see "Warm-up"
    verbose=False,
    **compile_params,       # load-time knobs (below), e.g. input_norm="imagenet"
) -> METEOR
```

What it does: resolves the weights first, so a Hub problem never claims the chip (`weights_dir` >
`$METEOR_WEIGHTS_DIR` > a local `model_id` directory > the HF snapshot at the pinned revision, restricted to
`meteor_v157c3Z.onnx, meteor_v157.param.yaml, LICENSE, SHA256SUMS`, with an offline fallback to the cache), opens the
chip (ETH dispatch, 12×10, 1 CQ; the other open parameters are `DEVICE_DEFAULTS` in `tt_meteor/device.py`:
`l1_small_size` 32 KiB, `trace_region_size` 256 MiB, overridable with `METEOR_*`), reads the parameters of the ONNX
file by consuming node, builds the graph, then compiles and captures the metal trace. If ETH dispatch cannot open
(tt-metal without the patch), it warns and falls back to WORKER dispatch.

Load-time knobs (`compile_params` in lower case, or the environment variable `METEOR_<NAME>`; read once at load):

| knob | default | meaning |
|---|---|---|
| `input_norm` | `onnx` | `onnx` = the released graph's `/255` (ONNX Runtime parity); `imagenet` = the trained ImageNet mean / std (an upstream export defect); with `imagenet` an absent camera is zeroed after normalising, as in training |
| `depth_mean_bins` | `log` | `depth_mean` bin centres: log-spaced as exported (parity) or `linear` 1 + 1.25 b m (the trained bins) |
| `max_streams` | 16 | host temporal states kept (seg fusion, yaw tracks, mode hysteresis); the least recently used is dropped |
| `profile` | `accurate` | the precision profile (the served default is `fast`, see "Precision profiles"): `accurate` = the image branch, the 3D detection path and the box refiner in fp32 activations with every conv as three bf16 terms; `fast` = reduced precision, 2.7x faster on the device. A knob set explicitly wins over its profile's value |
| `image_precision` | `terms3` (`fast`: `bf16`) | image branch: `terms3` = fp32 activations, every conv as three bf16 terms; `bf16` = single-term bf16 activations with the port's fp32 weights at HiFi4 (bf16 weights at HiFi2 in the 2D detection, traffic-light and 2D segmentation heads) |

## Precision profiles

`METEOR.from_pretrained(..., profile="fast")` (or `METEOR_PROFILE=fast`; `tt serve` uses `fast` unless
`--profile accurate` is given) chooses the precision of the whole graph at load time:

| profile | numerics | device time per frame | judged by |
|---|---|---:|---|
| `fast` (served default) | image branch, det stem and box refiner as single-term bf16 activations (fp32 weights at HiFi4; bf16 weights at HiFi2 in the 2D heads), BEV trunk and heads bf16 activations with bf16 weights at HiFi2 where measured safe, bilinear resizes as one block-sparse kernel | 102.8 ms | task-level agreement with the fp32 CPU reference: 3D boxes recall / precision 0.971 / 0.989, worst planned-path deviation 0.18 m, same plan mode and traffic light on every frame, lane agreement >= 98.6 % (45 public and in-domain frames) |
| `accurate` (Python default) | fp32 activations and three-term convs in the image branch, the 3D detection path and the box refiner; fp32 weights, HiFi4 | 278.6 ms | the per-output agreement gates (PCC, argmax agreement, strict box matching) |

Both keep fp32 accumulation. `fast` moves the dense maps by about 1 % of pixels (lane, 2D segmentation and depth
argmax agreement >= 98.6 / 99.0 / 98.3 % against 99.3 / 99.6 / 99.3 % with `accurate`), so near-threshold boxes and
class borders flip more often; use `accurate` when per-output parity with the fp32 network matters.

## Warm-up

The load runs one eager frame of the whole graph (it compiles every kernel and fills the program cache) and then
captures it as ONE metal trace, `frame`, so no call compiles anything. The first call with a new
calibration also builds and uploads its lift tables (about 0.2 s once; see "Rig changes").
With a warm JIT cache the load takes about 40-46 s (`fast` 41.2 s: weights 0.3 s, graph build 1.3 s, eager frame and
capture 35.6 s; `accurate` 46.1 s). A cold kernel cache compiles for minutes (a new dispatch configuration on the build host:
ETH-2CQ 142 s, WORKER 11×10 768 s). `warmup_variants="none"` skips the capture (the first call then captures).

## Call: `model(...)`

| Argument | Type | Description |
|---|---|---|
| `images` | list or mapping | the cameras `CAM_FRONT_WIDE`, `CAM_FRONT_LEFT`, `CAM_FRONT_RIGHT`, `CAM_BACK_WIDE`, `CAM_BACK_LEFT`, `CAM_BACK_RIGHT`, `CAM_FRONT_NARROW`, `CAM_BACK_NARROW`, any order: `CameraImage`s, dicts `{"camera", "image", "intrinsics", "T_ref_from_camera"}` or `{name: image}`. Raw, unrectified frames of at least 768×432; larger frames are resized with OpenCV INTER_AREA semantics (bit-exact numpy port) and K is scaled per axis. The six wide / corner cameras are required; a missing or all-zero narrow camera is **absent**: a zero image plus its donor's K and pose (`CAM_FRONT_WIDE` / `CAM_BACK_WIDE`), the trained 7-camera configuration |
| `calibration` | dict | `{"cameras": {name: {"intrinsics", "T_ref_from_camera"}}}` (K of the image as sent; camera optical frame -> base_link: x forward, y left, z up, origin on the road) or `{"preset": name}` (`code/tt_meteor/calib/`); inline calibration of a camera wins |
| `ego_speed` | float | m/s, the graph's `v0` (required) |
| `stream` | dict | optional: `{"id", "reset", "timestamp_s", "T_world_from_ego"}` (or `"pose": [x, y, yaw]`): METEOR's host temporal post-processing per stream id (BEV seg fusion needs the pose; yaw smoothing; plan-mode hysteresis). A new id or `reset=True` starts fresh |
| runtime params | keyword | host post-processing only (`METEOR.RUNTIME_PARAMS`, METEOR's C++ renderer defaults): 3D `det3d_threshold` 0.15, `det3d_topk` 64, `vehicle_threshold` 0.35, `vru_threshold` 0.15, `bev_nms_iou` 0.3, `bev_nms_containment` 0.6, `stationary_logit_threshold` 0.0; 2D `det2d_threshold` 0.30, `det2d_topk` 48, `det2d_hide` "7" (road paint); `unk2d` True, `unk2d_threshold` (= det2d), `ground_z` 0.0; plan `mode_hysteresis` 0.35, `straight_margin` 1.0; `seg_fuse` True, `thin_road_edge` True, `yaw_smoothing` True; `heads` False (adds the dense heads to `to_dict("npz")`) |

`tt_meteor.load_sample(path)` turns a sample manifest (`samples/<name>.json`: image paths relative to the file, a
preset, the ego speed and a stream) into these keyword arguments.

### Input types

- Images: a path, PNG / JPEG bytes, a `PIL.Image`, a uint8 H×W×3 RGB array, or a float array in [0, 1].
- Transforms: 4×4 (or 3×4) matrices, `{"translation", "rotation_wxyz"}` (or `rotation_xyzw`), or Autoware
  `{"x", "y", "z", "roll", "pitch", "yaw"}`. Intrinsics: 3×3 K, 3×4 P or `{"fx", "fy", "cx", "cy"}`.

## Output

`model(...)` returns a `MeteorOutput` (`tt_meteor.Output`); `out.to_dict()` is the `/predict` JSON:

| key | content |
|---|---|
| `detections` (`out.to_dicts()`) | 3D BEV boxes sorted by score: `label` (VEHICLE / VRU), `label_id`, `score`, `center` [x, y] and `size` [length, width] in metres (base_link; METEOR predicts no z, height or velocity), `yaw` (rad, CCW from +x), `stationary`, `future` (6 × [x, y] at 0.5 .. 3 s, the agent's best mode), `future_mode` |
| `trajectory`, `columns` | the selected ego path, 6 × [x, y] at 0.5 s steps |
| `plan` | `mode`, `mode_probs` (softmax of the hysteresis-adjusted logits), `mode_logits`, `paths` (3 × 6 × [x, y]), `steer` (rad), `accel` (m/s²), `brake_prob`, `dt` |
| `detections_2d` | per camera: `label` (10 classes), `label_id`, `score`, `box_xyxy` (pixels at 768×432) |
| `unknown_obstacles` | [x, y] of 2D "obstacle" boxes placed on the ground plane (METEOR's `unk2d`) |
| `traffic_light` | `state` (none / green / yellow / red), `state_id`, `probs` |
| `lane`, `lane_classes` | the BEV lane map, uint8 [800, 500] as PNG (0.2 m cells, row 0 = +80 m ahead, column 0 = +50 m left; classes bg, road, sidewalk, crosswalk, laneline, stopline, road_edge, marking, parking), after the optional seg fusion and road-edge thinning |
| `stationary_head_healthy`, `meta` | METEOR's health check of the stationary head; `present` cameras, `v0`, `stream_id` |
| `heads` (`to_dict("npz")` with `heads=True`) | the dense outputs: `seg2d`, `depth` (bins), `depth_mean`, `occupancy` (class per voxel), `risk` (sigmoid), `stationary` |
| `timing_ms` | `preprocess`, `device` (tables + upload + replay + read + conversion), `postprocess`, `total` |

`out.path` is the selected path as an array; `out.lane` the lane map; `out.boxes3d` / `out.boxes2d` the box objects.

## Lifetime and information

- `model.close()` releases the trace and the device tensors and closes the chip if the model opened it; idempotent.
  `with` calls it for you; an unclosed model is closed when Python exits.
- `model.info`: weights (repo, tag, revision, path), device (dispatch, grid), variant, warm variants, warm-up times,
  runtime parameter defaults, the load-time knobs and the trace description.
- Calls from several threads are safe: the device calls are serialised. One model per process per chip.
- **Rig changes.** The lift megakernel's tables (15.8 MB: per BEV cell the cameras that see it, their pixel
  positions and fp32 bilinear and depth-bin weights) are built on the host per calibration (the last 4 rigs are
  cached) and written to the chip only when the calibration changes. A cached rig costs a 5 ms write (call 143 ms
  instead of 136 ms with `fast`, 324 ms instead of 317 ms with `accurate`); a rig seen for the first time also pays the host build once (about 0.2 s). A fixed rig pays it
  once. A rig that puts more than 4 cameras over one 0.4 m lift cell raises `InputError` (HTTP 400); load the model
  with `METEOR_LIFT=stock` for it (any rig, 44 ms slower per frame).

## Speed

Warm, batch 1, ETH dispatch, 1 CQ, 12×10, AICLK 1350 MHz, p50 of 30 calls on the PandaSet 019 sample on a shared
8-core host (load average 4.5-5.0), 2026-10-19 (the device rows do not depend on the input):

| stage | `fast` ms | `accurate` ms |
|---|---:|---:|
| device trace, one replay (the whole network) | 102.80 (back to back 102.77: 9.73 frames/s) | 278.69 (back to back 278.64: 3.59 frames/s) |
| upload of the eight cameras (8 MB uint8, whole image rows) | 1.1 | 1.1 |
| readback + conversion to the 19 outputs (views of the packed segments) | ≈ 18 + 2.4 | ≈ 18 + 2.5 |
| host pre-processing | 3.0 | 3.1 |
| host post-processing (METEOR's C++ decode rules, temporal state) | 9.5-15.6 | 15.3-20.8 |
| `model(...)` end to end | 130.5-136.0 (7.4-7.7 calls/s) | 312.1-317.3 (3.2 calls/s) |

The end-to-end rows move with the load of the shared host.

## Limits

- Batch 1 on the chip; one model per process; eight 768×432 camera slots in METEOR's order, a 400×250 lift grid at
  0.4 m and an 800×500 BEV at 0.2 m (fixed shapes of the released graph); at most 3 cameras per lift cell on the
  rigs validated (the default lift takes up to 4).
- The released graph is camera-only and single-frame (its temporal memory was baked out upstream); the temporal
  behaviour of METEOR's runtime is host post-processing.
- Numerics: bf16 / fp32 on the chip; outputs differ slightly from the fp32 reference with `accurate` and more with
  `fast` ("Precision profiles"; the README's performance tables).