做AI芯片的人常把一句话挂在嘴边:真正决定一颗AI芯片成败的,往往不是峰值算力,而是软硬件设计能不能严丝合缝地咬合在一起。我最近参与评估某端侧AI芯片方案(下文叫模拟项目X)时,最深的感受是:算法团队、编译器团队和硬件架构团队,经常在“同一个算子”上说出三种完全不同的结论。这篇是“AI芯片的软硬件设计”系列的第一篇,先把分工、映射、协同验证这套大框架讲清楚,适合刚开始接触AI加速器的芯片前端工程师,也适合想给团队梳理协作流程的同行参考。1. AI芯片软硬件设计的分工:算力只是起点,接口才是命门1.1 从算法到电路之间隔着三层翻译AI芯片的设计很容易被简化成一句话:把卷积、矩阵乘这些算子用电路实现出来。但真正做过的人都知道,模型里跑的是一个高维张量运算,硬件里跑的是一堆乘加单元和存储搬运指令,中间隔着的东西比想象中多得多。我习惯把这条链路拆成三层翻译。第一层是算法/算子层。这一层关心的是数学语义:一个卷积的卷积核大小、步长、填充方式、输入输出形状,精度是多少位,激活函数放在哪里。算法工程师眼里的“卷积”,是一组有明确数学定义的循环。第二层是编译/运行时层。AI编译器拿到计算图之后,要做算子融合、数据布局转换、循环分块、指令调度,把张量运算变成能在目标硬件上执行的指令序列。这一层关心的是“怎么排产”,而不是“某一笔加法怎么算”。第三层是微架构/电路层。这里关心的是MAC阵列怎么排布、片上存储分几级、DMA怎么搬数据、流水线怎么互相等待。最终落到寄存器传输级设计,也就是RTL里面那一堆时序逻辑和状态机。三层之间靠什么对齐?靠接口。具体说,是算子语义约定、指令集架构、寄存器级接口、内存布局规则和数据精度定义。这些接口本质上是一种“契约”:硬件承诺在多少周期内完成某个操作,软件承诺按约定格式把数据送进来。可以拿一个表格快速看清分工:层级关心的问题典型产物算法/应用层精度、数值语义、模型结构计算图、算子定义编译/运行时层调度、内存分配、指令生成中间表示IR、指令序列、二进制微架构/电路层时序、面积、功耗、数据搬运指令集手册、寄存器说明、RTL我经常跟团队说:先别急着定用几个MAC、跑到多少GHz,先把三层之间的这几个产物定义清楚。因为后面所有争吵,大概率都出在这些边界上。1.2 为什么多数性能问题都出现在接口附近初入行的人会把性能问题归因于“硬件不够快”,但实际项目里,大量性能损耗发生在软硬件接口附近。举一个最常见的例子:数据布局。同一个卷积,输入张量按NCHW排还是按NHWC排,在硬件上的访存行为完全不同。如果编译器以为数据是按通道连续排列的,但硬件加速器实际按空间位置连续读取,两者之间就不得不插入一个布局转换算子。这个转换本身不产生任何“智能计算”,却会把DMA带宽吃掉一大截,甚至让加速单元停下来等数据。再说精度格式。算法团队可能希望模型全程用FP16训练,但端侧芯片量产时大概率跑INT8量化。如果软硬件设计阶段没有把量化参数放在寄存器里,让编译器和运行时能动态读取scale和zero point,后面做模型迁移时就会陷入“每个模型都要重新编译甚至重新设计硬件”的窘境。还有同步机制。多核NPU协同计算时,谁等谁、谁唤醒谁、什么时候刷缓存,这些如果不在指令集或者寄存器级接口里约定清楚,软件就只能靠轮询和超时来兜底。轮询本身不产生算力,却占据CPU调度时间,也掩盖了真正的性能瓶颈。所以我的观点很明确:AI芯片的软硬件设计,本质上是接口设计。算力只是起跑线,接口才决定这辆车能不能跑完比赛。2. 一次端侧检测任务的完整链路:从算子到MAC阵列怎么走2.1 算子映射不是一对一,而是“拆-排-拼”拿模拟项目X里最普通的一个3x3卷积举例。算法侧看到的是:输入张量 [1, 64, 112, 112],卷积核 [64, 64, 3, 3],输出 [1, 64, 110, 110]。硬件侧看到的是:一个由乘加单元组成的二维阵列,每个周期能从片上存储读入一组数据、算一组乘加。两者之间怎么对上?第一步是拆成矩阵乘。学术界和工业界常说的“隐式GEMM”,就是把卷积滑窗采集到的输入块按行展开,把卷积核按列展开,然后用矩阵乘来等价实现。这样做的好处是让卷积复用成熟的矩阵乘优化方法,但代价是数据排列和存储开销需要仔细设计。第二步是排分块。一个完整的大矩阵乘不可能一次性塞进片上buffer,必须按照输出通道和空间位置分块。分块大小直接影响数据重用:分块太大,片上存储不够;分块太小,数据搬运次数成倍增加,DMA忙不过来。第三步是拼到MAC阵列上。MAC阵列有固定宽度和高度,比如16x16或者32x16。编译器要把分块后的循环按阵列形状重新排序,就像往仓库货架上码箱子:先决定哪一块放哪个货架,再决定货架内部怎么码。如果只把算子看成一对一的“电路实例”,忽略拆、排、拼这三步,硬件资源再多也发挥不出来。我之前见过一个团队,硬件峰值设计得很高,但编译器给出来的调度让阵列利用率只有40%,原因就是分块粒度跟片上存储不匹配,数据搬运成了瓶颈。2.2 调度、流水与存储搬运决定有效算力评估一颗AI芯片不能只看“理论TOPS”,要算“有效算力”。有效算力的估算公式不会太神秘,用一句话概括就是:有效算力 = 峰值算力 × 计算时间 /(计算时间 + 访存等待时间 + 同步等待时间)这句话看着简单,实际设计时每项都要拆开算。假设MAC阵列为2048个乘加单元,跑0.5GHz整数运算,理论峰值就是2 TOPS。跑一个矩阵乘分块时,如果计算需要1000个周期,但DMA把下一块数据搬到片上buffer需要2000个周期,且没有做双缓冲重叠,那么整体利用率只会有三分之一左右。解决这个问题的经典手段是乒乓缓冲。片上准备两块buffer,一块给当前计算用,一块给DMA预取下一块数据用。计算和搬运重叠起来之后,有效算力才能接近峰值。可一旦遇到数据依赖很强的算子,比如池化之后马上接卷积,前一个算子的输出必须全部写完,后一个算子才能开始读,这个时候缓冲区设计再漂亮也没用,必须靠编译器做算子融合或者数据前推。我在模拟项目X里做了一次测量:一个普通检测网络,单独跑卷积算子时,阵列利用率可以到80%;整网跑起来之后,因为各算子之间的同步和布局转换,实际利用率只有55%左右。这个差距,就是软硬件设计有没有咬合的直接体现。2.3 模拟项目X的软硬件协同验证五步很多团队在项目早期会犯一个错误:硬件模块验证和软件工具链开发各做各的,等到流片前才发现两边对不上。我们后来把协同验证固定成了五步,效率提高不少。第一步,定义算子语义的黄金参考模型。这一步不是拿某个开源框架当标准,而是基于硬件意图写一套独立实现,明确每个算子的输入输出、精度、边界条件。第二步,在周期级模拟器上跑编译产物。这一步不看功能对不对,只看周期数、带宽占用、每类stall的占比,目的是磨合编译器和微架构设计。第三步,用RTL仿真抽测同一条指令序列。故意选边界算子,比如stride=2、dilation=3、通道数不能被MAC阵列整除的情况,把RTL结果和黄金模型逐拍比对。第四步,在FPGA验证平台上跑通完整软件栈。从模型导入、量化、编译到驱动加载,整条链路走一遍。第五步,把回归用例固化到持续集成系统。每次硬件小版本或编译器小版本更新,都自动跑一遍算子单元测试和整网基准测试。这五步没有一步可以省。早期多花一周在周期模型上,后面就少花一个月在联调排错上。3. 硬件为软件留哪些“后门”:可编程性与可配置性的取舍3.1 指令集风格影响编译器难度AI芯片的指令集架构,直接决定了编译器的日子好不好过。不同的指令集风格,不是性能差异问题,而是整个工具链复杂度的差异。一种是长指令字风格。这种风格把多个执行单元的并行操作显式打包在一条超长指令里,编译器负责在每个周期安排好所有执行单元做什么。硬件控制逻辑很简单,但编译器调度压力巨大。如果指令字里某个操作没对齐,整个周期就空转。一种是标量风格。指令一条条顺序执行,硬件内部做动态调度和乱序执行。这对软件确实友好,但AI芯片追求能效比,不太愿意在昂贵的动态调度逻辑上浪费面积和功耗。还有一种是脉动阵列风格。数据在阵列里像流水线一样一个接一个传递,非常适合卷积这类规则运算。但一旦遇到不规则形状、动态shape的算子,映射就会非常痛苦。实际项目里很少采用单一风格,更多是混合方案:规则卷积走脉动阵列,不规则算子走可编程单元,标量控制走通用CPU核。关键是软硬件设计阶段要明确“哪些算子必须走好路,哪些算子可以绕路”,而不是把所有情况都交给硬件,那样面积和功耗都受不了。我倾向于用一张简单的对比表帮助架构讨论:指令集风格硬件复杂度编译器压力适合场景超长指令字低高规则并行计算标量+动态调度高低通用控制逻辑脉动阵列中中卷积、矩阵乘可配置加速器低中固定shape的AI算子没有绝对的好坏,只有边界条件。设计AI芯片最忌讳的就是“我要支持所有模型”,这句话会让软硬件双方都失去收敛的方向。3.2 算例配置寄存器:用少量硬件复杂度换取软件通用性我曾经参与过一个加速器的架构评审,硬件团队非常自豪地说:“我们支持所有3x3卷积。”评审时软件团队问了一句:“那3x3卷积的stride从1改成2,需要重新综合吗?”会场安静了十秒。这个例子说明,硬件的“支持”和软件的“可用”是两回事。一个聪明的做法是:用少量可配置寄存器,把常见变化变成软件可调整的参数,而不是每个变体都要重新设计硬件。至少这几类参数值得做成寄存器:卷积的stride、padding、dilation;输入输出通道数和空间尺寸;数据格式,比如INT8、INT16、FP16;累加精度和饱和截断模式;写入加速器队列的同步方式。每增加一个配置项,验证空间都会翻倍。所以配置项不能拍脑袋加,应该根据目标负载的实际分布来决定。我在模拟项目X里做过一个统计,把候选模型组里所有算子参数跑了一遍,最后只保留了出现频率最高的几个配置维度。那些只有一两个模型用到的参数,直接放弃,不做寄存器,也不承诺支持。3.3 性能计数器是软件工程师最好的朋友如果硬件只留给软件一套“算完通知我”的寄存器,那软件工程师基本是瞎子。真正好用的AI芯片,一定要有足够的性能计数器。我在模拟项目X初期就坚持加了这样几组计数器:MAC阵列忙闲占比;DMA读写带宽和等待周期;片上buffer命中率;各执行单元互相等待的stall周期;以及指令队列深度。这些计数器不直接参与计算,但对软硬件设计的迭代价值极大。比如编译器做了一个新的调度版本,单看整网延迟无法判断好坏,但一看stall周期,就能立即定位是数据搬运问题还是同步问题。性能计数器配合一个能按周期快照的寄存器,软件甚至能把一段执行过程逐拍还原出来。这个“后门”,我认为应该从第一版RTL就带上,后期再加会非常痛苦。4. 软件栈厚度怎么设计:编译、运行时与驱动的分工4.1 图编译阶段该做什么:算子融合、布局转换、内存规划AI芯片的软件栈通常从图编译开始。这个阶段不能只是把每个算子翻译成指令,然后原样执行一遍,那样访存开销会把你拖垮。算子融合是最直观的优化。以推理场景里最常见的卷积+批归一化+ReLU为例。批归一化在推理时可以把scale和shift折叠进卷积的权重里,ReLU只是一个逐元素比较。如果三个算子分三次读写数据,同一块数据就要从DMA进出三次;融合成一个算子后,数据进片上存储一次,算完直接输出,带宽省两倍。布局转换也是编译阶段的必修课。硬件对数据布局有偏好,但模型来自各种训练框架,输入数据的排布五花八门。编译器需要判断是主动插入统一布局转换,还是让硬件在指令里支持一次“顺便转置”。后者对硬件设计要求更高,但往往省一轮完整搬运。内存规划同样重要。片上buffer就那么大,多个算子的中间结果谁先谁后、谁可以覆盖谁,本质上是一个变量活跃区间分析问题,跟通用编译器里的寄存器分配一模一样。如果编译阶段不规划,运行时再动态申请,不仅慢,而且碎片化严重。我在模拟项目X里的经验是:图编译阶段宁可多花一点时间把内存规划和数据布局算清楚,也不要依赖运行时“随机应变”。因为端侧AI芯片的容量和带宽都有限,任何临时补救措施都会放大功耗和延迟。4.2 运行时调度:多核任务分配与同步开销当芯片里有多个NPU核或一个核里多个执行单元时,运行时就要负责真正的调度了。任务分配的关键是把计算图切成多个子图,每个子图发给一个计算核。切法直接影响同步次数。比如一个卷积层可以按输出通道切成四块,分给四个核并行算,但算子如果依赖上一层的全部输出,就必须等所有核都完成才能进入下一层。同步次数多了,算力再高也会在等待中耗掉。运行时要做的一件事,就是把硬件同步原语包装成软件更容易使用的接口。常见做法是硬件提供event或者semaphore机制,一个核算完就触发事件,另一个核等事件到来再开始,而不是靠CPU反复轮询寄存器状态。这里有一个很容易被忽略的经验:尽量用“硬件事件通知”代替“软件轮询”。轮询的实现看起来简单,但CPU会被白白占住,而且问题定位时很难区分是计算慢还是轮询间隔太久。用硬件事件驱动,配合中断或等待指令,整个系统的调度延迟会稳很多。4.3 驱动只做最薄的一层:控制和状态同步驱动层的原则是“越薄越好”。它不应该替编译器做决策,也不应该在数据路径上做大量拷贝,只需要做三件事。第一,管理硬件资源,包括完成设备初始化、中断申请、DMA缓冲区的分配和释放。第二,把用户态已经编译好的指令序列提交给硬件,通过寄存器或者描述符队列的方式,把物理地址和长度交给硬件。第三,向上层报告状态,包括任务完成事件、错误异常、性能计数器读数。这里要特别提醒一个坑:驱动里不要为了好看把数据格式转来转去。我见过一个项目,驱动为了适配某个文件格式,在数据拷贝路径里加了一段格式转换,结果整网延迟多了5%。后来查下来,这段代码根本不应该存在,属于驱动工程师“顺手优化”,结果成了最大瓶颈。驱动层的数据面,永远保持零拷贝、零转换,只做控制和状态同步,这是最稳的做法。5. 联合仿真与性能回归:别等流片后才跟软件“大吵一架”5.1 功能模型和周期模型必须分开,但都要有软硬件协同验证最容易犯的错误,是想用一个模型解决所有问题。实际上需要两个模型。功能模型只保证数学结果正确,不模拟周期,也不模拟流水线竞争。它的价值是给RTL和编译器提供一个统一参照。无论是软件模拟输出,还是RTL仿真输出,都和功能模型对比,谁不对谁改。周期模型则逐拍模拟微架构行为,能给出指令时序、访存等待、流水线冲突。它比功能模型慢很多,但能回答“为什么性能只有60%”这类问题。我见过一个团队为了省事,只用功能模型做验证,等芯片回来才第一次跑性能测试,结果实际性能比预估低一半。原因是功能模型里没有模拟多核争用同一个DMA端口的情况,软件工程师也没有在编译调度里做避开端口冲突的尝试。最后只好在驱动层加回退逻辑,性能才勉强达标,但功耗已经压不住了。所以我的建议是:从一开始就维护两套模型,并且把“周期模型估算结果”作为软硬件设计迭代的硬性指标。每改一次编译器调度或硬件微架构,都要跑一遍回归,周期数不能恶化超过一定比例。5.2 性能回归基线集:按算子、网络、场景分层性能回归不能只放一两个大网络,那样出了问题不好定位。我习惯把基线集分成三层。第一层是算子微基准。每个常用算子单独测,比如不同尺寸的卷积、矩阵乘、逐元素算子。这一层的意义是快速定位“某个算子是不是变慢了”。第二层是网络级基准,拿几个有代表性的模型做整网延迟、带宽、内存占用统计。第三层是场景级基准,比如一个完整的拍照检测流程,从前处理、模型推理到后处理都串起来,反映真实用户体验。每一层都要有明确的阈值。比如算子微基准,周期数对比前一版本不能超过3%;网络级基准,端到端延迟不能超过5%。一旦超阈值,不急着修,先看性能计数器和波形,确认是编译器调度改变还是硬件时序变化导致的。这套回归体系看起来繁琐,但非常值得。因为软硬件设计是两边都在快速变化的,没有回归基线,每一次改动都是“摸着石头过河”,很容易改一个指标坏了另一个指标。6. 启动软硬件协同设计前,我会先问的八个问题6.1 八个问题的清单每次有新项目启动,我都会在架构评审会上摆出下面这张表,把八个问题全部过一遍。这不是走流程,而是逼着各团队把模糊前提提前暴露出来。序号问题为什么重要常见坑1目标负载到底是哪几个模型?决定硬件通用性边界嘴上说支持所有模型2精度格式固定到哪一版?影响寄存器设计和验证工作量架构冻结后算法改精度3算子分布里Top10是谁?决定MAC阵列和存储结构为低频算子堆大量硬件4编译器团队和硬件团队何时对齐接口?决定IR和指令集稳定性两边各自独立演进5软硬件接口冻结日期是哪天?给设计收敛设边界无限期改接口6性能验证以哪个周期模型为准?决定能效评估可信度RTL和周期模型结果对不上7如果某个算子映射不了怎么办?决定回退方案和软件兜底等到流片才发现不兼容8谁来维护黄金参考模型?决定协同验证的一致性各团队各写一套参考模型前四个问题决定架构方向,后四个问题决定项目能不能按时收敛。我建议把这八个问题回答完、形成书面结论之后再开始画详细框图,不然很容易画出一份“看起来很完整但不知道在解决谁的问题”的设计。6.2 怎么用这份清单做第一轮评审这套清单适合放在项目启动后的第一周,用半天到一天时间开一场闭门评审。各团队负责人现场回答,不许说“到时候再看”。评审后要做三件事。第一,把可以量化的目标写成数字,比如“INT8下整网延迟不超过30毫秒”“卷积算子有效利用率不低于70%”。第二,把软硬件接口冻结日期写进项目里程碑,并且设定变更控制流程,任何接口改动都要走评审。第三,把责任人定到具体的人,黄金模型谁维护、周期模型谁维护、性能回归谁跑,都要有人背责任。我见过太多AI芯片项目,架构阶段每个人都很兴奋,到了集成阶段却开始互相甩锅。提前用这张表把边界划好,至少能少一部分吵架。7. 收尾之前,分享两个真实项目里测得的关键教训7.1 教训一:接口文档要改到第三版再动手写RTL模拟项目X早期,硬件团队为了抢时间,在软硬件接口文档只有初稿的时候就开始写RTL。后来编译器团队在实现矩阵分块时发现,硬件定义的MAC阵列排布方式对常见卷积尺寸非常不友好,会导致大量尾部填充,浪费接近30%的算子。改动一个MAC阵列排布,意味着数据路径重写、地址生成器重写、编译器后端重写。那次返工大概花了三周。如果当时先把接口文档推演到第三版,用周期模型把Top算子的分块情况全部模拟一遍,这个坑完全可以避免。我的做法是:软硬件接口文档必须包含指令语义、寄存器说明、数据布局规则、错误处理流程四部分,并且至少经过一轮编译器团队的反向验证,再动手写RTL。反向验证的意思是:让编译器工程师拿着文档先写一个模拟器,把指令序列跑起来,输出周期数和资源占用。跑不通,就说明文档还不够清楚。7.2 教训二:黄金参考模型不能各团队各写各的另一个教训是关于参考模型一致性。模拟项目X早期,算法团队用一套脚本验证算子精度,硬件团队用另一套C模型验证RTL,编译器团队又有一套自己的数值模拟工具。看起来各司其职,但到了联调阶段,同一个卷积在三个环境里输出结果差了几个LSB,三方花了整整四天才定位到是舍入方式不一致。后来我们把黄金参考模型收敛成一份独立维护的代码,明确规定所有精度比对都以它为准,任何输出不一致都要回到这个模型查原因。舍入模式、饱和策略、累加顺序,都在模型里写清楚。这个决定看起来很基础,却可能是软硬件设计流程里回报最高的一件事。AI芯片的软硬件设计,真正复杂的不是单一模块,而是所有人在同一个问题上保持一致。接口、模型、基线、责任人,这四件事做扎实,项目就成功了一大半。下一篇我再接着写具体某类算子的映射案例,以及怎么用周期模型做分块参数搜索。
做AI芯片的人常把一句话挂在嘴边:真正决定一颗AI芯片成败的,往往不是峰值算力,而是软硬件设计能不能严丝合缝地咬合在一起。我最近参与评估某端侧AI芯片方案(下文叫模拟项目X)时,最深的感受是:算法团队、编译器团队和硬件架构团队,经常在“同一个算子”上说出三种完全不同的结论。这篇是“AI芯片的软硬件设计”系列的第一篇,先把分工、映射、协同验证这套大框架讲清楚,适合刚开始接触AI加速器的芯片前端工程师,也适合想给团队梳理协作流程的同行参考。1. AI芯片软硬件设计的分工:算力只是起点,接口才是命门1.1 从算法到电路之间隔着三层翻译AI芯片的设计很容易被简化成一句话:把卷积、矩阵乘这些算子用电路实现出来。但真正做过的人都知道,模型里跑的是一个高维张量运算,硬件里跑的是一堆乘加单元和存储搬运指令,中间隔着的东西比想象中多得多。我习惯把这条链路拆成三层翻译。第一层是算法/算子层。这一层关心的是数学语义:一个卷积的卷积核大小、步长、填充方式、输入输出形状,精度是多少位,激活函数放在哪里。算法工程师眼里的“卷积”,是一组有明确数学定义的循环。第二层是编译/运行时层。AI编译器拿到计算图之后,要做算子融合、数据布局转换、循环分块、指令调度,把张量运算变成能在目标硬件上执行的指令序列。这一层关心的是“怎么排产”,而不是“某一笔加法怎么算”。第三层是微架构/电路层。这里关心的是MAC阵列怎么排布、片上存储分几级、DMA怎么搬数据、流水线怎么互相等待。最终落到寄存器传输级设计,也就是RTL里面那一堆时序逻辑和状态机。三层之间靠什么对齐?靠接口。具体说,是算子语义约定、指令集架构、寄存器级接口、内存布局规则和数据精度定义。这些接口本质上是一种“契约”:硬件承诺在多少周期内完成某个操作,软件承诺按约定格式把数据送进来。可以拿一个表格快速看清分工:层级关心的问题典型产物算法/应用层精度、数值语义、模型结构计算图、算子定义编译/运行时层调度、内存分配、指令生成中间表示IR、指令序列、二进制微架构/电路层时序、面积、功耗、数据搬运指令集手册、寄存器说明、RTL我经常跟团队说:先别急着定用几个MAC、跑到多少GHz,先把三层之间的这几个产物定义清楚。因为后面所有争吵,大概率都出在这些边界上。1.2 为什么多数性能问题都出现在接口附近初入行的人会把性能问题归因于“硬件不够快”,但实际项目里,大量性能损耗发生在软硬件接口附近。举一个最常见的例子:数据布局。同一个卷积,输入张量按NCHW排还是按NHWC排,在硬件上的访存行为完全不同。如果编译器以为数据是按通道连续排列的,但硬件加速器实际按空间位置连续读取,两者之间就不得不插入一个布局转换算子。这个转换本身不产生任何“智能计算”,却会把DMA带宽吃掉一大截,甚至让加速单元停下来等数据。再说精度格式。算法团队可能希望模型全程用FP16训练,但端侧芯片量产时大概率跑INT8量化。如果软硬件设计阶段没有把量化参数放在寄存器里,让编译器和运行时能动态读取scale和zero point,后面做模型迁移时就会陷入“每个模型都要重新编译甚至重新设计硬件”的窘境。还有同步机制。多核NPU协同计算时,谁等谁、谁唤醒谁、什么时候刷缓存,这些如果不在指令集或者寄存器级接口里约定清楚,软件就只能靠轮询和超时来兜底。轮询本身不产生算力,却占据CPU调度时间,也掩盖了真正的性能瓶颈。所以我的观点很明确:AI芯片的软硬件设计,本质上是接口设计。算力只是起跑线,接口才决定这辆车能不能跑完比赛。2. 一次端侧检测任务的完整链路:从算子到MAC阵列怎么走2.1 算子映射不是一对一,而是“拆-排-拼”拿模拟项目X里最普通的一个3x3卷积举例。算法侧看到的是:输入张量 [1, 64, 112, 112],卷积核 [64, 64, 3, 3],输出 [1, 64, 110, 110]。硬件侧看到的是:一个由乘加单元组成的二维阵列,每个周期能从片上存储读入一组数据、算一组乘加。两者之间怎么对上?第一步是拆成矩阵乘。学术界和工业界常说的“隐式GEMM”,就是把卷积滑窗采集到的输入块按行展开,把卷积核按列展开,然后用矩阵乘来等价实现。这样做的好处是让卷积复用成熟的矩阵乘优化方法,但代价是数据排列和存储开销需要仔细设计。第二步是排分块。一个完整的大矩阵乘不可能一次性塞进片上buffer,必须按照输出通道和空间位置分块。分块大小直接影响数据重用:分块太大,片上存储不够;分块太小,数据搬运次数成倍增加,DMA忙不过来。第三步是拼到MAC阵列上。MAC阵列有固定宽度和高度,比如16x16或者32x16。编译器要把分块后的循环按阵列形状重新排序,就像往仓库货架上码箱子:先决定哪一块放哪个货架,再决定货架内部怎么码。如果只把算子看成一对一的“电路实例”,忽略拆、排、拼这三步,硬件资源再多也发挥不出来。我之前见过一个团队,硬件峰值设计得很高,但编译器给出来的调度让阵列利用率只有40%,原因就是分块粒度跟片上存储不匹配,数据搬运成了瓶颈。2.2 调度、流水与存储搬运决定有效算力评估一颗AI芯片不能只看“理论TOPS”,要算“有效算力”。有效算力的估算公式不会太神秘,用一句话概括就是:有效算力 = 峰值算力 × 计算时间 /(计算时间 + 访存等待时间 + 同步等待时间)这句话看着简单,实际设计时每项都要拆开算。假设MAC阵列为2048个乘加单元,跑0.5GHz整数运算,理论峰值就是2 TOPS。跑一个矩阵乘分块时,如果计算需要1000个周期,但DMA把下一块数据搬到片上buffer需要2000个周期,且没有做双缓冲重叠,那么整体利用率只会有三分之一左右。解决这个问题的经典手段是乒乓缓冲。片上准备两块buffer,一块给当前计算用,一块给DMA预取下一块数据用。计算和搬运重叠起来之后,有效算力才能接近峰值。可一旦遇到数据依赖很强的算子,比如池化之后马上接卷积,前一个算子的输出必须全部写完,后一个算子才能开始读,这个时候缓冲区设计再漂亮也没用,必须靠编译器做算子融合或者数据前推。我在模拟项目X里做了一次测量:一个普通检测网络,单独跑卷积算子时,阵列利用率可以到80%;整网跑起来之后,因为各算子之间的同步和布局转换,实际利用率只有55%左右。这个差距,就是软硬件设计有没有咬合的直接体现。2.3 模拟项目X的软硬件协同验证五步很多团队在项目早期会犯一个错误:硬件模块验证和软件工具链开发各做各的,等到流片前才发现两边对不上。我们后来把协同验证固定成了五步,效率提高不少。第一步,定义算子语义的黄金参考模型。这一步不是拿某个开源框架当标准,而是基于硬件意图写一套独立实现,明确每个算子的输入输出、精度、边界条件。第二步,在周期级模拟器上跑编译产物。这一步不看功能对不对,只看周期数、带宽占用、每类stall的占比,目的是磨合编译器和微架构设计。第三步,用RTL仿真抽测同一条指令序列。故意选边界算子,比如stride=2、dilation=3、通道数不能被MAC阵列整除的情况,把RTL结果和黄金模型逐拍比对。第四步,在FPGA验证平台上跑通完整软件栈。从模型导入、量化、编译到驱动加载,整条链路走一遍。第五步,把回归用例固化到持续集成系统。每次硬件小版本或编译器小版本更新,都自动跑一遍算子单元测试和整网基准测试。这五步没有一步可以省。早期多花一周在周期模型上,后面就少花一个月在联调排错上。3. 硬件为软件留哪些“后门”:可编程性与可配置性的取舍3.1 指令集风格影响编译器难度AI芯片的指令集架构,直接决定了编译器的日子好不好过。不同的指令集风格,不是性能差异问题,而是整个工具链复杂度的差异。一种是长指令字风格。这种风格把多个执行单元的并行操作显式打包在一条超长指令里,编译器负责在每个周期安排好所有执行单元做什么。硬件控制逻辑很简单,但编译器调度压力巨大。如果指令字里某个操作没对齐,整个周期就空转。一种是标量风格。指令一条条顺序执行,硬件内部做动态调度和乱序执行。这对软件确实友好,但AI芯片追求能效比,不太愿意在昂贵的动态调度逻辑上浪费面积和功耗。还有一种是脉动阵列风格。数据在阵列里像流水线一样一个接一个传递,非常适合卷积这类规则运算。但一旦遇到不规则形状、动态shape的算子,映射就会非常痛苦。实际项目里很少采用单一风格,更多是混合方案:规则卷积走脉动阵列,不规则算子走可编程单元,标量控制走通用CPU核。关键是软硬件设计阶段要明确“哪些算子必须走好路,哪些算子可以绕路”,而不是把所有情况都交给硬件,那样面积和功耗都受不了。我倾向于用一张简单的对比表帮助架构讨论:指令集风格硬件复杂度编译器压力适合场景超长指令字低高规则并行计算标量+动态调度高低通用控制逻辑脉动阵列中中卷积、矩阵乘可配置加速器低中固定shape的AI算子没有绝对的好坏,只有边界条件。设计AI芯片最忌讳的就是“我要支持所有模型”,这句话会让软硬件双方都失去收敛的方向。3.2 算例配置寄存器:用少量硬件复杂度换取软件通用性我曾经参与过一个加速器的架构评审,硬件团队非常自豪地说:“我们支持所有3x3卷积。”评审时软件团队问了一句:“那3x3卷积的stride从1改成2,需要重新综合吗?”会场安静了十秒。这个例子说明,硬件的“支持”和软件的“可用”是两回事。一个聪明的做法是:用少量可配置寄存器,把常见变化变成软件可调整的参数,而不是每个变体都要重新设计硬件。至少这几类参数值得做成寄存器:卷积的stride、padding、dilation;输入输出通道数和空间尺寸;数据格式,比如INT8、INT16、FP16;累加精度和饱和截断模式;写入加速器队列的同步方式。每增加一个配置项,验证空间都会翻倍。所以配置项不能拍脑袋加,应该根据目标负载的实际分布来决定。我在模拟项目X里做过一个统计,把候选模型组里所有算子参数跑了一遍,最后只保留了出现频率最高的几个配置维度。那些只有一两个模型用到的参数,直接放弃,不做寄存器,也不承诺支持。3.3 性能计数器是软件工程师最好的朋友如果硬件只留给软件一套“算完通知我”的寄存器,那软件工程师基本是瞎子。真正好用的AI芯片,一定要有足够的性能计数器。我在模拟项目X初期就坚持加了这样几组计数器:MAC阵列忙闲占比;DMA读写带宽和等待周期;片上buffer命中率;各执行单元互相等待的stall周期;以及指令队列深度。这些计数器不直接参与计算,但对软硬件设计的迭代价值极大。比如编译器做了一个新的调度版本,单看整网延迟无法判断好坏,但一看stall周期,就能立即定位是数据搬运问题还是同步问题。性能计数器配合一个能按周期快照的寄存器,软件甚至能把一段执行过程逐拍还原出来。这个“后门”,我认为应该从第一版RTL就带上,后期再加会非常痛苦。4. 软件栈厚度怎么设计:编译、运行时与驱动的分工4.1 图编译阶段该做什么:算子融合、布局转换、内存规划AI芯片的软件栈通常从图编译开始。这个阶段不能只是把每个算子翻译成指令,然后原样执行一遍,那样访存开销会把你拖垮。算子融合是最直观的优化。以推理场景里最常见的卷积+批归一化+ReLU为例。批归一化在推理时可以把scale和shift折叠进卷积的权重里,ReLU只是一个逐元素比较。如果三个算子分三次读写数据,同一块数据就要从DMA进出三次;融合成一个算子后,数据进片上存储一次,算完直接输出,带宽省两倍。布局转换也是编译阶段的必修课。硬件对数据布局有偏好,但模型来自各种训练框架,输入数据的排布五花八门。编译器需要判断是主动插入统一布局转换,还是让硬件在指令里支持一次“顺便转置”。后者对硬件设计要求更高,但往往省一轮完整搬运。内存规划同样重要。片上buffer就那么大,多个算子的中间结果谁先谁后、谁可以覆盖谁,本质上是一个变量活跃区间分析问题,跟通用编译器里的寄存器分配一模一样。如果编译阶段不规划,运行时再动态申请,不仅慢,而且碎片化严重。我在模拟项目X里的经验是:图编译阶段宁可多花一点时间把内存规划和数据布局算清楚,也不要依赖运行时“随机应变”。因为端侧AI芯片的容量和带宽都有限,任何临时补救措施都会放大功耗和延迟。4.2 运行时调度:多核任务分配与同步开销当芯片里有多个NPU核或一个核里多个执行单元时,运行时就要负责真正的调度了。任务分配的关键是把计算图切成多个子图,每个子图发给一个计算核。切法直接影响同步次数。比如一个卷积层可以按输出通道切成四块,分给四个核并行算,但算子如果依赖上一层的全部输出,就必须等所有核都完成才能进入下一层。同步次数多了,算力再高也会在等待中耗掉。运行时要做的一件事,就是把硬件同步原语包装成软件更容易使用的接口。常见做法是硬件提供event或者semaphore机制,一个核算完就触发事件,另一个核等事件到来再开始,而不是靠CPU反复轮询寄存器状态。这里有一个很容易被忽略的经验:尽量用“硬件事件通知”代替“软件轮询”。轮询的实现看起来简单,但CPU会被白白占住,而且问题定位时很难区分是计算慢还是轮询间隔太久。用硬件事件驱动,配合中断或等待指令,整个系统的调度延迟会稳很多。4.3 驱动只做最薄的一层:控制和状态同步驱动层的原则是“越薄越好”。它不应该替编译器做决策,也不应该在数据路径上做大量拷贝,只需要做三件事。第一,管理硬件资源,包括完成设备初始化、中断申请、DMA缓冲区的分配和释放。第二,把用户态已经编译好的指令序列提交给硬件,通过寄存器或者描述符队列的方式,把物理地址和长度交给硬件。第三,向上层报告状态,包括任务完成事件、错误异常、性能计数器读数。这里要特别提醒一个坑:驱动里不要为了好看把数据格式转来转去。我见过一个项目,驱动为了适配某个文件格式,在数据拷贝路径里加了一段格式转换,结果整网延迟多了5%。后来查下来,这段代码根本不应该存在,属于驱动工程师“顺手优化”,结果成了最大瓶颈。驱动层的数据面,永远保持零拷贝、零转换,只做控制和状态同步,这是最稳的做法。5. 联合仿真与性能回归:别等流片后才跟软件“大吵一架”5.1 功能模型和周期模型必须分开,但都要有软硬件协同验证最容易犯的错误,是想用一个模型解决所有问题。实际上需要两个模型。功能模型只保证数学结果正确,不模拟周期,也不模拟流水线竞争。它的价值是给RTL和编译器提供一个统一参照。无论是软件模拟输出,还是RTL仿真输出,都和功能模型对比,谁不对谁改。周期模型则逐拍模拟微架构行为,能给出指令时序、访存等待、流水线冲突。它比功能模型慢很多,但能回答“为什么性能只有60%”这类问题。我见过一个团队为了省事,只用功能模型做验证,等芯片回来才第一次跑性能测试,结果实际性能比预估低一半。原因是功能模型里没有模拟多核争用同一个DMA端口的情况,软件工程师也没有在编译调度里做避开端口冲突的尝试。最后只好在驱动层加回退逻辑,性能才勉强达标,但功耗已经压不住了。所以我的建议是:从一开始就维护两套模型,并且把“周期模型估算结果”作为软硬件设计迭代的硬性指标。每改一次编译器调度或硬件微架构,都要跑一遍回归,周期数不能恶化超过一定比例。5.2 性能回归基线集:按算子、网络、场景分层性能回归不能只放一两个大网络,那样出了问题不好定位。我习惯把基线集分成三层。第一层是算子微基准。每个常用算子单独测,比如不同尺寸的卷积、矩阵乘、逐元素算子。这一层的意义是快速定位“某个算子是不是变慢了”。第二层是网络级基准,拿几个有代表性的模型做整网延迟、带宽、内存占用统计。第三层是场景级基准,比如一个完整的拍照检测流程,从前处理、模型推理到后处理都串起来,反映真实用户体验。每一层都要有明确的阈值。比如算子微基准,周期数对比前一版本不能超过3%;网络级基准,端到端延迟不能超过5%。一旦超阈值,不急着修,先看性能计数器和波形,确认是编译器调度改变还是硬件时序变化导致的。这套回归体系看起来繁琐,但非常值得。因为软硬件设计是两边都在快速变化的,没有回归基线,每一次改动都是“摸着石头过河”,很容易改一个指标坏了另一个指标。6. 启动软硬件协同设计前,我会先问的八个问题6.1 八个问题的清单每次有新项目启动,我都会在架构评审会上摆出下面这张表,把八个问题全部过一遍。这不是走流程,而是逼着各团队把模糊前提提前暴露出来。序号问题为什么重要常见坑1目标负载到底是哪几个模型?决定硬件通用性边界嘴上说支持所有模型2精度格式固定到哪一版?影响寄存器设计和验证工作量架构冻结后算法改精度3算子分布里Top10是谁?决定MAC阵列和存储结构为低频算子堆大量硬件4编译器团队和硬件团队何时对齐接口?决定IR和指令集稳定性两边各自独立演进5软硬件接口冻结日期是哪天?给设计收敛设边界无限期改接口6性能验证以哪个周期模型为准?决定能效评估可信度RTL和周期模型结果对不上7如果某个算子映射不了怎么办?决定回退方案和软件兜底等到流片才发现不兼容8谁来维护黄金参考模型?决定协同验证的一致性各团队各写一套参考模型前四个问题决定架构方向,后四个问题决定项目能不能按时收敛。我建议把这八个问题回答完、形成书面结论之后再开始画详细框图,不然很容易画出一份“看起来很完整但不知道在解决谁的问题”的设计。6.2 怎么用这份清单做第一轮评审这套清单适合放在项目启动后的第一周,用半天到一天时间开一场闭门评审。各团队负责人现场回答,不许说“到时候再看”。评审后要做三件事。第一,把可以量化的目标写成数字,比如“INT8下整网延迟不超过30毫秒”“卷积算子有效利用率不低于70%”。第二,把软硬件接口冻结日期写进项目里程碑,并且设定变更控制流程,任何接口改动都要走评审。第三,把责任人定到具体的人,黄金模型谁维护、周期模型谁维护、性能回归谁跑,都要有人背责任。我见过太多AI芯片项目,架构阶段每个人都很兴奋,到了集成阶段却开始互相甩锅。提前用这张表把边界划好,至少能少一部分吵架。7. 收尾之前,分享两个真实项目里测得的关键教训7.1 教训一:接口文档要改到第三版再动手写RTL模拟项目X早期,硬件团队为了抢时间,在软硬件接口文档只有初稿的时候就开始写RTL。后来编译器团队在实现矩阵分块时发现,硬件定义的MAC阵列排布方式对常见卷积尺寸非常不友好,会导致大量尾部填充,浪费接近30%的算子。改动一个MAC阵列排布,意味着数据路径重写、地址生成器重写、编译器后端重写。那次返工大概花了三周。如果当时先把接口文档推演到第三版,用周期模型把Top算子的分块情况全部模拟一遍,这个坑完全可以避免。我的做法是:软硬件接口文档必须包含指令语义、寄存器说明、数据布局规则、错误处理流程四部分,并且至少经过一轮编译器团队的反向验证,再动手写RTL。反向验证的意思是:让编译器工程师拿着文档先写一个模拟器,把指令序列跑起来,输出周期数和资源占用。跑不通,就说明文档还不够清楚。7.2 教训二:黄金参考模型不能各团队各写各的另一个教训是关于参考模型一致性。模拟项目X早期,算法团队用一套脚本验证算子精度,硬件团队用另一套C模型验证RTL,编译器团队又有一套自己的数值模拟工具。看起来各司其职,但到了联调阶段,同一个卷积在三个环境里输出结果差了几个LSB,三方花了整整四天才定位到是舍入方式不一致。后来我们把黄金参考模型收敛成一份独立维护的代码,明确规定所有精度比对都以它为准,任何输出不一致都要回到这个模型查原因。舍入模式、饱和策略、累加顺序,都在模型里写清楚。这个决定看起来很基础,却可能是软硬件设计流程里回报最高的一件事。AI芯片的软硬件设计,真正复杂的不是单一模块,而是所有人在同一个问题上保持一致。接口、模型、基线、责任人,这四件事做扎实,项目就成功了一大半。下一篇我再接着写具体某类算子的映射案例,以及怎么用周期模型做分块参数搜索。