恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
AI芯片软硬件协同设计:算子映射、微架构裁剪与编译栈闭环
首页
资讯中心
/
AI芯片软硬件协同设计:算子映射、微架构裁剪与编译栈闭环
AI芯片软硬件协同设计:算子映射、微架构裁剪与编译栈闭环
发布时间:2026/10/11 14:12:54
AI 芯片这行的热度从来没降过但我越来越觉得真正拉开产品差距的早就不再是单点电路设计的精巧程度而是软硬件咬合的那道缝。这个系列写到第 8 篇我们终于可以展开聊一聊那道缝到底怎么补。前面几篇我们把 AI 计算的负载特征、基础算子、硬件模块都过了一遍这一篇要做的是把算法、编译、架构、验证串成一个完整的软硬件协同设计闭环重点讲算子怎么映射到硬件、微架构怎么裁剪、编译栈怎么接得住、性能模型怎么算清楚。这篇文章整理自最新一轮某边缘侧 AI 芯片项目的设计复盘涉及从算法选型到 RTL 交付的完整过程。内容更适合已经在做 AI 芯片架构、编译器映射、算子库优化、或者做 AI 加速器产品的读者也适合刚入行的同学理解软硬件协同到底在协什么。我把踩过的坑、反复权衡过的参数、以及最后落到芯片上的具体方案都拆开讲希望能帮大家省掉几轮来回试错的成本。1. AI 芯片软硬件协同设计到底在解什么题1.1 计算负载的两副面孔卷积与 Transformer软硬件协同设计的第一步不是画架构图而是先把要跑的计算负载彻底想清楚。当前 AI 负载基本上分成两大类一类是卷积网络比如检测、分割、超分这一类视觉任务另一类是 Transformer 结构表现在大语言模型和一部分多模态模型上。这两类负载的计算形态差别非常大。卷积网络的计算密度高但卷积核尺寸小3x3、1x1、5x5 这些算子占大头数据复用模式很规整Transformer 的主干是通用矩阵乘和大矩阵乘特征图动辄几千个通道中间还要穿插 LayerNorm、Softmax、残差连接这些内存密集型的点算子。访存特征上卷积网络更偏向滑动窗口 局部复用Transformer 则是整块矩阵吞吐 中间结果反复搬运。所以当我拿到一个AI 芯片软硬件设计的任务时第一件事一定不是选 IP而是把目标模型列表的算子分布和访存分布拉一张表看看到底是 compute-bound 还是 memory-bound。这个判断决定了后面所有架构参数的选择。你可以在设计初期做一个简单的统计脚本把每个算子的 MAC 数量和访存字节数算出来再算操作强度FLOP/Byte然后对照目标芯片的算力带宽比就能大致判断哪些算子是瓶颈。1.2 大而全通用核为什么在 AI 负载上吃力通用处理器设计逻辑是用灵活的指令流去适配各种计算但 AI 负载恰恰是计算模式极度规整、但数据搬运极度频繁。如果你用通用核去跑一个 3x3 卷积指令流每一拍都在进行取指、译码、执行、写回流水线在不停切换上下文真正有用的乘加操作只占很小比例同时数据要反复经过寄存器堆和一级缓存能效比自然打折扣。专用架构DSA的思路正好反过来——把某种计算模式直接固化到硬件数据通路上。卷积在专用加速器里不需要一条条指令来驱动你可以做一个脉动阵列或者 MAC 阵列数据一拍一拍往里灌乘法累加结果一拍一拍往外吐控制逻辑少到一个有限状态机就能搞定。这个差异在能效比上可以拉到几个数量级。不过专用也不是越专越好。芯片一旦流片就不能改如果硬件的计算模式绑得太死等下一代算法模型出来就过时了。真正稳健的做法是把确定性最高的计算内核做成硬逻辑把容易变化的算子组合交给可编程的流水线去组织。也就是说硬件上要有固定计算单元 灵活数据通路这种组合而不是一竿子插到底。1.3 协同设计的分层地图在实际项目里我把软硬件协同拆成了七个层次模型/算法层、算子层、计算图编译层、指令/微码层、数据通路层、存储层级层、物理实现层。最理想的情况是七层全部一起联合优化但任何大团队都做不到一次全打通现实做法是把接口切成三段。第一段是算法到算子模型用什么算子组合能不能把多个小算子合并成大算子。第二段是算子到硬件行为算子怎么被循环切分、怎么映射到 MAC 阵列、怎么安排数据搬运。第三段是物理实现到性能归因RTL 跑出来的结果反馈给性能模型性能模型再反过来指导算子实现。这三段接口里最容易出问题的是第二段。因为算子和硬件之间存在巨大的语义鸿沟算子是高维的数学描述硬件是一堆带约束的寄存器和加法树。填这条鸿沟靠的就是编译栈和手写算子的映射策略这也是这一篇要重点展开的部分。2. 算子映射把算法变成硬件愿意跑的循环2.1 卷积算子的循环表达任何算子落到硬件之前第一步是把它表达成嵌套循环。一个标准卷积在 NHWC 布局下可以写成for n in range(N): for oc in range(OC): for ic in range(IC): for kh in range(KH): for kw in range(KW): for oh in range(OH): for ow in range(OW): output[n][oc][oh][ow] ( input[n][ic][ohkh][owkw] * weight[oc][ic][kh][kw] )这七个循环维度每一个都有对应的硬件含义。N 是 batchOC 是输出通道IC 是输入通道KH/KW 是卷积核空间尺寸OH/OW 是输出特征图空间尺寸。MAC 阵列没法直接吃这种多层嵌套循环它只能逐拍执行拿到两个数、乘一下、累加到一个寄存器这种最基本的操作。所以映射的本质就是把多层循环拍平决定每一层循环在哪一级硬件上跑。比如OC 和 IC 这两个维度天然对应 MAC 阵列里的行列方向一组乘积累加可以排成阵列的一列或者一行OH/OW 这种空间维度则对应特征图像素的推进通常会在时间维度上展开用流水线逐点扫过。我在实践中建议先做一个循环映射表把每个循环维度标注成三类空间映射映射到硬件单元的物理位置、时间串行按顺序逐拍执行、分块迭代拆成多个 tile 分批执行。这个表一旦画清楚整个硬件的数据通路轮廓就出来了。2.2 分块参数的权衡循环分块tiling是算子映射里最核心的一步它直接决定数据复用率。我拿一个实际例子来说明假设输入特征图尺寸是 IC16IH32IW32输出通道 OC32卷积核 3x3。整张特征图的计算量是 OC×OH×OW×IC×KH×KW按无 padding、输出 30x30 来算大约是 32×30×30×16×9 ≈ 414 万次 MAC。如果一次把所有数据都放进片上需要的存储量是输入 16×32×3216384 字节按 8bit、权重 16×32×94608 字节、输出 32×30×3028800 字节总共约 48KB。看起来不大但如果模型加宽到 256 通道、特征图到 224x224这个数字立刻膨胀到几 MB片上根本放不下。所以必须分块。分块时主要权衡三个数输入 tile 尺寸、权重 tile 尺寸、输出 tile 尺寸。我的推荐做法是优先把权重驻留在片上因为权重在不同空间位置上是完全复用的其次是输入行缓存采用行缓冲line buffer的方式沿宽度方向滚动避免重复搬运相邻窗口重叠的像素最后才是输出累加输出累加器如果要写会 DRAM会产生非常频繁的小粒度写操作最好在片上攒够一整块再写出去。以刚才的 16 通道 32x32 输入为例我在实际设计里会先把 IC 按 8 通道分块OC 按 16 通道分块这样权重一次只有 8×16×91152 字节轻松放在片上。输入每次搬 8×34×34多两行 padding 边缘约 9.2KB输出每次累计 16×30×30 约 14KB。这个分块比例下数据搬运次数可以降一个数量级。注意分块不是越小越好。块太小循环层数变多每次搬运的固定开销占比上升块太大片上放不下反而因为溢出到 DRAM 导致更多搬运。工程上经常用双缓冲块大小容量的一半这种经验值开头再用性能模型微调。2.3 访存复用与流水安排细化到访存复用层面三种典型策略需要根据算子形态来选择。输入驻留Input Stationary把输入数据固定在片上权重不断流入适合输入通道少的浅层卷积权重驻留Weight Stationary把权重固定在片上输入不断流入适合通道数多、核小的卷积实际芯片里最常用输出驻留Output Stationary让输出累加值待在加法树里适合输出通道非常多的情况。实际做映射的时候我通常混着用。比如 3x3 卷积第一层用输入驻留因为输入通道只有 3搬进搬出成本低后面通道数上来了改用权重驻留。这里没有统一答案要算过操作强度才能定。流水安排则是把不同循环层级的操作叠起来执行。前面分块后的块内循环可以做成软件流水第一块数据从 DRAM 搬进片上 SRAM 的同时第二块正在上一级缓存里等候第三块已经进入 MAC 阵列开算。这样访存和计算的重叠能显著提高阵列利用率。设计时建议给搬运引擎单独一套寄存器配置让它能预取下一块否则计算单元会频繁空等。2.4 Transformer 算子怎么落Transformer 的算子形态和卷积很不一样。以自注意力模块为例主要的计算是 Q、K、V 三个矩阵的投影然后是 Q 与 K 的批量矩阵乘、softmax、再与 V 批量矩阵乘。这里的矩阵乘动辄是几 K 维度的大 GEMM权重完全没法全部驻留必须靠以 K 维分块 流水线的方式做。在实际的某边缘侧芯片项目里我们把大 GEMM 按照 K 维度分成 64 或者 128 一大块每次计算一块拿到部分和再把所有部分和累加。这个过程中最关键的是部分和的回写策略——如果每算一小块就写一次外部存储总线会被打满正确做法是在片上维护一个较大的累加缓冲区把所有 K 分块的中间结果加完再写回。这个逻辑和卷积的输出驻留策略很像但 scale 不同卷积的累加器窗口是 3x3注意力矩阵乘的累加窗口是序列长度。硬件设计上要提前考虑累加器的位宽INT8 的重复累加如果位宽不够很快饱和。4096 个元素累加至少要预留 20bit 以上我一般直接做到 32bit 累加器避免验证后期发现精度问题要改 RTL。3. 微架构与指令集裁剪的实战参数3.1 MAC 阵列规模反推MAC 阵列的规模不是拍脑袋定的核心约束是目标算力、工作频率和面积功耗预算。公式很简单实际有效算力 MAC 数量 × 2 × 频率。拿某边缘侧目标来做示例要求 INT8 算力达到 4 TOPS也就是每秒 4×10^12 次操作。如果目标频率是 1GHz每周期需要完成 4×10^12 / 10^9 4000 次操作对应 2000 个 MAC 单元。这里的 MAC 是乘加一体单元一个 MAC 每周期做一次乘法和一次加法贡献 2 次操作。得到 2000 这个数之后就要规划阵列形状了。2000 可以排成 16×128也可以排成 32×64。阵列形状直接影响数据广播方式16×128 倾向于输入通道方向宽度大适合卷积核输入通道比较多的层32×64 则是输出通道方向更宽适合输出通道大的网络。我倾向选 16×128因为边缘侧模型输入通道通常不超过 128这样单次可以覆盖全部输入通道减少部分和的循环次数。实际设计中还要留余量。一方面要考虑到阵列利用率不会 100%一般卷积映射能到 70%~80% 就不错了另一方面要留 20% 左右的算力裕量给后续升级模型。所以目标 4TOPS 的芯片阵列侧我们按 2500 左右 MAC 规划排成 16×160 也考虑过最后因为布线和 SRAM 端口限制选了两组 8×160 的方案。3.2 片上存储层级与带宽预算MAC 阵列确定后紧接着就是片上存储。这里用到一个关键思路存储带宽永远要比算力先算因为算力再高数据喂不进去就是白搭。还是用 2000 个 MAC、1GHz 的例子。每个 MAC 每周期要读入两个操作数输入和权重所以理论上每周期要提供 2000×2×1 字节 4KB 的数据带宽INT8。1GHz 下就是 4TB/s。这个带宽如果全从外部 DRAM 拿根本不可能DDR 接口做到几十 GB/s 已经非常吃力了。因此架构上必须有足够的片上 SRAM 来把这个带宽消化掉。片上 SRAM 的分层级做法是第一层是寄存器级缓存紧贴每个 MAC 单元的局部寄存器文件存权重和输入像素第二层是阵列共享的 SRAM 缓冲通常叫 Buffer Bank第三层是全局共享 SRAM同时给标量核、搬运引擎和加速器阵列用。具体容量上我一般按阵列单周期吞吐 × 需要覆盖的延迟周期数来估算局部缓存。2000 MAC 阵列2KB 权重驻留 2KB 输入窗口差不多够中间 Buffer Bank 则要能放下至少一个分块 tile常见取 32KB 到 128KB全局共享 SRAM 通常做到 256KB 到 2MB 之间看应用场景。实际项目中SRAM 的面积和功耗占比非常大。8bit 存储每位大约 6 个晶体管1MB SRAM 在先进制程下就能吃掉不少芯片面积。所以存储容量设计要抠得很紧多出来的容量如果利用率低纯属浪费。3.3 指令集设计的取舍AI 加速器的指令集和通用 CPU 指令集完全不是一回事。CPU 的指令追求通用性而加速器指令追求的是用最少的指令描述最多的硬件行为。我们做的时候把指令分成了三类搬运指令、计算指令、同步指令。搬运指令解决数据从哪个地址到哪个地址要有源地址、目的地址、长度、突发模式这些字段。计算指令则描述在哪组阵列、用什么算子、输入来自哪组 Buffer、累加到哪组累加器。最关键的是同步指令——加速器是一个多级流水体系同步错了就会发生数据覆盖或计算错误。一个具体的例子双缓冲切换需要一条切换指针指令它不真正搬运数据只是让硬件从 Buffer A 跳到 Buffer B。如果编译器没有正确插入这条指令计算单元很可能读到上一轮的旧数据。早期我们在这上面吃过亏后来干脆在硬件里加了依赖检查计算指令里带上需要等待的搬运编号由硬件做互锁比完全相信编译器要稳健得多。指令编码宽度上我倾向定长 64bit 或 128bit。定长有利于预译码和多发射设计变长指令虽然省存储但在加速器里并没有显著收益反而增加控制复杂度。指令存储直接放 SRAM不设指令 Cache因为加速器的指令流是高度确定性的Cache 命中率再高也省不了多少功耗。4. 编译栈与运行时调度把算子变成喂给硬件的指令流4.1 算子融合ConvBNReLU 的合并优化模型计算图里有很多可以合并的小算子。最经典的组合是 Conv BatchNorm ReLU。在推理场景下BatchNorm 在训练完成后可以折叠进卷积权重里ReLU 则可以挂在卷积的加法树输出端直接做截断不需要额外一次访存和计算。折叠 BatchNorm 的计算很直接假设 BatchNorm 有缩放系数 gamma、偏移 beta、均值 mean、方差 var卷积权重为 w偏置为 b折叠后的权重 w w × gamma / sqrt(var eps)折叠后的偏置 b (b - mean) × gamma / sqrt(var eps) beta。这一步在编译期完成运行时完全不感知。ReLU 融合更偏硬件设计侧。在 MAC 阵列的输出加法树末尾加一个可选的门限处理逻辑当指令配置为 ReLU 模式时小于零的累加结果直接清零否则原样输出。这样处理完后融合前后算力的差距也许不大但访存次数差距非常明显如果不融合Conv 输出要写一遍 DRAMReLU 再读一遍、写一遍一次融合省掉两轮完整读写。4.2 算子到指令生成的两种路线编译栈里算子映射到指令有两个实现路线模板匹配和直接代码生成。模板匹配是给每个经典算子准备一个预定义的循环切分和指令序列模板编译器识别到 Conv、GEMM、Pool 这种算子就直接套模板改动空间小工作量低。直接代码生成则是根据硬件资源约束实时推导最优循环分块和指令序列灵活度高但编译器复杂度暴涨。实际工业产品基本都是混合路线。在我们这个项目里核心算子如 3x3 卷积、1x1 卷积、矩阵乘走模板匹配因为这些算子的最优分块模式很稳定其他边界算子走代码生成能覆盖那些模型里偶尔出现的奇葩 shape。为了减小编译器的工作量我还做了一件很关键的事在硬件层面收敛算子语义让卷积、矩阵乘、点卷积共用同一套数据通路和指令格式只是配置参数不同。这样编译器的后端只需要学一套指令生成逻辑覆盖率却大大提升。4.3 双缓冲与软件流水双缓冲是加速器里最经典也最容易被忽视的技术。单缓冲模式下搬运引擎和计算引擎必须严格串行搬完一块算完一块再搬下一块阵列的空闲时间占了接近一半。双缓冲让两个 Buffer 交替工作搬运引擎往 Buffer A 写数据的同时计算引擎从 Buffer B 读数据算两边并行阵列的利用率理论上可以接近 100%。实现双缓冲最朴素的方式是 Buffer 的地址加上一个 bank 位由一条同步指令切换。编译器要生成正确顺序的指令流先发搬运指令目标 Buffer 0再发同步指令等搬运完成然后同时发搬运指令目标 Buffer 1和计算指令读 Buffer 0这样流水就转起来了。软件流水里的核心问题是同步粒度。如果每条搬运指令都做一次同步同步开销会吃掉流水收益如果间隔太长又可能出现 Buffer 还没写完就被读。我们在硬件里加了一组scalar handshake register搬运完成时硬件自动置位计算指令读取时如果发现未完成则 stall。这样把同步成本从编译器挪到了硬件编译器只需保证指令发射顺序正确。这个设计让我在调试阶段省了非常多事。否则每改一轮模型编译器开发者都要重新排查同步问题人会被耗死。5. 性能建模与量化评估上 RTL 之前先算明白5.1 理论峰值算力与 Roofline 模型拿到阵列规格以后我习惯先把理论峰值写下来贴在工位上。有效算力按公式来INT8 峰值 MAC 数量 × 2 × 主频。以我们那组 8×160 的两组阵列共 2560 MAC跑到 1.1GHz 来算INT8 峰值就是 2560 × 2 × 1.1G 5.6 TOPS。但这只是理论值。实践里 Roofline 模型更能说明问题。Roofline 的思想是把算力顶线和带宽顶线画在同一张图里横轴是操作强度FLOP/Byte纵轴是可获得的算力。如果你的算子操作强度低于交叉点说明它落在访存受限区再增加 MAC 阵列也提不了速只有操作强度高过交叉点阵列才能吃饱。前面 16 通道 32x32 的例子操作强度大约 180 FLOP/Byte假设芯片访存带宽是 50GB/s、算力 5.6TOPS交叉点就是 5.6T / 50G 112 FLOP/Byte。180 大于 112说明这个算子还算计算受限阵列能吃饱。但换了浅层 3 通道输入的算子操作强度骤降就会掉进访存受限区这时该优化的是数据搬运不是加 MAC。这个模型最好用的地方是指导优化优先级。每来一个新模型我先拉一张操作强度表凡是低于交叉点的算子重点找访存问题高于交叉点的算子才做计算优化效率比自己瞎调高太多。5.2 周期精确仿真与硬件验证怎么配合性能模型说能干到多少最终还要仿真验证。我们在项目里分三级仿真行为级模型SystemC/TLM用来快速验证算法映射和指令序列对不对周期精确模型用来数每个算子的准确 cycle 数RTL 仿真用来验证时序和硬件行为一致性。三级仿真之间的关系是层层收窄、层层校验。行为级模型跑一轮完整的检测网络大概需要十几分钟周期精确模型要几个小时RTL 仿真一个 3x3 卷积的小 case 都可能要跑几个小时到一天。所以我的策略永远是先用行为级模型把映射思路调对再用周期精确模型挑性能瓶颈最后才用 RTL 验证选中的几个关键场景。这里有个经验教训仿真提速不能只看模型本身还要会裁剪输入。RTL 仿真不需要跑完整模型可以把一个典型算子的输入特征图裁成 32x32 的小图只要覆盖边界情况和 bank 冲突场景就够了。我们曾试图跑完整 1080p 输入的卷积仿真结果一周都没跑完纯属浪费时间。6. 常见问题与排查技巧实录6.1 仿真跑得慢怎么办这是所有 AI 芯片项目都会遇到的问题。一开始我们每个算子都想用 RTL 验证后来发现根本跑不动。三个经验第一行为级模型调好之前坚决不碰 RTL否则每个 bug 都要循环几小时第二测试 vector 一定要做最小完备集确保覆盖关键路径和边界但体积足够小第三利用编译器的中间输出做差分验证——行为模型和周期精确模型用同一份指令流对比每一拍的寄存器值不一致的地方就是 bug 所在。6.2 数据冒险与缓存一致性问题数据冒险在加速器里特别容易出现在同一块 Buffer 被搬运和计算同时访问的场景。硬件做了互锁后这种问题少了但编译器还是有坑循环变换时不小心改了指令顺序可能让本应等待的搬运被提前发射。排查这类问题我常用的方法是在周期精确模型里加 assertion搬运开始和计算读取时检查 Buffer bank 状态冲突就立刻报错。另外全局共享 SRAM 如果要被 CPU 和加速器同时访问必须约定所有权转移指令否则会出现 CPU 写了新权重、加速器却还在读旧权重的经典 bug。这个所有权逻辑建议做成硬件原子操作别依赖软件约定。6.3 Shape 爆炸问题模型里经常出现各种奇怪的 tensor shape有的层通道数是 24、有的是 48、有的是 80经常不是 16 或 32 的倍数。这时候如果编译器做循环分块的逻辑太死板就会生成大量低利用率的指令比如 48 通道的算子用 16x128 阵列去跑第二块只有 1/3 利用浪费严重。我们的做法是给编译器加了一层通道补齐 部分利用的优化。通道数不能整除时把 MAC 阵列按列分组允许部分列时钟门控关闭剩下的列照常工作。这个功能对硬件要求是在阵列里增加更细粒度的列使能信号设计难度不大但对实际模型的性能提升非常明显。6.4 功耗墙问题最后说功耗。算力堆上去以后功耗往往比面积更早触及天花板。INT8 阵列在先进工艺下每 MAC 大约消耗几十微瓦2400 个 MAC 在 1GHz 下就是几十瓦量级对边缘设备完全不可接受。所以我们做了两个关键优化一是阵列时钟门控空闲列自动关断实测利用率不到 50% 的时候省电显著二是动态电压频率调节把算子按照时延需求分成高中低三档电压访存密集的算子降频跑计算密集的算子升压跑。这个优化做下来整芯片能耗比提升了约 30%。写到这里也该收了。我自己在反复做完几轮软硬件协同设计之后最大的体会是大部分项目失败不是输在单个模块不够精巧而是输在接口处——算法没跟编译器对齐、编译器没跟硬件对齐、性能模型和 RTL 对不上。做软硬件协同设计最重要的产出其实不是某一块 RTL而是一整套从模型到芯片的可复现方法论先拉负载表再定 Roofline 交叉点然后选阵列规模和存储层次再设计最小化的指令接口最后用三级仿真层层验证。这套流程度过了我们项目中大多数坑希望也能帮到正在做同类事情的你。最后再分享一个小技巧每次流片或者 FPGA 验证回来第一件事永远是拿实测数据回头校准你的性能模型而不是急着庆祝或修 bug因为性能模型的误差一旦超过 20%后面所有架构决策都可能是错的。