CosmicAC Logo
Installation

Requirements

Confirm your Kubernetes cluster meets these requirements before CosmicAC can run jobs on it.

Before you deploy CosmicAC on your host machine, your Kubernetes cluster must meet the requirements on this page so CosmicAC can run jobs on it.

The CosmicAC stack includes a GPU worker, cosmicac-wrk-server-k8s-nvidia. The worker is a control-plane agent that connects to your cluster's Kubernetes API. It creates GPU virtual machines as KubeVirt VMIs, along with their supporting resources. The worker doesn't install the operators, drivers, or node configuration it relies on. You install and maintain them on your cluster. For how the worker connects to your cluster and runs jobs, see Deployment architecture.

Use the readiness checklist that follows to confirm your cluster is ready, not to build one. Any version or value on this page comes from the CosmicAC reference cluster. Treat it as a reference point, not a hard requirement, unless the page marks it as required.

Readiness checklist

Your cluster is ready when all the following are true:

The NVIDIA driver and Fabric Manager often come from the NVIDIA GPU Operator, but the Operator itself isn't a readiness gate. The worker consumes GPUs through KubeVirt PCI passthrough, so the hard requirement is the passthrough path in Section 3, not the GPU Operator's health. On the CosmicAC reference cluster, VMIs run normally even when the GPU Operator's ClusterPolicy reports notReady.

1. Cluster baseline

SettingRequirementNotes
Kubernetes versionA version supported by KubeVirt 1.7 and CDI 1.62Reference v1.33.x. Newer minor versions are fine if KubeVirt and CDI still support them.
Container runtimeAny CRI. Reference CRI-O, and containerd also worksThe worker passes GPUs to VMs through PCI passthrough, so the cluster doesn't need the NVIDIA container runtime for this path.
Host OS on GPU nodesLinux with IOMMU and VFIO supportReference Ubuntu 24.04 LTS, specifically 24.04.3 with kernel 6.8.x. See Section 2.
CNIAny standard CNIReference Calico. Overlay-networking workloads layer Multus on top of your CNI rather than replacing it. See Section 8.
DNS and egressWorking cluster DNS and outbound egress from VMIsNeeded so guests can docker login and pull images. node-local-dns is optional and isn't a worker prerequisite. The reference cluster uses it for cache parity.

2. GPU node requirements

You need at least one GPU node. The CosmicAC reference cluster uses eight NVIDIA H100 80 GB SXM GPUs and eight ConnectX-7 InfiniBand NICs per node, but a single GPU is the minimum for a single-node workload.

2.1 Node labels

Each GPU node must carry the following labels:

LabelWhy it matters
nvidia.com/gpu.present=trueNode selector for the KubeVirt GPU device plugin daemonset. Without this label, the node doesn't advertise the passthrough GPU resource. See Section 3.
node-role.kubernetes.io/kubevirtMarks the node as a KubeVirt VM host so virt-handler schedules VMIs there. Any KubeVirt VM-host marker works. This is the reference.

The reference cluster also carries a node-role.kubernetes.io/gpu label, but the worker's VMI spec doesn't select it. The label is informational and not required for the worker.

2.2 Whole-device passthrough (VFIO)

VMIs claim whole GPUs and IB PFs through PCI passthrough, so each GPU node must bind those devices to vfio-pci at the host level. Each GPU node must meet the following:

  • IOMMU enabled and the host GPU driver kept off the passed-through devices
  • Every GPU PCI ID and every IB PF PCI ID bound to vfio-pci
  • NVSwitch left on its native driver, with Fabric Manager healthy so NVLink works across the passed-through GPUs

The NVIDIA Network Operator isn't required for the current VFIO and IB mechanism. IB PFs appear as plain passthrough PCI devices, not through the Network Operator.

2.3 Verify resource advertisement

After host preparation and the KubeVirt GPU device plugin from Section 3, each GPU node must advertise the GPU as an allocatable resource named nvidia.com/<gpuType>. For multi-node jobs, it must also advertise the IB PF as mellanox.com/<ibName>.

On the CosmicAC reference cluster, each GPU node advertises the following resources:

nvidia.com/GH100_H100_SXM5_80GB = 8
mellanox.com/cx7_ib_pf          = 8
nvidia.com/gpu                  = 0   # expected: the worker requests the passthrough name, not nvidia.com/gpu

nvidia.com/gpu can legitimately be 0. The worker requests nvidia.com/<gpu.type> and mellanox.com/<ib_name>, not the generic nvidia.com/gpu, so the advertised name must match the name you configure the worker to request. The worker takes the InfiniBand name from K8S_IB_NAME in your deployment configuration.

3. GPU and virtualization platform

This section lists the hard requirements for the worker's passthrough path, then the reference implementation that satisfies them.

3.1 Hard requirements

RequirementDetail
KubeVirt installed and healthyRuns VMs as VirtualMachineInstance. Reference version v1.7.0.
KubeVirt HostDevices feature gate enabledRequired for PCI passthrough of GPUs and IB into VMIs.
CDI installed and healthyImports VM root disks from a registry into DataVolumes. Reference version v1.62.0.
KubeVirt GPU device plugin runningAdvertises the passed-through GPU resource to KubeVirt. Reference daemonset nvidia-kubevirt-gpu-dp-daemonset, image docker.io/anurlan/kubevirt-gpu-device-plugin:v1.5.0-fm, args --fm-enabled=true --fm-address=127.0.0.1:6666, nodeSelector nvidia.com/gpu.present=true.
permittedHostDevices configuredThe KubeVirt CR must permit your GPU PCI ID, and the IB PCI ID for multi-node. See Section 3.3.
NVIDIA driver and Fabric Manager healthy on the hostAny delivery method works, whether the GPU Operator, a custom node image, or a manual install. Required for the GPUs to be usable and for NVLink.

3.2 Reference implementation

On the CosmicAC reference cluster, the following components meet these requirements:

ComponentNamespaceVersionNotes
NVIDIA GPU Operatorgpu-operatorv25.3.1Delivers the NVIDIA driver, container toolkit, DCGM, and Fabric Manager components. Its ClusterPolicy health isn't a worker readiness gate. VMIs run even when it reports notReady.
Node Feature Discoverygpu-operator0.17.3Ships with the GPU-operator chart.

The GPU Operator installs the nvidia and nvidia-cdi RuntimeClasses on the reference cluster, but they aren't a prerequisite. The worker's VMIs don't set runtimeClassName.

3.3 Required KubeVirt permittedHostDevices

KubeVirt allows passthrough only for PCI devices you explicitly permit. The kubevirt/kubevirt CR must permit your GPU and IB PFs in its permittedHostDevices. The reference value is as follows:

spec:
  configuration:
    permittedHostDevices:
      pciHostDevices:
        - pciVendorSelector: "10DE:2330"          # NVIDIA H100 SXM5 80GB
          resourceName: nvidia.com/GH100_H100_SXM5_80GB
          externalResourceProvider: true          # advertised by the KubeVirt GPU device plugin
        - pciVendorSelector: "15B3:1021"          # Mellanox ConnectX-7 IB PF
          resourceName: mellanox.com/cx7_ib_pf
          externalResourceProvider: false

On different hardware, replace the pciVendorSelector PCI vendor:device pair and the resourceName to match your GPUs and NICs. The resourceName must match what the worker requests. See Section 2.3.

4. Storage

The worker creates DataVolumes for VM root disks and optional PersistentVolumeClaims for external data disks, with storageClassName: local-path. Your cluster must meet the following requirements:

  • A working StorageClass named local-path must exist. It doesn't have to be the cluster default, but prefer WaitForFirstConsumer so the scheduler places the VMI on a GPU node before it provisions the disk locally.
  • CDI must have a scratch StorageClass set for image imports. Reference value scratchSpaceStorageClass: local-path with the HonorWaitForFirstConsumer feature gate enabled.

On the CosmicAC reference cluster, each node uses the local-path provisioner rancher.io/local-path with WaitForFirstConsumer. Provide enough local NVMe storage. VM root disks are commonly 150Gi or larger, so size the storage for your model cache.

4.1 Team storage volumes

Team storage volumes are optional. A team storage volume is persistent storage that a team attaches to its GPU Container Jobs. Each volume is a Gcore file share, so volumes work only when your cluster runs on Gcore. On any other cluster, leave the feature off.

To turn on volumes for a rack, set the Gcore project, region, and API token for its worker. See Team storage volumes. When a user creates a volume, CosmicAC creates a Network File System (NFS) file share through the Gcore API. Gcore then creates a StorageClass named gcore-nfs-<share-id> in your cluster.

Volumes also require the following:

5. Registry credentials in ghcr-pull-cred

The worker pulls private images from the GitHub Container Registry (GHCR) at two stages. Each stage reads different keys from the same secret in your workload namespace:

ConsumerWhenKeys it reads
CDI importer podBefore the VM boots, imports the root-disk image into a DataVolumeaccessKeyId, secretKey
In-guest docker-bootstrap.shAfter the VM boots, logs in to the registry and pulls the inference containerDOCKER_REGISTRY, DOCKER_USERNAME, DOCKER_TOKEN

KubeVirt references the secret as the vmImagePullSecret, named ghcr-pull-cred by default. The credentials are a GitHub personal access token (PAT) with at least read:packages, SSO-authorized for the tetherto org. Create the secret with all five keys:

kubectl -n <workload-namespace> create secret generic ghcr-pull-cred \
  --from-literal=accessKeyId='<github-username>' \
  --from-literal=secretKey='<github-PAT>' \
  --from-literal=DOCKER_REGISTRY='ghcr.io' \
  --from-literal=DOCKER_USERNAME='<github-username>' \
  --from-literal=DOCKER_TOKEN='<github-PAT>'

The resulting secret holds these five keys:

KeyConsumer
accessKeyIdCDI importer, as the registry username
secretKeyCDI importer, as the registry token (PAT)
DOCKER_REGISTRYIn-guest docker-bootstrap.sh, set to ghcr.io
DOCKER_USERNAMEIn-guest docker-bootstrap.sh
DOCKER_TOKENIn-guest docker-bootstrap.sh

5.1 Bootstrap ConfigMap

Workloads that boot an in-guest container mount a bootstrap-cm ConfigMap as a CD-ROM. The ConfigMap must exist in the workload namespace before the worker starts. The ConfigMap holds two scripts and a systemd unit for each script. The bootstrap script prepares the guest and starts the job's container. The host agent script reports details about the guest to the container.

The worker repository ships a ready-to-apply manifest at setup/cfg-create.yml. Change its namespace to your workload namespace, then apply it:

kubectl apply -f setup/cfg-create.yml

The following is the complete ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: bootstrap-cm
  namespace: <workload-namespace>   # e.g. cosmic-ac

data:
  docker-bootstrap.sh: |
    #!/bin/bash
    set -eu

    # Helper: wait for block device
    wait_for_dev() { for i in {1..60}; do [ -b "$1" ] && return 0; sleep 1; done; echo "ERROR: $1 not found"; lsblk -b; exit 1; }

    # Wait out cloud-init / unattended-upgrades / multi-node IB apt holding dpkg.
    for _ in $(seq 1 300); do
      fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || break
      sleep 1
    done
    for _ in 1 2 3 4 5; do
      DEBIAN_FRONTEND=noninteractive apt-get update -y \
        && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates docker.io parted e2fsprogs util-linux jq \
        && break
      sleep 10
    done
    dpkg -s docker.io >/dev/null 2>&1 || { echo "ERROR: apt install failed"; exit 1; }
    systemctl enable --now docker

    # Setup PVC disk
    ROOT_DISK="$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | xargs basename)"
    [ -f /etc/default/docker-bootstrap ] && source /etc/default/docker-bootstrap
    MNT="${MNT:-}"
    DISK_SERIAL="${DISK_SERIAL:-}"
    ENV_FILE="/etc/default/docker-bootstrap.env"

    if [ -n "${MNT}" ]; then
      # A team storage volume is addressed by its virtio serial. Size alone cannot
      # identify it, and picking the wrong disk would reformat somebody's data.
      if [ -n "${DISK_SERIAL}" ]; then
        BY_ID="/dev/disk/by-id/virtio-${DISK_SERIAL}"
        wait_for_dev "${BY_ID}"
        DISK="$(readlink -f "${BY_ID}")"
        [ -b "${DISK}" ] || { echo "ERROR: ${BY_ID} did not resolve to a block device"; lsblk -b; exit 1; }
      else
        # Legacy instances created before DISK_SERIAL: take the largest non-root
        # disk. A provisioner may hand back slightly less than the requested size
        # (KubeVirt itself tolerates up to 10 %), so no absolute floor is applied
        # beyond ruling out the cloud-init disk.
        PVC_DISK_NAME="$(lsblk -b -dn -o NAME,TYPE,SIZE | awk -v r="${ROOT_DISK}" '$2=="disk" && $1!=r && $1!~/^(sr|loop)/ && $3>=1073741824 {print $1,$3}' | sort -k2,2nr | head -n1 | awk '{print $1}')"
        [ -z "${PVC_DISK_NAME}" ] && { echo "ERROR: Could not find a data disk to mount at ${MNT}"; lsblk -b; exit 1; }
        DISK="/dev/${PVC_DISK_NAME}"
      fi

      PART="${DISK}1"
      wait_for_dev "${DISK}"

      # A reattached volume already carries its GPT label and ext4 filesystem, so
      # both of these are skipped and the existing data is preserved.
      [ ! -b "${PART}" ] && { parted -s "${DISK}" mklabel gpt mkpart primary ext4 1MiB 100%; udevadm settle || true; }
      wait_for_dev "${PART}"
      blkid "${PART}" | grep -q 'TYPE=' || mkfs.ext4 -F "${PART}"

      mkdir -p "${MNT}"
      UUID="$(blkid -s UUID -o value "${PART}")"
      grep -q "${UUID}" /etc/fstab || echo "UUID=${UUID} ${MNT} ext4 defaults,nofail 0 2" >> /etc/fstab
      mount -a
    else
      echo "MNT not set, skipping external disk setup"
    fi

    # Docker login and pull
    SECRET_MNT="/mnt/registrycred"
    mkdir -p "${SECRET_MNT}"
    if ! mountpoint -q "${SECRET_MNT}"; then
      wait_for_dev /dev/sr0
      mount -t iso9660 -o ro /dev/sr0 "${SECRET_MNT}"
    else
      echo "${SECRET_MNT} already mounted, reusing"
    fi

    REGISTRY="$(cat "${SECRET_MNT}/DOCKER_REGISTRY")"
    USERNAME="$(cat "${SECRET_MNT}/DOCKER_USERNAME")"
    DOCKER_CONFIG="/run/docker-config"
    mkdir -p "${DOCKER_CONFIG}" && chmod 700 "${DOCKER_CONFIG}"

    cat "${SECRET_MNT}/DOCKER_TOKEN" | docker login "${REGISTRY}" -u "${USERNAME}" --password-stdin

    IMAGE_FULL="${IMG:-${USERNAME}/cosmicac-wrk-agent-instance:latest}"
    NESTED_IMG="${NESTED_IMG:-}"

    # Retry each pull up to 3 times (transient registry/network failures)
    pull_with_retry() {
      local img="$1" attempt=1
      until docker pull "${img}"; do
        if [ "${attempt}" -ge 3 ]; then
          echo "ERROR: docker pull failed for ${img} after ${attempt} attempts"
          return 1
        fi
        echo "WARN: docker pull failed for ${img} (attempt ${attempt}/3), retrying..."
        attempt=$((attempt + 1))
        sleep $((attempt * 2))
      done
    }

    # Pull agent + nested images in parallel (each with up to 3 retries)
    IMAGES=("${IMAGE_FULL}")
    [ -n "${NESTED_IMG}" ] && [ "${NESTED_IMG}" != "${IMAGE_FULL}" ] && IMAGES+=("${NESTED_IMG}")

    PIDS=()
    for img in "${IMAGES[@]}"; do
      pull_with_retry "${img}" & PIDS+=($!)
    done
    ec=0; for p in "${PIDS[@]}"; do wait "$p" || ec=1; done
    [ "$ec" -eq 0 ] || { echo "ERROR: image pull failed"; exit 1; }

    docker logout "${REGISTRY}" || true
    rm -rf "${DOCKER_CONFIG}" && umount "${SECRET_MNT}" || true

    # Run container if not already running
    CONTAINER_NAME="${NAME:-cosmicac-container}"
    if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
      echo "Container ${CONTAINER_NAME} already running"
    elif docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
      # Created/Exited from a prior failed start — start it (systemd Restart retries if this fails).
      docker start "${CONTAINER_NAME}"
    elif [ -n "${IMG}" ]; then
      # Host agent FIFO lives under /var/run/cosmicac (bind-mounted into agent).
      mkdir -p /var/run/cosmicac/host-agent
      if [ -x /usr/local/bin/cosmicac-host-net-agent.sh ]; then
        systemctl daemon-reload 2>/dev/null || true
        # Overlay iface (if any) is discovered into /etc/default/cosmicac-overlay by cloud-init.
        systemctl enable --now cosmicac-host-net-agent.service 2>/dev/null \
          || nohup env HOST_AGENT_DIR=/var/run/cosmicac/host-agent /usr/local/bin/cosmicac-host-net-agent.sh >/var/log/cosmicac-host-net-agent.log 2>&1 &
        # Wait briefly so cmd_in.pipe exists before the agent container starts.
        for i in $(seq 1 40); do
          [ -p /var/run/cosmicac/host-agent/cmd_in.pipe ] && break
          sleep 0.25
        done
      fi

      # Inference: nested container gets GPUs; --gpus all on agent can leave it Created.
      BASE_CMD="docker run -d --name ${CONTAINER_NAME} --init --restart unless-stopped --ipc=host"
      [ -z "${NESTED_IMG:-}" ] && BASE_CMD="${BASE_CMD} --gpus all"
      [ -n "${MNT}" ] && BASE_CMD="${BASE_CMD} -v ${MNT}:${MNT}"
      # Nested docker (INFERENCE_MODE=docker) needs host docker socket + host-agent FIFOs.
      # OVERLAY_IFACE is appended to the env file by cosmicac-setup-overlay-nic.sh on multi-node guests.
      if [ -n "${NESTED_IMG}" ]; then
        mkdir -p /var/run/cosmicac/host-agent
        BASE_CMD="${BASE_CMD} --network host -v /var/run/docker.sock:/var/run/docker.sock -v /var/run/cosmicac:/var/run/cosmicac -e HOST_AGENT_DIR=/var/run/cosmicac/host-agent"
      fi

      if [ "${JOB_TYPE:-}" = "GPU_CONTAINER" ]; then
        BASE_CMD="${BASE_CMD} --privileged --cap-add=NET_ADMIN --cap-add=SYS_ADMIN --device /dev/net/tun"
        # Host-agent FIFOs only; GPU_CONTAINER keeps bridge networking and no docker socket.
        BASE_CMD="${BASE_CMD} -v /var/run/cosmicac:/var/run/cosmicac -e HOST_AGENT_DIR=/var/run/cosmicac/host-agent"
      fi

      [ -f "${ENV_FILE}" ] && BASE_CMD="${BASE_CMD} --env-file ${ENV_FILE}"

      eval "${BASE_CMD} ${IMG}"
    fi
    # Still Created/Exited → fail unit so Restart=on-failure re-enters the docker start branch.
    if [ -n "${IMG:-}" ] && ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
      echo "ERROR: ${CONTAINER_NAME} not running"; docker ps -a --filter "name=^${CONTAINER_NAME}$" || true; exit 1
    fi

    echo "Bootstrap completed successfully."
  cosmicac-host-net-agent.sh: |
    #!/bin/bash
    # CosmicAC host agent — runs on the VMI host (NOT in the agent container).
    # Container talks over FIFO: <op>|<reply_fifo>[|<arg>]
    # ops: ping | ip | ib | gpu | cpu
    set -eu
    D="${HOST_AGENT_DIR:-/var/run/cosmicac/host-agent}"
    C="${D}/cmd_in.pipe"
    IB_SYS=/sys/class/infiniband
    GPU_QUERY=index,uuid,utilization.gpu,utilization.memory,memory.used,memory.total,temperature.gpu,power.draw
    PROC_STAT="${HOST_AGENT_PROC_STAT:-/proc/stat}"
    # Gap between the two /proc/stat reads op_cpu diffs.
    CPU_SAMPLE_SEC="${HOST_AGENT_CPU_SAMPLE_SEC:-0.25}"
    T="${HOST_AGENT_TIMEOUT_SEC:-8}"
    # How long op_ip waits for DHCP on the overlay NIC (agent boots right after netplan).
    IP_WAIT_SEC="${HOST_AGENT_IP_WAIT_SEC:-30}"
    # Cap concurrent handle() subshells so a flood of FIFO lines cannot leak jobs.
    MAX_JOBS="${HOST_AGENT_MAX_JOBS:-8}"

    # OVERLAY_IFACE env → /etc/default/cosmicac-overlay → non-default-route NIC (prefer one with IPv4).
    resolve_overlay_iface () {
      [ -n "${OVERLAY_IFACE:-}" ] && [ "$OVERLAY_IFACE" != auto ] && { echo "$OVERLAY_IFACE"; return; }
      if [ -f /etc/default/cosmicac-overlay ]; then
        # shellcheck disable=SC1091
        . /etc/default/cosmicac-overlay
      fi
      [ -n "${OVERLAY_IFACE:-}" ] && [ "$OVERLAY_IFACE" != auto ] && { echo "$OVERLAY_IFACE"; return; }

      local p i cands=()
      p=$(ip -4 route show default 2>/dev/null | awk '{print $5; exit}' || true)
      shopt -s nullglob
      for i in /sys/class/net/*/device; do
        i=$(basename "$(dirname "$i")")
        case $i in lo|wg*|docker*|br-*|veth*|virbr*|tun*|tap*) continue;; esac
        [ "$i" = "${p:-}" ] && continue
        cands+=("$i")
      done
      for i in "${cands[@]+"${cands[@]}"}"; do
        ip -4 -o addr show dev "$i" 2>/dev/null | grep -q . && { echo "$i"; return; }
      done
      [ "${#cands[@]}" -gt 0 ] && { echo "${cands[0]}"; return; }
      return 1
    }

    mkdir -p "$D" && chmod 0777 "$D"
    rm -f "$C" && mkfifo "$C" && chmod 666 "$C"

    log () { printf '%s %s\n' "$(date -Is)" "$*" >&2; }
    # JSON-encode a string (includes quotes; jq escapes controls like \r/\n/\t).
    jesc () { printf '%s' "${1:-}" | jq -Rs .; }
    ok () { printf '{"ok":true,"op":"%s","data":%s}\n' "$1" "$2"; }
    fail () { printf '{"ok":false,"op":"%s","error":%s}\n' "$1" "$(jesc "$2")"; }

    op_ping () { ok ping '{}'; }

    op_ip () {
      local iface="${1:-}" ip n=0
      [ -z "$iface" ] && iface="$(resolve_overlay_iface || true)"
      [ -n "${iface:-}" ] || { fail ip 'no overlay iface'; return; }
      while [ "$n" -lt "$IP_WAIT_SEC" ]; do
        ip="$(timeout "$T" ip -4 -o addr show dev "$iface" 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -n1 || true)"
        [ -n "${ip:-}" ] && break
        sleep 1
        n=$((n + 1))
      done
      [ -n "${ip:-}" ] && ok ip "{\"ip\":\"$ip\",\"iface\":$(jesc "$iface")}" || fail ip "no ipv4 on ${iface}"
    }

    op_ib () {
      [ -d "$IB_SYS" ] || { ok ib '{"present":false,"hcaPrefix":null,"hcas":[]}'; return; }
      local names=() n p=null
      for n in "$IB_SYS"/*; do [ -e "$n" ] && names+=("$(basename "$n")"); done
      for n in "${names[@]+"${names[@]}"}"; do case "$n" in ibp*) p='"ibp"'; break;; esac; done
      [ "$p" = null ] && for n in "${names[@]+"${names[@]}"}"; do case "$n" in mlx5_*) p='"mlx5"'; break;; esac; done
      local arr='[' f=1
      for n in "${names[@]+"${names[@]}"}"; do [ $f -eq 1 ] || arr+=','; f=0; arr+="\"$n\""; done
      arr+=']'
      ok ib "{\"present\":$([ ${#names[@]} -gt 0 ] && echo true || echo false),\"hcaPrefix\":$p,\"hcas\":$arr}"
    }

    op_cpu () {
      local a b pct
      [ -r "$PROC_STAT" ] || { fail cpu "cannot read ${PROC_STAT}"; return; }
      a="$(awk '/^cpu /{print; exit}' "$PROC_STAT")"
      sleep "$CPU_SAMPLE_SEC"
      b="$(awk '/^cpu /{print; exit}' "$PROC_STAT")"
      pct="$(printf '%s\n%s\n' "$a" "$b" | awk '
        NR==1 { for (i = 2; i <= 9 && i <= NF; i++) t1 += $i; i1 = $5 + $6 }
        NR==2 { for (i = 2; i <= 9 && i <= NF; i++) t2 += $i; i2 = $5 + $6 }
        END {
          dt = t2 - t1; di = i2 - i1
          if (dt <= 0) exit 1
          p = (dt - di) / dt * 100
          if (p < 0) p = 0
          if (p > 100) p = 100
          printf "%.2f", p
        }')" || { fail cpu 'no cpu time elapsed between samples'; return; }
      [ -n "${pct:-}" ] || { fail cpu 'cpu sample unavailable'; return; }
      ok cpu "{\"percent\":${pct},\"cores\":$(nproc 2>/dev/null || echo null)}"
    }

    op_gpu () {
      local csv
      command -v nvidia-smi >/dev/null 2>&1 || { fail gpu 'nvidia-smi not found on host'; return; }
      csv="$(timeout "$T" nvidia-smi --query-gpu="$GPU_QUERY" --format=csv,noheader,nounits 2>&1 || true)"
      [ -n "${csv:-}" ] || { fail gpu 'no nvidia-smi output'; return; }
      ok gpu "{\"csv\":$(jesc "$csv")}"
    }

    handle () {
      local line="$1" op reply arg i=0 base
      IFS='|' read -r op reply arg <<<"$line" || true
      [ -n "${op:-}" ] && [ -n "${reply:-}" ] || return 0
      # Reply FIFO must be a direct child of $D named resp_*.pipe (no ../ or nested paths).
      base="$(basename -- "$reply")"
      case "$base" in resp_*.pipe) ;; *) log "reject $reply"; return 0 ;; esac
      [ "$reply" = "${D}/${base}" ] || { log "reject $reply"; return 0; }
      while [ ! -p "$reply" ] && [ "$i" -lt 50 ]; do sleep 0.05; i=$((i + 1)); done
      [ -p "$reply" ] || { log "missing $reply"; return 0; }
      {
        case "$op" in
          ping) op_ping ;;
          ip) op_ip "${arg:-}" ;;
          ib) op_ib ;;
          gpu) op_gpu ;;
          cpu) op_cpu ;;
          *) fail "$op" 'unknown op' ;;
        esac
      } >"$reply" 2>/dev/null || true
      rm -f "$reply"
    }

    running_jobs () { jobs -pr 2>/dev/null | grep -c . || true; }

    # Reap exited children so the jobs table does not grow unbounded.
    reap_finished () {
      local pid running
      running=" $(jobs -pr 2>/dev/null | tr '\n' ' ') "
      for pid in $(jobs -p 2>/dev/null); do
        case "$running" in
          *" $pid "*) ;;
          *) wait "$pid" 2>/dev/null || true ;;
        esac
      done
    }

    wait_for_slot () {
      reap_finished
      while [ "$(running_jobs)" -ge "$MAX_JOBS" ]; do
        wait -n 2>/dev/null || { sleep 0.05; wait 2>/dev/null || true; }
        reap_finished
      done
    }

    log "listening on $C (max ${MAX_JOBS} concurrent handlers)"
    while IFS= read -r line || [ -n "${line:-}" ]; do
      [ -z "${line:-}" ] && continue
      wait_for_slot
      ( handle "$line" ) &
    done <>"$C"
  cosmicac-host-net-agent.service: |
    [Unit]
    Description=CosmicAC host agent (FIFO IPC for inference container)
    After=network-online.target
    Wants=network-online.target
    Before=docker-bootstrap.service

    [Service]
    Type=simple
    Environment=HOST_AGENT_DIR=/var/run/cosmicac/host-agent
    EnvironmentFile=-/etc/default/cosmicac-overlay
    ExecStart=/usr/local/bin/cosmicac-host-net-agent.sh
    Restart=always
    RestartSec=2

    [Install]
    WantedBy=multi-user.target
  docker-bootstrap.service: |
    [Unit]
    Description=Deferred Docker Bootstrap (login + pull + run)
    Wants=network-online.target cosmicac-host-net-agent.service
    After=network-online.target multi-user.target cosmicac-host-net-agent.service apt-daily.service apt-daily-upgrade.service
    StartLimitIntervalSec=600
    StartLimitBurst=3

    [Service]
    Type=oneshot
    ExecStart=/usr/local/bin/docker-bootstrap.sh
    RemainAfterExit=yes
    Restart=on-failure
    RestartSec=20

    [Install]
    WantedBy=multi-user.target

Verify it exists:

kubectl get configmap bootstrap-cm -n <workload-namespace>   # must exist

6. Required container images

Your cluster and the VM guests must be able to pull the following:

ImageUsed by
ghcr.io/tetherto/kubevirt-image:latestDefault vmImage for the VMI root disk, pulled by CDI
ghcr.io/tetherto/cosmicac-wrk-agent-inference:latestIn-guest inference container
docker.io/anurlan/kubevirt-gpu-device-plugin:v1.5.0-fmKubeVirt GPU device plugin daemonset
bitnami/kubectl:latestSubcluster manager pod, overlay networking only
debian:bookworm-slim, alpine:latestOVS/VXLAN and WireGuard containers, overlay networking only
ghcr.io/k8snetworkplumbingwg/multus-cni:v4.2.4-thickMultus daemonset, overlay networking only
ghcr.io/k8snetworkplumbingwg/whereabouts:latestWhereabouts IPAM, overlay networking only

The ghcr.io/tetherto/* images are private. Access them with the ghcr-pull-cred PAT from Section 5.

7. Worker access and RBAC

The worker authenticates to your API server with a kubeconfig. The identity behind that kubeconfig needs two sets of permissions.

Namespace-scoped permissions cover the resources the worker creates and manages in the workload namespace, and, for overlay networking, in kube-system:

API groupResourcesVerbs
"", the core groupconfigmaps, secrets, persistentvolumeclaims, pods, servicescreate, get, list, delete
appsdeployments, daemonsetscreate, get, delete
cdi.kubevirt.iodatavolumescreate, get, delete
kubevirt.iovirtualmachineinstancescreate, get, delete
k8s.cni.cncf.ionetwork-attachment-definitionscreate, get, delete. Overlay networking only

Cluster-scoped permissions let the worker report cluster capacity through getClusterResources, which getInfo and the getClusterResources RPC both use. That call lists all nodes and all pods cluster-wide to compute GPU, CPU, and memory totals and usage:

API groupResourcesVerbsScope
"", the core groupnodesget, listcluster-wide
"", the core grouppodslistall namespaces

The following notes apply:

  • pods needs namespaced list and get so the worker can poll VMI launcher pods for status, and cluster-wide list for getClusterResources.
  • nodes is cluster-scoped, so it requires a ClusterRole.
  • For overlay networking, configmaps also needs update and patch. The worker rewrites the WireGuard peers ConfigMap as clients join or leave.

Team storage volumes need extra permissions. When a user creates a volume, the worker waits for the StorageClass that Gcore creates. When a user deletes a volume, the worker checks that no VM still mounts the volume, and then cleans up the objects that Gcore created for the file share.

If you turn on team storage volumes, grant the worker the following permissions:

API groupResourcesVerbsScope
kubevirt.iovirtualmachineinstanceslistworkload namespace
storage.k8s.iostorageclassesget, deletecluster-wide
"", the core grouppersistentvolumesdeletecluster-wide
"", the core grouppersistentvolumeclaimslistall namespaces
"", the core grouppersistentvolumeclaimsdeletekube-system
appsdeploymentsdeletekube-system

The namespaced set binds through a Role and RoleBinding in the workload namespace. A ClusterRole and ClusterRoleBinding cover the cluster-scoped node and pod reads along with the kube-system objects that overlay networking uses. The grant follows least privilege, and the worker never needs cluster-admin.

8. Overlay networking add-ons

CosmicAC builds an OVS/VXLAN subcluster for every multi-node Managed Inference replica, and joins that replica's nodes over the subcluster. Without these add-ons, a job that needs more GPUs than one node holds can't run. A workload that requires a per-instance isolated network uses the same add-ons, plus a WireGuard gateway. See Multi-node replicas.

Your cluster needs the following outcomes:

OutcomeNeeded forDetail
Multus CNI installedMulti-nodeAttaches the secondary overlay interface to the VMI through NetworkAttachmentDefinition. Reference image ghcr.io/k8snetworkplumbingwg/multus-cni:v4.2.4-thick.
Whereabouts IPAM installedMulti-nodeCluster-wide IPAM for the overlay range. Reference ghcr.io/k8snetworkplumbingwg/whereabouts:latest.
OVS CNI available on nodesMulti-nodeProvides the ovs CNI type used by the NAD bridge. Reference daemonsets ovs-cni-amd64 and ovs-node.
NAD CRD presentMulti-nodenetwork-attachment-definitions.k8s.cni.cncf.io, provided by Multus. The worker creates NADs of CNI type ovs with whereabouts IPAM.
subcluster-manager-sa + RBACMulti-nodeServiceAccount in kube-system that can label nodes and watch pods. ClusterRole rules grant nodes get,list,watch,patch,update and pods get,list,watch, bound through a ClusterRoleBinding.
UDP LoadBalancerIsolated networksThe WireGuard gateway is a Service of type: LoadBalancer on UDP 51820 with externalTrafficPolicy: Local. Your LB must support UDP. The reference manifests carry a Gcore-specific annotation loadbalancer.gcore.com/floating-ip-cleanup. On another provider, use the matching annotation or drop it.

These add-ons layer on top of your primary CNI. They don't replace it.

9. Observability and logs

CosmicAC collects logs and metrics from its services. You store them:

  • CosmicAC provides: cosmicac-wrk-monitor and the producers inside every service.
  • You provide: a Prometheus and a Loki.

CosmicAC runs jobs without Prometheus or Loki. Job metric charts need your Prometheus, and log history needs your Loki. Both also cover monitoring beyond CosmicAC, such as cluster, node, and host metrics.

Model health works without either one, from a separate system. See Model health.

For more information, see:

10. Verification checklist

Run these checks from a workstation with kubectl and virtctl pointed at the cluster.

10.1 Base platform

# Nodes Ready + control plane healthy; k8s version supported by KubeVirt/CDI
kubectl get nodes -o wide
kubectl version -o json | jq -r '.serverVersion.gitVersion'

# Per GPU node: GPU (and IB) advertised under the name the worker requests
kubectl get nodes -o json | jq -r '
  .items[] | .metadata.name + "  gpu=" +
  (.status.allocatable["nvidia.com/GH100_H100_SXM5_80GB"] // "0") + "  ib=" +
  (.status.allocatable["mellanox.com/cx7_ib_pf"] // "0")'

# KubeVirt + CDI healthy; HostDevices feature gate on
kubectl -n kubevirt get kubevirt kubevirt -o jsonpath='{.status.observedKubeVirtVersion}{"\n"}'
kubectl -n kubevirt get kubevirt kubevirt -o jsonpath='{.spec.configuration.developerConfiguration.featureGates}{"\n"}'  # includes HostDevices
kubectl -n cdi get cdi cdi -o jsonpath='{.status.observedVersion}{"\n"}'

# KubeVirt GPU device plugin advertising the passthrough resource
kubectl -n kube-system get ds nvidia-kubevirt-gpu-dp-daemonset

# permittedHostDevices includes your GPU/IB
kubectl -n kubevirt get kubevirt kubevirt -o jsonpath='{.spec.configuration.permittedHostDevices}{"\n"}' | jq .

# A working local-path StorageClass exists (need not be the default)
kubectl get sc

# Workload-namespace prerequisites
kubectl -n <workload-namespace> get secret ghcr-pull-cred
kubectl -n <workload-namespace> get configmap bootstrap-cm

# Device-admission smoke test: a VMI that CLAIMS a full GPU is admitted + scheduled.
# NOTE: with no boot disk this only proves device admission + scheduling, NOT a
# working guest. For a real boot test, add a DataVolume root disk.
cat <<'EOF' | kubectl apply -f -
apiVersion: kubevirt.io/v1
kind: VirtualMachineInstance
metadata:
  name: gpu-smoke
  namespace: default
spec:
  domain:
    devices:
      gpus:
        - deviceName: nvidia.com/GH100_H100_SXM5_80GB
          name: gpu0
    resources:
      requests: { memory: 8Gi }
  volumes: []
EOF
kubectl get vmi gpu-smoke -w     # reaches Scheduled/Running (device admitted), then:
kubectl delete vmi gpu-smoke

The checks pass when nodes are Ready, each GPU node reports the expected gpu= and ib= counts, KubeVirt is healthy with HostDevices on, CDI is healthy, a working local-path StorageClass exists, ghcr-pull-cred and bootstrap-cm are present, and the cluster admits and schedules the smoke VMI.

10.2 Overlay networking

Run these checks if your cluster serves multi-node inference.

# Multus / Whereabouts / OVS daemonsets Ready in kube-system
kubectl -n kube-system get ds | grep -iE 'multus|whereabouts|ovs'

# NAD CRD present
kubectl get crd network-attachment-definitions.k8s.cni.cncf.io

# Subcluster manager SA + RBAC present
kubectl -n kube-system get sa subcluster-manager-sa
kubectl get clusterrole subcluster-manager-role
kubectl get clusterrolebinding subcluster-manager-binding

# LoadBalancer can allocate an external IP for UDP services (provider-specific)

On this page