@akshay_pachaar: https://x.com/akshay_pachaar/status/2108252095380140203

X AI KOLs Timeline 工具

摘要

文章介绍开源推理引擎 Uzu(Mirai 出品),通过最多提前草拟 16 个 token 的推测解码与为 Apple M5 优化的 kernel,在 M5 芯片上运行 4-bit Qwen3.5 9B 达到 92-117 tokens/s,远超 llama.cpp 与 MLX 的 22-25 tokens/s,并解释了 batch-one 解码的内存带宽瓶颈原理。

https://t.co/5VrwQJ2JO7
查看原文
查看缓存全文

缓存时间: 2026/10/09 08:23

Karpathy 加速本地大模型的技巧,终于有了真正的解决方案

2023 年,Karpathy 解释了本地大模型为何缓慢。当模型在你自己的电脑上生成一段文本时,芯片大部分时间都在等待权重从内存中传输过来,而计算单元则处于闲置状态。

几周后,他描述了绕过这一瓶颈的方法:一次处理多个 token 的成本与处理一个几乎相同,因此可以用一个小草稿模型提前猜测,再由主模型一起验证这些猜测。

第一点在今天依然决定了性能上限。基础款 Apple M5 芯片每秒可传输 153 GB 数据,而 4-bit Qwen3.5 9B 模型约重 5.2 GB,因此每次处理只生成一个 token 的情况下,该芯片的上限约为每秒 29 个 token。

llama.cpp 和 MLX 都紧贴这条上限运行,在相同的编码提示词下分别达到 22 和 25 tokens/s。

第二点才是优化空间所在,但 llama.cpp 的 MTP 模式等本地方案每次只草拟三到四个 token。

Uzu 是由 Mirai 推出的开源推理引擎,在 Apple Silicon 上将这一方法推向了更远。它最多可提前草拟 16 个 token,保持这些猜测之间的连贯性,并在为 M5 编写的内核上一次性完成验证。

在同一台 M5、使用相同模型和提示词的情况下,Uzu 达到了 92 到 117 tokens/s 的速度。

下面我们将剖析性能上限的来源、Uzu 如何突破它,然后使用一个可以指向你自己提示词的基准测试客户端,将 Uzu 与 llama.cpp 和 MLX 进行对比。

为什么批量大小为 1 的解码受内存带宽限制

LLM 推理有两个不同的阶段。

  • **Prefill(预填充)**并行处理提示词中的所有 token。

  • **Decode(解码)**逐个生成响应 token。

解码是与本文相关的阶段,因为每生成一个新 token 都需要再运行一次模型。在每次运行中,运行时读取权重块并计算下一个 token 的分数。

在批量大小为 1 时,一次前向传播只推进一条序列。

相对于所传输的权重数据量,计算量非常小。为了生成一个 token,芯片需要将模型的所有权重从内存流式传输到 GPU 一次,并对每个权重执行大约一次乘加运算。

GPU 在内存送来下一个权重块之前就能完成这些计算。因此,计算速度往往受内存带宽瓶颈限制。

这带来了一个硬性上限。基础款 M5 每秒可从内存传输 153 GB 数据。4-bit Qwen3.5 9B 检查点约重 5.2 GB。如果每次前向传播只产生一个 token,那么该芯片的最大写入速度为 153 ÷ 5.2 ≈ 29 tokens/s,无论 GPU 计算多快。

实际引擎略低于这条上限,因为每次前向传播还会读取其他数据,例如先前 token 的缓存。

量化是提升这一指标的标准技术,通过使用更小的权重来减少每次前向传播的读取字节数。

然而,量化优化的仍然是同一个单 token 循环。主模型必须为每个生成的 token 重新运行。

实际上,量化已经相当普遍了。那么接下来就来了解 Uzu 作为推理引擎的独特之处。

Uzu 如何优化每次前向传播的字节数和 token 数

Uzu 针对解码循环中的两个量进行优化:减少每次前向传播传输的字节数,增加每次前向传播被接受的 token 数。

  • 量化负责第一个量。它用更少的比特位存储模型权重。更小的检查点需要更少的内存传输,并为上下文和应用状态腾出更多统一内存。

  • 投机解码负责第二个量。它同时验证多个候选 token。如果多个候选被接受,序列就前进多个 token。

许多本地引擎已支持 4 位权重。Uzu 的差异化在于围绕该格式的硬件路径。

Mirai 为同一基础模型发布多个优化检查点,并按大小层级进行标注。M 代表 Medium(中等),L 代表 Large(大)。

这些标签描述的是检查点,而非不同的模型架构。

  • Medium 检查点使用 4-bit 非对称整数量化。

  • Large 检查点使用 8-bit 对称量化。

Medium 偏向更小的内存占用,Large 则保留更多权重比特位。Mirai 的量化技术文档描述了它如何将训练后量化与量化感知蒸馏相结合。

使用整数格式是有意为之,因为这同时有助于硬件加速和压缩。

更具体地说,与旧款 Apple GPU 不同,M5 在 int8 下达到最高的算术吞吐量。

因此 Uzu 还将激活值(层与层之间传递的中间值)在线量化为 8 位。当矩阵乘法的两侧都以整数存储时,投机验证步骤就能在 M5 硬件加速的 int8 矩阵路径上运行。

更低的精度会带来另一个问题。

更具体地说,分组量化在某一组中存在异常值时会产生舍入误差。

同一组中的所有值共享一个缩放系数。该缩放系数必须表示最大的幅值。较小的值因此使用更少的整数层级,产生更多的舍入误差。

Uzu 在量化前应用了块对角随机 Hadamard 变换。Hadamard 矩阵只包含 +1 和 -1 元素,外加一个整体缩放系数。

用它乘以一组权重,会将每个值混合到每个位置,因此单个大幅值的异常值会变成多个中等大小的值。

“随机“指的是在混合之前应用的随机符号翻转,“块对角“意味着每个由 32 个值组成的块独立进行混合。该变换是精确可逆的,因此除了舍入误差外,不会改变模型的计算结果。

Mirai 选择 32 个元素是为了匹配 Apple GPU 内核的 SIMD 宽度。SIMD 意味着一条指令同时在 32 个 GPU 线程上运行,因此一组线程就可以变换一个块。

变换块和量化组控制的是不同的操作。

  • 变换块定义 Hadamard 矩阵混合哪些值。

  • 量化组定义哪些值共享量化参数。

这些大小不需要相同。

Uzu 的差异化在于数据格式、变换和运行时内核的联合设计。量化减少了每次模型前向传播内的内存传输和计算。

然而,普通的解码在每次目标模型前向传播后仍然只返回一个 token。投机解码是它用来改善这一瓶颈的下一个杠杆。

一次模型前向传播产出多个 token

投机解码在主模型旁边添加了一个小草稿模型。

草稿(小)模型以低成本提出未来的 token。主模型一起检查这些位置。它接受有效的前缀,并从第一个被拒绝的 token 处恢复。

假设草稿模型提出五个 token,主模型认同前四个。这一次验证就将序列推进了五个 token:四个被接受的草稿加上主模型在第五个位置自己生成的 token。

普通解码需要五次独立的主模型前向传播才能生成相同的文本。

主模型仍然控制最终输出。草稿模型只提出可以被并行检查的 token。正确的接受过程会保持目标模型的输出分布不变。

验证步骤需要比单 token 解码更多的计算量。

但由于本地批量大小为 1 的推理中,算力往往在权重经过内存传输时处于闲置状态,因此这条路径仍然更优。

投机解码利用这些闲置的算力来减少主模型前向传播的次数。

这种权衡取决于接受率。更长的草稿会创建更大的验证批次,但也会增加早期错误导致后续 token 无效的机会。

许多模型原生系统每次草拟三到四个 token。一个常见例子是多 token 预测(MTP),即模型内部额外训练的预测头来猜测接下来的几个 token。

Uzu 在其 M5 路径上使用 16 到 32 个 token 的预算。本文后面使用的 Qwen3.5 9B 检查点为 M5 系列芯片提供了 16 token 的树形预算。

更大的批次为 M5 的 Neural Accelerators 提供了足够的工作量。这些是内置于每个 GPU 核心的矩阵单元,只有当矩阵乘法有足够多的行时才能发挥作用。同时检查 16 个候选 token 给了它们 16 行的工作量,而不是 1 行。

更长的并行草稿引入了一个连贯性问题。

并行草稿生成的连贯性问题

快速并行草稿模型同时预测多个未来位置。

正如预期的那样,每个位置都能看到现有上下文,但看不到在更早草稿位置上选定的 token。

考虑一个以“She went to the store to buy.“(她去商店买_____。)结尾的提示词。独立的位置可能分别倾向于“a”、“few”、“of“和“milk”。每个 token 单独看起来都很合理,但组合在一起就成了“a few of milk“(一些牛奶)。

早期的不匹配会拒绝整个后续链。这就是为什么随着并行草稿变长,接受率往往会下降。更多的位置意味着更多的机会让单独看似合理的 token 组合成无效的序列。

Trees from Marginals 论文提出了 Weaver 来修复这一弱点。在论文的研究设置中,Weaver 是一个 5670 万参数的自回归适配器。它在候选短列表上运行,而不是在整个词汇表上进行预测。

一个并行的 DFlash 草稿模型首先一次性预测每个未来位置的候选 token。然后 Weaver 按顺序从这些候选中选择。每次选择都可以依赖之前的选择。

之前的例子现在可以变成“a gallon of milk“(一加仑牛奶)。后续的选择遵循已选定的路径。

Weaver 还可以保留多条连贯的路径。一个分支可能包含“a gallon of milk“,另一个分支可能包含“a bottle of juice“(一瓶果汁)。主模型验证整棵候选树并接受最长的有效路径。

树形结构可以根据提示词自适应。可预测的代码可以支持长而窄的分支。开放式写作可能需要更短但更多样的分支。

论文报告了相比优化后的 DFlash 基线有 24.7% 的提升,相比自回归解码有 4.37 倍的加速。这些实验使用了 CUDA 和 SGLang。它们是研究结果,不是通用的 Uzu 测量数据。

Uzu 将相同的草稿理念应用于其面向 Apple 的运行时。剩下的问题是如何在目标模型上高效地验证候选树。

无需恢复模型状态即可验证草稿树

某些模型层维护着运行中的内部状态。每个新 token 都会读取该状态并更新它。对于单条序列,运行时按顺序应用这些更新。

草稿树会产生多条可能的后续序列。每个分支都会以不同方式更新状态。如果运行时在检查一个分支时更新了实时状态,就必须在检查另一个分支前恢复之前的状态。在大型树上反复执行这一过程会增加内存操作和重复工作。

Qwen3.5 和 Qwen3.6 包含一种称为 Gated DeltaNet 层的有状态层。注意力层维护着不断增长的过去 token 缓存,而 Gated DeltaNet 层则将其看到的所有内容压缩为固定大小的状态,每个被接受的 token 都会改变该状态。

在 Qwen3.5 9B 中,32 层中有 24 层属于这一类型。

Uzu 将检查分支与提交状态分离开来。在验证过程中,它计算每条候选分支所需的值,但不改变实时状态。主模型随后选定被接受的路径。Uzu 仅为被接受的 token 应用状态更新。

Uzu 实现说明称之为无回滚树验证。其 Metal 内核在单次无递归的前向传播中计算整棵树的 DeltaNet 输出,而不是逐分支逐 token 地遍历。

状态仅沿选定的路径提交,因此永远不会出现需要回滚的错误更新。

这一点很重要,因为投机解码只有在验证成本低于普通逐 token 解码时才能发挥优势。反复的状态恢复会消耗掉部分收益。

Weaver 和无回滚验证的结合,使 Uzu 能够提出长 token 序列并在不反复恢复模型状态的情况下进行验证。

在基础款 M5 上的表现

为了观察普通解码、传统投机解码与 Uzu 方法之间的差异,我们在一台搭载基础款 M5 芯片和 16 GB 统一内存的 MacBook Pro 上,以三种方式运行了同一个 Qwen3.5 9B 模型:

  • llama.cpp,不使用投机解码

  • llama.cpp,使用模型内置的 MTP 头提前草拟 3 个 token(传统方案)

  • Uzu,使用 DFlash 和 Weaver

三者都使用 4-bit 权重,提示词相同:“Write a python function to merge two sorted lists.”(编写一个 Python 函数来合并两个已排序的列表。)

已发布的基准测试对比

目前已有若干已发布的 Uzu 基准测试,涵盖输出速度、输入速度、内存占用和量化质量。

以下投机解码结果使用的是 Qwen3.6 27B Mirai-M。该检查点占用 15.6 GB。Mirai 在搭载 128 GB 内存的 M5 Max 上进行了测量。

这些数据表明 Uzu 达到了:

  • MTPLX 的 2.1 倍

  • llama.cpp 的 3.8 倍

  • MLX 的 4.4 倍

此外,Uzu 在不使用投机解码的情况下达到 35.54 tokens/s,使用投机解码时达到 114 tokens/s。投机路径带来了 3.2 倍的提升。

还有按提示词类型分类的输出生成速度数据。

代码和数学包含大量可预测的延续内容。开放式对话给草稿模型提供的可靠 token 较少。因此投机解码的速度是工作负载的特性,而非固定的模型参数。

在本地运行模型并查看运行时状态

验证 Uzu 最快的方式是使用其命令行工具。它可以通过 Homebrew 安装,并在首次使用时下载所选模型。

我们在一台搭载基础款 M5 芯片和 16 GB 统一内存的 MacBook Pro 上,使用 Mirai CLI 0.6.1 和 macOS 26.6.2 进行了此操作。首次运行会下载模型。

这是一次实时运行:

回答完成后,CLI 打印了以下统计数据:

这些数字的含义如下:

  • 首 token 延迟(Time to first token):从请求发出到第一个回答 token 出现的等待时间。大部分时间用于读取提示词。

  • Prefill 速度:引擎读取提示词的速度,单位为提示词 tokens/s。我们的提示词只有约 20 个 token,太短了,这个数字说明不了太多问题。它会随着更长的提示词而提升。

  • 生成速度:回答写出的速度,单位为输出 tokens/s。这就是前面提到的解码阶段,受内存上限制约的阶段。

  • 每次前向传播的 token 数(Tokens per forward pass):9B 模型每次前向传播平均产生了多少个输出 token。普通解码恰好为 1.00。

  • 内存占用(Memory used):引擎占用的统一内存,主要是模型权重加上草稿模型和缓存。

8.39 t/f 这个值表明投机路径正在生效。平均而言,9B 模型每次前向传播产生约 8 个 token。这就是为什么在单 token 上限约为 29 tokens/s 的芯片上,生成速度能达到 98 tokens/s。

不同运行之间速度会有小幅波动。在我们对该命令的多次运行中,生成速度在 92 到 117 tokens/s 之间,每次前向传播为 7.5 到 9.6 个 token。

投机解码是按芯片启用的。该检查点的投机配置列出了 M5、M5 Pro 和 M5 Max,因此在 M1 到 M4 的 Mac 上,相同命令会不带草稿模型运行,并报告 1.00 t/f。

CLI 提供了延迟、吞吐量、内存、能耗和投机解码数据。我们现在可以从应用程序中调用同一个运行时。

通过本地应用端点调用 Uzu

Uzu 可以暴露一个 OpenAI 兼容的 HTTP 端点。模型仍然在用户的 Mac 上运行,HTTP 请求只经过本地回环接口。

在终端中启动服务器:

服务器加载一个本地检查点并监听 127.0.0.1,同一台 Mac 上的进程即可调用它。这对于已经通过 OpenAI 聊天协议交互的现有应用程序非常有用。

默认情况下,Uzu 保留一个前缀缓存。它存储之前已处理过的提示词开头部分,因此重复的提示词可以跳过大部分预填充工作。--no-prefix-cache 可以关闭此功能,使每个测试请求都执行完整工作,保持多次运行之间的可比性。

现有应用程序可以通过将 OpenAI

相似文章

@akshay_pachaar:投机解码详解(用得好,LLM 推理速度可提升 4 倍)本地 LLM 通常每次……

X AI KOLs Timeline

这篇帖子通俗解释了投机解码(Speculative Decoding)的工作原理——草稿模型预测、目标模型并行验证并以概率方式接受或纠正——并介绍了开源引擎 Uzu,它在 Apple Silicon 上结合并行起草、连贯草稿路径与免回滚验证,使 4-bit Qwen3.5 9B 的推理速度从 llama.cpp 的 22 tokens/s 提升到 92–117 tokens/s。

@servasyy_ai: https://x.com/servasyy_ai/status/2106563331976991217

X AI KOLs Timeline

这篇长文实测用两张二手 V100 32G 跑 Qwen3.8-27B,通过重写推理引擎 NInfer 的内核、加入双卡支持和推测解码,让 20 万 token 代码任务的生成速度从 36 tok/s 提升到约 100 tok/s,跑赢了单张 4090 48G 的 80.9 tok/s,同时分析了显存带宽、量化与 NVLink 对推理性能的影响。