第2章:AI计算核心集成:NPU内部架构解析、MAC阵列与数据流设计、激活函数与池化单元集成
各位同学,咱们今天聊聊NPU。说实话,很多做SoC集成的工程师,一听到NPU就觉得是黑盒子。其实拆开来看,核心就三块:计算阵列、数据搬运、非线性处理。我当年第一次接触NPU集成时,也踩过不少坑,今天把这些经验掰开揉碎了讲给你听。
2.1 NPU内部架构:从宏观到微观
NPU不是凭空造出来的。它的架构设计,说白了就是围绕“矩阵乘法”这个核心任务展开的。你想想看,神经网络里90%以上的计算量,都是卷积和全连接,本质就是矩阵乘加。
一个典型的NPU内部,我习惯把它分成四个功能区:
- 计算引擎:MAC阵列,这是心脏
- 数据通路:包括输入缓冲、权重缓冲、输出缓冲
- 控制单元:负责指令译码和调度
- 后处理单元:激活函数、池化、量化等
嗯,这里要注意:很多芯片把激活和池化放在计算引擎外面,但我在项目中遇到过,如果数据带宽不够,后处理反而会成为瓶颈。所以集成时,一定要看数据流的吞吐匹配。
核心观点:NPU的架构设计,本质是“计算-存储-控制”三者的平衡。别只盯着MAC数量,数据搬运效率往往才是真正的天花板。
2.2 MAC阵列设计:你堆了多少个乘法器?
MAC阵列,就是乘累加单元组成的阵列。每个MAC单元做一次 a * b + c 的操作。为什么用阵列?因为卷积的并行度极高,一张输入特征图可以同时和多个卷积核做运算。
我见过很多芯片,MAC阵列规模从64x64到256x256不等。但规模越大越好吗?不一定。举个例子:
// 假设MAC阵列为8x8,处理3x3卷积
// 每个周期,阵列可以同时计算64个乘加
// 但输入数据需要广播到所有MAC单元
// 这里的数据复用策略,决定了实际利用率
为什么会这样?因为数据搬运跟不上。我曾经在一个项目中,MAC阵列利用率只有40%,查了半天,发现是输入缓冲太小,数据喂不饱计算单元。后来把缓冲从16KB扩到64KB,利用率直接拉到85%。
个人经验:设计MAC阵列时,先算好“计算密度”和“访存带宽”的比例。一般建议:每1TOPS算力,至少配2GB/s的片上带宽。否则,再多的MAC也是摆设。
2.3 数据流设计:三种主流模式
数据流设计,说白了就是决定“谁不动、谁在动”。NPU里有三种数据:输入特征图、权重、输出特征图。根据它们各自的移动方式,主流设计有三种:
| 数据流模式 | 固定数据 | 移动数据 | 适用场景 |
|---|---|---|---|
| 权重固定(Weight Stationary) | 权重 | 输入、输出 | 权重复用度高,适合大模型 |
| 输入固定(Input Stationary) | 输入特征图 | 权重、输出 | 输入复用度高,适合视频流 |
| 输出固定(Output Stationary) | 输出累加结果 | 输入、权重 | 减少写回次数,适合小batch |
我个人习惯用权重固定模式。为什么?因为权重一旦加载到片上,就可以反复使用,减少片外访问。你想想看,DDR的功耗是片上SRAM的几十倍,能省则省。
避坑指南:我曾经在一个项目中,为了追求低延迟,选了输入固定模式。结果发现,每次卷积核切换都要重新加载权重,DDR带宽直接爆了。后来改成权重固定,虽然延迟多了几个周期,但整体吞吐提升了3倍。所以,别只看单次延迟,要看系统吞吐。
2.4 激活函数集成:ReLU、Sigmoid、GELU怎么选?
激活函数,说白了就是给神经网络加非线性。没有它,再多层也是线性变换,白搭。
常见的激活函数集成方式有两种:
- 查表法(LUT):把函数值预先算好,存在ROM里。输入作为地址,直接读出结果。速度快,但精度受表大小限制。
- 分段线性逼近:用几段直线拟合曲线。比如ReLU就是两段,GELU可能需要8-16段。面积小,精度可控。
我建议:对于ReLU这种简单函数,直接用硬件逻辑实现,一个比较器加一个选择器就搞定。对于Sigmoid或GELU,用分段线性逼近,8段就能达到99%的精度,面积只有查表法的1/5。
// 分段线性逼近示例(GELU近似)
// 输入x,输出y
if (x < -3.0) y = 0;
else if (x < -1.0) y = 0.125 * (x + 3.0);
else if (x < 1.0) y = 0.5 * x + 0.5;
else if (x < 3.0) y = 0.875 * (x - 1.0) + 1.0;
else y = x;
小技巧:集成激活函数时,记得考虑量化影响。如果输入是INT8,查表法只需要256个条目,非常省事。但如果输入是FP16,查表法就太浪费了,用分段线性更合适。
2.5 池化单元集成:Max Pooling vs Average Pooling
池化,说白了就是降采样。Max Pooling取最大值,Average Pooling取平均值。集成时,两者差别不大,但有个细节要注意:
Max Pooling的实现很简单,一组比较器就行。但Average Pooling需要除法器,面积大不少。我见过有些芯片,为了省面积,用移位代替除法(比如除以4用右移2位),但这样精度会损失。
嗯,这里有个坑:池化窗口大小不一定是2x2。有些网络用3x3甚至全局池化。所以设计时,最好支持可配置窗口大小,别写死。
实战建议:池化单元最好和激活函数放在同一个流水线阶段。因为很多网络结构是“卷积 -> 激活 -> 池化”,如果分开,中间要多一级缓冲,浪费面积和功耗。我在一个项目中,把激活和池化合并成一个单元,面积节省了15%,时序也更好收敛。
2.6 集成实战:一个完整的NPU计算流水线
好了,前面讲了各个模块,现在把它们串起来。一个典型的NPU计算流水线是这样的:
- 加载阶段:从DDR加载权重到权重缓冲,加载输入到输入缓冲
- 计算阶段:MAC阵列执行矩阵乘法,结果暂存到累加器
- 后处理阶段:累加结果经过激活函数,再经过池化
- 写回阶段:结果写回输出缓冲,再写回DDR
这个流水线看起来简单,但集成时要注意“背压”问题。比如,MAC阵列算得飞快,但后处理单元处理不过来,就会导致流水线 stall。我建议在后处理单元前加一个 FIFO,深度至少是MAC阵列输出宽度的4倍,这样能吸收大部分波动。
// 流水线控制伪代码
always @(posedge clk) begin
if (mac_valid && post_ready) begin
// MAC输出有效,后处理就绪,数据传递
post_data <= mac_result;
end else if (mac_valid && !post_ready) begin
// 后处理忙,数据暂存FIFO
fifo_push(mac_result);
end
end
最后提醒:集成NPU时,别忘了考虑测试模式。我吃过一次亏,芯片回来后发现MAC阵列有一行不工作,但因为没有测试模式,根本定位不到问题。后来花了两个月才找到是某个乘法器的进位链断了。所以,一定要加扫描链和BIST(内建自测试)。
好了,这一章的内容就到这里。NPU集成,说难不难,说简单也不简单。核心就是理解数据流,平衡计算和访存。下一章我们聊聊存储子系统,那是另一个容易踩坑的地方。