243 lines
11 KiB
Markdown
243 lines
11 KiB
Markdown
# Smoke Test Specification: Rent a GPU, Run Inference, Say Hello
|
|
|
|
## 1. Wanted Behavior
|
|
|
|
`smoke-run --vastai --api-key <key>` runs on the local machine. It:
|
|
1. Finds the cheapest available GPU on vast.ai and rents it.
|
|
2. Starts a local swactor node with iroh transport.
|
|
3. The rented instance boots our pre-built Docker image containing `gpu-node` + tinygrad.
|
|
4. The remote `gpu-node` reads `SEED_ADDR` from env and joins the local cluster via iroh/QUIC.
|
|
5. The local node discovers the remote `InferenceActor` via SWIM name resolution (`"inference"`).
|
|
6. The local node sends an `InferenceRequest` to the remote actor.
|
|
7. The remote actor runs tinygrad inference, replies with `InferenceResponse`.
|
|
8. Asserts the response is non-empty text and prints it.
|
|
9. Destroys the vast.ai instance.
|
|
10. Exits 0.
|
|
|
|
If anything fails, the script destroys the instance (if one was created) and exits 1.
|
|
|
|
Total wall-clock budget: 10 minutes.
|
|
|
|
```
|
|
local machine (smoke-run) vast.ai GPU (gpu-node)
|
|
| |
|
|
| 1. search offers (REST) |
|
|
| 2. create instance (REST) |
|
|
| SEED_ADDR=<local node id> |
|
|
| ------------------------------------> |
|
|
| | [pulling docker image]
|
|
| 3. poll vast.ai status every 10s | [gpu-node starting]
|
|
| ------------------------------------> | [tinygrad loading model into VRAM]
|
|
| "loading" |
|
|
| <------------------------------------ |
|
|
| "running" |
|
|
| <------------------------------------ |
|
|
| |
|
|
| 4. SWIM cluster join (iroh/QUIC) |
|
|
| <--------------------------------->>> |
|
|
| [cluster converged] |
|
|
| [name "inference" resolved] |
|
|
| |
|
|
| 5. InferenceRequest (swactor msg) |
|
|
| "Say hello" |
|
|
| ------------------------------------> |
|
|
| | [tinygrad forward pass]
|
|
| InferenceResponse |
|
|
| "Hello! How can I help you?" |
|
|
| <------------------------------------ |
|
|
| |
|
|
| 6. assert len(response) > 0 |
|
|
| 7. DELETE instance (vast.ai REST) |
|
|
| ------------------------------------> |
|
|
| 8. exit 0 |
|
|
```
|
|
|
|
---
|
|
|
|
## 2. What Already Exists
|
|
|
|
The entire pipeline is implemented and tested locally. The remaining work is deploying it to vast.ai for real.
|
|
|
|
### Implemented components
|
|
|
|
| Component | File(s) | Status |
|
|
|---|---|---|
|
|
| Message types + codec | `src/messages.rs` | Done, tested (T-codec: 7 tests) |
|
|
| InferenceActor + ProcessBridge | `src/inference_actor.rs` | Done, tested (T-actor: 4 tests) |
|
|
| RequestBridge (network→actor type bridge) | `src/inference_actor.rs` | Done |
|
|
| Iroh actor transport (shared) | `src/iroh_transport.rs` | Done, used by tests + binaries |
|
|
| vast.ai REST client | `src/vastai.rs` | Done, tested (T-vastai: 7 tests) |
|
|
| tinygrad worker (real GGUF model) | `tinygrad_worker.py` | Done, tested (T-worker: 5 test classes) |
|
|
| Cluster transport tests | `tests/t_cluster.rs` | Done (3 tests) |
|
|
| In-process integration test (echo) | `tests/t_integration.rs` | Done, passes |
|
|
| In-process integration test (tinygrad) | `tests/t_integration.rs` | Done, passes (`#[ignore]`, needs .venv) |
|
|
| `gpu-node` binary | `src/bin/gpu_node.rs` | Done, compiles |
|
|
| `smoke-run` binary (localhost + vastai) | `src/bin/smoke_run.rs` | Done, compiles |
|
|
| Binary e2e test (echo) | `tests/t_binary.rs` | Done, passes |
|
|
| Binary e2e test (tinygrad) | `tests/t_binary.rs` | Done, passes (`#[ignore]`, needs .venv) |
|
|
| Dockerfile | `Dockerfile` | Done |
|
|
|
|
### Test commands
|
|
|
|
```bash
|
|
# Fast tests (no GPU, no downloads, no vast.ai) — 22 tests
|
|
cargo test --manifest-path examples/single-gpu-inference/Cargo.toml
|
|
|
|
# Slow tests (downloads ~1GB GGUF model, runs tinygrad on CPU)
|
|
cargo test --manifest-path examples/single-gpu-inference/Cargo.toml -- --ignored
|
|
|
|
# Binary e2e on localhost (spawns gpu-node + smoke-run as child processes)
|
|
cargo test --manifest-path examples/single-gpu-inference/Cargo.toml binary_e2e_echo_worker
|
|
```
|
|
|
|
---
|
|
|
|
## 3. What Remains — Deployment to vast.ai
|
|
|
|
### 3.1 Fix relay mode for WAN
|
|
|
|
Both binaries currently use `RelayMode::Disabled`, which works on localhost but not over WAN. The remote `gpu-node` behind a vast.ai NAT cannot reach the local node without iroh relay servers.
|
|
|
|
**Changes needed:**
|
|
- `gpu-node`: change `RelayMode::Disabled` to `RelayMode::Default` so iroh uses its public relay infrastructure for NAT traversal.
|
|
- `smoke-run` (vastai path): same — use `RelayMode::Default`.
|
|
- The localhost path (`smoke-run --seed`) can keep `RelayMode::Disabled`.
|
|
|
|
### 3.2 Fix worker script path in Dockerfile
|
|
|
|
The `Dockerfile` copies `tinygrad_worker.py` to `/usr/local/share/tinygrad_worker.py`, but `gpu-node` defaults `WORKER_SCRIPT` to `./tinygrad_worker.py`.
|
|
|
|
**Fix**: either change the Dockerfile `COPY` destination to `/app/tinygrad_worker.py` and set `WORKDIR /app`, or set `ENV WORKER_SCRIPT=/usr/local/share/tinygrad_worker.py` in the Dockerfile.
|
|
|
|
### 3.3 Add `pid` to tinygrad worker ready signal
|
|
|
|
`tinygrad_worker.py` emits `{"status": "ready"}` but `echo_worker.py` emits `{"status": "ready", "pid": <pid>}`. The `InferenceActor` parses the `pid` field for `WorkerReady { pid }` status reporting. Without it, `worker_pid` is `None` — not fatal, but cleanup assertions in tests rely on it.
|
|
|
|
**Fix**: change the ready signal to `_write({"status": "ready", "pid": os.getpid()})`.
|
|
|
|
### 3.4 Build and push the Docker image
|
|
|
|
```bash
|
|
# Cross-compile gpu-node for linux/amd64 (if not already on linux/amd64)
|
|
cargo build --release --bin gpu-node --manifest-path examples/single-gpu-inference/Cargo.toml
|
|
|
|
# Build image
|
|
docker build -t <your-registry>/swactor-gpu:latest -f examples/single-gpu-inference/Dockerfile .
|
|
|
|
# Push to registry (vast.ai pulls from here)
|
|
docker push <your-registry>/swactor-gpu:latest
|
|
```
|
|
|
|
The image name in `vastai.rs::create_instance` is hardcoded to `"swactor-gpu:latest"`. Update this to match whatever registry you push to, or make it a parameter.
|
|
|
|
### 3.5 Run it for real
|
|
|
|
```bash
|
|
VAST_API_KEY=<key> cargo run --manifest-path examples/single-gpu-inference/Cargo.toml --bin single-gpu-inference -- \
|
|
--vastai --api-key <key> --gpu RTX_4090
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Architecture
|
|
|
|
### Message flow (same for localhost and vast.ai)
|
|
|
|
```
|
|
smoke-run gpu-node
|
|
───────── ────────
|
|
rt.send_to(bridge_addr, InferenceRequest)
|
|
→ codec encodes → TransportRouter
|
|
→ IrohActorTransport (QUIC to gpu-node)
|
|
drain_and_collect_reply_addrs()
|
|
→ decode_wire → codecs.receive()
|
|
→ rt.deliver_raw(bridge_addr, InferenceRequest)
|
|
→ extract reply_to, add return transport route
|
|
RequestBridge.handle()
|
|
→ ctx.send(inference_addr, InferenceActorMsg::Request)
|
|
InferenceActor.handle()
|
|
→ JSON to tinygrad_worker.py stdin
|
|
tinygrad_worker.py
|
|
→ Transformer forward pass
|
|
→ JSON response to stdout
|
|
InferenceActor.process_output_line()
|
|
→ ctx.send(reply_to, InferenceResponse)
|
|
→ TransportRouter → IrohActorTransport (QUIC back)
|
|
drain_actor_messages()
|
|
→ decode_wire → codecs.receive()
|
|
→ rt.deliver_raw(inbox_addr, InferenceResponse)
|
|
response_inbox.try_recv() ✓
|
|
```
|
|
|
|
### Name discovery
|
|
|
|
The `gpu-node` registers its `RequestBridge` under the name `"inference"` via `DistributedNode::register_name`. This propagates through SWIM gossip piggyback. The `smoke-run` orchestrator calls `driver.node().resolve_name("inference")` to discover the bridge's `ActorAddress` without needing to know it ahead of time.
|
|
|
|
### Dynamic return routing
|
|
|
|
The `gpu-node`'s drain loop inspects incoming `InferenceRequest` payloads to extract the `reply_to` address. It then dynamically registers a transport route for that address pointing back to the only alive SWIM member (the orchestrator). This is necessary because the `gpu-node` doesn't know the orchestrator's inbox address at startup.
|
|
|
|
---
|
|
|
|
## 5. tinygrad Worker
|
|
|
|
`tinygrad_worker.py` — managed by swactor's process crate via stdin/stdout JSON.
|
|
|
|
**Model**: `llama3.2:1b` from tinygrad's built-in GGUF catalog (~1GB download, fits in any modern GPU's VRAM). Loaded via `tinygrad.apps.llm.Transformer.from_gguf()`.
|
|
|
|
**Protocol**:
|
|
```
|
|
← stdout: {"status": "ready", "pid": 12345}
|
|
→ stdin: {"prompt": "Say hello", "max_tokens": 64, "temperature": 0.7}
|
|
← stdout: {"response": "Hello! How can I help you today?"}
|
|
→ stdin: {invalid json}
|
|
← stdout: {"error": "invalid JSON: ..."}
|
|
```
|
|
|
|
**Modes**:
|
|
- Default: downloads and loads real GGUF model, runs real inference.
|
|
- `--stub`: canned responses, no tinygrad import (for fast protocol tests).
|
|
- `--model <name>`: override model (e.g., `qwen3:0.6b` for smaller download).
|
|
|
|
**Environment**:
|
|
- `CUDA=1` → tinygrad uses CUDA backend (GPU).
|
|
- No env var → tinygrad auto-detects (CUDA if available, else CPU).
|
|
- `PYTHON=1` → forces tinygrad's pure-Python CPU backend (no clang needed).
|
|
|
|
---
|
|
|
|
## 6. Error Modes
|
|
|
|
| Failure | Detection | Response |
|
|
|---------|-----------|----------|
|
|
| No GPU offers available | vast.ai returns empty list | Print error, exit 1. No cleanup needed. |
|
|
| Instance creation rejected | API returns non-success | Print error, exit 1. No cleanup needed. |
|
|
| Instance never reaches `running` | 60 polls exhausted or terminal status | Destroy instance, exit 1. |
|
|
| Cluster never converges | SWIM timeout (2 min) | Destroy instance, exit 1. |
|
|
| Name `"inference"` never resolves | Timeout (1 min) | Destroy instance, exit 1. |
|
|
| tinygrad worker never reports ready | Process timeout (10 min) | Destroy instance, exit 1. |
|
|
| Empty/malformed response | Assertion on response text | Destroy instance, exit 1. |
|
|
| Destroy fails | Catch around cleanup | Print warning, exit 1. |
|
|
|
|
Every path that allocates an instance also destroys it.
|
|
|
|
---
|
|
|
|
## 7. Environment Requirements
|
|
|
|
- Rust toolchain
|
|
- Python 3.10+ with tinygrad 0.12.0 + numpy (in `.venv/`)
|
|
- Docker (for building the GPU node image)
|
|
- `VAST_API_KEY` (vast.ai deployment only — not needed for local tests)
|
|
- Network access to iroh relay servers (vast.ai deployment only)
|
|
|
|
---
|
|
|
|
## 8. What This Proves
|
|
|
|
1. swactor nodes on rented GPUs can join a cluster with a local node over WAN via iroh/QUIC.
|
|
2. swactor messages traverse the WAN transparently — no HTTP layer needed for node-to-node communication.
|
|
3. tinygrad can load and run a LLaMA model on rented hardware via its CUDA backend.
|
|
4. The process crate manages the Python child process lifecycle cleanly.
|
|
5. SWIM gossip propagates actor name registrations, enabling dynamic service discovery.
|
|
6. The full lifecycle (rent → cluster → discover → infer → teardown) is fully automated.
|