☰
AI芯片软硬件协同设计:从编译器到张量核的工程实践
2026/10/14 15:04:11 网站建设 项目流程

很多人第一次接触“AI 芯片的软硬件设计”这个概念时,会下意识觉得:这事是不是太硬核了?要懂数字电路、要啃指令集、还要会写编译器,门槛太高。但如果你真在芯片或AI部署这条线里混过几年,就会明白另一件事——AI芯片的门槛,不在某一项单点技术上,而在“软硬件怎么捏到一起”这件事上。

我这些年做过的项目里,最不缺的就是那种“硬件峰值算力很高,一跑实际模型利用率却惨不忍睹”的场面。问题往往不是芯片算力不够,而是软件栈没有把硬件的性子摸透。反过来也有,软件团队精心调了一堆算子,硬件一接口变更全盘作废。所以说到底,AI芯片设计本质上是系统工程:一边是MAC阵列、SRAM、总线这些看得见的砖瓦,一边是编译器、运行时、量化工具这些看不见的管线,两边必须同时开工、互相咬合。

这篇内容算是“AI 芯片的软硬件设计 1”,我按自己的实际经验把整个框架拆开讲一遍——适合准备入行做AI芯片架构、编译器开发、算子优化的工程师,也适合做AI部署却总搞不明白“为什么我的模型在NPU上这么慢”的算法同学。

1. 先搞清楚:AI芯片为什么不能只拼硬件

很多人以为做AI芯片就是把一堆乘法器堆上去,算力越高越厉害。这个想法如果停留在三年前还能理解,现在再做AI芯片还这么想,基本等于还没上路。拼硬件只是入场券,真正决定芯片能不能被用起来、能不能卖出去的,是软件工具链和硬件之间的契合度。

1.1 通用算力在AI负载面前力不从心

我们先看一个基础矛盾。CPU的核心设计目标是低延迟执行复杂控制逻辑,它的通用性很强,但算力密度很低——一个CPU核的面积拿去放SIMD单元和缓存,相比专用矩阵计算单元,单位面积能产出的TOPS(每秒万亿次操作)差距可以到一到两个数量级。GPU好一些,但GPU最初是为图形渲染设计的,执行AI推理时依然有不少迭代开销。

这意味着什么?如果你在一个数据中心里用纯GPU跑大规模推理服务,在功耗和成本上都会很吃力。当模型量上来之后,专用AI加速器几乎是必然选择。但专用也带来“专”的代价:硬件一旦写成死结构,软件只能在这个框框里跳舞。所以硬件设计并不是越专越好,而是要在“专”和“可编程性”之间取一个平衡点。

1.2 软硬件设计要解决的三层问题

一套完整的AI芯片软件栈和硬件结构可以拆成三层来看,这很重要,因为后面所有设计矛盾都源于这三层之间的语义鸿沟。

第一层是算法层。用户用的是PyTorch、TensorFlow或者ONNX模型,里面有卷积、矩阵乘、LayerNorm、Softmax这些算子,还有各种张量形状变换。算法层的特点是高度抽象、变化快。今天流行Transformer,明天可能就出了新结构,硬件没法追着模型变。

第二层是工具链层。编译器接收这些高层算子,经过图优化、算子融合、布局转换之后,变成面向特定硬件指令集的低层级IR,再生成汇编或二进制指令。这一层是真正的翻译官,翻译得好不好直接决定了“同样的模型在这个NPU上跑多快”。

第三层是硬件层。包括MAC(乘累加)阵列、张量核、DMA、片上SRAM、总线、中断控制器。硬件层的特点是死板但高效:指令来了就执行,数据到了就计算,没有太多智能可言。

这三层各有各的“语言”,软硬件设计的本质就是把鸿沟填平。你可以在一个芯片的验证流程里看到这样的场景:硬件RTL还在调时序,编译器团队已经用指令级模拟器在生成汇编代码了;算法团队已经在出部署脚本了。三拨人对同一块芯片有完全不同的抽象视角,但最终要被一套接口串起来。

1.3 为什么“软硬件协同”不是口号而是刚需

用生活里的事打比方:你买了一台性能很强的车,但变速箱逻辑糟透了,城市里开起来顿挫又费油,这车你再怎么说“发动机马力大”也没用。AI芯片里,编译器就是那个变速箱逻辑。更麻烦的是,你的“变速箱”还不是事后刷个固件就能调的——硬件架构一旦定下来,流片成本几百万美元起步,改一版几百万,谁也没钱试错。

所以软硬件必须同时设计:硬件团队定计算阵列拓扑的时候,编译器团队要评估这个拓扑能不能映射常见算子;编译器定指令格式的时候,硬件团队要评估指令解码和发射的开销;仓库里同时维护架构手册、指令集文档、寄存器说明,任何一边改接口,另一边当天就得同步。这不是流程建议,是所有做过AI芯片项目的人用成本换来的教训。

2. 硬件设计的核心:算力、存储、互联的三角权衡

硬件设计听起来是“堆块头”,实际做起来全是取舍。同一个芯片面积里,你多放一个MAC核,就要少一块SRAM;多挂一块SRAM,互联的线缆和布线压力就上来了;总线带宽不够,再多的算力也保证不了利用率。这个章节我把硬件设计的几个关键维度掰开讲。

2.1 计算单元:从向量单元到张量核的演进路径

早年的DSP和加速器做矩阵运算,用的是SIMD架构:一组ALU同时处理多个数据点,写成代码时类似“对数组的每个元素执行同样操作”。SIMD对付向量游刃有余,但对付矩阵偏弱。以2x2矩阵乘为例,SIMD得先把矩阵按行拆开,做多次乘加操作,循环嵌套之间还有数据搬运开销。问题在于SIMD没有把“数据复用”的结构优势发挥出来。

张量核的解决思路是直接在硬件里内建一个脉动阵列或类脉动结构。这个概念最早被Google TPU带火:把一组乘累加单元排成阵列,数据在阵列中像流水线一样从一个PE(处理单元)流到下一个PE。这样做的好处是减少了寄存器堆访问和总线访问次数,因为数据被相邻PE接力使用。

举个例子,一个8x8的脉动阵列做矩阵乘,权重从左边广播进来,输入数据从上方流入,输出在下方累积。每个PE内部只需要一个乘法器和一个累加器,通过数据流动天然实现了复用。相比每个PE各自去读寄存器,总线压力大幅下降。走低功耗路线的AI芯片,几乎都沿用了这个思路。

硬件设计里还有几个容易被忽略的位宽问题,我建议你记住:推理芯片主流算子精度是INT8,但累加器往往要做到INT32,防止连续乘加之后溢出;如果做训练,则要考虑FP16、BF16、FP32混合策略。BF16这几年很流行,因为它把指数量级范围和FP32对齐,训练稳定,代价是尾数精度低,常常配一个FP32累加器来兜底。设计计算阵列时,位宽组合决定了乘法器的面积和功耗,是成本最敏感的部分之一,千万不能只盯着“TOPS”这一个数字。

2.2 存储层级与数据流:把数据喂到计算嘴边

芯片性能的第二座大山是数据搬运。业界流行一句话:算力白菜价,带宽黄金价。多放点MAC阵列很便宜,带宽和存储却直接决定最终跑分。

AI计算里主要有三种数据复用形式:

  • 输入数据复用(Input Stationary):同一个输入特征图被多个权重反复使用,权重流式加载,特征图固定在片上。
  • 权重复用(Weight Stationary):权重固定留在片上SRAM里,多个输入行轮流来乘,适合推理场景下权重相对稳定。
  • 输出数据复用(Output Stationary):输出累加结果留在PE内,避免频繁写回DDR。

设计时要根据目标场景定主数据流。比如大量跑卷积模型,权重不大但复用高,可以偏权重复用;大量跑Transformer这类大模型,权重动辄几GB,根本放不进SRAM,就得靠预取和流水线掩盖访存。不要试图找一种“万能数据流”,不存在。好的做法是把数据流做成可配置的,让软件栈根据具体算子和shape去选择。

存储层级一般分三到四级:寄存器堆(单周期访问)、SRAM(片上缓存,多个bank拼出高带宽)、DDR/HBM(片外主存)。每往下一级,访问能耗差一个数量级。一级片外访问消耗的能量可能等于几十次MAC运算的能量,所以“少访存、多复用”是所有硬件优化心里那本账。

2.3 互联架构与多核扩展:别让总线拖后腿

单核MAC阵列做到一定程度,就得考虑多核组网。常见方案有总线、NoC(片上网络)、Crossbar。小规模芯片用AXI总线就够,但到了多核集群,总线会成为瓶颈,这时候NoC成了主流:每个核通过路由节点转发消息,带宽可以随着节点数扩展,但路由器和缓存的面积开销也随之上升。

组网还要考虑一致性。有的AI芯片里CPU和NPU共享DDR,最典型的坑是:CPU写好了描述符,NPU读到的是旧数据;NPU写完结果,CPU看到的也是旧数据。这就要做缓存一致性协议,或者干脆在软件里显式flush/invalidate。但别小看这个细节,我在后面“踩坑”章节会专门拿出来讲,因为它坑过太多人。

互联的另一个重点是带宽匹配。假设一个核峰值算力是16 TOPS INT8,也就是每秒16 x 10^12次乘加。如果一条指令里每个MAC要读取两个8位操作数,理想状态下需要2字节/次乘加的带宽,那就是每秒32GB的流式数据带宽。加上权重搬运和写回,实际需求只会更高。如果总线和SRAM带宽给不到这个量级,算力利用率天花板就被锁死了。所以看一颗AI芯片的规格书,别只盯TOPS,要看“TOPS/带宽比”和“TOPS/功耗比”,这两个比值更能说明问题。

下面用一个简化示意参数表,帮你建立“推论”的感觉:

设计项典型选择设计取舍
MAC阵列规模16x16、32x32的脉动阵列越大算力越高,但布线压力大、电源电涌难处理
累加精度INT32 / FP32高了防溢出,但面积功耗上升
片上SRAM2 MB ~ 8 MB 多Bank大了减少DDR访存,但占面积
总线标准AXI4 / 类NoCAXI简单,NoC扩展性好
片外存储DDR4 / LPDDR4 / HBMHBM带宽高,但贵且封装复杂

3. 软件栈设计:把硬件潜力变成真实性能的翻译官

硬件做得再漂亮,如果编译器出来的代码只会“暴力搬数据”,那这颗芯片基本就废了。AI芯片软件栈不是单纯的驱动加几个算子,而是一整套从模型到硬件的全链路。这一章从头到尾讲透。

3.1 编译器设计的关键环节:前端、优化器、后端

编译器可以粗分成三段:前端把PyTorch/TensorFlow的模型转到中间表示(IR),优化器在IR上做图级优化,后端把优化后的IR映射到硬件指令。

前端这步,大多数团队会接ONNX或者自己定义一套类ONNX的IR。不要小看这步,实际上很多模型在导出时就有问题:动态shape、不支持的自定义算子、控制流。前端做的仿佛只是“翻译”,但做的却是“能不能被翻译成硬件接受的格式”。通常我会建议规范团队前的模型输入格式,把动态shape限制住,或者给用户提供一个重写算子的模板。

优化器是最能体现编译器功底的部分。图级优化一般包括:算子融合(把Conv+BN+ReLU合成一个算子,减少中间张量的写回和读取)、常量折叠、死代码消除、layout转换(比如把NCHW转成NHWC,满足硬件对齐要求)。这些优化看起来没有直接降低计算量,但每一个都可能在减少访存上带来成倍收益。

拿算子融合举例。假设一个卷积加一个ReLU,如果分两步做,卷积输出一次到DDR,ReLU再读一次,一个回合下来多出两次整张特征图的访存。融合之后,卷积的输出留在寄存器或SRAM里直接做ReLU,再统一写回。对于几十上百层的大模型,这种省下来的访存按GB计算。在我做过的编译器里,算子融合是性价比最高的优化,没有之一。

后端的调度是另一个重头戏。要把一个算子切成多个小tile(数据块),安排好每个tile的DMA搬运时机、计算阵列的空闲窗口、多核之间的负载均衡。这部分的算法往往和硬件微架构强耦合,难以完全自动调优,所以业界普遍的做法是:先有一版手写或模板化的高性能kernel,再逐步用AutoTVM/MLIR这类工具自动搜参数。

3.2 算子库、Runtime与驱动:软件栈的地基

编译器生成的代码不能裸着跑,还需要三层支撑:算子库是已经优化好的高性能kernel;Runtime管理内存池、任务队列、设备生命周期;驱动负责和硬件直接打交道。

算子库在AI芯片中扮演的角色可以这样理解:编译器负责通用情况的翻译,算子库负责把最常用的算子做到极致。大模型推理时,Conv、GEMM、Softmax、LayerNorm这些算子可能占据90%以上耗时,值得为每一类写多版本:不同的tile大小、不同的double buffer策略、不同的多核拆分模式。算子库的调优通常是一个持续打磨的过程,投产初期每提升10%都是团队喝的庆功酒。

Runtime的设计中,内存管理是重点。频繁向操作系统申请释放内存,在嵌入式场景会引入无法接受的抖动。成熟方案是启动时就挖好一块物理连续内存池,按需分配,算子执行完后回收到池里。AI推理还有一个特点:中间张量的形状相对固定,合理复用可以显著降低内存峰值。

驱动这一层则要把硬件抽象成软件能理解的样子。常见接口是:打开设备得到文件描述符 -> 映射硬件寄存器 -> 提交任务描述符 -> 轮询或等待中断获取完成通知。下面放一个简化流程示例,你感受一下软件视角下的硬件控制是什么样的:

1. 设备初始化:打开 /dev/ai_acc0,映射寄存器区域 2. 内存准备:分配输入、输出、权重缓冲区,并 mmap 到用户空间 3. 构建描述符:填充输入地址、权重地址、输出地址、算子ID、shape参数 4. 任务下发:通过 ioctl 或写寄存器触发 NPU 开始执行 5. 等待完成:阻塞等待中断,或轮询完成标志位 6. 回收结果:读取输出缓冲区,释放描述符内存

每一条硬件接口定义背后都隐藏着软件团队的智商税。比如描述符里字段是做什么的、地址需要对齐到多少字节、是否需要提前执行内存屏障——这些在接口文档里不写清楚,回片后就是一周夜的调试苦战。

3.3 端到端部署工具链怎么搭

AI芯片研发项目走到半程,通常要交付一套全流程工具,让用户从PyTorch模型一路跑到板子上出结果,中间尽量不要漏出“编译器内部报错”之类的生硬信息。

我建议的标配链路是这样的:

  • 模型解析和转换:PyTorch导出ONNX,再由工具检查算子支持度,不支持的算子给出替换或分解建议。
  • 精度与量化:INT8部署前先做PTQ(训练后量化),用校准集算出每层缩放因子;效果不够再上QAT(量化感知训练)。这一步最容易出问题的是校准集太少或分布偏移,导致最大值统计失真,后面精度章节我会展开。
  • 图优化与编译:进入编译器,算子融合、layout转换、内存规划、指令生成。
  • 模型封装:把指令二进制和权重包装成统一的模型文件,带上头部信息校验。
  • 使用时Runtime加载模型、解析头部、初始化计算图、执行推理。
  • 性能分析工具:提供profiler,能打印每层耗时、访存次数、带宽利用率。

部署工具链的质量直接决定芯片的“路人缘”,如果一个用户拿到的文档和工具链是“能跑但不好用”,他会觉得这颗芯片很鸡肋。所以做软硬件设计的人,一定得把用户视角放在心里。

4. 软硬件协同设计的关键:接口、验证、迭代

前面几章讲了硬件和软件各自的内部结构,但它们不是两条平行线,而是必须互相咬合的两个齿轮。这一章讨论怎么把咬合面磨平。

4.1 软硬件接口定义:指令、寄存器与描述符

从实践看,软件看到的硬件有两种典型姿态。一种是把NPU做成“任务型加速器”:硬件不执行通用计算,只接收描述符,描述符指定“我要跑什么算子、数据在哪、结果写哪”。另一种是“指令流型加速器”:硬件能执行一条条细粒度指令,类似一个小型DSP,编译器的后端直接生成指令流。

两种风格各有利弊。任务型接口简单可靠,但灵活度有限,遇到新算子只能硬件升级或走CPU回退;指令流型灵活度高,但验证难、编译器后端工作量大,如果指令集设计得不够规整,硬件实现会变成灾难。我在实际项目里偏向任务型和轻量指令混搭:绝大多数算子走描述符下发,少量特殊算子(比如非标激活函数)走微码执行。

无论哪种风格,接口都要在架构阶段定死。为什么?因为软件栈需要提前开发:编译器后端要对指令集编码,Runtime要对齐寄存器偏移,驱动要写中断处理。如果架构阶段接口还含糊其辞,后面每改一版,三套代码一起返工,光是沟通成本就能拖垮项目进度。我自己踩过最痛的坑就是寄存器地址在设计和验证之间改动没有及时同步,回片后软件照着旧文档写,第一次联调直接崩掉。

4.2 模拟器、RTL仿真与FPGA验证:软件先行的手段

既然芯片样品要等大半年才能流片回来,那软件团队怎么提前干活的?答案是“用替身”。业界通用做法是维护一个instruction accurate / cycle approximate的模拟器,它在服务器上模拟指令执行行为,既给编译器做正确性对照,也给性能估算提供依据。模拟器跑得慢,但开发及时。

RTL仿真阶段,硬件验证团队用UVM等框架搭testbench,随机激励检查指令执行和总线行为。软件团队可以跑更真实的用例:把编译好的指令二进制加载到仿真环境里执行,对数输出结果。这个过程很慢,但能在流片前发现八成以上的软硬件接口问题。经验是:在RTL里预留性能计数器寄存器,提前定义好每个事件的计数语义,比如“MAC idel cycle”“访存stall cycle”“DMA耗时”,让软件在仿真中就能读取,培养性能分析习惯。这个投入回报率极高。

FPGA原型验证又是一个阶段。FPGA频率低很多,但真实外部接口和板上行为跟ASIC接近,能跑完整模型,往往用来验证软件栈端到端是否正确。注意FPGA时序收敛本身是个体力活,别把它当成“免费的芯片”,它的布线布局以及DDR约束和正式芯片仍有偏差,有些时序相关bug在FPGA上不一定暴露。

4.3 性能评估与瓶颈分析:让优化有的放矢

优化一个模型在AI芯片上的性能,不能靠拍脑袋。最常用的框架是Roofline模型。它的核心思想是:一个程序的性能由两个上限定住,一个来自算力上限(每秒能执行多少次操作),一个来自存储带宽上限(每秒能搬运多少字节)。你算出“运算量/访存量”的操作强度,对比硬件参数,就能判断自己是算力受限还是带宽受限。

做性能分析时的执行顺序,我建议这样:

  • 采集profiler数据,统计每层算子耗时和访存量。
  • 对照Roofline图,看主要算子是计算受限还是访存受限。
  • 如果访存受限,优先检查数据复用是否没做,比如没有融合、没有复用权重。
  • 如果计算受限,检查利用率:是不是有小tile导致MAC阵列空转太多。
  • 优化一个算子后再全模型联测,因为局部提速可能把瓶颈推给别的算子。

用一个实际例子说明:某个模型在128 TOPS的加速器上跑,实测平均只有40 TOPS,利用率为31%。看每层统计后发现GEMM层只占30%时间,大量时间花在LayerNorm和Softmax上,而这些算子访存重、计算轻,走的是“DDR搬来搬去”的路线,完全没吃满MAC阵列。这时候单靠优化GEMM的kernel无效,正确做法是改造整个执行流水,把norm算子融合到前一个GEMM的尾段里,降低DDR访存频率。类似的瓶颈分析是每个做AI芯片性能的工程师的基本功。

5. 工程实战:踩过的坑、排查思路和几条保命经验

前面讲的都是“书本上可能写好”的知识,这一章写的是我在项目现场摔出来的经验,可能更直接一些。

5.1 从仿真到回片:联调期最常见的几个坑

最经典的问题是DMA地址对齐。硬件为了简化设计,通常会要求地址按32字节或64字节对齐。真实模型里的Tensor地址很可能不满足对齐要求,一旦编译器没有处理好pad,硬件就直接访问了非法地址,表现是“结果随机错误”或者“卡死”。排查方式很简单:用万用表量总线上有没有异常地址,或者软件侧把所有地址打印出来比对对齐规则。

第二个高频问题是cache一致性。CPU侧把任务描述符写入内存,NPU直接通过DMA读取,如果CPU写的数还在cache里没有刷到主存,NPU拿到的是旧数据。很多团队的解决办法是在驱动里执行显式cache clean/invalidate。更隐蔽的是NPU写完结果后,CPU读数据时cache还是旧副本,这就要在NPU写完后执行invalid操作。看起来是软件层的小事,实际跑大模型时一旦出现偶发错误,排查周期至少一到两周。

第三个坑是启动流程和时钟稳定。SoC上电后,NPU的PLL不一定立刻稳定,中断控制器和工作时钟切换都有严格的时序要求。联调第一天经常是“软件永远等不到完成中断”,最后发现启动代码里没有等待锁相环锁定状态。建议回片前就把启动脚本的步骤和每步等待条件一条条和硬件验证团队过一遍。

5.2 算子精度问题:如何定位是硬件、编译器还是量化

精度不对是联调期另一大类问题。如果模型输出的错误率很高,先要区分是哪一环出的错。我的经验是按照下面顺序排查:

  • 跑最小的单算子用例。只执行一个卷积,输入全1或小随机数,和CPU参考结果对比。
  • 对比失败后,检查INT8量化参数。看每个张量的scale和zero point是否正确,特别是偏置量化的对齐方式。很多推理引擎的bias是INT32,和INT8激活一起进累加器,如果不转换就直接加,误差会直接累积。
  • 查看是否有溢出。INT8乘法结果进入INT32累加器一般不会溢出,但如果中间有FP16的存储,低尾数位宽可能导致累加误差。
  • 最后才怀疑硬件逻辑有bug。硬件bug通常表现为“固定pattern错误”或“某shape必然错误”,而不是随机误差。

有一个工程小技巧:通信端口打印出每一层的输出均值和方差,和参考模型对比,误差不大的说明链路基本通;误差在某一层突然放大,就锁定那一层。这个技巧我用了很多年,比直接整个模型对比高效得多。精度排查这块,最忌讳的是拿着一个大模型的现象去盲猜,那样只会绕圈子。

5.3 开发者效率:工具和沟通机制

最后分享一个人层面的经验:软硬件协同项目拼的不是个人英雄主义,而是沟通机制。

我常用一套“立桩式”开发法:先在整体架构图上标出所有核心接口和里程碑;每周固定一次接口Review会议,软件和硬件工程师一起过改动点;任何接口变更必须同步修改文档,并且用脚本做编译期检查,防止文档和代码脱节。听起来很枯燥,但它真能省下无数深夜debug时间。

另外一个建议是预留性能计数器。做RTL设计时,多花几个寄存器去记录MAC idle、DMA stall、总线等待这些事件,回片后的性能分析和优化就有据可查。很多团队嫌麻烦,结果回片后性能低于预期,连瓶颈出在哪都不知道,只能靠猜。

调试工具方面,JTAG和逻辑分析仪要常备。驱动跑不起来时,先在调试器里看寄存器是否按预期读写;总线波形能帮你确认DMA是否真的发出了突发读请求。软件侧呢,printf依旧是最流行的debug大法,尤其配合环形缓冲打印能保留最近一段log,偶发问题也好抓。

写到这,我不禁想起自己第一次完整走完“架构定义-编译器开发-回片联调”整个流程时的情形。最深的体会不是某项技术多厉害,而是“软硬件设计”这几个字里那个“软”字其实应该念作“协同”——两个团队从第一天就坐在一起,把接口、工具链、验证策略当成共同产品来做,项目才能走顺。如果你正打算启动一颗AI芯片,或者正准备优化一颗已有芯片,希望这篇内容能帮你在开工前少走几个弯路。第一版先讲到这里,后续有机会我再聊聊算子库的具体优化方法和编译器自动调优的深入细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询