File size: 13,450 Bytes
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be62f78
4d9b003
 
 
 
 
 
 
 
 
be62f78
4d9b003
be62f78
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be62f78
4d9b003
 
 
 
 
 
 
 
be62f78
4d9b003
be62f78
4d9b003
be62f78
 
 
 
 
 
 
 
 
 
 
4d9b003
 
 
 
 
be62f78
 
 
4d9b003
 
 
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
# Python API: Diffusion Planner v5.0 (Autoware diffusion_planner) 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 trace 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/`):

```bash
pip install -e .                        # the Python API (numpy<2, pillow, pyyaml, onnx, huggingface_hub)
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_diffusion_planner` (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.

The package carries `tt_diffusion_planner.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_diffusion_planner/ttaw/VENDORED.json`.

| You want to run | Extras |
|---|---|
| the Python API | none |
| the HTTP server (`tt_diffusion_planner.server.app`, see `SERVING.md`) | `server` |
| host tests (no device; device tests are skipped): `TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_diffusion_planner/tests` | `server,test` |
| device tests: `python -m pytest -q -s code/tt_diffusion_planner/tests/test_pcc_device.py code/tt_diffusion_planner/tests/test_e2e_device.py` | `test` |

## Quickstart

```python
from tt_diffusion_planner import DiffusionPlanner

with DiffusionPlanner.from_pretrained(device_id=0) as model:
    out = model(inputs="code/tt_diffusion_planner/samples/kashiwanoha_dense.npz")
print(out.to_dict())   # the POST /predict body
```

`examples/quickstart.py` runs the same snippet, writes `quickstart.json` and a bird's-eye view of the input and the
plan (`quickstart_bev.png`).

## `DiffusionPlanner.from_pretrained(...)`

```python
DiffusionPlanner.from_pretrained(
    model_id=None,          # HF repo or a local directory with the weights files; default AutowareFoundation/diffusion_planner
    *,
    revision=None,          # default for the default repo: the validated commit 423efde67f5 (tag v5.0)
    variant=None,           # "default" (the only v5.0 graph); default $DIFFUSION_PLANNER_VARIANT or "default"
    device_id=None,         # chip to open; default $TT_DEVICE_ID or 0
    device=None,            # an already-opened ttnn device (tt_diffusion_planner.device.open_device); close() does not close it
    dispatch=None,          # "eth" (p150 target, 12x10 grid) | "worker" (A/B only, 11x10) | "auto"; default $DIFFUSION_PLANNER_DISPATCH or "eth"
    num_command_queues=None,  # default $DIFFUSION_PLANNER_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,
    precision=None,         # the only compile parameter: extra precision-policy rules, e.g. "dec.*=HiFi2+fp32" (experiments only)
) -> DiffusionPlanner
```

What it does: resolves the weights first, so a Hub problem never claims the chip (`weights_dir` >
`$DIFFUSION_PLANNER_WEIGHTS_DIR` > a local `model_id` directory > the HF snapshot at the pinned revision, restricted to
the three v5.0 ONNX files and `diffusion_planner.param.json`, with an offline fallback to the cache; the sha256 of every
file and the weights' `major_version == 5` are checked), opens the chip (ETH dispatch, 12×10, 1 CQ; the other open
parameters are `DEVICE_DEFAULTS` in `tt_diffusion_planner/device.py`, overridable with `DIFFUSION_PLANNER_*`), reads the
ONNX initializers as data, uploads the weights and constants (48.1 MB), builds the graph, then compiles and captures
the metal traces. If ETH dispatch cannot open (tt-metal without the patch), it warns and falls back to WORKER dispatch
(`model.info["device"]["fallback"]` names it). Any other keyword argument is a `TypeError`.

The numerics are not arguments: the published configuration is the default of the `DIFFUSION_PLANNER_LN_FP32`,
`_SPLIT_MATMUL`, `_ATTN_MATMUL`, `_HIDDEN_FP32` and `_ATTN_FP32_ACC` knobs (`tt_diffusion_planner.tt.config.KNOBS`,
pinned in `tt-model.yaml` `serve.env`). Setting one of them in the environment changes the graph and invalidates the
accuracy figures of the card until the gates are re-run.

## Warm-up

`from_pretrained` returns a warm model: it builds the graph, runs the plan once eagerly (this first run compiles every kernel into the JIT cache), then captures the whole plan as metal traces (`warmup_variants="default"`: the full-capacity variant `plan` and one variant per agent bucket, `plan_r32`, `plan_r64`, `plan_r96`, `plan_r128`, `plan_r192`; each call replays the smallest that holds the scene, exact) with program-cache misses forbidden, so no later call compiles anything. `model.warmup()` is idempotent; `warmup_variants="none"` defers the capture to `model.warmup()`.

Measured on the shipped sample (`model.info["warmup_ms"]`, 2026-10-11): the load takes 352 s with an empty JIT cache and 12.9 s with a warm one (build 0.76 s: the ONNX initializers read and 118.9 MB of weights and constants uploaded; warm-up and capture of the 6 traces 7.7 s; the rest is the device open). The first call then takes 37 ms and the second 32 ms (an `.npz` path; the stage bench's steady state: 27 ms p50 for decoded arrays, 32 ms for an `.npz` path). The 6 traces hold 60.3 MB of DRAM (`trace_region_size` 192 MiB).

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

| Argument | Type | Description |
|---|---|---|
| `inputs` | mapping / `.npz` path / bytes / JSON envelope | the 15 raw planner tensors (see "Input types") |
| `velocity_smoothing_window` | int, 1..79, default 8 | forward moving average of the trajectory velocity, in points |
| `stopping_threshold` | float >= 0, default 0.3 | force stop below this smoothed speed (m/s), when the ego moves |
| `turn_indicator_keep_offset` | float, default -1.25 | added to the KEEP logit before the turn-indicator decision |
| `return_denoising_steps` | bool, default False | add the ego row of the 11 solver iterates (`out.meta["denoising_steps"]`, `[11, 81, 4]`, the node's `~/debug/denoising_steps`) |

### Input types

- `inputs=`: the 15 raw tensors of the Autoware node's `DiffusionPlannerCore::create_input_data()` (batch 1,
  float32, ego `base_link` frame, BEFORE normalization; names and shapes in `tt_diffusion_planner.INPUT_SCHEMA`):
  a `{name: array}` mapping (numpy or torch), an `.npz` path or its bytes, or the `/predict` envelope
  `{"format": "npz", "data": <base64>}` / `{"format": "json", "arrays": {...}}`. Names, shapes and finite values are
  checked (`InputError`). `tt_diffusion_planner.load_inputs(source)` is the same decoder.
- Any other input (`points`, `images`, `calibration`, ...) is refused (`InputError`).

### What the caller keeps (the API is stateless)

One call is one independent plan. The Autoware node keeps state between plans; to reproduce it over a sequence of
plans, the caller keeps the same state (SERVING.md 3.5 has the details):

- **The tensors.** The node's pre-processing from ROS messages and the Lanelet2 map (per-UUID agent buffers and their
  0.1 s resampling, the ego history, lane / route / polygon / line-string selection and encoding, traffic lights,
  speed limits, goal, turn-indicator report history) is not part of the bundle.
- **The turn-indicator hold window** (`turn_indicator_hold_duration`, 1.0 s in the node's YAML). Each call decides
  with a fresh manager; apply the node's hold across calls with the node's own manager:

  ```python
  from tt_diffusion_planner.host.postprocess import TurnIndicatorManager

  manager = TurnIndicatorManager()                     # hold 1.0 s, KEEP offset -1.25 (the node's YAML)
  out = model(inputs=tensors)
  decision = manager.evaluate(out.turn_indicator["logits"], stamp_s=now_s, prev_report=int(tensors["turn_indicators"][0, 30]))
  command = decision.command                           # the held command while less than 1.0 s has passed
  ```

- **The initial solver state** `sampled_trajectories` (`x_T`, normalised space): zeros is the node's default
  (`temperature: [0.0]`); for a temperature > 0 send N(0, 1) x temperature; for the RTC prefix (`delay_step` > 0) put
  the previous plan into the ego row, slots t = 0 .. delay_step (x as (x - 10) / 20, y as y / 20, cos / sin as they are,
  in the current ego frame). `delay` is accepted and ignored (the node's multi-step mode never reads it).
- **The map frame.** Outputs are in `base_link`; the node transforms them with the current ego pose to `map`.

## Output

`model(...)` returns a `tt_diffusion_planner.Output` (= `ttaw.outputs.Trajectory`); `out.to_dict()` is exactly the `POST /predict` body (SERVING.md section 3.2).

| field | type | meaning |
|---|---|---|
| `poses` | float32 `[80, 7]` | the ego trajectory at 0.1-8.0 s in `base_link`: x, y, yaw, cos, sin, velocity, acceleration (`out.columns`), post-processed like the node's `~/output/trajectory` |
| `turn_indicator` | dict | `command` (0 NO_COMMAND, 1 DISABLE, 2 ENABLE_LEFT, 3 ENABLE_RIGHT), `command_name`, `keep_selected`, `held` (always false: no hold window), the 5 raw `logits` (NONE, DISABLE, LEFT, RIGHT, KEEP), the decision's `probabilities` |
| `predicted_agents` | float32 `[N, 80, 5]` | x, y, yaw, cos, sin of each non-empty neighbour row, in input order |
| `meta` | dict | `predicted_agent_rows` (the rows of `predicted_agents`), `predicted_agent_columns`, `force_stop`, `time_from_start_s`, `valid_counts` (the entities the encoder saw), and with `return_denoising_steps` the encoded `denoising_steps` |
| `timing_ms` | dict | `preprocess`, `device` (host tensors + H2D + replay + D2H), `postprocess`, `total` |

`out.to_dicts()` gives one `{x, y, yaw, cos, sin, velocity, acceleration}` dict per trajectory point; `out.to_dict("npz")` adds the poses as a lossless base64 NPZ array.

## Lifetime and information

- `model.close()` releases the traces and the persistent 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 input schema, the numerics options and precision policy in effect, the
  trace (variants, persistent inputs, trace buffers in MB).
- 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, 12×10, the pinned numerics (`code/scripts/bench.py`, 100 iterations, 2026-10-11, the optimized release, on a shared host; p50, with p99 in brackets):

| stage | shipped sample `kashiwanoha_dense` (88 neighbours: bucket r96) |
|---|---:|
| `.npz` decode + schema check (path inputs only) | 5.76 (7.33) ms |
| host pre-processing (the node's normalization, the encoder's host features, masks, `x_T`) | 2.45 (3.16) ms |
| packing the persistent trace inputs · ttnn host tensors · H2D | 0.52 · 1.57 · 0.76 ms |
| **device trace, one blocking plan** | **20.05** (20.08) ms |
| D2H (one packed read) | 0.22 ms |
| host post-processing (trajectory, predicted paths, turn decision) | 1.39 (1.84) ms |
| **`model(inputs=arrays)` end to end** | **26.73** (27.48) ms |
| `model(inputs=<.npz path>)` | 32.27 (33.61) ms |
| back-to-back replays (device time per plan) | 20.00 ms = 50.0 plans/s |

The device time depends on the agent bucket the scene needs (exact compaction: the smallest of 32 / 64 / 96 / 128 / 192 decoder rows that holds the ego and every valid neighbour row, else the full capacity). Back to back: r32 (≤ 31 neighbours, e.g. `straight_road`) 17.44 ms, r64 (a nuScenes instant with 42 neighbours) 19.25 ms, r96 20.00 ms, r128 21.11 ms, r192 22.30 ms, full capacity (> 191) 26.65 ms (the last three: `OPT_REPORT.md` round 5). The map entities are computed at full capacity in every plan. The first release took 102.04 ms for every scene (`OPT_BASELINE.md`). 2 CQs were not re-measured on this release (at the first release they did not help a synchronous request: `OPT_BASELINE.md`). Where the time goes and what comes next: `OPT_REPORT.md`.

## Limits

- Batch 1 on the chip; one model per process.
- 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), so the device time depends on the
  neighbour count (17.4-26.7 ms); the map entities are computed at full capacity.
- The node's guidance services (start / stop / centerline guidance) are not available (the node's default is off).
- Accuracy is agreement with the fp32 CPU reference of the same network (README "Demo & Performances"); the planner's
  driving quality is the weights' (trained by TIER IV on data that is not public).