mirror of
https://github.com/ollama/ollama.git
synced 2026-10-24 20:41:37 +00:00
ci: rebuild MLX macOS test payloads the release can't supply (#18219)
* ci: rebuild MLX macOS test payloads the release can't supply The MLX unit test dependency payload can go stale when a PR changes the xgrammar wrapper (release dylib lacks the new symbols) or bumps MLX pins (no matching release, so MLX tests silently skipped). Compare the matched tag's build rules and wrapper sources against the checkout, rebuilding only libollama_xgrammar.dylib on drift; with no matching release, build one Metal variant at the platform default. * address comments
This commit is contained in:
parent
b79067b0db
commit
59fe23d85c
1 changed files with 233 additions and 52 deletions
285
.github/scripts/prepare_mlx_darwin.sh
vendored
285
.github/scripts/prepare_mlx_darwin.sh
vendored
|
|
@ -1,15 +1,21 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Prepare prebuilt MLX Metal runtime libraries for macOS CI unit tests.
|
||||
# Prepare MLX runtime libraries for macOS CI unit tests.
|
||||
#
|
||||
# Building MLX is expensive, so to enable the MLX-specific unit tests this
|
||||
# helper finds the newest Ollama release whose MLX_VERSION and MLX_C_VERSION
|
||||
# match the current checkout, downloads that release's ollama-darwin.tgz, and
|
||||
# extracts only mlx_metal_v* into build/lib/ollama
|
||||
# extracts only mlx_metal_v* into build/lib/ollama.
|
||||
#
|
||||
# If no matching release artifact exists, the helper only emits a warning. That
|
||||
# covers the expected window after an MLX update lands and before the next release
|
||||
# publishes a matching Darwin payload.
|
||||
# The payload also depends on Ollama's payload build rules (cmake glue and
|
||||
# carried mlx/compat patches) and the xgrammar native wrapper
|
||||
# (x/mlxrunner/xgrammar/native). Rule drift rebuilds the whole payload from
|
||||
# source; wrapper-only drift rebuilds just libollama_xgrammar.dylib.
|
||||
#
|
||||
# If no release matches the MLX pins (e.g. right after a pin bump), the
|
||||
# helper builds the minimal MLX payload for this platform: a single Metal
|
||||
# variant using the superbuild's platform default (metal_v4 on macOS 26.2+
|
||||
# SDKs, otherwise metal_v3), including a fresh libollama_xgrammar.dylib.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -17,6 +23,7 @@ repo="${OLLAMA_MLX_RELEASE_REPO:-ollama/ollama}"
|
|||
scan_limit="${OLLAMA_MLX_RELEASE_SCAN_LIMIT:-50}"
|
||||
cache_dir="${OLLAMA_MLX_DARWIN_CACHE:-.cache/mlx-darwin-release}"
|
||||
target_dir="${OLLAMA_MLX_DARWIN_TARGET:-build/lib/ollama}"
|
||||
ci_build_dir="${OLLAMA_MLX_CI_BUILD_DIR:-build/mlx-ci}"
|
||||
tarball="${cache_dir}/ollama-darwin.tgz"
|
||||
tag_file="${cache_dir}/matched-tag"
|
||||
pins_file="${cache_dir}/matched-pins"
|
||||
|
|
@ -42,6 +49,71 @@ read_pin() {
|
|||
tr -d '[:space:]' <"$1"
|
||||
}
|
||||
|
||||
# Native wrapper sources compiled into libollama_xgrammar.dylib — keep in
|
||||
# sync with the ollama_xgrammar target in cmake/mlx/CMakeLists.txt.
|
||||
xgrammar_native_dir=x/mlxrunner/xgrammar/native
|
||||
|
||||
# Payload build rules beyond the MLX_VERSION/MLX_C_VERSION pins.
|
||||
payload_rule_files=(
|
||||
"cmake/local.cmake"
|
||||
"cmake/apply-git-patches.cmake"
|
||||
"cmake/mlx/CMakeLists.txt"
|
||||
"cmake/mlx/CMakePresets.json"
|
||||
"x/mlxrunner/mlx/CMakeLists.txt"
|
||||
)
|
||||
|
||||
# Build-rule inputs: the rule files plus carried MLX/MLX-C patch content.
|
||||
rule_inputs() {
|
||||
local file
|
||||
for file in "${payload_rule_files[@]}"; do
|
||||
printf '%s\n' "${file}"
|
||||
done
|
||||
if [ -d mlx/compat ]; then
|
||||
find mlx/compat -type f | sort
|
||||
fi
|
||||
}
|
||||
|
||||
wrapper_inputs() {
|
||||
find "${xgrammar_native_dir}" -type f | sort
|
||||
}
|
||||
|
||||
payload_inputs() {
|
||||
rule_inputs
|
||||
wrapper_inputs
|
||||
}
|
||||
|
||||
payload_fingerprint() {
|
||||
local file
|
||||
{
|
||||
payload_inputs
|
||||
while IFS= read -r file; do
|
||||
cat "${file}" 2>/dev/null || true
|
||||
done < <(payload_inputs)
|
||||
} | shasum -a 256 | awk '{print $1}'
|
||||
}
|
||||
|
||||
# True when the tag matches the checkout on all payload inputs.
|
||||
tag_matches_payload() {
|
||||
local tag="$1" file
|
||||
while IFS= read -r file; do
|
||||
if ! curl -fsSL "https://raw.githubusercontent.com/${repo}/${tag}/${file}" 2>/dev/null | cmp -s - "${file}"; then
|
||||
return 1
|
||||
fi
|
||||
done < <(payload_inputs)
|
||||
return 0
|
||||
}
|
||||
|
||||
# True when the tag matches the checkout on the build rules.
|
||||
tag_matches_rules() {
|
||||
local tag="$1" file
|
||||
while IFS= read -r file; do
|
||||
if ! curl -fsSL "https://raw.githubusercontent.com/${repo}/${tag}/${file}" 2>/dev/null | cmp -s - "${file}"; then
|
||||
return 1
|
||||
fi
|
||||
done < <(rule_inputs)
|
||||
return 0
|
||||
}
|
||||
|
||||
has_payload() {
|
||||
local variant
|
||||
for variant in "${target_dir}"/mlx_metal_v*; do
|
||||
|
|
@ -54,7 +126,14 @@ has_payload() {
|
|||
has_matching_payload() {
|
||||
[ -f "${target_pins_file}" ] || return 1
|
||||
[ "$(cat "${target_pins_file}")" = "${current_pins}" ] || return 1
|
||||
has_payload
|
||||
has_payload || return 1
|
||||
# Every payload variant must carry libollama_xgrammar.dylib.
|
||||
local variant
|
||||
for variant in "${target_dir}"/mlx_metal_v*; do
|
||||
[ -d "${variant}" ] || continue
|
||||
[ -f "${variant}/libollama_xgrammar.dylib" ] || return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
extract_payload() {
|
||||
|
|
@ -89,6 +168,85 @@ extract_payload() {
|
|||
tmpdir=""
|
||||
}
|
||||
|
||||
# Resolve the superbuild's platform-default MLX backend (metal_v3/metal_v4 on
|
||||
# arm64; empty when the platform has no MLX backend, e.g. x86_64 macOS).
|
||||
ci_mlx_backend() {
|
||||
[ -f "${ci_build_dir}/CMakeCache.txt" ] || return 1
|
||||
sed -n 's/^OLLAMA_MLX_BACKENDS:STRING=//p' "${ci_build_dir}/CMakeCache.txt"
|
||||
}
|
||||
|
||||
# Configure the repo-root superbuild and fetch MLX/MLX-C sources at the
|
||||
# pinned revisions (only the full payload build needs this).
|
||||
build_ci_sources() {
|
||||
cmake -S . -B "${ci_build_dir}" \
|
||||
-DOLLAMA_LLAMA_BACKENDS= \
|
||||
-DOLLAMA_PAYLOAD_INSTALL_PREFIX="$(dirname "$(dirname "${target_dir}")")"
|
||||
cmake --build "${ci_build_dir}" --target ollama-mlx-sources --parallel
|
||||
}
|
||||
|
||||
# Rebuild only libollama_xgrammar.dylib into the extracted payload. The
|
||||
# target depends only on the pinned XGrammar sources and the native wrapper;
|
||||
# the Metal toolchain and the superbuild are not involved. MLX is fetched
|
||||
# only because the cmake/mlx project defines it — nothing from it is built.
|
||||
build_ci_xgrammar() {
|
||||
local lib variant
|
||||
local xg_build_dir="${ci_build_dir}/xgrammar"
|
||||
local -a configure_args=(-S cmake/mlx -B "${xg_build_dir}" -DOLLAMA_SOURCE_DIR="$(pwd)" -DMLX_BUILD_METAL=OFF)
|
||||
if [ -n "${OLLAMA_XGRAMMAR_SOURCE:-}" ]; then
|
||||
configure_args+=("-DFETCHCONTENT_SOURCE_DIR_XGRAMMAR=${OLLAMA_XGRAMMAR_SOURCE}")
|
||||
fi
|
||||
cmake "${configure_args[@]}"
|
||||
cmake --build "${xg_build_dir}" --target ollama_xgrammar --parallel
|
||||
lib="${xg_build_dir}/lib/ollama/libollama_xgrammar.dylib"
|
||||
[ -f "${lib}" ] || {
|
||||
echo "ollama_xgrammar build produced no library at ${lib}" >&2
|
||||
exit 1
|
||||
}
|
||||
for variant in "${target_dir}"/mlx_metal_v*; do
|
||||
[ -d "${variant}" ] || continue
|
||||
cp -f "${lib}" "${variant}/libollama_xgrammar.dylib"
|
||||
[ -f "${variant}/libollama_xgrammar.dylib" ] || {
|
||||
echo "failed to install ${variant}/libollama_xgrammar.dylib" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
echo "Rebuilt libollama_xgrammar.dylib from source into ${target_dir}"
|
||||
}
|
||||
|
||||
# Build the minimal MLX payload for this platform: one Metal variant,
|
||||
# whatever the superbuild defaults to here.
|
||||
build_ci_payload() {
|
||||
local backend variant
|
||||
build_ci_sources
|
||||
backend="$(ci_mlx_backend)"
|
||||
case "${backend}" in
|
||||
metal_v3 | metal_v4) ;;
|
||||
*)
|
||||
warn "no MLX backend applicable to this platform; MLX unit tests will be skipped"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "Building the ${backend} payload for unit tests"
|
||||
rm -rf "${target_dir}"/mlx_metal_v*
|
||||
cmake --build "${ci_build_dir}" --target "ollama-mlx-${backend}" --parallel
|
||||
for variant in "${target_dir}"/mlx_metal_v*; do
|
||||
[ -d "${variant}" ] || continue
|
||||
for lib in libmlx.dylib libmlxc.dylib libollama_xgrammar.dylib; do
|
||||
[ -f "${variant}/${lib}" ] || {
|
||||
echo "built payload is missing ${variant}/${lib}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
done
|
||||
has_payload || {
|
||||
echo "built payload is incomplete in ${target_dir}" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "${current_pins}" >"${target_pins_file}"
|
||||
echo "Built MLX payload for unit tests:"
|
||||
find "${target_dir}" -maxdepth 2 -type f \( -name 'libmlx.dylib' -o -name 'libmlxc.dylib' -o -name 'libollama_xgrammar.dylib' -o -name '*.metallib' \) -print
|
||||
}
|
||||
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
warn "MLX Darwin payload setup is only supported on macOS"
|
||||
exit 0
|
||||
|
|
@ -96,64 +254,87 @@ fi
|
|||
|
||||
current_mlx="$(read_pin MLX_VERSION)"
|
||||
current_mlxc="$(read_pin MLX_C_VERSION)"
|
||||
current_pins="${current_mlx} ${current_mlxc}"
|
||||
# The release tarball only depends on the MLX pins; the extracted payload's
|
||||
# xgrammar library additionally depends on the tree's XGrammar inputs.
|
||||
component_pins="${current_mlx} ${current_mlxc}"
|
||||
current_pins="${component_pins} $(payload_fingerprint)"
|
||||
|
||||
if has_matching_payload; then
|
||||
echo "MLX Darwin payload already present in ${target_dir}"
|
||||
echo "MLX payload already present in ${target_dir}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "${cache_dir}"
|
||||
|
||||
if [ -s "${tarball}" ] && [ -f "${tag_file}" ] && [ "$(cat "${pins_file}" 2>/dev/null || true)" = "${current_pins}" ]; then
|
||||
if [ -s "${tarball}" ] && [ -f "${tag_file}" ] && [ "$(cat "${pins_file}" 2>/dev/null || true)" = "${component_pins}" ]; then
|
||||
extract_payload "$(cat "${tag_file}")"
|
||||
else
|
||||
matched_tag=""
|
||||
matched_url=""
|
||||
|
||||
while read -r tag; do
|
||||
[ -n "${tag}" ] || continue
|
||||
|
||||
if ! tag_mlx="$(curl -fsSL "https://raw.githubusercontent.com/${repo}/${tag}/MLX_VERSION" | tr -d '[:space:]')"; then
|
||||
continue
|
||||
fi
|
||||
if [ "${tag_mlx}" != "${current_mlx}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! tag_mlxc="$(curl -fsSL "https://raw.githubusercontent.com/${repo}/${tag}/MLX_C_VERSION" | tr -d '[:space:]')"; then
|
||||
continue
|
||||
fi
|
||||
if [ "${tag_mlxc}" != "${current_mlxc}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
url="https://github.com/${repo}/releases/download/${tag}/ollama-darwin.tgz"
|
||||
if curl -fsIL "${url}" >/dev/null; then
|
||||
matched_tag="${tag}"
|
||||
matched_url="${url}"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "MLX pins match ${tag}, but ${url} is not available"
|
||||
done < <(
|
||||
git ls-remote --tags --refs --sort=-version:refname "https://github.com/${repo}.git" 'v*' |
|
||||
awk -v limit="${scan_limit}" '{ sub("refs/tags/", "", $2); print $2; if (limit > 0 && NR >= limit) exit }'
|
||||
)
|
||||
|
||||
if [ -z "${matched_tag}" ]; then
|
||||
echo "No release carries MLX_VERSION=${current_mlx} MLX_C_VERSION=${current_mlxc}"
|
||||
build_ci_payload
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp_tarball="${tarball}.tmp"
|
||||
rm -f "${tmp_tarball}"
|
||||
curl -fL --retry 3 --retry-delay 2 -o "${tmp_tarball}" "${matched_url}"
|
||||
mv "${tmp_tarball}" "${tarball}"
|
||||
tmp_tarball=""
|
||||
echo "${matched_tag}" >"${tag_file}"
|
||||
echo "${component_pins}" >"${pins_file}"
|
||||
|
||||
extract_payload "${matched_tag}"
|
||||
fi
|
||||
|
||||
if tag_matches_payload "$(cat "${tag_file}")"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
matched_tag=""
|
||||
matched_url=""
|
||||
|
||||
while read -r tag; do
|
||||
[ -n "${tag}" ] || continue
|
||||
|
||||
if ! tag_mlx="$(curl -fsSL "https://raw.githubusercontent.com/${repo}/${tag}/MLX_VERSION" | tr -d '[:space:]')"; then
|
||||
continue
|
||||
fi
|
||||
if [ "${tag_mlx}" != "${current_mlx}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! tag_mlxc="$(curl -fsSL "https://raw.githubusercontent.com/${repo}/${tag}/MLX_C_VERSION" | tr -d '[:space:]')"; then
|
||||
continue
|
||||
fi
|
||||
if [ "${tag_mlxc}" != "${current_mlxc}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
url="https://github.com/${repo}/releases/download/${tag}/ollama-darwin.tgz"
|
||||
if curl -fsIL "${url}" >/dev/null; then
|
||||
matched_tag="${tag}"
|
||||
matched_url="${url}"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "MLX pins match ${tag}, but ${url} is not available"
|
||||
done < <(
|
||||
git ls-remote --tags --refs --sort=-version:refname "https://github.com/${repo}.git" 'v*' |
|
||||
awk -v limit="${scan_limit}" '{ sub("refs/tags/", "", $2); print $2; if (limit > 0 && NR >= limit) exit }'
|
||||
)
|
||||
|
||||
if [ -z "${matched_tag}" ]; then
|
||||
warn "MLX unit tests are temporarily disabled until a release build publishes ollama-darwin.tgz for MLX_VERSION=${current_mlx} MLX_C_VERSION=${current_mlxc}"
|
||||
if [ "$(uname -m)" != "arm64" ]; then
|
||||
warn "MLX payload builds are only supported on arm64 macOS; MLX unit tests will be skipped"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp_tarball="${tarball}.tmp"
|
||||
rm -f "${tmp_tarball}"
|
||||
curl -fL --retry 3 --retry-delay 2 -o "${tmp_tarball}" "${matched_url}"
|
||||
mv "${tmp_tarball}" "${tarball}"
|
||||
tmp_tarball=""
|
||||
echo "${matched_tag}" >"${tag_file}"
|
||||
echo "${current_pins}" >"${pins_file}"
|
||||
if tag_matches_rules "$(cat "${tag_file}")"; then
|
||||
# Only the xgrammar wrapper drifted; keep the rest of the release payload.
|
||||
echo "Rebuilding libollama_xgrammar.dylib from source into ${target_dir}"
|
||||
rm -f "${target_dir}"/mlx_metal_v*/libollama_xgrammar.dylib
|
||||
build_ci_xgrammar
|
||||
exit 0
|
||||
fi
|
||||
|
||||
extract_payload "${matched_tag}"
|
||||
echo "Release payload build rules do not match this checkout"
|
||||
build_ci_payload
|
||||
Loading…
Add table
Add a link
Reference in a new issue