From 6a1ad2ac14b1116d877625ff1a2c6d627ab00584 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 26 Jul 2026 23:18:46 +0000 Subject: [PATCH 01/12] feat(vllm-cpp): enable and vendor the MLX GEMM provider on darwin/metal 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 Assisted-by: Claude Code:claude-opus-5 [ClaudeCode] --- backend/go/vllm-cpp/Makefile | 61 +++++++++++++++++++++++++++++++--- backend/go/vllm-cpp/README.md | 33 ++++++++++++++++++ backend/go/vllm-cpp/package.sh | 44 ++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 0f009340cc85..62d99396c2b4 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -13,6 +13,17 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp VLLM_CPP_VERSION?=9e1c9025ae61167a3335454d7cc0de6093c21845 +# MLX GEMM provider (darwin/metal only; see the metal branch below for why). +# Consumed as the prebuilt pip wheel: 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 mlx.metallib ready to link. +VLLM_CPP_MLX?=on +MLX_VERSION?=0.29.3 +MLX_VENV?=$(abspath ./mlx-venv) +# Resolved lazily (recursive `=`, not `:=`): the glob only matches once the venv +# target has run, and the interpreter version in the path varies per runner. +MLX_ROOT=$(shell echo $(MLX_VENV)/lib/python*/site-packages/mlx) + # The backend consumes only the stable C ABI (libvllm + include/vllm.h), so the # server, examples and tests of the engine are never built here. CMAKE_ARGS+=-DVLLM_CPP_SERVER=OFF -DVLLM_CPP_BUILD_TESTS=OFF -DVLLM_CPP_BUILD_EXAMPLES=OFF @@ -49,6 +60,23 @@ else ifeq ($(BUILD_TYPE),vulkan) CMAKE_ARGS+=-DVLLM_CPP_VULKAN=ON -DVLLM_CPP_CUDA=OFF else ifeq ($(BUILD_TYPE),metal) CMAKE_ARGS+=-DVLLM_CPP_METAL=ON + # The optional MLX GEMM provider. vllm.cpp keeps it OFF by default because it + # is a ~19 MB libmlx.dylib plus a ~105 MB mlx.metallib, and upstream's + # position is that it must earn that cost by measurement. It does, on the + # only hardware this build targets: measured on an Apple M4 against the + # native MSL GEMM in the SAME binary (arms toggled by + # VT_OP_PROVIDER_DISABLE=mlx), Qwen3-1.7B-bf16 p=512 g=128, it is 1.5x to + # 2.2x aggregate throughput and 2x to 3x faster TTFT, at equal peak memory + # and bit-identical output on every parity shape. See vllm.cpp + # docs/BENCHMARKS.md "MLX GEMM provider A/B on Apple M4". + # + # MLX delegates the dense GEMM ONLY: kPagedAttention stays vllm.cpp's own + # kernel, because MLX has no paged-KV primitive at all. + # + # Set VLLM_CPP_MLX=off for a Metal build without it (smaller image, slower). + ifeq ($(VLLM_CPP_MLX),on) + MLX_ENABLED=1 + endif else CMAKE_ARGS+=-DVLLM_CPP_CUDA=OFF endif @@ -68,10 +96,35 @@ sources/vllm.cpp: git fetch --depth 1 origin $(VLLM_CPP_VERSION) && \ git checkout FETCH_HEAD -$(LIB): sources/vllm.cpp +ifeq ($(MLX_ENABLED),1) +# A stamp FILE, not a phony target: a phony prerequisite is always "newer" than +# $(LIB) and would re-link libvllm on every invocation. Keyed on the version so +# a MLX_VERSION bump reinstalls instead of silently reusing the old wheel. +MLX_STAMP=$(MLX_VENV)/.mlx-$(MLX_VERSION).stamp +MLX_CMAKE_ARGS=-DVLLM_CPP_MLX=ON -DMLX_ROOT=$(MLX_ROOT) + +$(MLX_STAMP): + @if [ ! -x "$(MLX_VENV)/bin/pip" ]; then \ + python3 -m venv "$(MLX_VENV)" || { echo "vllm-cpp: python3 with venv is required to build the MLX provider; pass VLLM_CPP_MLX=off to build Metal without it" >&2; exit 1; }; \ + fi + "$(MLX_VENV)"/bin/pip install --quiet --disable-pip-version-check "mlx==$(MLX_VERSION)" + @# Resolved in the SHELL, not by $(MLX_ROOT): make expands a whole recipe + @# before running its first line, so the glob would still be unmatched here. + @# Every later use (the cmake args, package.sh) expands after this target has + @# completed, where $(MLX_ROOT) does resolve. + @root=$$(echo "$(MLX_VENV)"/lib/python*/site-packages/mlx); \ + test -f "$$root/lib/libmlx.dylib" -a -f "$$root/include/mlx/array.h" || \ + { echo "vllm-cpp: mlx==$(MLX_VERSION) did not provide lib/libmlx.dylib + include/mlx/array.h under $$root" >&2; exit 1; } + touch $@ +else +MLX_STAMP= +MLX_CMAKE_ARGS= +endif + +$(LIB): sources/vllm.cpp $(MLX_STAMP) mkdir -p build && \ cd build && \ - cmake ../sources/vllm.cpp $(CMAKE_ARGS) && \ + cmake ../sources/vllm.cpp $(CMAKE_ARGS) $(MLX_CMAKE_ARGS) && \ cmake --build . --config Release -j$(JOBS) --target vllm_shared cp -fL build/$(LIB) ./$(LIB) @@ -79,12 +132,12 @@ vllm-cpp: main.go govllmcpp.go backend.go options.go $(LIB) CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o vllm-cpp ./ package: vllm-cpp - bash package.sh + MLX_ROOT="$(MLX_ROOT)" bash package.sh build: package clean: purge - rm -rf libvllm.so libvllm.dylib package sources/vllm.cpp vllm-cpp + rm -rf libvllm.so libvllm.dylib package sources/vllm.cpp vllm-cpp "$(MLX_VENV)" purge: rm -rf build diff --git a/backend/go/vllm-cpp/README.md b/backend/go/vllm-cpp/README.md index 4d2c437f10b9..cd6eb94a08ca 100644 --- a/backend/go/vllm-cpp/README.md +++ b/backend/go/vllm-cpp/README.md @@ -41,5 +41,38 @@ options: - max_num_seqs:16 ``` +## Apple Silicon: the MLX GEMM provider + +`BUILD_TYPE=metal` builds the Metal backend with vllm.cpp's optional MLX +provider for the dense GEMM (`VLLM_CPP_MLX=on`, the default here). Upstream keeps +it off because it costs a ~19 MB `libmlx.dylib` plus a ~105 MB `mlx.metallib`; +this backend accepts that because the provider was measured to pay for it on an +Apple M4, against the native MSL GEMM in the SAME binary (arms toggled with +`VT_OP_PROVIDER_DISABLE=mlx`), Qwen3-1.7B-bf16 at p=512 g=128: + +| Concurrency | MLX agg tok/s | native agg tok/s | speedup | +|--:|--:|--:|--:| +| 1 | 5.79 | 3.08 | 1.88x | +| 8 | 25.70 | 13.69 | 1.88x | +| 16 | 38.65 | 17.69 | 2.19x | + +TTFT improves 2x to 3x, peak memory is unchanged, and the GEMM output is +bit-identical to the native kernel on every parity shape. MLX serves the dense +GEMM only: paged attention stays vllm.cpp's own kernel, because MLX has no +paged-KV primitive. Full disposition in vllm.cpp `docs/BENCHMARKS.md`, +"MLX GEMM provider A/B on Apple M4". + +Build knobs: + +- `VLLM_CPP_MLX=off` builds Metal without the provider: ~124 MB smaller, slower. +- `MLX_VERSION` pins the wheel (default `0.29.3`). MLX is consumed as the + prebuilt pip wheel because building it from source needs `xcrun metal`, i.e. a + full Xcode the macOS runners do not have. + +Packaging vendors `libmlx.dylib`, `mlx.metallib` and MLX's MIT license into +`package/lib/`, and rewrites `libvllm.dylib`'s rpath to `@loader_path/lib` +(re-signing it, since `install_name_tool` invalidates the signature). The +metallib must stay beside `libmlx.dylib`: MLX looks for it there. + Testing: `make test` runs the unit specs; export `VLLM_CPP_MODEL=` (and optionally `VLLM_CPP_LIBRARY=`) to enable the e2e specs. diff --git a/backend/go/vllm-cpp/package.sh b/backend/go/vllm-cpp/package.sh index 78dc4917888b..30a21d219e43 100644 --- a/backend/go/vllm-cpp/package.sh +++ b/backend/go/vllm-cpp/package.sh @@ -43,6 +43,50 @@ elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then 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 From 537dfd0b2aa428eed3df0567a55d772bdff8233a Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Mon, 27 Jul 2026 21:45:33 +0000 Subject: [PATCH 02/12] fix(vllm-cpp): default the MLX GEMM provider OFF on darwin This branch opened with VLLM_CPP_MLX=on, justified by an A/B that measured the MLX provider at 1.88x to 2.19x against the native MSL GEMM. That measurement was correct when taken and is now stale: vllm.cpp's own Metal kernels have improved several-fold since, through mma prefill attention, a vectorised decode V accumulation, vectorised attention staging, a fused qk-norm-RoPE preamble and a simdgroup-per-row softmax. The native path MLX was compared against no longer exists. Re-measured on the same Apple M4, in the same binary, with the arms toggled by VT_OP_PROVIDER_DISABLE=mlx, on Qwen3-1.7B-bf16 warm at p=512 g=128: MLX provider ON prefill TTFT 1370 ms warm throughput 11.98 tok/s MLX provider OFF prefill TTFT 1400 ms warm throughput 22.06 tok/s Shipping the previous default would have halved Apple Silicon throughput. MLX's steel GEMM is still about 20% faster than ours in isolation, but the provider pays a per-op mx::eval synchronisation plus an output memcpy, because it cannot write into our buffer. Across prefill's roughly 112 GEMMs that overhead leaves a 2% gain; on decode, where the same synchronisation is paid once per matmul per token, it costs 46%. The option is kept for prefill-dominated workloads, where the margin is small but real. The README section is rewritten rather than patched: it previously presented the stale table as the reason for the default, so leaving it in place would have made the new default look arbitrary. Assisted-by: Claude Code:claude-opus-5 [ClaudeCode] Signed-off-by: Ettore Di Giacinto --- backend/go/vllm-cpp/Makefile | 7 ++++- backend/go/vllm-cpp/README.md | 51 ++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 62d99396c2b4..8795f621db05 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -17,7 +17,12 @@ VLLM_CPP_VERSION?=9e1c9025ae61167a3335454d7cc0de6093c21845 # Consumed as the prebuilt pip wheel: 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 mlx.metallib ready to link. -VLLM_CPP_MLX?=on +# +# DEFAULT OFF. It was on when this branch opened, on the strength of an A/B that +# had MLX at 1.88x. That measurement is stale: vllm.cpp's own Metal kernels have +# since improved several-fold, and re-measured on the same M4 the provider is now +# 46% SLOWER end to end (11.98 vs 22.06 warm tok/s). See the README. +VLLM_CPP_MLX?=off MLX_VERSION?=0.29.3 MLX_VENV?=$(abspath ./mlx-venv) # Resolved lazily (recursive `=`, not `:=`): the glob only matches once the venv diff --git a/backend/go/vllm-cpp/README.md b/backend/go/vllm-cpp/README.md index cd6eb94a08ca..1589b1ff8f81 100644 --- a/backend/go/vllm-cpp/README.md +++ b/backend/go/vllm-cpp/README.md @@ -41,30 +41,39 @@ options: - max_num_seqs:16 ``` -## Apple Silicon: the MLX GEMM provider - -`BUILD_TYPE=metal` builds the Metal backend with vllm.cpp's optional MLX -provider for the dense GEMM (`VLLM_CPP_MLX=on`, the default here). Upstream keeps -it off because it costs a ~19 MB `libmlx.dylib` plus a ~105 MB `mlx.metallib`; -this backend accepts that because the provider was measured to pay for it on an -Apple M4, against the native MSL GEMM in the SAME binary (arms toggled with -`VT_OP_PROVIDER_DISABLE=mlx`), Qwen3-1.7B-bf16 at p=512 g=128: - -| Concurrency | MLX agg tok/s | native agg tok/s | speedup | -|--:|--:|--:|--:| -| 1 | 5.79 | 3.08 | 1.88x | -| 8 | 25.70 | 13.69 | 1.88x | -| 16 | 38.65 | 17.69 | 2.19x | - -TTFT improves 2x to 3x, peak memory is unchanged, and the GEMM output is -bit-identical to the native kernel on every parity shape. MLX serves the dense -GEMM only: paged attention stays vllm.cpp's own kernel, because MLX has no -paged-KV primitive. Full disposition in vllm.cpp `docs/BENCHMARKS.md`, -"MLX GEMM provider A/B on Apple M4". +## Apple Silicon: the MLX GEMM provider (OFF by default) + +`BUILD_TYPE=metal` can build vllm.cpp's optional MLX provider for the dense GEMM +(`VLLM_CPP_MLX=on`). **It is OFF by default, because it is currently slower.** + +This branch originally shipped it ON, on the strength of an A/B that had MLX at +1.88-2.19x against the native MSL GEMM. That measurement was honest when taken +and is now stale: vllm.cpp's Metal kernels have since improved several-fold +(mma prefill attention, vectorised decode V accumulation, a fused qk-norm-RoPE +preamble and more), so the native path no longer resembles the one MLX was +compared against. + +Re-measured on the same Apple M4, same binary, arms toggled with +`VT_OP_PROVIDER_DISABLE=mlx`, Qwen3-1.7B-bf16 warm at p=512 g=128: + +| | prefill TTFT | warm throughput | +|---|--:|--:| +| MLX provider ON | 1370 ms | **11.98 tok/s** | +| MLX provider OFF | 1400 ms | **22.06 tok/s** | + +MLX's steel GEMM is still ~20% faster than ours in isolation, but the provider +pays a per-op `mx::eval` synchronisation plus an output `memcpy` (it cannot write +into our buffer). On prefill's ~112 GEMMs that overhead leaves +2%; on decode, +where the same sync is paid once per matmul per token, it costs 46%. + +Turning it on is therefore only sensible for prefill-dominated workloads, and +even then the margin is small. Full disposition in vllm.cpp `docs/BENCHMARKS.md`, +"The MLX provider verdict". Build knobs: -- `VLLM_CPP_MLX=off` builds Metal without the provider: ~124 MB smaller, slower. +- `VLLM_CPP_MLX=on` builds the provider in: ~19 MB `libmlx.dylib` plus a ~105 MB + `mlx.metallib`, and currently slower end to end. Off is the default. - `MLX_VERSION` pins the wheel (default `0.29.3`). MLX is consumed as the prebuilt pip wheel because building it from source needs `xcrun metal`, i.e. a full Xcode the macOS runners do not have. From 52dfa682edbc2130a285ff2aa21bae36a75a587b Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 28 Jul 2026 08:36:30 +0000 Subject: [PATCH 03/12] feat(vllm-cpp): bump vllm.cpp and default MLX ON, gated to prefill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps VLLM_CPP_VERSION from 9e1c9025 to eec09bed and turns VLLM_CPP_MLX back on. These two must move together, which is why they are one commit. Upstream now shape-gates the MLX provider to prefill: it declines m < 2, which is exactly the decode GEMV. MLX's steel GEMM wins prefill, 524.5 ms of TTFT against 602 for the native path, but loses decode badly because the provider pays an mx::eval synchronisation and an output memcpy on every call while decode makes about 112 calls per token. Ungated it does both; gated it does only the good half. Measured on an Apple M4 with Qwen3-1.7B-bf16 warm at p=512 g=128: MLX gated to prefill (pin >= 89c46aeb) TTFT 524.5 ms 24.40 tok/s, 99.1% of MLX-LM MLX ungated (older pins) TTFT 537 ms 12.7 tok/s MLX off TTFT 602 ms 23.9 tok/s This branch briefly defaulted the provider off, which was the correct call for an ungated provider at the old pin. The gate is what makes on correct again, so the pin and the flag are coupled: rolling VLLM_CPP_VERSION back before 89c46aeb while leaving MLX on would select the middle row and roughly halve throughput. Both the Makefile comment and the README state that dependency explicitly. The bump also brings six Metal kernels landed upstream since the old pin — mma prefill attention, a vectorised decode V accumulation, vectorised attention staging, a fused qk-norm-RoPE preamble, a simdgroup-per-row softmax and a simdgroup-per-head preamble — which take the non-MLX Metal path from 89.4% to 96.4% of MLX-LM on their own. One caveat, recorded in the README: MLX's GEMM is not bit-identical to the native kernel, so an MLX build produces a different greedy sequence than a non-MLX build. That is a property of the provider rather than of the gate and predates this packaging. Assisted-by: Claude Code:claude-opus-5 [ClaudeCode] Signed-off-by: Ettore Di Giacinto --- backend/go/vllm-cpp/Makefile | 19 ++++++++++----- backend/go/vllm-cpp/README.md | 46 +++++++++++++++++------------------ 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 8795f621db05..e9b0fc06c331 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -11,18 +11,25 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e # vllm.cpp version VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp -VLLM_CPP_VERSION?=9e1c9025ae61167a3335454d7cc0de6093c21845 +VLLM_CPP_VERSION?=eec09bed5a03457837b499781c23d8e44f106813 # MLX GEMM provider (darwin/metal only; see the metal branch below for why). # Consumed as the prebuilt pip wheel: 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 mlx.metallib ready to link. # -# DEFAULT OFF. It was on when this branch opened, on the strength of an A/B that -# had MLX at 1.88x. That measurement is stale: vllm.cpp's own Metal kernels have -# since improved several-fold, and re-measured on the same M4 the provider is now -# 46% SLOWER end to end (11.98 vs 22.06 warm tok/s). See the README. -VLLM_CPP_MLX?=off +# DEFAULT ON, but ONLY because VLLM_CPP_VERSION above is pinned at or past +# vllm.cpp 89c46aeb, which SHAPE-GATES the provider to prefill. The ordering is +# load-bearing, not incidental: +# +# pin >= 89c46aeb, MLX on -> 99.1% of MLX-LM (gated: prefill only) +# pin < 89c46aeb, MLX on -> ~51% (ungated: it also takes decode) +# +# MLX's steel GEMM wins prefill (537 ms TTFT against 602) and loses decode badly, +# because the provider pays an mx::eval sync plus an output memcpy per call and +# decode makes ~112 calls per TOKEN. Ungated it does both; gated it does only the +# good half. So if this pin is ever moved BACKWARDS, this default must go with it. +VLLM_CPP_MLX?=on MLX_VERSION?=0.29.3 MLX_VENV?=$(abspath ./mlx-venv) # Resolved lazily (recursive `=`, not `:=`): the glob only matches once the venv diff --git a/backend/go/vllm-cpp/README.md b/backend/go/vllm-cpp/README.md index 1589b1ff8f81..3755340d404c 100644 --- a/backend/go/vllm-cpp/README.md +++ b/backend/go/vllm-cpp/README.md @@ -41,39 +41,37 @@ options: - max_num_seqs:16 ``` -## Apple Silicon: the MLX GEMM provider (OFF by default) +## Apple Silicon: the MLX GEMM provider (ON by default, gated to prefill) -`BUILD_TYPE=metal` can build vllm.cpp's optional MLX provider for the dense GEMM -(`VLLM_CPP_MLX=on`). **It is OFF by default, because it is currently slower.** +`BUILD_TYPE=metal` builds vllm.cpp's MLX provider for the dense GEMM +(`VLLM_CPP_MLX=on`, the default here). It is on because upstream now SHAPE-GATES +it to prefill; it was briefly off in this branch's history, and that was correct +at the time for an ungated provider. -This branch originally shipped it ON, on the strength of an A/B that had MLX at -1.88-2.19x against the native MSL GEMM. That measurement was honest when taken -and is now stale: vllm.cpp's Metal kernels have since improved several-fold -(mma prefill attention, vectorised decode V accumulation, a fused qk-norm-RoPE -preamble and more), so the native path no longer resembles the one MLX was -compared against. +The gate matters more than the flag. MLX's steel GEMM wins prefill but loses +decode, because the provider pays an `mx::eval` synchronisation plus an output +memcpy on every call and decode makes ~112 calls *per token*. Measured on an +Apple M4, Qwen3-1.7B-bf16 warm at p=512 g=128: -Re-measured on the same Apple M4, same binary, arms toggled with -`VT_OP_PROVIDER_DISABLE=mlx`, Qwen3-1.7B-bf16 warm at p=512 g=128: - -| | prefill TTFT | warm throughput | +| configuration | prefill TTFT | warm throughput | |---|--:|--:| -| MLX provider ON | 1370 ms | **11.98 tok/s** | -| MLX provider OFF | 1400 ms | **22.06 tok/s** | +| MLX **gated to prefill** (pin >= 89c46aeb) | **524.5 ms** | **24.40 tok/s — 99.1% of MLX-LM** | +| MLX ungated (older pins) | 537 ms | 12.7 tok/s | +| MLX off | 602 ms | 23.9 tok/s | -MLX's steel GEMM is still ~20% faster than ours in isolation, but the provider -pays a per-op `mx::eval` synchronisation plus an output `memcpy` (it cannot write -into our buffer). On prefill's ~112 GEMMs that overhead leaves +2%; on decode, -where the same sync is paid once per matmul per token, it costs 46%. +**`VLLM_CPP_VERSION` and this flag are coupled.** Moving the pin back before +`89c46aeb` while leaving `VLLM_CPP_MLX=on` would take the middle row — roughly +half throughput. If you roll the pin back, roll the default back with it. -Turning it on is therefore only sensible for prefill-dominated workloads, and -even then the margin is small. Full disposition in vllm.cpp `docs/BENCHMARKS.md`, -"The MLX provider verdict". +One caveat: MLX's GEMM is not bit-identical to the native kernel, so an MLX build +produces a different greedy sequence than a non-MLX one. That is a property of the +provider, not of the gate, and it predates this packaging. Full disposition in +vllm.cpp `docs/BENCHMARKS.md`. Build knobs: -- `VLLM_CPP_MLX=on` builds the provider in: ~19 MB `libmlx.dylib` plus a ~105 MB - `mlx.metallib`, and currently slower end to end. Off is the default. +- `VLLM_CPP_MLX=off` builds Metal without the provider: ~124 MB smaller, and + 96.4% of MLX-LM instead of 99.1%. - `MLX_VERSION` pins the wheel (default `0.29.3`). MLX is consumed as the prebuilt pip wheel because building it from source needs `xcrun metal`, i.e. a full Xcode the macOS runners do not have. From a2da05767862cdc4d2886c9ba8948e382e3f0099 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 28 Jul 2026 08:41:18 +0000 Subject: [PATCH 04/12] docs(vllm-cpp): correct the MLX-gated figure to 97.6%, from 99.1% The previous commit quoted 99.1% of MLX-LM for the prefill-gated MLX build. That figure divided by a two-run MLX-LM baseline, 27.135 and 27.744 generation tok/s averaged to 27.44. Re-measured interleaved with ours over four ABBA blocks, MLX-LM's decode is 27.848 with a 0.34% spread across six runs, so the 27.135 was an outlier and averaging it in overstated us by roughly 1.5 points. Corrected: the gated configuration is 24.37 tok/s, or 97.6% of MLX-LM, and the MLX-off build is 23.9 tok/s or 95.9%. Prefill TTFT is unchanged at 524.5 ms against MLX-LM's 532.6, so we remain about 1.5% faster there. Nothing else changes. MLX still wins prefill and loses decode, the shape gate is still the right disposition, and the pin and the flag are still coupled. The gate is worth about 1.7 points over the MLX-off build rather than 2.7. Assisted-by: Claude Code:claude-opus-5 [ClaudeCode] Signed-off-by: Ettore Di Giacinto --- backend/go/vllm-cpp/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/go/vllm-cpp/README.md b/backend/go/vllm-cpp/README.md index 3755340d404c..65636e1a8e45 100644 --- a/backend/go/vllm-cpp/README.md +++ b/backend/go/vllm-cpp/README.md @@ -55,9 +55,14 @@ Apple M4, Qwen3-1.7B-bf16 warm at p=512 g=128: | configuration | prefill TTFT | warm throughput | |---|--:|--:| -| MLX **gated to prefill** (pin >= 89c46aeb) | **524.5 ms** | **24.40 tok/s — 99.1% of MLX-LM** | +| MLX **gated to prefill** (pin >= 89c46aeb) | **524.5 ms** | **24.37 tok/s, 97.6% of MLX-LM** | | MLX ungated (older pins) | 537 ms | 12.7 tok/s | -| MLX off | 602 ms | 23.9 tok/s | +| MLX off | 602 ms | 23.9 tok/s, 95.9% | + +Ratios are against an MLX-LM baseline measured INTERLEAVED with ours over four +ABBA blocks (its spread 0.34%, ours 0.12%). An earlier revision of this file +claimed 99.1%; that used a two-run MLX-LM baseline containing an outlier and +overstated us by about 1.5 points. **`VLLM_CPP_VERSION` and this flag are coupled.** Moving the pin back before `89c46aeb` while leaving `VLLM_CPP_MLX=on` would take the middle row — roughly From 766a5f0480265ffb9b8c7c2ccd22652f9e3c9fc2 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Thu, 30 Jul 2026 05:10:27 +0000 Subject: [PATCH 05/12] fix(vllm-cpp): pin MLX gate from mainline The previous pin was a merge commit from the experimental C ABI v9 branch. Pin the same MLX prefill gate on upstream main so the backend build does not pull unrelated ABI v9 work into every platform variant. Assisted-by: Codex:gpt-5 [systematic-debugging] --- backend/go/vllm-cpp/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index e9b0fc06c331..0cb1306726e7 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e # vllm.cpp version VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp -VLLM_CPP_VERSION?=eec09bed5a03457837b499781c23d8e44f106813 +VLLM_CPP_VERSION?=59343930f37ba08abfccb8de544bc51fc3828b16 # MLX GEMM provider (darwin/metal only; see the metal branch below for why). # Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun From 2ddb0e03ee16033e5b473020720ce8234f949e1d Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:03:56 +0000 Subject: [PATCH 06/12] fix(vllm-cpp): restore backend build portability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the current master pin when enabling MLX so every backend variant builds against the known-good vllm.cpp revision. Suppress Apple clang’s GNU constant-folding diagnostic for Objective-C++ Metal compilation only, since upstream treats warnings as errors. Assisted-by: Codex:gpt-5 [systematic-debugging] --- backend/go/vllm-cpp/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 0cb1306726e7..e5f2757149ef 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e # vllm.cpp version VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp -VLLM_CPP_VERSION?=59343930f37ba08abfccb8de544bc51fc3828b16 +VLLM_CPP_VERSION?=9e1c9025ae61167a3335454d7cc0de6093c21845 # MLX GEMM provider (darwin/metal only; see the metal branch below for why). # Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun @@ -71,7 +71,7 @@ ifeq ($(BUILD_TYPE),cublas) else ifeq ($(BUILD_TYPE),vulkan) CMAKE_ARGS+=-DVLLM_CPP_VULKAN=ON -DVLLM_CPP_CUDA=OFF else ifeq ($(BUILD_TYPE),metal) - CMAKE_ARGS+=-DVLLM_CPP_METAL=ON + CMAKE_ARGS+=-DVLLM_CPP_METAL=ON -DCMAKE_OBJCXX_FLAGS=-Wno-gnu-folding-constant # The optional MLX GEMM provider. vllm.cpp keeps it OFF by default because it # is a ~19 MB libmlx.dylib plus a ~105 MB mlx.metallib, and upstream's # position is that it must earn that cost by measurement. It does, on the From c963f8d301acfbef6c5fcbbd77fda2dec3135e77 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sun, 2 Aug 2026 02:04:21 +0000 Subject: [PATCH 07/12] fix(vllm-cpp): demote MLX header VLA warning MLX 0.29.3 headers trigger Apple clang's gnu-folding-constant diagnostic in the Objective-C++ provider. Keep the diagnostic visible while exempting only it from vllm.cpp's global warnings-as-errors policy. Assisted-by: Codex:gpt-5 [systematic-debugging] --- backend/go/vllm-cpp/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index e5f2757149ef..8fa341e428be 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -71,7 +71,7 @@ ifeq ($(BUILD_TYPE),cublas) else ifeq ($(BUILD_TYPE),vulkan) CMAKE_ARGS+=-DVLLM_CPP_VULKAN=ON -DVLLM_CPP_CUDA=OFF else ifeq ($(BUILD_TYPE),metal) - CMAKE_ARGS+=-DVLLM_CPP_METAL=ON -DCMAKE_OBJCXX_FLAGS=-Wno-gnu-folding-constant + CMAKE_ARGS+=-DVLLM_CPP_METAL=ON -DCMAKE_OBJCXX_FLAGS=-Wno-error=gnu-folding-constant # The optional MLX GEMM provider. vllm.cpp keeps it OFF by default because it # is a ~19 MB libmlx.dylib plus a ~105 MB mlx.metallib, and upstream's # position is that it must earn that cost by measurement. It does, on the From 8f0aa3c1002172a4be846063c530801996a4c18f Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sun, 2 Aug 2026 09:03:14 +0000 Subject: [PATCH 08/12] fix(vllm-cpp): suppress MLX header VLA warning Target-level Objective-C++ -Werror is appended after the directory flags, so a no-error demotion is re-promoted. Disable this single warning for the MLX header while keeping every other warning fatal. Assisted-by: Codex:gpt-5 [systematic-debugging] --- backend/go/vllm-cpp/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 8fa341e428be..e5f2757149ef 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -71,7 +71,7 @@ ifeq ($(BUILD_TYPE),cublas) else ifeq ($(BUILD_TYPE),vulkan) CMAKE_ARGS+=-DVLLM_CPP_VULKAN=ON -DVLLM_CPP_CUDA=OFF else ifeq ($(BUILD_TYPE),metal) - CMAKE_ARGS+=-DVLLM_CPP_METAL=ON -DCMAKE_OBJCXX_FLAGS=-Wno-error=gnu-folding-constant + CMAKE_ARGS+=-DVLLM_CPP_METAL=ON -DCMAKE_OBJCXX_FLAGS=-Wno-gnu-folding-constant # The optional MLX GEMM provider. vllm.cpp keeps it OFF by default because it # is a ~19 MB libmlx.dylib plus a ~105 MB mlx.metallib, and upstream's # position is that it must earn that cost by measurement. It does, on the From 9d29e7251ed0bed85fafd5a34de19a7d3ad1370f Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:06:55 +0000 Subject: [PATCH 09/12] fix(vllm-cpp): pin source-scoped MLX warning fix Move the AppleClang warning exception into vllm.cpp where its target warning policy is defined, and pin LocalAI to that source-scoped fix. Assisted-by: Codex:gpt-5 --- backend/go/vllm-cpp/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index e5f2757149ef..06a358c59d32 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e # vllm.cpp version VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp -VLLM_CPP_VERSION?=9e1c9025ae61167a3335454d7cc0de6093c21845 +VLLM_CPP_VERSION?=f59cf5429046194bf8b7844c5761a34d24f21c22 # MLX GEMM provider (darwin/metal only; see the metal branch below for why). # Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun @@ -71,7 +71,7 @@ ifeq ($(BUILD_TYPE),cublas) else ifeq ($(BUILD_TYPE),vulkan) CMAKE_ARGS+=-DVLLM_CPP_VULKAN=ON -DVLLM_CPP_CUDA=OFF else ifeq ($(BUILD_TYPE),metal) - CMAKE_ARGS+=-DVLLM_CPP_METAL=ON -DCMAKE_OBJCXX_FLAGS=-Wno-gnu-folding-constant + CMAKE_ARGS+=-DVLLM_CPP_METAL=ON # The optional MLX GEMM provider. vllm.cpp keeps it OFF by default because it # is a ~19 MB libmlx.dylib plus a ~105 MB mlx.metallib, and upstream's # position is that it must earn that cost by measurement. It does, on the From 03d8002675215c0256c3129179847b7b81c26ebc Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:04:26 +0000 Subject: [PATCH 10/12] fix(vllm-cpp): pin effective MLX warning suppression The source-scoped no-error flag was overridden by the target warning policy. Pin the companion vllm.cpp change that disables only the MLX header diagnostic for its Objective-C++ translation unit. Assisted-by: Codex:gpt-5 --- backend/go/vllm-cpp/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 06a358c59d32..024f69d73d67 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e # vllm.cpp version VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp -VLLM_CPP_VERSION?=f59cf5429046194bf8b7844c5761a34d24f21c22 +VLLM_CPP_VERSION?=865593b1c861c1240d4289a01909ac1c0c312b66 # MLX GEMM provider (darwin/metal only; see the metal branch below for why). # Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun From b82b3e4eeba8474e0e69f88a276850e6b73e5742 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sun, 2 Aug 2026 15:07:31 +0000 Subject: [PATCH 11/12] fix(vllm-cpp): pin diagnostic pragma fix Pin the companion vllm.cpp correction that scopes the AppleClang folding warning suppression inside the MLX translation unit, after command-line warning policy. Assisted-by: Codex:gpt-5 [systematic-debugging] --- backend/go/vllm-cpp/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 024f69d73d67..455ee73ee721 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e # vllm.cpp version VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp -VLLM_CPP_VERSION?=865593b1c861c1240d4289a01909ac1c0c312b66 +VLLM_CPP_VERSION?=ac8cc63278c86d994c51d0f09312fbced1573b93 # MLX GEMM provider (darwin/metal only; see the metal branch below for why). # Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun From 5f9f886b54d5f098afbc89f994a1c480e1057896 Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Mon, 3 Aug 2026 04:03:44 +0000 Subject: [PATCH 12/12] fix(vllm-cpp): pin remaining Darwin build fixes Advance the MLX-enabled backend to the vllm.cpp revision already validated by the dependency update branch. This includes the feature guards and AppleClang pragma boundary needed by the Darwin build. Assisted-by: Codex:gpt-5 [systematic-debugging] --- backend/go/vllm-cpp/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/go/vllm-cpp/Makefile b/backend/go/vllm-cpp/Makefile index 455ee73ee721..7f886bb19e72 100644 --- a/backend/go/vllm-cpp/Makefile +++ b/backend/go/vllm-cpp/Makefile @@ -11,7 +11,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e # vllm.cpp version VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp -VLLM_CPP_VERSION?=ac8cc63278c86d994c51d0f09312fbced1573b93 +VLLM_CPP_VERSION?=6199acab55546df949a8e19509e2bb95ec0df874 # MLX GEMM provider (darwin/metal only; see the metal branch below for why). # Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun