朋友给我看过一块 AI 加速芯片的规格书,账面数据相当漂亮:MAC 阵列合计几十 TOPS 峰值算力,片上 SRAM 做了四级缓存式分层,外存接口带宽也标得很足。但再往下翻,软件那一栏只有两页 PPT,其中一个算子库的 Demo 还只支持固定尺寸输入。我当时的第一反应是:这块芯片回片了也跑不起来。AI 芯片的软硬件设计,真正决定成败的从来不只是数字电路和工艺节点,而是从指令集定义、编译器后端到运行时调度这一整条链能不能在流片之前就闭环。这篇文章没有打算讲一套放之四海皆准的方法论,而是把我在若干 AI 芯片项目里沉淀下来的设计流程、取舍逻辑和踩坑记录,按一条能复现的思路从头到尾梳理一遍。如果你正在做芯片架构、AI 编译器、算子库开发,或者刚拿到一块新 NPU 不知道从哪里下手,这篇应该能帮上忙。
1. 链条的起点不是 RTL,而是算法语义的拆解
1.1 我为什么要先写一个“什么都跑不了”的模型
很多芯片团队的习惯是先把硬件框图画出来:这里放一个矩阵乘引擎,那里挂一个向量单元,旁边配上一块 DMA,然后就开始写 RTL。这种做法在传统 SoC 里问题不大,但在 AI 芯片上行不通。原因很简单:AI 算法不是由离散的固定功能模块堆出来的,而是由张量算子组合出来的,算子之间的数据布局、依赖关系和复用模式,直接决定了计算阵列到底能不能真正跑满。
我现在的习惯是把设计流程反过来。拿到一个 AI 芯片项目的第一周,不碰 RTL,先写一个系统级模型。这个模型的精度不用细到时钟周期,但要把每个算子的行为、数据吞吐、内存访问模式描述清楚。说白了,就是用一两百行代码把算法图里每个 Tensor 的尺寸、每个算子的计算量、每段数据在片上和片外之间搬了几次都记账记下来。
这个模型的核心价值是把“算法需求”翻译成“硬件设计约束”。比如你在模型里看到某个卷积层需要反复读取 3×3 的权重,权重总量不大但使用频率极高,于是片上就应该留一小块紧邻计算阵列的常数缓存,而不是把权重和中间特征图混在一个大池子里。这种决定如果等 RTL 写了一半再改,成本非常高;而在系统级模型里改一行参数就能评估。
系统级模型还有另外一个隐藏作用:它是算法团队和硬件团队之间唯一的共同语言。算法的人拿 PyTorch 说话,硬件的人拿波形说话,两边直接聊一定崩。我在模型力里定义好算子语义和数据流之后,算法团队可以把模型里的近似运算、量化误差边界直接写进约束文件,硬件团队再拿这些约束去做微架构,至少不会出现“硬件做了软 ReLU,算法用的却是带斜率的泄漏版本”这种低级错位。
1.2 算子清单是一份软硬件谈判协议
拆解完算法语义之后,你手上会得到一份算子清单:卷积、全连接、归一化、池化、各种激活函数、逐元素操作、缩放平移、拼接切分,诸如此类。这份清单不要急着转成硬件单元列表,我习惯先把它当成一份“软硬件谈判协议”来处理。
所谓谈判,就是要决定每个算子到底在哪儿实现。有些算子适合做成专用硬件单元,比如矩阵乘;有些算子用向量指令序列组合更划算,比如 LayerNorm 这种需要跨通道求统计量的操作;还有一些算子本质上就是搬运数据,比如 transpose、reshape,应该交给 DMA 和地址生成器,而不是让计算单元傻等。
我把这种决定做成一张表格,每次架构评审会都摊开来看。表格上一列是算子名,一列是它在芯片上的映射位置,一列是数据特征,一列是软件侧的调用方式。这张表看起来非常简单,但它其实就是指令集架构的雏形。一个 AI 芯片的指令怎么设计,归根结底就是这张表里每一行的通过率、延迟和数据吞吐决定的。
这里有一个我踩过的坑:算子清单本身会漂移。某算法团队为了调精度,把一个普通的卷积换成了可变形卷积;或者为了省算力,把激活函数换成了带近似查找表的版本。如果软硬件团队没有建立“算子变更 = 架构变更”的意识,硬件单元就会在半年后突然暴露出来一堆匹配不上的算子,实际利用率断崖式下跌。所以后来我要求算子清单必须纳入版本控制,每次改动都要过评审——这不是为了卡算法团队,而是为了让他们知道,芯片上的每一寸面积都是根据某几行算子语义做的承诺。
2. 计算引擎的选择:MAC 阵列、脉动阵列还是可重构数据流
2.1 三种计算范式的差异落在哪
AI 芯片的计算引擎有几种主流选择。我见过不少团队一开始迷信某种高大上的结构,等软硬件联调才发现控制逻辑、数据流组织和编译器后端全都跟不上。
简单来说,MAC 阵列是最直观的方案。一组乘累加单元按行和列排成网格,每个周期做若干次乘加,数据从一个方向灌进去,结果从另一个方向流出来。它的优点是设计简单、控制逻辑直接,编译器好写——你甚至可以直接按普通循环嵌套的方式生成调度。缺点是数据复用主要靠软件显式搬数,片上互联一旦复杂一些,布线和时序就会很难受。
脉动阵列把数据复用做进了结构本身。数据在阵列里像水流一样沿着固定方向流动,每个 PE 把收到的输入和权重做乘加后传给下一个 PE。这样一来,同一份数据可以被多个 PE 复用,搬运数据的次数大幅下降,功耗也更好看。但它的问题是有很强的“形状假设”。脉动阵列最适合规则的数据流,一旦碰到稀疏化、动态形状、非规则卷积,编译器就要花很大力气做数据重排,否则阵列一半的 PE 在空转。
可重构数据流走的是另一条路。它把计算单元和互联结构做成可配置的,算法图可以比较直接地映射到硬件拓扑上,灵活性最高。但代价是:控制复杂度陡增,指令和配置的上下文切换成本高,软件团队需要写专门的映射工具。这个方案目前在学术圈和少数产品里能看到,工程上对团队能力要求非常苛刻。
这三种方案没有绝对优劣,差异本质是“用硬件结构换数据复用”和“用软件调度换硬件灵活度”之间的权衡。
2.2 峰值 TOPS 和有效 TOPS 不是一回事
规格书上的 TOPS 只是纸面数字,实际运行效率取决于另一组问题:数据能不能按阵列希望的节奏到位?控制逻辑能不能及时切换操作?片上资源能不能在计算单元等待数据时干别的活?
我习惯算一个指标叫“有效算力利用率”,公式很简单:在一段有代表性的 workload 上,实际完成的有效运算量除以理论峰值乘以运行时间。这个数字往往让人清醒。比如一个峰值 20 TOPS 的核,跑标准卷积时可能只有 60% 利用率,跑深度可分离卷积直接掉到 25%。原因不是硬件不行,而是深度可分离卷积的计算密度太低了,一个字节能摊到的算力很少,存储带宽成了瓶颈。
我之前做过一个项目,硬件团队花了很多精力把 MAC 阵列的峰值翻了一倍,结果在跑某个主流检测网络时,端到端速度几乎没有变化。后来复盘下来,瓶颈根本不在计算单元,而是在权重和中间特征图的缓慢内存搬移上。计算阵列每算一次要等很久的喂数,阵列越快,空转反而越明显。这就是典型的只优化硬件不顾软件行为导致的反效果。
所以现在我在架构评审时,要求每张算力报表都要附带一个“利用率达成条件”的说明:什么样的数据布局、多大的 tile、什么样的调度,才能摸到纸面峰值的八折。这个说明不是空话,它直接定义了编译器后端的目标函数——硬件给软件提约束,软件反过来告诉硬件哪里该优化。
2.3 调度和控制开销是隐藏的算力杀手
还有一个容易被忽略的点:指令调度的切换成本。很多 AI 芯片不是一次性把整张网络加载进硬件的,而是按算子、按 tile 逐步执行。每切换一次算子,就要重配寄存器、搬移描述符、等待流水线排空。这部分时间如果积累起来,会让有效算力再打折扣。
我见过一个设计,单算子调度开销做到了几十个周期,听起来不多。但一个网络几百个算子,每个算子又要分几十个 tile,加起来就是几百万个周期的纯调度等待。对一颗周期频率只有 1GHz 左右的芯片来说,这相当于直接浪费了几毫秒。把调度开销从几十个周期压到个位数,和把 MAC 阵列面积翻倍,哪个划算,不言而喻。
软件侧能做的,是把多个算子的配置提前打包成指令链,用硬件队列做预取;硬件侧能做的,是支持后台重配置,让当前算子还在算的时候下一个算子的描述符已经就位。这两件事必须从第一天起就一起设计,否则后期只能在性能和面积之间痛苦地取舍。
3. 编译器后端和硬件微架构是一对必须同时评审的孪生兄弟
3.1 工具链三段论不是新鲜事,但 AI 芯片有它的特殊性
编译器本身的架构并不神秘:前端负责解析网络模型,中间层做计算图优化和量化处理,后端负责把优化后的图映射成芯片上的指令序列。真正难的是后端映射,因为芯片指令的目标不是通用处理器,而是一个有存储层次、数据复用规则和并行边界都很特殊的加速器。
后端映射要做的事情可以拆成三步。第一步,把算子树切分成硬件能“一口吃下”的块。这个块的大小取决于片上 SRAM 容量和 MAC 阵列的尺寸,就叫 tile。第二步,把每个 tile 里的数据布局转换成硬件指定的格式,比如把 NCHW 排布转成更适合 DMA 搬运的 CHW 分块。第三步,为每个 tile 生成配置序列,包括地址、步长、计算模式和控制信号。
这三步里输入数据布局和调度策略,如果硬件微架构设计时没想清楚,编译器怎么写都别扭。所以我要求编译器后端架构师在微架构定义阶段就参与评审,甚至拥有一票否决权。硬件团队觉得某个结构很优雅,但如果编译器后端评估后认为调度复杂度指数上升,就必须改。
3.2 算子融合:一张网络省掉一半内存搬移
我拿一个最典型的算子融合来演示编译器后端的作用:卷积后面的归一化。如果按原始计算图一个算子一个算子执行,卷积的结果要写回片外 DRAM,归一化再从 DRAM 读出来,一个几兆字节的中间张量被来回写读,非常浪费。
融合之后,编译器在单个 tile 的层面把归一化的均值方差参数“折进”卷积的乘加结果里,整个中间张量只需要留在片上 SRAM 里,省掉一次写出和一次读入。内存访问量下降,效果立竿见影。推广开来看,激活函数、缩放、偏置、残差相加这类逐元素算子,都可以和前面的大算子融合进同一个硬件调度。
但融合不是白送的。它要求硬件提供一条数据回读或寄存器直连的通道,让算完卷积的 PE 结果不必经过片上通用缓存就能进入逐元素单元。如果硬件在设计时没有预留这条旁路,编译器融合之后反而要绕更远的路,性能不升反降。这就是我反复强调“硬件要给软件留后门”的原因。
融合还有精度问题。很多融合实现会把归一化在数学上等价地变换成乘法和加法,但如果硬件用的浮点格式或累加顺序和软件不一致,融合前后的结果会有微小差异。训练阶段可能无所谓,推理阶段做量化对齐时会成为 bug 的来源。所以编译器后端必须维护一份“精度变化记录表”,每个融合操作都标注数学等价性和误差界。
3.3 片上存储分配和指令调度是同一个问题
AI 芯片的片上内存不是均匀的,它通常被切分成一块块 scratchpad、缓冲区和 cache。编译器后端要做的存储分配,本质上是一个资源分配问题:给每个 tile 的数据计算好生命周期,决定它什么时候进哪个缓冲区,什么时候可以被覆盖。
我见过最经典的失误是只做了地址分配,没有做生命周期重叠分析。每个算子都往固定的几个缓冲区写数据,排布上互不重叠,结果同一块 SRAM 的使用效率极低,大量数据不得不提前搬到片外。后来引入 liveness 分析,让编译器能把生命周期不重叠的中间张量重叠存到同一块 SRAM,中间结果留在片上的比例大幅度上升。
指令调度和存储分配必须一起做,因为调度的顺序决定了数据的生存时间。如果你把 A 算子的 tiling 顺序和 B 算子的 tiling 顺序对齐,B 算子每算完一个小块,A 的结果就可以被复用并立即释放,存储需求大减。这个优化没有硬件改动,纯粹是编译器后端调度,就能省出很大的片上空间。
4. 存储墙不算硬件问题,而是软硬件合填的一张分配表
4.1 片上 SRAM 和片外 DRAM 的边界在哪里
AI 芯片的存储层次其实就两级:片上 SRAM 快但小,片外 DRAM 大但慢。设计问题等价于:哪些数据必须留在片上?哪些数据可以允许慢速访问?哪些数据甚至可以不落内存直接流过去?
我习惯把网络里所有张量按“重用距离”分类。如果一个数据在短时间内会被反复使用,比如卷积权重,所有者就要把它的生命周期画在片上;如果一个数据只用一次,比如前一层的激活输出,它应该走流式路径,而不是在一个固定的缓冲区里攒着等下一次访问。这个分类不是硬件单方面决定的,它高度依赖编译器后端怎么调度算子。
举个例子,假设片上有 1MB SRAM,某卷积层的中间特征图是 2MB。方案 A:把特征图拆成两个 tile,分两次处理;方案 B:换一种数据布局,让权重常驻片上,特征图逐行流出,把存储需求压到 600KB。两种方案在硬件上运行的是同一批 MAC 单元,但 B 的内存压力和调度复杂度明显更优。这里的选择权不在硬件,而在软件。
4.2 一次带宽预算的实际演算
我把一次真实的带宽预算过程简化一下,方便你理解标定过程。假设你有一个卷积层,输入通道数 128,输出通道数 128,特征图尺寸 64×64,卷积核 3×3。单层计算量大约 128×128×3×3×64×64,也就是大约 6 亿次乘加。如果用 1GHz 主频、256 个 MAC 的阵列去算,需要大约 2300 个周期,约 2.3 微秒。
但数据访问方面的需求呢?输入特征图是 128×64×64 个元素,假设每个元素 2 字节,大约 1MB;权重是 128×128×3×3×2 字节,约 300KB;输出特征图又是 1MB。如果这些数据全放在片外,按常见的 32GB/s 有效带宽计算,搬运一次输入就要 32 微秒——是计算时间的十倍以上。
于是这里就看出积蓄存储容量的意义。如果片上 SRAM 能装下权重和一块输入 tile,并把输出在片上累积一小段时间,那么片外带宽的需求会骤降。实际工程里我会画一张表,列出不同 tile 尺寸下的片外访问量和计算时间,再选折衷点。这就是“存储墙分配表”,软硬件团队各拿一列,合起来才是完整的决策依据。
4.3 数据布局转换是不可忽视的软件开销
AI 计算里最常见的数据排布是 NCHW,也就是按通道、高度、宽度排。但芯片上的 MAC 阵列通常希望数据按特定的小块排布,比如每个通道块同时送进多个 MAC 单元,或者把 4 个通道交织排列以利用总线位宽。
布局转换不是免费的。它涉及把数据从一个访存方向拷贝到另一个方向,期间还要做地址重排。我见过的情况是,布局转换开销占到整个网络运行时间的 15%,而且它不像算子那样有直观的计算密度,所以容易被忽略。如果硬件能提供高效的转置 DMA 或者地址模式重映射机制,编译器就能把转换开销控制得很好;如果硬件只支持线性搬移,软件就必须在调度中找机会,把转换和计算重叠起来。
这类“要不要在硬件里做支持”的问题,正是软硬件一体化设计的最典型议题。硬件提出一个低成本的转置模式,软件侧立刻能消除掉整个布局转换的等待时间,这是会在架构评审中反复出现的共赢局面。
5. 回片之前,软件必须已经“假装”芯片存在
5.1 模拟器、仿真器和 FPGA 原型各有各的用处
流片周期很长,如果等真芯片回来才让软件团队进场,半年时间基本就打水漂了。所以我们会在回片前搭三个层次的软件运行环境,从快到慢、从粗到细依次排开。
第一层是指令级模拟器。它的功能单元用 C 语言建模,不追求时钟精度,但指令语义必须和最终芯片一致。软件工程师可以在上面跑完整的网络,调试算子库逻辑,校准数值精度。第二层是周期近似仿真器,或者干脆用 RTL 仿真环境跑一段 Tiny 模型,把时序时序的关键行为暴露出来,用来验证缓冲冲突、带宽瓶颈这类周期敏感问题。第三层是 FPGA 原型平台,把设计综合到 FPGA 上跑真实 workload,速度比仿真器快几个数量级,但资源有限,通常只能做一些切分后的验证。
这三个层次不是递进关系,它们是并行的。指令级模拟器要支撑算子库开发;周期仿真要支撑架构调优;FPGA 原型要跑端到端 demo,用于给算法团队早期反馈。每一层都有各自的建模精度和速度约束,任何一层缺失,都会导致回片后的问题集中爆发。
5.2 Bring-up 不是调试硬件,而是调试接口契约
真芯片回来后,最常见的错误期望是“一试就通”。我的经验是:第一件事是先跑那些极其简单的功能点——写一个寄存器、读回一个寄存器。这一步看起来 trivial,但它能确认总线接口、时钟复位和寄存器访问都符合预期。然后跑一个单像素的数据通路测试,把一个数从 DMA 搬到片上再读出来,确认地址映射和字节顺序没错。接着跑一个最小 MAC 阵列测试,比对一个已知输入和黄金模型输出,确认硬件计算语义和软件一致。
每跑一层,就相当于验证一层软硬件之间的接口契约。如果第几步不通过,问题可能出在硬件 bug、软件 bug 或者接口定义歧义三者之一。最忌讳的是跳过单功能测试直接跑一个大网络,那样一旦结果不对,你根本不知道问题出在哪里,只能靠猜。
我记得有一次,一个算子库测试在模拟器上完全正常,回片后输出一直差几百。查了两天才发现,硬件实现的累加顺序是分块累加,而模拟器用的是普通顺序累加,浮点误差累加出细微差异。从那以后,数值一致性测试就成了 Bring-up 流程里的必备项,点位数据一致但误差在阈值之内才是“通过”,而不是只看像素级结果完全一致。
5.3 回归测试集的配置方式要提前定
软件团队在回片前还需要准备好回归测试集,配置上要考虑三点。一是覆盖度和多样性:不仅要覆盖每个算子单独正确,还要覆盖融合后的组合算子、不同 tile 尺寸、不同通道数、不同对齐边界。二是数值容差:要在文档里明确误差上限,不能拍脑袋。三是一键执行能力:所有测试最好能在一条命令下跑完,输出结果自动对比。
回归测试集同时还是硬件验证团队的输入。硬件 RM(参考模型)的比对、形式验证的环境,都直接复用这套数据集。这样一来,软件测试、硬件验证和编译器后端开发三组人马用的是同一批基准,口径一致,联调的时候就少了很多吵架。
6. 贯穿始终的几条教训,越早明白越好
6.1 不要急着冻结需求,但要快速冻结接口
很多 AI 芯片项目死在需求蔓延上。今天算法团队说想支持动态形状,明天架构师说想加稀疏加速,后天市场又提出一个新的模型类型。我的建议是:需求永远可以滚动,但软硬件接触的接口协议必须尽早冻结。算子语义、数据格式、指令编码、寄存器映射,这些一旦定下来,后面再改,成本会指数上升。需求变了,可以在接口之上加扩展,但不能让最底层契约三天两头变。
6.2 数值精度是算法、编译器和硬件三方的共同问题
量化格式、浮点累加顺序、近似函数逼近精度,这些看起来像细节的东西,联合调试时全都成了拦路虎。算法团队觉得硬件是黑盒,硬件团队觉得算法精度是算法的人自己调的。但只要有一次跨平台部署的精度对不上,这个责任划分就会立刻变得模糊。正确做法是:在项目初期就成立一个三方参与的精度工作组,把每个算子在不同实现下的误差上限写清楚,并留出联调时的对照测试用例。
6.3 软件评估应该参与架构取舍,甚至一票否决
硬件团队经常喜欢某个预取策略或者某种互联拓扑,因为它在某些 benchmark 上表现好。但如果编译器后端评估后认为调度复杂度太高,或者运行库的开发量过大,我宁可牺牲一点理论性能。因为一个没法由软件高效驱动的硬件结构,最终只能在演示 Demo 里灵光一现,落到真实产品里就变成硅背面上的小面积摆设。
6.4 AI 芯片软硬件设计最终交付的是一套能力,不是一颗裸片
回片只是起点。真正交付给用户的,是能跑得动模型、能耗达标、精度可接受、开发工具链顺手的整体能力。这也是我为什么越来越推崇“软件先行的设计哲学”:先把软件栈建好,让硬件在软件的环境里去完成验证和迭代,比反过来在真芯片上等软件成熟要靠谱得多。
我这些年最大的体会是,AI 芯片是一个典型的“木桶工程”,MAC 阵列再快、SRAM 再大、编译器再聪明,任何一块短板都会让整体表现塌下来。软硬件一体化设计的核心,不是让一方迁就另一方,而是让两个团队在同一个模型、同一套接口约定、同一张分配表面前,把“算得快”和“用得上”当成同一个目标来追求。落地到日常,就是多开联审会、多维护统一文档、多设一些跨团队可执行的自动化检查——用流程把分歧消灭在设计阶段,而不是等回片后在测试室里互相猜疑。