00摘要:九条可证伪的判断
如果只读一页,读这一页。每条判断都标注了它在本报告中的证据位置,也标注了它可能在什么条件下失效。
Qwen3.5-122B-A10B-4bit,M3 Ultra
第 2 次命中 16384 块(第三方实测)
不足一个块无法命中
加载 + 首次推理预热
oMLX 解决的不是「Mac 上能不能跑」,而是 Agent 工作负载的 prefill 税:把重复前缀的计算成本,用存储换掉。
分层 KV 缓存的价值集中在冷路径的恢复,不在热路径的提速。与 mlx-lm 相比,热态几乎打平;差异只在重启之后才出现(7.2s vs 17s)。
它的架构是 vLLM 的调度思想在 MLX 统一内存上的再实现(block / CoW / 前缀共享 / 内容寻址),而非 vllm-mlx 的封装。vllm-mlx 只是起点。
连续批处理是 macOS 生态上真正稀缺的能力,但 3.36–4.14× 的 8 路加速比说明 decode 阶段被内存带宽锁死——并发是给多用户/多智能体的,不是给单用户提速的。
在 oMLX 上,缓存命中率就是 SLA。命中率不由服务调优决定,而由客户端如何组装 prompt 决定:前缀顺序、tools 是否稳定、动态内容放在哪。
在 Mac 推理栈里它不替代 Ollama。Ollama 卖易用性与模型库,oMLX 卖「智能体后端」这一格的工程属性,两者目标函数不同。
能力上限由物理决定:它只把 KV 溢出到 SSD,不溢出权重。64GB 的机器买到的是「巨大上下文复用」,不是「更大的模型」。
成熟度存在结构性矛盾:3465 次提交、近每日发版,修得快也破得快。生产使用必须锁版本 + 固定内核构建。
正确的验收方式是 「看第 2 次请求,不看第 1 次」。任何用首轮 TTFT 做的评测,测到的都是冷启动税而不是缓存收益。
如果你的负载是「同一段大前缀被反复重发」——编码智能体、长系统提示、固定工具集、多轮会话——oMLX 是目前 macOS 上唯一把这件事当第一性问题来解的服务;如果你的负载是「单轮问答」或「一次性脚本」,它给你的只有冷启动的账单。
01定位判定:它是什么,以及它刻意不是什么
同名分支在 GitHub 上至少有四个,营销站点也在抢注相似域名。先把项目边界钉死,后面的所有性能讨论才有对象。
oMLX 是 jundot 维护的、Apache 2.0 授权的 Apple Silicon 本地推理服务。官方仓库的自我定位只有一句话:「带连续批处理与 SSD 缓存的 Apple Silicon LLM 推理服务器,可从 macOS 菜单栏管理」。支撑这一定位的是三层东西:一个 FastAPI 服务(暴露 OpenAI 与 Anthropic 兼容端点)、一套迁移自 vLLM 的分页缓存调度层、一个原生 SwiftUI 菜单栏应用。
它不是模型、不是微调框架、不是聊天客户端。它更接近于「把 vLLM 的工程模型搬到一个没有独立显存、没有 CUDA、没有多卡 NVLink 的硬件平台上」这一次移植尝试的产物。
1.1 起点与演化速度
项目于 2026 年 3 月在 Apple MLX 仓库的 GitHub Discussions 首次公开,起点是 vllm-mlx v0.1.0——一个把 vLLM 的 API 形状套在 MLX 后端上的薄封装。此后它长出了原创新实现:SSD 分层缓存、连续批处理、VLM 全分页缓存、Anthropic 兼容端点、原生 macOS 界面。仓库累计提交数已达 3,465,这个数字本身就是一条关于成熟度的重要证据(见 §09)。
1.2 与相邻项目的概念分野
最容易犯的错误,是把 oMLX 当作「Ollama 的 MLX 版」。两者共享的只有「本地 + OpenAI 兼容 API」这两点表面特征,目标函数完全不同:Ollama 优化的是首次可用时间(装完就能聊),oMLX 优化的是第 N 次请求的边际成本。
| 维度 | Ollama | LM Studio | llama.cpp | mlx-lm | oMLX |
|---|---|---|---|---|---|
| 推理后端 | llama.cpp;2026 起对支持模型默认走 MLX runtime(0.40+) | llama.cpp + MLX,按模型择优 | GGUF / 自研 CPU+Metal kernel | MLX 原生 | MLX + 可选原生 Metal 自定义内核 |
| KV 缓存持久化 | 仅内存,重启丢失 | 仅内存 | prompt cache 落盘(非块粒度) | 内存 prompt cache | 三层:GPU / RAM / SSD |
| 连续批处理 | 串行,无真并发 | 有限 | slot 级并发 | 无调度器 | BatchGenerator + FCFS 调度 |
| 多模型同服 | 可同时常驻 | 可同时常驻 | 单进程单模型 | 单模型 | EnginePool + LRU + pin + TTL |
| API 面 | OpenAI | OpenAI | OpenAI-ish | OpenAI | OpenAI + Anthropic + Responses + MCP |
| 平台 | mac / Linux / Win | Apple Silicon only | 几乎全平台 | Apple Silicon | macOS 15+ / Apple Silicon only |
| 界面 | CLI | 成熟 GUI | CLI | CLI | 原生菜单栏 App + Web 管理面板 |
| 许可 | MIT | 闭源免费 | MIT | MIT | Apache 2.0 |
一个推理服务属于「oMLX 这一类」,当且仅当同时满足三条:① 目标负载是智能体后端(长前缀、多轮、工具调用),而非聊天;② KV 缓存具备跨进程生命周期的持久层,而不只是进程内 LRU;③ 提供 Anthropic Messages 原生端点,使 Claude Code 一类客户端无需协议代理层。缺任一条,它解决的是另一个问题。
02理论根基:prefill 税,以及它为什么可以被存储掉
如果不理解 prefill 与 decode 的物理分野,就会把 oMLX 当成一个「更快的推理器」。它不是。它是一个「把已经算过的东西记住」的服务。
2.1 两个阶段的物理性质不同
Transformer 解码分两段,两段的瓶颈在硬件上是相反的:
- Prefill(预填充):把整段 prompt 一次性前向,产出每个位置的 KV。所有 token 可以并行,矩阵乘占主导——这是算力受限(compute-bound)。在 M4 Max 上,Qwen3.8-27B dense 的 prefill 速率稳定在约 250 tok/s,这个数字几乎不随工程优化变化,它是 GPU 算力的下限。
- Decode(解码):逐 token 生成,每步都要把全部权重从内存搬到计算单元——这是带宽受限(memory-bandwidth-bound)。所以 decode 速度基本由「内存带宽 ÷ 权重字节数」决定,与提示长度无关(只受 attention 的 O(n²) 轻微拖累)。
这个分野直接决定了一件事:你能优化的只有 prefill,decode 你优化不动。这也解释了后文所有数字的形状——为什么 oMLX 的 TTFT 能从 68.6s 降到 2.5s,而 tok/s 始终在 40–60 徘徊。
2.2 智能体请求的真实形状
一次普通聊天请求的 prompt 可能是几百 token。而一次编码智能体请求是这样的:
[system prompt] 约 2k–8k tok ← 几乎不变
[tool definitions] 约 3k–12k tok ← 几乎不变,但顺序敏感
[项目上下文/RAG] 约 2k–20k tok ← 会话内不变
[对话历史] 逐轮增长
[当前用户输入] 几百 tok ← 唯一真正新增的部分
第三方实测给了一个具体样本:把 GitHub Copilot CLI 指向本地 26B 模型,每次请求都发送 26,627 个 token 的 system + tool 定义。第一轮 0 命中,耗时 1 分 28 秒;第二轮命中 14,330(54%),耗时降到 33 秒。同一个「2+2」,同样的模型,差 2.7 倍。
这就是 prefill 税:用户为了一段从未改变的文本,每一轮都重新付一次全量计算的账。
2.3 成本模型
把首 token 延迟(TTFT)拆开,可以得到一个能指导调优的式子:
T_ttft = T_load # 模型权重进入统一内存(懒加载时发生)
+ T_warmup # Metal kernel JIT / MLX 图优化,首次推理独有
+ N_uncached / R_prefill # 未命中 token 数 ÷ prefill 速率
+ T_book # 块哈希查找、元数据、调度开销
四项里,只有第三项与缓存直接相关,而它的分子 N_uncached 完全由客户端的 prompt 组装策略决定。这是本报告最重要的一条推论:在 oMLX 上,性能调优的主战场不在服务端配置,而在调用方的 prompt 结构。
① 静态在前,动态在后。system → tools → 稳定上下文 → 历史 → 当前轮。任何把时间戳、随机 ID、变量内容塞进 system prompt 的做法,都会让缓存永久失配。
② tools 顺序必须冻结。实测中仅交换两个 tool 的定义顺序,命中率从 16,384 块直接掉到 0,TTFT 从 2.5s 回到 81.4s。
③ 一次性调用主动放弃写缓存。摘要器、标题生成、分类器这类不重复的调用,用请求头 X-oMLX-Cache-Store: off 跳过存储,避免挤掉多轮会话真正要复用的前缀。
④ 客户端超时必须大于冷 prefill。如果客户端 30s 就断开,而冷 prefill 需要 68s,则缓存永远建不起来——每一轮都是冷启动,这是最隐蔽的死循环。
2.4 为什么 KV 可以持久化:一个结构性红利
因果注意力有一个性质:位置 i 的 KV 只依赖位置 0..i 的 token,与后面的内容无关。这意味着 KV 是「前缀的函数」——只要前缀相同,产出的 KV 就完全相同。
于是 KV 可以被当作内容寻址的数据来处理:给定前缀,算出它的哈希,去查有没有存过;存过就直接取回,没存过就算一遍再存。这一步跨过去,KV 就从「一次会话的临时产物」变成了「可索引、可去重、可落盘、可跨进程共享的模型状态数据库」。oMLX 的全部设计都是这句话的技术展开。
反过来说,这个红利有严格前提:前缀必须逐 token 完全一致。不是语义一致,不是「差不多」,是二进制一致。这个苛刻条件,就是后面所有工程细节(块大小 4096、链式哈希、tools 敏感)的来源。
03系统架构:五层,以及一条被刻意保留的窄通道
oMLX 的架构图不复杂,但每一层都对应一个 Apple Silicon 特有的约束。读这张图的正确方式,是问「这一层如果不这么设计,会死在哪里」。
ProcessMemoryEnforcer 不在最底部而在引擎池下方,是为了在加载发生前就拒绝,而不是加载失败后再回滚。3.1 五个层次各自的「不这么设计会死在哪里」
| 层 | 组件 | 如果缺省,故障形态 |
|---|---|---|
| API | /v1/chat/completions、/v1/messages、/v1/responses、/v1/mcp/tools | Claude Code / Codex 这类客户端强制走 Anthropic 或 Responses 协议,缺原生端点就得外挂代理(LiteLLM、claude-code-router),多一跳延迟与一处维护点。 |
| 引擎池 | Batched / VLM / Embedding / Reranker / Decision 五类引擎 | RAG 场景要同时跑 chat 与 embedding,单模型服务就得起两个进程、两份 Python 运行时、两次 Metal 上下文初始化,统一内存被重复占用。 |
| 内存治理 | ProcessMemoryEnforcer | macOS 的统一内存被 GPU 与系统共用。加载一个超限模型不会优雅失败,而是把系统拖进 swap,整机卡死——这是 Apple Silicon 上最典型的「服务把机器搞崩」形态。 |
| 调度 | FCFS + BatchGenerator | 无调度器时请求线性排队(Ollama 现状)。多智能体并发时总吞吐不增反降,因为每个请求都要独立跑完整 prefill。 |
| 缓存栈 | Paged / Hot / SSD 三层 + 前缀匹配 | 进程重启即丢全部上下文。智能体重连一次,等于把整段历史重新 prefill 一遍。这是 oMLX 存在的唯一理由。 |
3.2 被刻意保留的窄通道:自定义 Metal 内核
在对 Qwen3.5、GLM-5.2、MiniMax M3 这几族模型上,oMLX 提供可选的原生自定义内核(如 GLM-5.2 的融合 DSA prefill)。这不是通用优化,而是一条按模型手工开的窄通道,代价与收益都极端:
收益端
GLM-5.2 在 M3 Ultra 上,融合 DSA prefill 实测 845 tok/s,通用回退路径约 29 tok/s——将近 30 倍。这不是调参能补的差距。
代价端
需要完整 Xcode 与 Metal toolchain(xcodebuild -downloadComponent MetalToolchain)。只有 Command Line Tools 会报 xcrun: unable to find utility "metal"。缺失时不会报错,而是静默回退到慢路径,同时占用更多内存。
「静默回退」意味着你无法从服务日志判断自己跑在快路径还是慢路径上。部署后必须显式验证:python -c "from omlx.custom_kernels import native_kernel_status; print(native_kernel_status())"。DMG 安装包已预编译内核,源码安装则需 make kernels;pip install -e . 会跳过内核构建。
04三层 KV 缓存:一个内容寻址的状态数据库
这是整份白皮书的技术核心。如果只能记住一句话:oMLX 把 KV 当作「前缀的哈希映射」来管理,而不是当作「这次请求的临时张量」。
4.1 三级存储与各自的职责
常见的误解是「三层缓存是三级速度」。实际上三层的分工并不对称——L1 管的是元数据与活跃性,L2 管的是避免重复读盘,L3 管的是跨重启的持久性:
| 层 | 物理位置 | 承载实体 | 实际职责 | 失效后果 |
|---|---|---|---|---|
| L1 | GPU / 统一内存 | PagedCacheManager | 块表、引用计数、活跃请求映射、写时复制。在「纯 SSD」模式下只留元数据,数据全在盘上。 | 活跃批次无法生成 |
| L2 | 宿主 RAM | PagedSSDCacheManager 的 Hot Cache | 一个 OrderedDict 实现的 LRU 旁路缓冲,防止最近用过的块反复读盘。 | 命中但延迟升高(每次命中都变成一次磁盘读) |
| L3 | SSD | PagedSSDCacheManager | 以 .safetensors 持久化块,哈希分桶子目录;后台写线程 + 原子 rename + 磁盘配额。 | 进程重启后全部重算(退化为普通服务器) |
4.2 链式哈希:为什么不能只用 token 前缀的简单哈希
compute_block_hash(model_name, parent_hash, token_ids, extra_keys) 返回 SHA-256,关键在于 parent_hash 这一项:每个块的哈希都把它前面所有块的哈希卷进去,形成一条加密链。
这不是为了安全,是为了正确性。若只对块内 token 求哈希,那么「块 A 后面接块 B」和「块 C 后面接块 B」会因为块 B 内容相同而共享同一份 KV——但它们的注意力上下文完全不同,KV 也不同,复用直接产出错误输出。链式结构从机制上排除了这种可能:前缀不同,哈希必然不同。
代价是级联失效:只要有一个块变了,它之后所有块的哈希全变,全部失效。这解释了实测中最刺眼的那个数字——仅仅交换两个 tool 定义的顺序,命中块数从 16,384 归零。改动发生在很前面,后面 12 个块跟着一起死。
4.3 异构层:不是所有 KV 都能切块
MLX 的缓存类型并不统一。标准注意力层产出可切的 KVCache,滑动窗口模型产出 RotatingKVCache,而 Mamba / Jamba 这类状态空间层用的是 ArraysCache——它没有序列维度的分块语义,无法按块切片存储。oMLX 用一套类型注册表分流:
| 类别 | 典型类名 | 存储策略 |
|---|---|---|
| 可切片 | KVCache、QuantizedKVCache | 按块存为 4D slice,标准分页路径 |
| 旋转窗口 | RotatingKVCache、PrefillReadyRotatingKVCache | 专用 handler 处理窗口回绕,单独计算有效范围 |
| 不可切片 | ArraysCache(Mamba / Jamba) | 走 BoundarySnapshotSSDStore:在块边界处做整状态快照,序列化为原始字节由后台线程 flush;快照完成后立即释放 GPU 内存 |
BoundarySnapshotSSDStore 这个设计值得单独说一句:不可切片层的状态在长 prefill 期间会持续累积占用 GPU 内存,如果不做快照,128K 上下文的 prefill 会直接把统一内存吃穿。它选择「到边界就落盘、落完立刻释放」,本质是把内存压力换成了磁盘 I/O——又一次「用存储买空间」。
4.4 工程细节:那些决定它能不能长期跑下去的东西
异步写入
磁盘序列化在后台线程完成,队列有界(_MAX_PENDING_WRITES)。队列满时丢弃写入而不是阻塞推理——这是正确的取舍:宁可少缓存一块,不能卡住生成。
原子落盘
自定义的 _write_safetensors_no_mx 走原子 rename,避免半写文件被后续请求当作有效缓存读入。分桶子目录防止单目录文件数爆炸。
配额策略
默认 auto = (空闲磁盘 + 已有缓存文件)的 50%,且预算只增不减,不会因缓存增长而反复收缩。也可用 --paged-ssd-cache-max-size 20GB 硬限。
损坏恢复
校验和验证 + Metal command buffer 检查,失败自动重试并降级到重算路径。缓存坏掉不会让服务挂,只会让它变慢。
可观测性
stats.py 记录命中/未命中比、写丢弃数、恢复事件数。调优时这几个计数比 tok/s 更有诊断价值。
选择性写入
X-oMLX-Cache-Store: off 只跳过存储,不跳过查找。一次性调用仍能复用已有前缀,只是不再污染缓存。
它把「进程重启」这个在本地推理里几乎每十分钟发生一次的事件,从语义上的灾难降级成了性能上的小挫折。对云端服务这是理所当然的,对个人机器上的推理服务,此前没人做过。这才是 oMLX 真正的原创点——不是更快的 kernel,而是让缓存活过进程生命周期。
05调度与批处理:3.36× 背后的物理上限
连续批处理常被讲成「并发让一切变快」。在 Apple Silicon 上这个说法只对一半——它对,但只在多请求场景对;而它之所以不更快,原因在内存带宽,不在调度器。
5.1 连续批处理到底批了什么
传统静态批处理(static batching)要求一批请求全部生成完才能释放批次。一个请求生成 20 token、另一个生成 2000 token,整批就被后者拖住,短请求的槽位在空转。
连续批处理(continuous batching)改为按迭代步调度:每生成一步,完成的请求立刻退出、新请求立刻补位。oMLX 通过 Scheduler(FCFS 队列,并发数可配,默认 8)驱动 mlx-lm 的 BatchGenerator 实现这一点。
这在云 GPU 上是标配,在 Mac 本地推理市场上不是。Ollama 的请求是串行的;mlx-lm 本身没有服务层调度器。oMLX 在 macOS 上是第一个提供真实并发的选项——这个「第一」有实际价值,但价值的上限由下面的数字决定。
5.2 为什么加速比是 3.4× 而不是 8×
decode 阶段每生成一个 token,都要把全部模型权重从统一内存搬到 GPU 计算单元。8 个并发请求共享同一份权重搬运——这是批处理收益的来源。但共享之后,瓶颈就变成了带宽本身:每步的时间被「权重字节数 ÷ 带宽」锁死,与批里有几个请求无关。
所以在理想情况下,8 路并发的吞吐上限是单路的 8 倍(一次搬运服务 8 个 token),但实际上:注意力计算量随批增大而增加、KV 读取量线性增长、调度与同步开销上升。最终落在 3.4–4.1×,意味着带宽利用率从单路的约 12% 提到了约 45%——已经是很有价值的改善,但距离线性很远。
并发数是多租户指标,不是单用户指标。把 --max-concurrent-requests 从 8 调到 32,对单个用户自己的请求延迟没有帮助,只会让 32 个请求一起变慢、并抬高内存占用到可能触发模型驱逐。正确的用法是:一个人跑一个智能体 → 保持默认;一台机器服务一个团队 → 才调高。
5.3 FCFS 的代价:缺少优先级是设计选择,也是缺陷
Scheduler 用先来先服务,没有优先级、没有公平份额、没有抢占。这个选择的后果是典型的队头阻塞:
- 一个 128K 上下文的慢请求排在前面,会把它后面所有请求的首 token 延迟一起拖长;
- 交互式请求(人打字等回复)与批处理请求(离线跑评测)竞争同一队列时,前者会被后者拖累;
- 没有 per-tenant 配额,一个失控的智能体外循环可以吃掉全部并发槽位。
缓解手段只有工程侧的:把批处理任务指向独立的第二个实例、或通过模型别名 + Profile 把它们在引擎层面分开。这不是优雅方案,但对单机场景够用——真正需要优先级的场景,通常也已经在考虑多机了。
06性能实证:把厂商数字与受控实验分开看
这一节只做一件事:把不同来源的证据按强度排队,找出它们在哪些结论上一致、在哪些地方互相矛盾。因为 oMLX 的性能叙事里,最有价值的事实恰恰是一条「反直觉」的。
6.1 证据分级
| 强度 | 来源类型 | 样本 | 可采信范围 |
|---|---|---|---|
| 受控实验 | 同机、同模型、同请求,改单一变量 | oMLX 0.6.4 + Qwen3.8-27B-4bit,M4 Max | 缓存机制的因果效应(命中/未命中差异) |
| 受控实验 | 同机对照 + 重启验证 | mlx-lm vs oMLX,M4 Max 64GB | 「持久化带来的差异」这一具体命题 |
| 第三方实测 | 单机单场景,参数公开 | M1 Max 64GB(0.3.9.dev2)、M3 Max 128GB | 冷启动税、Copilot CLI 命中率 |
| 厂商自测 | 官方基准表 | M3 Ultra 512GB,四个模型 | 吞吐上限与批处理加速比 |
| 社区提交 | 用户上传,参数不完全可控 | omlx.ai/benchmarks(M4 Max 40c 128GB 等) | 硬件档位的量级参考 |
6.2 决定性实验:同一个请求,跑两遍
最有说服力的一组数据来自一台 M4 Max 上的 0.6.4 版本 + Qwen3.8-27B-4bit。实验设计极简:同一个请求,连发两次,只观察变化。
这组数字的完整形态是:
| 场景 | 命中块数 | 重算 token | TTFT |
|---|---|---|---|
| 相同请求(冷,缓存空) | 0 | 16,700 | 68.6 s |
| 相同请求(第 2 次) | 16,384 | 322 | 2.5 s |
| 仅交换 tools 顺序 | 0 | 16,700 | 81.4 s |
| 稳定前缀 + 逐轮增长(模拟智能体) | 12,288 | 增量 | 5–12 s |
6.3 反直觉的发现:热路径几乎打平,差异只在重启后
一位评测者在 M4 Max 64GB 上做了 mlx-lm 与 oMLX 的对照,最初的假设是「分层缓存会在重复长前缀上碾压 mlx-lm」。结果假设被推翻:
热态 prefill
3.4s(mlx-lm)vs 3.6s(oMLX)
基本平手。原因很简单:mlx-lm 在内存里也缓存 prompt。
decode 吞吐
22–25 tok/s(两者一致)
符合预期——decode 由带宽决定,与服务层无关。
重启后恢复
17s(mlx-lm)vs 7.2s(oMLX)
这才是差异真正出现的地方。
如果 oMLX 的价值主张是「更快的 prefill」,它在热路径上就输了——因为 mlx-lm 早就做到了。它真正独有的是让缓存活过进程死亡。所以正确的价值陈述不是「oMLX 更快」,而是「oMLX 在重启后依然快」。这两个说法指向完全不同的使用场景:前者是性能优化,后者是可用性工程。
6.4 冷启动税:必须预付款的 20 秒
oMLX 采用懒加载模型。这意味着服务启动后第一次请求要一次性付清三笔账:
三笔账的构成是:模型加载(9.7–11.9s,权重进统一内存)、首次推理预热(9–10s,Metal 内核 JIT + MLX 图优化)、全量 prefill(取决于 prompt 长度)。前两笔与 prompt 无关,是纯固定成本;第三笔才是缓存能优化掉的部分。
如果你的自动化流程在服务启动后立刻发请求并设置 30s 超时,会稳定撞上「30 秒无响应」,然后被误判为「服务挂了」。正确做法:启动后先发一个 1 token 的探针请求(或从管理面板 pin 模型),把加载与预热成本前置支付,再进入真实负载。
6.5 硬件档位的现实参考
| 机器 | 统一内存 | 实测模型 | Prefill | Decode | 适用判断 |
|---|---|---|---|---|---|
| M4 Max(40c) | 128 GB | Qwen3.8-Flash-Next oQ4e + MTP,1k→128k 上下文 | 535 → 855 | 48.0 → 62.5 | 日常编码主力档,长上下文仍保 48+ |
| M3 Ultra | 512 GB | Qwen3.5-122B-A10B-4bit | 768 → 941 | 42.4 → 56.6 | 厂商基准机,非典型用户配置 |
| M4 Max | 64 GB | Qwen3.8-27B-4bit | 约 250(dense) | 约 42 | 单智能体舒适的性价比档 |
| M1 Max | 64 GB | 26B A4B / 31B dense | — | — | 26B A4B 可用;31B dense 明显吃力 |
07生态位置:Who should not use this
技术选型文档的价值,一半在「推荐什么」,另一半在「明确劝退谁」。oMLX 的适用面比它的星标数窄得多。
7.1 判断路径
7.2 应该劝退的四类用户
「我就想在本机聊天」
单轮问答、单模型、每轮 prompt 都不同——缓存命中率结构性地接近 0。你会付 20 秒冷启动、装一个 Python 环境、还要管一个后台服务,换回来的是零收益。用 Ollama 或 LM Studio。
「我要跑 70B 以上」
SSD 缓存只存 KV,不存权重。64GB 机器上 70B 4bit 大约需要 40GB+ 权重,能装但没有余量给长上下文。这类需求不是换软件能解决的,是换机器或租云。
「我需要 Linux 服务器 / 团队共享集群」
macOS 15+ 与 Apple Silicon 是硬约束(MLX 依赖 Metal,菜单栏用 PyObjC)。团队共享的正确形态是一台 Mac Studio 加反向代理,而不是「部署到 K8s」。
「我需要稳定不变的 API 契约」
近每日发版意味着端点行为、默认参数、模型兼容性都可能在两次 patch 之间变化。如果你的下游有 SLA,锁版本比追新版更重要(见 §09)。
08部署:三种安装路径与四层配置优先级
安装方式的选择不只影响便利性,它决定了你是否拥有那 30 倍的 prefill 内核。
8.1 三条安装路径的实际差异
| 路径 | 命令 | 自定义内核 | 适用 |
|---|---|---|---|
| DMG(推荐) | Releases 下载 → 拖入 Applications | 已预编译 | 不想碰 Xcode 的日常使用者;自带更新、菜单栏、CLI shim |
| Homebrew | brew tap jundot/omlx https://github.com/jundot/omlxbrew install jundot/omlx/omlx --with-custom-kernel | 需完整 Xcode;可用 brew services start omlx 托管 | 需要开机自启、崩溃自动重启的常驻服务 |
| 源码 | git clone … && make install | make install 含内核;make install-no-kernels 不含 | 要改代码、跑测试、或打包自用 App |
8.2 配置优先级:CLI > 环境变量 > settings.json > 每模型设置
四层覆盖关系清晰,但有一个容易误判的点:CLI 标志优先于持久化配置,且不写回。也就是说 omlx serve --max-concurrent-requests 16 只在本次运行生效,不会改变 ~/.omlx/settings.json,下次用默认值启动就回到 8。要让修改持久,必须在 /admin 面板里改。
| 参数 | 默认 | 作用与误用后果 |
|---|---|---|
--model-dir | 必需 | MLX 格式模型目录,支持两级组织。会同时读取 HuggingFace 缓存与 LM Studio 目录,无需重复下载。 |
--max-concurrent-requests | 8 | 连续批处理并发上限。调高的收益是团队吞吐,不是个人延迟;超过内存带宽能支撑的量只会整体变慢。 |
--paged-ssd-cache-dir | 未启用 | SSD 冷层目录。必须放在内置 NVMe 上——外置盘的随机读延迟会吃掉大部分收益,而现有公开数据全部来自内置盘。 |
--hot-cache-max-size | auto | L2 内存热层占比。设太大挤压模型权重空间,反而触发 LRU 模型驱逐,得不偿失。 |
--paged-ssd-cache-max-size | auto(空闲盘 50%) | 磁盘配额。auto 预算只增不减,不会因缓存增长反复收缩;有硬性磁盘约束时改固定值。 |
--memory-guard / --memory-guard-gb | balanced / RAM−8GB | 进程内存上限。低内存机器上这是防「把整机拖进 swap」的唯一保险,不要为了跑大模型关掉。 |
--api-key | 无 | 未设置时拒绝在非 loopback 地址启动。这是内置的安全底线。 |
--hf-endpoint | 官方 | 受限网络环境改用镜像(如 https://hf-mirror.com)。 |
--headless | 关 | 只跑推理与管理 API,不挂 Web UI。适合纯后端部署,且不写入设置。 |
8.3 安全模型:一条不许绕过的规则
oMLX 的安全设计有一条硬边界和若干软开关,理解这个分层很重要:
- 硬边界:未设置主 API key 时,服务拒绝在任何非 loopback 地址上启动。想监听
0.0.0.0,必须先有 key,或 key 与 host 一起保存。 - 软开关 1:
skip_api_key_verification会同时关闭推理与管理鉴权,因此被强制限制在 loopback 绑定下。 - 软开关 2:
auth.allow_unauthenticated_inference(默认 false)允许网络绑定下的无密钥推理,但管理端点仍需主 key 或 admin 登录。这是为「局域网内多客户端调用、但管理面要锁」的场景准备的。
如果你用 Pinggy、Cloudflare Tunnel 之类的隧道把 localhost:8000 暴露出去,oMLX 会认为自己在 loopback 上,安全底线因此不生效。此时必须自行在 --api-key 或隧道层加认证——不要把「服务端有保护」当成「隧道后也安全」。
8.4 客户端接入
管理面板会为选定模型生成现成的配置命令,覆盖 Claude Code、OpenClaw、OpenCode、Codex、Copilot CLI、Hermes Agent、Pi、Mistral Vibe、DeepSeek Harness 等。CLI 侧可用 omlx launch list 查看本机已安装的集成。
一个实测细节值得记下:Copilot CLI 走的是 /v1/responses 端点,仓库中有注释明确指出它对 completions 端点存在问题。这类客户端差异说明「OpenAI 兼容」是个光谱而非开关——真正决定兼容性的是客户端实际调用的端点与字段,不是宣传语。
09能力边界:反证据清单
一个只列优点的技术文档没有价值。下面每一条都是「它做不到」或「它做得不够好」,且都能在实测或源码里找到依据。
| 边界 | 具体表现 | 性质 | 缓解手段 |
|---|---|---|---|
| 不扩大模型上限 | SSD 只溢 KV,不进权重。64GB 机器仍是 64GB 的模型容量。 | 物理约束 | 无。换硬件或量化到更低比特。 |
| 冷启动税更重 | 懒加载 + 内核 JIT,首请求 TTFT 约为 mlx-lm 的 2–3 倍;总预算 20–22s。 | 设计取舍 | 启动后打探针请求预热;管理面板 pin 模型。 |
| 平台锁死 | macOS 15+ / Apple Silicon only。无 Linux、无 Intel、无 Windows。 | 硬约束 | 无。跨平台需求请用 llama.cpp 系。 |
| 对 prompt 结构极度敏感 | 块粒度 4096;tools 顺序变化导致级联失效;动态内容插在 system 里则永不命中。 | 机制副作用 | 规范 prompt 组装顺序;把动态内容后置。 |
| 无优先级调度 | FCFS,无抢占、无公平份额。长请求阻塞短请求(队头阻塞)。 | 实现简化 | 批处理任务用第二个实例或独立模型别名隔离。 |
| 版本节奏带来的稳定性风险 | 3,465 次提交、近每日发版;曾有 72 小时内连发三个版本。 | 项目阶段 | 锁版本;固定内核构建;上线前跑自己的回归负载。 |
| 分布式是实验特性 | 多 Mac pipeline 并行(Ring / Thunderbolt RDMA / JACCL)标注为 experimental。 | 成熟度 | 仅用于非关键扩容;生产依赖单机。 |
| 生态覆盖面 | 仅 MLX 格式权重(GGUF 需外部 Splash 引擎);无 Gemini CLI 集成实现。 | 生态 | 主流模型有 mlx-community 版本;小众微调需自行转换。 |
| 基准数据自证为主 | 官方表格来自 M3 Ultra 512GB;SSD 恢复路径在慢速外置盘上缺第三方验证。 | 证据缺口 | 用自己的负载实测,别外推厂商数字。 |
官方宣传语是「TTFT 从 90 秒降到 5 秒以下」。这个数字在特定配置下成立,但它描述的是长上下文的重复前缀这一种场景。把它理解成「oMLX 让本地推理快 18 倍」,会导致在三类场景上做出错误决策:单轮对话、短 prompt、以及需要最大单发速度的脚本——在这三类里,oMLX 与 mlx-lm 的差距在噪声范围内,而它多出来的部署复杂度是确定的。
10反模式清单:三类会白付成本的用法
每一条都给出症状、成因、修正动作。症状部分写得具体,是为了让你能对着自己的日志认出来。
10.1 架构层
| 反模式 | 症状 | 成因 | 修正 |
|---|---|---|---|
| 把 oMLX 当加速器接单轮 API | 命中率长期低于 5%;每次请求 8–70s;用户以为服务性能差 | 每个请求都是新前缀,缓存层只增加查找开销与写盘 I/O | 单轮场景换 mlx-lm;或保留 oMLX 但显式关闭写入(X-oMLX-Cache-Store: off),只享受端点兼容 |
| 动态内容塞进 system prompt | 改动一行代码后命中率归零;明明「prefix 没怎么变」却总在重算 | 时间戳、会话 ID、随机种子、当前文件内容插在 system 里 → 块 0 就变 → 链式哈希全链失效 | 静态在前、动态在后。system 只放真正不变的部分 |
| 每轮重新排序 tools | TTFT 在 2.5s 与 80s 之间来回跳 | tools 渲染进前缀,顺序变化即内容变化(实测命中从 16384 归零) | tools 定义做确定性排序(如按 name 字典序)并固化在代码里 |
| 让一次性任务写缓存 | 多轮会话的命中率被「别的任务」挤掉;SSD 增长快但复用少 | 摘要器、标题生成、意图分类各自写入大量只出现一次的前缀块 | 这类调用加 X-oMLX-Cache-Store: off;或把它们路由到独立模型别名 |
10.2 调参层
| 反模式 | 症状 | 成因 | 修正 |
|---|---|---|---|
| 并发数往大调 | 单请求延迟上升、总吞吐没涨、偶发模型驱逐 | decode 是带宽瓶颈,8 路已把带宽利用率推到约 45%;再加只是分摊 | 个人用保持默认 8;团队按「并发用户数 × 1.5」估,且用真实负载压测确认 |
| hot cache 占比设太大 | 模型频繁被驱逐又重载,出现规律性的 10s 卡顿 | L2 吃掉统一内存 → 触发 EnginePool 的 LRU 驱逐 → 下次请求重新加载 | 让 hot cache 留在 auto;先保证模型权重与 KV 有空间 |
| SSD 缓存放外置盘 | 命中却有 1–3s 甚至更高的额外延迟;「持久化好像没用」 | 外置 USB/网络盘的随机读延迟远高于内置 NVMe。所有公开数据均来自内置盘 | 缓存目录固定在内置 NVMe;外置盘只放模型权重(顺序读为主) |
| 关掉 memory guard 跑大模型 | 整机卡死、风扇狂转、需要强制重启 | 统一内存被 GPU 与系统共用,超限不是优雅失败而是 swap 风暴 | 保留 --memory-guard;用 --memory-guard-gb 设定明确上限 |
10.3 运维层
| 反模式 | 症状 | 成因 | 修正 |
|---|---|---|---|
| 客户端超时太短 | 缓存永远建不起来;每次都是冷启动;日志里全是「连接中断」 | 冷 prefill 需要 60s+,客户端 30s 就断开 → prefill 被打断 → 块从未写盘 | 把首个请求的 timeout 设到 120s+;启动后先打探针预热 |
| 不 pin 模型 | 每隔一段时间出现一次「莫名其妙很慢」 | TTL 到期或内存压力触发卸载,下一次请求重新付加载成本 | 主力模型在管理面板 pin 住;其余模型设合理 TTL |
| 跟着主干走 | 昨晚还好今天挂;两次重启行为不一致 | 近每日发版;0.7.3.dev2 这类 dev 版本出现在主干 | 锁定 release tag,升级走「起旁路实例 → 跑同一负载 → 比对命中率与 TTFT」 |
| 隧道后当内网用 | 公网可无认证调用推理接口,账单/数据双风险 | 服务看到 loopback 就跳过验证,安全底线不生效 | 隧道层加认证,或显式设 --api-key |
| 用首轮 TTFT 做验收 | 基准数字难看,误判「方案不可行」;或数字好看,上线后打脸 | 首轮包含加载 + 内核 JIT,与被测机制无关 | 跑三次,只取第 2、3 次;并把「重启后第一次」单列为一项指标 |
11落地路径:五级成熟度与验收清单
从「装上试试」到「团队共享生产」,中间隔的不是配置项,而是四个具体的验证动作。
11.1 四个必须自己跑的验证
以下四条不依赖任何第三方基准,二十分钟内可完成,但它们能决定方案是否成立:
验证 1 · 缓存真的生效了吗
同一个请求连发三次,记录第 1、2、3 次的 TTFT。期望形态:第 1 次显著高,第 2、3 次接近且大幅下降。如果第 2 次没降,说明前缀里混进了动态内容。
验证 2 · 重启还活吗
跑一轮让缓存建立,重启服务,再发同样的请求。这是 oMLX 与 mlx-lm 唯一的分水岭。若重启后退回到冷启动水平,说明 SSD 层没生效(目录配置错误或配额为 0)。
验证 3 · 顺序敏感度
把 tools 数组顺序交换一次,再发同一请求。预期命中归零。这一步不是找 bug,是确认自己理解了机制——也顺便证明缓存判定确实在按内容工作。
验证 4 · 内核在不在
跑 native_kernel_status()。如果你用的是 GLM-5.2 / MiniMax M3 / Qwen3.5,这一项决定你是 845 还是 29 tok/s 的 prefill。静默回退不会给你任何提示。
11.2 需要长期盯的三个指标
| 指标 | 健康区间 | 告警含义 |
|---|---|---|
| 前缀命中率(命中块 / 总块) | 智能体负载 ≥ 60% | 持续低于 30% → 负载类型不匹配,或 prompt 结构有动态污染 |
| SSD 缓存写丢弃数 | 接近 0 | 持续上升 → 写入队列长期打满,磁盘 IO 跟不上,考虑换内置盘或降低并发 |
| 模型重载次数 / 天 | ≤ 1(pin 的主力模型应为 0) | 频繁上升 → 内存治理在赶模型,需要下调并发或 hot cache 占比 |
12结论:一次成功的「错位移植」
oMLX 做的事情,用一句话概括是:把一个为数据中心设计的服务模型,硬塞进了一台个人电脑,而且塞对了。
vLLM 的 PagedAttention 诞生于「显存稀缺、请求高并发、GPU 昂贵」的云环境,目的是让同一块 GPU 服务尽可能多的用户。把它搬到 Apple Silicon 上,前提全变了:没有独立显存(统一内存),没有多用户(通常一个人),GPU 不贵(已经买断了)。按常理,这套工程应该水土不服。
但它找到了一个在新环境里成立的对应物:云上稀缺的是显存,Mac 上稀缺的是「不被打断的连续计算时间」。智能体的存在制造了这个稀缺——每一轮都要求机器把两万个 token 重新算一遍,而这台机器同时还要跑 IDE、浏览器和 Docker。
于是那些在云上用来省显存的机制,在这里被用来省时间:分页是为了碎片化复用,这里是为了让不同会话共享同一段前缀;写时复制是为了并发安全,这里是为了让多个客户端读同一份缓存而不互相污染;LRU 驱逐是为了容量管理,这里是为了在 64GB 里同时放下模型、KV 和 IDE。连 SSD 落盘这个在云端几乎不存在的设计(云盘太慢),在 Mac 上反而成立——因为本地 NVMe 的随机读延迟,比重新跑一次 16K prefill 快了两个数量级。
判断(这是观点,不是共识)
oMLX 的技术价值不在于它比谁快,而在于它证明了「本地推理服务的缓存可以是持久的」这件事值得做。一旦这个前提被接受,后续所有优化(KV 量化、跨进程共享、多机分片)都有了新的落点。目前它更接近「一个方向正确的早期实现」,而不是「一个可以无脑上生产的组件」——但这个方向本身,比它当下的完成度更重要。
12.1 三句话行动建议
- 如果你的日常是 Claude Code / Cursor / Codex 指向本地模型,现在就换,收益立刻可见,而且改造成本只有一次配置。做完 §11.1 的四个验证再决定是否长期使用。
- 如果你只是在本机聊天或跑脚本,不要换。你付出的部署复杂度与冷启动税是确定的,换回的收益是零。
- 无论哪种情况,先花半天把自己的 prompt 组装顺序规范化。这一步的收益不依赖 oMLX——它对任何带前缀缓存的推理服务都成立,而它恰好是绝大多数团队从未做过的事。
12.2 尚待观察的三个问题
- 单维护者风险。项目由一位独立开发者维护,没有公司背书。3,465 次提交的活跃度是优点,也是单点依赖的放大器。团队采用前应评估「如果明天停更」的迁移成本——好消息是 MLX 生态本身在扩张,坏消息是这套 SSD 缓存没有等价替代。
- 跨进程 / 跨模型共享尚未出现。当前缓存绑定在单个服务实例上。如果未来能做到多个本地进程共用一个缓存池(比如 IDE 插件与 CLI 共享),收益会再上一个量级。这是最值得期待的方向。
- 基准数据的独立性。目前几乎所有性能数字都来自维护者自测或单篇第三方评测,缺少多环境、多负载的交叉验证。SSD 恢复路径在慢速盘、低内存机器上的表现尤其缺乏数据。
13参考来源
按证据类型分组。项目自述与第三方实测严格分列,厂商基准单列,便于读者按需回溯。
一手项目资料
- 代码与文档:
github.com/jundot/omlx—— README(架构段、CLI 段、模型支持表、致谢)、docs/admin-api.md。注意 GitHub 上存在多个同名 fork(longfei12k/omlx、jgranie/omlx等),内容为镜像,非原始项目。 - 源码级机制:DeepWiki 对
jundot/omlx的索引页,关键文件omlx/cache/paged_cache.py(块哈希compute_block_hash)、omlx/cache/prefix_cache.py(BlockAwarePrefixCache)、omlx/cache/paged_ssd_cache.py(Hot Cache 与_write_safetensors_no_mx)、omlx/cache/boundary_snapshot_store.py(BoundarySnapshotSSDStore)、omlx/cache/type_registry.py、omlx/cache/stats.py、omlx/server.py。本报告的三层缓存与类型注册表描述均据此。 - 官方站点:
omlx.ai(作者维护,含功能说明与/benchmarks社区提交页)。相关内容需与 README 交叉核对。
第三方受控实验(证据强度最高)
- 缓存命中对照实验:EOGEE《Qwen3.8-27B 本地高速部署方案(Mac Studio M4 Max / oMLX)》,oMLX 0.6.4 + Qwen3.8-27B-4bit。提供 16.7K 请求的冷/热 TTFT、命中块数、tools 顺序敏感性、块粒度 4096 与 TTFT 三层根因分解。本报告 §06.2 全部数字与 §02.3 的成本模型据此。
- mlx-lm vs oMLX 对照:daily.dev《mlx-lm vs oMLX: I Was Wrong About the Winner》,Mac Studio M4 Max 64GB。提供热态 prefill 3.4/3.6s、decode 22–25 tok/s、重启后 17s/7.2s 三组关键对照,以及四个评测陷阱(mlx-lm 的 prompt 缓存、缓冲输出、oMLX 懒加载、SSD 缓存污染冷启动测量)。本报告 §06.3 据此。
- 冷启动分解与客户端实测:lilting.ch《oMLX 0.3.9.dev2 tested on M1 Max 64GB》。提供卸载 0.9s、加载 9.7–11.9s、首次推理预热 9–10s 的分解,以及 Copilot CLI 26.6K token 请求 88s → 33s(54% 命中)的实测。§06.4 与 §08.4 据此。
- RAG 场景 TTFT 表:Stochastic Sandbox《omlx: Run Local LLMs on Apple Silicon with a RAG Customer Support App》,M3 Max 128GB + Qwen3-8B-4bit,三种 prompt 长度的冷/热 TTFT 与 tok/s。
厂商自测基准
- 吞吐与批处理:官方基准表(M3 Ultra 512GB),Qwen3.5-122B-A10B-4bit、Qwen3-Coder-Next-8bit、MiniMax-M2.5-8bit、GLM-5-4bit 的单请求与 1×–8× 并发数据(pp1024 / tg128,无缓存复用)。本报告图 4 据此。
- 社区提交基准:
omlx.ai/benchmarks,含 M4 Max 40c 128GB 上 Qwen3.8-Flash-Next 各量化与各上下文的 PP/TG 数据(1k–128k)。表 7 部分行据此。 - 内核性能:README 安装段注释,GLM-5.2 融合 DSA prefill 在 M3 Ultra 上 845 vs 约 29 tok/s(约 30×),需完整 Xcode 与 Metal toolchain。
背景资料
- MLX 框架机制:Apple MLX 的统一内存、惰性求值、动态图、Metal 后端与
mx.compile融合机制(用于 §02.1 的 prefill/decode 物理性质分析)。 - Mac 推理栈全景:多篇 2026 年横评(Ollama 的 MLX runner 演进、LM Studio 双后端、llama.cpp 的 slot 并发、mlx-lm 的无服务层),用于表 1 的对比校准。
- 项目评测与背景:PromptQuorum 的 oMLX 评测(项目起源、维护者背景、fork 泛滥情况)、RunAIHome 的 Apple Silicon 综述(边界判断、硬件档位建议)。
本报告所有性能数字均保留原始来源的单位与量级,未做跨来源归一化。不同来源的硬件、版本、模型、prompt 长度、是否含预热均不相同,横向相减或相除会得到错误结论。表格内的对比只在同一来源内部有效,跨来源的并列仅用于展示量级。