Skip to content

Bump llama.cpp to 4801e3c56 (b10362), release v0.8.43 (#84) #83

Bump llama.cpp to 4801e3c56 (b10362), release v0.8.43 (#84)

Bump llama.cpp to 4801e3c56 (b10362), release v0.8.43 (#84) #83

Workflow file for this run

name: Precompile NIFs
on:
push:
tags: ['v*']
# Every `uses:` below is pinned to a full commit SHA rather than a moving tag.
# This is the workflow that holds HEX_API_KEY, so a retagged action would be a
# supply-chain takeover of the published package. Pinned releases:
# actions/checkout v6.1.0 -> d23441a48e516b6c34aea4fa41551a30e30af803
# erlef/setup-beam v1.24.1 -> 54075bcc5e249e4758d363f27d099f55d843f124
# Re-resolve with `git ls-remote https://github.com/<owner>/<repo> 'refs/tags/v*'`
# and take the dereferenced (`^{}`) line when the tag is annotated.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
ELIXIR_VERSION: "1.18"
OTP_VERSION: "27.0"
jobs:
# Create the GitHub Release up front, as a draft. The matrix below uploads into
# it, so users never see a release carrying a partial set of assets: the draft
# is only flipped off by the `checksum` job, after every declared artifact has
# been confirmed present.
prepare_release:
name: Create draft release
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Create or reuse the draft release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
run: |
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "release $TAG_NAME already exists; reusing it"
else
gh release create "$TAG_NAME" --draft --verify-tag --generate-notes
fi
precompile:
needs: [prepare_release]
strategy:
# Report every leg. Failing fast would hide a second broken target, and the
# release stays a draft either way until all four artifacts are present.
fail-fast: false
matrix:
include:
# `toolkit` and `variant` are spelled out on every leg, empty where
# there is no CUDA. An absent matrix key is null, and `null != ''` in a
# GitHub expression resolves through numeric coercion rather than
# string comparison; being explicit keeps the `if:` guards below from
# depending on that.
- os: macos-14
target: aarch64-apple-darwin
otp: '27.0'
elixir: '1.18'
backend: metal
toolkit: ''
variant: ''
# OTP 25 reports NIF 2.16, OTP 26/27/28 all report 2.17, and OTP 29 is
# the first release to report 2.18 (verified against
# erts/emulator/beam/erl_nif.h in the OTP source). So OTP 27 and OTP 29
# are the two builds that produce distinct artifacts; adding 28 would
# just overwrite the 27 one.
- os: macos-14
target: aarch64-apple-darwin
otp: '29.0'
elixir: '1.20'
backend: metal
toolkit: ''
variant: ''
- os: ubuntu-22.04
target: x86_64-linux-gnu
otp: '27.0'
elixir: '1.18'
backend: cpu
toolkit: ''
variant: ''
- os: ubuntu-22.04
target: x86_64-linux-gnu
otp: '29.0'
elixir: '1.20'
backend: cpu
toolkit: ''
variant: ''
# CUDA artifacts are per major version because the NIF links
# libcudart/libcublas/libcublasLt dynamically and those sonames are
# major-versioned: one Linux CUDA build cannot serve both. 22.04 is
# kept for the same reason as the CPU legs -- it is the oldest glibc
# these artifacts have to load against.
- os: ubuntu-22.04
target: x86_64-linux-gnu-cu12
otp: '27.0'
elixir: '1.18'
backend: cuda
toolkit: '12-9'
variant: cu12
- os: ubuntu-22.04
target: x86_64-linux-gnu-cu12
otp: '29.0'
elixir: '1.20'
backend: cuda
toolkit: '12-9'
variant: cu12
- os: ubuntu-22.04
target: x86_64-linux-gnu-cu13
otp: '27.0'
elixir: '1.18'
backend: cuda
toolkit: '13-0'
variant: cu13
- os: ubuntu-22.04
target: x86_64-linux-gnu-cu13
otp: '29.0'
elixir: '1.20'
backend: cuda
toolkit: '13-0'
variant: cu13
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
submodules: recursive
# Before setup-beam, which installs into the tool cache this would remove.
- name: Free disk space for the CUDA toolkit
if: matrix.toolkit != ''
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/share/boost
df -h /
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1
with:
otp-version: ${{ matrix.otp }}
elixir-version: ${{ matrix.elixir }}
- name: Install cmake (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y cmake
- name: Install the CUDA toolkit
if: matrix.toolkit != ''
run: |
curl -fsSLO https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get install -y --no-install-recommends cuda-toolkit-${{ matrix.toolkit }}
# Not added to PATH on purpose: the Makefile resolves the toolkit
# itself, and leaving nvcc off PATH keeps this leg honest about the
# discovery path most real machines take.
ls -d /usr/local/cuda-*
- name: Set version from tag
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
# GITHUB_REF is attacker-influenceable — anyone who can push a tag
# controls it — and it is interpolated into the sed script below, where
# GNU sed's `e` flag turns a crafted value into command execution. It
# also lands in $GITHUB_ENV. Accept strict semver and nothing else.
# The `case` glob is load-bearing, not belt-and-braces: `grep -Eq` is
# LINE-oriented, so a two-line value whose *second* line is valid semver
# passes the regex on its own. The glob rejects the newline (it is
# outside [0-9A-Za-z.+-]) before grep ever sees it. Do not "simplify"
# this to the regex alone.
case "$TAG_VERSION" in
''|*[!0-9A-Za-z.+-]*)
echo "::error::refusing to release '${GITHUB_REF}': version '${TAG_VERSION}' has characters outside [0-9A-Za-z.+-]"
exit 1
;;
esac
if ! printf '%s' "$TAG_VERSION" |
grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
echo "::error::refusing to release '${GITHUB_REF}': version '${TAG_VERSION}' is not semver"
exit 1
fi
echo "TAG_VERSION=${TAG_VERSION}" >> "$GITHUB_ENV"
sed -i'' -e "s/@version \".*\"/@version \"${TAG_VERSION}\"/" mix.exs
grep '@version' mix.exs
- run: mix deps.get
- name: Create precompiled library
run: |
export ELIXIR_MAKE_CACHE_DIR=$(pwd)/cache
mkdir -p "${ELIXIR_MAKE_CACHE_DIR}"
mix elixir_make.precompile
env:
LLAMA_BACKEND: ${{ matrix.backend }}
# Drop -march=native. ggml enables it by default, which would tune this
# artifact to whatever CPU the runner happened to have and hand users a
# SIGILL on older hardware.
LLAMA_PORTABLE: '1'
# Names the artifact. Detection cannot be trusted here: it requires a
# driver before it will claim a CUDA target, and a release runner has
# a toolkit and no driver, so an unset variant would silently publish
# a CUDA build under the CPU name. Empty on the CPU legs, which pins
# them to the CPU name even on a runner that happens to have CUDA.
LLAMA_CUDA_VARIANT: ${{ matrix.variant }}
- name: The artifact must be named for the target this leg builds
run: |
set -eu
# A mismatch here means the matrix and the precompiler disagree about
# what this leg produced. Left unchecked it surfaces as the `checksum`
# job failing on a missing artifact, long after the build that could
# have explained it.
ls cache/
# Anchored on the version, so it cannot pass on a neighbouring name:
# `*-x86_64-linux-gnu-*` alone would happily match the cu12 artifact.
ls cache/*-${{ matrix.target }}-"${TAG_VERSION}".tar.gz >/dev/null \
|| { echo "::error::this leg produced no artifact named for ${{ matrix.target }}"; exit 1; }
- name: Upload artifacts to the draft release
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.ref_name }}
run: |
# Only the tarballs. The .sha256 sidecars elixir_make writes alongside
# them stay local on purpose: the checksum job then hashes the bytes it
# actually downloads instead of trusting a checksum uploaded by this job.
gh release upload "$TAG_NAME" cache/*.tar.gz --clobber
checksum:
name: Checksum and publish the release
needs: [precompile]
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
# master, not the tag: this job pushes the generated checksum.exs to master.
# The `publish` job below deliberately does the opposite and builds the Hex
# package from the tag, so the published source matches the binaries.
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: master
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1
with:
otp-version: ${{ env.OTP_VERSION }}
elixir-version: ${{ env.ELIXIR_VERSION }}
- name: Set version from tag
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
# See the matching step in `precompile`: GITHUB_REF reaches sed, and
# GNU sed's `e` flag would execute it.
# The `case` glob is load-bearing, not belt-and-braces: `grep -Eq` is
# LINE-oriented, so a two-line value whose *second* line is valid semver
# passes the regex on its own. The glob rejects the newline (it is
# outside [0-9A-Za-z.+-]) before grep ever sees it. Do not "simplify"
# this to the regex alone.
case "$TAG_VERSION" in
''|*[!0-9A-Za-z.+-]*)
echo "::error::refusing to release '${GITHUB_REF}': version '${TAG_VERSION}' has characters outside [0-9A-Za-z.+-]"
exit 1
;;
esac
if ! printf '%s' "$TAG_VERSION" |
grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
echo "::error::refusing to release '${GITHUB_REF}': version '${TAG_VERSION}' is not semver"
exit 1
fi
echo "TAG_VERSION=${TAG_VERSION}" >> "$GITHUB_ENV"
sed -i'' -e "s/@version \".*\"/@version \"${TAG_VERSION}\"/" mix.exs
grep '@version' mix.exs
- run: mix deps.get
- name: List the artifacts mix.exs declares
run: |
# Derived from mix.exs, never hardcoded here, so the gates below cannot
# drift from `all_supported_targets/1` and `make_precompiler_nif_versions`.
cat > /tmp/expected_artifacts.exs <<'SCRIPT'
Mix.start()
Code.put_compiler_option(:no_warn_undefined, :all)
Code.compile_file("mix.exs")
config = LlamaCppEx.MixProject.project()
{:nif, precompiler} = config[:make_precompiler]
for target <- precompiler.all_supported_targets(:fetch),
nif <- config[:make_precompiler_nif_versions][:versions] do
IO.puts("#{config[:app]}-nif-#{nif}-#{target}-#{config[:version]}.tar.gz")
end
SCRIPT
elixir /tmp/expected_artifacts.exs | tee /tmp/expected_artifacts.txt
test -s /tmp/expected_artifacts.txt
- name: Verify every declared artifact reached the draft release
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.ref_name }}
run: |
# `gh release view/upload/edit <tag>` all resolve a draft by its pending
# tag name (gh's shared.FetchRelease does a draft lookup alongside the
# published one), so the release is addressable before it goes live.
gh release view "$TAG_NAME" --json assets --jq '.assets[].name' > /tmp/uploaded.txt
cat /tmp/uploaded.txt
missing=0
while read -r name; do
if ! grep -Fxq "$name" /tmp/uploaded.txt; then
echo "::error::release ${TAG_NAME} is missing declared artifact ${name}"
missing=1
fi
done < /tmp/expected_artifacts.txt
test "$missing" -eq 0
- name: Publish the GitHub release
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.ref_name }}
run: gh release edit "$TAG_NAME" --draft=false
- name: Wait for the release assets to become publicly downloadable
env:
REPO: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
run: |
# mix elixir_make.checksum fetches the plain public download URLs, which
# only start serving once the release is out of draft.
first="$(head -n 1 /tmp/expected_artifacts.txt)"
url="https://github.com/${REPO}/releases/download/${TAG_NAME}/${first}"
for attempt in $(seq 1 30); do
if curl -fsSL -o /dev/null "$url"; then
echo "release assets are public"
exit 0
fi
echo "not downloadable yet (attempt ${attempt}); retrying in 5s"
sleep 5
done
echo "::error::release assets for ${TAG_NAME} never became downloadable"
exit 1
# No --ignore-unavailable: a skipped target means users download a binary
# nothing vouches for.
- run: mix elixir_make.checksum --all
- name: Verify checksum.exs covers every declared artifact
run: |
# This gate is not redundant with dropping --ignore-unavailable. On a
# failed download elixir_make prints an error and still returns [] for
# that artifact (deps/elixir_make/lib/mix/tasks/elixir_make.checksum.ex),
# so the task exits 0 with an incomplete checksum.exs either way.
cat checksum.exs
missing=0
while read -r name; do
if ! grep -Fq "\"${name}\"" checksum.exs; then
echo "::error::checksum.exs has no entry for ${name}"
missing=1
fi
done < /tmp/expected_artifacts.txt
test "$missing" -eq 0
- name: Commit checksum.exs
env:
TAG_NAME: ${{ github.ref_name }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add checksum.exs
if git diff --staged --quiet; then
echo "No changes"
exit 0
fi
git commit -m "Update checksum.exs for ${TAG_NAME}"
for attempt in 1 2 3 4 5; do
if git push origin HEAD:master; then
exit 0
fi
echo "Push rejected (attempt ${attempt}); rebasing onto origin/master"
git fetch origin master
git rebase origin/master
done
echo "Failed to push checksum commit after 5 attempts"
exit 1
publish:
name: Publish to Hex
runs-on: ubuntu-22.04
needs: [checksum]
permissions:
contents: read
steps:
# The tag, not master. The checksum job just pushed to master and its
# 5-attempt rebase loop exists because concurrent commits land there, so
# master can already differ from the source the precompiled binaries in
# checksum.exs were built from.
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.ref }}
- name: Take the generated checksum.exs from master
run: |
# The one file that legitimately comes from master: the checksum job
# generated it from the artifacts that are now on the release and
# pushed it there. Everything else in the package is the tag's tree.
git fetch --no-tags --depth 1 origin master
git checkout FETCH_HEAD -- checksum.exs
cat checksum.exs
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1
with:
otp-version: ${{ env.OTP_VERSION }}
elixir-version: ${{ env.ELIXIR_VERSION }}
- name: Set version from tag
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
# See the matching step in `precompile`: GITHUB_REF reaches sed, and
# GNU sed's `e` flag would execute it. The tag's mix.exs should already
# carry this version; the rewrite makes the tag authoritative.
# The `case` glob is load-bearing, not belt-and-braces: `grep -Eq` is
# LINE-oriented, so a two-line value whose *second* line is valid semver
# passes the regex on its own. The glob rejects the newline (it is
# outside [0-9A-Za-z.+-]) before grep ever sees it. Do not "simplify"
# this to the regex alone.
case "$TAG_VERSION" in
''|*[!0-9A-Za-z.+-]*)
echo "::error::refusing to release '${GITHUB_REF}': version '${TAG_VERSION}' has characters outside [0-9A-Za-z.+-]"
exit 1
;;
esac
if ! printf '%s' "$TAG_VERSION" |
grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
echo "::error::refusing to release '${GITHUB_REF}': version '${TAG_VERSION}' is not semver"
exit 1
fi
echo "TAG_VERSION=${TAG_VERSION}" >> "$GITHUB_ENV"
sed -i'' -e "s/@version \".*\"/@version \"${TAG_VERSION}\"/" mix.exs
grep '@version' mix.exs
- run: mix deps.get
env:
MIX_ENV: dev
- name: Verify every published artifact matches the checksum.exs being shipped
env:
REPO: ${{ github.repository }}
TAG_NAME: ${{ github.ref_name }}
run: |
# Download the bytes users will actually get and compare them against the
# checksum.exs about to be published. `mix compile` is not a substitute:
# on a checksum mismatch elixir_make prints an error and recovers with a
# source build, so it goes green on precisely the failure that matters.
cat > /tmp/expected_artifacts.exs <<'SCRIPT'
Mix.start()
Code.put_compiler_option(:no_warn_undefined, :all)
Code.compile_file("mix.exs")
config = LlamaCppEx.MixProject.project()
{:nif, precompiler} = config[:make_precompiler]
for target <- precompiler.all_supported_targets(:fetch),
nif <- config[:make_precompiler_nif_versions][:versions] do
IO.puts("#{config[:app]}-nif-#{nif}-#{target}-#{config[:version]}.tar.gz")
end
SCRIPT
elixir /tmp/expected_artifacts.exs > /tmp/expected_artifacts.txt
fail=0
while read -r name; do
expected="$(sed -n "s/.*\"${name}\" *=> *\"sha256:\([0-9a-f]*\)\".*/\1/p" checksum.exs)"
if [ -z "$expected" ]; then
echo "::error::checksum.exs has no entry for ${name}"
fail=1
continue
fi
url="https://github.com/${REPO}/releases/download/${TAG_NAME}/${name}"
if ! curl -fsSL -o /tmp/artifact.tar.gz "$url"; then
echo "::error::cannot download ${name} from the release"
fail=1
continue
fi
actual="$(sha256sum /tmp/artifact.tar.gz | cut -d ' ' -f 1)"
if [ "$actual" != "$expected" ]; then
echo "::error::${name}: published sha256 ${actual} does not match checksum.exs ${expected}"
fail=1
else
echo "ok ${name} ${actual}"
fi
done < /tmp/expected_artifacts.txt
test "$fail" -eq 0
# Build smoke test: downloads this runner's artifact through the normal
# install path and compiles the Elixir side. The checksums were already
# verified above; this catches a broken package rather than a bad binary.
- run: mix compile
env:
MIX_ENV: dev
- name: Publish to Hex
run: mix hex.publish --yes
env:
MIX_ENV: dev
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}