|
Download code/PYTHON.md from changh95/bevdet-p150: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/changh95/bevdet-p150/resolve/main/code/PYTHON.md
- Command line
-
hf download hf://changh95/bevdet-p150/code/PYTHON.md
-
curl -L -o PYTHON.md https://huggingface.co/changh95/bevdet-p150/resolve/main/code/PYTHON.md
14.9 kB
| # Python API: BEVDet (Autoware tensorrt_bevdet) 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 are the same bit for | |
| bit (`tests/test_e2e_device.py::test_api_equals_server`) and the speed is 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`, 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, opencv-python-headless<4.12) | |
| 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_bevdet` (there | |
| is no `pyproject.toml` inside `code/`, because the container build copies `code/` over the tt-metal tree). ttnn and | |
| torch come from tt-metal and are not declared, so pip never replaces tt-metal's torch. | |
| The package carries `tt_bevdet.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_bevdet/ttaw/VENDORED.json`. | |
| | You want to run | Extras | | |
| |---|---| | |
| | the Python API | none | | |
| | the HTTP server (`tt_bevdet.server.app`, see `SERVING.md`) | `server` | | |
| | host tests (no device; device tests are skipped): `TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_bevdet/tests` | `server,test` | | |
| | device tests: `python -m pytest -q -s code/tt_bevdet/tests/test_pcc_device.py code/tt_bevdet/tests/test_e2e_device.py code/tt_bevdet/tests/test_streams_device.py` | `test` (most gates need the tt-models workspace data; outside it they skip) | | |
| ## Quickstart | |
| ```python | |
| from tt_bevdet import BEVDet, load_sample | |
| with BEVDet.from_pretrained(device_id=0) as model: | |
| out = model(**load_sample("code/tt_bevdet/samples/synthetic_6cam.json")) # six cameras + calibration preset | |
| for d in out.to_dicts(): | |
| print(d["label"], d["autoware_label"], d["score"], d["center"], d["size"], d["yaw"], d["velocity"]) | |
| ``` | |
| The relative path assumes the current directory is the repository root. `examples/quickstart.py` finds the sample | |
| relative to its own location (it runs from any directory) and also writes the `/predict` JSON and a bird's-eye view | |
| PNG to `examples/output/`. The shipped sample is a synthetic test pattern (eight objects placed by its generator: | |
| `code/tt_bevdet/samples/README.md`); use your own rig's images and calibration for anything else. | |
| `from_pretrained` returns a model that is ready: the load ends with one synthetic frame through the whole path (see | |
| "Warm-up"). The first call on a new rig calibration builds that calibration's tables, and the first image of a new | |
| non-1600×900 size checks the host resize against OpenCV once, so it takes longer: 0.18 s (nuScenes), 0.54 s (the | |
| shipped sample) and 1.6 s (PandaSet 1920×1080) measured; the later calls take the times under "Speed". A load takes | |
| about 14-15 s with a warm kernel cache on the measurement host. The first load on an empty kernel cache also compiles | |
| the kernels (a few minutes). `verbose=True` shows the progress. | |
| ## `BEVDet.from_pretrained(...)` | |
| ```python | |
| BEVDet.from_pretrained( | |
| model_id=None, # HF repo or a local directory holding bevdet_one_lt_d.onnx; default AutowareFoundation/tensorrt_bevdet | |
| *, | |
| revision=None, # default for the default repo: the validated commit 391466349a1 (tag v1.0) | |
| variant=None, # "default" (the only variant) | |
| device_id=None, # chip to open; default $TT_DEVICE_ID or 0 | |
| device=None, # an already-opened ttnn device (tt_bevdet.open_device); close() does not close it | |
| dispatch=None, # "eth" (p150 target, 12x10 grid) | "worker" (A/B only, 11x10) | "auto"; default $BEVDET_DISPATCH or "eth" | |
| num_command_queues=None, # default $BEVDET_NUM_CQS or 1 (2 is selectable: uploads on CQ1; no gain for one stream) | |
| weights_dir=None, # explicit local weights directory; no Hub access | |
| warmup_variants="default", # "default" captures every trace now; "none" defers it to model.warmup() | |
| verbose=False, | |
| # load-time knobs (default: $BEVDET_<NAME>, else the value below; SERVING.md section 3.5): | |
| max_streams=4, # stream ids that keep a BEV history at once (LRU eviction; one 25 MB device ring bank each) | |
| history_stride=1, # frames per history push (1 = the Autoware node; > 1 is not device-validated yet) | |
| iou_mode="vendor", # rotated IoU of the Scale-NMS: "vendor" (deployed) | "exact" (polygon clipping) | |
| precision="HiFi4+fp32+l1acc:w=fp32", # conv / linear precision (the release policy: fp32 weights, bf16 activations) | |
| ) -> BEVDet | |
| ``` | |
| What it does: | |
| 1. It finds the weights before it claims the chip: `weights_dir` > `$BEVDET_WEIGHTS_DIR` > a local `model_id` | |
| directory > the Hugging Face snapshot at the pinned revision, restricted to `bevdet_one_lt_d.onnx` (305 MB), with | |
| an offline fallback to the cache. The repo is public: if your HF cache holds a token, set | |
| `HF_HUB_DISABLE_IMPLICIT_TOKEN=1` so none is sent (the container does). | |
| 2. It opens the chip as every published number was measured: ETH dispatch, 1 command queue, the 12×10 compute grid | |
| (read from the device, never hard-coded); L1_SMALL 32 KiB and a 256 MiB trace region (`DEVICE_DEFAULTS` in | |
| `tt_bevdet/device.py`, overridable with `BEVDET_*`). If ETH dispatch cannot open (tt-metal without the patch), it | |
| warns and falls back to WORKER dispatch (11×10); the card's numbers do not apply to that mode. | |
| 3. It reads the ONNX initializers as data (no ONNX Runtime), folds BatchNorm into the convs and applies the port's | |
| exact rewrites (ASPP dilation-18 branch as a 1×3 conv, the camera-aware MLP + SE folded into per-calibration gates, | |
| a merged CenterHead), then builds the device graph: two trace segments around the host BEVPool (`encode`, `decode`) | |
| and the history-ring updates (`push`, `push_x`, `fill`). | |
| ## Warm-up | |
| `warmup_variants="default"` compiles every program, captures the seven trace variants (`encode`; `decode` and its `decode_fresh` / `decode_move` forms; the ring updates `push`, `push_x`, `fill`) and runs one synthetic frame through | |
| the whole host + device path (the warm-up stream is forgotten afterwards), so no real call compiles anything; only | |
| the first call on a new rig calibration builds that calibration's tables (above). The shapes are fixed (six cameras, 256×704 network input, 128×128 BEV), so there is nothing else to | |
| warm: any input image size works without a new compile (non-1600×900 images are resized on the host, like the node). | |
| `model.warmup()` is idempotent; with `warmup_variants="none"` call it before the first frame (a call without captured | |
| traces raises). | |
| ## Call: `model(...)` | |
| | Argument | Type | Description | | |
| |---|---|---| | |
| | `images` | list or dict | The six cameras `CAM_FRONT_LEFT`, `CAM_FRONT`, `CAM_FRONT_RIGHT`, `CAM_BACK_LEFT`, `CAM_BACK`, `CAM_BACK_RIGHT`, in any order: a list of `{"camera": name, "image": <image>, "intrinsics": K, "T_ref_from_camera": T}` dicts (or `"data"`: base64, the `/predict` spelling), a `{name: image}` mapping, or `tt_bevdet.io.CameraImage`s. Each camera exactly once. | | |
| | `calibration` | dict | `{"cameras": {name: {"intrinsics", "T_ref_from_camera"}}}` or `{"preset": "<name>"}` (files in `code/tt_bevdet/calib/`); values given with an image win. `"scale_intrinsics": False` keeps the raw K when an image is resized to 1600×900 (the node's quirk); the default `True` scales it. | | |
| | `stream` | dict | `{"id": "default", "reset": False, "timestamp_s": None, "T_world_from_ego": None}`: the BEV history (below) | | |
| | `score_threshold` | float 0..1 | 0.2: the node's publication filter | | |
| | `decode_score_threshold` | float 0..1 | 0.1: the vendor decode keeps cells with sigmoid(max logit) above it | | |
| | `nms_iou_threshold` | float 0..1 | 0.2: the Scale-NMS rotated BEV IoU | | |
| | `max_pre_nms`, `max_detections` | int 1..500 | 500, 500: the pre-NMS cap and the post-NMS cap | | |
| | `nms_rescale_factor` | list of 10 or dict | the Scale-NMS factor per class (default `[1.0, 0.7, 0.7, 0.4, 0.55, 1.1, 1.0, 1.0, 1.5, 3.5]`, class order of `BEVDet.LABELS`), or `{"pedestrian": 2.0}` to override some; each in (0, 100] | | |
| Unknown keyword arguments or values out of range raise `tt_bevdet.io.InputError` (the server's 400). | |
| ### Input types | |
| - Images: a path, PNG / JPEG bytes, a `PIL.Image`, a uint8 HxWx3 RGB array or 3xHxW tensor, or a float array in | |
| [0, 1]. Any size: images that are not 1600×900 are resized to it with OpenCV `INTER_LINEAR` (the node's | |
| `cv::resize`), and their K is scaled with them unless `scale_intrinsics=False`. | |
| - Intrinsics: 3×3 K, 3×4 P or `{"fx", "fy", "cx", "cy"}` of the image **as sent** (rectified rigs: P[0:3, 0:3]). | |
| - Extrinsics `T_ref_from_camera`: camera optical frame (x right, y down, z forward) -> `base_link`, as a 4×4 / 3×4 | |
| matrix, `{"translation", "rotation_wxyz"}` (nuScenes / bevdet_vendor yaml) or `{x, y, z, roll, pitch, yaw}` | |
| (Autoware `sensor_kit_calibration.yaml`). | |
| - `tt_bevdet.load_sample(path, with_stream=False)` reads a sample manifest (`{"images": {camera: relative path}, | |
| "calibration": {...} | {"preset": name}, "stream": {...}}`) into these keyword arguments; `with_stream=True` adds | |
| the manifest's stream (ego pose). | |
| ### Streams: the BEV history | |
| - A new `stream["id"]` starts like a freshly launched Autoware node: zero history on its first frame. | |
| - `reset=True` is a scene change: every history slot is replaced by the current frame. | |
| - `T_world_from_ego` (4×4 pose of `base_link` in a fixed world frame) turns on motion compensation: each history | |
| slot is warped into the current ego frame. Without it the history is never aligned, exactly as in the deployed | |
| node. Pass the pose whenever you have a localisation: on nuScenes mini_val it is worth +0.11 mAP / +0.16 NDS | |
| (README "Demo & Performances"). | |
| - `max_streams` (default 4) ids keep their history at once; a further id evicts the least recently used one, whose | |
| next frame starts fresh. Switching the active stream swaps a 25 MB device bank (save + load: 1.07 ms on the | |
| device, measured). | |
| - A frame that raises after the device ring changed restarts its stream (next frame: zero history), so the device | |
| ring and the host bookkeeping never disagree; a frame that raises earlier leaves the history untouched. | |
| - `model.stream_info()` returns the table (`max_streams`, `history_stride`, policy, the ids that hold a history). | |
| ## Output | |
| `model(...)` returns a `tt_bevdet.host.outputs.BEVDetDetections` (a `ttaw.outputs.Detections3D`): | |
| | attribute | meaning | | |
| |---|---| | |
| | `boxes` | float32 [N, 7]: x, y, z (box centre; z = gravity centre), length, width, height, yaw (CCW from +x), `base_link` metres / radians | | |
| | `scores` | float32 [N], sorted descending | | |
| | `label_ids` | int32 [N], index into `labels` | | |
| | `labels` | the 10 network class names (`car`, `truck`, `construction_vehicle`, `bus`, `trailer`, `barrier`, `motorcycle`, `bicycle`, `pedestrian`, `traffic_cone`) | | |
| | `autoware_labels` | per class, the label the node publishes (UNKNOWN for construction_vehicle, barrier, traffic_cone); `autoware_label_names` gives it per row | | |
| | `velocities` | float32 [N, 2]: (vx, vy) m/s in `base_link` axes | | |
| | `twists()` | float64 [N, 2]: the node's (twist.linear.x, twist.angular.z) | | |
| | `timing_ms` | `preprocess`, `encode` (H2D + `encode` trace + D2H), `bevpool` (host BEVPool + AlignBEV grid), `bev_decode` (H2D + `decode` trace + D2H + ring update), `device` (the three), `postprocess`, `total` | | |
| | `meta` | `class_names`, `autoware_labels`, `score_threshold`, `stream` (`id`, `frame`, `flag`) | | |
| `out.to_dicts()` gives one dict per detection (`label`, `label_id`, `autoware_label`, `score`, `center`, `size`, | |
| `yaw`, `velocity`, `twist`); `out.to_dict()` gives the `/predict` body, `out.to_dict("npz")` adds lossless arrays. | |
| ## Lifetime and information | |
| - `model.close()` releases the traces 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, CQs, fallback), variant, warm variants, | |
| warm-up times, runtime parameter defaults, the trace layout, the knobs and the stream table. | |
| - Calls from several threads are safe: the device calls are serialised. One model per process per chip. | |
| ## Speed | |
| Measured with `code/scripts/bench.py` (OPT_REPORT.md, VERIFICATION_OPT_2026-10-14.md; ETH dispatch, 1 CQ, 12×10 | |
| grid, warm, batch 1; decoded images in, so JPEG / PNG decoding is not included): | |
| | | p50 | | |
| |---|---:| | |
| | device, one steady frame (`encode` + `decode` + `push` back to back) | about 10.0 ms (about 100 frames/s device-bound; 14.7 ms with ego poses) | | |
| | `model(...)` on 1600×900 images (synthetic, Autoware mode) | about 31 ms | | |
| | `model(...)` on nuScenes 1600×900 with ego poses | about 39-40 ms | | |
| | `model(...)` on PandaSet 1920×1080 (host resize) | about 34-36 ms | | |
| The first release took 34.15 ms on the device and 173 / 220 / 342 ms per `model(...)` call on the same inputs. The | |
| host stages (pre-processing, BEVPool, Scale-NMS) are now numba twins and the images go up as uint8 crops, so the | |
| host and the device share the time about equally; the host stages move with the load of the shared measurement host. | |
| Decode your images in a separate thread to hide the JPEG cost. | |
| ## Tests | |
| - Host (no device): `TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_bevdet/tests` (with the fake ttnn of the | |
| workspace: `-p fake_ttnn_plugin`, SERVING.md section 1). | |
| - Device: `python -m pytest -q -s code/tt_bevdet/tests/test_pcc_device.py code/tt_bevdet/tests/test_e2e_device.py | |
| code/tt_bevdet/tests/test_streams_device.py`. The gates (`tests/*.gates.json`) were frozen at the first green run and | |
| are never loosened. The public-data gates need the tt-models workspace (nuScenes, PandaSet); outside it they skip, | |
| and `test_shipped_sample_agreement` (the container smoke's comparison) runs anywhere. | |
| ## Limits | |
| - Batch 1 on the chip; one model per process; six cameras, 256×704 network input, 118 depth bins, 128×128 BEV grid | |
| (±51.2 m at 0.8 m), 8 history frames, 10 classes. A rig whose BEVPool tables exceed M 370,000 / N 14,000 is | |
| refused (`InputError`), as TensorRT would. | |
| - BEVPool and the Scale-NMS run on the host (numba, bit-identical to the reference order); a device BEVPool | |
| (`BEVDET_BEVPOOL=device`) is experimental, not served, and hung the chip in one of its forms (OPT_REPORT.md round 7). | |