TT-Studio Model Bring-up
Battle-tested playbook for taking a model from "listed in tt-inference-server"
to "generating output through the TT-Studio UI", including the cross-repo
triage when it doesn't work. Motif-Image-6B-Preview on p300x2 (tt-studio#1234,
tt-metal#53197, tt-inference-server#4955/#4956) and mochi-1-preview are the
reference runs of this playbook.
Ground rules
- Work in a git worktree off
origin/dev (git worktree add .claude/worktrees/<name> -b <username>/<feature> origin/dev). Never edit the
main checkout's branch — it may be another session's live work.
- All git/PR mechanics follow the feature-branch-pr skill: branch
<username>/<feature> off dev, minimal in-scope diff, human commit messages,
PR targets dev, no AI attribution anywhere (no Co-Authored-By trailers,
no mention of AI tools in commits, PR text, or issues).
- Ask the user before stopping someone's running deployment or changing shared
state (tokens,
.env, board resets).
1. Recon before touching anything
- Upstream truth: read the model_support doc
(
tt-inference-server/docs/model_support/<type>/<Model>_<device>.md) —
HF repo, docker image + tag, impl, inference engine, tt-metal commit.
Treat its "Complete 🟢" status as a claim to verify, not a fact: the Motif
doc's own pinned image could not run the model at all.
- Artifact spec (the deploy-time source of truth for prod models):
.artifacts/tt-inference-server/workflows/model_specs/<env>/*.yaml.
If the model + device entry is already in the pinned artifact version, no
artifact bump and no requires_dev_catalog/inference_artifact_ref are
needed. Root model_spec.json / release_model_spec.json are generated
exports — never edit them; .artifacts/ edits are ephemeral anyway.
- tt-studio catalog: check
app/backend/shared_config/models_from_inference_server.json for the model
and for a same-type sibling to copy field-for-field (e.g. FLUX.1-dev for an
image model). Field mapping rules live in
app/backend/shared_config/sync_models_from_inference_server.py
(map_model_type, map_service_route, DEVICE_TYPE_TO_CONFIG).
- Hardware reality:
tt-smi -ls, GET :8000/docker/chip-status/.
Know the board semantics: a P300x2 "board" is ALL 4 chips (2 p300c cards);
one multi-chip deployment occupies the whole board, and another multi-chip
model cannot deploy until it's stopped.
- What's running:
docker ps, GET :8000/docker/deployments/. If chips
are occupied, ask the user before stopping anything, and record the
occupant's model_id so it can be redeployed.
2. Add the catalog entry
Append to models_from_inference_server.json exactly what a resync from the
artifact would emit (copy a sibling entry; IMAGE → IMAGE_GENERATION +
/v1/images/generations, VIDEO → /v1/videos/generations, etc.). Bump
total_models. Rules:
- Model in the pinned prod artifact → plain entry, no
hand_owned (a
future resync converges).
- Model not in any prod snapshot, or entry carrying local overrides that a
resync must not clobber →
"hand_owned": true (see HAND_OWNED_KEYS in the
sync script).
- Image models:
param_count: null, env_vars: {} unless the artifact spec
sets some (watch for TT_DIT_CACHE_DIR).
Run the guards (inside the backend container; they need Django + the
shared_config dir on PYTHONPATH):
docker exec -w /backend -e PYTHONPATH=/backend/shared_config \
tt_studio_backend_api_dev pytest shared_config/test_sync_models.py shared_config/test_model_config.py -q
Pre-existing ImplSelectorTests DB-setup errors in this docker-exec context
are a known environment artifact — compare against a clean tree before blaming
your change.
3. Deploy through the real TT-Studio path
Test with the change applied to the checkout the dev stack mounts (apply the
same edit there temporarily if your worktree isn't the mounted one; revert
after). uvicorn --reload only watches .py files — after a JSON-only catalog
edit, touch app/backend/shared_config/model_config.py to force a reload.
# catalog pickup
curl -s :8000/docker/get_containers/ | grep <model-name>
# deploy (weights_id must be "" — null is rejected)
curl -X POST :8000/docker/deploy/ -d '{"model_id": "<id>", "weights_id": ""}'
# progress / logs
curl :8000/docker/deploy/progress/<job_id>/ # and deploy/logs/<job_id>/
Then watch the model container: run.py streams it to
.artifacts/tt-inference-server/workflow_logs/docker_server/*.log, and
docker logs <container> works too. Scope watchers with --since <ts> —
docker logs survive restarts and stale error lines will false-positive your
grep. Watch for both success ("Model warmup completed") and the failure
signatures below; the backend's log classifier often misattributes failures
(e.g. "HF_TOKEN authentication failed" for a container that died at import),
so always read the real log.
Success criteria — all three, not just health:
GET :8000/models/health/?deploy_id=<id> → {"message":"Healthy"}
(the bare endpoint 400s: deploy_id is required).
- Real inference through the backend route (e.g.
POST :8000/models/image-generation/ {"deploy_id": ..., "prompt": ...};
direct :7000/v1/... needs the JWT). Validate the output artifact (file,
open the image) — don't trust a 200.
- Repeat after a clean redeploy if you hot-patched anything (see §5).
4. Triage ladder — attribute the failure to the right repo
Work down the stack; each signature below was hit in a real bring-up:
| Signature |
Layer at fault |
Meaning / fix |
| Catalog/deploy 4xx, wrong route, model missing from UI |
tt-studio |
Fix in your branch; part of the same PR |
ValueError: '<Model>' is not a valid ModelNames at server import |
tt-inference-server (stale image pin) |
The spec pins an image whose server predates the model / its exact-case enum name. Find a newer tag: GET ghcr.io/v2/tenstorrent/<image>/tags/list (anonymous token flow) |
Missing (ModelRunners.X, DeviceTypes.Y) entry in the image's config/constants.py |
tt-inference-server |
The image has no device config for this board — needs newer image or upstream addition |
KeyError: 'num_links' (or missing mesh preset) in models/tt_dit/pipelines/<model>/pipeline_*.py |
tt-metal |
The pipeline _PRESETS lacks this mesh shape. Derive one from an existing preset (e.g. the T3K layout with tensor-parallel scaled to the chip count) — it must be hardware-verified before PRing |
TT_FATAL: Creating trace buffers of size N ... only M is allocated |
tt-inference-server (runner) |
Raise trace_region_size in the runner's get_pipeline_device_params; tt-metal's own pipeline tests show a known-good value |
GatedRepoError / HF 403 inside the container |
environment |
The deploy token lacks access to a gated repo (models may pull other repos, e.g. Motif uses SD3.5-large's VAE). Verify with curl -H "Authorization: Bearer $TOK" https://huggingface.co/<repo>/resolve/main/config.json |
| Token updated via Settings UI but container still gets the old one |
tt-studio (known bug #1235) |
user_config.env in the persistent volume is root-owned; the host inference-api can't read it and silently falls back to .env. Workaround: sudo chown <host-user> the file |
Interrogate the image, not the repo checkout — the container's code is what
runs: docker run --rm --entrypoint python3 <image> -c "..." to dump enums,
presets, device configs from candidate tags.
5. Verify fixes by hot-patching the running container
Fast iteration loop, no rebuilds:
- Patch files in-place with
docker exec -i <c> python3 - <<'EOF' ... (heredoc
needs -i; verify with grep before restarting).
docker restart <c> keeps the container filesystem and your patches —
model containers are --rm, so a stop destroys them (and the patches).
- Iterate one failure at a time until warmup completes and generation works.
6. Patched GHCR image (when upstream fixes can't wait)
Never rebuild media images from source (tt-metal compile = hours). Layer:
FROM ghcr.io/tenstorrent/<stock-image>:<tag>
COPY --chown=container_app_user:container_app_user <patched-file> <same-path-in-image>
- The layered file must be the image's own copy with the minimal patch
applied (
docker cp it out of the patched container) — never a copy from
the repo's main, which references symbols the image doesn't have.
- Push to the
tt-studio/studio_images GHCR package
(ghcr.io/tenstorrent/tt-studio/studio_images) — never an ad-hoc path like
tt-studio/<image-name>. Tag model-first to match the package's existing
versions: studio_images:<model>-<board>-<builddate>-<stock-tag> (e.g.
motif-image-6b-p300x2-20260814-0.18.0-c49bb76).
- Redeploy from the clean built image and re-verify end-to-end (§3) — the
hot-patched container proving it and the baked image proving it are
different facts.
- The user must run the
docker push (agent pushes to GHCR are blocked);
hand over the exact command and note it as a pending item in the PR.
7. Pin the image in tt-studio
For prod (non-dev-catalog) media models, the catalog docker_image field is
display-only — the deploy image comes from the artifact spec. The pin that
actually works is override_docker_image in
app/backend/docker_control/docker_utils.py (see the Wan2.2 and Motif blocks
for the pattern). Add the pin with a comment explaining exactly why and when it
can be dropped, and set the catalog docker_image to the same tag for UI
honesty.
8. Upstream PRs
- tt-inference-server: base
main (dev is dead). One concern per PR,
minimal diff — prefer a true one-liner with the rationale in the commit
message and PR body (problem, exact error, fix, hardware verification,
reproduce steps). Cross-link related PRs.
- Never PR direct version/commit edits to the model spec files (
prod.yaml
/ catalogs / release model specs). Maintainers reject them
(tt-inference-server#4941): version promotions must go through their Models
CI Release Process (evals, benchmarks, acceptance criteria), and a spec pin
touches many files that must stay in sync. Instead: request inclusion in the
next release via #model-requests, and carry any interim pin tt-studio
side via override_docker_image (§7) with a patched studio_images tag
(§6) if needed. Code fixes (runners, configs, constants) are still fine as
direct PRs.
- tt-metal: huge repo — use a sparse partial clone:
git clone --filter=blob:none --no-checkout --depth 1 <url> && git sparse-checkout set <dir> && git checkout main. Same minimal-diff rules.
A derived mesh preset/parallel config is model bring-up work: only PR it
with hardware verification evidence, never speculatively.
- CI note: an infra-killed job ("runner has received a shutdown signal",
exit 143) is a flake — check no test actually pins your value, then re-push.
9. tt-studio PR and cleanup
- From the worktree, follow feature-branch-pr end-to-end: health checks
(
/up/, /models/health/?deploy_id=, :8001/health, :3000/), stage only
intended files, human commit message, PR → dev. The PR body should carry
the triage summary, upstream PR links, verification evidence, and any
pending items (GHCR push).
- Revert every temporary edit to the mounted/live checkout;
git status there
must match how you found it (or tell the user exactly what was left and why).
- Restore or leave stopped any deployment you displaced — per what the user
chose in §1.
- File issues for real bugs found along the way that are out of scope to fix.
1---2name: model-bringup3description: Bring up a model that tt-inference-server lists as supported but that TT-Studio can't deploy today — add it to the catalog, deploy it on real hardware, triage failures layer by layer (tt-studio → tt-inference-server → tt-metal), verify fixes by hot-patching the running container, ship a patched GHCR image when upstream fixes can't wait, and open minimal PRs in every repo at fault. Use when asked to "add model X for <board>", "bring up <model> from the inference server", or when a model_support doc URL from tt-inference-server is shared. Ends with a tt-studio PR opened via the feature-branch-pr skill.4---56# TT-Studio Model Bring-up78Battle-tested playbook for taking a model from "listed in tt-inference-server"9to "generating output through the TT-Studio UI", including the cross-repo10triage when it doesn't work. Motif-Image-6B-Preview on p300x2 (tt-studio#1234,11tt-metal#53197, tt-inference-server#4955/#4956) and mochi-1-preview are the12reference runs of this playbook.1314## Ground rules1516- Work in a **git worktree off `origin/dev`** (`git worktree add17 .claude/worktrees/<name> -b <username>/<feature> origin/dev`). Never edit the18 main checkout's branch — it may be another session's live work.19- All git/PR mechanics follow the **feature-branch-pr** skill: branch20 `<username>/<feature>` off dev, minimal in-scope diff, human commit messages,21 PR targets `dev`, **no AI attribution anywhere** (no Co-Authored-By trailers,22 no mention of AI tools in commits, PR text, or issues).23- Ask the user before stopping someone's running deployment or changing shared24 state (tokens, `.env`, board resets).2526## 1. Recon before touching anything27281. **Upstream truth**: read the model_support doc29 (`tt-inference-server/docs/model_support/<type>/<Model>_<device>.md`) —30 HF repo, docker image + tag, impl, inference engine, tt-metal commit.31 Treat its "Complete 🟢" status as a claim to verify, not a fact: the Motif32 doc's own pinned image could not run the model at all.332. **Artifact spec** (the deploy-time source of truth for prod models):34 `.artifacts/tt-inference-server/workflows/model_specs/<env>/*.yaml`.35 If the model + device entry is already in the pinned artifact version, no36 artifact bump and no `requires_dev_catalog`/`inference_artifact_ref` are37 needed. Root `model_spec.json` / `release_model_spec.json` are generated38 exports — never edit them; `.artifacts/` edits are ephemeral anyway.393. **tt-studio catalog**: check40 `app/backend/shared_config/models_from_inference_server.json` for the model41 and for a same-type sibling to copy field-for-field (e.g. FLUX.1-dev for an42 image model). Field mapping rules live in43 `app/backend/shared_config/sync_models_from_inference_server.py`44 (`map_model_type`, `map_service_route`, `DEVICE_TYPE_TO_CONFIG`).454. **Hardware reality**: `tt-smi -ls`, `GET :8000/docker/chip-status/`.46 Know the board semantics: a P300x2 "board" is ALL 4 chips (2 p300c cards);47 one multi-chip deployment occupies the whole board, and another multi-chip48 model cannot deploy until it's stopped.495. **What's running**: `docker ps`, `GET :8000/docker/deployments/`. If chips50 are occupied, ask the user before stopping anything, and record the51 occupant's `model_id` so it can be redeployed.5253## 2. Add the catalog entry5455Append to `models_from_inference_server.json` exactly what a resync from the56artifact would emit (copy a sibling entry; `IMAGE` → `IMAGE_GENERATION` +57`/v1/images/generations`, `VIDEO` → `/v1/videos/generations`, etc.). Bump58`total_models`. Rules:5960- Model **in** the pinned prod artifact → plain entry, no `hand_owned` (a61 future resync converges).62- Model **not** in any prod snapshot, or entry carrying local overrides that a63 resync must not clobber → `"hand_owned": true` (see `HAND_OWNED_KEYS` in the64 sync script).65- Image models: `param_count: null`, `env_vars: {}` unless the artifact spec66 sets some (watch for `TT_DIT_CACHE_DIR`).6768Run the guards (inside the backend container; they need Django + the69`shared_config` dir on `PYTHONPATH`):7071```bash72docker exec -w /backend -e PYTHONPATH=/backend/shared_config \73 tt_studio_backend_api_dev pytest shared_config/test_sync_models.py shared_config/test_model_config.py -q74```7576Pre-existing `ImplSelectorTests` DB-setup errors in this docker-exec context77are a known environment artifact — compare against a clean tree before blaming78your change.7980## 3. Deploy through the real TT-Studio path8182Test with the change applied to the checkout the dev stack mounts (apply the83same edit there temporarily if your worktree isn't the mounted one; revert84after). uvicorn `--reload` only watches `.py` files — after a JSON-only catalog85edit, `touch app/backend/shared_config/model_config.py` to force a reload.8687```bash88# catalog pickup89curl -s :8000/docker/get_containers/ | grep <model-name>90# deploy (weights_id must be "" — null is rejected)91curl -X POST :8000/docker/deploy/ -d '{"model_id": "<id>", "weights_id": ""}'92# progress / logs93curl :8000/docker/deploy/progress/<job_id>/ # and deploy/logs/<job_id>/94```9596Then watch the model container: run.py streams it to97`.artifacts/tt-inference-server/workflow_logs/docker_server/*.log`, and98`docker logs <container>` works too. **Scope watchers with `--since <ts>`** —99docker logs survive restarts and stale error lines will false-positive your100grep. Watch for both success ("Model warmup completed") and the failure101signatures below; the backend's log classifier often misattributes failures102(e.g. "HF_TOKEN authentication failed" for a container that died at import),103so always read the real log.104105Success criteria — all three, not just health:1061. `GET :8000/models/health/?deploy_id=<id>` → `{"message":"Healthy"}`107 (the bare endpoint 400s: `deploy_id` is required).1082. Real inference through the backend route (e.g.109 `POST :8000/models/image-generation/ {"deploy_id": ..., "prompt": ...}`;110 direct `:7000/v1/...` needs the JWT). Validate the output artifact (`file`,111 open the image) — don't trust a 200.1123. Repeat after a clean redeploy if you hot-patched anything (see §5).113114## 4. Triage ladder — attribute the failure to the right repo115116Work down the stack; each signature below was hit in a real bring-up:117118| Signature | Layer at fault | Meaning / fix |119|---|---|---|120| Catalog/deploy 4xx, wrong route, model missing from UI | **tt-studio** | Fix in your branch; part of the same PR |121| `ValueError: '<Model>' is not a valid ModelNames` at server import | **tt-inference-server** (stale image pin) | The spec pins an image whose server predates the model / its exact-case enum name. Find a newer tag: `GET ghcr.io/v2/tenstorrent/<image>/tags/list` (anonymous token flow) |122| Missing `(ModelRunners.X, DeviceTypes.Y)` entry in the image's `config/constants.py` | **tt-inference-server** | The image has no device config for this board — needs newer image or upstream addition |123| `KeyError: 'num_links'` (or missing mesh preset) in `models/tt_dit/pipelines/<model>/pipeline_*.py` | **tt-metal** | The pipeline `_PRESETS` lacks this mesh shape. Derive one from an existing preset (e.g. the T3K layout with tensor-parallel scaled to the chip count) — it must be hardware-verified before PRing |124| `TT_FATAL: Creating trace buffers of size N ... only M is allocated` | **tt-inference-server** (runner) | Raise `trace_region_size` in the runner's `get_pipeline_device_params`; tt-metal's own pipeline tests show a known-good value |125| `GatedRepoError` / HF 403 inside the container | **environment** | The deploy token lacks access to a gated repo (models may pull *other* repos, e.g. Motif uses SD3.5-large's VAE). Verify with `curl -H "Authorization: Bearer $TOK" https://huggingface.co/<repo>/resolve/main/config.json` |126| Token updated via Settings UI but container still gets the old one | **tt-studio** (known bug #1235) | `user_config.env` in the persistent volume is root-owned; the host inference-api can't read it and silently falls back to `.env`. Workaround: `sudo chown <host-user>` the file |127128Interrogate the *image*, not the repo checkout — the container's code is what129runs: `docker run --rm --entrypoint python3 <image> -c "..."` to dump enums,130presets, device configs from candidate tags.131132## 5. Verify fixes by hot-patching the running container133134Fast iteration loop, no rebuilds:135136- Patch files in-place with `docker exec -i <c> python3 - <<'EOF' ...` (heredoc137 needs `-i`; verify with grep before restarting).138- `docker restart <c>` **keeps** the container filesystem and your patches —139 model containers are `--rm`, so a *stop* destroys them (and the patches).140- Iterate one failure at a time until warmup completes and generation works.141142## 6. Patched GHCR image (when upstream fixes can't wait)143144Never rebuild media images from source (tt-metal compile = hours). Layer:145146```dockerfile147FROM ghcr.io/tenstorrent/<stock-image>:<tag>148COPY --chown=container_app_user:container_app_user <patched-file> <same-path-in-image>149```150151- **The layered file must be the image's own copy with the minimal patch152 applied** (`docker cp` it out of the patched container) — never a copy from153 the repo's main, which references symbols the image doesn't have.154- Push to the **`tt-studio/studio_images` GHCR package**155 (`ghcr.io/tenstorrent/tt-studio/studio_images`) — never an ad-hoc path like156 `tt-studio/<image-name>`. Tag model-first to match the package's existing157 versions: `studio_images:<model>-<board>-<builddate>-<stock-tag>` (e.g.158 `motif-image-6b-p300x2-20260814-0.18.0-c49bb76`).159- Redeploy from the clean built image and re-verify end-to-end (§3) — the160 hot-patched container proving it and the baked image proving it are161 different facts.162- **The user must run the `docker push`** (agent pushes to GHCR are blocked);163 hand over the exact command and note it as a pending item in the PR.164165## 7. Pin the image in tt-studio166167For prod (non-dev-catalog) media models, the catalog `docker_image` field is168**display-only** — the deploy image comes from the artifact spec. The pin that169actually works is `override_docker_image` in170`app/backend/docker_control/docker_utils.py` (see the Wan2.2 and Motif blocks171for the pattern). Add the pin with a comment explaining exactly why and when it172can be dropped, and set the catalog `docker_image` to the same tag for UI173honesty.174175## 8. Upstream PRs176177- **tt-inference-server**: base `main` (`dev` is dead). One concern per PR,178 minimal diff — prefer a true one-liner with the rationale in the commit179 message and PR body (problem, exact error, fix, hardware verification,180 reproduce steps). Cross-link related PRs.181- **Never PR direct version/commit edits to the model spec files** (`prod.yaml`182 / catalogs / release model specs). Maintainers reject them183 (tt-inference-server#4941): version promotions must go through their Models184 CI Release Process (evals, benchmarks, acceptance criteria), and a spec pin185 touches many files that must stay in sync. Instead: request inclusion in the186 next release via `#model-requests`, and carry any interim pin **tt-studio187 side** via `override_docker_image` (§7) with a patched `studio_images` tag188 (§6) if needed. Code fixes (runners, configs, constants) are still fine as189 direct PRs.190- **tt-metal**: huge repo — use a sparse partial clone:191 `git clone --filter=blob:none --no-checkout --depth 1 <url> && git192 sparse-checkout set <dir> && git checkout main`. Same minimal-diff rules.193 A derived mesh preset/parallel config is model bring-up work: only PR it194 with hardware verification evidence, never speculatively.195- CI note: an infra-killed job ("runner has received a shutdown signal",196 exit 143) is a flake — check no test actually pins your value, then re-push.197198## 9. tt-studio PR and cleanup1992001. From the worktree, follow **feature-branch-pr** end-to-end: health checks201 (`/up/`, `/models/health/?deploy_id=`, `:8001/health`, `:3000/`), stage only202 intended files, human commit message, PR → `dev`. The PR body should carry203 the triage summary, upstream PR links, verification evidence, and any204 pending items (GHCR push).2052. Revert every temporary edit to the mounted/live checkout; `git status` there206 must match how you found it (or tell the user exactly what was left and why).2073. Restore or leave stopped any deployment you displaced — per what the user208 chose in §1.2094. File issues for real bugs found along the way that are out of scope to fix.