Download SERVING.md from changh95/diffusion-planner-p150: direct link, hf CLI and curl.
- Browser
- Download file 23 kB
-
https://huggingface.co/changh95/diffusion-planner-p150/resolve/main/SERVING.md
- Command line
-
hf download hf://changh95/diffusion-planner-p150/SERVING.md
-
curl -L -o SERVING.md https://huggingface.co/changh95/diffusion-planner-p150/resolve/main/SERVING.md
Serving Diffusion Planner v5.0 (Autoware diffusion_planner) on Blackhole with tt-model-manager
This repo is the authoring source of the tt-model container package changh95/diffusion-planner-p150
(kind: tt-dit-server, schema 5.1): tt-model.yaml is the manifest, code/ the port, and
code/tt_diffusion_planner/server/app.py the ASGI app uvicorn runs. Weights are a pinned pointer, never baked into the
image. The device open, the decoders, the Python-API contract and the HTTP contract come from
code/tt_diffusion_planner/ttaw/, the shared package of the Autoware ports vendored into this repo
(ttaw/VENDORED.json records its version, source commit and file hashes; it is not edited here).
| item | value |
|---|---|
| tt-metal tree | <workspace>/tt-metal (main 44d66500520, v0.80.0-dev20261006-78-g44d6650052, + patches/tt-metal-eth-dispatch.patch + patches/tt-metal-reshape-rm-sys1419.patch; torch 2.11.0+cpu) |
| weights | AutowareFoundation/diffusion_planner @ 423efde67f5414734da43a7ad856c17ceb8b51aa (tag v5.0; files diffusion_planner_encoder.onnx, diffusion_planner_decoder.onnx, diffusion_planner_turn_indicator.onnx, diffusion_planner.param.json; 58.9 MB; Apache-2.0; public, ungated; sha256 of every file checked at load) |
| Autoware reference | planning/autoware_diffusion_planner @ autoware_universe 9ceaccf026c31ffc5319bc9eeb4bd7bede0af3fd (package 0.53.0), the node's multi_step mode |
| shared package | ttaw 0.23.2 (common @ 60b6dd7) |
| app | tt_diffusion_planner.server.app:app (uvicorn; the ASGI lifespan does weights -> device -> graph -> trace capture) |
| device recipe | ttnn.open_device(device_id, dispatch_core_config=DispatchCoreConfig(ETH), l1_small_size=32768, trace_region_size=192 MiB, num_command_queues=1) (DEVICE_DEFAULTS in code/tt_diffusion_planner/device.py; the 6 traces hold 60.3 MB) |
| graph | ONE metal trace per plan: the encoder (6 MLP-Mixer trunks, small encoders, 6 fusion blocks), the cross-attention K / V hoisted once, 11 DiT evaluations with the 10 fp32 DPM-Solver++(2M) updates and the prefix constraint, the turn-indicator head, one packed readback. Six traces are captured at startup, the full capacity and one per agent bucket (32 / 64 / 96 / 128 / 192 decoder rows); each request replays the smallest that holds its valid neighbour rows (exact). 1,418 device programs at r96 (6,282 in the first release); custom generic_op kernels in code/tt_diffusion_planner/tt/kernels/ |
| hardware | one Blackhole p150 (hardware: p150, mesh_device: P150, TT_MESH_SHAPE=1x1), 12×10 compute grid |
1. Run on the HOST (hardware validation, no Docker)
The tt-metal python_env has ttnn and torch; bin/tt-env.sh puts the workspace's dev overlay (fastapi, uvicorn, onnx)
on PYTHONPATH instead of installing into it. On the shared workspace box every command that opens the chip goes
through the device lock (bin/devrun).
ROOT=<workspace>
source $ROOT/bin/tt-env.sh # TT_METAL_HOME, PYTHONPATH (+ the dev overlay: fastapi, uvicorn, onnx), python_env
cd $ROOT/bundles/diffusion-planner-p150
export PYTHONPATH=$PWD/code:$PYTHONPATH
export HF_MODEL=AutowareFoundation/diffusion_planner TT_MODEL_WEIGHTS_REVISION=423efde67f5414734da43a7ad856c17ceb8b51aa TT_MESH_SHAPE=1x1
# host tests (no device; the device tests are skipped; ttnn is the fake of common/tests/host: a real ttnn host
# tensor opens the chip, so only devrun jobs may build one)
PYTHONPATH=$ROOT/research/packaging/scripts:$PYTHONPATH TT_VISIBLE_DEVICES=none \
python -m pytest -q -p no:cacheprovider -p fake_ttnn_plugin code/tt_diffusion_planner/tests
# device tests (per-module PCC + end-to-end agreement; 99 scenes with the workspace goldens, the 2 shipped samples elsewhere)
$ROOT/bin/devrun -t 1800 -- python -m pytest -q -s code/tt_diffusion_planner/tests/test_pcc_device.py code/tt_diffusion_planner/tests/test_e2e_device.py
# the server, with the serve pins of tt-model.yaml, and the container smoke test against it
$ROOT/bin/devrun -t 1800 -- bash -c 'env DIFFUSION_PLANNER_DISPATCH=eth DIFFUSION_PLANNER_NUM_CQS=1 \
python -m uvicorn --host 127.0.0.1 --port 20000 --lifespan on tt_diffusion_planner.server.app:app & \
S=$!; python3 code/tt_diffusion_planner/server/smoke_test.py --url http://127.0.0.1:20000 --wait 1200; R=$?; kill -TERM $S; wait $S; exit $R'
Boot log landmarks (they drive tt-model serve's progress view, boot_progress.py TT_DIT_PHASES):
Loading weights -> Opening device -> Warming up: capturing trace ... (once per trace variant) -> Warmup complete -> uvicorn
Application startup complete. A cold JIT cache compiles every kernel first (352 s measured for
from_pretrained); later boots take about 13 s to READY (measured on the host, warm JIT cache). The packaged container measured the same on 2026-10-11: tt-model serve READY after 6 min 0 s on its first start (server boot 358 s, empty JIT cache) and after 20.9 s on the next one (server boot 19.0 s). Startup failures raise and uvicorn exits non-zero (no CPU
fallback). SIGTERM: the lifespan releases the traces and closes the chip (within tt-model stop's 120 s budget).
Offline override (no Hub): DIFFUSION_PLANNER_WEIGHTS_DIR=<dir holding diffusion_planner_encoder.onnx, diffusion_planner_decoder.onnx, diffusion_planner_turn_indicator.onnx, diffusion_planner.param.json>.
2. Package, serve, push (Docker)
Docker 29 on this box is rootful (/var/lib/docker), and the user's session may not carry the docker group:
wrap every docker-using command in sg docker -c "..." (no docker-env.sh is needed here; the reference bundles'
docker-env.sh was the authors' rootless-Docker box). Run every tt-model command from this directory
(source.tt_metal and extra_code[].root: code are CWD-relative) and point --out outside it.
ROOT=<workspace>; cd $ROOT/bundles/diffusion-planner-p150
TTM=$HOME/.local/share/uv/tools/tt-model/bin/python
# offline validation (seconds; no docker, no device) -- must print VALID
$TTM -c "from tt_kernel.container_manifest import load_container_manifest as L; m=L('tt-model.yaml', check_sources=True); p=m.resolve_profile(); print('VALID', m.name, m.kind, p.hardware, p.mesh_device, m.weights_ref)"
python3 $ROOT/research/packaging/scripts/check_bundle.py . # conventions (add --stage publish before a push)
# build (one at a time on the box: the absolute lock file): ~35 min cold, ~12 min after a code/ change (the C++
# build re-runs, ccache-warm, whenever code/ changes), ~1 min for a manifest- or lock-only change
flock $ROOT/.package.lock sg docker -c "tt-model package --container tt-model.yaml --out $ROOT/build" # log: ~/.cache/tt-model/build/diffusion-planner-p150.log
# provenance: the base-image digests this build resolved (tt-model's FROM tags float) -> build_info.json (published)
sg docker -c "python3 $ROOT/research/packaging/scripts/record_build_info.py --staged $ROOT/build/diffusion-planner-p150 --bundle ."
# serve + smoke + stop, all inside ONE device-lock window (there is one serve profile, the default; serve returns
# once READY and leaves the container running; the script's trap saves the container log and always runs
# `tt-model stop`, also when devrun's timeout fires: -k 150 > the 120 s grace). No token reaches the public weights
# download. Evidence: logs/smoke/ (--log-dir).
$ROOT/bin/devrun -t 3600 -k 150 -- env -u HF_TOKEN -u HUGGING_FACE_HUB_TOKEN \
sg docker -c "bash code/scripts/container_smoke.sh $ROOT/build/diffusion-planner-p150 20000"
# publish: fill the provenance from the staged manifest, run the publish gate, overlay the hand-written docs, the pip
# project and the build record onto the staged dir, then push (HF_TOKEN only from the environment)
python3 $ROOT/research/packaging/scripts/instantiate_bundle.py --values $ROOT/research/diffusion-planner/bundle_values.json \
--fill . --manifest $ROOT/build/diffusion-planner-p150/tt_kernel_manifest.json
python3 $ROOT/research/packaging/scripts/check_bundle.py . --stage publish
rsync -a README.md SERVING.md OPT_BASELINE.md OPT_REPORT.md VERIFICATION_*.md tt-model.yaml pyproject.toml build_info.json \
examples media patches $ROOT/build/diffusion-planner-p150/
cp code/PYTHON.md $ROOT/build/diffusion-planner-p150/PYTHON.md
tt-model push $ROOT/build/diffusion-planner-p150 --publish
The smoke test fails unless /info reports ETH dispatch, the 12x10 grid and what the staged package pins
(DIFFUSION_PLANNER_DISPATCH, DIFFUSION_PLANNER_NUM_CQS, DIFFUSION_PLANNER_VARIANT, the weights revision), and it
compares the served trajectory of the shipped sample with the stored CPU-reference output
(code/tt_diffusion_planner/samples/kashiwanoha_dense.reference.json: average displacement <= 0.3 m, final
displacement <= 1.0 m). On the host, the same smoke test against uvicorn with the serve pins passed:
PASS diffusion-planner-p150: profile=- variant=default dispatch=eth grid=12x10 cqs=1 n=80 turn=DISABLE reference=kashiwanoha_dense.reference.json (trajectory ade=0.0109 fde=0.0173; predicted_agents max_abs=6.283) device_ms=109.402 total_ms=133.384 rtt_ms=139.0
(predicted_agents max_abs is reported, not gated. Here it is the yaw column at ±π: -3.1413 vs +3.1415
rad, the same heading. The neighbour paths are gated in the end-to-end tests, on the per-agent maximum displacement.)
serve publishes the first free port from 20000 and exports into the container: HF_MODEL=AutowareFoundation/diffusion_planner,
MESH_DEVICE=P150, TT_MESH_SHAPE=1x1, TT_MODEL_WEIGHTS_REVISION=423efde67f5414734da43a7ad856c17ceb8b51aa, then serve.env. The HF cache is
mounted at /hf (rw), the JIT cache at /cache (host ~/.cache/tt-model/diffusion-planner-p150/cache), and the container
sees only its own chip (--device /dev/tenstorrent/<n>). tt-cli users: tt serve changh95/diffusion-planner-p150 /
tt model stop changh95/diffusion-planner-p150 (point tt at this tt-model with
tt config set tools.override.tt-model ~/.local/bin/tt-model or TT_TOOL_BIN_TT_MODEL).
What push does to the repo: code/ and image/ on the Hub become exactly the staged trees (extra_code.paths +
models/common/lightweightmodule.py); every top-level file of the staged dir is uploaded (the overlaid README
replaces the generated card; pyproject.toml, PYTHON.md and build_info.json arrive the same way, never through
code/); top-level files already on the Hub and not in the staged dir are kept.
3. The request / response contract
| route | purpose |
|---|---|
GET /health |
{"status": "ok" | "starting" | "error", "model", "device": {"dispatch", "grid", "cores", "device_id"}, "error"}, always 200; ok only after warm-up |
GET /v1/health |
same as /health (tt-model's hint for non-chat packages points here) |
GET /info |
model, task, io, Autoware package + commit, weights {repo, tag, revision, path, license}, device (dispatch, grid, CQs, fallback), the input schema (the 15 tensors with shapes and dtypes), labels (turn-indicator logit order), variant, warm variants, runtime params + defaults, the numerics options and precision policy in effect, the trace (variants, persistent inputs, trace buffers), warm-up and boot times |
GET /v1/models |
{"object": "list", "data": [{"id": "AutowareFoundation/diffusion_planner", "object": "model", "owned_by": "changh95"}]} -- a stub so OpenAI-shaped probes do not 404; NOT a chat API |
POST /predict |
one plan -> the model output (below) |
3.1 Request (application/json; unknown fields are a 422)
| field | type | meaning |
|---|---|---|
inputs |
object | the planner tensors: {"format": "npz", "data": <base64 .npz>} or {"format": "json", "arrays": {name: nested list}} with exactly the 15 raw tensors of the node's create_input_data(): sampled_trajectories [1,321,81,4], ego_agent_past [1,31,4], ego_current_state [1,10], neighbor_agents_past [1,320,31,11], static_objects [1,5,10], lanes [1,140,20,33], lanes_speed_limit [1,140,1], route_lanes [1,25,20,33], route_lanes_speed_limit [1,25,1], polygons [1,10,40,3], line_strings [1,60,20,4], goal_pose [1,4], ego_shape [1,3], turn_indicators [1,31], delay [1,1]; float32, ego (base_link) frame, BEFORE normalization; names, shapes and finite values are checked (400) |
params |
object | per-request knobs (host post-processing only): velocity_smoothing_window (8, 1..79), stopping_threshold (0.3 m/s), turn_indicator_keep_offset (-1.25), return_denoising_steps (false) |
output_format |
str | json (default) or npz (adds the poses as a lossless base64 NPZ array) |
Base64 may be standard or URL-safe, wrapped, with or without a data: prefix. python3 code/tt_diffusion_planner/server/client.py --inputs <scene.npz> [--param name=value] --out req.json builds the request
with the standard library only (no numpy); --url http://127.0.0.1:20000 sends it.
3.2 Response (200)
The served body of the shipped sample (uvicorn on the host with the serve pins; trajectory cut to 3 of 80 rows and the predicted-agents array to its header):
{
"model": "diffusion-planner-p150",
"frame_id": "base_link",
"meta": {"predicted_agent_columns": ["x", "y", "yaw", "cos", "sin"], "predicted_agent_rows": [0, 1, 2, "... 85 more"], "force_stop": false, "time_from_start_s": [0.1, 0.2, 0.3, "... 77 more"], "valid_counts": {"ego": 1, "neighbor": 88, "static": 0, "lane": 123, "route": 17, "polygon": 0, "line_string": 60, "goal": 1, "ego_shape": 1, "turn": 1}},
"timing_ms": {"preprocess": 2.4, "device": 22.8, "postprocess": 1.2, "total": 36.9, "decode": 8.4, "model_call": 26.7},
"num_poses": 80,
"columns": ["x", "y", "yaw", "cos", "sin", "velocity", "acceleration"],
"trajectory": [[0.3644, 0.01, -0.0169, 0.9964, -0.0169, 3.8033, -0.0392], [0.7626, -0.0021, -0.0412, 0.9968, -0.0412, 3.7994, -0.5496], [1.1517, -0.0281, -0.0674, 0.9942, -0.0672, 3.7444, -0.4683], "... 77 more rows"],
"turn_indicator": {"command": 1, "command_name": "DISABLE", "keep_selected": true, "held": false, "logits": [-15.960197448730469, -5.119836807250977, -5.23490047454834, -0.7636525630950928, 5.502235412597656], "probabilities": [1.655269699085693e-09, 8.448460721410811e-05, 7.530191942350939e-05, 0.00658634165301919, 0.9931545257568359]},
"predicted_agents": {"format": "npz", "key": "predicted_agents", "dtype": "float32", "shape": [88, 80, 5], "data": "<base64 npz>"}
}
trajectory: 80 points at 0.1-8.0 s inbase_link(the ego frame of the input tensors), post-processed like the node's~/output/trajectory: velocity from consecutive 3-D points / 0.1 s, forward moving average overvelocity_smoothing_windowpoints, force stop (poses frozen once the smoothed speed falls belowstopping_thresholdwhile the ego moves), acceleration by finite difference.yawis whattf2::getYawreads from the node's (unnormalised) quaternion of the raw networkcos/sin, which are returned as they are.turn_indicator:command0 NO_COMMAND, 1 DISABLE, 2 ENABLE_LEFT, 3 ENABLE_RIGHT (when the head selects KEEP, the command repeats the last input reportturn_indicators[0, 30]); the 5 rawlogits(NONE, DISABLE, LEFT, RIGHT, KEEP) and the decision'sprobabilities. No hold window is applied (see 3.5).predicted_agents: one 80-point path (x, y, yaw, cos, sin) per non-empty neighbour row, in input order (meta.predicted_agent_rowsgives the rows).meta:force_stop,time_from_start_s,valid_counts(entities the encoder saw), and withreturn_denoising_stepsthe ego row of the 11 solver iterates (denoising_steps,[11, 81, 4], the node's~/debug/denoising_steps).
timing_ms: decode (base64 + npz parsing + the schema check), preprocess (the node's normalization and the encoder's
host features), device (host tensors + H2D + one trace replay of the request's agent bucket + D2H), postprocess, model_call, total (server
side, after the body arrived).
3.3 Errors
400 undecodable or malformed input (bad base64, a missing or unknown tensor, a wrong shape, a non-finite value,
unknown or out-of-range params), 422 schema violation (unknown field, wrong type), 503 while starting (or
when the boot failed), 500 inference failed: <ExceptionType>: <message> if the device call raises.
3.4 Environment the app reads (lifespan only, never at import)
| var | set by | meaning / default |
|---|---|---|
HF_MODEL |
launcher (weights.repo) |
weights repo id; default AutowareFoundation/diffusion_planner |
TT_MODEL_WEIGHTS_REVISION |
launcher (weights.revision) |
pinned commit; TT_WEIGHTS_REVISION (serve.env) is the same for older tt-model |
DIFFUSION_PLANNER_WEIGHTS_DIR |
you (offline) | local weights directory; overrides the Hub |
TT_MESH_SHAPE |
launcher (runtime.mesh_shape_env) |
1x1; anything else -> RuntimeError at startup |
TT_DEVICE_ID |
you | chip to open, default 0 |
DIFFUSION_PLANNER_DISPATCH |
serve.env |
eth (default) | worker (A/B only, 11×10) | auto (ETH if the patch is present) |
DIFFUSION_PLANNER_NUM_CQS |
serve.env |
1 (at the first release 2 CQs replayed ~10 % slower on ETH, OPT_BASELINE.md; not re-measured since) |
DIFFUSION_PLANNER_VARIANT |
serve.env |
default (the only v5.0 graph) |
DIFFUSION_PLANNER_LN_FP32, _SPLIT_MATMUL, _ATTN_MATMUL, _HIDDEN_FP32, _ATTN_FP32_ACC |
serve.env |
the numerics knobs, pinned at their gated defaults (enc.mixer.*,dec.*; enc.island.*,enc.pre.*,dec.*; enc.fusion.attn,dec.*; empty; empty: tt/config.py KNOBS); changing one changes the numerics and needs the gates re-run |
DIFFUSION_PLANNER_ENC_CH2D, _ATTN_FAST, _DEC_MMCFG, _LN_KERNEL, _LN_RESID, _SPLIT_KCAT, _ATTN_SMASK, _ATTN_SMSM, _ATTN_FUSED, _KCAT_EMIT, _LN_TR, _KCAT_L1, _ATTN_L1, _DEC_L1, _ENC_KCAT, _KCAT_ACT, _LN_SPLIT, _LN_SFPU_BCAST, _COMPACT, _AGENT_BUCKETS, _HOST_FAST, _INPUT_TRIM (and the rejected _KCAT_ACT_ONCE, _ENC_L1, _FUS_L1, _LIN_ACT = 0) |
serve.env |
the optimization knobs, pinned at the shipped defaults (each one an A/B of OPT_REPORT.md; tt/config.py KNOBS); the gates were run with exactly these values |
DIFFUSION_PLANNER_PRECISION |
serve.env |
empty: the precision policy DEFAULT_PRECISION of tt/config.py (HiFi4 + fp32 accumulation everywhere); extra rules such as dec.*=HiFi2+fp32 are for experiments |
DIFFUSION_PLANNER_WARMUP |
you | JSON list of warm-up variants, default or none |
DIFFUSION_PLANNER_TRACE_REGION, DIFFUSION_PLANNER_L1_SMALL, DIFFUSION_PLANNER_WORKER_L1_SIZE |
you | device-open overrides (validated values: DEVICE_DEFAULTS in device.py) |
DIFFUSION_PLANNER_MAX_BODY_MB |
you | request size guard, default 256 (the shipped sample's request is 0.15 MB of base64 npz) |
TT_METAL_VISIBLE_DEVICES, MESH_DEVICE, HF_HUB_DISABLE_IMPLICIT_TOKEN |
serve.env / launcher |
0, P150, 1 (the weights are public: no token is ever sent) |
Server-side pipeline: JSON -> tt_diffusion_planner.io decoders (schema check) -> DiffusionPlanner.__call__ (the node's
host pre-processing ported from planning/autoware_diffusion_planner -> H2D into the persistent device inputs ->
execute_trace of the plan's agent-bucket trace -> one D2H -> the node's host post-processing) under one lock -> Output.to_dict().
3.5 What the client keeps (the API is stateless)
Every request is one independent plan. The node keeps state between plans; a client that wants its behaviour over a sequence of plans keeps the same state and puts it into the request or applies it to the response:
- The input tensors. Converting ROS messages and the Lanelet2 map into the 15 tensors is the client's: the per-UUID agent buffers with their 0.1 s resampling, the ego history, lane / route / polygon / line-string selection and encoding, traffic-light states, speed limits, the goal and the turn-indicator report history.
- The turn-indicator hold window. The node holds its last non-KEEP command for
turn_indicator_hold_duration(1.0 s in its YAML). The server evaluates every request with a fresh manager (no held command). To reproduce the node, keep the last non-KEEP command and its time, and return it instead while less than 1.0 s has passed; the node's manager ships in the package (tt_diffusion_planner.host.postprocess.TurnIndicatorManager, fed withturn_indicator.logits, the plan time and the last report). - The initial solver state
x_T(sampled_trajectories, normalised space). Zeros is the node's default (temperature: [0.0]). With a temperature > 0 the node draws N(0, 1) x temperature for every element (a freshstd::random_deviceseed per call); send that noise insampled_trajectories. With the RTC prefix (delay_step> 0, at most 40) the node also writes its previous plan into the ego row, slots t = 0 .. delay_step: the previous poses re-expressed in the current ego frame,xas (x - 10) / 20,yas y / 20,cos/sinas they are. Non-zerox_Twas checked on the device against ONNX Runtime (noise of scale 0.5 and 1.0, an RTC prefix: within the gates,VERIFICATION_2026-10-08.md). delayis accepted and ignored, as in the node's multi-step mode.- The map frame. The output is in
base_link(the ego frame of the inputs); the node transforms it tomapwith the current ego pose before publishing.
4. Caveats
- Batch 1, one plan per request; concurrent clients queue on the lock.
- Fixed shapes of the v5.0 export (320 neighbours, 140 lanes, 25 route lanes, 10 polygons, 60 line strings, 31 history and 80 future steps) and 10 DPM-Solver steps (11 decoder evaluations) are compiled into the traces. The neighbour trunk and the decoder run on the smallest agent bucket that holds the scene (exact; one trace per bucket), so the device time depends on the neighbour count (17.4-26.7 ms per plan); the map entities are computed at full capacity.
- The node's guidance services (start / stop / centerline guidance) are off (the node's default too); a guided mode would need the host in the solver loop.
- Warm-up captures the traces inside the lifespan, so READY means warm; the first cold boot pays the ttnn JIT.
- Weights are pinned by sha in
weights.revision, exported by the launcher and repeated inserve.env;snapshot_download(..., revision=<sha>)is a cache hit afterserve's pre-download and falls back tolocal_files_only=Trueif the Hub is unreachable. The sha256 of the four files andmajor_version == 5are checked at load. - The runtime image has no host C/C++ compiler: device kernels JIT-compile (sfpi ships in the image); the host code is numpy.
tt-model curland the ready card's/v1/modelshint are OpenAI-shaped and are not this API; use the routes above.