一个iPhone用10Gb/s的USB-C线挂在macbook旁边给本地大模型加速,第一眼看上去是种华而不实的geek toy。手机那点计算能力+一根物理线带来的通信延迟很容易杀鸡用牛刀。
我一开始看这个开源项目 backburner,也是这么想的。看了代码之后才发现很多人误解了。作者并不是想在手机上运行大模型。他始终在 Mac 上进行计算,并没有在手机上进行整个推理。
在这个系统中,MacBook Pro 是一个对外提供服务的宿主,上面跑着一个普通的本地服务端。比如一台 24 GB 内存的 Mac,读到长文本的时候内存吃紧,那部用线缆连着的 iPhone 就充当挂在线另一头的外部内存池和协处理器。它负责两个环节:一是在 prefill 阶段(读入长文本时)分担一部分网络层,二是在 Mac 装不下超长上下文时接管溢出的历史键值缓存。这两个概念你可能不陌生:prefill 是把整段输入一次读完,decode 是之后一个一个往外吐词。手机在这两个阶段里的角色完全不同,后面会看到。
即便你手边没有 iPhone 或内存小一点的电脑,这依然是一个有意思的场景。它描述的是许多人都可能遇到的困境:物理资源受限,但计算任务还要继续。长文本填满了主机的显存,外接设备的算力并不对称,二者仅以一根线相连。在这种限制下,下一步该怎么走?跳出具体机型,这种在物理约束中做取舍的思路,对多卡并行、分布式推理乃至边缘设备协作都同样适用:手头的资源有限,可任务必须完成,突破口在哪里?
人的本能会先去优化这根线——测通信带宽够不够,压协议延迟,把要发的数据高比压缩。但这样容易让人误以为只要数据跑得够快,两边的计算就能自然对接上。
干过分布式的人都知道,这里面有个最大坑。两台机器能不能一起做一件事,决定因素不是网线跑多快,而是你要交给它们做的计算有没有办法在数学上独立求解、最后还能无损合并。网线带宽只是第二步的事。
如果一个算子切分后仍要求大量的跨设备数据依赖,两头就需要同步中间状态。这种情况下即便线缆延时很低,往返的等待时间也可能抵消多设备分担算力带来的好处。这种算子不适合这里的切分方式。
在这个系统里,被选出来分流的计算是注意力。这是因为注意力有一个数学性质,就是它天然就可以做局部独立的计算。
当模型的全注意力层遇到长上下文时,它需要把当前的查询向量和历史上所有出现过的键做点积,再用注意力权重对值求加权和。文本越长,历史键值缓存就越大,最终会超出单机的内存容量。这时系统就让 Mac 把较早的那批键值缓存整体搬到手机上去托管,但前馈层和模型权重还是在 Mac 本地。
在进行注意力计算的过程中,Mac负责将当前的查询向量通过数据线发送给手机。手机则仅对其所存储的历史键值页进行局部注意力计算,得到一个局部结果,包括局部输出向量、局部最大值以及指数和。随后,手机将归一化后的局部输出向量和由最大值、指数和算出的对数归一化量返回给Mac。Mac利用对数求和指数公式,将手机的局部结果与本地的结果进行合并。
所以这样的分割,得到的结果和之前是一样的。从数学上讲,局部注意力合并起来是完全等价的,这种分割没有额外引入有损量化或者近似截断。在默认模式的贪婪解码测试下可以看到,生成的每一个词元都和单机逐字的结果完全一致。要想让两台设备合作,先把数学上能拆开的计算挑出来,比到处找更快的传输线实用多了。
一旦确认了注意力算子可切分,下一步自然的直觉就是按两台设备的算力大小来分配计算量——主机强就多给几层,副机弱就少给几层,只要两边时间对上,吞吐就稳。
但一旦真的去切模型,往往会发现这样切非常不work。因为很多模型里面不同的层记东西的方法是不一样的。
比如这个项目用的 Qwen3.8-27B。它的大部分层不存每个历史词,而是记一个固定大小的摘要,新词来了滚动更新一下,前面信息被揉进摘要。这样省内存,但摘要就是一坨,没法把前半段历史抽出来给另一台机器,因为不再分了。摘要还往前滚,想回头只能从头算。
而另外的少数层用的另一种方法,给每个历史词单独存一行,攒成一张随上下文变长的表。这个表的每一行彼此独立,最早的几行可以整片挪走。这两类层在整个网络里交替出现:连续 4 层里,前 3 层是上面那种摘要层,第 4 层才是这种存表的层,然后 4 层一循环,一直到模型末尾。
这直接决定手机能干啥。第一种层只有一个整块的摘要,要给手机用只能把整层挪过去,没法按位切。第二种层有一张表,可以把表里最老的几行挪到手机内存里留着,位置随便切。于是系统的整层切分点必须对齐每 4 层一次的循环边界,顺着数据自己的接缝来。能切哪儿,受这两类层的排列约束,具体切在哪个边界仍由工程师选择。
切好之后,在默认配置不超过 64k 上下文的 split prefill 中,Mac 跑前 40 层,把中间结果扔到手机继续算后面的 24 层;每批最后一个 ubatch 则全部留在 Mac 上跑。看到这儿容易犯嘀咕:Mac 先算完 40 层再交给手机,这不还是你先我后,能快在哪?诀窍是流水线。长输入本来就切成一批批小块送进来,Mac 算完一块的前 40 层,不等手机,把中间结果发过去就转头去算下一块的头 40 层。手机还在啃第一块的尾巴,Mac 已经进了第二块。两台设备在同一段时间里各忙各的,前后依赖被流水线盖住一部分,等待就减少了。
做完状态切分,系统立刻被另一个端侧推理的常见瓶颈卡住,它在 decode 阶段。在全注意力层,每个词元出来,芯片都要读取之前的键值缓存,越长越贵,内存带宽就可能成为瓶颈。
对这个瓶颈,一般的思路是优化缓存的布局,或者对内存传输做一些小改动。但仔细看自回归推理的过程,就会发现一个容易被忽视的情况:那些几十步之前生成的历史键值,在整个 decode 中只会被频繁读取而不会被修改。既然这些数据已经固定,还一直把它们当动态数据从内存里读,就非常不划算。
开发团队基于这个洞察换了一种方案。手机里有个 Apple 神经引擎(ANE),它可以运行以固定权重做矩阵乘法的模型。于是系统把副设备持有的历史键值页,一页一页地直接编译成一个 ANE 模型文件,这些页面在 prefill 阶段就陆续在后台编译,详见 神经引擎实现文档。编译好的模型里,原本在内存里的键值就直接固化成了静态权重。
等到 decode 时主机发来新的查询向量,副设备把查询向量作为输入送进这个 ANE 模型,由它计算已编译页面的局部注意力结果。查询向量每步更新,已编译页面的历史键值作为模型权重保持不变,并不是不再需要内存访问。
这个把历史缓存搬上手机、并且用手机分担旧上下文的注意力机制,在项目的设计说明里有更详细的解释。实测发现,在 A18 的旧页注意力算子测试中,这种把静态数据编译成硬件权重的做法比手机的 GPU 通用图形处理器快 2-3 倍。页权重采用 fp16,没有采用精度不足的 int8 页权重;在 51k 真实会话测试中,33 个词元与精确路径全部一致。数据不变,就把它交给硬件当权重去算,旧页注意力计算就这么得到加速了。
即使确定了数据的表示形式和算子的划分方法,两个设备之间仍然需要通过一条链路进行通信。主机设备计算速度快,但副设备相对较慢,这条链路的任何不必要延迟都会导致整个流水线的速度下降。
很多分布式系统遇到这种情况,就会在通信层不断加戏,堆调度协议、堆多路复用、堆重试机制,用更精巧的管道工程去应付窄带互联。这套系统反其道而行之:线窄,那就在架构上做减法,让绝大多数数据根本不用过线。
它在线缆两端做了三条减法。
首先,在手机端镜像自己那些网络层的键值缓存和循环状态,跨线同步的只有新增加的几行。
第二,层间只传残差。这里不传输层内中间张量,而是传输层间残差向量。
第三,在 split prefill 模式下,终点计算在本地。每批输入的最后一个 ubatch 强制在Mac上本地运行。这样,词表维度巨大的 logits 张量留在主机上,无需过线。
在大幅降低跨线数据量后,又加入了时间上的流水线:超过 64k 后,512-token ubatch 交错成两半,Mac 负责一半词元的全部网络层计算,手机同时计算另一半词元对旧键值的注意力。两者在时间上交错。这样在 140k 上下文测试中,prefill 阶段吞吐从 58 提升至 68 tok/s,约提升 17%;约三成的提升则来自 32k 和 48k 的 split prefill 测试。窄链路上,花时间搭管道收益有限,更管用的是靠本地闭环削减大部分跨线交互。
观察一个系统设计得怎么样,不能只看它实现了什么功能,还要看它在实验中发现并抛弃了什么。在纸上谈兵的时候,很容易构想一些看似合理的协作模式,比如 Mac 自己有 ANE,为什么不利用它在 decode 的时候也加入计算?手机里面有 GPU 也有 ANE,为什么不把两者都利用起来?但真正到了硬件上面,往往会发现这些设想是错误的。项目记录了几种经测量后禁用或搁置的方案:
早期他们尝试过用 Mac 自己的 ANE 来帮忙 decode,但测量后发现生成速度反而慢了 26%,因为 ANE 在频繁读写内存时和图形核心抢总线,拖慢了主计算通路。这个想法也被删。
手机端也试验过让图形核心和 ANE 一起跑 prefill。两者都忙起来会打架,ANE 的吞吐直接掉很多,两者合起来只有单方的 1.1-1.25 倍,所以方案被搁置了。
他们也尝试过在运行时动态学习 ANE 和图形核心的分片比例。但 ANE 空闲时会降频,探测请求的耗时忽高忽低,把在线调度逻辑搅乱了,最后还是换回了一个固定的静态常数。
除了去除反例之外,这个系统非常坦诚地说明了它的局限性。在默认模式下,对于 64k 以下的中短文本,它只依赖 Mac 进行 decode,因为此时副设备没有参与 decode,也就谈不上加速。而对于不超过约 512 个词元的轻量读取请求,则干脆不启用 split prefill,直接在主机上单机处理,以避免通信成本不划算。同时,该系统对使用场景也有要求,比如在移动操作系统中,由于系统不允许 app 在后台使用 GPU,因此如果手机参与计算的话需要前台亮屏。此外,整个系统目前也只支持单请求串行。
这整套设计真正解决的是从跑不起来到能稳定跑起来,而不是把常规推理速度翻番。一个 8GB 内存的主机装不下项目使用的 27B 模型,24 GB 主机在原有配置下也装不下超出容量的长文本缓存;用一根线把外部设备拉进来协同,这道门槛被推开了。把不可切分的工作挡在外面,顺着数据结构的接缝划分流水线,把凝固的数据转成前向算力,在窄链路上做足减法,这套在极端限制里逼出来的决策路径,即便拿掉线缆和具体设备,遇到资源瓶颈时照样能用。
| 评测维度 | 场景与配置基准 | 实测表现或测量基线 |
|---|---|---|
| 长文本预填吞吐提升 | 16k / 32k / 48k 上下文 | 单机 109 / 101 / 87 tok/s 提升至协同 157 / 130 / 113 tok/s |
| 冷启动大文本首字响应 | 27k 词元会话首次应答 | 原版 245 秒,单机优化 228 秒,接入协同加速降至 168 秒 |
| 专用硬件算子求值耗时 | 每 16k 键值页(4 个头) | 耗时 1.85 毫秒(折合每 1k 键值 0.113 毫秒),达通用核心 2-3 倍 |
| 超长上下文单步耗时 | 140k 极端深度逐字生成 | 纯通用计算 279 毫秒/词元,按层固化 2 页降至 208 毫秒,3 页降至 176 毫秒 |
| 物理线缆交互延迟基准 | 经手机 app 的 USB-C 16 字节请求/回复往返时间 | 中位数 96 微秒,90 分位值 183 微秒 |
| 极限受限场景从零到一 | 8 GB MacBook Neo + iPhone Air,以 split decode 运行 27B 模型 | 使原本装不下的模型能运行,实现协同解码 3.9 tok/s 与预填 64 tok/s |
现在大语言模型的架构正在变得越来越混合注意力,那系统状态里面可切分和不可切分的比例也在发生着变化,未来的模型会有更多定长的循环状态,更少的注意力层,但是每个注意力层的计算会更重,这又会给主从计算节点之间带来什么样的切分界限呢?