Files
spinifex/scripts/launch-gpu.sh
T
tomnewton-mulga 43fa6eae59 feat: Updated GPU-Passthrough - AMD support, GPU images, /demo folder (#217)
* feat: add GPU fields to NodeStatus and spx top nodes (mulga-996)

- Add TotalGPUs/AllocGPUs to NodeStatusResponse (zero on non-GPU nodes)
- Populate from gpuManager in handleNodeStatus
- Show GPU (used/total) column in spx top nodes; "-" when no GPUs present

* Fixed GPU passthrough and added test script

* feat: always-probe GPU at startup, surface capability in NodeStatusResponse

- Add gpuProbeResult struct and probeGPU() which runs at daemon startup
  regardless of gpu_passthrough config. Checks sysfs for GPUs, IOMMU
  groups, and vfio_pci module presence.
- Store gpuProbe on Daemon struct; split NewDaemon into probe (always)
  and activate (opt-in) phases.
- Populate GPUCapable, GPUPassthrough, GPUModels on NodeStatusResponse
  so operators can see hardware state before enabling passthrough.
- Update 'spx top' GPU column: '-' = no hardware, '0/N*' = capable but
  passthrough not active, 'used/total' = passthrough active.

* feat: SIGHUP hot-reload for GPU passthrough config (mulga-1001)

- Export GenerateGPUTypes for use by the reload path
- Add ResourceManager.reloadGPUTypes: mutates instanceTypes map in-place
  (preserves references held by instanceService) and updates NATS
  subscriptions without full daemon restart
- Separate SIGHUP from SIGTERM/SIGINT: setupReload() listens for SIGHUP
  in a context-aware loop; setupShutdown no longer intercepts SIGHUP
- Add reloadConfig() + applyGPUConfig(): re-reads spinifex.toml on signal,
  handles false→true (re-probe + activate) and true→false (refuse if
  instances running, otherwise deactivate) transitions

* installer grub image fixes

* made gpu-passthrough more user facing

* vpcd fixes to set br-wan to promisc for public IP DHCP processes

* fixed vpcd br-wan promisc and veth pair

* ssh keygen fix

* Fix: AMD GPU edge cases

* script for running 8x instances on SMCI computer

* fix: increase disk space on instances

* feat: multi-GPU passthrough support for g7e.12xlarge (2× MI350X)

- vm.VM: GPUPCIAddress string → GPUPCIAddresses []string for multi-GPU
- instancetypes: add g7e.12xlarge (2 GPUs), GPUCountForType(), AMDmi350x model
- instancetypes/generate: use GPUCountForType for GpuInfo.Count and TotalGpuMemoryInMiB
- gpu/manager: Release() loops all pool entries for instanceID (not just first)
- vm/lifecycle: loop over GPUPCIAddresses, emit gpu0/gpu1/... QEMU devices
- daemon/handlers: claim N GPUs in loop for both RunInstances and StartStoppedInstance
- daemon/vm_adapters: ReclaimByAddress loops GPUPCIAddresses; ReleaseGPU checks len > 0
- scripts/launch-gpu-fleet: fixed fleet — 6× g7e.4xlarge + 1× g7e.12xlarge

* feat: per-instance-type disk size in GPU fleet (300 GB single, 600 GB dual)

* fix: multi-gpu instance types

* fix: kill QEMU synchronously when QMP handshake fails to free VFIO device

When startQEMU succeeds but newQMPClientWithHandshake times out, the
instance has not yet been inserted into the manager map so vmMgr.Delete
in the rollback path is a no-op. QEMU keeps running and holds the VFIO
group open. The GPU pool entry gets freed, the next Claim returns the
same PCI address, and every subsequent QEMU fails with 'device or
resource busy'.

Fix: SIGKILL QEMU and wait for it to exit before returning the QMP
error, ensuring the VFIO device is released before the caller recycles
the GPU pool slot.

* fix: quarantine GPU after QEMU launch failure to prevent re-assignment cascade

When QEMU crashes during startup (e.g. pci_irq_handler assertion on a
specific GPU), the pool Release frees that address and the next NATS
request immediately re-claims the same broken device. Add MarkFailed()
to gpu.Manager which sets Available=false for a PCI address after a
launch failure, preventing re-assignment until the daemon restarts.

Call MarkFailed for each GPU address in both RunInstances and
StartStoppedInstance rollback paths after the Release succeeds.

* fixed instance fleet launch script

* remove known hosts and stagger provisioning

* improvements to launch script

* more upgrades

* single instance gpu instance provisioning

* fixed single instance provision

* smci computer demo dashboard

* fixes to launch script

* updated smci dashboard_server

* fixed demo dashboard

* updated dashboard.py

* feat: GPU Passthrough fixes to passthrough process - builds proper AMD or NVIDA ready images, generalised launch script

* feat: gpu-passthrough updating image builder to include gpu-ready ubuntu images

* fixing image builder process

* fixed image builder to remove /tmp/ file before import to free space

* fix: Updated COMMANDS.md to reflect new GPU management admin commands

* Update docs, update internal launch script to work with new SGs, test tweaks

* Updated Docs, fixed internal launch-gpu.sh script, test tweaks

* fxing CI race due to GPU load
2026-05-13 11:07:13 +10:00

249 lines
8.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
set -euo pipefail
export AWS_PROFILE=spinifex
# Resolve the actual user's home — $HOME is /root when run under sudo.
_USER_HOME=$(getent passwd "${SUDO_USER:-$(id -un)}" | cut -d: -f6)
SSH_KEY="${SSH_KEY:-${_USER_HOME}/.ssh/spinifex-key}"
SSH_USER="${SSH_USER:-ec2-user}"
SSH_TIMEOUT="${SSH_TIMEOUT:-300}"
usage() {
cat <<'EOF'
launch-gpu.sh — Launch a GPU instance from the Spinifex AMD or NVIDIA GPU AMI.
Detects the GPU brand from the specified instance type and selects the matching
pre-built AMI automatically. Use --amd or --nvidia to override.
Usage:
scripts/launch-gpu.sh --type <instance-type> [--disk GB] [--amd|--nvidia]
Options:
--type TYPE EC2 instance type to launch (required)
--disk GB Root disk size in GB (default: 50)
--ami NAME Override the AMI name to use (default: ubuntu-amd-gpu or ubuntu-nvidia-gpu)
--amd Force AMD GPU AMI (ubuntu-amd-gpu)
--nvidia Force NVIDIA GPU AMI (ubuntu-nvidia-gpu)
Env overrides:
SSH_KEY Path to SSH private key (default: ~/.ssh/spinifex-key)
SSH_USER SSH user inside guest (default: ec2-user)
SSH_TIMEOUT Seconds to wait for initial SSH (default: 300)
EOF
}
INSTANCE_TYPE=""
DISK_GB=50
GPU_BRAND=""
AMI_OVERRIDE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--type) INSTANCE_TYPE="$2"; shift 2 ;;
--disk) DISK_GB="$2"; shift 2 ;;
--ami) AMI_OVERRIDE="$2"; shift 2 ;;
--amd) GPU_BRAND="amd"; shift ;;
--nvidia) GPU_BRAND="nvidia"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
esac
done
list_gpu_types() {
aws ec2 describe-instance-types \
--query 'InstanceTypes[?GpuInfo].[InstanceType,GpuInfo.Gpus[0].Count,GpuInfo.Gpus[0].Manufacturer,GpuInfo.Gpus[0].Name]' \
--output table 2>/dev/null || true
}
if [ -z "$INSTANCE_TYPE" ]; then
echo "Error: --type is required."
echo ""
echo "Available GPU instance types on this node:"
list_gpu_types
exit 1
fi
# --- Validate instance type and fetch GPU info in one call ---
echo "==> Checking instance type: ${INSTANCE_TYPE}"
INSTANCE_INFO=$(aws ec2 describe-instance-types \
--query "InstanceTypes[?InstanceType=='${INSTANCE_TYPE}' && GpuInfo].[GpuInfo.Gpus[0].Count,GpuInfo.Gpus[0].Manufacturer,GpuInfo.Gpus[0].Name]" \
--output text 2>/dev/null || true)
if [ -z "$INSTANCE_INFO" ] || [ "$INSTANCE_INFO" = "None" ]; then
echo "'${INSTANCE_TYPE}' is not available or is not a GPU type on this node."
echo ""
echo "Available GPU instance types:"
list_gpu_types
exit 1
fi
IFS=$'\t' read -r GPU_COUNT GPU_MANUFACTURER GPU_NAME <<< "$INSTANCE_INFO"
echo " ${GPU_COUNT}× ${GPU_NAME} (${GPU_MANUFACTURER})"
# --- Resolve GPU brand ---
if [ -z "$GPU_BRAND" ]; then
case "$GPU_MANUFACTURER" in
AMD|amd) GPU_BRAND="amd" ;;
NVIDIA|nvidia) GPU_BRAND="nvidia" ;;
*)
echo "Cannot auto-detect GPU brand from manufacturer '${GPU_MANUFACTURER}'."
echo "Use --amd or --nvidia to specify explicitly."
exit 1
;;
esac
fi
case "$GPU_BRAND" in
amd) AMI_NAME="ubuntu-amd-gpu" ;;
nvidia) AMI_NAME="ubuntu-nvidia-gpu" ;;
esac
# --- Resolve AMI ---
AMI_LOOKUP="${AMI_OVERRIDE:-${AMI_NAME}}"
echo "==> Resolving AMI: ${AMI_LOOKUP}"
AMI_ID=$(aws ec2 describe-images \
--query "Images[?Name=='${AMI_LOOKUP}'].ImageId | [0]" --output text 2>/dev/null || true)
if [ -z "$AMI_ID" ] || [ "$AMI_ID" = "None" ]; then
echo "No AMI found with name '${AMI_LOOKUP}'."
echo "Build and import it first, or specify the AMI name with --ami <name>"
echo " scripts/build-system-image.sh scripts/images/ubuntu-gpu-${GPU_BRAND}.conf --import"
exit 1
fi
echo " $AMI_ID"
# --- Resolve subnet ---
echo "==> Resolving subnet"
SUBNET_ID=$(aws ec2 describe-subnets \
--query 'Subnets[?MapPublicIpOnLaunch==`true`].SubnetId | [0]' --output text 2>/dev/null || true)
if [ -z "$SUBNET_ID" ] || [ "$SUBNET_ID" = "None" ]; then
echo "No public subnet found"
exit 1
fi
echo " $SUBNET_ID"
VPC_ID=$(aws ec2 describe-subnets --subnet-ids "$SUBNET_ID" \
--query 'Subnets[0].VpcId' --output text 2>/dev/null || true)
if [ -z "$VPC_ID" ] || [ "$VPC_ID" = "None" ]; then
echo "Could not resolve VPC for subnet $SUBNET_ID"
exit 1
fi
# --- Ensure SSH security group ---
# The default VPC SG only allows intra-SG traffic; we need a group that
# permits port 22 from anywhere so the SSH wait below can connect.
SSH_SG_NAME="spinifex-gpu-ssh"
echo "==> Ensuring SSH security group (${SSH_SG_NAME})"
SG_ID=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=${SSH_SG_NAME}" "Name=vpc-id,Values=${VPC_ID}" \
--query 'SecurityGroups[0].GroupId' --output text 2>/dev/null || true)
if [ -z "$SG_ID" ] || [ "$SG_ID" = "None" ]; then
SG_ID=$(aws ec2 create-security-group \
--group-name "$SSH_SG_NAME" \
--description "Allow SSH access to GPU instances" \
--vpc-id "$VPC_ID" \
--query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0 >/dev/null
echo " Created $SG_ID"
else
echo " Reusing $SG_ID"
fi
# --- Ensure SSH key ---
if [ ! -f "$SSH_KEY" ]; then
echo "==> Generating SSH key"
sudo -u "${SUDO_USER:-$(id -un)}" mkdir -p "$(dirname "$SSH_KEY")"
sudo -u "${SUDO_USER:-$(id -un)}" ssh-keygen -t ed25519 -f "$SSH_KEY" -N ""
fi
# Delete before import — stale key pair material causes import to fail
# silently, leaving the instance unreachable via SSH.
echo "==> Importing SSH key"
aws ec2 delete-key-pair --key-name spinifex-key >/dev/null 2>&1 || true
aws ec2 import-key-pair --key-name spinifex-key \
--public-key-material "fileb://${SSH_KEY}.pub" >/dev/null 2>&1 || true
# --- Launch ---
echo "==> Launching ${INSTANCE_TYPE} (${DISK_GB} GB)..."
ID=$(aws ec2 run-instances \
--image-id "$AMI_ID" \
--instance-type "$INSTANCE_TYPE" \
--key-name spinifex-key \
--subnet-id "$SUBNET_ID" \
--security-group-ids "$SG_ID" \
--count 1 \
--block-device-mappings "[{\"DeviceName\":\"/dev/sda1\",\"Ebs\":{\"VolumeSize\":${DISK_GB},\"VolumeType\":\"gp3\",\"DeleteOnTermination\":true}}]" \
--query 'Instances[0].InstanceId' --output text)
if [ -z "$ID" ] || [ "$ID" = "None" ] || [ "$ID" = "null" ]; then
echo "run-instances returned no InstanceId"
exit 1
fi
echo " $ID"
# --- Wait for running ---
echo "==> Waiting for running state..."
COUNT=0; STATE="unknown"
while [ $COUNT -lt 120 ]; do
STATE=$(aws ec2 describe-instances --instance-ids "$ID" \
--query "Reservations[0].Instances[0].State.Name" --output text 2>/dev/null || echo "unknown")
[ "$STATE" = "running" ] && break
[ "$STATE" = "terminated" ] && { echo "Instance terminated unexpectedly"; exit 1; }
sleep 2; COUNT=$((COUNT + 2))
done
[ "$STATE" = "running" ] || { echo "Instance did not reach running state (last: $STATE)"; exit 1; }
# --- Wait for public IP ---
echo "==> Waiting for public IP..."
IP=""
for _i in $(seq 1 120); do
IP=$(aws ec2 describe-instances --instance-ids "$ID" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text 2>/dev/null || true)
[ -n "$IP" ] && [ "$IP" != "None" ] && [ "$IP" != "null" ] && break
sleep 1
done
[ -n "$IP" ] && [ "$IP" != "None" ] || { echo "No public IP assigned after 120s"; exit 1; }
echo " $IP"
# Clear stale known_hosts entry — instances reuse IPs across runs.
KNOWN_HOSTS="$HOME/.ssh/known_hosts"
[ -f "$KNOWN_HOSTS" ] && ssh-keygen -f "$KNOWN_HOSTS" -R "$IP" >/dev/null 2>&1 || true
# --- Wait for SSH ---
echo "==> Waiting for SSH..."
_ssh() {
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 -o BatchMode=yes \
-i "$SSH_KEY" "${SSH_USER}@${IP}" "$@"
}
elapsed=0
while [ "$elapsed" -lt "$SSH_TIMEOUT" ]; do
_ssh 'echo ready' >/dev/null 2>&1 && break
sleep 5; elapsed=$((elapsed + 5))
done
[ "$elapsed" -lt "$SSH_TIMEOUT" ] || { echo "SSH timeout after ${SSH_TIMEOUT}s"; exit 1; }
# --- GPU visibility check ---
echo "==> Checking GPU visibility..."
lspci_out=$(_ssh 'lspci -nn 2>/dev/null' || true)
if printf '%s\n' "$lspci_out" | grep -qiE "1002:|10de:|Instinct|Aqua|Processing accelerator|NVIDIA"; then
printf '%s\n' "$lspci_out" \
| grep -iE "1002:|10de:|Instinct|Aqua|Processing accelerator|NVIDIA" \
| sed 's/^/ ✓ /'
else
echo " WARNING: GPU not visible in lspci — check host-side passthrough assignment"
printf '%s\n' "$lspci_out" | head -20 | sed 's/^/ /'
fi
# --- Done ---
echo ""
echo "✅ ${INSTANCE_TYPE} ready — ${ID} (${IP})"
echo ""
echo " ssh -i ${SSH_KEY} -o StrictHostKeyChecking=no ${SSH_USER}@${IP}"
echo ""
echo " Terminate when done:"
echo " AWS_PROFILE=spinifex aws ec2 terminate-instances --instance-ids ${ID}"