File size: 18,428 Bytes
a1fdb70
2e73062
 
a1fdb70
 
a9aa4ae
 
 
a1fdb70
fcc6866
a9aa4ae
2e73062
 
 
 
 
 
 
 
 
a1fdb70
 
2e73062
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a9aa4ae
 
 
2e73062
a9aa4ae
 
 
 
 
 
 
 
 
2e73062
 
 
a9aa4ae
2e73062
a9aa4ae
 
2e73062
 
a9aa4ae
 
2e73062
a9aa4ae
 
2e73062
 
a9aa4ae
2e73062
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a9aa4ae
2e73062
 
a9aa4ae
2e73062
 
a9aa4ae
2e73062
 
 
a9aa4ae
 
2e73062
a9aa4ae
 
 
 
 
 
 
 
2e73062
a9aa4ae
 
a1fdb70
2e73062
a1fdb70
2e73062
 
 
 
 
 
 
 
 
 
 
 
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
---
title: GPU Goblin
emoji: 🧌
colorFrom: red
colorTo: red
sdk: streamlit
sdk_version: "1.32.0"
app_file: ui/auto_tune_ui.py
pinned: false
license: mit
short_description: AI auto-tuner for MI300X fine-tuning workloads.
tags:
  - amd
  - mi300x
  - rocm
  - qwen
  - huggingface
  - agent
  - fine-tuning
  - llm
---

# GPU Goblin

> An AI agent that hunts wasted compute on AMD MI300X. Powered by Qwen.

GPU Goblin profiles a fine-tuning run, diagnoses inefficiency against a
curated ROCm knowledge base, recommends MI300X-specific fixes, and
re-benchmarks to prove the speedup with real numbers. The agent itself runs
on a Qwen model via Hugging Face Inference Providers; the canonical demo
workload is `Qwen/Qwen2.5-7B-Instruct` LoRA fine-tuning on MI300X.

Submitted to the **AMD Developer Hackathon**, Track 1: AI Agents & Agentic
Workflows. Incorporates the Qwen Technology Partner challenge (Qwen as both
agent brain and audit target) and uses Hugging Face as the model hub +
deployment layer.

See [`brainstorming/idea.md`](brainstorming/idea.md),
[`brainstorming/architecture.md`](brainstorming/architecture.md), and
[`brainstorming/goals.md`](brainstorming/goals.md).

## Quick Start

```bash
pip install -e ".[dev]"

# Required for the live agent loop:
export HF_TOKEN=hf_...                       # Hugging Face Inference token

# Optional β€” override the default Qwen model / provider:
# export GOBLIN_QWEN_MODEL=Qwen/Qwen2.5-7B-Instruct
# export GOBLIN_QWEN_PROVIDER=auto           # or together / fireworks-ai / nebius / ...

uvicorn agent.server:app --reload --port 8000
streamlit run ui/app.py
```

The Streamlit UI works **without `HF_TOKEN`** in offline-replay mode β€” it
plays a cached audit trajectory (`tests/fixtures/cached_audit.json`) so
judges can see the canonical `142 β†’ 318 tok/s (2.24Γ—)` demo without our
backend or any live LLM.

## Repo Layout

```
agent/
  schemas.py     # Shared pydantic models (RunMetrics, WorkloadConfig, ...)
  backends/      # Pluggable LLM driver (Qwen via HF Inference Providers)
  tools/         # 6 tools the agent can call
  loop.py        # Provider-agnostic tool-use loop
  server.py      # FastAPI + SSE
runner/          # GPU runner (rocprofv3 wrapper) + FakeRunner fallback
kb/              # ROCm knowledge base (22 curated rules, the moat)
ui/              # Streamlit chat UI
workloads/       # Canonical Qwen demo + synthetic corpus
tests/           # Pytest suite + fixtures
brainstorming/   # Design docs (idea / architecture / goals)
```

## Development

The agent loop is testable on a laptop without an MI300X via the `FakeRunner`
and the synthetic corpus in `workloads/synthetic/`. Real benchmarks require
ROCm + MI300X (the `LiveRunner` auto-falls-back to `FakeRunner` when
`rocprofv3` / `amd-smi` / a render device are missing).

```bash
python3 -m pytest tests/ -v          # 86 tests, no GPU required
python3 -m agent workloads/train_qwen_lora.py   # CLI driver, prints SSE events
```

## Running on AMD Developer Cloud (MI300X)

End-to-end recipe for the live demo path. Assumes you've got the $100 hackathon
credits. Plan on ~10-15 GPU-hours total (well under budget).

### 1. Provision an MI300X instance

1. Sign in to [AMD AI Developer Program](https://www.amd.com/en/developer/resources/developer-program.html)
   and join the AMD Developer Cloud waitlist (instant approval for hackathon
   participants).
2. Spin up an **MI300X** instance. Pick the largest container disk you can
   (the model weights cache is 15-30 GB).
3. SSH in. You should land in a Linux shell with a `/dev/dri/renderD*`
   device visible β€” confirm with `ls /dev/dri`.

### 2. Pull the ROCm + PyTorch container

```bash
docker pull rocm/pytorch:rocm6.1_ubuntu22.04_py3.10_pytorch_2.3
```

Run it with the GPU exposed. The `--device` / `--group-add video` lines are
the only ones that matter for ROCm passthrough:

```bash
docker run -it --rm \
    --device=/dev/kfd --device=/dev/dri \
    --group-add video --ipc=host --shm-size=16g \
    -v $HOME:/workspace \
    -e HF_TOKEN=$HF_TOKEN \
    -e GOBLIN_QWEN_MODEL=Qwen/Qwen2.5-7B-Instruct \
    -e ROCM_IMAGE_TAG=rocm6.1_pytorch2.3 \
    -p 8000:8000 -p 8501:8501 \
    rocm/pytorch:rocm6.1_ubuntu22.04_py3.10_pytorch_2.3 bash
```

### 3. Verify the GPU is visible inside the container

```bash
amd-smi monitor          # shows utilization, HBM, power per GPU
rocprofv3 --version      # confirms the profiler is on PATH
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
# β†’ True MI300X
```

If any of these fail, `LiveRunner` will fall back to `FakeRunner` β€” the
agent loop still works, but you get cached metrics instead of a real
benchmark. Don't chase the bug; the demo lane is intact.

### 4. Clone, install, run

```bash
cd /workspace
git clone https://github.com/Manoj-Sri/amd-hackathon-rocm.git goblin
cd goblin

pip install -e ".[dev]"
python -m pytest tests/ -q              # 86 tests pass without GPU; faster sanity check

# Live run on MI300X:
python -m agent workloads/train_qwen_lora.py
# Streams SSE events: thought, tool_call, tool_result, ..., final_report
```

### 5. Run the FastAPI server + UI

```bash
# Terminal 1 (inside the container):
uvicorn agent.server:app --host 0.0.0.0 --port 8000

# Terminal 2:
streamlit run ui/app.py --server.port 8501 --server.address 0.0.0.0
```

If the cloud instance gives you a public IP/URL, port-forward 8501 to your
laptop. If not, SSH-tunnel: `ssh -L 8501:localhost:8501 user@instance` β†’
open `http://localhost:8501` locally.

### 6. Cost-control checklist

- Cache benchmark results (`bench_cache/` is content-addressed by config +
  workload SHA + container tag, so identical configs are free).
- Day-1 baseline run is the only "must-burn-GPU" task; everything else can
  use cached metrics or the FakeRunner.
- Stop the instance between work sessions. AMD Developer Cloud bills only
  for running time.
- Public reference price: ~$1.99/GPU-hour for MI300X VMs. ~$8 of your $100
  covers a full demo + dry-runs.

## Integrating with HF Qwen

The agent runs on Qwen via Hugging Face **Inference Providers**, which
auto-routes your request to one of HF's serving partners (Together,
Fireworks-AI, Nebius, Replicate, ...). You do not run Qwen yourself β€” HF
does β€” and you authenticate with a single token.

### 1. Get a Hugging Face token

1. Sign up at [huggingface.co](https://huggingface.co/).
2. Create a token at [Settings β†’ Access Tokens](https://huggingface.co/settings/tokens)
   with **read** + **inference** scope.
3. Export it:
   ```bash
   export HF_TOKEN=hf_yourtokenhere
   ```

### 2. Join the AMD Developer Hackathon HF Organization

The hackathon submission requires publishing your project as a Hugging Face
Space within the event organization. Click the "Join" link on the
[hackathon page](https://lablab.ai/ai-hackathons/amd-developer) (look for
"Join the AMD Developer Hackathon HF Organization") and accept the
invitation in your HF account.

### 3. Confirm Qwen reachability

Before running the full agent, smoke-test the HF Inference connection:

```bash
python - <<'PY'
import asyncio, os
from huggingface_hub import AsyncInferenceClient

async def go():
    client = AsyncInferenceClient(token=os.environ["HF_TOKEN"])
    resp = await client.chat_completion(
        model="Qwen/Qwen2.5-7B-Instruct",
        messages=[{"role": "user", "content": "Say hello in 5 words."}],
        max_tokens=32,
    )
    print(resp.choices[0].message.content)

asyncio.run(go())
PY
```

Expect a 1-line Qwen response. If you get an auth error, your token is
missing the `inference` scope. If you get a 404, the chosen model isn't
served by any active provider β€” try `Qwen/Qwen2.5-32B-Instruct` or set
`provider="together"` explicitly.

### 4. Run the agent against Qwen

The agent picks Qwen automatically β€” no env var needed beyond `HF_TOKEN`:

```bash
python -m agent workloads/train_qwen_lora.py
```

You should see SSE events streaming: `thought` blocks from Qwen, `tool_call`
events as it picks tools, `tool_result` events, and finally a `final_report`
with the canonical `142 β†’ 318 tok/s (2.24Γ—)` line.

### 5. Switching the model or provider

Qwen has many variants. Override at process start:

```bash
# Bigger model, more reliable tool calls:
export GOBLIN_QWEN_MODEL=Qwen/Qwen2.5-32B-Instruct

# Pin to a specific provider (skip auto-routing):
export GOBLIN_QWEN_PROVIDER=together     # or fireworks-ai / nebius / replicate
```

`Qwen/Qwen2.5-7B-Instruct` (the default) is the sweet spot: ~14 GB at
bf16, fast, supports tool calls. Bump to `-32B-Instruct` if 7B starts
emitting malformed tool arguments mid-audit.

### 6. Self-host Qwen on the same MI300X via vLLM (Path B)

The strongest "AMD-end-to-end" story: Qwen runs on the same MI300X that
GPU Goblin is auditing, served by vLLM behind an OpenAI-compatible
endpoint. Goblin already supports this β€” pick it with one env var. The
recipe below mirrors the [lablab vLLM-on-AMD-Developer-Cloud
tutorial](https://lablab.ai/ai-tutorials/amd-developer-cloud-host-llm-vllm).

#### 6a. Stand up vLLM on the MI300X

```bash
# Inside your MI300X cloud instance (rocm/pytorch container or bare host).
# Pull AMD's official rocm/vllm image β€” has vLLM + ROCm + Qwen support baked in.
docker pull rocm/vllm:latest

# Run vLLM serving a Qwen tool-calling model. `--tool-call-parser hermes`
# is the critical flag β€” it tells vLLM to parse Qwen's Hermes-format
# <tool_call> tags into the OpenAI `tool_calls` shape the agent expects.
#
# Pick ONE of the model recipes below.

# (a) Qwen2.5-32B-Instruct β€” recommended for the AMD GPU path. ~64 GB at
#     bf16 (well under MI300X's 192 GB). Tool calling is significantly
#     more reliable than 7B and the 32K context fits any audit conversation
#     comfortably. First run downloads ~64 GB, takes 5-10 minutes.
docker run -d --name qwen-vllm \
    --device=/dev/kfd --device=/dev/dri --group-add video \
    --ipc=host --shm-size=16g \
    -p 8000:8000 \
    -v $HOME/.cache/huggingface:/root/.cache/huggingface \
    -e HF_TOKEN=$HF_TOKEN \
    rocm/vllm:latest \
    --model Qwen/Qwen2.5-32B-Instruct \
    --dtype bfloat16 \
    --max-model-len 32768 \
    --gpu-memory-utilization 0.85 \
    --enable-auto-tool-choice \
    --tool-call-parser hermes

# (b) Qwen2.5-7B-Instruct β€” light/fast, fine for smoke tests, occasionally
#     hallucinates rule ids on tool calls. Use --max-model-len 32768 (the
#     model's native cap) to keep audits from exhausting context near the
#     compare_runs step.
# docker run -d --name qwen-vllm \
#     ...same flags as above except...
#     --model Qwen/Qwen2.5-7B-Instruct \
#     --max-model-len 32768 \
#     ...

# Wait for "Application startup complete" in the logs, then verify:
docker logs -f qwen-vllm    # ctrl-C once you see "Application startup complete"
curl http://localhost:8000/v1/models
# β†’ JSON listing the model id you served
```

Sanity check tool calling end-to-end:
```bash
curl -s http://localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "Qwen/Qwen2.5-32B-Instruct",
    "messages": [{"role": "user", "content": "Call get_weather for Paris."}],
    "tools": [{"type":"function","function":{"name":"get_weather","description":"weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
    "tool_choice": "auto"
  }' | python3 -c "import sys,json;r=json.load(sys.stdin);print(r['choices'][0]['message'])"
```
Expect a `tool_calls` array with `name=get_weather` and `arguments` mentioning Paris. If you get plain text instead, the `--tool-call-parser hermes` flag was dropped.

#### 6b. Point GPU Goblin at the local vLLM

```bash
export GOBLIN_AGENT_BACKEND=qwen-vllm
export GOBLIN_QWEN_VLLM_URL=http://localhost:8000/v1
export GOBLIN_QWEN_VLLM_MODEL=Qwen/Qwen2.5-32B-Instruct  # match the model you served
# Optional β€” only if you fronted vLLM with auth (default vLLM ignores the key):
# export GOBLIN_QWEN_VLLM_KEY=<your-token>

python -m agent workloads/train_qwen_lora.py
```

Verify the agent picked up the right backend:
```bash
curl http://localhost:8000/healthz   # the Goblin server, not vLLM
# β†’ {"backend": "qwen-vllm", "vllm_url": "http://localhost:8000/v1", ...}
```

That's it β€” the agent loop, the tools, the system prompt, the SSE
streaming, the offline-replay fallback all carry over unchanged. The
only thing that's different is which OpenAI-compatible endpoint
``QwenVLLMBackend`` talks to.

#### 6c. Comparing the two backends

| Aspect | `qwen-hf` (default) | `qwen-vllm` |
|---|---|---|
| Auth | `HF_TOKEN` | none by default; optional `GOBLIN_QWEN_VLLM_KEY` |
| Compute | Together / Fireworks-AI / Nebius (HF routes) | Your MI300X |
| Latency | 200-500 ms / turn (network-bound) | 50-150 ms / turn (in-cluster) |
| Cost | HF Inference credits | Your AMD Developer Cloud GPU-hours |
| Demo story | "uses HF as the model hub" | "Qwen runs on the same MI300X it audits" |
| Setup time | 30 sec (just `HF_TOKEN`) | 2-3 min (model download + warmup) |
| Best for | HF Space deployment | Pitch demo on MI300X |

Run **both** during the hackathon: `qwen-hf` for the Space (judges who
click the URL get a real audit without an MI300X); `qwen-vllm` for the
live pitch demo (the strongest "all AMD" story the judging criterion
"How effectively the chosen model is integrated" rewards).

## Deploying to Hugging Face Spaces

This repo is **already shaped to be a Hugging Face Space** β€” `README.md`
carries the YAML frontmatter HF needs, and `requirements.txt` at the root
is the deliberately-minimal Streamlit-only dependency set (no torch /
transformers / huggingface_hub at runtime). The deployed Space is the
**offline-replay lane**: judges interact with a Streamlit UI that streams
the canonical `142 β†’ 318 tok/s (2.24Γ—)` audit trajectory from
`tests/fixtures/cached_audit.json`, without a live LLM, without a backend,
and without our laptop. This is what satisfies the hackathon's "Demo
Application Platform + Application URL" submission fields.

### One-time setup

1. Create a Hugging Face account at [huggingface.co](https://huggingface.co/)
   and accept the invite to the **AMD Developer Hackathon HF Organization**
   (link is on the [hackathon page](https://lablab.ai/ai-hackathons/amd-developer)
   under the Hugging Face section).
2. Create a token at [Settings β†’ Access Tokens](https://huggingface.co/settings/tokens)
   with **`write`** scope (you need write access to push to the Space repo).
   Save it as `HF_PUSH_TOKEN`.
3. On the HF organization's page, click **"New Space"**:
   - Owner: AMD Developer Hackathon org
   - Space name: `gpu-goblin` (or your preferred slug)
   - License: MIT
   - SDK: **Streamlit**
   - Hardware: **CPU basic** (free; the Space loads no GPU code path)
   - Visibility: Public
4. Don't initialize the Space with anything β€” leave it empty so the first
   push lands cleanly.

### Deploy

From the project root, push the existing `feat/scaffold` branch to the
Space's git remote:

```bash
# Add the Space remote (use HTTPS with your username + HF_PUSH_TOKEN as password):
git remote add space https://huggingface.co/spaces/<org-slug>/gpu-goblin

# Push (HF Spaces use 'main' as the default branch):
git push space feat/scaffold:main
```

You'll see a build log at `https://huggingface.co/spaces/<org-slug>/gpu-goblin`.
Cold-start takes 30-60 seconds (Streamlit + the pure-pydantic deps); once
up, the canonical demo trajectory replays in ~10 seconds when a judge
clicks **"Use sample workload"**.

### What the Space looks like to judges

When a judge opens the Space URL:

1. The lane toggle defaults to **Offline replay** β€” appropriate for the Space
   since there's no MI300X behind it.
2. They click **"Use sample workload"** (which references
   `workloads/train_qwen_lora.py`).
3. Streamlit attempts to reach `http://localhost:8000/audit`, fails with
   `ConnectionError`, surfaces a one-line warning ("Backend unreachable β€”
   running offline-replay demo from cached audit"), then plays the cached
   audit trajectory event-by-event with `~0.4s` pauses between events.
4. Final report renders: `Tokens/sec: 142 β†’ 318 (2.24Γ—)` with the
   side-by-side metrics table, waste-budget bar chart, diff viewer, and
   per-rule citations.

### Updating the Space

After any change to the main repo, redeploy:

```bash
git push space feat/scaffold:main
```

HF rebuilds the Space automatically on push.

### (Stretch) Live agent in the Space

The shipped Space is read-only β€” it doesn't reach a real LLM. If you want
judges to drive the agent live, two paths:

1. **Stand up the FastAPI backend somewhere reachable** (an MI300X on AMD
   Developer Cloud, an HF Inference Endpoint, a small CPU box) and set the
   Space's `GOBLIN_BACKEND_URL` secret to that URL. The Streamlit app will
   stream real SSE from your backend instead of the cached replay.
2. **Embed the agent loop in-process** (refactor `ui/app.py` to call
   `agent.loop.run_audit` directly via `asyncio.run`). This adds
   `huggingface_hub` to `requirements.txt` and requires `HF_TOKEN` as a
   Space secret. Larger cold-start, fully self-contained.

Both are post-MVP; the offline-replay Space is what satisfies the
submission requirement.

## Configuration Reference

| Env var | Default | Purpose |
|---|---|---|
| `GOBLIN_AGENT_BACKEND` | `qwen-hf` | Pick the LLM backend: `qwen-hf` (HF Inference Providers) or `qwen-vllm` (self-hosted vLLM on MI300X). |
| `HF_TOKEN` | *(none)* | Required when `qwen-hf` is active. Hugging Face Inference token. |
| `GOBLIN_QWEN_MODEL` | `Qwen/Qwen2.5-7B-Instruct` | Model id used by the `qwen-hf` backend. |
| `GOBLIN_QWEN_PROVIDER` | `auto` | HF Inference Provider routing (`auto` / `together` / `fireworks-ai` / `nebius` / ...). |
| `GOBLIN_QWEN_VLLM_URL` | `http://localhost:8000/v1` | Base URL of the self-hosted vLLM endpoint (only used when `qwen-vllm` is active). |
| `GOBLIN_QWEN_VLLM_MODEL` | `Qwen/Qwen2.5-7B-Instruct` | Model id served by your local vLLM. |
| `GOBLIN_QWEN_VLLM_KEY` | `EMPTY` | Optional auth token if you front vLLM with nginx/Caddy + auth. |
| `GOBLIN_BACKEND_URL` | `http://localhost:8000/audit` | UI's backend endpoint. |
| `ROCM_IMAGE_TAG` | `unknown` | Container tag mixed into the benchmark cache key. |
| `GOBLIN_GPU_ID` | `0` | Which `/dev/dri/renderD*` to bind in `goblin_runner.sh`. |