#!/usr/bin/env bash # upgrade-irv-ml1-cuda.sh — bump irv-ml1's NVIDIA driver to support # CUDA 12.9+ (currently capped at 12.8 per driver 570.124.06). # # Why two phases: the upgrade involves a reboot, which kills the SSH # session mid-flight. Splitting at the reboot lets phase 2 wait for # the host to come back and verify the new driver took. # # Usage: # scripts/upgrade-irv-ml1-cuda.sh phase1 # Stop GPU containers, add NVIDIA CUDA APT repo, install # cuda-drivers (currently 580 series), reboot. # (interactive — sudo password prompted once) # # ... wait ~2 min for irv-ml1 to come back ... # # scripts/upgrade-irv-ml1-cuda.sh phase2 # Verify new driver + CUDA version, restart containers, # and print the next step (flip Kokoro to GPU variant). # # Implementation note: the remote work runs from a script scp'd to # /tmp on irv-ml1, then invoked via `ssh -t host bash /tmp/script`. # Piping a heredoc to `ssh -t host bash -s` doesn't work — `-t` needs # a real TTY but heredoc puts stdin in pipe mode (orientation.md # gotcha; tripped over once already). # # Idempotent: rerunning phase1 after the repo is already added is a # no-op until the apt step. phase2 can be re-run safely; container # restarts via `docker compose up -d` are idempotent. set -euo pipefail HOST=irv-ml1 REMOTE_SCRIPT=/tmp/upgrade-cuda-remote.sh say() { printf '\033[1;36m▸ %s\033[0m\n' "$*"; } ok() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; } warn() { printf '\033[1;33m⚠ %s\033[0m\n' "$*"; } err() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; } # Build the remote-side script as a local tempfile, scp it, exec it # via `ssh -t`. The remote script takes its own phase as $1. write_remote_script() { local local_tmp local_tmp=$(mktemp) cat > "$local_tmp" <<'REMOTE' #!/usr/bin/env bash # Remote half of upgrade-irv-ml1-cuda.sh — runs on irv-ml1. set -euo pipefail CONTAINERS=(comfyui cosyvoice qwen3-tts index-tts parakeet kokoro) remote_phase1() { echo "=== current driver + CUDA ===" nvidia-smi --query-gpu=driver_version --format=csv,noheader nvidia-smi | grep "CUDA Version" echo echo "=== prime sudo (one password prompt for everything below) ===" sudo -v echo echo "=== ensuring NVIDIA CUDA APT repo for Debian 12 is configured ===" # Clean up any orphan file from a prior failed run (script used to # add /etc/apt/sources.list.d/nvidia-cuda.list with its own keyring, # which collided with the upstream-provided cuda-debian12-x86_64.list # using cuda-archive-keyring.gpg; APT refused both with # 'Conflicting values set for option Signed-By'). if [ -f /etc/apt/sources.list.d/nvidia-cuda.list ]; then echo " removing orphan /etc/apt/sources.list.d/nvidia-cuda.list" sudo rm -f /etc/apt/sources.list.d/nvidia-cuda.list /etc/apt/keyrings/nvidia-cuda.gpg fi # Detect an existing cuda repo entry pointing at developer.download.nvidia.com. # If found, trust it (it's how driver 570 got installed in the first place). if grep -RlsE 'developer\.download\.nvidia\.com/compute/cuda/repos/debian12' /etc/apt/sources.list.d/ \ >/dev/null; then echo " cuda repo already configured — skipping add" else echo " no cuda repo found — adding upstream-style entry" sudo install -d -m 0755 /usr/share/keyrings sudo apt-get install -y curl gpg curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/3bf863cc.pub \ | sudo gpg --dearmor --yes -o /usr/share/keyrings/cuda-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/cuda-archive-keyring.gpg] https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/ /" \ | sudo tee /etc/apt/sources.list.d/cuda-debian12-x86_64.list >/dev/null fi sudo apt-get update -qq echo echo "=== cuda-drivers candidate ===" # Don't `| head` here — apt-cache policy emits enough that head # closes the pipe early and SIGPIPEs apt-cache, which under # set -o pipefail aborts the whole script before we get to the # actual install + reboot. Output is short anyway. apt-cache policy cuda-drivers echo echo "=== snapshotting current nvidia packages (rollback reference) ===" dpkg -l | awk '/^ii\s+(nvidia-|cuda-)/ {print $2 "=" $3}' \ | sudo tee /tmp/nvidia-pre-upgrade.txt >/dev/null echo "snapshot at /tmp/nvidia-pre-upgrade.txt" echo echo "=== stopping GPU containers ===" for stack in "${CONTAINERS[@]}"; do if [ -d "/opt/docker/compose/$stack" ]; then echo " stopping $stack" (cd "/opt/docker/compose/$stack" && docker compose stop) || echo " (no-op)" fi done echo echo "=== installing cuda-drivers (DKMS rebuild ~3-5 min) ===" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y cuda-drivers echo echo "=== rebooting in 5 s ===" sleep 5 sudo systemctl reboot } remote_phase2() { echo "=== new driver + CUDA ===" nvidia-smi --query-gpu=driver_version --format=csv,noheader nvidia-smi | grep "CUDA Version" echo echo "=== restarting GPU containers ===" for stack in comfyui cosyvoice qwen3-tts index-tts parakeet; do if [ -d "/opt/docker/compose/$stack" ]; then echo " starting $stack" (cd "/opt/docker/compose/$stack" && docker compose up -d) || echo " (failed — check manually)" fi done sleep 8 echo echo "=== container status ===" # `| head -15` would SIGPIPE docker ps under pipefail; just print all. docker ps --format "table {{.Names}}\t{{.Status}}" } remote_rollback() { echo "=== rollback: reinstalling pre-upgrade nvidia packages ===" [ -f /tmp/nvidia-pre-upgrade.txt ] || { echo "no snapshot found"; exit 1; } sudo apt-get install -y --allow-downgrades $(tr '\n' ' ' < /tmp/nvidia-pre-upgrade.txt) echo "=== rebooting in 5 s ===" sleep 5 sudo systemctl reboot } case "${1:-}" in phase1) remote_phase1 ;; phase2) remote_phase2 ;; rollback) remote_rollback ;; *) echo "remote: usage: $0 {phase1|phase2|rollback}" >&2; exit 2 ;; esac REMOTE scp -q "$local_tmp" "$HOST:$REMOTE_SCRIPT" rm -f "$local_tmp" ssh "$HOST" "chmod +x $REMOTE_SCRIPT" } phase1() { say "Phase 1: pre-flight + install + reboot" say "Staging remote script on $HOST" write_remote_script say "Running phase1 (sudo will prompt once via ssh -t)" # No piped stdin — ssh -t's TTY is real and sudo can prompt. ssh -t "$HOST" "$REMOTE_SCRIPT phase1" ok "Phase 1 dispatched. Host rebooting." say "Wait ~2 min, then run: $0 phase2" } phase2() { say "Phase 2: wait for host, verify, restart containers" say "waiting for $HOST to come back online…" for i in $(seq 1 60); do if ssh -o ConnectTimeout=3 -o BatchMode=yes "$HOST" true 2>/dev/null; then ok "$HOST reachable" break fi printf '.' sleep 3 done echo # Re-stage in case the previous /tmp script was wiped on reboot. write_remote_script ssh "$HOST" "$REMOTE_SCRIPT phase2" ok "Phase 2 complete." cat <<'NEXT' ──────────────────────────────────────────────────────────────────── NEXT — flip Kokoro from cpu → gpu variant now that CUDA 12.9 is live: ssh irv-ml1 ' cd /opt/docker/compose/kokoro sed -i "s/^KOKORO_VARIANT=.*/KOKORO_VARIANT=gpu/" .env sed -i "s/^KOKORO_USE_GPU=.*/KOKORO_USE_GPU=true/" .env sed -i "s/^KOKORO_GPU_DEVICES=.*/KOKORO_GPU_DEVICES=0/" .env ' scripts/elway irv-ml1 --playbook playbooks/deploy-kokoro.yaml After that, the two outstanding deploys: scripts/elway irv-ml1 --playbook playbooks/deploy-vibevoice.yaml scripts/elway irv-ml1 --playbook playbooks/deploy-chatterbox.yaml ──────────────────────────────────────────────────────────────────── NEXT } rollback() { say "Rollback: reinstalling pre-upgrade nvidia packages on $HOST" write_remote_script ssh -t "$HOST" "$REMOTE_SCRIPT rollback" ok "Rollback dispatched. Host rebooting." } case "${1:-}" in phase1) phase1 ;; phase2) phase2 ;; rollback) rollback ;; *) echo "usage: $0 {phase1|phase2|rollback}" >&2; exit 2 ;; esac