Download code/PYTHON.md from changh95/ptv3-p150: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/changh95/ptv3-p150/resolve/main/code/PYTHON.md
- Command line
-
hf download hf://changh95/ptv3-p150/code/PYTHON.md
-
curl -L -o PYTHON.md https://huggingface.co/changh95/ptv3-p150/resolve/main/code/PYTHON.md
Python API: Point Transformer V3 (Autoware autoware_ptv3) 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, 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
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_ptv3 (there is no
pyproject.toml inside code/, because the container build copies code/ over the tt-metal tree).
The package carries tt_ptv3.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_ptv3/ttaw/VENDORED.json.
| You want to run | Extras |
|---|---|
| the Python API | none |
the HTTP server (tt_ptv3.server.app, see SERVING.md) |
server |
host tests (no device; device tests are skipped): TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_ptv3/tests/test_bundle_host.py code/tt_ptv3/tests/test_host_units.py code/tt_ptv3/tests/test_io_host.py code/tt_ptv3/tests/test_server_host.py (and the other test_*_host.py / reference files; see "Tests") |
server,test |
device tests: python -m pytest -q -s code/tt_ptv3/tests/test_pcc_device.py code/tt_ptv3/tests/test_e2e_device.py code/tt_ptv3/tests/test_e2e_seg_device.py |
test |
Quickstart
Run it from the model repo root: code/tt_ptv3/samples/test_pcd.npz (Autoware's test.pcd, Apache-2.0) is a path
relative to it.
from tt_ptv3 import PTv3
with PTv3.from_pretrained(device_id=0) as model: # variant "seg_det": both heads
out = model("code/tt_ptv3/samples/test_pcd.npz")
print(out.class_counts()) # input points per class
print(out.detections.to_dicts()) # the boxes (None with variant="seg")
body = out.to_dict() # the same JSON as POST /predict
examples/quickstart.py runs this and also writes quickstart_bev.png (tt_ptv3.viz.render_bev: the points coloured
by class, the boxes, from above).
PTv3.from_pretrained(...)
PTv3.from_pretrained(
model_id=None, # HF repo or a local directory with the weights files; default AutowareFoundation/ptv3
*,
revision=None, # default for the default repo: the validated commit 1673104843a (tag v4.0)
variant=None, # load-time variant: "seg_det" (encoder + seg head + det head, traces A and B) or "seg"
# (encoder + seg head, trace A: the autoware_launch wiring); default $PTV3_VARIANT or "seg_det"
device_id=None, # chip to open; default $TT_DEVICE_ID or 0
device=None, # an already-opened ttnn device (tt_ptv3.device.open_device); close() does not close it
dispatch=None, # "eth" (p150 target, 12x10 grid) | "worker" (A/B only, 11x10) | "auto"; default $PTV3_DISPATCH or "eth"
num_command_queues=None, # default $PTV3_NUM_CQS or 1
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, # none for this model (the capacity buckets are knobs, see "Warm-up")
) -> PTv3
What it does: resolves the weights first, so a Hub problem never claims the chip (weights_dir >
$PTV3_WEIGHTS_DIR > a local model_id directory > the HF snapshot at the pinned revision, restricted to
ptv3_encoder.onnx, ptv3_seg3d_head.onnx, ptv3_det3d_head.onnx, ml_package_ptv3_*.param.yaml, deploy_metadata.yaml, 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_ptv3/device.py, overridable with PTV3_*), builds the
graph, then compiles and captures the metal trace of every warm-up variant. If ETH dispatch cannot open (tt-metal
without the patch), it warns and falls back to WORKER dispatch.
Warm-up
from_pretrained returns a warm model: it reads the three ONNX graphs (tt_ptv3.reference.weights, BN and the CPE
linear folded exactly), uploads the weights, runs each trace once eagerly (this first run compiles the kernels) and
captures it as a metal trace, so no call compiles anything. The graph is captured once per capacity bucket
(PTV3_BUCKETS=ladder, PTV3_POOL_PROFILES=p2,p3: 15 stage-0 capacities from 16k to 224k voxels, each with two
deep-stage profiles, plus the node's 256k maximum n256k; 31 buckets, one trace A encseg_<bucket> and, for
seg_det, one trace B det_<bucket> each, sharing the weights). Each call runs in the cheapest bucket whose five
stage capacities hold the frame's stage counts, chosen on the host (tt.model.select_bucket; meta.frame.stage_counts
in the output); the outputs do not depend on the bucket (bit-identical to n256k on the 22 gate frames).
- Measured (2026-10-15, ETH dispatch, 1 CQ, 12x10, warm JIT cache,
VERIFICATION_OPT_2026-10-15.md): 57-64 s forseg_detwith the whole ladder (graph build and weight upload 12.5 s, eager warm-up + capture of the 62 traces 40-47 s, the rest the device open). A process with an empty JIT cache compiles every kernel of every bucket first (tens of minutes for the full ladder);PTV3_BUCKETS=32k,80k(a list of stage-0 capacities, + n256k) trims the ladder and the load. The traces are captured during the load, so the first call costs as much as the next. - The load prints one stderr line per warm-up variant (
[ptv3-p150/seg_det] warm-up 33/62 encseg_n96k +12.5 s). On a cold cache the lines come minutes apart for the large buckets (compiling); a load whose last line is many minutes old with no new kernels in the JIT cache is stalled. warmup_variants="none"(server:PTV3_WARMUP=none) skips the capture at load; callmodel.warmup()before the first frame (it is idempotent).- The JIT cache is tt-metal's (
~/.cache/tt-metal-cache, orTT_METAL_CACHE); inside the container it is~/.cache/tt-model/ptv3-p150/cacheof the host.
Call: model(points, **kwargs)
| Argument | Type | Description |
|---|---|---|
points (first positional) |
path, bytes + fmt=, (N, C) array / tensor, structured array, PointCloud, JSON envelope |
one concatenated LiDAR cloud in base_link with x, y, z, intensity (see "Input types") |
run_detection |
bool, default True |
run trace B (the det head) and return boxes; False skips it (same segmentation). The node runs a head only when its output topic has a subscriber. No effect with variant="seg" (no det head) |
score_threshold |
float in [0, 1], default 0.1 | the node's per-distance-bin score threshold (all four bins are 0.1 in its config) |
reconstruction |
"full" (default) | "partial" | "none" |
which points get an output row (the node's source_reconstruction at @5eb9286): full every input point in input order (out-of-crop points INVALID); partial the points inside the crop, in input order; none one row per voxel (its first point) |
neighbour_mode |
"alias-first" (default) | "exact" |
how the host builds the sub-manifold neighbour tables: spconv's linear-key aliasing of out-of-range top-z voxels, as the TensorRT engine builds it (the lowest-index voxel wins an aliased key), or exact keys. Only the host tables change; both are validated against their own CPU goldens under the same gates |
min_voxels |
int in [0, 256000], default 16000 | the node's minimum: below it the node publishes nothing, and so does the model (meta["rejected"], empty output, no device run). 0 disables it (every single-sweep nuScenes key-frame has 10-16k voxels) |
Every knob is host-only (PLAN.md 2.7, RT-host): no device shape changes. Defaults reproduce node @5eb9286's
config/ptv3.param.yaml and the weights' ml_package files. A bad value raises tt_ptv3.io.InputError (HTTP 400)
before the device runs. sweeps, stream, images, calibration and inputs (the shared call signature of the
Autoware ports) are refused: the v4.0 node reads one concatenated cloud, with no densification and no state.
Input types
- Point clouds: a path (
.binraw float32 rows -- passfields=if notx, y, z, intensity;.npy;.npz;.pcdascii / binary / binary_compressed), raw bytes withfmt=, an (N, C) numpy array or torch tensor, a structured array, the JSON envelope of/predict, ortt_ptv3.io.PointCloud. Frame:base_link, metres. An unnamed (N, C) array is x, y, z, intensity, ...: the first four columns are used (Autoware's concatenatedXYZIRC...layouts start with them). intensityis Autoware's uint8 value, 0-255: the feature isintensity / 255(the node's uint8-intensity path).- The node's crop is x, y in [-122.88, 122.88) m and z in [-3, 5) m (base_link); points outside it get label 255. Voxels are 0.12 m; the first point of a voxel (in input order) is its feature, so point order matters; at most 256,000 voxels (the rest is clipped, as the node does).
Output
model(...) returns a tt_ptv3.PTv3Output (a ttaw.outputs.Segmentation3D); out.to_dict() is exactly the
POST /predict body (SERVING.md section 3.2).
| field | type | meaning |
|---|---|---|
label_ids |
uint8 [N] |
the PTv3 class per output point (class_names: car, truck, bus, bicycle, pedestrian, traffic_cone, barrier, debris, drivable_flat, non_drivable_flat, vegetation, building, vertical_thin, static_clutter, noise); 255 = outside the crop. With reconstruction="full", N = input points, in input order |
scores |
float32 [N] |
the probability of that class (softmax over 15); 0 for invalid points |
classification |
uint8 [N] |
Autoware's PointCloudClassification value of the node's class_mapping (CAR 0, TRUCK 1, BUS 2, BICYCLE 4, PEDESTRIAN 5, HAZARD 7, FLAT_SURFACE 8, STRUCTURE 9, VEGETATION 10, NOISE 11; INVALID 255) |
entropy |
float32 [N] |
the normalised entropy of the 15 probabilities, -sum p ln p / ln 15; NaN for invalid points |
filter_keep |
bool [N] |
membership of the node's ~/output/pointcloud/filtered (the obstacle cloud): False for drivable_flat, non_drivable_flat and noise; points outside the crop are kept |
detections |
Detections3D or None |
boxes float32 [M, 7] (x, y, z = box centre, length, width, height, yaw in base_link), scores, label_ids / labels (CAR, TRUCK, BUS, BICYCLE, PEDESTRIAN, TRAFFIC_CONE, BARRIER), velocity [M, 2], meta["autoware_labels"] (the ObjectClassification name the node publishes: CAR, TRUCK, BUS, BICYCLE, PEDESTRIAN; TRAFFIC_CONE and BARRIER become HAZARD; also autoware_label per box in the JSON), after the score threshold, the yaw-norm gate, IoU-BEV NMS and the area class remapper; None with run_detection=False or variant="seg" |
meta |
dict | frame (points, points in range, voxels, stage counts, neighbour mode, rejected / clipped / overflow flags), neighbour_mode, reconstruction; rejected (the reason) when the node would publish nothing |
timing_ms |
dict | preprocess (decode, voxelization, serialization, neighbour maps), device (tables, H2D, trace A (+ B), D2H, unpack), postprocess, total of this call |
out.class_counts() gives the points per class (key "255" for invalid points); out.detections.to_dicts() the
box list [{"label", "label_id", "score", "center", "size", "yaw", "velocity"}].
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), variant, warm variants, warm-up times, runtime parameter defaults.- Calls from several threads are safe: the device calls are serialised. One model per process per chip.
Speed
Warm calls, batch 1, ETH dispatch, 1 CQ, 12x10, the shipped precision and knobs, variant seg_det
(code/scripts/bench.py, 20 iterations, mean of the p50s of 2 processes; VERIFICATION_OPT_2026-10-15.md, an
independent re-measurement of the optimized code, 2026-10-15, on a shared host whose load moves the host stages; the
first release's numbers are in OPT_BASELINE.md):
| stage, p50 | test.pcd (shipped; bucket n32k_p3) |
PandaSet 019 f40 (not shipped; bucket n80k) |
|---|---|---|
model(path) end to end, seg_det |
72.8 ms | 164.4 ms |
model(path, run_detection=False) (no trace B) |
62.8 ms | 141.9 ms |
| host pre-processing (decode, voxelize, serialize, pooling metadata, numba neighbour maps) | 25.8 ms | 59.4 ms |
| device tables + host tensors + H2D | 5.6 + 4.5 + 3.5 ms | 11.3 + 6.9 + 6.8 ms |
| trace A (encoder + seg head), one replay | 20.96 ms | 51.20 ms |
| trace B (det head), one replay | 9.18 ms | 9.53 ms |
| D2H A + B | 0.42 + 0.21 ms | 0.72 + 0.20 ms |
| host post-processing (reconstruction, LUT, filter; decode, NMS, remap) | 2.9 ms | 14.8 ms |
- Back-to-back replays take 30.0 ms per frame on test.pcd (A 20.9 + B 9.2; 33.3 frames/s of device throughput) and 60.6 ms on the PandaSet frame (16.5 frames/s); the first release took 885 ms for any cloud. A synchronous call gives 13.7 (test.pcd) and 6.1 (PandaSet) calls/s.
- Trace A grows with the cloud: it runs at the selected bucket's capacities, so a denser frame (the T4 frames
have 96k-132k voxels) runs in a larger bucket and takes longer; the host pre-processing grows with the point count
too.
OPT_REPORT.mdhas every optimization step, its A/B and the remaining backlog.
Limits
- Batch 1 on the chip; one model per process; the largest capacity bucket is the node's maximum (256,000 / 179,200 /
107,520 / 53,760 / 27,136 voxels per stage): voxels beyond 256k are clipped as the node does, a frame whose pooled
stages exceed a capacity is
refused (
InputError), never truncated. - Frames below 16,000 voxels give the node's empty output unless
min_voxelsis lowered. - Outputs differ slightly from the fp32 reference (README "Demo & Performances": 99.65-99.89 % of point labels agree; boxes near the score threshold or an NMS / top-k near-tie can appear or disappear).
- One p150, ETH dispatch (12x10);
dispatch="worker"is an A/B opt-in (11x10). Not a ROS 2 node.
Tests
From the repository root, with code/ on PYTHONPATH (or the package installed). Name the test files; never run
pytest on the whole directory on a shared device host.
- Host (no device, no ttnn call):
TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_ptv3/tests/test_bundle_host.py code/tt_ptv3/tests/test_host_units.py code/tt_ptv3/tests/test_io_host.py code/tt_ptv3/tests/test_server_host.py. Inside the tt-models workspace the suite also runstest_tt_host.py,test_tt_fake_host.py(the whole graph on a fake ttnn),test_reference_cpu.pyandtest_host_vs_research.pyagainst workspace oracles (ONNX ReferenceEvaluator, ONNX Runtime, the research reference); tests that need workspace-only data skip elsewhere, and a missing sample FAILS inside the workspace. - Device (one p150):
python -m pytest -q -s code/tt_ptv3/tests/test_pcc_device.py code/tt_ptv3/tests/test_e2e_device.py code/tt_ptv3/tests/test_e2e_seg_device.py(per-module PCC against the fp32 CPU reference fed identical host tables, on replay outputs; end-to-end agreement on 21 frames in both neighbour modes and both variants; the samples against their stored references; replay == eager; API ==/predict). The frame gates run only inside the tt-models workspace, where their data lives (T4 sample-dataset, PandaSet and nuScenes frames, which this repository does not ship); outside it only the shippedtest.pcdchecks run. - The gates are frozen in
tests/*.gates.jsonat the first green run and are never loosened.