Distributed GPU worker for the AI Capstone Leaderboard federated architecture (Phase 3+4). Runs on a partner school's GPU host and communicates with the central control plane over HTTP.
- Register + heartbeat to central with a one-shot join token
- Long-poll central for QUEUED submissions in the school's queue
- Receive uploads from student browsers directly (presigned URL flow)
- Run rollouts (Isaac Sim, or mock mode for smoke testing)
- Report results back to central; central aggregates the leaderboard
The central platform never sees the student's zip bytes when this agent is deployed — data locality is enforced by design.
中文版學校端啟動流程 (SCHOOL_ADMIN 主要看這份) → STARTUP.md
Prereqs on the GPU host: Docker Engine 20.10+, docker compose plugin, curl.
For EVAL_MODE=real: NVIDIA driver + nvidia-container-toolkit.
git clone https://github.com/HCIS-Lab/aicapstone-worker.git
cd aicapstone-worker
./setup.sh # interactive — asks for join token + public URLOr fully non-interactive:
./setup.sh \
--central=https://leaderboard.hcislab.tw \
--token=<JOIN_TOKEN_FROM_CENTRAL> \
--public-url=https://gpu-01.your-school.edu.tw \
--name=gpu-01 \
--mode=mock \
--yesWhat setup.sh does:
- verifies docker + docker compose + (optionally) nvidia runtime
- prompts for anything not passed as a flag (or reuses
.envon re-run) - writes
.env, builds the image,docker compose up -d - waits for local
/health - tails the agent log until it sees
registered as worker <id>
Verify manually anytime with:
./setup.sh --status # ps + last 20 log lines + /health probe
docker compose logs -f # live tailReset (wipe cached machine JWT + reconfigure):
./setup.sh --resetYou can run any number of workers on one machine — each gets its own GPU, host port, and container name. The central plane doesn't need to know how you split them; workers pull work themselves.
How the assignment actually works (no per-school dispatcher needed):
- Students submit at central → central persists the row with
status=QUEUEDand tags it with the student'sschool_id. - Every online worker for that school long-polls
POST /api/workers/next-job. Whichever worker asks first while free gets the oldest QUEUED submission — pure pull architecture, no push from central and no coordinator inside the school. - Central issues a presigned URL to the student's browser of the form
{WORKER_PUBLIC_URL}/upload/{token}so the zip goes directly to the worker that will run it — the bytes never traverse central.
The one thing you must give each worker is its own reachable URL.
Two workers can't share a port, so setup.sh will:
- Auto-pick a free host port in
8081..8181 - Adjust
WORKER_PUBLIC_URLif the guess was:8081but a later port was picked - Name each container
worker-agent-<port>so a seconddocker compose upon the same host doesn't error with "name is already in use"
Practical layout for a two-GPU host:
# First worker
git clone https://github.com/HCIS-Lab/aicapstone-worker.git worker-gpu0
cd worker-gpu0 && ./setup.sh # picks GPU 0, port 8081
cd ..
# Second worker (separate clone → separate .env / state volume)
git clone https://github.com/HCIS-Lab/aicapstone-worker.git worker-gpu1
cd worker-gpu1 && ./setup.sh # picks GPU 1, port 8082Both workers register under the same SCHOOL_ADMIN join token and show
up as separate rows in the Observability tab.
Central issues one from the SCHOOL_ADMIN dashboard:
/admin/dashboard→Workerstab →+ Issue Join Token
The token is one-shot and expires in 15 min. Central swaps it for a
long-lived machine JWT during registration; the JWT lives under
WORKER_STATE_DIR (docker volume worker_state) and survives restarts.
Set EVAL_MODE=real in .env, then:
-
Bind-mount the simulator + task configs into the container. Uncomment the volumes in
docker-compose.yml:volumes: - ../aicapstone:/aicapstone:ro - ../private_tasks:/private_tasks:ro
-
Enable the NVIDIA runtime:
runtime: nvidia environment: NVIDIA_VISIBLE_DEVICES: ${GPU_ID:-all} NVIDIA_DRIVER_CAPABILITIES: graphics,display,utility,compute
-
Rebuild with GPU driver deps (add to the Dockerfile as needed).
-
Storage volume must be big enough for expected submissions (~2–5 GB each, kept until admin cleans up).
/var/lib/worker-agent/
├── storage/ # WORKER_STORAGE_ROOT
│ ├── submissions/
│ │ └── sid=<id>/
│ │ ├── pretrained_model.zip
│ │ ├── extracted/ (rmtree'd after rollout)
│ │ └── eval_video.mp4
│ └── tokens/
│ └── <token_hash>.json
└── state/ # WORKER_STATE_DIR
├── machine_jwt # cached long-lived JWT
└── worker_id # our numeric worker id
┌──────────────┐ join_token ┌─────────┐
│ Agent │────────────────▶│ Central │
│ (WORKER_ │ │ │
│ JOIN_TOKEN) │◀────────────────│ │ machine_jwt (30d TTL)
└──────┬───────┘ └─────────┘
│
│ machine_jwt cached in WORKER_STATE_DIR
│
│ heartbeat, next-job, result (every request Bearer-authed)
▼
- Join token is one-shot and expires in 15 min (default).
- Machine JWT rotates automatically at heartbeat when it's within 3 days of expiry.
- Admin can revoke a worker from the UI → next heartbeat 401s → agent
re-registers (needs a fresh join token if
WORKER_JOIN_TOKENis still set, otherwise exits and requires operator action).
The agent calls these on ${CENTRAL_API_URL}:
| Method | Path | Notes |
|---|---|---|
| POST | /api/workers/register |
one-shot with join_token |
| POST | /api/workers/heartbeat |
every 30s |
| POST | /api/workers/next-job |
poll for work |
| POST | /api/workers/submissions/{sid}/result |
terminal report |
| POST | /api/submissions/{sid}/upload-complete |
after receiving student zip |
Students' browsers + central <a href> links hit these directly:
| Method | Path | Auth |
|---|---|---|
| GET | /health |
none |
| POST | /upload/{token} |
presigned token in URL |
| GET | /download/{token} |
presigned token in URL |
| GET | /video/{token} |
presigned token in URL |
{token} values come from central; see the central-side flow in
../backend/app/routers/submissions.py
init_submission and issue_download_url.
See .env.example for the full list. The important ones:
| Var | Default | Notes |
|---|---|---|
CENTRAL_API_URL |
http://localhost:8001 |
Central FastAPI base URL |
WORKER_JOIN_TOKEN |
(required first boot) | One-shot from central |
WORKER_NAME |
$(hostname) |
Shown in admin UI |
WORKER_PUBLIC_URL |
http://$(hostname):8080 |
External URL for /upload |
EVAL_MODE |
mock |
mock | real |
ISAAC_SIM_SCRIPT_PATH |
/aicapstone/scripts/rollout.py |
Real mode only |
WORKER_STORAGE_ROOT |
/var/lib/worker-agent/storage |
Persist across container recreation |
WORKER_STATE_DIR |
/var/lib/worker-agent/state |
Ditto |
HEARTBEAT_INTERVAL_SEC |
30 |
Central marks stale > 60s |
JOB_POLL_INTERVAL_SEC |
10 |
Backoff when idle |
MAX_CONCURRENT_EVALS |
1 |
Currently the agent runs serially |
ALLOWED_TASK_KEYS |
(empty = all) | Restrict to specific tasks |
-
WORKER_JOIN_TOKEN not seton boot Cachedmachine_jwtis missing/corrupt AND no fresh join token was provided. Issue a new token from central, put it in.env, restart. -
Health probe 200 but worker stays OFFLINE in central Heartbeat is failing silently — check
docker compose logs worker-agentfor a network error toCENTRAL_API_URL, or a 401 saying the JWT was revoked. -
Student upload times out
WORKER_PUBLIC_URLisn't reachable from the student's network. Check the school firewall + DNS. Central issues the URL at init time; the browser needs to reach that host directly. -
policy_id mismatchcomplained about at central The zip doesn't contain the 5 canonical training files — reject it at the source (fix the student's tooling) rather than patching the agent.
pip install -r requirements.txt
export CENTRAL_API_URL=http://localhost:8001
export WORKER_JOIN_TOKEN=<paste>
export WORKER_STORAGE_ROOT=./_dev_storage
export WORKER_STATE_DIR=./_dev_state
python -m worker_agent.main