1. 为什么“AI芯片的软硬件设计”值得单独拎出来聊
这两年跟不少做芯片和做算法的朋友聊天,发现一个特别有意思的现象:做算法的觉得硬件那帮人不懂模型,做硬件的觉得算法那帮人不懂时序。结果就是,一颗AI芯片流片回来,跑分看着还行,一到真实模型部署就各种掉链子。这个项目标题“AI芯片的软硬件设计”,说白了就是要把这两拨人的语言打通,从模型需求反推硬件架构,再从硬件约束倒逼算法优化。
我自己在这个方向上踩过不少坑,从最早拿FPGA搭卷积加速器,到后来参与几款边缘端推理芯片的架构评估,再到帮团队做算子库和编译器的适配,最大的体会就是:AI芯片不是“把GPU那套搬过来”那么简单,它的软硬件设计是一个强耦合的协同过程。你选什么样的数据流,决定了你片上缓存怎么设计;你片上缓存怎么设计,又决定了编译器怎么做算子融合和调度。任何一个环节拍脑袋,最后都会在能效比或者实际吞吐上还债。
这篇文章适合谁看?如果你是刚入行的数字IC设计工程师,想搞清楚AI加速器跟传统SoC设计到底差在哪;如果你是算法工程师,想知道自己的模型为什么在特定芯片上跑不快;或者你是系统架构师,正在评估自研AI芯片的可行性,那这篇内容应该能给你一些可以直接参考的思路和避坑经验。我会尽量把“为什么这么选”讲透,而不是只丢一堆参数。
2. 从模型到芯片:整体设计思路的拆解
2.1 先搞清楚你要跑什么模型,再谈架构
很多团队一上来就说“我们要做一颗支持Transformer的AI芯片”,然后就开始画MAC阵列。这个顺序其实是反的。正确的做法是先把目标模型族锁定,至少锁定未来两到三年的主力模型形态。因为不同模型对硬件的诉求差异极大。
举个例子,卷积神经网络为主的场景,比如图像分类、目标检测,它的计算特征是高空间复用、权重共享、局部感受野。这种场景下,权重 stationary 的数据流往往能拿到很高的能效比,因为权重可以常驻片上缓存,输入特征图在PE阵列里流动,减少反复搬运。而Transformer类的模型,核心是矩阵乘和Softmax,它的计算密度高但访存模式更偏向大块矩阵的连续读写,对片上缓存带宽和容量要求完全不同。如果你用做CNN的思路去做Transformer加速器,很可能出现PE利用率上不去、Softmax成为瓶颈的情况。
所以我在实际项目里通常建议先做一件事:把目标模型的计算图拆开,统计各类算子的占比。具体来说,用 profiling 工具跑一遍典型模型,记录卷积、全连接、矩阵乘、激活、归一化这些操作各自占了多少计算量和多少访存量。这个数据出来之后,你才知道MAC阵列该做多大、片上缓存该配多少、需不需要专门的向量处理单元来处理非矩阵运算。
2.2 数据流选择:权重固定、输出固定还是行固定
数据流的选择是AI芯片硬件设计里最核心的决策之一,它直接决定了PE阵列的利用率和访存功耗。常见的有三种:权重固定、输出固定、行固定。我分别说一下它们的适用场景和实际取舍。
权重固定的思路是让权重留在PE里不动,输入特征图和部分和在里面流动。这种方式的优势是权重访存次数最少,适合卷积核复用率高的场景。但缺点是如果模型层与层之间权重差异大,权重加载的开销就不能忽略。输出固定则是让输出特征图的部分和留在PE里累加,输入和权重都流动。这种方式适合输出通道数很大的层,因为部分和不需要反复写回。行固定算是前两者的折中,在特定卷积形状下能取得不错的平衡。
实际项目中,我见过不少团队直接照搬某篇论文的数据流,结果发现自己的模型形状跟论文里的假设不一致,PE利用率只有百分之四五十。所以我的经验是:不要迷信单一数据流,而是根据目标模型的主力层形状做加权评估。你可以写一个简单的模拟脚本,把几种数据流在目标模型上的PE利用率和访存次数都算一遍,选综合最优的那个。这个脚本不需要多复杂,用Python写个几百行就能跑出趋势。
2.3 软硬件协同的分界线在哪里
软硬件协同设计听起来很美好,但实际操作中最难的是划清边界:哪些事情交给硬件做,哪些交给编译器做,哪些留给运行时。这个边界划不好,要么硬件过度设计导致面积浪费,要么编译器复杂度爆炸导致开发周期失控。
我的经验法则是:高频、固定模式、对能效敏感的操作下沉到硬件;低频、多变、对灵活性要求高的操作交给软件。比如卷积和矩阵乘这种计算模式相对固定的,做成硬化的MAC阵列和专用数据通路是划算的。而像动态shape处理、稀疏化模式选择、算子融合策略这些,放在编译器里做更合适,因为硬件一旦流片就没法改了。
还有一个容易被忽视的点是控制流的下沉程度。有些团队把整个调度逻辑都做进硬件状态机,结果遇到新模型就要重新流片。更稳妥的做法是硬件只提供基本的同步和中断机制,具体的执行顺序由编译器生成的指令流来控制。这样虽然增加了一点指令开销,但换来了对模型变化的适应能力。
3. 核心细节解析:从MAC阵列到编译器栈
3.1 MAC阵列的尺寸怎么定,别拍脑袋
MAC阵列是AI芯片的计算核心,它的尺寸直接决定了峰值算力和面积成本。很多人在定尺寸的时候喜欢往大了做,觉得算力越高越好。但实际约束是:阵列越大,对片上缓存带宽和容量要求越高,而且小模型跑不满会导致利用率极低。
我通常用这样一个估算流程:先确定目标模型的主力卷积层或矩阵乘的维度,比如卷积的输入通道、输出通道、特征图尺寸,然后看这些维度里最小的那个是多少。MAC阵列的边长一般不会超过这个最小值太多,否则就会出现大量PE空转。举个例子,如果主力层的输出通道是64,输入通道是128,特征图是56x56,那你的阵列如果做成128x128,在输出通道维度上就有一半是浪费的。
更具体的计算方法是:假设阵列是NxN,每个周期能处理N个输出通道和N个输入通道的乘加。对于输出通道为C_out、输入通道为C_in的层,需要的周期数大约是 ceil(C_out/N) * ceil(C_in/N) * H * W。你可以用这个公式去评估不同N值下的周期数和PE利用率,再结合面积和功耗约束做取舍。实测下来,对于边缘端推理芯片,64x64到128x128是比较常见的区间;云端训练芯片则可能到256x256甚至更大。
3.2 片上缓存的层次划分与容量分配
AI芯片的片上缓存通常分几级:寄存器文件、PE本地缓存、共享缓存、全局缓存。每一级的容量和带宽分配都需要仔细权衡。寄存器文件在PE内部,用来暂存当前参与计算的权重和激活值,容量很小但带宽极高。PE本地缓存用来做数据复用,比如权重固定数据流里权重就放在这里。共享缓存供多个PE簇共用,用来做特征图的行缓存或块缓存。全局缓存则是芯片级别的,通常用SRAM实现,容量在几MB到几十MB之间。
容量分配的核心依据是数据复用距离。如果一个数据在被换出之前能被复用很多次,那就值得放在更靠近PE的地方。以卷积为例,权重在整个特征图计算过程中都会被复用,所以权重适合放在PE本地缓存或寄存器里。输入特征图的每个像素会被多个卷积核复用,适合放在共享缓存里。输出部分和则需要在PE里累加,适合放在寄存器或本地缓存。
我踩过的一个坑是:早期设计时把共享缓存做得太小,结果编译器为了适配缓存容量,不得不把特征图切成很多小块,导致块与块之间的边界数据反复搬运,整体能效比下降了三成。后来把共享缓存加大了一倍,虽然面积增加了一些,但整体能效反而更好了。所以缓存容量不是越小越省,关键看能不能支撑起高效的数据复用模式。
3.3 编译器栈:从计算图到指令流的关键步骤
AI芯片的编译器栈通常包括图优化、算子融合、内存分配、指令生成这几个阶段。每个阶段都有不少门道。
图优化阶段主要是做常量折叠、死代码消除、算子替换这些通用优化。算子融合是AI编译器里最影响性能的一步,它把多个连续的小算子合并成一个大的kernel,减少中间结果的写回和读取。比如卷积后面跟BatchNorm和ReLU,如果不融合,就要写三次内存;融合之后,卷积算完直接在寄存器里做归一化和激活,只写一次内存。这个优化在访存受限的模型上能带来两三倍的性能提升。
内存分配阶段要解决的是片上缓存的复用问题。因为片上缓存容量有限,编译器需要决定哪些张量放在缓存里、哪些放回外部内存、缓存的复用顺序是什么。这里常用的方法是生命周期分析加内存池化,把生命周期不重叠的张量分配到同一块缓存区域。但要注意,过度复用会导致缓存冲突,反而降低性能。所以实际实现里通常会留一些余量,或者用多bank结构来减少冲突。
指令生成阶段就是把优化后的计算图翻译成硬件能执行的指令序列。这里的关键是指令的粒度和并行度。指令太细,控制开销大;指令太粗,灵活性差。我的经验是让指令覆盖一个基本的计算块,比如一次完整的卷积操作或者一次矩阵乘的分块,这样既不会太碎,又能让硬件有足够的调度空间。
3.4 量化与稀疏化:软硬件都要配合
量化是AI芯片绕不开的话题。把FP32量化到INT8甚至INT4,能大幅降低访存带宽和计算功耗。但量化不是简单地把数据截断,它需要软硬件配合。硬件端要支持低精度的MAC运算,比如INT8的乘加器面积只有FP32的几分之一。软件端要做量化感知训练或者训练后量化,尽量减小精度损失。
实际项目中,我建议硬件至少支持INT8和INT16两种精度,因为有些层对精度敏感,比如第一层卷积和最后的全连接层,用INT8可能会掉点。硬件支持混合精度之后,编译器可以根据每层的敏感度自动选择精度,在精度和性能之间取得平衡。
稀疏化是另一个软硬件协同的方向。模型剪枝之后会产生大量零值,如果硬件能跳过零值的计算和访存,就能直接获得加速。但稀疏化的硬件支持需要额外的索引和调度逻辑,设计复杂度不低。我的建议是先从结构化稀疏入手,比如按块稀疏或者按通道稀疏,这样硬件实现起来简单,编译器也容易做调度。非结构化稀疏虽然理论加速比更高,但实际硬件利用率往往不理想。
4. 实操过程:从架构评估到原型验证
4.1 架构评估阶段的快速建模方法
在正式写RTL之前,一定要先做架构评估。我常用的方法是用Python或C++写一个周期级的模拟器,不需要太精确,但要把PE阵列、缓存层次、访存带宽这些关键参数都建模进去。然后拿目标模型跑一遍,看周期数、PE利用率、访存次数这些指标是否达标。
这个模拟器的输入是模型的计算图和各层的维度参数,输出是预估的性能和功耗。功耗可以用简单的活动因子模型来估算,比如每次MAC运算消耗多少能量、每次缓存访问消耗多少能量。虽然精度不如后面的门级仿真,但在架构探索阶段足够用了,而且迭代速度极快,改个参数几秒钟就能看到结果。
我一般会在这个阶段跑几十组参数组合,比如阵列尺寸从32x32到256x256,缓存容量从1MB到16MB,数据流换两三种,然后画成功率-面积曲线,找帕累托最优的那个点。这个过程能避免很多后期返工。
4.2 RTL实现中的关键模块与注意事项
RTL实现阶段,AI芯片跟普通SoC最大的区别在于数据通路的位宽和并行度。MAC阵列的输入输出位宽通常很宽,比如64个INT8乘加器,输入就是64x8位,输出是64x32位。这种宽数据通路对布局布线是个挑战,需要提前做好物理设计的规划。
另一个关键是时钟域和复位策略。AI芯片通常有多个时钟域,计算阵列跑在高频,控制逻辑和接口跑在低频。跨时钟域的信号同步要做仔细,否则会出现亚稳态问题。复位策略上,我建议对计算阵列采用局部复位,不要全局复位,因为全局复位会导致大量寄存器同时翻转,产生很大的瞬态电流。
还有一个容易忽视的点是调试和可观测性。AI芯片流片回来之后,如果性能不达标,你需要知道瓶颈在哪里。所以在设计阶段就要预留性能计数器和调试接口,比如记录PE利用率、缓存命中率、访存带宽这些指标。这些计数器面积不大,但后期调试时能救命。
4.3 编译器与硬件的联调流程
编译器和硬件的联调是AI芯片开发里最耗时的环节之一。我的经验是尽早建立端到端的验证环境,不要等硬件RTL完全稳定了才开始调编译器。可以用FPGA原型或者硬件仿真器先跑起来,哪怕性能只有真实芯片的十分之一,也能提前发现很多接口和语义上的问题。
联调时最常见的问题是编译器生成的指令序列跟硬件实际行为不一致。比如编译器假设某个缓存操作是原子的,但硬件实现里不是,结果导致数据竞争。这类问题往往很难定位,因为现象是随机的。解决办法是在硬件里加断言和检查逻辑,一旦发现非法操作就报错,而不是 silently 产生错误结果。
另一个常见问题是性能不达预期。这时候需要用性能计数器去定位瓶颈。是先看PE利用率,如果利用率低,说明数据供给跟不上,要查缓存带宽和调度策略。如果PE利用率高但整体吞吐还是低,那可能是频率上不去或者存在流水线气泡。这个排查过程需要耐心,但有了计数器数据,方向会清晰很多。
4.4 原型验证与性能调优的实操记录
拿一个实际案例来说,我们之前做的一款边缘推理芯片,目标模型是MobileNet系列的卷积网络。架构评估阶段选了64x64的INT8 MAC阵列,共享缓存2MB,权重固定数据流。模拟器预估的PE利用率在75%左右,能效比目标是2TOPS/W。
RTL实现之后,FPGA原型上跑出来的实际利用率只有55%,能效比也差了不少。用性能计数器一查,发现瓶颈在深度可分离卷积的逐通道卷积层。这种层的输入通道和输出通道相等,但每个通道的卷积核很小,导致权重固定的优势发挥不出来,PE阵列有大量空转。
解决办法是在编译器里对逐通道卷积做特殊处理,把它映射成分组卷积的形式,让多个通道共享PE阵列。同时硬件上增加了对分组卷积的地址生成支持,减少了地址计算的开销。改完之后,PE利用率回到了70%以上,能效比也接近了目标值。
这个案例给我的教训是:架构评估阶段的模型一定要覆盖目标模型里的所有算子类型,不能只看主力卷积层。那些占比不大但形状特殊的层,往往才是性能杀手。
5. 常见问题与排查技巧实录
5.1 性能不达预期的排查思路
性能问题是最常见的,我整理了一个排查顺序,基本能覆盖大部分情况。
| 排查步骤 | 检查内容 | 可能原因 | 解决方向 |
|---|---|---|---|
| 1 | PE利用率 | 数据供给不足或阵列尺寸不匹配 | 调整数据流或阵列尺寸 |
| 2 | 缓存命中率 | 缓存容量不足或复用策略差 | 增大缓存或优化调度 |
| 3 | 访存带宽 | 外部内存带宽瓶颈 | 增加数据复用或压缩数据 |
| 4 | 流水线气泡 | 同步开销或依赖停顿 | 优化指令调度或增加并行度 |
| 5 | 时钟频率 | 关键路径太长 | 插入流水线或优化逻辑 |
这个表看着简单,但实际用起来很有效。我一般从PE利用率开始查,因为这是最直观的指标。如果PE利用率低于60%,那基本可以确定是数据供给或者阵列匹配的问题,不用往下查了。
5.2 精度掉点的常见原因与修复
量化之后精度掉点也是高频问题。常见原因有几个:一是激活值的动态范围估计不准,导致量化scale选得不好;二是某些层的权重量化误差累积,比如深层网络里误差会逐层放大;三是Softmax和LayerNorm这类操作对精度敏感,用INT8做会出问题。
修复方法上,我通常建议对敏感层保留FP16或者INT16精度,其他层用INT8。硬件上支持混合精度之后,这个策略很容易实现。另外,量化感知训练里要加入量化误差的直通估计,让梯度能正确回传。如果不想重新训练,训练后量化里可以用KL散度校准来选scale,比简单的min-max校准效果好不少。
5.3 编译器与硬件接口不一致的调试技巧
这类问题往往表现为随机错误或者死锁,定位起来很头疼。我的经验是在硬件里加协议检查器,比如检查指令的合法性、缓存的访问权限、同步信号的时序关系。一旦发现违规就记录下出错时的指令地址和状态信息,这样就能快速定位到编译器生成的哪条指令有问题。
另外,编译器和硬件团队要共用同一份接口文档,而且这份文档要随着设计迭代实时更新。我见过太多因为文档不同步导致的联调问题,两边理解不一致,查半天才发现是文档写错了。所以我现在都要求接口文档用版本控制管理,每次改动都要双方确认。
5.4 功耗和散热问题的实际处理
AI芯片的功耗密度通常很高,尤其是MAC阵列全速运转的时候。如果散热设计跟不上,芯片会降频甚至宕机。实际处理上,除了常规的散热方案,还可以在架构层面做功耗管理。比如根据工作负载动态调整电压频率,在PE阵列空闲的时候关掉时钟,对缓存做分bank供电管理。
还有一个技巧是在编译器里做功耗感知的调度,把高功耗的算子分散到不同的时间段执行,避免瞬时功耗过高。这个优化对移动端芯片特别有用,因为移动端的散热能力有限,瞬时功耗过高会触发温控降频。
6. 一些个人体会和后续可以扩展的方向
做AI芯片的软硬件设计,最大的感受就是没有银弹。每一款成功的芯片背后都是大量的权衡和迭代。架构评估阶段多花一周时间做模拟,可能就能省下流片后几个月的调试。编译器和硬件的接口定义得越早、越清晰,后期联调就越顺利。
如果让我给刚入行的朋友一个建议,那就是先把一个小的端到端流程跑通,哪怕是一个只有16x16 MAC阵列的FPGA原型,配上最简单的编译器和运行时。这个完整流程跑通之后,你对整个系统的理解会深刻很多,再去做大芯片的时候就知道每个环节的坑在哪里了。
后续这个方向还可以往几个方向扩展:一是动态shape支持,现在很多模型输入尺寸不固定,硬件和编译器都要能灵活应对;二是多芯片互联,单芯片算力总有上限,多芯片协同是必然趋势;三是安全与隐私,在边缘端做推理时,模型和数据的保护越来越重要。这些方向都值得深入探索。