changh95 commited on
Commit
e6cb218
·
verified ·
1 Parent(s): 3eefc09

tt-model authoring files

Browse files
Files changed (1) hide show
  1. SERVING.md +159 -99
SERVING.md CHANGED
@@ -1,112 +1,172 @@
1
- # Serving this port with tt-model-manager
2
 
3
- This repo carries everything needed to build a **tt-model container bundle** from it, so
4
- `tt-model pull` / `tt-model serve` and `tt-cli` can run it. Do this on a
5
- **Blackhole host** (amd64 Linux + Docker >= 25).
 
 
6
 
7
- ## Why not on a Mac
 
 
 
 
 
 
8
 
9
- `tt-model package --container` builds from `ghcr.io/.../ubuntu-22.04-dev-amd64` and a cold
10
- build is 2.5-4 hours. Packaging and pushing need **no card** (`require_host(need_devices=False)`),
11
- but they do need amd64. `tt-model serve` needs `/dev/tenstorrent/*`.
12
 
13
- ## Steps
 
 
 
 
14
 
15
  ```bash
16
- # 0. get tt-model (not on PyPI)
17
- git clone https://github.com/tenstorrent/tt-model-manager && cd tt-model-manager
18
- python -m venv .venv && source .venv/bin/activate && pip install -e .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- # 1. pull THIS repo
21
- hf download changh95/rf-detr-blackhole --local-dir rf-detr-blackhole && cd rf-detr-blackhole
 
22
 
23
- # 2. point the manifest at your tt-metal checkout
24
- $EDITOR tt-model.yaml # source.tt_metal: /path/to/tt-metal
25
 
26
- # 3. finish the serving adapter
27
- $EDITOR code/rf_detr/server/app.py # see "VERIFY ON HARDWARE" markers
28
 
29
- # 4. validate with no hardware and no build
30
- python -c "from tt_kernel.container_manifest import ContainerManifest as M; M.load('tt-model.yaml')"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- # 5. build, prove, publish
33
- tt-model package --container tt-model.yaml
34
- tt-model serve build/rf-detr-blackhole/tt_kernel_manifest.json
35
- tt-model push build/rf-detr-blackhole --publish
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  ```
37
 
38
- Step 5's `--publish` implies `--public` and adds the `tt-model-catalog` tag, which is what
39
- lists it in the community catalog.
40
-
41
- > **Run every `tt-model` command from this repo's root.** `source.extra_code[].root: code`
42
- > is resolved against the **current working directory**, not against the YAML's location --
43
- > verified: loading the same manifest from a parent directory fails with
44
- > `source.extra_code lists 1 path(s) that do not exist under code`. `cd` into the repo first.
45
-
46
- ## What the bundle push does to this repo
47
-
48
- - **`code/` and `image/` are replaced wholesale** (`_prune_removed` in `tt_kernel/hub.py`).
49
- The port code staged here is exactly what `extra_code` re-stages, so it is replaced, not
50
- duplicated. Files under `code/` that the allowlist does not ship get pruned.
51
- - **`README.md` is overwritten** by the generated card. The text you want to keep belongs in
52
- `card.description` / `card.quickstart` in `tt-model.yaml` -- it is already seeded there.
53
- - **Root files survive**, which is why `media/` and this file live at the root.
54
-
55
- ## The serving contract
56
-
57
- `kind: tt-dit-server` requires only `hardware` + `mesh_device` (no `max_num_seqs` /
58
- `block_size` -- those are vLLM engine settings). `runtime.app` is an ASGI target
59
- `"module:attr"` whose **top-level package must appear in an allowlist entry**, which is why
60
- `runtime.app` and `source.extra_code[0].paths` have to stay in sync. The kind installs
61
- fastapi / uvicorn / pydantic / pillow; `runtime.packages` adds to that set.
62
-
63
- Despite the name, this kind is not diffusion-only -- `jashansinghTT/s2-pro-blackhole` is a
64
- TTS model using it. It simply means "the model's own ASGI app under uvicorn".
65
-
66
- ## The catalog tags are automatic -- do not add them by hand
67
-
68
- `tt-model-cache`, `tt-model-container` and `tt-model-catalog` are written by the real flow,
69
- not by editing this card:
70
-
71
- | tag | written by | source |
72
- |---|---|---|
73
- | `tt-model-cache` | `package` | `TT_MODEL_TAG` in `tt_kernel/__init__.py:9` |
74
- | `tt-model-container` | `package` | `build.py:996` -- `{TT_MODEL_TAG, m.arch, m.kind, "tt-model-container"}` |
75
- | `blackhole`, `tt-dit-server`, `p150` | `package` | same line, from `arch` / `kind` / each profile's `hardware` |
76
- | `tt-model-catalog` | `--publish` | `cli.py:620` / `cli.py:820` |
77
-
78
- So finishing step 5 produces exactly the tag set the catalog models carry. Adding them to
79
- this repo now would be actively harmful, because **nothing validates them**:
80
-
81
- - `tt-model-cache` is what `tt-model search` filters on (`hub.py:268`), so the repo would
82
- show up as an installable bundle and `tt-model pull` would then fail -- there is no
83
- `tt_kernel_manifest.json` here, only the authoring `tt-model.yaml`.
84
- - `tt-model-container` asserts a v5.1 OCI image in `image/`, which does not exist yet.
85
- - `tt-model-catalog` lists it in the public community index, which renders each entry by
86
- reading `tt_kernel_manifest.json`.
87
- - `tt-model publish <id>` does **not** check that the target is a real bundle -- it only
88
- flips visibility and sets the tag -- so the guard rail you might expect is not there.
89
-
90
- Until the bundle is built, the honest tag set is the one this card already carries
91
- (`tenstorrent`, `blackhole`, `p150`, `ttnn`, `tt-metal`, plus the task tags).
92
-
93
- ## Known sharp edges (offline-validated, not yet hardware-validated)
94
-
95
- The manifest loads clean (`load_container_manifest(..., check_sources=False)` passes, which
96
- includes the launcher's own `validate()`), but two things can only be settled on the box:
97
-
98
- 1. **`code/` becomes the image's ONLY `models` package.** `source.code: [models/common]` is
99
- staged from tt-metal into `code/models/common`, and an `extra_code` path of `models`
100
- merges the port's own tree into the same `code/models/`. tt-metal's `models/` has no
101
- `__init__.py`, so this relies on namespace packages resolving. Check the import in
102
- `verify.sh` output on the first build.
103
- 2. **Generically-named top-level packages.** Repos whose package is `models`, `tt`, or
104
- `common` put a very common name at the root of the image's import path. If an import
105
- collides, rename the package in the port repo (and update `runtime.app` +
106
- `source.extra_code[].paths` together -- the launcher cross-checks them).
107
-
108
- `source.code` needs at least one tt-metal-relative entry even when all real code comes from
109
- `extra_code`; `models/common` is used as that minimum. If your port genuinely imports more
110
- from tt-metal (`locate-anything` needs `models/tt_transformers` and
111
- `models/demos/qwen25_vl`), list it there instead -- under-listing fails the image's own
112
- build-time import check on your machine, which is the cheap place to find out.
 
1
+ # Serving RF-DETR-base on Blackhole with tt-model-manager
2
 
3
+ This repo is a **tt-model container package source**: `tt-model.yaml` + `code/rf_detr`
4
+ (the tt-nn port, with the FastAPI server under `code/rf_detr/server/`). It packages into
5
+ an OCI image that `tt-model serve` / `tt serve changh95/rf-detr-blackhole` run on a
6
+ Blackhole p150a host. Weights (`Roboflow/rf-detr-base`, Apache-2.0, ~129 MB) are a pinned
7
+ pointer, never baked into the image.
8
 
9
+ | | |
10
+ |---|---|
11
+ | tt-metal tree | `/home/deepgadget/experiments/gbp-tt/tt-metal` (main `8b98410e730`, v0.78.0-dev20260820-25; torch 2.11.0+cpu) |
12
+ | weights | `Roboflow/rf-detr-base` @ `7b95b089788e6c7db56d5ea9b0a07ca08ea6ac0a` (`model.safetensors`, `config.json`) |
13
+ | app | `rf_detr.server.app:app` (uvicorn, ASGI lifespan does weights -> device -> model -> warm-up) |
14
+ | device recipe | `ttnn.open_device(device_id, l1_small_size=32768, trace_region_size=90_000_000, num_command_queues=1)` — the port's validated params (`conftest.py`, `scripts/make_demo.py`, `benchmark.py`) |
15
+ | port source | github.com/changh95/tt-RF-DETR @ `5b0149869465892477141b4c1dfc4622db0d1ec5` (+ `rf_detr/server/` added here) |
16
 
17
+ Below, `$ROOT=/home/deepgadget/experiments/tt-models`.
 
 
18
 
19
+ ## 1. Run on the HOST (hardware validation, no Docker)
20
+
21
+ The tree venv `$TT_METAL_HOME/python_env` (py3.10, torch 2.11.0+cpu, ttnn editable) has
22
+ everything except `fastapi`/`uvicorn`. Do not install into the tree venv; put the HTTP
23
+ stack in a side directory and prepend it to `PYTHONPATH`:
24
 
25
  ```bash
26
+ export TT_METAL_HOME=/home/deepgadget/experiments/gbp-tt/tt-metal
27
+ export PATH=$HOME/.local/bin:$PATH # uv
28
+ mkdir -p /tmp/rfdetr-http && uv pip install --python $TT_METAL_HOME/python_env/bin/python \
29
+ --target /tmp/rfdetr-http fastapi uvicorn
30
+
31
+ cd $ROOT/models/rf-detr-blackhole
32
+ export PYTHONPATH=$PWD/code:$TT_METAL_HOME:$TT_METAL_HOME/ttnn:$TT_METAL_HOME/tools:/tmp/rfdetr-http
33
+ export HF_MODEL=Roboflow/rf-detr-base
34
+ export TT_WEIGHTS_REVISION=7b95b089788e6c7db56d5ea9b0a07ca08ea6ac0a # already in ~/.cache/huggingface
35
+ export TT_MESH_SHAPE=1x1
36
+ export TT_DEVICE_ID=0 # optional (default 0)
37
+ export TT_METAL_VISIBLE_DEVICES=0 # optional, one-card box
38
+
39
+ $TT_METAL_HOME/python_env/bin/python -m uvicorn --host 0.0.0.0 --port 20000 --lifespan on rf_detr.server.app:app
40
+ ```
41
+
42
+ Boot log landmarks: `Loading weights` -> `Opening device` -> `TT model built` ->
43
+ `Warming up: capturing trace` -> `Warmup complete` -> uvicorn `Application startup complete`.
44
+ The first boot compiles kernels (minutes on a cold `~/.cache/ttnn` / `TT_METAL_CACHE`);
45
+ later boots take seconds. Startup failures raise and uvicorn exits non-zero (no CPU
46
+ fallback). Stop with Ctrl-C / SIGTERM: the lifespan releases the trace, drops the device
47
+ tensors and calls `ttnn.close_device`.
48
+
49
+ Then, from a second shell (stdlib only, any Python):
50
+
51
+ ```bash
52
+ python code/rf_detr/server/smoke_test.py --url http://127.0.0.1:20000 # PASS/FAIL one-liner
53
+ python code/rf_detr/server/smoke_test.py --url http://127.0.0.1:20000 --wait 1800 --out /tmp/rfdetr.json
54
+ ```
55
 
56
+ Expected on the demo image (`media/demo_source.png`, 640x480): `cat` 0.96 / 0.94,
57
+ `remote` 0.90 / 0.73, `couch` 0.67 — `PASS n=5 counts={"cat": 2, "couch": 1, "remote": 2} ...`.
58
+ The CPU reference gives cat 0.960/0.933, remote 0.898/0.728, couch 0.673 for the same input.
59
 
60
+ Offline override (no Hub, no HF cache): `TT_RF_DETR_WEIGHTS=<dir holding model.safetensors + config.json>`.
 
61
 
62
+ Offline checks that need no device (what the implement phase ran):
 
63
 
64
+ ```bash
65
+ # manifest + launcher preview (from the repo dir — `root: code` is CWD-relative)
66
+ $ROOT/.venv/bin/python -c "from tt_kernel.container_manifest import load_container_manifest; m = load_container_manifest('tt-model.yaml', check_sources=True); p = m.resolve_profile(); print('VALID', m.name, m.kind, p.hardware, p.mesh_device, m.weights_ref); from tt_kernel.launchers import launcher_for; L = launcher_for(m.kind); print(L.install_lines(m)); print(L.verify_lines(m))"
67
+ # import the app with no device
68
+ PYTHONPATH=$PWD/code:$TT_METAL_HOME:$TT_METAL_HOME/ttnn:$TT_METAL_HOME/tools:/tmp/rfdetr-http $TT_METAL_HOME/python_env/bin/python -c "import rf_detr.server.app as a; assert a.app"
69
+ ```
70
+
71
+ ## 2. Package, serve, push (Docker)
72
+
73
+ Rootless Docker on this box needs `source $ROOT/bin/docker-env.sh` first (sets PATH and
74
+ `DOCKER_HOST`; the bare `docker` on PATH is podman). **Run every `tt-model` command from
75
+ this directory**: `source.tt_metal` and `source.extra_code[].root: code` resolve against
76
+ the process CWD, not against the YAML. `--out` points outside the git checkout because
77
+ `stage()` deletes `<out>/rf-detr-blackhole` before rebuilding.
78
+
79
+ ```bash
80
+ source $ROOT/bin/docker-env.sh
81
+ cd $ROOT/models/rf-detr-blackhole
82
+
83
+ tt-model package --container tt-model.yaml --out $ROOT/build # builds the image (hours cold), runs verify.sh
84
+ tt-model serve $ROOT/build/rf-detr-blackhole/tt_kernel_manifest.json # pre-downloads the pinned weights, boots, waits for READY
85
+ python code/rf_detr/server/smoke_test.py --url http://127.0.0.1:<port serve printed>
86
+ tt-model stop changh95/rf-detr-blackhole
87
+ tt-model push $ROOT/build/rf-detr-blackhole --publish # uploads code/, image/, README (generated card)
88
+ ```
89
 
90
+ `serve` publishes the first free port from 20000 and exports into the container exactly:
91
+ `HF_MODEL=Roboflow/rf-detr-base`, `MESH_DEVICE=P150`, `TT_MESH_SHAPE=1x1`, then `serve.env`
92
+ (`TT_WEIGHTS_REVISION=7b95b0…`, `TT_METAL_VISIBLE_DEVICES=0`). The HF cache is mounted at
93
+ `/hf` (rw), kernels at `/cache`, and `/weight-cache` is unused by this port (weights are
94
+ converted to bf16/bf8 device tensors at boot in seconds). tt-cli users:
95
+ `tt serve changh95/rf-detr-blackhole` / `tt model stop changh95/rf-detr-blackhole`.
96
+
97
+ `push` makes `code/` and `image/` on the Hub exactly the staged trees (`rf_detr`,
98
+ `scripts`, `conftest.py` and the `models/common/lightweightmodule.py` filler) and replaces
99
+ `README.md` with the generated card (everything worth keeping lives in
100
+ `card.description` / `card.quickstart`). `media/`, `SERVING.md`, `.gitattributes` and
101
+ `tt-model.yaml` at the repo root survive; the orchestrator restores the `license` /
102
+ `pipeline_tag` front matter afterwards.
103
+
104
+ ## 3. The request / response contract
105
+
106
+ | route | purpose |
107
+ |---|---|
108
+ | `GET /health` | `{"status": "ok" \| "starting", "model": "RF-DETR-base", "device": "blackhole p150 (device_id=0, mesh 1x1)" \| null}` — 200 always; `ok` only after warm-up |
109
+ | `GET /info` | model, task, hardware, weights repo + revision + resolved files, port source + commit, input limits (560x560, 1 image, batch 1), COCO labels, device params |
110
+ | `GET /v1/models` | `{"object": "list", "data": [{"id": "Roboflow/rf-detr-base", "object": "model", "owned_by": "changh95"}]}` — only so the OpenAI-shaped ready card / `tt-model curl` do not 404 |
111
+ | `POST /predict` | one image -> detections (below) |
112
+
113
+ `POST /predict` request (JSON):
114
+
115
+ | field | type | default | meaning |
116
+ |---|---|---|---|
117
+ | `image` | str | required | base64 PNG/JPEG, any size (max side 8192, max 32 MB); a `data:image/...;base64,` prefix and wrapped base64 are tolerated |
118
+ | `threshold` | float 0..1 | `0.5` | keep detections with `sigmoid(logit) > threshold` |
119
+ | `max_detections` | int 1..300 | `100` | keep at most this many (highest scores first) |
120
+ | `return_raw` | bool | `false` | also return `raw.logits` `[300][91]` and `raw.pred_boxes` `[300][4]` (normalised cxcywh) |
121
+
122
+ Response (200):
123
+
124
+ ```json
125
+ {
126
+ "detections": [
127
+ {"label": "cat", "label_id": 17, "score": 0.96,
128
+ "box": [7.35, 54.64, 318.45, 472.14], "box_cxcywh_norm": [0.2545, 0.5487, 0.4861, 0.8698]}
129
+ ],
130
+ "num_detections": 5,
131
+ "image_size": {"width": 640, "height": 480},
132
+ "input_size": [560, 560],
133
+ "threshold": 0.5,
134
+ "timing_ms": {"decode": 3.1, "preprocess": 4.0, "inference": 47.0, "postprocess": 0.4, "total": 54.5}
135
+ }
136
  ```
137
 
138
+ `box` is `[x1, y1, x2, y2]` in pixels of the ORIGINAL image (the preprocessor squashes the
139
+ whole image to 560x560, so the model's normalised cxcywh is relative to the original too;
140
+ clamped to the image bounds), sorted by score descending. `label_id` is the COCO-91 index
141
+ from `config.json`'s `id2label`. Errors: **400** undecodable/oversized image, **422**
142
+ schema violation (missing `image`, `threshold` out of range), **503** while starting,
143
+ **500** with `inference failed: <ExceptionType>: <text>` if the device call raises.
144
+
145
+ Server-side pipeline (the port's validated recipe): PIL RGB -> bilinear+antialias resize
146
+ to 560x560 -> /255 -> ImageNet mean/std -> `TtRfDetr(pixel_values)` under one global
147
+ `threading.Lock` and `torch.inference_mode()` -> `sigmoid(logits).max(-1)` -> threshold ->
148
+ sort -> scale cxcywh by (W, H).
149
+
150
+ ## 4. Caveats
151
+
152
+ - **Batch 1, one image per request.** The TT graph is shape-locked to 560x560
153
+ (16 windows x 101 tokens, 40x40 feature grid, 300 queries, 91 classes). Requests are
154
+ serialised by a lock; concurrent clients queue.
155
+ - **Warm-up captures the metal-trace** of the projector+transformer tail inside the
156
+ lifespan (two forwards on a synthetic 560x560 image), so READY means warm; the first
157
+ cold boot pays the ttnn JIT (minutes), later boots reuse `/cache` (`TT_METAL_CACHE`).
158
+ - **Weights are pinned by sha** in both `weights.revision` and `serve.env.TT_WEIGHTS_REVISION`;
159
+ `hf_hub_download(..., revision=<sha>)` is a pure cache hit after `serve`'s pre-download
160
+ and falls back to `local_files_only=True` if the Hub is unreachable.
161
+ - **Precision knobs stay off.** `RFDETR_BB_FIDELITY` / `RFDETR_BB_FP32ACC` / `RFDETR_BB_L1`
162
+ are read by `TtRfDetr` but every setting other than the default regressed accuracy or hung
163
+ (port README); do not put them in `serve.env`.
164
+ - **ttnn API drift** is the main hardware-phase risk: the port was validated on a
165
+ ~June-2026 tree; the packaged tree is 2026-08-20. `Conv2dConfig.shard_layout`,
166
+ `ttnn.topk` index dtype, `execute_trace` kwargs and `grid_sample` kwargs all exist in
167
+ this tree (checked offline); numerics/gate (98.5 detection-IoU) must be re-confirmed
168
+ with `pytest code/rf_detr/tests/test_pretrained_eval.py` or the smoke test on the box.
169
+ - The `tt_metal/python_env` venv on this host is Python 3.10 (the image uses 3.12); the
170
+ code is version-agnostic (3.9+ syntax, `from __future__ import annotations`).
171
+ - `tt-model curl` and the ready card's `/v1/models` hint are OpenAI-shaped and are not this
172
+ API; use the routes above.