REST reference for the /bench/* endpoints served by service/bench_api.py.
These drive the Benchmark tab: building the Python environment, listing and downloading
models, starting and watching runs, and reading results. They are mounted only when the
benchmark feature is present and enabled — check GET /smartune/capabilities
(benchmark: 1) before offering the tab.
Base URL, TLS, authentication and the unified {retcode, retmsg, data} envelope are the
same as the rest of the product; see the Backend API Guide for
those and for the shared RetCode table. Auth is applied app-wide by smartune_api's
before_app_request gate, so none of these routes carries a per-route token check — with
one exception noted under GET /bench/events.
| Endpoint | Method | Description |
|---|---|---|
/bench/events |
GET | SSE stream: job, log, env, models, results |
/bench/env |
GET | Environment status; also bootstraps the base venv |
/bench/env/setup |
POST | Install or rebuild the benchmark Python environment |
/bench/env/ov |
POST | Switch the active OpenVINO version |
/bench/env/setup/log |
GET | Incremental log of the most recent setup job |
/bench/models |
GET | Cached model list, searchable |
/bench/models/memory |
GET | Weight / KV-cache / logits footprint for one model |
/bench/models/disk |
GET | Free space where weights land |
/bench/models/refresh |
POST | Rebuild the cached model list |
/bench/models/local |
DELETE | Delete downloaded weights for one model |
/bench/run |
POST | Start a pipeline run |
/bench/run |
GET | Recent runs plus the job holding the slot |
/bench/run/plan |
POST | Advanced phase 1: print-only pass |
/bench/run/<run_id>/plan |
GET | The commands a plan job produced |
/bench/run/advanced |
POST | Advanced phase 2: run with operator-edited commands |
/bench/run/<run_id> |
GET | Status of one run, with optional log tail |
/bench/run/<run_id>/cancel |
POST | Request cancellation |
/bench/run/metrics/latest |
GET | The running run's most recent sampled row |
/bench/preflight |
GET | Whether a measured run can start, and what blocks it |
/bench/quiet_mode |
GET | Current quiet-mode state |
/bench/quiet_mode |
POST | Raise or drop the quiet-mode gate |
/bench/results |
GET | Benchmark results tree |
/bench/results/matrix |
GET | Results flattened across run directories |
/bench/results |
DELETE | Remove named cases |
/bench/results/job |
DELETE | Remove everything one run produced |
/bench/results/log |
GET | Tail of a per-case benchmark.log |
/bench/results/timeline |
GET | Hardware samples taken while one case was measured |
At most one job — a setup, a download, a plan or a benchmark — runs at a time. Anything
that would need the slot while it is taken returns RetCode.CONFLICT (409) with the
occupying job in data.current, so the UI can offer to watch it instead:
{
"retcode": 409,
"retmsg": "A benchmark run is already running. Wait for it to finish or cancel it first.",
"data": {"current": { /* job object */ }}
}Returned by every endpoint that starts or reports a job:
| Field | Type | Description |
|---|---|---|
| id | string | Job / run id |
| kind | string | setup, run, plan, download |
| status | string | RUNNING or a terminal status |
| returncode | int | Process exit code; null while running |
| started_at | float | Epoch seconds |
| finished_at | float | Epoch seconds; null while running |
| duration | float | Seconds elapsed so far, or total once finished |
| log_path | string | Path of the job's log file |
| meta | object | Per-kind details (models, stage, devices, ov, …) |
Purpose: Server-sent events for the Benchmark tab, replacing its polling loop. The
stream opens with a snapshot event carrying the whole current state, so a client never
needs a separate "what is going on" request — on first load or after a reconnect.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Query | logs | No | bool | 1 / true / yes additionally streams the running job's output as incremental [start, end) deltas |
| Query | client | No | string | Identifies the browser tab |
| Query | token | Yes | string | Auth token (see note) |
Response: text/event-stream. Event names: snapshot, job, log, env, models,
results.
Notes:
- This route authenticates via
?token=, not theX-Auth-Tokenheader —EventSourcecannot set headers. The path must be listed insmartune_api._SSE_TOKEN_PATHSfor that to work. logs=1should only be set while the tab is on screen. The small events still reach a backgrounded tab (that is how it reports a finished run), but a build writes megabytes nobody is reading.clientlets a reconnect (which togglinglogscauses) replace that tab's stream rather than stack a second one on a connection the server cannot yet tell is dead.- Too many concurrent clients returns
RetCode.EXCEPTION_ERRORas an ordinary JSON response, not a stream.
Purpose: Environment status — venv presence, package versions, OpenVINO columns built on disk, and whether a run is possible at all. This is also where the base environment installs itself: it is the request the Benchmark tab opens with, so a missing huggingface-only venv is bootstrapped here.
Request: No parameters.
Response:
{
"retcode": 0,
"retmsg": "Successfully retrieved benchmark environment",
"data": {
"enabled": true,
"env_root": "/opt/intel/smartune/benchmark/runtime",
"venv_exists": true,
"venv_usable": true,
"hf_ready": true,
"genai_ready": true,
"ready": true,
"probing": false,
"versions": {"huggingface_hub": "0.35.0", "transformers": "4.57.0"},
"ov_versions": ["2026.3.1", "2026.4.1"],
"active_ov": "2026.4.1",
"ov_choices": [],
"setup_job": null,
"busy": null
}
}Response Fields:
| Field | Type | Description |
|---|---|---|
| ready / hf_ready | bool | The tab is usable once huggingface_hub is available |
| venv_usable | bool | transformers present too, i.e. a run can be attempted |
| genai_ready | bool | The genai checkout with tools/llm_bench is in place |
| probing | bool | Package version probing still in progress; versions may be incomplete |
| ov_versions | string[] | OpenVINO columns actually built on disk |
| setup_job | object | The most recent setup job, or null |
| busy | object | The job currently holding the slot, or null |
Notes:
- A bootstrap started by this very call is reported in the same response (
busyandsetup_job), rather than leaving the tab to discover it on the next event. - A failed bootstrap attempt does not turn reading the status into an error — the status is what was asked for.
Purpose: Install, or with force rebuild, the benchmark Python environment.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | ov | No | string | Also install this OpenVINO version's complete column (OpenVINO plus every transformers the model router can pick). Omitted = only the genai checkout and the huggingface base venv |
| Body | force | No | bool | Rebuild even when a usable environment exists (default false) |
Request Example:
{
"ov": "2026.4.1",
"force": false
}Response: The job object, with retmsg: "Environment setup started".
Response (already installed):
{
"retcode": 409,
"retmsg": "A usable benchmark environment already exists. Re-run with force to rebuild it.",
"data": {"status": { /* env status */ }, "already_installed": true}
}Notes:
- Everything asked for already being in place, with
force=false, answersCONFLICTrather than silently rebuilding: that moves a working environment aside, so the choice belongs to the operator — who gets the environment details back to decide with. - A busy job slot also returns
CONFLICT, withdata.currentinstead.
Purpose: Switch the active OpenVINO version of the benchmark environment.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | version | Yes | string | A version present in ov_versions from GET /bench/env |
Request Example:
{
"version": "2026.3.1"
}Response: The fresh environment status, with retmsg: "Switched to OpenVINO 2026.3.1".
Notes:
- Only relinks the pre-built pool, so it answers inline with the new status rather than starting a job.
- An unknown version is
RetCode.ARGUMENT_ERROR; a busy job slot isCONFLICT.
Purpose: Incremental log of the most recent setup job.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Query | offset | No | int | Byte offset to resume from (default 0) |
Response: A job view: the job object plus log (the [offset, end) delta) and the new
offset.
Notes:
- Returns
RetCode.NOT_EXISTINGwhen no setup has been run in this session.
Purpose: The cached, benchmarkable model list.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Query | search | No | string | Filter by name |
| Query | limit | No | int | Max rows; 0 or absent = no limit |
Response: The model list payload, with retmsg: "Successfully retrieved model list".
Notes:
- Served from a cache; enumerating models on HuggingFace takes minutes. See
benchmark.prefetch_modelsandbenchmark.models_cache_ttl_daysinconfig.yaml, andPOST /bench/models/refreshto rebuild on demand. - The list is served without the memory block so the tab loads in seconds — use
GET /bench/models/memoryper model.
Purpose: The weight / KV-cache / logits footprint for one model, computed on demand and then cached, so a second open is free.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Query | model | Yes | string | HuggingFace model id, e.g. Qwen/Qwen3-8B |
Response:
{
"retcode": 0,
"retmsg": "Memory footprint ready",
"data": {
"model": "Qwen/Qwen3-8B",
"memory": { "weights": 16106127360, "kv_cache": 2147483648 }
}
}Notes:
- The id is a query parameter, not a path segment: a HuggingFace id carries a slash that a path would split.
memory: nullwithretmsg: "Memory footprint unavailable"is a normal "not known" — an offline machine, or a model whose config the hub would not give up — not an error.
Purpose: Free space where downloaded weights land, for the pre-download check.
Request: No parameters.
Response: A disk-usage payload (total / free / path).
Notes:
- Its own route rather than a field on
GET /bench/env: the figure is only interesting at the moment Download is pressed, and an env payload cached since the tab opened would answer with the free space of an hour ago. Reading it is astatvfs, so asking per click costs nothing.
Purpose: Rebuild the cached model list in the background.
Request: No parameters.
Response:
{
"retcode": 0,
"retmsg": "Model list refresh started",
"data": {"started": true, "reason": null}
}Notes:
- A refusal is still
retcode: 0withstarted: falseand the reason in bothdata.reasonandretmsg. Not being able to search is a normal state of the machine (no benchmark environment, or a search already in flight), and the reason is what the tab shows.
Purpose: Give back the disk one model's downloaded weights are holding.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | model | Yes | string | HuggingFace model id |
| Body | precisions | No | string[] | Which precisions to remove; absent = all of them |
Request Example:
{
"model": "Qwen/Qwen3-8B",
"precisions": ["fp16"]
}Response:
{
"retcode": 0,
"retmsg": "Removed fp16 weights for Qwen/Qwen3-8B",
"data": {
"model": "Qwen/Qwen3-8B",
"removed": ["fp16"],
"skipped": [],
"freed_bytes": 16106127360
}
}Notes:
- Deletion is per precision, because that is how they were fetched: one directory each, and keeping int4 while dropping fp16 is the ordinary request.
- The model is named by its HuggingFace id, never by a path; the directory is derived the same way the download does and confined to the models root.
- A busy job slot returns
CONFLICT— a download or benchmark in flight is reading exactly these files. - Nothing found by that name and precision returns
RetCode.NOT_EXISTING: reporting "freed 0 bytes" as a success reads as a bug. - Emits a
model.weights.deletedaction event and republishes the models list over SSE.
Purpose: Start a pipeline run over the selected models.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | models | Yes | string[] | Model ids to run |
| Body | opt / stage | No | string | Pipeline stage (default all); stage is the alias |
| Body | devices | No | string[] | Devices to run on; absent = every device |
| Body | ov | Conditional | string | OpenVINO version to build/run against. Required for a benchmark, ignored for a build/download |
Request Example:
{
"models": ["Qwen/Qwen3-8B"],
"stage": "all",
"devices": ["CPU", "GPU"],
"ov": "2026.4.1"
}Response: The job object, with retmsg: "Benchmark run started".
Error cases:
| Condition | retcode | Notes |
|---|---|---|
| Invalid request (bad models/stage/ov) | ARGUMENT_ERROR |
|
| Job slot taken | CONFLICT |
data.current carries the occupying job |
| Quiet mode blocked | CONFLICT |
data.blocked: true, data.blockers lists what to resolve and where |
| Environment not ready / feature disabled | NOT_EFFECTIVE |
Actionable, not a server fault |
Notes:
- A measured run raises quiet mode, which suspends the balancer's automatic control for its duration. See the Balancer Guide.
Purpose: Recent runs plus whichever job currently holds the slot.
Request: No parameters.
Response:
{
"retcode": 0,
"retmsg": "Successfully retrieved benchmark runs",
"data": {
"current": null,
"recent": [ /* job objects */ ]
}
}Purpose: Phase 1 of an advanced run — launch a print-only pass. Renders and runs exactly
what POST /bench/run would, but with the plan envelope on, so it lists the per-case
benchmark commands without measuring or writing anything.
Request: Same body as POST /bench/run.
Response: The job object, with retmsg: "Benchmark plan started".
Notes:
- Then poll
GET /bench/run/<run_id>/planfor the commands, edit them, and send them back toPOST /bench/run/advanced. - Same error cases as
POST /bench/run, minus the quiet-mode block — a plan measures nothing.
Purpose: The commands a plan job has produced, and whether it has finished.
Request: run_id in the path.
Response:
{
"retcode": 0,
"retmsg": "Successfully retrieved benchmark plan",
"data": {
"job": { /* job object */ },
"ready": true,
"commands": {"qwen3-8b__CPU__fp16": "python3 benchmark.py ..."}
}
}Notes:
readyflips true once the plan job leavesRUNNING; until thencommandsis whatever it has flushed so far.- The job's own status rides along, so a caller that only polls this endpoint can also see a plan that failed.
- An unknown
run_idreturnsRetCode.NOT_EXISTING.
Purpose: Phase 2 of an advanced run — run the request with the operator's edits.
Request: Same body as POST /bench/run, plus:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | commands | Yes | object | Map of case_key → the edited command |
Response: The job object, with retmsg: "Advanced benchmark run started".
Notes:
- The body must describe the same models / stage / devices / ov the plan was built from,
so the rendered
case_keys line up with the keys here. A key that matches no case simply never fires. commandsmust be an object, otherwiseRetCode.ARGUMENT_ERROR.- Same error cases as
POST /bench/run, quiet-mode block included.
Purpose: Status of one run, with the log tail when ?offset= is supplied.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Path | run_id | Yes | string | Job id |
| Query | offset | No | int | Byte offset to resume the log from; absent means no log is returned at all |
Response: A job view: the job object, plus log and the new offset when offset was
given.
Notes:
- An unknown
run_idreturnsRetCode.NOT_EXISTING.
Purpose: Request cancellation of a running job.
Request: run_id in the path; no body.
Response: {"cancelled": true} with retmsg: "Cancellation requested".
Notes:
- An unknown or already-finished job returns
RetCode.NOT_EFFECTIVEwith{"cancelled": false}.
Purpose: The running run's most recent sampled row — what the Live tiles read under quiet mode.
Request: No parameters.
Response:
{
"retcode": 0,
"retmsg": "Successfully retrieved the latest run sample",
"data": {
"row": {"ts": 1755764400, "cpu": 88.2, "memory": 61.0, "gpu": 97.4},
"sampling": true,
"period_s": 2.0,
"quiet_mode": {"active": true, "held": true, "owner": "bench-...", "since": 1755764000, "user_exited": false}
}
}Notes:
- A dict lookup, no collection: the row was already produced for
metrics.csv. Showing the tiles therefore costs one read-only poll rather than bringing the 2 s hardware collector back. - Only CPU / memory / GPU / NPU are present — the sampler collects no disk or network columns, so those tiles have no source here and the UI hides them.
row: nullwithsampling: falsewhen no run is sampling.
Purpose: Whether a measured run can start right now, and what is in the way. Lets the Models tab say "resolve these two apps first" before the user commits to a run, instead of taking the click and returning an error.
Request: No parameters.
Response:
{
"retcode": 0,
"retmsg": "Successfully retrieved benchmark preflight state",
"data": {
"blocked": false,
"blockers": [],
"quiet_mode": {"active": false, "held": false, "owner": null, "since": null, "user_exited": false}
}
}Notes:
- Advisory only —
POST /bench/runre-checks, because nothing holds the machine's state still between this call and that one.
Purpose: Current quiet-mode state.
Request: No parameters.
Response Fields:
| Field | Type | Description |
|---|---|---|
| active | bool | The gate is up: automatic control and the hardware collector are suspended |
| held | bool | A run owns quiet mode (it may have dropped the gate) |
| owner | string | The run holding it, or null |
| since | float | Epoch seconds the hold started |
| user_exited | bool | The operator dropped the gate during this hold |
Purpose: Raise or drop the gate for the run that currently holds it.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | active | Yes | bool | true raises the gate, false drops it |
Request Example:
{
"active": false
}Response: The new quiet-mode state, with retmsg either Quiet mode restored or
Quiet mode exited; full monitoring resumed.
Notes:
- Dropping the gate is a deliberate choice with a cost the UI spells out first: full monitoring comes back, including the balancer's automatic control, and the run's numbers stop being comparable with other runs.
- Refused with
RetCode.NOT_EFFECTIVEwhen no run holds quiet mode — there is nothing to toggle outside a run, and silently accepting would let the UI believe it had changed something. - A missing
activekey isRetCode.ARGUMENT_ERROR.
Purpose: The benchmark results tree.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Query | backend | No | string | Restrict to one backend; must be a known value |
Notes:
- An unknown
backendreturnsRetCode.ARGUMENT_ERRORlisting the valid ones.
Purpose: The same cases as /bench/results, flattened across run directories, with the
axes and metric descriptors shipped alongside.
Request: Same backend parameter as above.
Notes:
- A run directory holds one device, so comparing devices means reading several of them; this view does that join server-side.
Purpose: Remove the named cases from the results tree. Used when a configuration that has already been measured is re-run and the operator says the old measurement should not be kept.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | cases | Yes | string[] | Case directories, as carried in the matrix the UI is displaying. Must be non-empty |
Response:
{
"retcode": 0,
"retmsg": "Removed 3 benchmark case(s)",
"data": {"removed": 3, "runs_removed": 1, "skipped": []}
}Notes:
- Every case is confined to the benchmarks tree before anything is removed.
- Emits a
benchmark.cases.deletedaction event and republishes results over SSE.
Purpose: Remove everything one press of Run produced. The whole job rather than a list of cases: the Results tab lists by job, so that is the unit the operator is looking at when they decide a run was not worth keeping.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Body | job | Yes | string | Job name, as listed on the Results tab |
Response:
{
"retcode": 0,
"retmsg": "Removed 12 case(s) in 2 run directory(ies)",
"data": {"job": "bench-20261013-0912", "removed_runs": 2, "removed_cases": 12, "skipped": []}
}Notes:
- The name is matched against the directories that exist, so nothing the browser sends is joined onto a path.
- Nothing by that name returns
RetCode.NOT_EXISTINGrather than reporting "removed 0" as a success.
Purpose: Tail of a per-case benchmark.log, named by a summary row.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Query | path | Yes | string | Log path from the results row |
Response: {"path": "...", "content": "..."}.
Notes:
- A missing log returns
RetCode.NOT_EXISTING.
Purpose: The hardware samples taken while one case was being measured. The results tables report a median per case; this is what that median was taken over.
Request:
| Type | Parameter | Required | Format | Description |
|---|---|---|---|---|
| Query | case | Yes | string | Case directory |
Notes:
RetCode.NOT_EXISTINGcovers every honest absence at once — a case with no measurement window, a run whose sampling CSV is gone, an old tree that predates in-process sampling — so the tab can say so rather than drawing an empty chart.