☰
AI芯片软硬件协同设计:脉动阵列与FP8量化实践
2026/10/8 10:56:06 网站建设 项目流程

1. AI芯片软硬件协同设计的核心逻辑

1.1 为什么软硬件必须一起设计

做AI芯片这行的人都有一个共识:硬件堆算力容易,让算力真正跑满难。我见过太多团队,芯片流片回来理论峰值算力标得漂亮,实际跑主流模型连30%的利用率都摸不到。问题出在哪?出在软硬件各做各的。

AI芯片和通用CPU最大的区别在于,它的计算模式高度特化。矩阵乘法、卷积、注意力机制这些操作有固定的数据流模式,硬件如果按照这些模式去定制数据通路,效率能比通用架构高出一到两个数量级。但代价是,一旦硬件定型,能高效支持的算子集合就固定了。这时候如果软件栈不能精准地把模型映射到硬件上,芯片就是一块昂贵的硅片。

所以软硬件协同设计的本质是:在硬件设计阶段就考虑软件怎么用,在软件优化阶段理解硬件的瓶颈在哪。具体到工程实践,这意味着几个关键决策要一起做:

  • 数值格式的选择直接决定乘法器面积和功耗,也决定量化工具链的设计
  • 数据复用策略决定片上缓存层次和带宽需求,也决定编译器的调度算法
  • 指令集架构决定编程模型,也决定算子库怎么写

这三个决策环环相扣,任何一个单独优化都可能让另外两个变成瓶颈。

1.2 从模型到芯片的完整映射链路

一个典型的AI芯片软件栈,从上层到下层大致是这样的:

PyTorch / ONNX 模型 ↓ 图优化与算子融合(编译器前端) ↓ 量化与数值格式转换 ↓ 算子映射与调度(编译器后端) ↓ 指令生成与内存分配 ↓ 硬件执行

每一层都在做一件事:把上一层的抽象逐步降低,直到变成硬件能执行的信号。这个链路里最容易出问题的地方是量化与算子映射的衔接。量化把FP32的权重和激活值压成FP8或INT8,但不同硬件对FP8的支持方式不一样——有的支持E4M3和E5M2两种格式,有的只支持一种,有的甚至对累加器精度有特殊要求。如果编译器不知道这些细节,生成的代码要么跑不对,要么跑不快。

我个人的经验是,做AI芯片的软件栈,量化工具链和编译器后端必须是同一拨人设计,或者至少要有非常紧密的沟通。否则量化方案选了一个硬件不擅长的格式,后面怎么调都别扭。

1.3 当前主流技术路线的取舍

市面上AI芯片的软硬件设计大致分三个流派:

第一派:通用矩阵加速路线。代表是各类GPGPU和脉动阵列架构。硬件提供大规模的MAC阵列,软件通过编译器把各种算子拆解成矩阵乘法。优点是通用性好,新算子适配快;缺点是数据搬运开销大,能效比受限于内存带宽。

第二派:数据流架构路线。代表是各种TPU-like设计。硬件按照固定的数据流模式(如权重 stationary、输出 stationary)设计,软件需要把模型严格映射到这种模式上。优点是能效比极高;缺点是灵活性差,不支持的算子只能回退到CPU。

第三派:可重构架构路线。硬件提供可配置的计算单元和互连网络,软件在运行时动态配置。优点是兼顾灵活性和效率;缺点是编译复杂度高,工具链开发难度大。

选哪条路线,取决于目标场景。如果是云端训练,通用性优先,第一派更合适;如果是边缘推理,能效比优先,第二派更有优势;如果是需要支持多种模型且对能效有要求,第三派值得考虑但工具链投入会很大。

2. 脉动阵列的基本原理与设计要点

2.1 脉动阵列到底在解决什么问题

脉动阵列这个概念最早是1978年H.T. Kung提出的,当时是为了解决VLSI时代计算单元和内存之间带宽不匹配的问题。放到今天的AI芯片语境下,它解决的是同一个问题:计算单元太快,内存太慢,数据搬运成了瓶颈。

传统CPU做矩阵乘法,是取一个数、算一次、存回去,内存访问次数和计算次数是同一个量级。脉动阵列的思路是让数据在计算单元之间流动,每个数据被取进来之后,参与多次计算再离开。这样内存访问次数可以降到计算次数的几分之一甚至几十分之一。

用一个生活化的类比:传统计算像是一个厨师做菜,每做一道菜都要去仓库拿一次食材。脉动阵列像是一条流水线,食材从一端进入,经过多个工位,每个工位加工一下再传给下一个,最后从另一端出来。仓库只需要在流水线入口送一次食材,出口收一次成品。

2.2 权重固定型脉动阵列的工作机制

以最经典的权重固定(Weight Stationary)脉动阵列为例,假设我们要计算矩阵乘法 C = A × B,其中A是M×K的激活矩阵,B是K×N的权重矩阵。

硬件是一个M×N的PE(Processing Element)阵列。每个PE里存一个权重值,第(i,j)个PE存的是B的第i行第j列的元素。计算过程是这样的:

  1. 激活值从左侧流入,第i行的激活值a[i][k]依次进入第i行的第一个PE
  2. 每个PE收到激活值后,与本地权重相乘,结果累加到本地累加器
  3. 激活值继续向右流动,进入下一个PE
  4. 经过K个周期后,每个PE的累加器里就是C的一个元素

这个过程中,权重只在初始化时加载一次,之后一直留在PE里。激活值从左边流入,横向流动。部分和留在PE内部,不需要搬来搬去。

注意:权重固定型脉动阵列适合权重矩阵较大的场景,因为权重加载的开销可以被大量激活值的计算摊薄。如果权重矩阵很小,加载开销占比就高了。

2.3 输出固定型与行固定型的对比

除了权重固定,还有两种常见的脉动阵列数据流:

输出固定(Output Stationary):每个PE负责计算输出矩阵的一个元素,激活值和权重都从外部流入,在PE内部相乘累加。这种方式的优点是输出不需要在PE之间传递,适合输出矩阵较大的场景。缺点是激活值和权重都需要广播,对互连网络的压力较大。

行固定(Row Stationary):这是Google TPU采用的方式,结合了权重固定和输出固定的特点。每一行PE负责计算输出的一行,权重在行内固定,激活值在行内流动,部分和在行间传递。这种方式在卷积神经网络上表现很好,因为卷积可以展开成矩阵乘法,且权重复用率高。

三种数据流的对比如下:

数据流类型权重加载次数激活值加载次数部分和传递适用场景
权重固定1次N次无权重矩阵大,激活矩阵小
输出固定M次K次无输出矩阵大
行固定1次N次行间传递卷积为主,权重复用高

实际芯片设计中,很少有纯用一种数据流的,通常是混合方案。比如卷积层用行固定,全连接层用权重固定,通过配置寄存器切换模式。

2.4 脉动阵列的尺寸选择与利用率计算

脉动阵列的尺寸(M×N)是一个关键设计参数。尺寸越大,峰值算力越高,但利用率可能越低。原因很简单:如果矩阵维度小于阵列尺寸,部分PE就会闲置。

假设阵列是128×128,处理一个64×64的矩阵乘法。实际用到的PE只有64×64=4096个,而总共有16384个PE,利用率只有25%。如果处理的是256×256的矩阵,利用率可以到100%。

利用率计算公式:

利用率 = (实际计算量) / (阵列峰值算力 × 时间)

对于M×N阵列处理K维矩阵乘法,如果M、N、K都大于等于阵列尺寸,利用率接近100%。如果某个维度小于阵列尺寸,利用率就会下降。

实际模型中,矩阵维度是变化的。Transformer的注意力矩阵可能是序列长度×序列长度,序列长度从几十到几千不等。所以脉动阵列的尺寸选择要折中:太大则小矩阵利用率低,太小则大矩阵算得慢。

我个人的经验是,128×128到256×256是比较甜点的区间。再大,小模型利用率掉得厉害;再小,大模型算力不够。

3. 数值格式的选择与FP8实践

3.1 为什么数值格式是AI芯片的核心设计决策

数值格式决定了三件事:计算精度、硬件面积、功耗。这三者互相制约。

用FP32做推理,精度绰绰有余,但乘法器和加法器的面积是FP16的4倍左右,功耗也是4倍左右。用INT8,面积和功耗都小,但精度损失可能影响模型准确率。FP8是这两者之间的折中:面积和功耗接近INT8,但动态范围比INT8大得多,对异常值的容忍度更好。

FP8有两种主流格式:

  • E4M3:4位指数,3位尾数。动态范围较小,但精度较高。适合前向传播的激活值和权重。
  • E5M2:5位指数,2位尾数。动态范围较大,但精度较低。适合梯度计算,因为梯度值分布范围广。

注意:E4M3和E5M2的选择不是随意的。E4M3能表示的最大值是448,E5M2能表示的最大值是57344。如果激活值中有超过448的值,用E4M3就会溢出,必须用E5M2或者做缩放。

3.2 FP8量化对模型精度的影响

FP8量化不是简单地把FP32截断成FP8。直接截断会导致精度大幅下降,因为FP8的尾数只有2到3位,舍入误差很大。实际做法是缩放+舍入:

  1. 统计张量的最大值或分位数
  2. 计算缩放因子,把张量映射到FP8的可表示范围
  3. 对缩放后的值做舍入

缩放因子的选择很关键。如果按最大值缩放,异常值会拉低整体精度;如果按分位数缩放,超出范围的值会被截断。实践中常用的是动态缩放:每个batch或每个通道单独计算缩放因子。

我实测下来,FP8量化对大多数视觉模型的影响在1%以内,对语言模型的影响稍大,在1%到3%之间。但如果量化方案设计得不好,掉点可能超过10%。

3.3 FP8与GGUF量化格式的关系

GGUF是llama.cpp推出的一种模型文件格式,里面包含了多种量化方案。常见的GGUF量化类型有Q4_0、Q4_K、Q5_K、Q8_0等,数字代表位数,字母代表具体的量化策略。

FP8和GGUF量化是不同层面的东西。FP8是一种数值格式,GGUF是一种文件格式和量化方案集合。GGUF里也可以包含FP8量化的模型,但更常见的是INT4、INT5、INT8等整数量化。

如果要把FP8模型转成GGUF格式,需要注意几点:

  • GGUF的量化工具链主要支持整数量化,FP8支持有限
  • FP8的缩放因子需要额外存储,GGUF格式对元数据的支持需要确认
  • 转换后的模型在推理时的反量化开销需要考虑

提示:如果你手头有FP8量化的模型,想用GGUF生态的工具跑,最稳妥的做法是先反量化回FP16,再用GGUF的工具重新量化。直接转换可能会遇到格式不兼容的问题。

3.4 FP8在训练和推理中的不同策略

FP8在训练和推理中的使用方式差别很大。

推理场景:权重和激活值都用FP8,累加器用FP16或FP32。缩放因子可以离线计算好,固定在模型文件里。推理时不需要动态调整,硬件实现简单。

训练场景:前向传播用FP8,反向传播的梯度用FP8或FP16,权重更新用FP32。缩放因子需要动态调整,因为训练过程中激活值的分布会变化。这就需要硬件支持动态缩放,实现复杂度高不少。

目前FP8训练的主流方案是混合精度:大部分计算用FP8,关键部分(如LayerNorm、Softmax)用FP16或FP32。这样既能享受FP8的速度,又能保持训练的稳定性。

4. 软硬件协同的实操要点

4.1 从模型到硬件的映射流程

把一个训练好的模型部署到AI芯片上,大致要经过这些步骤:

  1. 模型导出:从训练框架导出为ONNX或类似格式
  2. 图优化:常量折叠、算子融合、死代码消除
  3. 量化:把FP32权重和激活值转成FP8或INT8
  4. 算子映射:把优化后的算子映射到硬件支持的指令
  5. 内存分配:为每个张量分配片上或片外内存
  6. 指令调度:生成指令序列,安排计算和数据搬运的顺序
  7. 代码生成:输出硬件可执行的二进制

这个流程里,算子映射和指令调度是最容易出问题的环节。因为硬件支持的算子集合有限,遇到不支持的算子就要拆解或回退。拆解的方式直接影响性能,回退到CPU则可能成为瓶颈。

4.2 算子融合的收益与风险

算子融合是编译器优化的常用手段。比如把Conv+BN+ReLU融合成一个算子,可以减少内存访问次数,提升性能。

融合的收益可以用一个简单模型估算:假设三个算子单独执行,每个算子需要读一次输入、写一次输出,总共6次内存访问。融合后只需要读一次输入、写一次输出,内存访问降到2次。如果内存访问是瓶颈,性能提升可能接近3倍。

但融合也有风险:

  • 融合后的算子可能超出硬件资源限制(如寄存器数量、片上缓存大小)
  • 融合可能改变数值精度(如BN的均值和方差在融合后需要重新计算)
  • 融合可能影响调试,因为中间结果不可见

我个人的做法是先融合收益大的算子,保留必要的中间结果用于调试。等模型稳定后再做更激进的融合。

4.3 内存层次的设计与数据复用

AI芯片的内存层次通常包括:

  • 寄存器文件:最快,容量最小,每个PE私有
  • 片上缓存:较快,容量中等,多个PE共享
  • 片外内存:最慢,容量最大,全局共享

数据复用的核心思想是:让数据在最快的存储层次里被多次使用。比如权重加载到寄存器后,参与多次乘加运算;激活值加载到片上缓存后,被多个PE读取。

数据复用率可以用算术强度(Arithmetic Intensity)来衡量:

算术强度 = 计算操作数 / 内存访问字节数

算术强度越高,说明每次内存访问对应的计算越多,对内存带宽的要求越低。脉动阵列的设计目标就是提高算术强度。

以一个128×128的脉动阵列为例,处理128×128×128的矩阵乘法:

  • 计算量:128×128×128 = 2,097,152次乘加
  • 内存访问:读入128×128的激活矩阵和128×128的权重矩阵,共2×128×128×4字节 = 131,072字节
  • 算术强度:2,097,152 / 131,072 = 16次乘加/字节

如果内存带宽是100GB/s,理论算力就是100×16 = 1600 GOPS。这个估算可以帮助判断内存带宽是否足够。

4.4 编译器的调度策略

编译器后端要做的事情,简单说就是在满足资源约束的前提下,最小化执行时间。资源约束包括片上缓存大小、寄存器数量、指令队列深度等。执行时间取决于计算时间和数据搬运时间的重叠程度。

一个好的调度策略应该做到:

  • 计算和数据搬运重叠:在计算当前数据块的同时,预取下一个数据块
  • 双缓冲:用两块缓存交替,一块用于计算,一块用于加载
  • 流水线填充:让脉动阵列的流水线尽量满

这些策略说起来简单,实现起来要考虑很多细节。比如双缓冲的切换时机,太早切换会导致数据还没算完就被覆盖,太晚切换会导致计算单元等待数据。

我踩过的一个坑是:预取距离设得太远,导致片上缓存不够用。预取距离是指提前多少个周期开始加载下一个数据块。设得太小,数据来不及加载;设得太大,缓存里堆了太多数据,挤占了其他用途的空间。后来我们做了一个自适应预取距离的机制,根据缓存压力动态调整,效果好了很多。

5. 常见问题与排查技巧

5.1 精度掉点问题的排查思路

FP8量化后模型精度掉点是最常见的问题。排查思路可以按这个顺序来:

第一步:确认掉点是否在可接受范围。不同任务对精度的敏感度不同。分类任务掉1%可能可以接受,检测任务掉1%可能就不能接受了。

第二步:定位掉点发生在哪一层。逐层量化,看哪一层的输出误差最大。通常是那些激活值分布范围大的层,比如注意力层的Softmax之前。

第三步:检查缩放因子。缩放因子是按最大值算的还是按分位数算的?分位数选了多少?如果按最大值算,异常值会拉低整体精度;如果分位数选得太低,截断误差会大。

第四步:考虑混合精度。对精度敏感的层保留FP16或FP32,其他层用FP8。混合精度的粒度可以是层级别,也可以是通道级别。

提示:排查精度问题时,建议保留一个FP32的参考实现,逐层对比输出。这样能快速定位问题层。

5.2 性能不达预期的常见原因

芯片跑起来但性能不达预期,可能的原因有:

现象可能原因排查方法
算力利用率低矩阵维度小于阵列尺寸检查模型各层的矩阵维度
算力利用率低内存带宽不足计算算术强度,对比带宽
算力利用率低算子融合不够查看生成的指令序列
延迟高数据搬运未重叠检查是否有双缓冲
延迟高流水线填充不足检查流水线深度和启动开销
功耗高数据搬运过多检查数据复用率

我遇到最多的情况是矩阵维度不匹配导致的利用率低。比如阵列是128×128,但模型里有很多64×64的矩阵乘法,利用率直接掉到25%。解决办法要么是调整阵列尺寸,要么是把多个小矩阵拼成一个大矩阵。

5.3 软硬件接口的调试技巧

软硬件接口是最容易出问题的地方,因为两边的人用的语言和工具都不一样。硬件工程师看波形,软件工程师看日志。接口出问题时,两边经常互相甩锅。

我的经验是:在接口上定义一套完整的断言和计数器。断言用于检查协议合规性,比如握手信号是否成对出现、数据是否在有效期内稳定。计数器用于统计接口的吞吐量、延迟、错误次数。

这套机制在流片前就能发现大部分接口问题。流片后如果还有问题,也可以通过计数器快速定位是硬件的问题还是软件的问题。

5.4 模型适配的避坑清单

最后整理一份模型适配的避坑清单,都是实际踩过的坑:

  • 动态shape支持:很多AI芯片对动态shape支持不好,模型导出时尽量固定shape,或者把动态维度放在最外层
  • 算子版本兼容:ONNX的算子版本更新很快,导出时注意目标硬件支持的算子版本
  • 量化校准集:校准集要覆盖实际推理时的数据分布,否则缩放因子会偏
  • 内存对齐:很多硬件要求内存地址对齐,分配内存时注意对齐要求
  • 边界情况:padding、stride、dilation这些参数在边界情况下的行为要和硬件确认
  • 数值溢出:FP8的表示范围有限,中间结果可能溢出,需要插入缩放或截断

这些坑每一个都可能导致模型跑不对或跑不快,而且排查起来都很费时间。提前了解可以省很多事。


我个人在实际操作中的体会是,AI芯片的软硬件设计没有银弹。脉动阵列、FP8、算子融合这些技术都是工具,用得好不好取决于对具体场景的理解。同样一个模型,在不同的硬件上最优的映射方案可能完全不同。所以做这个方向,既要懂硬件原理,又要懂模型特性,还要有足够的耐心去调优。踩坑是常态,但每踩一个坑,对系统的理解就深一层。

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

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

立即咨询