ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

苹果M3神经引擎带宽受限:拆分传输可从17 - 19GB/s恢复至约60GB/s!

苹果M3神经引擎带宽受限:拆分传输可从17 - 19GB/s恢复至约60GB/s! 引言苹果M3神经引擎ANE存在一个RTL性能错误当总权重大小为1 MiB的整数倍时DRAM权重流式传输吞吐量会从标称的45 - 60 GB/s降至17 - 19 GB/s目前这影响了[ANEMLL](https://github.com/anemll/anemll) 15个模型中的7个。通过避开内核DMA引擎推测预取环中的问题路径Llama 3.2 1B的令牌吞吐量从10.0提升到24.3令牌/秒DRAM使用率从24.7提升到60.0 GB/sQwen3 - 8B从1.36提升到2.97令牌/秒DRAM使用率从22.4提升到48.7 GB/s。发现在对神经引擎单令牌解码的DRAM权重流式传输吞吐量GB/s进行分析时使用了公式\[ X[1,D] \times W[D,N] Y[1,N]. \]当\(N 4096\)时注意到\(D 1536\)的运行速度几乎比Llama 3.2默认的\(D 2048\)快3倍。静态纯KernelDMA每个副本的中位微秒数N 4096| D | 576 | 768 | 1024 | 1280 | 1536 | 2048 || --- | --- | --- | --- | --- | --- | --- || rep a | 150.4 | 196.8 | 238.1 | 293.8 | 310.9 | 997.6 || rep b | 157.8 | 190.2 | 250.1 | 288.3 | 326.8 | 995.0 || rep c | 148.7 | 189.6 | 249.4 | 275.2 | 316.5 | 995.4 |在D 2048附近对D进行扫描当D 2048时吞吐量为16.93 GB/s当D 2016时吞吐量为44.5 GB/s这意味着吞吐量下降了27.57 GB/s从44.5降至16.93 GB/s降低比例达61.96%。这些扫描数据是在M3 Air上收集的在一次运行中相同的温度和负载条件下重复进行了40次。还确保ANE寄存器文件的DMA大小和地址是唯一变化的变量。于是对整个D范围进行了扫描发现D 2048处存在共振现象。对吞吐量GB/s和张量维度D进行快速傅里叶变换FFT后发现内存控制器的吞吐量在张量维度空间中存在一个波长为2048的主导谐波且是一个低谷。所有D 2048的倍数都被限制在17 - 19 GB/s的固定带宽下限。在D 2048的倍数处吞吐量从标称的45 - 60 GB/s急剧下降到17 - 19 GB/s而在大约256行之外又恢复到标称值。这不是一个RTL正确性错误但在2048附近的请求被强制进入一个单独的、缺乏信用的调度机制导致吞吐量不合理地降低了28 - 43 GB/s。假设1 - DRAM空间相关性核心竞争神经引擎有核心级并行ANE有16个核心并行工作通过将缓冲区均匀地划分为\(N\)份来分配工作。每个核心会从DRAM中获取权重缓冲区的不同部分但16个核心会在同一周期内并行地从DRAM中请求各自的部分。如果每个核心从DRAM中获取各自的部分那么无论启用一个核心还是全部16个核心流式传输延迟应该相同。然而如果由于核心竞争导致带宽下降那么在D 2048的受限制情况下减少核心数量反而可能提高吞吐量。对D 2048和D 2016的活动核心数量进行扫描后发现对于D 2016和D 2048从1个到16个活动核心的延迟保持不变这意味着即使只有1个核心吞吐量限制仍然存在。地址竞争即使排除了核心级竞争仍然怀疑由于2的幂次周期导致了DRAM竞争。像2048这样的2的幂次步长每次循环都会增加\(2^k\)这意味着低\([0..k - 1]\)位是恒定的。DRAM会对物理地址进行哈希处理以便让步长访问模式在不同存储体之间实现空间去相关。因此哈希函数将低位比特折叠或使高位\(2^k\)比特重叠都可能解释这种2的幂次周期性。为了测试DRAM空间相关性是否是问题所在对权重获取的地址进行了随机打乱处理。地址被随机打乱并分布在整个约64 MiB的IOVA区域跨度为59.90 MiB这样在页面内和页面外都实现了打乱。为了排除无风扇M3 Air上的热漂移影响基线样本和打乱样本在每次运行中交替进行。基线的中位吞吐量为31.37 GB/s随机打乱地址后的中位吞吐量为32.29 GB/s。随机打乱后的持续吞吐量平均高出1 GB/s这表明在这次运行中可能通过打乱处理解决了一些空间相关性问题但这在所有情况下尚未得到证明且打乱处理无法恢复约200%的吞吐量下降不足以解释性能崩溃的原因。假设2 - RTL整数溢出内核维度公式\[ X[1,D] \times W[D,N] Y[1,N]. \]中\(D\)Cin表示每个内核的长度包含\(D\)个FP162字节权重即\(2D\)字节\(N\)Cout表示内核的数量每个核心处理\(N/16\)个内核。由于最初的图表是在N固定为4096的情况下对D进行扫描的实际上从未确定这个低谷是由\(D\)引起的还是由\(D\)和\(N\)的乘积引起的这个乘积决定了每个核心在整个任务中必须处理的内核总字节数。为了分离未知变量对\(D\)和\(N\)进行反向扫描使得编译后的任务每个核心都有相同的1 MiB静态内核数据。执行的寄存器文件的十六进制差异显示只有相关字段地址、大小发生了变化。任何D和N的组合只要每个核心的内核总字节数为1 MiB都会使核心吞吐量下降到观察到的17 GB/s。鉴于每个核心的常驻“L1” KMem为64 KiB现在知道在1 MiB上存在某种推测预取/信用机制。相反现在知道可以通过不传输1 MiB的倍数来避免性能崩溃编译器可以通过拆分任何编译后每个核心正好为1 MiB内核DMA的任务来解决这个问题。推测预取高带宽内存控制器以最小传输“行”粒度而不是单字节进行操作。如果内核DMA的行粒度是64字节当N 4096时每增加D 2048请求的总字节数就会增加1 MiB1 MiB的传输请求总共需要\(\texttt{0x4000}\)个64字节的行。现在推测在\(\texttt{0x4000}\)行处会发生计数器溢出。在\(\texttt{0x4000}\)或\(2^{14}\)处发生的溢出需要14位的存储空间苹果Silicon使用的16 KiB虚拟内存页面大小就是\(2^{14}\)字节。对于16 KiB的页面地址位\([13:0]\)是页面偏移量在虚拟到物理地址的转换中保持不变。对低地址位addr 0x3fff进行的预取算术运算会导致14位溢出。定义\(k\)为传输跨越的\(\texttt{0x4000}\)行周期数称为一圈定义\(x\)为距离第\(k\)个低谷的行数。以\(x\)为归一化参数V形低谷在每个\(\texttt{0x4000}\)行周期的低谷周围\(x \pm256\)行处完全恢复。低谷正好出现在一个虚拟内存页面大小的DMA行数内这看起来像是一个深度为一个页面的预取窗口。手动叠加\(D 2048\)\(k 1\)和\(D 4096\)\(k 2\)的带宽曲线当以低谷为中心重新对齐时会得到几乎相同的带宽曲线。现在绘制每个\(k\)在\(x 0, 32, 64, 128, 256\)处的带宽图可以注意到每个\(k\)曲线在\(x \to 0\)时向内汇聚。每个\(k\)圈的时间曲线实际上是第1圈曲线的垂直缩放版本第6圈的曲线比第1圈大约陡6倍。在以\(k\cdot\texttt{0x4000}\)为中心重新对齐每个低谷后所有\(k\)的带宽曲线都汇聚到同一条线上。在测量数据中每圈的斜率确实与\(k\)呈线性关系每个的R² 0.96 - 0.99斜率为\( 3.18 \cdot k\) 微秒/行。综合这些观察结果可以得出以下结论受限制的传输“模式”每\(k\)次重复一次每个周期内的带宽模式主要由相对于中心的位移\(x\)决定当\(x 0\)时无论\(k\)的值是多少传输速率都会下降到相同的\(B(0)\)。所以\(x\)选择带宽状态\(k\)决定该状态重复的次数。预取环前瞻每次请求一个1 MiB的环。该环将总传输大小视为\(k\)个重复的1 MiB池进行获取。如果每个1 MiB预取有某个带宽曲线\(B(x)\)那么总传输时间为\(t(k,x) \frac{S(k,x)}{B(x)} \frac{k\cdot1\ \text{MiB}64x}{B(x)}\)。关于\(x\)的斜率为\(\frac{\partial t(k,x)}{\partial x} \approx -k\cdot1\ \text{MiB}\ \frac{B(x)}{B(x)^2}\)对于较小的\(x\)值256斜率主要由\(k\)决定。每圈16 MiB的情况下18 GB/s的下限对应每圈900微秒。在256行内从下限约18 GB/s恢复到上限约45 GB/s每圈时间大约减半平均为(900 - 380)/256 2微秒/行。如果在边界附近更陡峭约3.2那么每圈每线3.18微秒的斜率是合理的。可能的RTL错误最有可能的是内核DMA中的推测预取环存在问题其14位的头/尾地址算术运算省略了一个溢出/周期位。因此当传输大小恰好是\(2^{14} \texttt{0x4000}\)的整数倍时会将“还剩一圈”错误地识别为“已完成”从而导致预取请求管道饥饿。推测路径无法在数据消耗之前发出足够的请求因此虽然数据仍然可以完成传输但会将原本应该是带宽受限的流式传输变成走走停停的传输。苹果Silicon的16 KiB页面\(2^{14}\)字节使得使用14位地址进行操作很有吸引力但14位算术运算会使每\(k\cdot\texttt{0x4000}\)的间隔产生重叠这意味着相隔0x4000的传输会产生相同的内部环状态。实际上0x4000行的周期性可以用14位行指针溢出算术来解释。由于没有周期位来识别是“完整一圈”而不是“已完成”预取器在整个0x4000行的传输中不会发出前瞻请求导致整个传输被限制在17 - 19 GB/s的慢速非推测路径上这可能是串行路径。对最多一个页面进行前瞻预取是合理的64字节粒度的行指针的256行对应一个16 KiB的页面这解释了为什么低谷在256行或一个页面的窗口内能够完全恢复。软件修复修复方法是不要请求1 MiB的内核传输。最简单的软件解决方法是找出所有总大小为1 MiB的kernelDMA任务并将1 MiB拆分为非1 MiB倍数的块例如两个512 KiB的传输。调度N个任务带来的亚毫秒级延迟开销与灾难性的30 GB/s - 50 GB/s预取限制相比可以忽略不计。另一个选择是对传输进行填充使其距离1 MiB /- 8 KiB256行但这需要更多工作因为它会改变计算图。将每个核心1 MiB的传输进行拆分可以恢复正常带宽一个0x4000行的任务为17.25 GB/s两个0x2000行的任务为45.52 GB/s快2.66倍四个0x1000行的任务为44.83 GB/s快2.60倍。对照实验证实2.6倍的加速直接来自于避免预取错误。结果DRAM吞吐量所有1 MiB倍数的原始吞吐量都被限制在17 - 19 GB/s而拆分为512k块的版本则能顺利提升到标称的约60 GB/s。| D | 传输大小 | 原始未拆分 | 修复后拆分 | 加速比 || --- | --- | --- | --- | --- || 2048 | 1 MiB | 17.3 GB/s | 43.5 GB/s | 2.51× || 4096 | 2 MiB | 18.4 GB/s | 52.1 GB/s | 2.84× || 8192 | 4 MiB | 18.8 GB/s | 57.8 GB/s | 3.07× || 12288 | 6 MiB | 19.0 GB/s | 59.8 GB/s | 3.15× || 16384 | 8 MiB | 19.1 GB/s | 60.5 GB/s | 3.16× |大语言模型性能受影响的模型见| 模型 | 映射层 | Cin × Cout | kMiB/通道 | 拆分数量S | 拆分后MiB/通道 || --- | --- | --- | --- | --- | --- || Llama 3.2 1B | gate/up/down | 2048 × 8192 | 2 | 4 | 0.5 || Llama 3.1 8B / DeepSeek / DeepHermes 8B | q, o | 4096² | 2 | 4 | 0.5 || | gate/up/down | 4096 × 14336 | 7 | 2 | 3.5 || DeepHermes 3B | gate/up/down | 3072 × 8192 | 3 | 2 | 1.5 || Qwen3 - 8B | q, o | 4096² | 2 | 4 | 0.5 || | gate/up/down | 4096 × 12288 | 6 | 4 | 1.5 || Gemma 3 4B | lm_head shard | 2560 × 16384 | 5 | 2 | 2.5 |Llama 3.2 1B从10令牌/秒提升到24令牌/秒。将其表示为两个部分归约这样编译器会发出两个0x2000行的KernelDMA任务而不是将它们融合回一个0x4000行的任务。例如Llama补丁拆分了MLP的三个1×1卷积。Qwen3 - 8B从1.36令牌/秒提升到2.97令牌/秒。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进