AI 芯片的软硬件设计这件事我前后参与了几个完整项目从架构定义一路跟到软件工具链落地最大的体会就是芯片流片回来只是开始配套软件能不能把硬件“伺候好”才决定这颗芯片是样片还是产品。市面上讲AI芯片架构的文章很多但真正聊“硬件和软件边界在哪里”的很少。这篇文章想讲的就是把一个 AI 芯片项目推进到中后段时那些绕不开的软硬件协同设计问题包括硬件侧的计算阵列、存储层次、互联方案软件侧的编译器、量化、运行时以及两边对不上的时候怎么定位、怎么解决。内容大致对应这类项目里“第十个迭代节点”前后团队要啃的硬骨头适合在做的工程师、系统软件开发者也适合想入行异构计算的高年级学生。我不会在这儿堆一堆架构名词然后戛然而止而是尽量把每次决策背后的原因说清楚比如为什么这里选脉动阵列不选SIMD为什么编译器要管内存排布而不是只生成指令。这些细节平时散落在会议纪要和调试记录里很少被整理成文今天一并写出来。1. 先捋思路AI芯片的软硬件设计到底在设计什么1.1 芯片的成功标准不是“流片回片”而是“能快乐地跑起模型”很多项目在立项阶段就搞反了一件事把流片当作终点。实际上一颗AI芯片流片回来如果配套编译器不能把常见模型高效映射上去如果操作系统的设备驱动和运行时调度器不稳定这芯片就是一块昂贵的砖头。我见过不止一个项目芯片功耗和面积都控制得不错结果卡在软件栈交付上最后项目评估非常被动。所以现在业内普遍强调 hardware/software co-design也就是软硬件协同设计。这不是一句口号而是具体的工作分工硬件架构在定义寄存器、存储结构、算力阵列的时候就要把软件的写法考虑进去软件在写编译器、算子库的时候也要反过来给硬件提需求。举个例子硬件团队决定把片上SRAM容量从 4MB 扩到 8MB听起来只是面积和功耗增加但软件层面的影响是编译器能否把更多中间张量留在片上避免频繁把数据搬回片外DRAM。如果容量翻倍能把某几个关键模型的片上命中率从 60% 提到 90%那这个面积代价就非常值。这个测算必须在架构定义早期就做等RTL冻结了再改存储架构代价是按人月算的。软硬件协同设计落实到工程上要明确三件事第一硬件寄存器接口和软件内存视图要提前约定第二硬件提供的抽象原语比如DMA描述符格式、同步机制必须能被编译器简洁地表达第三性能和精度的回归基准不能只有两三个模型至少要有覆盖图像分类、检测、分割、自然语言处理的一整套基准集。1.2 一个 AI 芯片项目的典型推进节点讲到“第十个迭代节点”阶段经历的节奏其实是类似的第 1-2 轮明确目标场景确定算力指标和功耗budget选择制程。第 3-4 轮架构探索用性能仿真器和已有模型基准做数据流模拟权衡计算阵列形态和存储层次。第 5-6 轮RTL实现与验证同时软件团队基于虚拟原型开发编译器和运行时。第 7-8 轮FPGA验证和emulation平台联调把编译生成的代码跑在硬件模型上开始真正的软硬件对账。第 9-10 轮性能优化、精度对齐、软件工具链打磨为流片做最终准备。“第十个节点”基本对应的是“性能压测寄存器级调优 软件语义正确性收敛”的阶段。这个阶段最累因为问题不再是单个模块能不能跑而是整条链路从模型输入到硬件计算结果在各种边界情况下能不能保持一致、性能和精度指标是否达标。2. 硬件侧的核心决策计算阵列、存储与互联2.1 计算阵列怎么选脉动阵列、SIMD还是“大小核”AI芯片的核心计算资源主流方案无非三类脉动阵列systolic array、SIMD向量单元、以及“大算力阵列小向量核”的混合结构。三者各有适用场景不是越高端越好。脉动阵列的做法是让数据在二维的PE处理单元阵列里像波浪一样流动每个PE只管本地的乘累加数据从相邻PE传来。它的最大优势是数据复用性好读片上的开销低非常适合卷积这类计算密集、权重复用高的算子。缺点也明显矩阵形状必须规整遇到稀疏、动态形状的算子比如某些NLP模型里的Gather操作利用率会掉得很快。SIMD向量单元则是每个周期处理一组向量数据灵活性高适合元素级操作和形状不规整的算子但数据搬运和寄存器压力更大。混合结构是当前很多SoC型AI芯片的选择主计算用脉动阵列扛卷积和矩阵乘旁边配一组迷你向量核处理激活、归一化、池化和一些标量逻辑。选型层面我的建议是不要拍脑袋。拿目标模型集在性能模型上分别模拟三种方案的吞吐和利用率再看面积和功耗预算匹配度。在第一版架构中不妨保守一点主阵列规模按最常跑的三五个模型定别为了追峰值算力把阵列做大。峰值算力宣传好看实际应用跑不出来是这个行业最常见的尴尬。2.2 存储层次设计片上SRAM与多级缓存的数据流约定AI芯片的存储设计是最能体现软硬件协同水平的地方。片外DRAM带宽宝贵一片高性能AI芯片如果在计算单元高负载时不断因为缺数据而stall那算力再高也白搭。所以片上存储通常是SRAM负责缓存权重、输入和中间结果容量规划要结合编译器实际能产生的数据复用模式来定。存储层次一般分两级一级是紧贴计算阵列的寄存器级buffer做数据的暂存和重排二级是容量较大的共享SRAM像一个L2 cache但通常由软件显式管理而不是硬件自动缓存。显式管理听起来麻烦但好处是编译器可以精确知道数据何时被搬到片上、何时可以释放。硬件自动cache管理听起来省心但AI工作负载的访问模式高度可预测硬件猜不准的反而不如软件算得准。在寄存器接口设计上最关键的一点是给软件提供灵活的DMA描述符支持多维度张量搬运、跨步访问、甚至简单的地址拼接能力。我第一次做芯片时觉得DMA只要支持一维搬运就够了结果软件团队为了把一个 [1,64,56,56] 的tensor拼成适合阵列的输入格式写了一堆冗余搬运带宽消耗翻倍。后来专门在DMA里加了2D搬运支持软件代码简化了很多带宽也下来了。2.3 片上互联与SoC集成别小看数据搬移很多人关注计算阵列我却更重视NoC网络片上互联和总线设计。AI芯片里最容易被低估的瓶颈就是数据搬移。计算峰值提上去了数据还在片外或者从一个子系统搬到另一个子系统的路上花的时间比计算还长。片上互联设计有三个具体建议第一区分高带宽流和低带宽控制流。计算阵列到SRAM之间的通路要尽量宽、尽量短最好做成专用的高带宽直连而CPU核、外设、调试接口这些低频率访问走通用总线不要挤占高带宽通路。第二重视多核情况下的“广播”和“聚合”模式。数据并行切分到多个计算核时每个核都需要读一份权重或者处理完以后需要把部分和合起来。如果NoC不支持高效的广播/规约操作软件就会被迫加上大量同步和中间拷贝这个损耗在跑大模型时会被放大。第三给软件留足够的同步原语。硬件至少提供栅栏barrier、事件event、中断或轮询寄存器几种同步手段。编译器生成异步流水线时最怕的就是同步代价太高。如果同步一次要几十个周期小算子的异步化就完全没有收益。这个数字在架构阶段就要测出来。3. 软件栈编译器、量化与运行时3.1 编译器把算子映射到硬件而不只是“翻译一下”AI芯片的编译器核心工作是把计算图里的算子映射到计算阵列上并且尽可能压榨硬件的利用率。以卷积为例编译器要决定循环展开的顺序哪些维度在空间上展开成PE阵列的并行度哪些维度在时间上循环复用权重和输入分别以什么方式搬运到片上。这个决策直接与硬件的物理结构绑定。具体说编译器要知道三件事PE阵列的行列维度、片上buffer容量、DMA的搬运能力。如果编译器对这三件事一无所知只能生成通用代码那硬件的利用率大概率在 10%-30% 之间徘徊。图优化层面最常见也最有效的两个pass是算子融合和布局转换。算子融合把卷积后接的ReLU、Add等操作尽量合进一次计算流程减少中间张量落片外的机会布局转换则统一不同算子间的张量内存格式比如把默认的NCHW排布转成更适合计算阵列的NHWC或tiled排布。这些优化看似平平无奇但对最终性能的影响往往比指令级优化更显著。3.2 量化不是简单截断精度和性能的平衡AI芯片普遍靠低精度数值格式来换取吞吐提升最常见的是INT8和FP16更激进的有INT4、INT2。但低精度不是“把fp32模型的权重截一下”就完事这里面精度敏感层、混合精度、量化感知训练三件事必须配合起来做。量化工作流里踩过最深的坑是“per-tensor vs per-channel”。刚开始图省事整个tensor共享一组scale和zero_point结果某几个通道的数值范围差异很大小通道直接被噪点淹没模型精度掉得离谱。改成per-channel之后图像分类模型的精度损失从 3% 降到了 0.8% 以内。代价是硬件和编译器都要支持按通道读取scale好在绝大多数现代AI芯片都支持了这个特性。另外务必建立一个精度回归自动化框架。每次代码改动后把十几个代表性模型端到端跑一遍对比fp32的基线输出统计最大误差和平均误差。这个框架要足够快最好在半天内能出全套报告。没有它精度问题会藏在代码深处等流片前才暴雷。3.3 运行时与内存生命周期的管理运行时runtime承担任务调度、内存分配、DMA管理和同步。它和操作系统驱动不同runtime更接近“用户态库”负责把编译产物变成一次真正能在硬件上执行的作业。内存生命周期管理是软件栈里最容易失控的部分。AI推理时分了几大块内存输入缓冲、权重缓冲、中间张量区、输出缓冲。中间张量的生命周期很短如果不复用会出现内存耗尽或者频繁申请释放的问题。好的做法是编译器在编译期就做内存规划给每个中间张量分配固定偏移运行时只维护一个大内存池按静态规划好的布局使用。任务调度也要考虑多硬件队列。如果硬件只提供一个执行队列软件侧没法同时跑多个自媒体算子或者做流水线重叠比如DMA搬运和计算重叠。所以硬件设计早期要预留多队列或异步指令通道。这个需求如果不提给硬件团队后面软件做流水线就是空中楼阁。4. 软硬件协同设计的关键流程与实操细节4.1 从基准集出发的架构探索架构探索阶段很多人喜欢拿几个“标杆模型”手动排playground脚本但这样做很容易漏掉实际部署时的边界条件。我更倾向于准备一套分层的基准集L1单元算子级比如单个卷积、单个矩阵乘用来测峰值性能。L2端到端模型级5 到 8 个主流模型覆盖CNN、轻量级网络、Transformer结构。L3系统级场景比如连续多次推理、多路并发推理用来测稳定性和调度能力。每次架构调整比如改变PE行数、调整SRAM分区方案都必须在这套基准集上跑完整轮对比。不能只看单个算子的提升因为某种改动可能提升了卷积乘加效率却导致编译器无法做张量布局转换反而整条模型性能下降。这个阶段仿真器精度很重要。性能仿真器至少要能做周期级估算允许配置ALU吞吐、存储带宽、访问延迟这些参数。内部模型误差控制在 15% 以内就基本可信但如果误差超过 30%先别急着优化架构回去校准仿真器。4.2 Emulation和FPGA验证提前暴露软件问题在RTL真正可跑之前软件团队往往拿着一个“模拟器”在开发但模拟器对时序、带宽的模拟通常很粗略。等RTL ready后FPGA原型和emulation平台成了软硬件集中联调的战场。这个阶段最常发现的问题不是功能错误而是协议理解不一致。举例硬件团队定义某个状态寄存器表示“任务完成”但没写清楚这个状态是“最后一次写请求已接收”还是“所有数据已在片上且结果可读”。软件团队按后者理解结果拿到的部分数据还是脏的。刚开始觉得是软件时序问题最后排查到协议文档的语义歧义上。自此我在每个新项目启动时都会要求硬件团队把寄存器icon尽量描述成“可测试的断言”由软件和验证团队共同评审一遍寄存器手册再冻结接口。FPGA验证还有一个作用跑真实模型收集实际DMA占用、buffer冲突事件反哺编译器优化。很多在理想模型上看起来合理的调度策略在真实硬件上会因为竞争和同步开销而失效。这时需要通过运行时统计接口performance counter找到热点再回到编译器里调整。4.3 代码生成、算子库与手工优化如何分工编译器自动代码生成和手工算子库不是非此即彼的。我的经验是核心高频算子卷积、矩阵乘、某些常见融合模式用手工算子库保证性能下限其余非常规频繁算子走编译器自动生成保证覆盖度。编译器慢慢变聪明之后再逐步把手写算子的规则内化为templates或者转译规则。这里有个很重要的工程原则算子库和编译器要共用同一套“硬件描述文件”比如PE阵列大小、buffer容量、DMA能力避免两边各维护一份配置然后悄悄不一致。我见过因为硬件描述文件分裂手工算子库以为SRAM是8MB、编译器以为4MB导致性能完全对不上且难以定位。统一配置文件之后这类问题少了很多。另外要预留好profiling机制。芯片上至少要有足够的性能计数器统计阵列利用率、DMA带宽占用、buffer miss次数、同步等待周期。没有这些数据所谓“性能优化”就是瞎猜。这些计数器属于硬件和软件之间的“共同语言”必须在设计阶段就规划好。5. 常见问题与排查经验5.1 编译生成的代码结果不对先软还是先硬联调阶段最常见的恐怖故事是编译出的算子计算结果偶尔不对看起来像硬件bug也像编译器bug。定位这类问题我的流程是第一步把计算图切成单算子逐算子跑确定问题算子是哪个。第二步查算子内算法逻辑是不是溢出了是不是量化参数用错是不是地址越界读到了脏数据这些属于编译器和算子库的锅好查。第三步能排除软件之后再用硬件自测指令直接写寄存器、喂已知输入、检查输出buffer验证硬件逻辑。第四步交叉验证同一个算子用不同布局、不同数据块跑看错误是否有重现性。特定数据形状才出错大概率跟对齐、边界条件相关随机出错大概率是同步或竞争问题。踩过最多的坑是“边界数据 汇编生成器的对齐假设”。比如编译器假设输入通道数必须是SIMD宽度整数倍遇到特殊形状就做填充但填充值没写对计算结果就在边界处发生污染。这种问题往往单算子功能测试过了端到端才爆发非常隐蔽。最终解法是给编译器生成代码添加“预先断言”发现形状不满足假设直接报错而不是默默生成错误代码。5.2 性能不达标带宽受限、并行度不足还是同步开销性能不达标的原因可以归成三类带宽受限片上能放下的数据太少大量时间花在DMA搬运上。对策是修改内存切分策略增加数据复用或者硬件上扩buffer。软件侧经过存储在编译器里对循环进行分块tiling让同一块数据在片上尽量被多次计算使用。并行度不足主流算子在阵列上的利用率不高原因可能是卷积输出通道只有16而PE阵列行数是32等于一半PE闲置。可以在编译器里做输出通道的“空间打包”把多个不完整的卷积batch拼起来算但也取决于硬件是否支持多个逻辑任务共享一块阵列。同步开销过大每次任务切换/数据搬入都要花大量周期同步。对策是设计异步流水DMA搬运第N1块数据的同时计算第N块。这需要运行时调度和硬件多队列配合后面再遇到类似问题先查硬件支持几个outstanding请求。实测最好用的调优次序是先解决带宽受限数据放不下再解决并行度不足利用率低最后才抠同步延时。不要反向优化。5.3 工具链debug的几条实用建议调试AI芯片软件栈和调试普通应用很不一样出问题后往往没法单步执行硬件只能靠log、寄存器和数据dump。下面几条是反复用到的第一给编译器加“可编译导出模式”每次生成调度计划时顺手输出一份人类可读的解释信息比如哪个循环被分块成多少、哪个张量被分配到SRAM的哪个偏移。有人觉得这是额外负担但它能在排查性能问题时帮你省一个下午。第二用“最小复现”思路保留用例。发现一个模型性能异常时不要直接在完整模型上调一次次剪枝直到剪出一个几十行的算子序列能复现问题。这个序列就是队友和后续测试集的宝贵用例。第三重视回归测试的护城河作用。每次修改编译器或算子库跑一遍预定义的用例集。用例集不需要完全覆盖所有模型但必须覆盖不同形状卷积、不同量化精度、不同batch大小、以及几种典型融合模式。出现回归时先用二分法定位是哪次改动引入的。表常见错误来源与定位优先级错误表现优先怀疑对象常用定位方法特定形状下结果错误对齐/填充逻辑、边界处理对比不同shape输入检查地址偏移计算随机偶发错误同步/竞争、DMA完成事件丢失增加同步点缩小复现范围全模型精度退化量化参数、数据范围缩放单算子级精度对比查scale/zero_point性能差但功能正确数据布局、存储分配性能计数器分析检查使用率和带宽占用多核结果不一致核间任务切分、共享内存写冲突单核跑通后逐步增加核数确认冲突位置写到这里想起早期项目里的一段经历我们花了整整两周查一个偶发的死锁最后定位到同步寄存器在极端时序下被软件轮询读到了旧值。硬件团队觉得软件应该更强壮软件团队觉得硬件应该保证一致性。后来两边各让一步硬件在寄存器更新后加了一个屏障周期软件在轮询时主动重复读两次从此再没见过那个死锁。这类问题的本质不是谁错了而是软硬件之间缺少统一的时序契约。如果看完这篇对你有什么实际帮助我个人的建议只有两条一是在项目早期就把软硬件的“接口语义”当成最高优先级的交付物白纸黑字写清楚这不是文档工程这是工程师的地基二是无论硬件性能多亮眼永远先做一个能自动跑精度回归和性能回归的最小工具链再开始优化其他的部分。有了这个工具链后面每一步改动都是有底气的。