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
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/):
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
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(...)
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:
- It finds the weights before it claims the chip:
weights_dir>$BEVDET_WEIGHTS_DIR> a localmodel_iddirectory > the Hugging Face snapshot at the pinned revision, restricted tobevdet_one_lt_d.onnx(305 MB), with an offline fallback to the cache. The repo is public: if your HF cache holds a token, setHF_HUB_DISABLE_IMPLICIT_TOKEN=1so none is sent (the container does). - 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_DEFAULTSintt_bevdet/device.py, overridable withBEVDET_*). 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. - 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.CameraImages. 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 OpenCVINTER_LINEAR(the node'scv::resize), and their K is scaled with them unlessscale_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}(Autowaresensor_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=Trueadds 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=Trueis a scene change: every history slot is replaced by the current frame.T_world_from_ego(4×4 pose ofbase_linkin 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.withcalls 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, andtest_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).