Skip to content

Publish Nightly

Publish Nightly #69

Workflow file for this run

name: Publish Nightly
# Manual builds of `pie` for distribution. Each run rewrites the
# `nightly` rolling pre-release on this repo's GitHub Releases —
# every artifact is uploaded with `gh release upload --clobber`, so
# download URLs stay stable across runs.
#
# The prepare-release job creates the release if needed before any
# matrix jobs start uploading assets.
on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to update (assets uploaded with --clobber)"
type: string
default: "nightly"
portable:
description: "Build portable matrix (Linux x86_64/aarch64 Vulkan + macOS arm64 Metal + Windows x86_64 Vulkan/CUDA)"
type: boolean
default: true
cuda_manylinux:
description: "Build the per-compute-capability CUDA matrix (manylinux_2_28, glibc 2.28; the CUDA release path)"
type: boolean
default: true
permissions:
contents: write # needed for `gh release upload`
env:
# Release notes body — keeps the release page in sync with the
# latest workflow run.
RELEASE_NOTES: |
Rolling build from commit ${{ github.sha }} on ${{ github.ref_name }}.
Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
jobs:
# =========================================================================
# Create/update the rolling release before any matrix job tries to upload.
# This keeps `gh release upload` from racing a missing release, and keeps
# the rolling build out of GitHub's "Latest" release slot.
# =========================================================================
prepare-release:
if: inputs.portable || inputs.cuda_manylinux
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Ensure rolling release exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ inputs.tag }}
run: |
set -euxo pipefail
if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
release_id="$(gh release view "${TAG}" --json databaseId --jq .databaseId --repo "${GITHUB_REPOSITORY}")"
gh api \
-X PATCH \
"repos/${GITHUB_REPOSITORY}/releases/${release_id}" \
-f tag_name="${TAG}" \
-f name="${TAG}" \
-f body="${RELEASE_NOTES}" \
-F draft=false \
-F prerelease=true \
-f make_latest=false
else
gh release create "${TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--title "${TAG}" \
--notes "${RELEASE_NOTES}" \
--prerelease \
--latest=false
fi
# =========================================================================
# Portable matrix — Linux/Windows Vulkan, macOS Metal, plus Windows
# portable CUDA. Runs on the larger org runners wherever they exist.
# =========================================================================
portable:
if: inputs.portable
needs: prepare-release
name: portable / ${{ matrix.target.label }}
strategy:
fail-fast: false
matrix:
target:
# Linux portable artifacts ship the Vulkan ggml backend (works on
# NVIDIA/AMD/Intel GPUs). Built directly on the larger Ubuntu
# runners — NOT in a manylinux container — because ggml-vulkan
# hard-requires `glslc` (find_package(Vulkan COMPONENTS glslc)),
# which Rocky 8 / manylinux_2_28 doesn't package but Ubuntu 24.04+
# does. glibc floor is therefore the runner's Ubuntu (>= 2.39),
# acceptable for a GPU artifact.
- label: x86_64-linux-vulkan
runner: ubuntu-large-x64
container: ""
platform: linux
triple: x86_64-unknown-linux-gnu
archive: tar.gz
backend: vulkan
backend_env: "PIE_PORTABLE_VULKAN=1"
- label: aarch64-linux-vulkan
runner: ubuntu-large-arm64
container: ""
platform: linux
triple: aarch64-unknown-linux-gnu
archive: tar.gz
backend: vulkan
backend_env: "PIE_PORTABLE_VULKAN=1"
- label: aarch64-macos-metal
# macos-15 gets Xcode 16 with libc++ 17, which has
# `std::atomic_ref` (C++20). macos-14's Xcode 15.4 ships
# libc++ 15, which doesn't.
runner: macos-15
container: ""
platform: macos
triple: aarch64-apple-darwin
archive: tar.gz
backend: metal
backend_env: "PIE_PORTABLE_METAL=1"
- label: x86_64-windows-vulkan
runner: windows-large-x64
container: ""
platform: windows
triple: x86_64-pc-windows-msvc
archive: zip
backend: vulkan
backend_env: "PIE_PORTABLE_VULKAN=1"
- label: x86_64-windows-cuda
runner: windows-large-x64
container: ""
platform: windows
triple: x86_64-pc-windows-msvc
archive: zip
backend: cuda
backend_env: "PIE_PORTABLE_CUDA=1"
# ggml-cuda compiles many template instances slowly with nvcc.
# Build one real+PTX arch; the PTX keeps the artifact
# forward-JIT-able on newer NVIDIA GPUs.
cuda_arch: "75"
runs-on: ${{ matrix.target.runner }}
container: ${{ matrix.target.container || '' }}
env:
ARTIFACT_NAME: pie-${{ matrix.target.label }}.${{ matrix.target.archive }}
steps:
- uses: actions/checkout@v6
# manylinux containers ship gcc + cmake + ninja but no rust.
# We dnf-install:
# - `openssl-devel` + `pkg-config`: for `openssl-sys` (reqwest's
# default features).
# - `perl`: openssl-sys build script's vendored fallback.
# - `gcc-toolset-14-gcc(-c++)`: needed for libstdc++ 14, which
# ships <span> and other C++20 headers driver/portable's
# source uses. The container's stock gcc-8 / libstdc++-8
# is missing them.
#
# Then we pin CC/CXX explicitly to the toolset's binaries —
# without that, cmake-rs picks `/opt/clang/bin/cc` (also on
# PATH inside the manylinux image) and we lose the libstdc++ 14
# come link time.
- name: Install build deps + rustup (manylinux only)
if: startsWith(matrix.target.container, 'quay.io/pypa/manylinux')
shell: bash
run: |
set -euxo pipefail
dnf install -y --setopt=install_weak_deps=False \
openssl-devel pkg-config perl git \
gcc-toolset-14-gcc gcc-toolset-14-gcc-c++
# GitHub CLI — used by the upload step. The manylinux image
# doesn't ship `gh` and the upload step's --clobber flow has
# no clean curl equivalent. `gh` rpm depends on `git`,
# installed above.
dnf install -y 'dnf-command(config-manager)'
dnf config-manager --add-repo \
https://cli.github.com/packages/rpm/gh-cli.repo
dnf install -y gh --repo gh-cli
# Pin compiler explicitly. cmake-rs (via cc-rs) reads
# CC/CXX, so this forces CMake to use gcc-toolset-14 even
# though manylinux's `/opt/clang/bin` is also on PATH and
# would otherwise be picked. We need libstdc++ 14 for the
# C++20 headers (`<span>`) the portable driver source uses.
{
echo "CC=/opt/rh/gcc-toolset-14/root/usr/bin/gcc"
echo "CXX=/opt/rh/gcc-toolset-14/root/usr/bin/g++"
echo "AR=/opt/rh/gcc-toolset-14/root/usr/bin/ar"
echo "PATH=/opt/rh/gcc-toolset-14/root/usr/bin:${PATH}"
echo "MANPATH=/opt/rh/gcc-toolset-14/root/usr/share/man:"
echo "LD_LIBRARY_PATH=/opt/rh/gcc-toolset-14/root/usr/lib64:/opt/rh/gcc-toolset-14/root/usr/lib"
echo "PKG_CONFIG_PATH=/opt/rh/gcc-toolset-14/root/usr/lib64/pkgconfig"
} >> "$GITHUB_ENV"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --profile minimal --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install Rust (non-container runners)
if: matrix.target.container == ''
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target.triple }}
- name: Install Vulkan toolchain + build deps (linux only)
if: matrix.target.platform == 'linux' && matrix.target.backend == 'vulkan'
shell: bash
run: |
set -euxo pipefail
# No manylinux container on this leg: ggml-vulkan hard-requires
# `glslc` (find_package(Vulkan COMPONENTS glslc REQUIRED)), which
# Rocky 8 / manylinux_2_28 doesn't package but Ubuntu 24.04+ does.
# Install the Vulkan headers/loader/compiler plus the C/C++ build
# toolchain that the cmake-rs driver build and openssl-sys need.
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update
# spirv-headers provides <spirv/unified1/spirv.hpp> (namespace spv),
# which ggml-vulkan.cpp includes for SPIR-V reflection — not pulled
# in by libvulkan-dev/glslc alone.
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build pkg-config \
glslc libvulkan-dev spirv-headers libssl-dev
glslc --version
- name: Install Vulkan SDK (windows only)
id: vulkan_sdk
if: matrix.target.platform == 'windows' && matrix.target.backend == 'vulkan'
# node24 action that installs the full SDK (headers + import libs).
# Replaces humbletim/install-vulkan-sdk (node20 + a transitive
# actions/cache@v4) which warns under the Node-20 deprecation.
uses: jakoch/install-vulkan-sdk-action@v1.5.5
with:
cache: true
- name: Export VULKAN_SDK (windows only)
if: matrix.target.platform == 'windows' && matrix.target.backend == 'vulkan'
shell: bash
run: |
set -euxo pipefail
# cmake-rs / build.rs read the VULKAN_SDK env var; surface jakoch's
# output to the environment and sanity-check the files ggml needs.
echo "VULKAN_SDK=${{ steps.vulkan_sdk.outputs.VULKAN_SDK }}" >> "$GITHUB_ENV"
test -f "${{ steps.vulkan_sdk.outputs.VULKAN_SDK }}/Include/vulkan/vulkan.h"
test -f "${{ steps.vulkan_sdk.outputs.VULKAN_SDK }}/Lib/vulkan-1.lib"
- name: Install CUDA toolkit (windows portable cuda only)
if: matrix.target.platform == 'windows' && matrix.target.backend == 'cuda'
uses: Jimver/cuda-toolkit@v0.2.35
with:
cuda: '12.9.1'
- name: Verify CUDA toolkit (windows portable cuda only)
if: matrix.target.platform == 'windows' && matrix.target.backend == 'cuda'
shell: bash
run: |
set -euxo pipefail
nvcc --version
test -n "${CUDA_PATH:-${CUDA_HOME:-}}"
- name: Set up MSVC environment (windows cuda only)
# ggml-cuda's enable_language(CUDA) under CMake's Visual Studio
# generator needs the CUDA MSBuild toolset integration, which the
# toolkit installer doesn't reliably register on the hosted image
# ("No CUDA toolset found"). We build with the Ninja generator
# instead (set in the cargo step); sourcing the MSVC dev environment
# here puts cl.exe on PATH so nvcc/CMake can use it as host compiler.
if: matrix.target.platform == 'windows' && matrix.target.backend == 'cuda'
# node24 equivalent of ilammy/msvc-dev-cmd (which is node20-only and
# warns under the Node-20 deprecation). Sets up vcvars so cl.exe is on
# PATH for the Ninja CUDA build.
uses: TheMrMilchmann/setup-msvc-dev@v4.0.0
with:
arch: x64
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: build-${{ matrix.target.label }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: build-${{ matrix.target.label }}-
- name: cargo build (release-min)
shell: bash
run: |
set -euxo pipefail
if [ -n "${{ matrix.target.backend_env }}" ]; then
export ${{ matrix.target.backend_env }}
fi
if [[ "${{ matrix.target.backend }}" == "cuda" ]]; then
export PIE_PORTABLE_CUDA_ARCH='${{ matrix.target.cuda_arch }}'
fi
if [[ "${{ matrix.target.platform }}" == "windows" ]]; then
# ggml-vulkan nests a shader-generator CMake build under Cargo's
# OUT_DIR, and ggml-cuda produces many long object paths. Keep the
# target root short enough for the legacy 260-character path limit
# on Windows, and isolate backend flavors from each other.
export CARGO_TARGET_DIR="C:/t/pie-${{ matrix.target.backend }}"
# 32-core windows-large runner: parallelize the native ggml build
# fully (was capped at 4 for the old small hosted runners).
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc 2>/dev/null || echo 8)"
if [[ "${{ matrix.target.backend }}" == "cuda" ]]; then
# Drive nvcc via Ninja — the VS generator can't find the CUDA
# toolset on the hosted image. cl.exe is on PATH from the
# preceding msvc-dev-cmd step.
export CMAKE_GENERATOR=Ninja
ninja --version
# Git Bash ships /usr/bin/link.exe (GNU coreutils `link`), which
# shadows MSVC's link.exe for rustc once msvc-dev-cmd activates the
# MSVC env (rustc then resolves the linker via PATH and picks the
# GNU one -> "extra operand" link errors). Drop it so `link`
# resolves to MSVC's linker. (Only needed on the cuda leg, which is
# the only one that runs msvc-dev-cmd.)
rm -f /usr/bin/link /usr/bin/link.exe || true
fi
fi
# `release-min`: thin LTO, panic=abort, strip=symbols.
# Defined in workspace Cargo.toml. Smaller + faster than
# plain `release` (no LTO at all by default), without fat
# LTO's many-hour link cost. Distribution profile.
cargo build --profile release-min \
--target ${{ matrix.target.triple }} \
-p pie-server \
--features driver-portable,driver-dummy
- name: Package
shell: bash
run: |
set -euxo pipefail
mkdir -p dist
TARGET_ROOT="target"
if [[ "${{ matrix.target.platform }}" == "windows" ]]; then
TARGET_ROOT="C:/t/pie-${{ matrix.target.backend }}"
fi
BIN_DIR="${TARGET_ROOT}/${{ matrix.target.triple }}/release-min"
if [[ "${{ matrix.target.archive }}" == "zip" ]]; then
# Windows .exe; 7z preinstalled on windows-latest.
7z a "dist/${ARTIFACT_NAME}" "${BIN_DIR}/pie.exe"
else
tar -C "${BIN_DIR}" -czf "dist/${ARTIFACT_NAME}" pie
fi
ls -la dist/
- name: Upload to release (--clobber)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euxo pipefail
# Many matrix legs --clobber the same rolling release concurrently;
# GitHub's asset API intermittently 5xx/422s under that contention.
# Retry a few times before failing the leg.
for attempt in 1 2 3 4 5; do
if gh release upload "${{ inputs.tag }}" "dist/${ARTIFACT_NAME}" --clobber --repo "${{ github.repository }}"; then
exit 0
fi
echo "upload attempt ${attempt} failed; retrying in $((attempt * 10))s" >&2
sleep $((attempt * 10))
done
echo "upload failed after retries" >&2
exit 1
# =========================================================================
# Per-compute-capability CUDA matrix on GH-hosted larger runners with a
# manylinux_2_28 base — binaries with a glibc 2.28 floor (RHEL 8 / Ubuntu
# 20.04 / Debian 11).
#
# Matrix = compute-capability × CUDA-version × os-arch; each leg builds a
# SINGLE SM, so the build parallelizes (wall-clock ~= one arch, not a
# 6-arch fatbin) and each binary is small. The installer detects the GPU's
# compute capability and picks the matching asset. Built inside
# nvidia/cuda:X.Y-devel-rockylinux8 containers on ubuntu-large-{x64,arm64}.
# Opt-in via the `cuda_manylinux` workflow input.
# =========================================================================
cuda-manylinux:
if: inputs.cuda_manylinux
needs: prepare-release
name: cuda / ${{ matrix.target.os_arch }}-sm${{ matrix.target.cc }} / cuda-${{ matrix.cuda.short }}
runs-on: ${{ matrix.target.runner }}
# One compute-capability per job (not a 6-arch fatbin), so each leg is
# quick and the legs run in parallel across the larger runners. Keep a
# generous net for cold caches.
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
# Per-compute-capability: CC × CUDA-version × os-arch. Each entry
# builds ONE arch. The installer detects the GPU's compute capability
# (nvidia-smi, or the PIE_CC override) and picks the matching asset.
cuda:
- { version: "12.8.1", short: "12.8" }
- { version: "13.0.0", short: "13.0" }
target:
# x86_64: Ampere (80/86), Ada (89), Hopper (90), Blackwell DC (100) + consumer (120).
- { os_arch: x86_64, cc: "80", runner: ubuntu-large-x64, triple: x86_64-unknown-linux-gnu }
- { os_arch: x86_64, cc: "86", runner: ubuntu-large-x64, triple: x86_64-unknown-linux-gnu }
- { os_arch: x86_64, cc: "89", runner: ubuntu-large-x64, triple: x86_64-unknown-linux-gnu }
- { os_arch: x86_64, cc: "90", runner: ubuntu-large-x64, triple: x86_64-unknown-linux-gnu }
- { os_arch: x86_64, cc: "100", runner: ubuntu-large-x64, triple: x86_64-unknown-linux-gnu }
- { os_arch: x86_64, cc: "120", runner: ubuntu-large-x64, triple: x86_64-unknown-linux-gnu }
# aarch64: Grace-Hopper (90), Grace-Blackwell (100).
- { os_arch: aarch64, cc: "90", runner: ubuntu-large-arm64, triple: aarch64-unknown-linux-gnu }
- { os_arch: aarch64, cc: "100", runner: ubuntu-large-arm64, triple: aarch64-unknown-linux-gnu }
exclude:
# Blackwell consumer (RTX 50) shipped in the CUDA-13/driver-580 era —
# no one pairs sm120 with a CUDA 12.8 runtime.
- cuda: { version: "12.8.1", short: "12.8" }
target: { os_arch: x86_64, cc: "120", runner: ubuntu-large-x64, triple: x86_64-unknown-linux-gnu }
# Grace-Blackwell (GB200) is a 2025/CUDA-13 platform — the
# arm64 × Blackwell × CUDA-12.8 intersection is empty.
- cuda: { version: "12.8.1", short: "12.8" }
target: { os_arch: aarch64, cc: "100", runner: ubuntu-large-arm64, triple: aarch64-unknown-linux-gnu }
container: nvidia/cuda:${{ matrix.cuda.version }}-devel-rockylinux8
env:
ARTIFACT_NAME: pie-${{ matrix.target.os_arch }}-linux-cuda${{ matrix.cuda.short }}-sm${{ matrix.target.cc }}.tar.gz
PIE_PORTABLE_CUDA: "1"
PIE_PORTABLE_CUDA_ARCH: ${{ matrix.target.cc }}
CMAKE_CUDA_ARCHITECTURES: ${{ matrix.target.cc }}
CMAKE_CUDA_RUNTIME_LIBRARY: Shared
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v6
- name: Bootstrap container
shell: bash
run: |
set -euxo pipefail
# `ninja-build` lives in the PowerTools repo on Rocky 8;
# EPEL covers extras. `patch` for the vendored llama.cpp
# patches; `git` for CPM's git fetches. NCCL ships in
# NVIDIA's cuda repo (already configured by the base image)
# as `libnccl-devel`; pulls `libnccl` runtime in via deps.
dnf install -y epel-release dnf-plugins-core
dnf config-manager --set-enabled powertools
dnf install -y --setopt=install_weak_deps=False \
openssl-devel pkg-config perl git patch \
cmake ninja-build \
gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ \
libnccl libnccl-devel
# Pin compiler explicitly: `source enable` only adjusts PATH —
# cmake-rs probes CC/CXX env vars and otherwise picks
# `/usr/bin/cc` (gcc-8.5, no <span> / C++20). Same nvcc
# host-compiler issue applies via CUDAHOSTCXX (nvcc
# auto-picks gcc-8.5 and silently drops `-std=c++20`).
{
echo "CC=/opt/rh/gcc-toolset-12/root/usr/bin/gcc"
echo "CXX=/opt/rh/gcc-toolset-12/root/usr/bin/g++"
echo "AR=/opt/rh/gcc-toolset-12/root/usr/bin/ar"
echo "CUDAHOSTCXX=/opt/rh/gcc-toolset-12/root/usr/bin/g++"
echo "PATH=/opt/rh/gcc-toolset-12/root/usr/bin:${PATH}"
echo "LD_LIBRARY_PATH=/opt/rh/gcc-toolset-12/root/usr/lib64:/opt/rh/gcc-toolset-12/root/usr/lib"
echo "PKG_CONFIG_PATH=/opt/rh/gcc-toolset-12/root/usr/lib64/pkgconfig"
# rustc's final link must go through the gcc-toolset-12 gcc driver
# so libstdc++_nonshared.a is pulled in for the C++17/20 libstdc++
# symbols (std::filesystem, std::__throw_bad_array_new_length) the
# C++ drivers use. The stock /usr/bin/cc (gcc-8.5) links the old
# system libstdc++ and fails the link with undefined symbols.
# The linker env var is per-target-triple; derive its name from the
# triple so this one bootstrap works for both x86_64 and aarch64.
echo "CARGO_TARGET_$(echo '${{ matrix.target.triple }}' | tr 'a-z-' 'A-Z_')_LINKER=/opt/rh/gcc-toolset-12/root/usr/bin/gcc"
} >> "$GITHUB_ENV"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --profile minimal --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: build-cuda-${{ matrix.target.os_arch }}-sm${{ matrix.target.cc }}-${{ matrix.cuda.short }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: build-cuda-${{ matrix.target.os_arch }}-sm${{ matrix.target.cc }}-${{ matrix.cuda.short }}-
- name: cargo build (release-min)
shell: bash
run: |
set -euxo pipefail
# `release-min`: thin LTO, panic=abort, strip=symbols.
# Defined in workspace Cargo.toml.
cargo build --profile release-min \
--target ${{ matrix.target.triple }} \
-p pie-server \
--features driver-portable,driver-cuda,driver-dummy
- name: Package
shell: bash
run: |
set -euxo pipefail
mkdir -p dist
BIN_DIR="target/${{ matrix.target.triple }}/release-min"
tar -C "${BIN_DIR}" -czf "dist/${ARTIFACT_NAME}" pie
ls -la dist/
- name: Upload to release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ inputs.tag }}
files: dist/${{ env.ARTIFACT_NAME }}
fail_on_unmatched_files: true
make_latest: false
# =========================================================================
# Refresh the release notes once after all builds finish.
# =========================================================================
refresh-notes:
if: always() && (inputs.portable || inputs.cuda_manylinux)
needs: [portable, cuda-manylinux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Edit release notes + body
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ inputs.tag }}
run: |
set -euxo pipefail
release_id="$(gh release view "${TAG}" --json databaseId --jq .databaseId --repo "${GITHUB_REPOSITORY}")"
gh api \
-X PATCH \
"repos/${GITHUB_REPOSITORY}/releases/${release_id}" \
-f tag_name="${TAG}" \
-f name="${TAG}" \
-f body="${RELEASE_NOTES}" \
-F draft=false \
-F prerelease=true \
-f make_latest=false