mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
The darwin vllm-cpp image built the Metal backend with vllm.cpp's native MSL
GEMM only. vllm.cpp also ships an optional MLX provider for the dense GEMM,
kept OFF upstream because it costs a ~19 MB libmlx.dylib plus a ~105 MB
mlx.metallib, on the stated position that it must earn that cost by
measurement.
Measured on an Apple M4 (16 GiB, macOS 26.5.2) it does. One binary, arms
toggled with VT_OP_PROVIDER_DISABLE=mlx so there is no build-difference
confound, Qwen3-1.7B-bf16 p=512 g=128, 2 reps, arm order alternated per rep:
B=1 5.79 vs 3.08 agg tok/s (1.88x) TTFT 3.32 s vs 7.68 s
B=8 25.70 vs 13.69 (1.88x) TTFT 13.95 s vs 34.38 s
B=16 38.65 vs 17.69 (2.19x) TTFT 18.33 s vs 54.48 s
Peak RSS is unchanged (6.65 to 7.50 GB in both arms) and the output is
bit-identical: vllm.cpp's three-way parity test measures mlx-vs-msl NMSE of 0
on all six shapes, and mlx-vs-cpu equal to msl-vs-cpu, against a 5e-4 bar. MLX
serves the dense GEMM alone; paged attention stays vllm.cpp's own kernel
because MLX has no paged-KV primitive. Full disposition, including the
INDICATIVE status and the isolation actually achieved, is in vllm.cpp
docs/BENCHMARKS.md "MLX GEMM provider A/B on Apple M4".
Build: MLX comes from the pinned prebuilt pip wheel (MLX_VERSION, default
0.29.3) into a venv under the backend dir. Building MLX from source needs
`xcrun metal`, i.e. a full Xcode the macOS runners do not have, while the wheel
ships include/, lib/libmlx.dylib and the compiled metallib ready to link. The
install is a stamp FILE rather than a phony target, because a phony
prerequisite is always newer than libvllm and would re-link it every
invocation. VLLM_CPP_MLX=off restores the previous Metal build.
Packaging vendors libmlx.dylib, mlx.metallib and MLX's MIT license into
package/lib/. Three things this had to get right, each verified on the M4
before it was written rather than after:
1. libvllm.dylib links @rpath/libmlx.dylib and its build-time LC_RPATH points
inside the build venv, a path no user has. Every build rpath is deleted
and replaced with @loader_path/lib.
2. MLX loads its metallib from beside its OWN dylib, so both files must land
in the same directory or every Metal op fails with "Failed to load the
default metallib".
3. install_name_tool invalidates the code signature and macOS refuses to load
an arm64 image with a stale one, so the patched library is re-signed
ad-hoc.
Verified end to end on the M4 by building through this Makefile and running the
packaged artifact: `DYLD_PRINT_LIBRARIES` resolves libmlx from package/lib/,
`codesign -v` passes, no build-venv path survives in the load commands, and a
real generation runs with the provider selected (op=65 selected=mlx) and zero
metallib failures. A missing rpath now fails the build instead of the user's
first inference.
Cost: the darwin vllm-cpp image grows by about 124 MB.
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]
106 lines
5.4 KiB
Bash
106 lines
5.4 KiB
Bash
#!/bin/bash
|
|
|
|
# Script to copy the appropriate libraries based on architecture
|
|
# This script is used in the final stage of the Dockerfile
|
|
|
|
set -e
|
|
|
|
CURDIR=$(dirname "$(realpath $0)")
|
|
REPO_ROOT="${CURDIR}/../../.."
|
|
|
|
# Create lib directory
|
|
mkdir -p $CURDIR/package/lib
|
|
|
|
cp -avf $CURDIR/vllm-cpp $CURDIR/package/
|
|
cp -fLv $CURDIR/libvllm.so $CURDIR/package/ 2>/dev/null || true
|
|
cp -fLv $CURDIR/libvllm.dylib $CURDIR/package/ 2>/dev/null || true
|
|
cp -fv $CURDIR/run.sh $CURDIR/package/
|
|
|
|
# Detect architecture and copy appropriate libraries
|
|
if [ -f "/lib64/ld-linux-x86-64.so.2" ]; then
|
|
# x86_64 architecture
|
|
echo "Detected x86_64 architecture, copying x86_64 libraries..."
|
|
cp -arfLv /lib64/ld-linux-x86-64.so.2 $CURDIR/package/lib/ld.so
|
|
cp -arfLv /lib/x86_64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
|
|
cp -arfLv /lib/x86_64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
|
|
cp -arfLv /lib/x86_64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
|
|
cp -arfLv /lib/x86_64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
|
|
cp -arfLv /lib/x86_64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
|
|
cp -arfLv /lib/x86_64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
|
|
cp -arfLv /lib/x86_64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
|
|
cp -arfLv /lib/x86_64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
|
|
elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then
|
|
# ARM64 architecture
|
|
echo "Detected ARM64 architecture, copying ARM64 libraries..."
|
|
cp -arfLv /lib/ld-linux-aarch64.so.1 $CURDIR/package/lib/ld.so
|
|
cp -arfLv /lib/aarch64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
|
|
cp -arfLv /lib/aarch64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
|
|
cp -arfLv /lib/aarch64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
|
|
cp -arfLv /lib/aarch64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
|
|
cp -arfLv /lib/aarch64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
|
|
cp -arfLv /lib/aarch64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
|
|
cp -arfLv /lib/aarch64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
|
|
cp -arfLv /lib/aarch64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
|
|
elif [ $(uname -s) = "Darwin" ]; then
|
|
echo "Detected Darwin"
|
|
# Vendor the optional MLX GEMM provider, when libvllm was built against it.
|
|
# Three facts drive every line below, each verified on an Apple M4 before it
|
|
# was written:
|
|
# 1. libvllm.dylib carries an LC_LOAD_DYLIB on @rpath/libmlx.dylib, and its
|
|
# build-time LC_RPATH points inside the build venv. That path does not
|
|
# exist on a user's machine, so it must become @loader_path/lib.
|
|
# 2. MLX finds its ~100 MB mlx.metallib beside its OWN dylib, so the two
|
|
# files have to land in the same directory or every Metal op dies with
|
|
# "Failed to load the default metallib".
|
|
# 3. install_name_tool invalidates the code signature, and macOS refuses to
|
|
# load an arm64 image whose signature does not match, so the patched
|
|
# library must be re-signed ad-hoc afterwards.
|
|
if otool -L "$CURDIR/package/libvllm.dylib" 2>/dev/null | grep -q "libmlx.dylib"; then
|
|
MLX_LIB_DIR="${MLX_ROOT}/lib"
|
|
if [ ! -f "$MLX_LIB_DIR/libmlx.dylib" ] || [ ! -f "$MLX_LIB_DIR/mlx.metallib" ]; then
|
|
echo "Error: libvllm.dylib links libmlx.dylib but $MLX_LIB_DIR is missing libmlx.dylib/mlx.metallib" >&2
|
|
exit 1
|
|
fi
|
|
echo "Vendoring the MLX GEMM provider from $MLX_LIB_DIR"
|
|
cp -fLv "$MLX_LIB_DIR/libmlx.dylib" "$CURDIR/package/lib/"
|
|
cp -fLv "$MLX_LIB_DIR/mlx.metallib" "$CURDIR/package/lib/"
|
|
# MLX is MIT and we redistribute its binaries, so its license ships with
|
|
# them. mlx-metal is the wheel carrying the dylib and the metallib.
|
|
MLX_LICENSE=$(ls "${MLX_ROOT}"/../mlx_metal-*.dist-info/licenses/LICENSE 2>/dev/null | head -1)
|
|
if [ -z "$MLX_LICENSE" ]; then
|
|
MLX_LICENSE=$(ls "${MLX_ROOT}"/../mlx-*.dist-info/licenses/LICENSE 2>/dev/null | head -1)
|
|
fi
|
|
if [ -z "$MLX_LICENSE" ]; then
|
|
echo "Error: could not find the MLX LICENSE to redistribute alongside libmlx.dylib" >&2
|
|
exit 1
|
|
fi
|
|
cp -fLv "$MLX_LICENSE" "$CURDIR/package/lib/LICENSE.mlx"
|
|
# Drop every build-tree rpath, then point at the packaged copy.
|
|
otool -l "$CURDIR/package/libvllm.dylib" | awk '/LC_RPATH/{f=1;next} f&&/ path /{print $2;f=0}' | while read -r rp; do
|
|
install_name_tool -delete_rpath "$rp" "$CURDIR/package/libvllm.dylib" 2>/dev/null || true
|
|
done
|
|
install_name_tool -add_rpath "@loader_path/lib" "$CURDIR/package/libvllm.dylib"
|
|
codesign -f -s - "$CURDIR/package/libvllm.dylib"
|
|
# A broken rpath must fail the BUILD, not the user's first inference.
|
|
if ! otool -l "$CURDIR/package/libvllm.dylib" | grep -q "@loader_path/lib"; then
|
|
echo "Error: libvllm.dylib did not get the @loader_path/lib rpath" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
else
|
|
echo "Error: Could not detect architecture"
|
|
exit 1
|
|
fi
|
|
|
|
# Package GPU libraries based on BUILD_TYPE
|
|
GPU_LIB_SCRIPT="${REPO_ROOT}/scripts/build/package-gpu-libs.sh"
|
|
if [ -f "$GPU_LIB_SCRIPT" ]; then
|
|
echo "Packaging GPU libraries for BUILD_TYPE=${BUILD_TYPE:-cpu}..."
|
|
source "$GPU_LIB_SCRIPT" "$CURDIR/package/lib"
|
|
package_gpu_libs
|
|
fi
|
|
|
|
echo "Packaging completed successfully"
|
|
ls -liah $CURDIR/package/
|
|
ls -liah $CURDIR/package/lib/
|