Prepare 0.2.0 and fix Compose GPU syntax #52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Container | |
| # Building the image compiles whisper.cpp from source, which is by far the most | |
| # expensive thing in CI. Application behavior is covered by the Quality | |
| # workflow; this one runs on the container definition and dependency inputs | |
| # that decide whether the image itself assembles. Its CPU smoke test then proves | |
| # the installed package starts and serves from that image. | |
| # | |
| # One Dockerfile now builds all three accelerator images, so a change to it can | |
| # break cuda or vulkan while cpu still succeeds. The matrix below builds each | |
| # one. They are the reason this belongs in CI rather than on a laptop. A | |
| # portable cuda image compiles kernels for a broad GPU architecture spread; the | |
| # matrix narrows its compile-only validation to one architecture because no GPU | |
| # executes the cache-only result. Release/operator builds keep the portable | |
| # Dockerfile default. | |
| on: | |
| push: | |
| branches: [main] | |
| paths: &container-paths | |
| - 'Dockerfile' | |
| - '.dockerignore' | |
| - 'compose.yaml' | |
| - 'pyproject.toml' | |
| - 'uv.lock' | |
| - '.github/workflows/container.yml' | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| paths: *container-paths | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| build: | |
| if: github.event_name != 'pull_request' || !github.event.pull_request.draft | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: ${{ matrix.timeout }} | |
| strategy: | |
| # One accelerator failing says nothing about the others, and the cuda job | |
| # is long enough that cancelling it on a fast vulkan failure wastes the | |
| # whole run. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # The cpu image is the one every deployment gets by default, so it is | |
| # also the one that is loaded and actually run below. | |
| - accel: cpu | |
| timeout: 40 | |
| smoke: true | |
| reclaim-disk: false | |
| build_jobs: 4 | |
| cmake_extra: '' | |
| - accel: vulkan | |
| timeout: 40 | |
| smoke: false | |
| reclaim-disk: false | |
| build_jobs: 4 | |
| cmake_extra: '' | |
| # The CUDA devel/runtime bases and Python engine layers are still much | |
| # larger than the other variants, so reclaim the runner's unused SDKs. | |
| - accel: cuda | |
| timeout: 40 | |
| smoke: false | |
| reclaim-disk: true | |
| build_jobs: 3 | |
| # Compiling every CUDA architecture dominates the job despite the | |
| # image never running on a GPU. Ada is a representative real-code | |
| # compile; CPU dispatch is already built and exercised above. | |
| cmake_extra: >- | |
| -DCMAKE_CUDA_ARCHITECTURES=89-real | |
| -DGGML_CPU_ALL_VARIANTS=OFF | |
| name: build (${{ matrix.accel }}) | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Reclaim runner disk space | |
| if: matrix.reclaim-disk | |
| run: | | |
| # Preinstalled SDKs this build never touches. Worth ~25 GB, which is | |
| # the difference between the cuda image building and the runner | |
| # filling up partway through. | |
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc | |
| sudo rm -rf /usr/local/share/boost "${AGENT_TOOLSDIRECTORY}" | |
| df -h / | |
| - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 | |
| - name: Build ${{ matrix.accel }} image | |
| run: | | |
| # A smoke-tested image has to reach the local daemon; the others only | |
| # need to prove they assemble, so they stop at the cache. | |
| if [ "${SMOKE}" = "true" ]; then | |
| output="type=docker" | |
| else | |
| output="type=cacheonly" | |
| fi | |
| # Only the sha is stamped here: a commit subject is attacker-supplied | |
| # text on a pull request, and expanding it into this shell would be an | |
| # injection. Release builds that publish an image add the subject from | |
| # a checkout instead. | |
| docker buildx build \ | |
| --tag "vocagateway:test-${ACCEL}" \ | |
| --build-arg "ACCEL=${ACCEL}" \ | |
| --build-arg "BUILD_JOBS=${BUILD_JOBS}" \ | |
| --build-arg "WHISPER_CMAKE_EXTRA=${CMAKE_EXTRA}" \ | |
| --build-arg VOCAGATEWAY_GIT_COMMIT="${GITHUB_SHA}" \ | |
| --cache-from "type=gha,scope=gateway-${ACCEL}" \ | |
| --cache-to "type=gha,mode=max,scope=gateway-${ACCEL}" \ | |
| --output "${output}" \ | |
| . | |
| env: | |
| ACCEL: ${{ matrix.accel }} | |
| BUILD_JOBS: ${{ matrix.build_jobs }} | |
| CMAKE_EXTRA: ${{ matrix.cmake_extra }} | |
| SMOKE: ${{ matrix.smoke }} | |
| # Everything below is the cpu image only. It is what a laptop build was | |
| # being used to check by hand, and it catches the failures a successful | |
| # `docker build` does not: a backend that is present but never loaded, a | |
| # wheel that is missing its templates, a container that cannot start under | |
| # the hardening compose applies. | |
| - name: Check ggml loads a CPU backend variant | |
| if: matrix.smoke | |
| run: | | |
| set -euo pipefail | |
| curl --fail --location --silent --show-error \ | |
| --output jfk.wav \ | |
| https://raw.githubusercontent.com/ggml-org/whisper.cpp/v1.9.1/samples/jfk.wav | |
| curl --fail --location --silent --show-error \ | |
| --output ggml-tiny.en.bin \ | |
| https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin | |
| docker run --rm --volume "${PWD}:/bench:ro" \ | |
| --entrypoint whisper-cli vocagateway:test-cpu \ | |
| -m /bench/ggml-tiny.en.bin -f /bench/jfk.wav -nt -t 2 -bs 2 -bo 2 \ | |
| > transcript.txt 2> backend.log || { cat backend.log; exit 1; } | |
| grep 'load_backend: loaded CPU backend' backend.log | |
| # GGML_CPU_ALL_VARIANTS exists so a portable image still runs the | |
| # host's instruction set. A GitHub runner is well past Haswell, so | |
| # loading the x64 baseline means dispatch silently regressed and every | |
| # deployment is running scalar kernels. | |
| if grep -q 'libggml-cpu-x64\.so' backend.log; then | |
| echo "::error::ggml fell back to the baseline x64 CPU backend" >&2 | |
| cat backend.log >&2 | |
| exit 1 | |
| fi | |
| grep -qi 'ask not what your country can do for you' transcript.txt | |
| - name: Check the container serves under compose's hardening | |
| if: matrix.smoke | |
| run: | | |
| set -euo pipefail | |
| # The same restrictions compose.yaml applies, so a capability the | |
| # gateway turns out to need fails here rather than on a deployment. | |
| docker run --detach --name vg-smoke --publish 18765:8765 \ | |
| --cap-drop ALL \ | |
| --security-opt no-new-privileges:true \ | |
| --tmpfs /tmp:size=64m,mode=1777 \ | |
| vocagateway:test-cpu | |
| for _ in $(seq 1 30); do | |
| status="$(docker inspect --format '{{.State.Health.Status}}' vg-smoke)" | |
| [ "${status}" = "healthy" ] && break | |
| [ "${status}" = "unhealthy" ] && break | |
| sleep 2 | |
| done | |
| if [ "${status}" != "healthy" ]; then | |
| echo "::error::container never became healthy (${status})" >&2 | |
| docker logs vg-smoke >&2 | |
| exit 1 | |
| fi | |
| curl --fail --silent --show-error --output /dev/null http://127.0.0.1:18765/health/live | |
| # The WebUI renders Jinja templates from the installed wheel, so a 200 | |
| # here is what proves the non-editable install carried app/templates. | |
| curl --fail --silent --show-error --output /dev/null http://127.0.0.1:18765/ | |
| - name: Container logs | |
| if: failure() && matrix.smoke | |
| run: docker logs vg-smoke || true | |
| compose: | |
| if: github.event_name != 'pull_request' || !github.event.pull_request.draft | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Validate every profile interpolates | |
| run: | | |
| # `just compose` checks the default service. The profile services carry | |
| # the device wiring, so they need naming explicitly to be parsed. | |
| VOCAGATEWAY_TOKEN=test-token-with-at-least-thirty-two-characters \ | |
| docker compose --profile cuda --profile vulkan config --quiet |