工程调查:这个 MLX 端口还能再快 10× 吗?
日期:2026-09-19。机器:Apple M3 Max,40 个 GPU 核心,128 GiB 统一内存, macOS 27.2,MLX / MLX Metal 0.32.2,FP16 推理。本报告包含真实的 本地实验,其中包括一个手写的 Metal kernel。它不改变 生产运行时,也不发布量化权重。
这些经过测试的工程改动没有带来 10×。 交错测量支持来自编译和 裁剪最后决策头层未使用输出的、适度的、依赖形状的改进。用成对的每轮中位数 衡量,选定用例改进了约 3–8%。一些更大的批区间不包含改进。一个自定义的 精确 erf GELU/gate kernel 在数值上成功,但相比 MLX 编译没有提供 一致的额外端到端收益。朴素的 8-bit 和 4-bit 主干量化减小了存储, 却没能加速更大的试点工作负载,并改变了预测或校准概率。
数学极限和近似权衡另行在 MATH_10X_RESEARCH.md 中考察。最初的实现审阅 在 PERFORMANCE_RESEARCH.md 中;已发布 checkpoint 的基准仍然是 BENCHMARKS.md。
实验控制与局限
所有研究 GPU 工作都串行运行。其他 agent 工作只用 CPU/文件系统/网络。
机器使用交流电,实验期间没有记录到 pmset 散热/性能警告,也没有报告
使用交换区。正常的桌面活动继续。这不是一个受控的恒温箱,也不是一台在
其他方面空闲的专用基准机器。
最初的筛选运行让每个候选在全新进程里跑,带 4–5 次预热和 12–16 个样本。 它们揭示了明显的运行间漂移。例如,英文单问题试点暗示有 1.24× 的编译改进, 而随后的交错实验只发现约 1.03×。因此顺序试点的延迟是筛选证据,不是主要 的因果加速主张。
确认脚本 paired.py:
- 在每一轮内轮换候选顺序,并为该轮中的每个候选使用相同的输入。
- 在各轮之间改变实际的 state 文本。它生成至多 16 个 state 变体,并保留 张量形状相同的变体;multilingual 短用例有 10 个这样的变体,而其他报告的 用例有 16 个。
- 使用不同的自然语言问题,最大的短工作负载包含 50 条不同指令。它检查输入 哈希,不缓存答案、不去重问题,也不复用上下文编码器状态。
- 在停止每个计时器之前求值结果并同步 GPU。它同时测量已准备的前向调用 和公开的预测路径,包含分词和输出格式化。模型加载不计入。
- 在预热之后,英文头/编译实验跑 32 个测量轮,multilingual 和自定义 Metal 实验跑 16 个。每个候选看到相同的轮数和输入序列。
这些输入不同于已发布的基线固定样本。下面的比较是研究实验内部的, 而不是通过相除不相关的表得到的 before/after 比较。研究的批上限是 64, 而已发布 API 默认是 16。重复的 state 变体是有意的重复测量;没有结果缓存。
analyze.py 计算每轮的
eager_time / candidate_time 比值及其中位数的探索性百分位 bootstrap 区间,
使用 2,000 次轮索引重采样。这些区间没有考虑操作系统噪声或序列相关的
每一个来源,也不能替代多会话复现。把独立计算的 p50 值相除得到的比值
可能与成对中位数比值不同。
原始 JSON 包含所有计时、输入哈希、环境元数据、保真指标,以及测量时记录的 源指纹。实验脚本随后被格式化,并扩展了不重叠的可选候选;更早的指纹描述的 是那些更早的脚本版本。
编译与精确的末头裁剪
比较了四条路径:
- Eager: 已发布的 FP16
DecisionModel。 - 编译: 在已加载、已求值、冻结的模型外包裹
mx.compile,使用 常规的形状特化。 - 选中的 Q + 编译: 在最后一个头层中保留全长的 QKV 投影和 K/V,但只 发出 CLS/选项标记的注意力 query。只对这些选中的输出运行输出投影和 FFN。
- 全注意力 + 选中输出 + 编译: 保留原始的全长 QKV 和 SDPA 调用,然后 在输出投影和 FFN 之前收集 CLS/选项输出。这保留了原始的注意力 kernel 形状, 同时移除大部分未使用的末头稠密工作。
两个裁剪原型都保留了模型的数学依赖关系。它们仍然计算所有 QKV 投影; 它们没有实现数学上界计算中那部分额外的仅 Q 投影节省。改变 GEMM 和 SDPA 形状可能改变浮点舍入。两个原型都不是解码器缓存、提前退出,或丢弃 更早 Transformer 层的近似。
端到端 p50 延迟,毫秒:
| 模型 / 请求 | B × L | Eager | 编译 | 选中的 Q + 编译 | 全注意力 + 选中输出 + 编译 |
|---|---|---|---|---|---|
| 英文短 1 | 1 × 78 | 16.628 | 16.185 | 15.925 | 15.636 |
| 英文短 16 | 16 × 82 | 116.920 | 113.700 | 112.009 | 110.217 |
| 英文长 1 | 1 × 512 | 53.921 | 53.078 | 52.301 | 52.121 |
| 英文长 8 | 8 × 512 | 531.166 | 518.428 | 504.135 | 488.980 |
| 英文短 50 | 50 × 82 | 456.333 | 439.013 | 445.223 | 438.293 |
| Multilingual 短 1 | 1 × 80 | 8.050 | 7.570 | 7.438 | 7.388 |
| Multilingual 短 16 | 16 × 83 | 44.351 | 43.830 | 42.281 | 42.968 |
| Multilingual 长 1 | 1 × 1024 | 41.964 | 42.017 | 40.492 | 41.120 |
| Multilingual 长 8 | 8 × 1024 | 326.327 | 323.053 | 327.842 | 319.010 |
来源:英文成对数据和 multilingual 成对数据。
对全注意力/选中输出路径,成对中位加速和探索性 95% 区间包括:
| 请求 | 成对中位加速 | Bootstrap 区间 |
|---|---|---|
| 英文短 1 | 1.049× | 1.043–1.056× |
| 英文短 16 | 1.059× | 1.033–1.077× |
| 英文长 1 | 1.039× | 1.027–1.052× |
| 英文长 8 | 1.061× | 1.020–1.095× |
| 英文短 50 | 1.022× | 0.977–1.050× |
| Multilingual 短 1 | 1.077× | 1.046–1.140× |
| Multilingual 短 16 | 1.042× | 1.017–1.067× |
| Multilingual 长 1 | 1.027× | 1.012–1.054× |
| Multilingual 长 8 | 1.067× | 0.958–1.082× |
英文 50 问题和 multilingual 长批的区间包含 1。它们不能确立一个可重复的 改进。选中的 Q 路径在 multilingual 短 16 和长 1 用例上略好,但没有哪条 裁剪路径在每个形状上都占优。所有候选区间、前向测量和原始每轮比值都在 paired_analysis.json。
在这个头/编译实验中,编译在两个模型系列的 1,530 个变化输入问题比较上, 与 eager 的 logits、动作 logits 和校准概率完全一致。两条裁剪路径在所有 1,530 个 argmax 决策上一致,最大的校准概率差异为 0.0001883。全注意力 裁剪路径还通过了每个模型单独的 63 问题固定样本套件:126/126 一致,英文 最大概率差异 4.31e-5,multilingual 为 6.48e-6。这些是回归检查,不是对 1,530 个独立带标签样本的任务准确率主张。
整模型编译和逐块编译都做了筛选。块实验也保留了全部 63 个英文固定样本
输出,但没有确立相对于整模型编译的实质优势。形状特化在一个服务里必须有界。
模型使用依赖形状的 Python reshape 和掩码,所以不加区别地应用
shapeless=True 是不安全的。官方编译指南
记录了形状特化和状态捕获。
第一次英文整模型候选调用耗时 2,166.7 ms,随后在那个试点中是约 12.75 ms
的热态前向 p50;一个新的 B16 形状首次调用耗时 272.4 ms。该 JSON 字段名叫
cold_forward,但它指的是在 eager 参考推理之后的第一次候选调用,不是
完全冷启动的应用或全新初始化的 Metal driver。后续候选复用了此前编译的
Metal kernel,所以它们的首次调用时间不是冷启动成本的有控排名。试点中已编译
英文短 1 的 MLX 活动/峰值内存约为 803.6/918.6 MiB;multilingual 约为
614.1/676.9 MiB。这些分配器测量不包含每一处宿主侧编译器分配,也不能确定
在无界形状变动下的内存上限。见
英文编译试点和
multilingual 编译试点。
选择性量化:存储节省有用,但不适合作为加速主张
这个原型在加载稠密 FP16 模型之后调用 nn.quantize。它只选择
encoder.layers.* 线性模块,仿射组大小 64,然后编译得到的模型。嵌入、
归一化、决策头、评分器和动作头保持 FP16。这避免了让打包的整数权重经过
当前的稠密加载器,也避开了动作头不可整除的 1028/772 输入宽度。没有发布
任何量化 checkpoint 格式或加载契约。官方 MLX 量化层实现
提供了这个选择机制。
| 模型 / 编码器精度 | 张量总存储 | 固定样本一致 | 固定样本最大概率变化 | 不同工作负载一致 | 不同工作负载最大概率变化 |
|---|---|---|---|---|---|
| 英文 FP16 | 803.55 MiB | 参考 | — | 参考 | — |
| 英文 8-bit | 496.76 MiB | 62/63 | 0.0401 | 18/18 | 0.0312 |
| 英文 4-bit | 333.13 MiB | 50/63 | 0.3256 | 18/18 | 0.2224 |
| Multilingual FP16 | 613.99 MiB | 参考 | — | 参考 | — |
| Multilingual 8-bit | 515.38 MiB | 63/63 | 0.0133 | 26/26 | 0.0358 |
| Multilingual 4-bit | 462.79 MiB | 63/63 | 0.1268 | 19/26 | 0.8008 |
multilingual 的 4-bit 结果说明了为什么仅靠小型固定样本套件不够:它的 63 个 固定样本 argmax 保持不变,但 26 个不同工作负载决策中有 7 个改变。这些是 与 FP16 的一致性测量,不是真值准确率测量。0.8008 的绝对概率变化就是 80.08 个百分点。
在英文短 16 的试点输入上,FP16 eager/编译的端到端 p50 是 91.26/87.94 ms; 8-bit/4-bit 编译是 96.66/93.20 ms。短 1 的量化在那次筛选运行中看起来略快, 而更大的形状没有。multilingual 大形状筛选也没能显示速度优势,但它的顺序 运行有明显的漂移。这些观察为拒绝一个无条件的加速或发布主张提供了理由, 而不是在没有交错量化复现的情况下指定精确的减速因子。进一步的量化工作需要 激活感知的校准或微调,以及一个有代表性的带标签质量套件。
原始来源:英文 8-bit、 英文 4-bit、 multilingual 8-bit、 multilingual 4-bit。
手写 Metal:精确的 GELU/gate 融合已实现并测试
kernels.py 实现了一个真正的自定义 Metal kernel,它读取两个拼接的 MLP 分支,计算相同的基于 erf 的 GELU,乘以门控, 并写出单个输出。它不替换成 tanh-GELU 或 sigmoid 近似。该 kernel 使用 MLX v0.32.2 自带的 erf 和 expm1 辅助函数,并在 vendor/README.md 中保留它们的许可和声明。它显式 只支持 FP16,并使用安全的 Metal 数学模式。官方自定义 kernel 指南 描述了这套 API 及其数学模式控制。
在八个有代表性的激活形状上,27,958,016 个随机生成的 FP16 输出元素与原始 操作的值完全相等。这个微基准比较的是数值相等,不是零的符号位。全模型 变化输入测试也完全匹配:跨两个模型系列的 474/474 个问题比较,外加两个 63 问题固定样本套件,logit、动作 logit 和校准概率差异均为零。
这个正确性结果并没有转化为相对 MLX 融合编译表达式的一致速度优势。例如,
在 1,312 token、中间宽度 2,624 时,每次调用同步的激活计时对 eager 的
GELU-then-gate 是 0.378 ms,对 mx.compile 是 0.268 ms,对自定义 kernel
是 0.280 ms。在 8,192 token、宽度 1,152 时,对应值是 0.846/0.764/0.714 ms。
这些微基准包含调度和同步开销,是筛选探针;它们不是对隔离设备执行时间的
测量。完整输入、原始计时和相等性检查都在
microbench.json。
随后该自定义 kernel 被装入每一个编码器 MLP,并在轮换候选顺序和变化 输入下于完整模型中测量:
| 模型 / 请求 | 原始编译 p50 | Metal + 编译 p50 |
|---|---|---|
| 英文短 1 | 23.795 ms | 23.837 ms |
| 英文短 16 | 142.716 ms | 139.355 ms |
| 英文长 1 | 68.241 ms | 68.982 ms |
| Multilingual 短 1 | 7.557 ms | 7.437 ms |
| Multilingual 短 16 | 49.683 ms | 50.301 ms |
| Multilingual 长 1 | 48.906 ms | 51.032 ms |
完整的自定义 kernel 成对运行使用一个权重相同的第二个模型实例,这样未修改 和自定义实现可以共存而不发生变更或陈旧的编译捕获。它们的绝对计时不得与 更早的末头裁剪运行比较。这些适度的混合结果不支持把自定义 kernel 作为通用 性能改进来发布。来源: 英文 Metal 成对数据和 multilingual Metal 成对数据。
自定义工程在哪些地方值得进一步调查
模型已经调用 mx.fast.scaled_dot_product_attention、mx.fast.rope
和优化过的层归一化。它的 D64 布尔掩码 SDPA 路径是融合的;并不存在一个能
解释 10× 差距的缺失 Flash Attention 开关。局部注意力仍然遍历稠密的
key/value tile。一个真正的双向窗口 kernel 可以跳过那些 tile,同时保留
包含端点的距离 <=64 和填充语义,但它的整模型算术机会在短输入上很小,
在已发布的长形状上也是有界的。现有源码审阅
和数学报告量化了这一区别。
有用的后续项目及各自的证据要求是:
- 长输入窗口注意力: 针对 D64、实际的双向窗口和填充批特化 tile 边界。 在 512/1024 token 上与融合的稠密 SDPA 比较,然后在完整模型中比较。 本报告没有构建或基准测试这个 kernel。
- 稠密 kernel 的尾声与调度: 调查把带门控 MLP 的尾声融合进 GEMM,或改进 短 M 的矩阵调度。MLX 已经使用专用的 Metal GEMM 实现,所以替换它们需要 真实的调度/kernel 剖析,以及在精确 M/N/K 形状上实测的收益。单独的激活 结果表明,为什么仅再写一个逐元素 kernel 并不够。
- 长度感知的批处理与共享 CPU 准备: 在构造每个问题序列之前,把共享 state 文本只分词一次,同时保留精确的输入 ID,并避免把小项填充到不相关的 长项。multilingual 长 8 试点花约 13.1 ms 准备输入, 而端到端是数百毫秒。即使完全消除那部分准备,在这个工作负载上也不会产生 10×。排队延迟和唯一推理数必须成为任何批处理主张的一部分。
- 一个更小的联合作答学生: 如果 10× 是产品需求,就蒸馏或重设计模型, 以移除大部分稠密工作,或用一次上下文编码回答许多固定问题。这会改变学习到 的模型,需要代表性的带标签训练/评估;它不是一次精确的端口优化。在当前的 双向编码器里跨问题复用任意的上下文 state/KV 是无效的。
八个独立的 FP16 编码器输入投影 GEMM 探针达到了 0.66–11.55
TFLOP/s(含每次调用的同步)。英文大型的
M=4096, N=5248, K=1024 探针达到 11.55 TFLOP/s;multilingual 的
M=8192, N=2304, K=768 探针达到 7.96 TFLOP/s。这些是观测到的吞吐
值,不是硬件峰值指标,也不是全图吞吐的上界。小 M 的测量尤其被提交和
同步成本主导;流式的图会以不同方式摊薄它们。它们显示哪些形状值得剖析,
而不是证明不存在更好的 kernel。数学报告中的 10× 同工作量吞吐预算仍是理论
要求,而不是实测的设备能力。
复现与发布决定
脚本使用现有的 .venv 和本地锁定版本的 checkpoint。按顺序运行 GPU 命令,
绝不要与正式基准并行:
# Screening: repeat for eager, compiled, blocks, q8, q4, selected-compiled.
.venv/bin/python -m experiments.engineering.run_variants \
--model laya --variant compiled --iterations 12 --warmup 4 --quality \
--output experiments/engineering/reproduced-compiled.json
# Primary confirmation, including 50 genuinely different questions.
.venv/bin/python -m experiments.engineering.paired \
--model laya --iterations 32 \
--output experiments/engineering/reproduced-laya-paired.json
.venv/bin/python -m experiments.engineering.paired \
--model laya-multilingual --iterations 16 --cases short1,short16,long1,long8 \
--output experiments/engineering/reproduced-multilingual-paired.json
# Hand-written kernel microbench and complete-model comparison.
.venv/bin/python -m experiments.engineering.microbench
.venv/bin/python -m experiments.engineering.paired \
--model laya --iterations 16 --cases short1,short16,long1 --metal \
--output experiments/engineering/reproduced-metal-paired.json
.venv/bin/python -m experiments.engineering.run_variants \
--model laya --variant metal-compiled --iterations 5 --warmup 3 \
--cases short1 --quality --output experiments/engineering/reproduced-metal-quality.json
# CPU-only paired analysis.
.venv/bin/python -m experiments.engineering.analyze
所有实验性 Python 文件都通过 Ruff 格式化和 lint 检查。稳定的运行时、原始 基准结果和已发布的 FP16 checkpoint 仍是发布工件。在冷形状/缓存策略和更 广泛的质量验证之后,编译和精确的末头裁剪是可信的可选的未来优化;实测 收益不足以证明悄悄把编译延迟或自定义 kernel 加入默认路径是合理的。没有 声称 10× 加速、可用于生产的量化 checkpoint,或实测的局部窗口 kernel 收益。