1. 从一颗芯片的诞生说起:为什么软硬件必须一起设计
很多人第一次接触AI芯片,脑子里冒出来的画面是一块焊在板子上的黑色方块,上面印着某个厂商的logo,插上电就能跑模型。但真正做过一轮完整流片的人都知道,芯片从来不是"设计出来就能用"的东西,它更像是一场软硬件团队之间长达一年半载的拉锯战。硬件团队把面积、功耗、时序抠到小数点后两位,软件团队则盯着编译器能不能把算子映射上去、内存带宽够不够喂饱计算单元。任何一方掉链子,最后拿到的要么是一块跑不动主流模型的废片,要么是一块性能只有理论值三成的"半成品"。
这篇内容想聊的,就是AI芯片软硬件协同设计这件事。它适合三类人看:一是刚入行、想搞清楚芯片从需求到落地全流程的工程师;二是做算法或框架、需要理解底层硬件约束的开发者;三是负责技术选型、想知道一块AI芯片到底该怎么评估的决策者。我不会只讲概念,而是把架构选型、数据流设计、编译器配合、验证方法这些环节拆开,讲清楚每一步"为什么这么做"以及"实际做的时候会踩什么坑"。
先给一个反直觉的结论:AI芯片的性能瓶颈,绝大多数时候不在算力,而在数据搬运。一颗标称256 TOPS的芯片,如果内存带宽只有几十GB/s,跑真实模型时有效算力可能连三分之一都发挥不出来。这就是为什么软硬件必须一起设计——硬件决定数据能搬多快,软件决定数据该怎么搬,两者脱节,算力就是纸面数字。
2. 架构选型:为什么大多数AI芯片最终都走向了"矩阵+向量"的组合
2.1 从通用CPU到专用加速器的必然性
通用CPU的设计目标是"什么都能干",所以它把大量面积花在了分支预测、乱序执行、多级缓存这些通用性机制上。跑一个矩阵乘法,CPU真正用于计算的晶体管可能只占芯片总面积的百分之几,其余都在为"通用"买单。AI负载的特点是计算模式高度规整——卷积、矩阵乘、激活函数,翻来覆去就这几类,而且数据复用率极高。这种负载用通用架构跑,等于开着卡车送快递,能送到但效率极低。
专用加速器的思路是把面积尽可能堆到计算单元和片上存储上。但"专用"也有程度之分。早期有些团队做过极端的固定功能加速器,只支持某一种卷积核尺寸,结果模型一换代,芯片直接报废。所以现在主流的做法是"可编程的专用架构"——计算单元是规整的阵列,但通过指令和配置能支持一族算子,兼顾效率和灵活性。
2.2 矩阵单元与向量单元的职责划分
实际做架构设计时,一个核心决策是:哪些算子交给矩阵单元,哪些交给向量单元。矩阵单元擅长的是大规模、规整的乘加运算,比如全连接层和卷积展开后的GEMM。它的特点是吞吐高,但灵活性差,通常要求数据按特定格式排布。向量单元则负责那些"不那么规整"的操作——激活函数、归一化、逐元素运算、池化。这些操作计算量不大,但种类多、形状杂,用矩阵单元跑反而浪费。
我见过一些设计把激活函数也硬塞进矩阵单元,结果为了适配不同激活函数的非线性,在矩阵单元外围加了一堆查表和分段逻辑,面积涨了不说,时序还很难收敛。后来改成矩阵单元只做乘加、向量单元做后处理,整个流水线清爽很多。这个划分没有绝对标准,但一个经验法则是:能用矩阵单元批量处理的,绝不放到向量单元;形状变化频繁、逐元素的,坚决交给向量单元。
2.3 片上存储的层次设计比算力更考验功力
前面说了瓶颈在数据搬运,那片上存储怎么设计就成了重中之重。典型的层次是:寄存器文件、片上缓存、全局缓存、外部内存。每一层的容量、带宽、访问延迟都不同,设计时要让数据尽量在高层级里复用,减少往低层级跑的次。
这里有个容易被忽略的点:片上缓存的bank划分和访问冲突。如果多个计算单元同时访问同一个bank,就会产生冲突,流水线直接停顿。做设计时通常要根据算子的数据访问模式,反推bank的数量和宽度。比如卷积的滑窗访问,相邻计算单元读的数据高度重叠,如果bank划分不合理,冲突率能到百分之几十。解决办法之一是让数据在写入时就按计算单元的访问模式做重排,这就是软硬件协同的典型场景——硬件提供重排能力,软件负责生成重排后的数据布局。
3. 数据流设计:决定芯片实际性能的隐形战场
3.1 三种典型数据流及其适用场景
数据流决定了计算单元怎么从存储里取数、怎么复用、怎么写回。常见的有三种:权重固定、输出固定、行固定。权重固定是把权重留在片上,输入数据流过,适合权重复用率高的场景,比如大批量推理。输出固定是把部分和留在片上累加,适合输出通道多的卷积。行固定介于两者之间,适合特定卷积形状。
选哪种数据流,不能拍脑袋,要看目标模型的计算特征。我一般会先统计目标模型里各类算子的占比、输入输出通道数、特征图尺寸,然后算每种数据流下的片上存储需求和访存次数。举个例子,如果模型里大量是3x3卷积、通道数几百,那权重固定往往更划算,因为权重总量大、复用次数多,留在片上能省下大量访存。但如果模型里有大量1x1卷积或全连接,权重和输入规模相当,就要重新权衡。
3.2 双缓冲与流水线填充的实际收益
数据流设计里有个几乎必用的技巧:双缓冲。计算单元在算当前这块数据时,DMA在后台把下一块数据搬进来,两者并行。没有双缓冲,计算单元就得等数据搬完才能开工,利用率直接砍半。但双缓冲不是加个buffer就完事,它涉及到buffer大小的权衡——太小了流水线填不满,太大了面积浪费。
实际做的时候,我会先估算单块数据的计算时间和搬运时间,让两者尽量匹配。如果搬运时间明显大于计算时间,说明带宽是瓶颈,加buffer也没用,得回去改数据流或存储层次。如果计算时间远大于搬运,那buffer可以小一点,把面积省给计算单元。这个匹配过程往往要迭代好几轮,配合仿真数据来调。
3.3 内存带宽的估算:一个具体的计算过程
带宽估算不能靠感觉,得有数字。假设目标模型是ResNet-50级别的卷积网络,峰值算力需求是每秒钟完成X次乘加。每次乘加需要读取两个操作数、写回一个结果,但因为有数据复用,实际访存量远小于理论值。关键是算清楚"算术强度"——每字节访存能支撑多少次计算。
举个简化的例子:一个3x3卷积,输入通道256,输出通道256,特征图56x56。权重总量是3x3x256x256约59万个参数,输入特征图是56x56x256约80万个数据。如果权重全部复用,输入特征图每个数据被复用256次(输出通道数),那算术强度就很高。但如果片上存不下全部权重,需要反复从外部内存读,算术强度就掉下来了。算清楚这个,才能定外部内存的带宽指标。我见过不少项目,芯片做出来才发现带宽不够,模型跑起来只有理论性能的两三成,回头改已经来不及了。
4. 编译器与硬件之间的"翻译"难题
4.1 算子映射:从计算图到硬件指令
算法工程师写的是计算图,硬件执行的是指令序列,中间隔着编译器。编译器的核心任务是把计算图切分成硬件能处理的算子,再把每个算子映射成具体的指令和数据搬运。听起来简单,做起来处处是坑。
第一个坑是算子融合。计算图里相邻的算子,比如卷积后面跟一个ReLU,如果分开执行,中间结果要写回内存再读出来,白白浪费带宽。编译器要能识别这种模式,把它们融合成一个硬件能一次完成的操作。但融合不是无脑合并,要考虑硬件是否支持融合后的数据流、片上存储够不够放中间结果。我见过编译器为了融合而融合,结果中间结果溢出片上缓存,性能反而更差。
第二个坑是数据布局转换。不同算子对数据排布的要求可能不同,卷积喜欢NCHW或NHWC,矩阵乘喜欢特定的分块格式。编译器要在算子之间插入布局转换,而转换本身是要花时间和带宽的。好的编译器会尽量让相邻算子用同一种布局,减少转换次数。
4.2 内存分配与生命周期管理
片上存储是稀缺资源,编译器要决定每个张量放在哪、什么时候释放。这本质上是个内存分配问题,但比通用程序的内存分配复杂得多,因为张量的生命周期和计算顺序强相关,而且片上存储有多个层级,每层的容量和带宽都不同。
实际做的时候,常用的是基于生命周期分析的内存复用——两个张量如果生命周期不重叠,就可以共用同一块存储。但AI计算图里张量数量多、依赖关系复杂,简单的贪心算法往往分配得不好。有些团队会用图着色或者线性规划来求更优解,代价是编译时间变长。这里有个权衡:编译时间太长,算法工程师改一次模型要等半天,体验很差;分配得太粗糙,片上存储浪费,性能上不去。我的经验是,对推理场景可以多花点编译时间换性能,对训练场景则要优先保证编译速度。
4.3 指令调度与流水线隐藏
硬件流水线要跑满,指令调度很关键。计算指令、搬运指令、同步指令之间的顺序和依赖,编译器都要管。理想情况下,搬运指令要提前发出,让数据在计算需要时刚好到位,这就是所谓的"软件流水"。
但软件流水对编译器的要求很高,它要能预测每条指令的延迟、知道硬件的并行度、还要处理循环边界。我见过一些编译器在这块做得不好,生成的代码里搬运和计算串行执行,硬件利用率只有三成。改进的办法之一是让硬件提供更明确的异步搬运指令和同步原语,降低编译器的调度难度。这也是软硬件协同的体现——硬件接口设计得好,软件就好写。
5. 验证:怎么确认这颗芯片真的能跑起来
5.1 从单元测试到系统级仿真的分层验证
芯片验证是个无底洞,但AI芯片有个特殊性:它的功能正确性最终要用模型精度来衡量。所以验证不能只停留在"指令执行对不对",还要看"跑完整个模型,输出和参考实现的误差在不在可接受范围"。
分层验证是常规做法。最底层是计算单元的单元测试,用随机数据验证乘加、激活这些基本操作。往上是算子级验证,把单个算子放到硬件模型上跑,和软件参考实现对比。再往上是子图级,验证多个算子组合后的数据流和内存分配。最顶层是系统级,跑完整模型,看端到端精度和性能。
这里有个容易踩的坑:定点量化的误差累积。AI芯片为了效率和面积,通常用定点数而非浮点数。单个算子的量化误差可能很小,但几十层累积下来,精度可能掉得没法看。所以验证时要特别关注量化策略,包括每层的缩放因子怎么定、累加器位宽够不够、溢出怎么处理。我一般会建议在算子级验证时就引入量化,而不是等到系统级才发现精度崩了。
5.2 性能验证:仿真与实测的差距从哪来
性能验证比功能验证更棘手,因为仿真环境很难完全复现真实芯片的行为。仿真时通常假设内存访问零冲突、DMA带宽满速、时钟无抖动,但真实芯片里这些假设都不成立。结果就是仿真报告里性能很好,实测打七折。
缩小差距的办法是让仿真模型尽量贴近硬件。比如在仿真里加入bank冲突模型、DMA的实际带宽曲线、流水线的真实延迟。这些参数有些来自硬件的时序分析,有些来自早期样片的实测。我参与过的项目里,性能仿真和实测的差距从最初的百分之四十逐步收敛到百分之十以内,靠的就是不断用实测数据校准仿真模型。
5.3 精度与性能的联合调优
最后一步是把精度和性能放在一起调。很多时候,提高精度和提升性能是矛盾的——用更高位宽、更复杂的量化策略,精度上去了,但面积和功耗也上去了。反过来,为了性能砍位宽,精度可能就守不住。
实际操作中,我会先定一个精度底线,比如分类任务top-1掉点不超过百分之一,检测任务mAP掉点不超过两个点。然后在这个约束下,尽可能压位宽、简化量化。这个过程要反复跑模型、看逐层误差、调缩放因子。有些层对量化敏感,就单独给它高位宽;有些层不敏感,就大胆压。这种"混合精度"的策略,往往能在精度和性能之间找到不错的平衡点。
6. 几个实际项目里踩出来的经验
6.1 别在项目初期就把架构定死
我见过太多团队在需求还没完全明确时,就急着把架构框图定下来,结果模型一更新,发现硬件不支持新算子,只能打补丁。AI模型迭代太快,芯片设计周期又长,这个矛盾没法完全消除,但可以缓解。办法是让架构保留一定的可编程性,比如向量单元支持更丰富的指令、片上存储可以灵活配置。代价是面积和功耗会高一点,但比起流片后不能用,这点代价值得。
6.2 软件团队要尽早介入
硬件团队容易有个惯性思维:先把硬件做出来,软件后面再适配。但AI芯片的软件栈极其复杂,编译器、运行时、驱动、框架适配,哪一样都不是几个月能搞定的。软件团队越早介入,越能在架构阶段就提出约束,避免硬件做完了才发现编译器映射不上去。我参与过的一个项目,软件团队在架构评审阶段就指出某个数据流会导致编译器无法生成高效代码,硬件团队据此调整了存储层次,省下了后期大量的返工。
6.3 留足验证和调优的时间
芯片项目的时间表往往前松后紧,架构和设计阶段拖一拖,验证和调优就被压缩。但AI芯片的调优特别耗时,量化策略、数据流参数、编译器配置,每一项都要反复试。我的建议是,在项目排期时就把验证和调优单独列出来,给足时间,别指望"设计对了就一定能跑好"。实际经验是,从第一次跑通模型到性能达标,花的时间往往和设计阶段相当。
6.4 建立可复现的基准测试集
最后一条经验是关于基准测试的。没有一套稳定、可复现的基准,性能调优就是盲人摸象。基准要覆盖目标场景的主流模型,还要有明确的精度和性能指标。每次改动硬件或软件,都跑一遍基准,看指标是涨是跌。这样既能及时发现回归,也能积累数据,为后续决策提供依据。我习惯把基准测试脚本化、自动化,每次提交代码或更新硬件模型都自动跑,省得人工比对。
这套软硬件协同的方法论,说起来都是常识,但真正做起来,每一个环节都有无数细节要抠。AI芯片这个领域,没有银弹,只有把每一步都做扎实,最后拿到的芯片才可能既跑得动模型,又跑得好。