Skip to content

Commit e99b4e3

Browse files
committed
feat(vllm-cpp): bump vllm.cpp and default MLX ON, gated to prefill
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 <mudler@localai.io>
1 parent bd06352 commit e99b4e3

2 files changed

Lines changed: 35 additions & 30 deletions

File tree

backend/go/vllm-cpp/Makefile

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,25 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e
1111

1212
# vllm.cpp version
1313
VLLM_CPP_REPO?=https://github.com/mudler/vllm.cpp
14-
VLLM_CPP_VERSION?=9e1c9025ae61167a3335454d7cc0de6093c21845
14+
VLLM_CPP_VERSION?=eec09bed5a03457837b499781c23d8e44f106813
1515

1616
# MLX GEMM provider (darwin/metal only; see the metal branch below for why).
1717
# Consumed as the prebuilt pip wheel: building MLX from source needs `xcrun
1818
# metal`, i.e. a full Xcode the macOS runners do not have, while the wheel ships
1919
# include/, lib/libmlx.dylib and the compiled mlx.metallib ready to link.
2020
#
21-
# DEFAULT OFF. It was on when this branch opened, on the strength of an A/B that
22-
# had MLX at 1.88x. That measurement is stale: vllm.cpp's own Metal kernels have
23-
# since improved several-fold, and re-measured on the same M4 the provider is now
24-
# 46% SLOWER end to end (11.98 vs 22.06 warm tok/s). See the README.
25-
VLLM_CPP_MLX?=off
21+
# DEFAULT ON, but ONLY because VLLM_CPP_VERSION above is pinned at or past
22+
# vllm.cpp 89c46aeb, which SHAPE-GATES the provider to prefill. The ordering is
23+
# load-bearing, not incidental:
24+
#
25+
# pin >= 89c46aeb, MLX on -> 99.1% of MLX-LM (gated: prefill only)
26+
# pin < 89c46aeb, MLX on -> ~51% (ungated: it also takes decode)
27+
#
28+
# MLX's steel GEMM wins prefill (537 ms TTFT against 602) and loses decode badly,
29+
# because the provider pays an mx::eval sync plus an output memcpy per call and
30+
# decode makes ~112 calls per TOKEN. Ungated it does both; gated it does only the
31+
# good half. So if this pin is ever moved BACKWARDS, this default must go with it.
32+
VLLM_CPP_MLX?=on
2633
MLX_VERSION?=0.29.3
2734
MLX_VENV?=$(abspath ./mlx-venv)
2835
# Resolved lazily (recursive `=`, not `:=`): the glob only matches once the venv

backend/go/vllm-cpp/README.md

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,39 +41,37 @@ options:
4141
- max_num_seqs:16
4242
```
4343
44-
## Apple Silicon: the MLX GEMM provider (OFF by default)
44+
## Apple Silicon: the MLX GEMM provider (ON by default, gated to prefill)
4545
46-
`BUILD_TYPE=metal` can build vllm.cpp's optional MLX provider for the dense GEMM
47-
(`VLLM_CPP_MLX=on`). **It is OFF by default, because it is currently slower.**
46+
`BUILD_TYPE=metal` builds vllm.cpp's MLX provider for the dense GEMM
47+
(`VLLM_CPP_MLX=on`, the default here). It is on because upstream now SHAPE-GATES
48+
it to prefill; it was briefly off in this branch's history, and that was correct
49+
at the time for an ungated provider.
4850

49-
This branch originally shipped it ON, on the strength of an A/B that had MLX at
50-
1.88-2.19x against the native MSL GEMM. That measurement was honest when taken
51-
and is now stale: vllm.cpp's Metal kernels have since improved several-fold
52-
(mma prefill attention, vectorised decode V accumulation, a fused qk-norm-RoPE
53-
preamble and more), so the native path no longer resembles the one MLX was
54-
compared against.
51+
The gate matters more than the flag. MLX's steel GEMM wins prefill but loses
52+
decode, because the provider pays an `mx::eval` synchronisation plus an output
53+
memcpy on every call and decode makes ~112 calls *per token*. Measured on an
54+
Apple M4, Qwen3-1.7B-bf16 warm at p=512 g=128:
5555

56-
Re-measured on the same Apple M4, same binary, arms toggled with
57-
`VT_OP_PROVIDER_DISABLE=mlx`, Qwen3-1.7B-bf16 warm at p=512 g=128:
58-
59-
| | prefill TTFT | warm throughput |
56+
| configuration | prefill TTFT | warm throughput |
6057
|---|--:|--:|
61-
| MLX provider ON | 1370 ms | **11.98 tok/s** |
62-
| MLX provider OFF | 1400 ms | **22.06 tok/s** |
58+
| MLX **gated to prefill** (pin >= 89c46aeb) | **524.5 ms** | **24.40 tok/s — 99.1% of MLX-LM** |
59+
| MLX ungated (older pins) | 537 ms | 12.7 tok/s |
60+
| MLX off | 602 ms | 23.9 tok/s |
6361

64-
MLX's steel GEMM is still ~20% faster than ours in isolation, but the provider
65-
pays a per-op `mx::eval` synchronisation plus an output `memcpy` (it cannot write
66-
into our buffer). On prefill's ~112 GEMMs that overhead leaves +2%; on decode,
67-
where the same sync is paid once per matmul per token, it costs 46%.
62+
**`VLLM_CPP_VERSION` and this flag are coupled.** Moving the pin back before
63+
`89c46aeb` while leaving `VLLM_CPP_MLX=on` would take the middle row — roughly
64+
half throughput. If you roll the pin back, roll the default back with it.
6865

69-
Turning it on is therefore only sensible for prefill-dominated workloads, and
70-
even then the margin is small. Full disposition in vllm.cpp `docs/BENCHMARKS.md`,
71-
"The MLX provider verdict".
66+
One caveat: MLX's GEMM is not bit-identical to the native kernel, so an MLX build
67+
produces a different greedy sequence than a non-MLX one. That is a property of the
68+
provider, not of the gate, and it predates this packaging. Full disposition in
69+
vllm.cpp `docs/BENCHMARKS.md`.
7270

7371
Build knobs:
7472

75-
- `VLLM_CPP_MLX=on` builds the provider in: ~19 MB `libmlx.dylib` plus a ~105 MB
76-
`mlx.metallib`, and currently slower end to end. Off is the default.
73+
- `VLLM_CPP_MLX=off` builds Metal without the provider: ~124 MB smaller, and
74+
96.4% of MLX-LM instead of 99.1%.
7775
- `MLX_VERSION` pins the wheel (default `0.29.3`). MLX is consumed as the
7876
prebuilt pip wheel because building it from source needs `xcrun metal`, i.e. a
7977
full Xcode the macOS runners do not have.

0 commit comments

Comments
 (0)