从一块矩阵乘法的算力账本说起我如何从零搭出一台能跑起来的AI加速器很多人第一次听到自己设计AI加速器这六个字第一反应是这不是芯片公司几百人团队干的事吗。我一开始也这么想直到我把这件事拆开看所谓AI加速器本质上就是一台专门为矩阵运算服务的计算设备它的核心任务只有一件事——把乘加运算做得又快又省。你不需要一上来就流片也不需要几千万的研发预算用FPGA、用现成的NPU开发板、甚至用软件模拟器都能把一台加速器的完整链路走通一遍。这篇文章就是把我从零设计一台AI加速器的完整思路摊开讲从算力需求怎么估、数据流怎么设计、矩阵单元怎么搭、存储层次怎么配一直到怎么验证它真的能跑起来。适合对计算机体系结构有兴趣、想搞明白NPU内部到底在干什么、又不想只停留在看论文层面的朋友。看完你至少能自己画出一张加速器的架构图并且知道每个模块为什么长那样。1. 先算清楚一笔账你的加速器到底要加速什么1.1 从矩阵乘法反推算力需求设计加速器的第一步不是画架构图而是算账。你得先知道目标 workload 的算力需求有多大否则后面所有决策都是拍脑袋。AI 模型里 90% 以上的计算量集中在矩阵乘法一个全连接层就是Y X·W b卷积层经过 im2col 展开后也是矩阵乘法。所以加速器的核心指标可以用一个公式概括算力需求OPs 2 × M × N × K × 层数 × batch_size这里的 2 是因为一次乘加算两次操作一次乘法、一次加法。M、N、K 是矩阵维度。举个具体例子一个 768×768 的权重矩阵输入 batch 是 1序列长度 512那么单层算力就是2 × 512 × 768 × 768 ≈ 6.04 亿次操作。一个 12 层的 Transformer 编码器光注意力部分的 QKV 投影加输出投影就是四倍这个量级再加上 FFN 里 768 到 3072 的升维和降维整体轻松上到百亿次操作级别。算这笔账的意义在于它直接决定了你的加速器需要多高的峰值算力进而决定了你并行多少个乘法单元、跑多高的时钟频率。如果你只是想在 FPGA 上跑一个小型 MLP那算力需求可能只有几亿次操作一块中端 FPGA 的 DSP 资源就够用但如果你想跑完整的 Transformer那就得认真考虑数据复用和存储带宽了因为这时候瓶颈往往不在算力而在数据搬运。1.2 算力、带宽、存储三个互相拉扯的指标新手最容易犯的错误是只盯着峰值算力看。我早期也是这样觉得把 MAC 单元堆得越多越好结果发现实际跑起来利用率只有百分之十几。原因很简单乘法单元再快数据喂不上来也是白搭。这就是经典的内存墙问题。这里有个经验公式可以帮你快速判断瓶颈在哪计算强度 总操作数 / 总数据搬运字节数如果一个 workload 的计算强度低于你硬件的拐点计算强度峰值算力除以峰值带宽那它就是带宽受限的堆再多算力也没用。以矩阵乘法为例如果每次从外部存储读一个数只做一次乘加计算强度就是 0.5 OPs/Byte这是典型的带宽受限场景。解决办法就是数据复用——把读进来的数据在片上多留一会儿反复用。这就是为什么所有 NPU 都有片上缓存和寄存器阵列本质都是在提高计算强度。所以设计加速器时我习惯先画一张数据流预算表把每一层的数据搬运量和计算量都列出来看看哪一层是带宽瓶颈哪一层是算力瓶颈。这张表后面会直接指导你决定片上缓存多大、用几级流水。指标典型瓶颈场景设计对策峰值算力大矩阵、高并行增加 MAC 阵列规模存储带宽小 batch、逐元素操作提高数据复用率、加片上缓存片上容量大模型权重权重分块加载、量化压缩时钟频率控制逻辑复杂流水线切分、降低关键路径1.3 明确边界你要做的是推理还是训练这个决策会彻底改变你的架构。推理加速器只需要前向计算数据流是单向的可以做极致的流水线和定点量化训练加速器需要反向传播和梯度更新对数值精度要求高通常得支持浮点架构复杂度高一个数量级。我建议从零开始的朋友一律先做推理加速器把前向链路跑通理解清楚数据怎么流动再考虑要不要碰训练。这不是偷懒而是因为推理加速器的每一个设计决策都更容易验证你能更快看到结果学习曲线也平缓得多。2. 数据流设计加速器架构的灵魂所在2.1 三种经典数据流权重固定、输出固定、行固定数据流决定了数据在计算阵列里怎么移动、在哪里被复用。这是加速器设计里最核心也最容易被忽视的部分。目前主流有三种权重固定Weight Stationary的思路是把权重锁在计算单元里不动让输入数据流过。适合权重矩阵大、需要反复使用的场景比如卷积层。缺点是权重加载阶段算力闲置。输出固定Output Stationary是把部分和留在计算单元里累加输入和权重都流动。适合输出维度大的场景能减少部分和的搬运。缺点是权重和输入的复用率相对低。行固定Row Stationary是前两者的折中在行方向和列方向分别做复用是很多现代 NPU 采用的方案。我实际做的时候选的是权重固定原因很直接我的目标模型权重矩阵大且固定推理时权重不变把它锁在片上能最大化复用。具体做法是每个 PE处理单元持有一列权重输入向量从左侧流入横向广播给所有 PE每个 PE 做乘加后把结果向下传给累加器。这样一次输入广播能同时服务一整行 PE复用率拉满。2.2 脉动阵列让数据像心跳一样流动脉动阵列Systolic Array是权重固定数据流的一种极致实现也是 TPU 的核心结构。它的精妙之处在于数据在阵列里一格一格地脉动传递每个 PE 只和相邻 PE 通信不需要全局广播布线简单、时钟容易跑高。我搭的是一个 8×8 的脉动阵列。工作过程是这样的权重预先加载到每个 PE 的寄存器里输入矩阵的行从左侧逐拍注入部分和从上往下逐拍传递。第 t 拍时第 i 行第 j 列的 PE 拿到的是输入矩阵第 i 行第 (t-i) 列的元素和它持有的权重相乘后加上从上方传来的部分和再往下传。整个矩阵乘法的结果会在若干拍后从底部流出。这个结构的实测好处是关键路径短只有一个乘法器加一个加法器在 FPGA 上能轻松跑到 200MHz 以上而且扩展性好8×8 不够就拼 16×16结构完全一样。坏处是填充开销——矩阵维度不是阵列尺寸整数倍时边缘会有大量无效计算。我的处理办法是把矩阵 padding 到整数倍虽然浪费一点算力但控制逻辑简单太多实测下来整体效率反而更高。2.3 双缓冲让计算和搬运重叠起来数据流设计里还有一个不能省的技巧双缓冲Double Buffering。如果计算单元算完一批数据才去搬下一批那搬运期间算力就是闲置的。双缓冲的做法是准备两块缓存一块在算的时候另一块在后台加载下一批数据算完直接切换。我在设计里用了两级双缓冲外部存储到片上缓存一级片上缓存到 PE 阵列一级。实测下来这个改动让整体利用率从不到 40% 提升到了 75% 以上。代价是多了一倍的片上缓存面积但对于矩阵乘法这种计算密集的场景这笔买卖非常划算。这里有个经验双缓冲的深度要根据你的数据搬运延迟和计算延迟来配如果搬运延迟远大于计算延迟那缓冲再深也没用得先去优化搬运路径。3. 计算单元从单个MAC到可配置的矩阵引擎3.1 MAC单元最基础的积木一切从乘法累加单元MAC开始。一个 MAC 就是乘法器 加法器 寄存器的组合每个时钟周期完成一次acc a × b。听起来简单但里面有不少门道。首先是数值格式的选择。定点还是浮点如果做推理定点比如 INT8几乎是必然选择因为定点乘法器面积只有浮点的几分之一功耗也低得多。我用的是 INT8 输入、INT32 累加这个组合在精度和效率之间平衡得很好。实测发现对于大多数分类和检测模型INT8 量化后的精度损失在 1% 以内完全可接受。其次是流水线设计。MAC 的乘法器和加法器如果串在一起关键路径会很长时钟跑不高。我的做法是在乘法器和加法器之间插一级寄存器做成两级流水。这样虽然单次计算延迟增加一拍但吞吐率翻倍整体算力反而更高。这是典型的用延迟换吞吐。3.2 处理单元PE加上本地存储和控制单个 MAC 没法独立工作得给它配上本地寄存器和控制逻辑组成一个 PE。我的 PE 结构是这样的一个权重寄存器存这一列固定的权重、一个输入寄存器、一个部分和寄存器外加一个简单的状态机控制数据流动。PE 的设计要点在于寄存器要尽量少因为寄存器多了面积和功耗都上去了。我一开始给每个 PE 配了四个寄存器后来精简到两个发现功能完全够用。这里的心得是PE 里的存储只放这一拍必须用到的数据其他都放片上缓存让缓存去承担复用PE 保持轻量。3.3 阵列规模怎么定算力、面积、利用率的三角平衡阵列规模不是越大越好。8×8 的阵列有 64 个 MAC16×16 有 256 个。规模翻倍理论算力翻倍但面积翻倍、布线复杂度上升、小矩阵的填充浪费也更严重。我做过一组对比实验用同一个模型在不同规模阵列上跑阵列规模MAC数量峰值算力(200MHz)实测利用率适用场景4×4166.4 GOPS85%小模型、边缘设备8×86425.6 GOPS78%中等模型、通用推理16×16256102.4 GOPS62%大模型、数据中心可以看到阵列越大实测利用率越低因为填充浪费和调度开销占比上升。我的建议是先确定你的目标模型矩阵维度的公因数阵列规模取这个公因数附近的值能最大化利用率。比如你的模型里矩阵维度都是 64 的倍数那 8×8 就很合适。3.4 支持多种精度可配置乘法器实际部署时不同层对精度的需求不一样。第一层和最后一层通常需要高精度中间层可以低精度。如果整个加速器只支持一种精度要么浪费算力要么精度不够。我的做法是把乘法器做成可配置的通过控制信号切换 INT8 和 INT16 模式。INT8 模式下两个 MAC 共享一个乘法器资源吞吐翻倍INT16 模式下每个 MAC 独占精度更高。这个设计让加速器能灵活应对不同层的需求实测下来整体精度和效率都比单一精度方案好。代价是控制逻辑复杂了一些但完全值得。4. 存储层次决定实际性能的隐形战场4.1 三级存储外部、片上、寄存器加速器的存储通常分三级外部存储DDR、片上缓存SRAM、寄存器。越靠近计算单元速度越快、容量越小、成本越高。设计的核心就是让数据尽量在快的层级里被复用减少慢层级的访问。我的配置是外部 DDR 存整个模型权重和输入数据片上 SRAM 分两块——一块存当前计算需要的权重块一块存输入数据块寄存器在 PE 内部存当前拍的数据。数据从 DDR 加载到 SRAM 用 DMA从 SRAM 到寄存器用流水线自动搬运。这里有个关键决策片上 SRAM 要多大太小了数据复用不够频繁访问 DDR太大了面积和成本上去了。我的经验公式是SRAM容量 ≥ 2 × (单个权重块大小 单个输入块大小) × 双缓冲系数双缓冲系数取 2因为要准备两块。按这个公式算我的设计里 SRAM 配了 256KB实测下来能覆盖绝大多数层的计算块DDR 访问次数比不配缓存时减少了 90% 以上。4.2 数据复用把每个字节榨干存储层次设计的本质是提高数据复用率。矩阵乘法里一个输入元素会被 N 个输出用到一个权重元素会被 M 个输出用到。如果每次用都去读一遍带宽需求会爆炸。我的复用策略分三层第一层输入数据在 PE 阵列里横向广播一次读入服务一整行 PE第二层权重在 PE 里固定不动服务所有经过的输入第三层片上 SRAM 缓存整个计算块块内数据反复用。三层复用叠加下来实际 DDR 带宽需求降到了理论值的十分之一左右。这里有个容易踩的坑复用率不是越高越好因为高复用意味着数据要在片上停留更久需要更大的缓存和更复杂的调度。我一开始追求极致复用结果调度逻辑复杂到难以验证后来退回到够用就好的复用率整体反而更稳。4.3 DMA引擎后台搬运工DMA直接内存访问引擎负责在 DDR 和 SRAM 之间搬数据不占用计算单元的控制资源。它的设计要点是支持多维传输和自动地址生成因为矩阵数据在内存里通常是按行或按块存储的需要能高效地搬一个子矩阵。我的 DMA 支持三种模式线性传输搬连续数据、二维传输搬矩阵块带行跨步、转置传输搬的同时做转置。转置传输特别有用因为矩阵乘法里经常需要把权重转置后使用如果让计算单元去做转置会浪费算力交给 DMA 在搬运时顺手做了最划算。DMA 的另一个关键是和计算的重叠。我用中断加描述符链的方式让 DMA 搬完一块自动触发下一块计算单元只管算两者通过双缓冲解耦。实测下来只要 DMA 带宽足够计算单元几乎不会因为等数据而停顿。5. 控制与调度让所有模块协同工作5.1 指令集设计给加速器一套语言加速器需要一个控制器来指挥各个模块而控制器需要一套指令集。我的指令集很精简只有几类加载权重、加载输入、启动计算、写回结果、同步。每条指令对应一个描述符描述符里包含地址、维度、步长等参数。指令集设计的原则是够用就好别过度设计。我一开始想搞一套很通用的指令结果发现大部分指令根本用不上反而增加了译码复杂度。后来精简到五条核心指令覆盖了所有需要的操作。这里的心得是先把你所有层的操作列出来找出共性指令集自然就出来了不要凭空设计。5.2 流水线调度填满每一个时钟周期调度器的任务是把指令排好序让计算、搬运、写回尽量并行。我的调度策略是静态调度加动态微调编译时把指令排成一个流水线运行时根据实际延迟做小调整。一个典型的流水线是这样的DMA 加载第 i1 块权重的同时计算单元在算第 i 块计算完第 i 块后DMA 立刻加载第 i2 块同时写回第 i 块的结果。这样计算、加载、写回三条流水线并行理论利用率能到 90% 以上。实际因为各种依赖和冲突能到 75% 左右已经很不错了。5.3 处理边界情况维度不匹配怎么办真实模型里矩阵维度很少是整齐的经常出现 768、3072 这种不是阵列尺寸整数倍的情况。处理不好要么算错要么效率暴跌。我的处理办法是分块加 padding。把大矩阵切成阵列能处理的块最后一块如果维度不够就补零。补零的部分计算结果无效写回时丢弃。这样控制逻辑统一不用为边界单独写一套逻辑。代价是浪费一点算力但换来的是代码简洁和验证容易非常值得。另一个边界情况是矩阵维度小于阵列尺寸比如 3×3 的矩阵扔进 8×8 阵列。这时候大部分 PE 闲置利用率很低。我的做法是对小矩阵走单独的路径用少量 PE 处理避免整个阵列空转。这个优化对包含大量小矩阵的模型比如某些注意力变体效果很明显。6. 验证与实测怎么知道它真的能跑6.1 从单元测试到系统测试加速器设计最怕的是看起来对跑起来错。我的验证分三层第一层是单元测试每个 PE、每个 DMA 通道单独测用已知输入验证输出第二层是模块测试把阵列、缓存、控制器组合起来测一个小矩阵乘法第三层是系统测试跑完整的模型层和 CPU 上的参考实现对比。单元测试里我踩过一个坑PE 的部分和寄存器复位没做好导致连续计算时上一批的残留值污染了下一批结果。这个问题在单次测试里看不出来只有连续跑才暴露。所以单元测试一定要做连续多批的测试不能只测一次。6.2 和参考实现对比精度和性能双验证精度验证的方法很简单同样的输入和权重在加速器上跑一遍在 CPU 上用 NumPy 跑一遍对比结果。允许的误差取决于你的数值格式INT8 的话相对误差在 1% 以内算正常。性能验证要测两个指标峰值算力和实际算力。峰值算力是理论值实际算力是跑真实模型测出来的。两者的比值就是利用率。我实测下来大矩阵场景利用率能到 78%小矩阵场景只有 40% 左右。这个差距主要来自填充浪费和调度开销是正常的。6.3 性能瓶颈定位从数据反推问题跑得慢的时候怎么知道瓶颈在哪我的方法是看三个计数器计算单元忙碌周期数、DMA 忙碌周期数、计算单元等待数据的周期数。如果等待周期占比高说明是带宽瓶颈要优化 DMA 或加缓存如果计算单元忙碌但利用率低说明是填充浪费要调整阵列规模或分块策略。这套方法帮我定位过一个问题某个层的利用率特别低一查发现是权重矩阵的列数不是 8 的倍数导致每次都要 padding 大量无效列。后来我把这个层的权重重新排列了一下利用率立刻上去了。这种问题不看计数器根本发现不了。7. 几个让我印象深刻的坑和对应的解法7.1 时序不收敛关键路径太长第一次综合的时候时序完全不收敛时钟只能跑到 80MHz。查下来是 PE 阵列里的部分和传递路径太长8 个 PE 串起来路径延迟累加超过了时钟周期。解法是在部分和传递路径上插寄存器做成流水线。每级 PE 之间加一拍延迟虽然整体计算延迟增加了但时钟频率提到了 220MHz算力反而翻倍还多。这个经历让我深刻理解了流水线是时序收敛的万能药这句话。7.2 数据竞争双缓冲的同步问题双缓冲用不好会出数据竞争计算单元还在读 A 块DMA 已经把 B 块写进来了如果地址没管好可能覆盖了正在读的数据。我一开始就踩了这个坑结果算出来的结果时对时错非常难查。解法是给每块缓存加一个状态标志空闲、加载中、计算中、可写回DMA 和计算单元都先检查标志再操作。这个机制增加了一点控制开销但彻底解决了竞争问题。教训是只要有多方访问共享资源就必须有明确的同步机制不能靠应该不会冲突的侥幸心理。7.3 量化误差累积精度掉得比预期快做 INT8 量化时单层误差很小但十几层累积下来最终输出偏差就大了。我一开始只做了权重量化激活值还是高精度结果发现中间层的激活值范围波动很大量化后误差明显。解法是同时对权重和激活做量化并且用每层独立的缩放因子scale。缩放因子根据每层激活值的实际分布来定而不是全局统一。这个改动让最终精度损失从 5% 降到了 1% 以内。经验是量化不是简单地把浮点转定点缩放因子的选择直接决定成败一定要按层统计分布来定。7.4 资源不够用FPGA上的面积焦虑在 FPGA 上实现时DSP 资源和 BRAM 资源都很紧张。8×8 阵列用掉了我大部分 DSP缓存又占了大半 BRAM剩下的资源连个像样的控制器都放不下。解法是资源复用和位宽优化。乘法器在 INT8 模式下拆成两个 4 位乘法器共享节省了一半 DSP缓存用更紧凑的存储结构把位宽从 32 位压到 16 位部分和用定点表示。这些优化让整个设计刚好塞进目标 FPGA。教训是FPGA 实现一定要先看资源报告别等综合完了才发现放不下。8. 如果想继续往下走可以试试这些方向把基础版本跑通之后我陆续尝试了几个扩展方向每个都让我对加速器设计有了新理解。第一个是稀疏化支持。真实模型里很多权重接近零如果能跳过这些零的乘法算力能省一大半。我加了一个简单的零检测逻辑遇到零权重就跳过对应 PE 的计算。实测在稀疏度 50% 的模型上速度提升了 30% 左右。难点在于稀疏模式不规则调度复杂需要专门的压缩格式。第二个是多核扩展。单个阵列算力有限把多个阵列拼起来能线性提升算力。我试了双核方案两个阵列共享外部存储但各有独立缓存通过一个简单的任务队列分配工作。实测算力接近翻倍但核间同步和缓存一致性问题需要仔细处理。第三个是动态电压频率调节。根据当前负载调整时钟频率和电压能在低负载时省电。这个在 FPGA 上不太好验证但在真实芯片上是很重要的优化。思路是监控计算单元的忙碌率忙碌率低就降频。回头看从零设计一台 AI 加速器最难的不是某个具体模块而是把算力、带宽、存储、控制这几个维度平衡好。任何一个维度短板都会拖累整体。我个人的体会是先把数据流想清楚再定阵列规模最后配存储和控制这个顺序不能乱。如果反过来先堆算力后面一定会被带宽和存储卡住。另外验证一定要趁早每做完一个模块就测别等全部搭完再调那时候问题会多到让你怀疑人生。
阅读完成 · 觉得有帮助?