Files
0raclus 1672858674 feat: prove a rented GPU runs a real model (Llama 3.2 on Apple Metal via DanteGPU dispatch)
Adds the "real model on a rented GPU" proof the rental-proof was building toward.

scripts/gpu-model-proof.sh — one command:
  - ensures mlx-lm in a throwaway venv (Apple Silicon / Metal),
  - starts a JetStream nats-server + the provider-daemon,
  - dispatches an ai-inference task over NATS whose payload (scripts/gpu_infer.py)
    loads a real 4-bit quantized LLM and generates text on the GPU,
  - prints the daemon-captured model output and tears everything down.

Verified on an Apple M4 Max: the daemon receives the task over NATS, runs
mlx-community/Llama-3.2-1B-Instruct-4bit on the Metal GPU (device "Device(gpu, 0)",
~235 tokens/sec, 0.81 GB GPU peak), and returns a real coherent answer with
exitCode 0. So the full chain is real end to end: detect GPU -> rent -> dispatch
over NATS -> daemon executes -> real model runs on the GPU -> output returned.

scripts/gpu_infer.py is the MLX inference payload (swap --model for any
mlx-community repo; a PyTorch payload works for CUDA/NVIDIA). docs/PROOF.md and
the README document the run; the only thing still unproven is *distributed*
multi-node inference (one model sharded across many rented GPUs).
2026-06-22 20:49:21 +03:00

122 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# gpu-model-proof.sh — prove that a RENTED GPU runs a REAL model.
#
# Dispatches an AI-inference task through the DanteGPU flow (NATS JetStream ->
# provider-daemon) whose payload loads a real quantized LLM and generates text
# on the local GPU. The daemon's captured stdout is the model's answer, the
# device line proves it ran on the accelerator (Apple Metal / "Device(gpu, 0)").
#
# Defaults to MLX (Apple Silicon). Requires: go, nats-server, python3.
# It creates a throwaway venv and installs mlx-lm on first run, and downloads a
# ~0.7 GB model the first time (cached afterwards). A JetStream nats-server is
# started and torn down; nothing you already run is touched.
#
# Usage: ./scripts/gpu-model-proof.sh [--model HF_MLX_REPO] [--prompt "..."]
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
MODEL="mlx-community/Llama-3.2-1B-Instruct-4bit"
PROMPT="In one sentence, what is a GPU and why is it good for AI?"
while [ $# -gt 0 ]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
*) echo "unknown arg: $1"; exit 2 ;;
esac
done
PORT_NATS=4222
VENV="/tmp/dante-mlx-venv"
TMP="$(mktemp -d /tmp/dante-gpuproof.XXXXXX)"
DAEMON_BIN="$TMP/dante-daemon"
NATS_PID=""; DAEMON_PID=""
cleanup() {
[ -n "$DAEMON_PID" ] && kill "$DAEMON_PID" 2>/dev/null || true
pkill -f "$DAEMON_BIN" 2>/dev/null || true
[ -n "$NATS_PID" ] && kill "$NATS_PID" 2>/dev/null || true
rm -rf "$TMP" 2>/dev/null || true
}
trap cleanup EXIT
export GOFLAGS=-mod=mod
echo "== ensuring mlx-lm is available =="
if ! "$VENV/bin/python" -c "import mlx_lm" 2>/dev/null; then
python3 -m venv "$VENV"
"$VENV/bin/pip" install --quiet --upgrade pip
"$VENV/bin/pip" install --quiet mlx-lm
fi
"$VENV/bin/python" -c "import mlx.core as mx; print(' accelerator:', mx.default_device())"
echo "== building daemon + starting NATS JetStream =="
go build -C clients/provider-daemon -o "$DAEMON_BIN" ./cmd/daemon
go build -C cmd/setup-nats -o "$TMP/setup-nats" .
mkdir -p "$TMP/nats"
nats-server -js -sd "$TMP/nats" -p $PORT_NATS >"$TMP/nats.log" 2>&1 &
NATS_PID=$!
sleep 1.5
"$TMP/setup-nats" >/dev/null
"$DAEMON_BIN" --config clients/provider-daemon/configs/config.yaml >"$TMP/daemon.log" 2>&1 &
DAEMON_PID=$!
sleep 3
grep -q "Successfully subscribed to JetStream" "$TMP/daemon.log" \
&& echo " daemon subscribed to tasks.dispatch.provider-daemon-01.*"
echo "== dispatching an AI-inference task over NATS (runs $MODEL on the GPU) =="
JOB="llm-$(date +%s)"
SCRIPT="PYTHONWARNINGS=ignore HF_HUB_DISABLE_PROGRESS_BARS=1 $VENV/bin/python $ROOT/scripts/gpu_infer.py '$MODEL' '$PROMPT' 2>/dev/null"
mkdir -p "$TMP/pub"
cat > "$TMP/pub/go.mod" <<EOF
module pub
go 1.24
require github.com/nats-io/nats.go v1.39.1
EOF
cat > "$TMP/pub/main.go" <<'EOF'
package main
import ("encoding/json";"fmt";"os";"time";"github.com/nats-io/nats.go")
func main(){
nc,_:=nats.Connect("nats://localhost:4222"); defer nc.Close()
js,_:=nc.JetStream()
job,script:=os.Args[1],os.Args[2]
t:=map[string]any{"job_id":job,"user_id":"proof","job_type":"ai-inference",
"job_name":"real-llm-on-gpu",
"job_params":map[string]any{"script_content":script,"script_interpreter":"/bin/sh"},
"execution_type":"script","assigned_provider_id":"provider-daemon-01",
"dispatched_at":time.Now().UTC().Format(time.RFC3339)}
d,_:=json.Marshal(t)
ack,err:=js.Publish(fmt.Sprintf("tasks.dispatch.provider-daemon-01.%s",job),d)
if err!=nil{fmt.Println("publish:",err);os.Exit(1)}
fmt.Printf(" dispatched %s (TASKS stream seq %d)\n",job,ack.Sequence)
}
EOF
(cd "$TMP/pub" && go mod tidy >/dev/null 2>&1 && go run . "$JOB" "$SCRIPT")
echo "== waiting for the GPU to run the model =="
for _ in $(seq 1 40); do
grep -q "Task execution finished" "$TMP/daemon.log" && break
sleep 1
done
echo
echo "== the rented GPU's answer (daemon-captured stdout) =="
"$VENV/bin/python" - "$TMP/daemon.log" <<'PY'
import json, re, sys
log = open(sys.argv[1]).read()
m = re.search(r'"stdout": "((?:[^"\\]|\\.)*)"', log)
if not m:
print("no model output captured"); sys.exit(1)
out = json.loads('"' + m.group(1) + '"')
r = json.loads(out)
print(f" device {r['device']}")
print(f" model {r['model']}")
print(f" prompt {r['prompt']}")
print(f" tokens/sec {r['tokens_per_sec']} ({r['tokens']} tokens, {r['gpu_peak_mem_gb']} GB GPU)")
print()
print(" >>", r['output'])
PY
echo
echo "== proof complete: a real model ran on the rented GPU, dispatched via DanteGPU =="