Files
llama-swap/.github/workflows/unified-docker.yml
T
Benson WongandGitHub 6c7f9587dd docker/unified: add cuda13 with multi-platform support for arm64 (#1093)
Key changes: 

- add cuda13 container for Ampere to Blackwell GPUs
- support DGX Spark (GB10) with arm64 cuda 13 manifests
- enable ik_llama on vulkan container 
- map `LLAMA_SWAP_*` env vars to llama-server CLI flags with backwards support
- refactor GHA/docker build to accept variants

closes: #905, #1066
supercedes: PR #841, PR #1084
2026-09-05 09:22:52 -07:00

276 lines
11 KiB
YAML

name: Build Unified Docker Image
on:
schedule:
- cron: "37 5 * * *"
workflow_dispatch:
inputs:
llama_cpp_ref:
description: "llama.cpp commit hash, tag, or branch"
required: false
default: "master"
whisper_ref:
description: "whisper.cpp commit hash, tag, or branch"
required: false
default: "master"
sd_ref:
description: "stable-diffusion.cpp commit hash, tag, or branch"
required: false
default: "master"
audio_ref:
description: "audio.cpp commit hash, tag, or branch"
required: false
default: "main"
ik_llama_ref:
description: "ik_llama.cpp commit hash, tag, or branch"
required: false
default: "main"
llama_swap_version:
description: "llama-swap version (e.g. v198, latest, main)"
required: false
default: "main"
build_cuda:
description: "Build CUDA 12 image (Pascal through Ada)"
type: boolean
required: false
default: true
build_cuda13:
description: "Build CUDA 13 image (Ampere through Blackwell)"
type: boolean
required: false
default: true
build_arm64:
description: "Include linux/arm64 in the CUDA 13 image (NVIDIA GB10 / DGX Spark)"
type: boolean
required: false
default: true
build_vulkan:
description: "Build Vulkan image"
type: boolean
required: false
default: true
push_to_ghcr:
description: "Push the unified images to ghcr.io (per-project artifacts images are always pushed; they are how the build stages reach the assemble job)"
type: boolean
required: false
default: true
permissions:
contents: read
packages: write
# The build is one Dockerfile per piece: a builder base per backend, one per
# upstream project, and the runtime. Each compiles in its own job and publishes
# an image; a final job copies the /install trees into the unified image.
#
# Building everything in one job put five concurrent CUDA compiles on a
# four-core runner, which stopped fitting in the 6h job limit; a cancelled job
# also never reached --cache-to, so nothing was cached and every later run
# rebuilt from scratch. Images are addressed by content -- the base by its
# Dockerfile, a project by its upstream commit plus its own two files and the
# base tag -- so anything unchanged is skipped, and one project overrunning no
# longer discards the others.
#
# Each variant runs as its own call to unified-docker-backend.yml, so Vulkan
# publishes as soon as its own projects finish rather than waiting on the
# multi-hour CUDA compiles.
#
# There are two CUDA variants. cuda is CUDA 12 compiled for Pascal through Ada;
# cuda13 is CUDA 13 compiled for Ampere through Blackwell. CUDA 13 removed
# Maxwell, Pascal and Volta from nvcc, so the older cards need an image of their
# own. They share every Dockerfile and install script -- only CUDA_VERSION and
# CMAKE_CUDA_ARCHITECTURES differ, and the builder base tag is keyed on both, so
# each variant gets its own base and artifacts images and neither invalidates
# the other.
#
# cuda13 is also the only variant built for linux/arm64. NVIDIA's GB10 -- the
# Blackwell GPU in DGX Spark -- sits next to a Grace CPU, so aarch64 is the only
# way to reach it, and its sm_121 needs a CUDA 13 nvcc. Every other NVIDIA arm64
# part is a Grace pairing too, so there is nothing for an arm64 CUDA 12 image to
# run on, and no arm64 GPU that the Vulkan image would serve. Each platform is a
# separate matrix cell over the backend workflow, compiled natively on a runner
# of that architecture; a manifest job then joins them into the tag users pull.
jobs:
setup:
runs-on: ubuntu-latest
outputs:
build_cuda: ${{ steps.plan.outputs.build_cuda }}
build_cuda13: ${{ steps.plan.outputs.build_cuda13 }}
build_vulkan: ${{ steps.plan.outputs.build_vulkan }}
cuda_projects: ${{ steps.plan.outputs.cuda_projects }}
vulkan_projects: ${{ steps.plan.outputs.vulkan_projects }}
amd64_only: ${{ steps.plan.outputs.amd64_only }}
cuda13_platforms: ${{ steps.plan.outputs.cuda13_platforms }}
date_tag: ${{ steps.plan.outputs.date_tag }}
push_to_ghcr: ${{ steps.plan.outputs.push_to_ghcr }}
llama_hash: ${{ steps.refs.outputs.llama_hash }}
whisper_hash: ${{ steps.refs.outputs.whisper_hash }}
sd_hash: ${{ steps.refs.outputs.sd_hash }}
audio_hash: ${{ steps.refs.outputs.audio_hash }}
ik_llama_hash: ${{ steps.refs.outputs.ik_llama_hash }}
ls_hash: ${{ steps.refs.outputs.ls_hash }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
# Resolve every ref once, here. The build and assemble jobs are handed
# full commit hashes, so a branch that moves mid-run cannot leave two
# jobs building different revisions of the same project.
- name: Resolve upstream commits
id: refs
env:
LLAMA_REF: ${{ inputs.llama_cpp_ref || 'master' }}
WHISPER_REF: ${{ inputs.whisper_ref || 'master' }}
SD_REF: ${{ inputs.sd_ref || 'master' }}
AUDIO_REF: ${{ inputs.audio_ref || 'main' }}
IK_LLAMA_REF: ${{ inputs.ik_llama_ref || 'main' }}
LS_VERSION: ${{ inputs.llama_swap_version || 'main' }}
run: |
chmod +x docker/unified/build-image.sh
docker/unified/build-image.sh --cuda --resolve | tee -a "$GITHUB_OUTPUT"
- name: Plan variants
id: plan
run: |
# schedule uses defaults (build all, push); workflow_dispatch respects inputs
if [[ "${{ github.event_name }}" == "schedule" ]]; then
build_cuda=true; build_cuda13=true; build_vulkan=true
build_arm64=true; push=true
else
build_cuda="${{ inputs.build_cuda }}"
build_cuda13="${{ inputs.build_cuda13 }}"
build_vulkan="${{ inputs.build_vulkan }}"
build_arm64="${{ inputs.build_arm64 }}"
push="${{ inputs.push_to_ghcr }}"
fi
# cuda and vulkan are amd64 only; cuda13 adds arm64 for GB10.
if [[ "${build_arm64}" == "true" ]]; then
cuda13_platforms='["linux/amd64","linux/arm64"]'
else
cuda13_platforms='["linux/amd64"]'
fi
{
echo "build_cuda=${build_cuda}"
echo "build_cuda13=${build_cuda13}"
echo "build_vulkan=${build_vulkan}"
echo "push_to_ghcr=${push}"
echo 'cuda_projects=["whisper","sd","audio","llama","ik-llama"]'
echo 'vulkan_projects=["whisper","sd","audio","llama","ik-llama"]'
echo 'amd64_only=["linux/amd64"]'
echo "cuda13_platforms=${cuda13_platforms}"
# Resolved once so every platform's image and the manifest that
# joins them carry the same date, even across UTC midnight.
echo "date_tag=$(date -u +%Y-%m-%d)"
} >> "$GITHUB_OUTPUT"
cuda:
needs: setup
if: ${{ needs.setup.outputs.build_cuda == 'true' }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJSON(needs.setup.outputs.amd64_only) }}
uses: ./.github/workflows/unified-docker-backend.yml
permissions:
contents: read
packages: write
with:
variant: cuda
platform: ${{ matrix.platform }}
projects: ${{ needs.setup.outputs.cuda_projects }}
push_to_ghcr: ${{ needs.setup.outputs.push_to_ghcr == 'true' }}
date_tag: ${{ needs.setup.outputs.date_tag }}
llama_hash: ${{ needs.setup.outputs.llama_hash }}
whisper_hash: ${{ needs.setup.outputs.whisper_hash }}
sd_hash: ${{ needs.setup.outputs.sd_hash }}
audio_hash: ${{ needs.setup.outputs.audio_hash }}
ik_llama_hash: ${{ needs.setup.outputs.ik_llama_hash }}
ls_hash: ${{ needs.setup.outputs.ls_hash }}
cuda-manifest:
needs: [setup, cuda]
if: ${{ needs.setup.outputs.build_cuda == 'true' && needs.setup.outputs.push_to_ghcr == 'true' }}
uses: ./.github/workflows/unified-docker-manifest.yml
permissions:
contents: read
packages: write
with:
variant: cuda
platforms: ${{ needs.setup.outputs.amd64_only }}
date_tag: ${{ needs.setup.outputs.date_tag }}
cuda13:
needs: setup
if: ${{ needs.setup.outputs.build_cuda13 == 'true' }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJSON(needs.setup.outputs.cuda13_platforms) }}
uses: ./.github/workflows/unified-docker-backend.yml
permissions:
contents: read
packages: write
with:
variant: cuda13
platform: ${{ matrix.platform }}
projects: ${{ needs.setup.outputs.cuda_projects }}
push_to_ghcr: ${{ needs.setup.outputs.push_to_ghcr == 'true' }}
date_tag: ${{ needs.setup.outputs.date_tag }}
llama_hash: ${{ needs.setup.outputs.llama_hash }}
whisper_hash: ${{ needs.setup.outputs.whisper_hash }}
sd_hash: ${{ needs.setup.outputs.sd_hash }}
audio_hash: ${{ needs.setup.outputs.audio_hash }}
ik_llama_hash: ${{ needs.setup.outputs.ik_llama_hash }}
ls_hash: ${{ needs.setup.outputs.ls_hash }}
cuda13-manifest:
needs: [setup, cuda13]
if: ${{ needs.setup.outputs.build_cuda13 == 'true' && needs.setup.outputs.push_to_ghcr == 'true' }}
uses: ./.github/workflows/unified-docker-manifest.yml
permissions:
contents: read
packages: write
with:
variant: cuda13
platforms: ${{ needs.setup.outputs.cuda13_platforms }}
date_tag: ${{ needs.setup.outputs.date_tag }}
vulkan:
needs: setup
if: ${{ needs.setup.outputs.build_vulkan == 'true' }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJSON(needs.setup.outputs.amd64_only) }}
uses: ./.github/workflows/unified-docker-backend.yml
permissions:
contents: read
packages: write
with:
variant: vulkan
platform: ${{ matrix.platform }}
projects: ${{ needs.setup.outputs.vulkan_projects }}
push_to_ghcr: ${{ needs.setup.outputs.push_to_ghcr == 'true' }}
date_tag: ${{ needs.setup.outputs.date_tag }}
llama_hash: ${{ needs.setup.outputs.llama_hash }}
whisper_hash: ${{ needs.setup.outputs.whisper_hash }}
sd_hash: ${{ needs.setup.outputs.sd_hash }}
audio_hash: ${{ needs.setup.outputs.audio_hash }}
ik_llama_hash: ${{ needs.setup.outputs.ik_llama_hash }}
ls_hash: ${{ needs.setup.outputs.ls_hash }}
vulkan-manifest:
needs: [setup, vulkan]
if: ${{ needs.setup.outputs.build_vulkan == 'true' && needs.setup.outputs.push_to_ghcr == 'true' }}
uses: ./.github/workflows/unified-docker-manifest.yml
permissions:
contents: read
packages: write
with:
variant: vulkan
platforms: ${{ needs.setup.outputs.amd64_only }}
date_tag: ${{ needs.setup.outputs.date_tag }}