跳到正文
vLLM Blog· Ranran Haoran Zhang, Lik Xun Yuan, Chao Ju Chen, Eric Curtin, Michael Goin·· 13 天前精选AI 评分71

vLLM 发布 vllm-metal:在 Apple Silicon 上并发服务

Announcing vllm-metal: Concurrent Serving on Apple Silicon

AI 导读

vLLM 官方发布 vllm-metal,把 vLLM 的 V1 调度器、分页 KV cache 和 OpenAI 兼容服务带到 Apple Silicon,执行层由 MLX 与 Metal 承担。

推荐理由

vLLM 官方把调度器、分页 KV cache 和 OpenAI 兼容服务搬到 Apple Silicon,并给出与 llama.cpp、oMLX 的并发实测对比。

正文 · AI 翻译

在 Mac 上进行本地推理很简单,直到多个请求重叠。此时,首 token 时间(TTFT)、内存增长和准入控制就变成了服务问题,而不是模型执行问题。vllm-metal 将 vLLM 的调度器、分页 KV 缓存和 OpenAI 兼容服务器带到 Apple Silicon,由 MLX 和 Metal 负责执行。

我们的首个正式版本 v0.28.0 将 vllm-metal 的版本号与上游 vLLM 对齐。它引入了批量多 token 预测(MTP)、GGUF 和混合模型支持,并在 M5 上实现了更快的预填充。你可以通过 Homebrew 安装 v0.29.0。

vllm-metal 接入上游 vLLM。vLLM 提供 V1 调度器、分页 KV 块管理、分块预填充、采样,以及支持流式传输和工具调用解析的 OpenAI 兼容前端。mlx_lm 提供模型实现;MLX 负责执行它们。

在模型层面,vllm-metal 原样复用 mlx_lm 的权重加载、RMSNorm、线性层、MoE 和 MLP 层。这些层独立处理每个 token,因此它们在打包的 token 轴上运行,无需知道请求边界。注意力机制确实需要这些边界,因此 vllm-metal 用分页 varlen Metal 内核替换了原生注意力。因此,该插件的大部分模型特定代码都集中在一个层中。

Architecture overview: clients speak the OpenAI API to upstream vLLM's frontend and V1 scheduler, which hand the vllm-metal model runner a packed step plus block tables; the runner reuses mlx_lm's token-wise layers and adds custom Metal paths for paged varlen attention, MTP, and M5 NAX prefill, all executing through MLX and Metal on Apple Silicon unified memory
架构概览:客户端通过 OpenAI API 与上游 vLLM 的前端和 V1 调度器通信,后者将打包的步骤和块表交给 vllm-metal 模型运行器;运行器复用 mlx_lm 的逐 token 层,并为分页 varlen 注意力、MTP 和 M5 NAX 预填充添加自定义 Metal 路径,所有这些都通过 MLX 和 Metal 在 Apple Silicon 统一内存上执行

启动 OpenAI 兼容服务器

在搭载 macOS 15 或更高版本的 Apple Silicon 上,使用 Homebrew 安装稳定版:

brew tap vllm-project/vllm-metal https://github.com/vllm-project/vllm-metal
brew install vllm-project/vllm-metal/vllm-metal

Homebrew 管理 Python 和依赖项。直接运行 vllm 即可启动模型:

# --gpu-memory-utilization sets the serving memory budget; see below.
vllm serve Qwen/Qwen3.5-0.8B --gpu-memory-utilization 0.5
 
# 64 GB Macs: the 27B hybrid
# vllm serve mlx-community/Qwen3.8-27B-4bit --gpu-memory-utilization 0.7
 
# Speculative decoding: Gemma 4 with its MTP assistant
# vllm serve google/gemma-4-E4B-it --gpu-memory-utilization 0.5 \
#   --max-model-len 16384 --no-async-scheduling \
#   --speculative-config '{"method":"mtp","model":"mlx-community/gemma-4-E4B-it-assistant-bf16","num_speculative_tokens":1}'

更多模型请参阅模型矩阵,其他安装方法请参阅安装指南。

该服务器支持 OpenAI API:

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "Qwen/Qwen3.5-0.8B",
       "messages": [{"role": "user", "content": "Say hi"}]}'

任何接受 OpenAI 兼容基础 URL 的工具都可以指向 http://localhost:8000/v1,包括编码代理;vLLM 文档涵盖了 Claude Code 和 Codex 的设置。

设置可预测的内存预算

vllm-metal 的内存保护允许你通过 --gpu-memory-utilization 设置推理预算,为 macOS 和你的应用留出余量。与上游 vLLM 一样,它在启动时运行预热过程,以计入模型权重、激活值和临时缓冲区,然后将剩余预算分配给固定的 KV 缓存。它还会限制 MLX 的可复用缓冲区缓存,以防止服务期间内存累积。无法放入 KV 池的请求会等待页面可用。

打包查询与分页 KV

在 mlx_lm 的填充批次中,注意力查询的形状为 [B, H, T_max, D]:每个请求都获得批次中最长的查询长度。MLX 的 scaled_dot_product_attention 没有 varlen 接口。

vllm-metal 保留了 vLLM V1 用于分块预填充和解码的统一模型步骤。它将每个已调度的查询 token 打包到 [total_q, H, D] 中,用 cu_seqlens 标记请求边界,并在一次模型前向中运行混合步骤。

KV 是独立的:mlx_lm 保持连续的 [B, H, T, D] 缓存,而 vllm-metal 将 KV 存储在固定大小的页面中,由每个请求的块表寻址。已准入的请求可以增长,而无需重塑填充缓存。

预填充和解码使用不同的批处理策略:

引擎预填充注意力解码批处理KV
Lily逐提示调用单请求连续
Uzu逐提示调用单请求contiguous
oMLXper-prompt callsbatchedcontiguous
Splashper-prompt callsbatched, max 4paged
mlx_lmpaddedbatchedcontiguous
llama.cppmask over slotsbatched + prefillfixed cells
vllm-metalpacked, cu_seqlensbatched + prefillpaged

Prefill attention 描述 prompt 查询如何进入 attention kernel:分别进入、填充到共同长度,或拼接。Decode batching 在一个模型步骤中处理多个请求;“+ prefill”表示该批次中包含 prompt token。KV 描述逻辑存储:连续缓冲区、单个 token 单元或 token 块。

A padded rectangle versus vllm-metal's packed varlen step for a 4,000 + 1,000 + 1,000 token batch, with KV read from paged storage
一个填充矩形与 vllm-metal 针对 4,000 + 1,000 + 1,000 token 批次的 packed varlen 步骤对比,KV 从 paged 存储读取

在 4-bit 的 Qwen3.6-35B-A3B 上,我们比较了八个请求的批次,总 prompt token 约为 6,000,每个请求输出 20 个 token。批次 A 的 prompt 长度相近;批次 B 有一个较长的 prompt。表格报告批次墙钟时间(秒),prompt 长度已取整。

批次Prompt tokenmlx_lmoMLXllama.cppvllm-metal
A750 × 84.527.325.293.87
B3,000 + 430 × 710.997.225.333.64
变化+143%−1%+1%−6%

Packing 还能将参差不齐的工作保留在一个批次中。在投机步骤中,一个请求可能贡献一个 decode token,另一个贡献其最后一个 token 加上请求特定数量的草稿,还有一个贡献一个 prefill 块。一个 [B, H, T_max, D] 查询张量必须要么将这些行填充到共同宽度,要么将它们拆分到多次前向中。vllm-metal 则将这些窗口拼接为 [total_q, H, D],并在一次目标模型前向中验证它们。

Metal kernel 将 vLLM 的统一 Triton kernel(在 The Anatomy of a Triton Attention Kernel 中描述)移植到 Apple GPU,甚至包括每个 threadgroup 对 cu_seqlens 运行的二分查找,以找出其查询 token 属于哪个请求。

agent 负载下的并发服务

多个编码 agent 或会话可以同时发送模型请求。我们使用 SiliconBench 的 agent split 对此进行了测量:100 个多轮 prompt,每个约 4.6K 输入 token,在配备 64 GB 内存的 M5 Pro 上运行。所有三个模型比较均使用 4-bit 权重。

SiliconBench 论文对九种 Apple Silicon 服务引擎提供了更广泛的评估,涵盖速度、内存使用和输出保真度。

每个并发级别都从全新服务器开始。oMLX 展示了其默认 SSD 缓存和仅 RAM 缓存;两者都从空开始。附录给出了服务配置,图注报告了完成数量。

Qwen3.8-27B

SiliconBench agent split on Qwen3.8-27B: TTFT, end-to-end request latency, and output token throughput versus concurrency for llama.cpp, vllm-metal, and oMLX with its prefix cache in memory and on SSD
Qwen3.8-27B 上的 SiliconBench agent split:llama.cpp、vllm-metal 以及前缀缓存在内存和 SSD 上的 oMLX 的 TTFT、端到端请求延迟和输出 token 吞吐量随并发变化

vllm-metal 在并发 2 和 4 时具有最低的 TTFT 和端到端延迟。启用 SSD offload 的 oMLX 在并发 1 时领先。

Gemma 4 E4B

对于 Gemma 4 E4B,我们将扫描扩展到并发 16,并纳入带有 MTP drafter 的 vllm-metal。

SiliconBench agent split on Gemma 4 E4B: TTFT, end-to-end request latency, and output token throughput versus concurrency for llama.cpp, vllm-metal with and without the MTP drafter, and oMLX with its prefix cache in memory and on SSD
Gemma 4 E4B 上的 SiliconBench agent split:llama.cpp、带与不带 MTP drafter 的 vllm-metal,以及前缀缓存在内存和 SSD 上的 oMLX 的 TTFT、端到端请求延迟和输出 token 吞吐量随并发变化

vllm-metal 在整个扫描过程中保持低 TTFT。llama.cpp 使用其默认的四个服务器槽位。

Qwen3.6-35B-A3B

Qwen3.6-35B-A3B 总参数为 35B,每个 token 激活 3B。它结合了混合专家层与标准 attention 和 gated-delta-net (GDN) 线性 attention。

SiliconBench agent split on Qwen3.6-35B-A3B: TTFT, end-to-end request latency, and output token throughput versus concurrency for llama.cpp, vllm-metal, oMLX with its prefix cache in memory and on SSD, and mlx_lm
SiliconBench 智能体拆分在 Qwen3.6-35B-A3B 上:llama.cpp、vllm-metal、带内存和 SSD 前缀缓存的 oMLX 以及 mlx_lm 的 TTFT、端到端请求延迟和输出 token 吞吐量随并发数的变化

在并发数为 4 时,vllm-metal 和带 RAM 缓存的 oMLX 在吞吐量和端到端延迟上接近,TTFT 差距更大。mlx_lm 的曲线只覆盖了它完成的一小部分请求。

并发负载下的批量 MTP

MTP 使用辅助模型起草 token,目标模型在连续批次中对其进行验证。下表比较了在 Gemma 4 E4B 上每步一个草稿 token 与不使用 MTP 的生成:

并发数墙钟时间 vs. 无 MTP输出 tok/s vs. 无 MTPTTFT 平均值 vs. 无 MTP
1−15%+20%−1%
8−1%+0%+4%
16−8%+9%+20%

MTP 通过 --speculative-config 选择启用。Metal 路径目前支持 Gemma 4 的纯贪心采样(temperature=0)和同步调度(--no-async-scheduling)。

v0.28.0 中的其他功能

M5 上更快的预填充

在 M5 Mac 上,vllm-metal 会自动对兼容的预填充批次使用 NAX 注意力内核,该内核利用 GPU 的张量硬件。较早的 Mac 继续使用现有路径。此比较使用 Qwen3-0.6B:

NAX versus tiled attention on Qwen3-0.6B: time to first token, total token throughput, and time per output token
NAX 与分块注意力在 Qwen3-0.6B 上的对比:首 token 时间、总 token 吞吐量和每输出 token 时间

在混合模型上复用对话历史

多轮智能体在每一轮都会重新发送其不断增长的对话的大部分内容。前缀缓存让下一轮可以复用为较早轮次计算出的块,而不是再次预填充完整历史。

对于 Qwen3.5 风格的混合模型,vllm-metal 支持 vLLM 的 align 模式。它在与注意力 KV 相同的块边界处保存 GDN 循环状态,使两者都能从缓存前缀恢复(PR #634)。此路径仍为实验性,尚不能与推测解码结合使用。

模型与服务功能

v0.28.0 还包括:

  • LoRA 适配器、结构化输出,以及三种推测解码方法:Gemma 4 MTP、独立草稿模型和提示查找 n-gram。
  • GGUF 检查点,包括用于本地 GGUF 权重的 Hugging Face 配置源。
  • 来自 Qwen3.5、Qwen3.6、Qwen3.8 和 Qwen3-Next 系列的混合注意力模型。
  • 通过 MLX ring 后端在多台 Mac 上进行流水线并行。
  • 实验性视觉语言模型、文本嵌入与重排序,以及语音转文本。

支持的模型矩阵和功能指南见 vllm-metal 文档。

从 M1 Pro 到 M5 Pro 的同一套技术栈

我们在四台 Mac 上以并发数 1 和 8 运行了相同的 Gemma 4 E4B 4 位工作负载。

Two side-by-side bar charts comparing M1 Pro 32 GB, M1 Max 64 GB, M2 Max 64 GB, and M5 Pro 64 GB at concurrency 1 and 8: average TTFT in seconds on the left (lower is better), and output throughput in tokens per second on the right (higher is better).
两张并排柱状图比较 M1 Pro 32 GB、M1 Max 64 GB、M2 Max 64 GB 和 M5 Pro 64 GB 在并发数 1 和 8 下的表现:左侧为平均 TTFT(秒,越低越好),右侧为输出吞吐量(token/秒,越高越好)。

运行跨机器基准测试

每次运行使用 vllm bench serve,包含 100 条 Sonnet 提示,约 1,024 个输入 token 和 128 个输出 token,--gpu-memory-utilization 0.5,禁用前缀缓存,并为每个并发级别启动全新服务器。

在每台机器上使用 vllm-metal v0.29.0 和 vLLM 0.29.0。通过 Homebrew 安装稳定版:

brew tap vllm-project/vllm-metal https://github.com/vllm-project/vllm-metal
brew install vllm-project/vllm-metal/vllm-metal

在一个终端中启动服务器:

vllm serve mlx-community/gemma-4-e4b-it-4bit \
  --gpu-memory-utilization 0.5 \
  --max-model-len 2048 \
  --no-enable-prefix-caching \
  --host 127.0.0.1 --port 8000

在另一个终端中,下载 Sonnet 文本并运行客户端:

curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm/main/benchmarks/sonnet.txt \
  -o sonnet.txt
 
BENCH_MACHINE=m1pro-32gb
BENCH_CONCURRENCY=1
 
vllm bench serve \
  --backend vllm \
  --model mlx-community/gemma-4-e4b-it-4bit \
  --base-url http://127.0.0.1:8000 \
  --dataset-name sonnet --dataset-path sonnet.txt \
  --sonnet-input-len 1024 --sonnet-output-len 128 \
  --num-prompts 100 --num-warmups 3 \
  --request-rate 10 --max-concurrency "$BENCH_CONCURRENCY" \
  --temperature 0 --ignore-eos --seed 0 \
  --save-result --result-dir benchmark-results \
  --result-filename "${BENCH_MACHINE}-c${BENCH_CONCURRENCY}.json"

对于并发数 8,用 Ctrl+C 停止服务器,用相同命令再次启动,并用 BENCH_CONCURRENCY=8 重新运行客户端代码块。在 M5 Pro 上,设置 BENCH_MACHINE=m5pro-64gb;为每台额外机器使用不同的名称。

结果保存在 benchmark-results/ 下。图表使用 Mean TTFT(毫秒除以 1,000 即为秒)和 Output token throughput。请在每个结果旁保留成功请求数。

附录:基准测试复现

跨引擎基准测试脚本和结果位于 SiliconBench。运行使用 vllm-metal 0.28.0.dev20260901062632 搭配 vLLM 0.28.0、llama.cpp 0eadefeb 和 oMLX dc312e6e。

运行在固定并发下以闭环方式使用贪心采样。结果涵盖已完成的请求;空响应计为失败。每个引擎使用各自的 4-bit 转换。内存设置为 vllm-metal 的默认值 --gpu-memory-utilization 0.92、oMLX 的平衡内存保护,llama.cpp 则没有显式上限。

服务配置与测量细节

为每个并发级别启动一个全新的服务器,并创建一个空的 oMLX 缓存目录,RAM 模式也不例外。

# llama.cpp
llama-server -m <model>.gguf --host 0.0.0.0 --port 8001 \
  -ngl 99 --parallel 4 -c 65536
 
# vllm-metal
vllm serve <model> --host 0.0.0.0 --port 8004 \
  --enable-prefix-caching --max-model-len 16384
 
# vllm-metal + MTP (Gemma only)
vllm serve <model> --host 0.0.0.0 --port 8004 \
  --enable-prefix-caching --max-model-len 16384 --no-async-scheduling \
  --speculative-config '{"method":"mtp","model":"mlx-community/gemma-4-E4B-it-assistant-bf16","num_speculative_tokens":1}'
 
# oMLX, SSD offload
omlx serve --model-dir <dir> --host 0.0.0.0 --port 8005 \
  --paged-ssd-cache-dir <fresh-empty-dir> --paged-ssd-cache-max-size 100GB \
  --hot-cache-max-size 0
 
# oMLX, RAM-only cache
OMLX_HOT_CACHE_ONLY=true omlx serve --model-dir <dir> --host 0.0.0.0 --port 8005 \
  --paged-ssd-cache-dir <fresh-empty-dir> --paged-ssd-cache-max-size 100GB \
  --hot-cache-max-size 8GB

填充对比报告每个单元格两次全新服务器运行的中位数,每次运行包含八个请求,每个请求 20 个输出 token。

NAX A/B 使用 Qwen3-0.6B,输入 2,048 / 输出 32 个 token 以及输入 1,024 / 输出 128 个 token。每种配置以请求速率 10 和并发 32 运行 100 条 Sonnet 提示。

致谢

vllm-metal 基于 Apple MLX 团队的 MLX 和 mlx_lm、用于视觉语言路径的 mlx-vlm,以及 vLLM 的引擎和硬件插件接口构建。感谢上游 vLLM 维护者一路以来的审查和支持,也感谢所有针对 v0.2 和 v0.3 版本提交 issue 并分享基准测试的人。

来源:vLLM Blog · vllm.ai