Skip to content

python-wheels

python-wheels #26

Workflow file for this run

name: python-wheels
# Builds, repairs, and proves the distributable Python artifacts (plan M5):
# - wheel-linux: manylinux_2_28 {x86_64, aarch64}, fat CPU + Vulkan
# backend modules (Vulkan toolchain built from pinned
# Khronos tags inside the container; cached across
# runs; native ARM runner for aarch64 — no QEMU)
# - wheel-macos: macosx arm64, Metal with embedded shaders — built
# AND Metal-compute-tested on the bare-metal M4 mini
# (VM runners' paravirtual GPUs can't do Metal compute)
# - wheel-macos-x86: macosx x86_64, CPU only — cross-compiled on the
# mini, tested under Rosetta 2
# - wheel-windows: win_amd64, fat CPU + Vulkan backend modules
# (LunarG SDK; Blacksmith Windows beta)
# - sdist: the from-source fallback, proven by actually
# compiling and transcribing from the tarball
# - api-wheel: the pure-Python transcribe-cpp package
# - cuda-wheel-modal: the cu12 provider built + T4-smoked on Modal
# - clean-install: bare container, wheels only; Vulkan degradation
# tiers incl. a REAL lavapipe transcription (the
# capability-positive gate)
# - bundle-smoke: the extracted native bundle (see below) loaded
# OUTSIDE any wheel via TRANSCRIBE_LIBRARY — the
# posture npm/prebuilt-Rust consumers have
#
# Every native wheel lane also EXTRACTS its repaired wheel's _native/ dir
# (+ contract.json + licenses) into a transcribe-native-<tuple>.tar.gz
# artifact (native-<tuple>): the canonical native bundle non-Python
# ecosystems repackage instead of rebuilding. publish.yml attaches these to
# the GitHub release on tags.
# - vulkan-hw: shipped linux wheel on the T14 (RADV Renoir):
# real-GPU Vulkan + the fat-CPU tier assertion
# - co-import: numpy/torch coexistence with a real transcription,
# both import orders, on all three platforms
#
# CADENCE (user decision 2026-06-12): this matrix does NOT run per PR or per
# push — wheels matter at release time. It runs on workflow_dispatch (manual
# validation after packaging/CMake changes) and via workflow_call from
# publish.yml, so every rehearsal and release builds + validates the FULL
# matrix and publishes those exact artifacts. The every-PR correctness gate
# is python-bindings.yml.
#
# Every native wheel is tested AFTER repair (auditwheel/delocate/delvewheel)
# in a fresh venv — cibuildwheel's test phase guarantees the tested artifact
# is the one that ships. Real-model tests need the private canary GGUFs and
# run when the HF_TOKEN secret exists; forks degrade to no-model tests.
on:
# Called by publish.yml so a release builds + validates the FULL matrix in
# the same run and publishes those exact artifacts.
workflow_call:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CIBW_PIN: "cibuildwheel>=4,<5"
jobs:
wheel-linux:
# 2vcpu everywhere: Blacksmith bills per vcpu-minute and compiles scale
# ~linearly, so bigger runners cost the same and only buy wall-clock.
# One matrix job for both Linux arches — the lane posture is identical
# (cpu-vulkan; ggml has a dedicated Linux-ARM tier list for
# GGML_CPU_ALL_VARIANTS: armv8.0..armv9.2 with runtime feature scoring,
# and the x86 SIMD-off floor flags are inert options there). aarch64
# targets Graviton/Ampere servers, Raspberry Pi 5, SBCs — a real
# on-device-transcription market.
name: wheel-linux (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runner: blacksmith-2vcpu-ubuntu-2404
cibw-archs: x86_64
- arch: aarch64
runner: blacksmith-2vcpu-ubuntu-2404-arm
cibw-archs: aarch64
runs-on: ${{ matrix.runner }}
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
# Container path of the toolchain cache volume (see CIBW_CONTAINER_ENGINE).
VK_TOOLCHAIN_CACHE: /vk-toolchain-cache
# pyproject [tool.cibuildwheel.linux] pins archs=["x86_64"]; override
# per matrix leg (a no-op for x86_64, the aarch64 enabler otherwise).
CIBW_ARCHS_LINUX: ${{ matrix.cibw-archs }}
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- name: Cache the source-built Vulkan toolchain
uses: actions/cache@v5
with:
path: vk-toolchain-cache
key: vk-toolchain-${{ matrix.arch }}-${{ hashFiles('scripts/ci/manylinux-vulkan-toolchain.sh') }}
# cibuildwheel bind-mounts the project at /project inside the manylinux
# container; the canary env paths must resolve there.
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
model-path-prefix: /project
- name: Build, repair, and test the wheel (cibuildwheel)
env:
CIBW_CONTAINER_ENGINE: "docker; create_args: --volume ${{ github.workspace }}/vk-toolchain-cache:/vk-toolchain-cache"
run: |
mkdir -p vk-toolchain-cache
uvx --from "$CIBW_PIN" cibuildwheel --output-dir wheelhouse
- uses: actions/upload-artifact@v7
with:
name: dist-native-linux-${{ matrix.arch }}
path: wheelhouse/*.whl
# The canonical native bundle for non-Python ecosystems (npm platform
# packages, prebuilt-Rust, ...): the REPAIRED wheel's _native/ dir +
# contract.json + licenses, re-containered byte-for-byte. One build
# per tuple, every ecosystem ships the same bytes.
- name: Extract the native bundle
run: |
python3 scripts/ci/extract_native_bundle.py --wheel-dir wheelhouse \
--tuple linux-${{ matrix.arch }}-cpu-vulkan --out bundles
- uses: actions/upload-artifact@v7
with:
name: native-linux-${{ matrix.arch }}-cpu-vulkan
path: bundles/*.tar.gz
wheel-macos:
# Bare-metal M4 Mac mini (self-hosted, runner "m4-mini"): builds the
# SHIPPING macOS wheel and proves Metal COMPUTE on it in one lane — the
# artifact that ships is the artifact tested on a real GPU. VM-based
# macOS CI cannot do this: the probe proved (2026-06-11) that Apple's
# Virtualization.framework paravirtual GPU decodes garbage on both
# GitHub-hosted and Blacksmith M4 VMs (the former Blacksmith lane was
# retired 2026-06-11; flip runs-on back to a hosted label if the mini's
# availability ever becomes a problem). Notes: cibuildwheel needs a
# python.org CPython framework on the runner (installed once, manually —
# the runner service cannot sudo); the Apple-clang CC/CXX pin in
# pyproject [tool.cibuildwheel.macos] is harmless here.
runs-on: [self-hosted, macOS, ARM64]
timeout-minutes: 45 # a sleeping/offline mini must not hang the run
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
# Host-side copy of the lane for wheel_smoke's declared-backends assert.
TRANSCRIBE_WHEEL_LANE: metal
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
- name: Build, repair, and test the wheel on real Metal (cibuildwheel)
run: uvx --from "$CIBW_PIN" cibuildwheel --output-dir wheelhouse
- uses: actions/upload-artifact@v7
with:
name: dist-native-macos-arm64
path: wheelhouse/*.whl
- name: Extract the native bundle
run: |
python3 scripts/ci/extract_native_bundle.py --wheel-dir wheelhouse \
--tuple macos-arm64-metal --out bundles
- uses: actions/upload-artifact@v7
with:
name: native-macos-arm64-metal
path: bundles/*.tar.gz
wheel-macos-x86:
# Intel macOS: the CPU-ONLY x86_64 wheel, CROSS-COMPILED on the M4 mini
# (no Intel hardware in the fleet or on Blacksmith; GitHub's macos-13 was
# the only hosted option and is billing-gated). cibuildwheel handles the
# cross: CIBW_ARCHS_MACOS=x86_64 on an arm64 host builds with
# -arch x86_64 and runs the test phase under Rosetta 2 via the python.org
# universal2 CPython already installed on the mini for wheel-macos.
# No Metal — Intel-Mac GPUs are out of scope (no hardware to validate
# Metal compute); Metal / tuned CPU on Intel is served by the sdist.
# The smoke is CPU-steered (TRANSCRIBE_SMOKE_BACKEND=cpu): wheel_smoke.py
# sees platform.machine()=="x86_64" under Rosetta and asserts the artifact
# exposes only CPU (no metal/vulkan/cuda), then runs the full suite on
# CPU. The arm64/Metal lane is `wheel-macos`. Overrides the pyproject
# macos defaults (arm64/metal) via CIBW_ARCHS_MACOS / CIBW_ENVIRONMENT_MACOS.
runs-on: [self-hosted, macOS, ARM64]
timeout-minutes: 45
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
# Host-side copy of the lane for wheel_smoke's declared-backends assert.
TRANSCRIBE_WHEEL_LANE: cpu
CIBW_ARCHS_MACOS: x86_64
CIBW_ENVIRONMENT_MACOS: >-
TRANSCRIBE_WHEEL_LANE=cpu
TRANSCRIBE_SMOKE_BACKEND=cpu
MACOSX_DEPLOYMENT_TARGET=11.0
CC=/usr/bin/clang
CXX=/usr/bin/clang++
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
- name: Build, repair, and test the CPU-only wheel (cibuildwheel)
run: uvx --from "$CIBW_PIN" cibuildwheel --output-dir wheelhouse
- uses: actions/upload-artifact@v7
with:
name: dist-native-macos-x86_64
path: wheelhouse/*.whl
- name: Extract the native bundle
run: |
python3 scripts/ci/extract_native_bundle.py --wheel-dir wheelhouse \
--tuple macos-x86_64-cpu --out bundles
- uses: actions/upload-artifact@v7
with:
name: native-macos-x86_64-cpu
path: bundles/*.tar.gz
wheel-windows:
# Blacksmith Windows Server 2025 (public beta) — same image family as
# GitHub's windows-2025, so vcvars/vcpkg/the Vulkan SDK installer behave
# identically. This lane is the overall long pole (MSVC over ggml + the
# fat-CPU tier matrix + Vulkan shaders) — expect a long wall-clock on
# 2vcpu; cost is the same as a bigger runner (per-vcpu-minute billing).
# If the beta misbehaves, the fallback is `windows-latest` (needs
# GitHub-hosted billing).
runs-on: blacksmith-2vcpu-windows-2025
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
# Host-side copy of the lane for wheel_smoke's declared-backends assert
# (the test phase runs on the host here, not in a container).
TRANSCRIBE_WHEEL_LANE: cpu-vulkan
# LunarG prunes old SDK downloads — when bumping, verify the URL exists.
VULKAN_VERSION: "1.4.350.0"
# The hf CLI prints ✓ marks; Windows' default cp1252 console codec
# chokes on them (charmap codec error). Force UTF-8 for all Python.
PYTHONUTF8: "1"
steps:
- uses: actions/checkout@v6
# vcvars for the whole job: the Ninja generator (CMAKE_GENERATOR in
# pyproject [tool.cibuildwheel.windows]) needs cl.exe on PATH — without
# it CMake silently picks the runner image's MinGW gcc (observed:
# gcc-flavored M_PI errors). The VS generator didn't need this but is
# ~3x slower on this lane.
# (still node20 upstream — no node24 release yet; runners force node24 from
# 2026-06-16, which is fine for this env-setup action)
- uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- uses: astral-sh/setup-uv@v8.2.0
- name: Install Vulkan SDK ${{ env.VULKAN_VERSION }}
run: |
curl.exe -o "$env:RUNNER_TEMP\vulkan_sdk.exe" -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/vulkan_sdk.exe"
& "$env:RUNNER_TEMP\vulkan_sdk.exe" --accept-licenses --default-answer --confirm-command install
Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}"
Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin"
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
- name: Install zlib (vcpkg, static — libtranscribe requires it)
# Static (-static-md: static lib, dynamic CRT) so no zlib1.dll exists
# at runtime and delvewheel has nothing to vendor for it. The
# toolchain file reaches scikit-build-core via CMAKE_ARGS.
run: |
vcpkg install zlib:x64-windows-static-md
# Forward slashes: CMAKE_ARGS values pass through scikit-build-core's
# CMakeInit.txt cache file, where backslashes are eaten as escapes.
$tc = "$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -replace '\\','/'
Add-Content $env:GITHUB_ENV "CMAKE_ARGS=-DCMAKE_TOOLCHAIN_FILE=$tc -DVCPKG_TARGET_TRIPLET=x64-windows-static-md"
- name: Build, repair, and test the wheel (cibuildwheel)
run: uvx --from "$env:CIBW_PIN" cibuildwheel --output-dir wheelhouse
- uses: actions/upload-artifact@v7
with:
name: dist-native-windows-amd64
path: wheelhouse/*.whl
- name: Extract the native bundle
run: |
python scripts/ci/extract_native_bundle.py --wheel-dir wheelhouse `
--tuple windows-x86_64-cpu-vulkan --out bundles
- uses: actions/upload-artifact@v7
with:
name: native-windows-x86_64-cpu-vulkan
path: bundles/*.tar.gz
sdist:
# The universal fallback must actually work: build the sdist, audit what
# went into it, then compile-and-transcribe from the tarball alone.
runs-on: blacksmith-2vcpu-ubuntu-2404
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
- name: Build sdist
run: uv build --sdist
- name: Audit sdist contents
run: |
python3 - <<'EOF'
import glob, tarfile
[path] = glob.glob("dist/*.tar.gz")
with tarfile.open(path) as tf:
names = tf.getnames()
total = sum(m.size for m in tf.getmembers())
print(f"{path}: {len(names)} files, {total/1e6:.1f} MB uncompressed")
# Weights by extension anywhere; heavy repo dirs at the ROOT only
# (docs/models/*.md is documentation and belongs in the sdist).
ROOT_BANNED = {"models", "dumps", "reports", "canary", "wheelhouse"}
banned = [n for n in names if n.endswith((".gguf", ".safetensors", ".bin"))
or (len(n.split("/")) > 1 and n.split("/")[1] in ROOT_BANNED)]
assert not banned, f"sdist contains artifacts that must never ship: {banned[:10]}"
assert any(n.endswith("src/arch/canary/model.cpp") for n in names), \
"sdist excludes over-matched (root-anchoring regression)"
assert total < 40e6, f"sdist unexpectedly large: {total/1e6:.1f} MB (28 MB expected)"
for required in ("CMakeLists.txt", "include/transcribe.h",
"ggml/CMakeLists.txt", "cmake/python-wheel-install.cmake",
"bindings/python-native/_contract.py.in"):
assert any(n.endswith(required) for n in names), f"missing {required}"
print("sdist audit ok")
EOF
- name: Compile + transcribe from the sdist (no repo sources)
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
uv venv --seed sdist-venv --python 3.12
# The tarball is the only native source here: pip drives the full
# scikit-build-core compile (cmake/ninja arrive as build deps).
./sdist-venv/bin/pip install dist/*.tar.gz
./sdist-venv/bin/pip install pytest numpy huggingface_hub
./sdist-venv/bin/python scripts/ci/wheel_smoke.py "$PWD"
- uses: actions/upload-artifact@v7
with:
name: dist-sdist
path: dist/*.tar.gz
api-wheel:
runs-on: [self-hosted, Linux, X64, hetzner]
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- name: Build transcribe-cpp (pure wheel + sdist)
run: |
cd bindings/python
rm -rf dist && uv build
- uses: actions/upload-artifact@v7
with:
name: dist-api
path: bindings/python/dist/*
# Assets for the checkout-free jobs (clean-install, smoke-testpypi):
# test audio + the shared degradation-check script. Paths inside the
# artifact keep their repo-relative layout (samples/..., scripts/ci/...).
- uses: actions/upload-artifact@v7
with:
name: smoke-assets
path: |
samples/jfk.wav
scripts/ci/vulkan_degradation_check.py
cuda-wheel-modal:
# The cu12 provider is built and smoked on Modal, not on CI runners: the
# manylinux_2_28 + CUDA-toolkit + Vulkan-toolchain environment is a
# cached Modal image layer, the 16-core build bills by the second, and
# the flow ends with a REAL T4 runtime smoke
# (scripts/ci/modal_cuda_build.py). This job is just the driver — almost
# all of its wall-clock is idle waiting on Modal, which is exactly why it
# lives on the (free, mostly idle) Hetzner box instead of a billed
# runner. uv supplies Python (its standalone builds are fully
# relocatable — actions/setup-python's hosted-image tarballs are not
# reliable on self-hosted runners). Gated on the Modal token secrets —
# absent on forks, steps no-op cleanly.
runs-on: [self-hosted, Linux, X64, hetzner]
timeout-minutes: 75
env:
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- name: Build, repair, packaging-smoke, and T4 runtime smoke (Modal)
if: env.MODAL_TOKEN_ID != ''
run: uvx --from modal modal run scripts/ci/modal_cuda_build.py
- name: Fetch the repaired wheel from the Modal volume
if: env.MODAL_TOKEN_ID != ''
run: |
mkdir -p wheelhouse-cu12
# Root-path download is recursive (glob patterns are deprecated in
# the modal CLI); assets/ comes along but the artifact glob below
# only picks the wheels.
uvx --from modal modal volume get transcribe-cu12-wheelhouse / wheelhouse-cu12/ --force
ls -la wheelhouse-cu12/
# Named OUTSIDE the dist-* pattern on purpose: co-import merges dist-*
# and must not pick up the cu12 provider (it would outrank the default
# provider and change what that job tests).
# NOTE: no native-bundle extraction here (or in cuda-windows.yml) —
# the mechanism would work identically (the cu12 wheel has the same
# _native/ + contract.json shape), but no ecosystem consumes a CUDA
# bundle yet. Add the extract step the day one does.
- uses: actions/upload-artifact@v7
if: env.MODAL_TOKEN_ID != ''
with:
name: cuda-dist-linux-x86_64
path: wheelhouse-cu12/*.whl
clean-install:
# Punch-list "clean machine": a bare python:3.12-slim container — no
# compilers, no repo checkout, no env vars. Installs ONLY the built
# wheels (resolver pulls the native provider through the hard pin) and
# transcribes. This is also the shipped-artifact proof of the
# Vulkan-by-default degradation contract, driven by the shared
# scripts/ci/vulkan_degradation_check.py (from the smoke-assets
# artifact — this job has no checkout):
# tier A (bare container, no Vulkan loader): the bundled module stays
# quietly unloaded — CPU transcribes, vulkan answers unavailable,
# backend="vulkan" raises a clean error.
# tier B (apt install loader + lavapipe): the SAME installed wheel
# discovers a Vulkan device and transcribes on it.
needs: [wheel-linux, api-wheel]
runs-on: blacksmith-2vcpu-ubuntu-2404
# (2026-06-12) tier B failures here were NOT a docker/runner problem: the
# wheel itself had been built with the wrong lane posture (unanchored
# scikit-build override regex — "cpu" matched "cpu-vulkan") and simply
# contained no Vulkan module. vulkaninfo inside this container proved the
# loader + lavapipe fine on Blacksmith docker with default options.
container: python:3.12-slim
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- uses: actions/download-artifact@v8
with:
name: dist-native-linux-x86_64
path: wheelhouse
- uses: actions/download-artifact@v8
with:
name: dist-api
path: wheelhouse
- uses: actions/download-artifact@v8
with:
name: smoke-assets
path: assets
- name: Canary model cache (avoid HF rate limits)
if: env.HF_TOKEN != ''
uses: actions/cache@v5
with:
path: canary
key: canary-models-v1
- name: Fetch canary model (cache miss only; pip — no uv in this container)
if: env.HF_TOKEN != ''
run: |
pip install -q huggingface_hub
[ -f canary/whisper-tiny-Q5_K_M.gguf ] || \
hf download handy-computer/whisper-tiny-gguf \
whisper-tiny-Q5_K_M.gguf --local-dir canary
- name: Install from the built wheels only (no index)
run: pip install --no-index --find-links wheelhouse transcribe-cpp
- name: "Tier A: bare machine — CPU transcribes, Vulkan quietly absent"
run: |
if [ -f canary/whisper-tiny-Q5_K_M.gguf ]; then
python assets/scripts/ci/vulkan_degradation_check.py \
--tier no-loader --expect-provider transcribe-cpp-native \
--model canary/whisper-tiny-Q5_K_M.gguf --audio assets/samples/jfk.wav
else
echo "!! no canary (fork without HF_TOKEN) — install-only check"
python assets/scripts/ci/vulkan_degradation_check.py \
--tier no-loader --expect-provider transcribe-cpp-native
fi
- name: "Tier B: install a Vulkan loader + lavapipe — same wheel uses it"
if: env.HF_TOKEN != ''
run: |
apt-get update -q && apt-get install -y -q libvulkan1 mesa-vulkan-drivers vulkan-tools
# Diagnostic first: prove the LOADER sees lavapipe before asking
# ggml to. If this shows no devices, the runner/docker host is the
# problem (seccomp/shm), not the wheel.
vulkaninfo --summary || echo "::warning::vulkaninfo failed — loader-level problem on this runner"
# lavapipe is a CPU-type Vulkan device; ggml's default filter only
# admits real GPUs (CI-only override, real GPUs need none).
export GGML_VK_VISIBLE_DEVICES=0
python assets/scripts/ci/vulkan_degradation_check.py \
--tier loader --expect-provider transcribe-cpp-native \
--model canary/whisper-tiny-Q5_K_M.gguf --audio assets/samples/jfk.wav
bundle-smoke:
# The extracted native BUNDLE works outside any wheel — the exact
# posture npm platform packages / prebuilt-Rust consume. One tuple
# suffices (the other tuples' bytes are already validated inside their
# wheels; the bundle step only re-containers them): bare container, no
# provider package installed, the API package dlopens the bundle's
# libtranscribe via TRANSCRIBE_LIBRARY, backend modules load from the
# bundle dir, CPU transcribes, vulkan quietly unavailable. contract.json
# is asserted against the lane posture — the capability-positive gate
# that catches a wrong-posture bundle (the 2026-06-12 bug class).
needs: [wheel-linux, api-wheel]
runs-on: blacksmith-2vcpu-ubuntu-2404
container: python:3.12-slim
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- uses: actions/download-artifact@v8
with:
name: native-linux-x86_64-cpu-vulkan
path: bundles
- uses: actions/download-artifact@v8
with:
name: dist-api
path: dist-api
- uses: actions/download-artifact@v8
with:
name: smoke-assets
path: assets
- name: Canary model cache (avoid HF rate limits)
if: env.HF_TOKEN != ''
uses: actions/cache@v5
with:
path: canary
key: canary-models-v1
- name: Fetch canary model (cache miss only; pip — no uv in this container)
if: env.HF_TOKEN != ''
run: |
pip install -q huggingface_hub
[ -f canary/whisper-tiny-Q5_K_M.gguf ] || \
hf download handy-computer/whisper-tiny-gguf \
whisper-tiny-Q5_K_M.gguf --local-dir canary
- name: Unpack the bundle + assert the contract posture
run: |
tar xzf bundles/transcribe-native-linux-x86_64-cpu-vulkan.tar.gz
python - <<'EOF'
import json
c = json.load(open(
"transcribe-native-linux-x86_64-cpu-vulkan/contract.json"))
print("contract:", c)
assert c["lane"] == "cpu-vulkan", c
assert set(c["backends"]) == {"vulkan", "cpu"}, c
EOF
- name: API package only (no provider) + the bundle via TRANSCRIBE_LIBRARY
run: |
# --no-deps: the resolver pin would pull the provider wheel; the
# point here is that the BUNDLE is the only native artifact.
pip install --no-deps --no-index --find-links dist-api transcribe-cpp
export TRANSCRIBE_LIBRARY="$PWD/transcribe-native-linux-x86_64-cpu-vulkan/libtranscribe.so"
if [ -f canary/whisper-tiny-Q5_K_M.gguf ]; then
python assets/scripts/ci/vulkan_degradation_check.py \
--tier no-loader \
--model canary/whisper-tiny-Q5_K_M.gguf --audio assets/samples/jfk.wav
else
echo "!! no canary (fork without HF_TOKEN) — load-only check"
python assets/scripts/ci/vulkan_degradation_check.py --tier no-loader
fi
vulkan-hw:
# Real-GPU Vulkan truth lane — the Linux mirror of wheel-macos on the
# mini: installs the SHIPPED linux x86_64 wheel on the self-hosted T14
# (AMD Renoir iGPU, RADV — runner "linux-t14-4750u", labels include
# "vulkan" so generic [self-hosted, Linux, X64] jobs can never land here
# by accident) and proves what no hosted runner can:
# 1. the Vulkan device is discovered through the DEFAULT selection path
# (no GGML_VK_VISIBLE_DEVICES — integrated GPUs pass ggml's filter,
# first proven manually on this same machine 2026-06-10);
# 2. a real transcription runs ON the GPU with backend="vulkan";
# 3. the fat-CPU runtime scoring picks the right tier for Zen2
# (AVX2, no AVX-512 ⇒ ggml-cpu-haswell) — the M6.5 hardware
# assertion. lavapipe degradation stays on the hosted lanes
# (provider-dl-vulkan, clean-install); this lane is the hardware
# truth for the artifact that ships.
needs: [wheel-linux, api-wheel]
runs-on: [self-hosted, Linux, X64, vulkan]
timeout-minutes: 20 # bound the run if the T14 is offline
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
- uses: actions/download-artifact@v8
with:
pattern: dist-*
merge-multiple: true
path: wheelhouse
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
- name: Install the built wheels (venv; resolver pulls the native pin)
run: |
python3 -m venv "$RUNNER_TEMP/vk-venv"
"$RUNNER_TEMP/vk-venv/bin/pip" install -q --no-index \
--find-links wheelhouse transcribe-cpp
- name: "Real Vulkan: default-path discovery + GPU transcription + CPU tier"
if: env.HF_TOKEN != ''
run: |
"$RUNNER_TEMP/vk-venv/bin/python" - <<'EOF' 2>&1 | tee probe.log
import array, wave
import transcribe_cpp as t
devs = [(d.name, d.kind) for d in t.backends()]
print("devices:", devs)
# DEFAULT selection path — no GGML_VK_VISIBLE_DEVICES override.
assert any(k == "vulkan" for _, k in devs), devs
assert t.backend_available("vulkan")
with wave.open("samples/jfk.wav", "rb") as w:
pcm16 = array.array("h")
pcm16.frombytes(w.readframes(w.getnframes()))
pcm = array.array("f", (s / 32768.0 for s in pcm16))
with t.Model("canary/whisper-tiny-Q5_K_M.gguf", backend="vulkan") as m:
with m.session() as s:
text = s.run(pcm).text
print("text (vulkan):", text.strip())
assert "country" in text.lower(), text
print("ok: real-GPU Vulkan transcription on RADV")
EOF
# M6.5 tier assertion: Zen2 = AVX2 without AVX-512, so runtime
# feature scoring must select the haswell CPU module.
grep -q "load_backend: loaded CPU backend from .*ggml-cpu-haswell" probe.log \
|| { echo "::error::expected the haswell CPU tier on Zen2"; exit 1; }
co-import:
# numpy/torch coexistence (the reason the wheels vendor no OpenMP/BLAS),
# proven by real transcriptions in both import orders. Also a de-facto
# clean-install test: pip resolves transcribe-cpp + its native pin purely
# from the built artifacts.
name: co-import (${{ matrix.label }})
needs: [wheel-linux, wheel-macos, wheel-windows, api-wheel]
strategy:
fail-fast: false
matrix:
include:
- label: linux
runner: blacksmith-2vcpu-ubuntu-2404
torch-args: "--index-url https://download.pytorch.org/whl/cpu"
# macOS arm64 runs on the bare-metal M4 mini, not a GitHub VM: the
# transcription here exercises the REAL shipping Metal path alongside
# numpy/torch, and avoids the broken "Apple Paravirtual device" Metal
# that GitHub-hosted macos runners expose (it decodes garbage — the
# exact reason wheel-macos is on the mini too).
- label: macos-arm64
runner: [self-hosted, macOS, ARM64]
torch-args: ""
- label: windows
runner: blacksmith-2vcpu-windows-2025
torch-args: ""
runs-on: ${{ matrix.runner }}
timeout-minutes: 45 # bound the run if the self-hosted mini is offline
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0 # for fetch-canary's uvx
# setup-python's macOS downloads are built for GitHub's hosted images
# (non-relocatable, hardcoded /Users/runner prefix) and cannot install
# on a self-hosted runner under a different user (observed:
# "mkdir: /Users/runner: Permission denied"). On the mini, use the
# python.org universal2 CPython already installed for wheel-macos,
# isolated in a fresh venv so pip installs never touch the system copy.
- uses: actions/setup-python@v6
if: matrix.label != 'macos-arm64'
with:
python-version: "3.12"
- name: Python venv from the mini's python.org CPython
if: matrix.label == 'macos-arm64'
run: |
/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12 \
-m venv "$RUNNER_TEMP/ci-venv"
echo "$RUNNER_TEMP/ci-venv/bin" >> "$GITHUB_PATH"
- uses: actions/download-artifact@v8
with:
pattern: dist-*
merge-multiple: true
path: wheelhouse
- name: Install the built artifacts (resolver picks the platform wheel)
run: |
pip install --no-index --find-links wheelhouse transcribe-cpp
pip install numpy
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
- name: numpy co-import smoke
run: python scripts/ci/co_import_smoke.py numpy .
- name: torch co-import smoke
run: |
pip install torch ${{ matrix.torch-args }}
python scripts/ci/co_import_smoke.py torch .