☰
AI芯片软硬件协同设计:从编译器到Runtime的实战经验
2026/10/21 1:04:49 网站建设 项目流程

AI芯片开发这几年热度一直很高,但很多人聊的是架构、算力、制程这些硬件话题,真正干过软硬件协同的人才知道,一颗芯片从点亮到能把模型跑起来、把性能压榨出来,中间隔着一条巨大的鸿沟。这条鸿沟就是软硬件接口的设计质量。我前前后后参与过两颗AI芯片的完整流程,一颗是面向端侧推理的小芯片,一颗是面向云端训练的大芯片,踩过的坑、填过的洞、撕过的文档数都数不清。这篇东西不打算讲那些浮在表面的概念,我想把“AI芯片的软硬件设计”这件事拆开,讲讲芯片真正从设计到跑通模型的过程中,软硬件两边到底是怎么咬合的、哪些地方最容易翻车、以及为什么很多性能问题最后都指向软件工具链而非硬件本身。

无论你是刚入行的芯片验证工程师、做算子库的软件工程师、还是带团队的架构师,这篇内容都值得你花十几分钟过一遍。它不是教科书,更像是一个做过的同事在给你划重点。

1. 为什么AI芯片必须“软硬件一起设计”,而不是先硬件后软件

很多人第一次接触AI芯片项目时,默认的流程是:先定硬件架构、做RTL、流片回来之后再写软件栈。这个思路在通用CPU时代基本成立,因为在通用处理器上,软件生态是现成的,编译器、操作系统、应用层都已经非常成熟。但AI芯片不是通用处理器,它是一颗典型的DSA架构芯片,也就是面向特定领域设计的专用架构。DSA的核心逻辑是:用硬件上的定制化换取某个特定计算模式的效率,同时把灵活性牺牲掉,寄希望于编译器能在编译阶段把这些定制硬件管起来。

这就带来了一个根本性的问题:硬件一旦流片,它的计算模式、数据通路、存储布局就全部冻结了。如果软件工具链在芯片回来之前没有并行开发、没有在仿真环境里做足够深度的软硬件联合验证,那等芯片真的点亮的时刻,往往就是你项目噩梦的开始。

1.1 软件吃什么、硬件拉什么:两条线的咬合点

我们把一次AI推理过程简化一下。模型在网络层面是算子图,你写一行conv2d(x, weight),这行代码在PyTorch里只是一层抽象。真正落到芯片上执行时,它需要经历:算子被解析成底层指令、指令被调度器分配计算单元、数据从外部存储搬到片上、计算阵列执行乘加运算、结果再搬运回去。这条链路里,硬件的职责是“执行搬运和计算”,软件的职责是“决定什么顺序搬、搬多少、什么时候算”。

软硬件设计的本质,就是在两者之间划定一条边界。边界画得好,软件可以充分调度硬件资源,性能逼近理论峰值;边界画得差,哪怕硬件峰值算力再高,实际跑出来的有效算力可能只有两成。这条边界由ISA、寄存器配置、中断机制、存储映射规则等等共同决定,它们就是芯片的“软件接口”。

很多人以为软件接口就是SDK里那几个API,其实真正的接口层面有两层:

  • 第一层是给上层模型开发者的:比如ONNX Runtime适配层、自研推理框架的算子API。这一层要保证“看起来像GPU那样开发便利”。
  • 第二层是给底层驱动和Runtime的:比如指令下发格式、DMA描述符、Ring Buffer管理、中断机制。这一层要保证“硬件的每个能力都能被触达,同时不会让软件误操作踩坏硬件状态机”。

这两层中间还有一层就是编译器。任何一个AI芯片项目,软件栈基本都逃不开“框架适配层、图编译器、算子编译器、运行时Runtime、驱动”这几大块。

1.2 一个看似正常却慢两倍的算子,往往是软硬件接口设计埋的雷

我举一个真实经历过的例子。我们内部那颗端侧芯片在仿真阶段跑的ResNet50,性能模型预测的延迟是5毫秒,编译出来的第一个版本实测跑了11毫秒。性能差了整整一倍多。当时第一反应是算子没优化好,大家蹲在那里调卷积的tiling参数,折腾了一周,性能只从11提升到了9。后来我们一个做验证的同事无意中发现,指令解析器在每个算子结束之后都会做一次同步屏障,而同步屏障会等待所有计算单元完全空闲才放行下一条指令。

问题就出在这里:卷积算子在最后一段循环,计算单元之间负载不均衡,有的PE已经算完了,但还有一小撮PE在等最后一批数据从DMA搬过来。因为软硬件接口里定义了一条“算子结束时必须等待所有PE空闲”的状态机规则,导致每次算子切换都白白浪费大量等待时间。而后续指令明明不依赖上一个算子的内部数据,完全可以提前启动。

这个问题的根不在硬件算力,也不在算子算法本身,而是软硬件接口把同步粒度定得太粗。后面我们把同步语义改成了“仅同步依赖边所指的存储缓冲区”,并在编译器里增加依赖分析,性能才回到6毫秒以内。这种问题在芯片回来之前想都想不到,只能在仿真平台里通过跑真实模型才能发现。所以软硬件协同设计,不只是口号,它需要工具链和硬件验证平台在项目初期就深度绑定。

2. 硬件侧的架构决策:算力之外几乎全是存储与数据通路的故事

聊到AI芯片,所有人第一眼看的就是TOPS,也就是每秒万亿次操作数。但真的做过芯片你就会明白,TOPS只是天花板,真正决定实际性能的是三件事:数据能不能喂得进去、片上存储够不够用、计算单元能不能一直满载。这三件事没有一件是单纯靠堆算力能解决的。

2.1 主计算阵列:MAC阵列要配合数据复用,而不是光堆数量

AI芯片的核心计算单元是MAC阵列,每个MAC单元一次做一个乘加运算。一个8x8的阵列,一个周期能做64次乘加。听起来很简单,但阵列的利用率取决于数据复用。卷积操作里,同一个输入像素会被多个输出通道的卷积核重复使用,同一个权重也会被多个输入位置的像素重复使用。如果阵列每次计算都从外部存储重新取数,那带宽马上变成瓶颈,算力再高也白搭。

所以硬件设计里必须有线缓冲和输出缓冲。比如我们常用的策略是:输入有行缓冲,把输入特征图按行流水地往阵列送;权重有权重缓冲,一个权重复用给一批输入;输出有累加缓冲,等一个tile的所有累加完成后一次性写回。片内缓冲和计算阵列之间是乒乓关系,一组缓冲在往阵列送数据的时候,另一组缓冲正在接收DMA从外部存储搬来的新数据,这样计算才不至于停下来等数据。

具体来说,tile大小的选择是软硬件协同的第一个分水岭。我见过很多做算法的人习惯直接选整个通道作为tile,这在片内SRAM足够大时没问题,但在端侧芯片上很快就发现权重放不下、输入特征图的一整行也放不下。所以算子编译器必须结合SRAM容量做分块,这个分块参数直接决定实际能跑多少效率。分太小,搬运次数指数级增加;分太大,SRAM溢出,只能依赖编译器做spill,性能崩盘。

2.2 片上存储层级:为什么SRAM容量比峰值算力更影响真实性能

假设一颗芯片峰值算力是20 TOPS,工作频率是1GHz,那意味着每个周期要完成20000次乘加。一次MAC需要读两个操作数、写一个累加结果,粗略算下来,每周期至少要读40000字节的数据。如果这些数据全部从DDR拿,DDR带宽就算做到100GB/s,也覆盖不了这个需求。这就是AI芯片设计里常说的一句老话:“算力易得,带宽难求”。

因此片上的多级存储就非常重要。通常AI芯片会有三级:

  • 寄存器/累加器:每个PE内部的数据暂存,速度最快,容量很小。
  • 片内SRAM:通常几百KB到几MB,作为主缓冲池,承载tile数据、权重、中间结果。
  • 外部DDR/HBM:模型参数和整幅特征图的存放地,速度慢但容量大。

SRAM的容量直接决定了编译器能把多远的数据“捞”到片上。如果SRAM有2MB,那很多模型层级就可能整个放进去,减少外部访问;如果只有256KB,那每一层都得分块搬运,软件开销会非常重。选型时我们一般会算一个“单算子最大tile需求”:输入特征图一个tile + 权重切片 + 输出tile,再加上中间缓冲和double buffer的空间,4倍于计算所需的数据量。这个数算出来,基本就知道SRAM该做多大了。

2.3 数据通路设计:软流水、ping-pong buffer与访存冲突

存储层级定了之后,下一个问题就是数据怎么流动。单纯把数据从DDR搬到SRAM,再从SRAM送进阵列,这个顺序执行的话,计算单元大部分时间在空转。所以硬件上要有足够深的流水线机制。

最基础的设计是DMA搬运和计算阵列并行工作。比如在计算第N个tile的时候,DMA同时预取第N+1个tile到SRAM的另一块缓冲里。这个机制要求SRAM至少分成两块独立缓冲,也就是我们常说的ping-pong buffer。编译器在生成指令序列时,必须按照“搬运、计算、搬运、计算”的节奏交错安排,把计算时间隐藏到搬运时间里。

但这里有个很难缠的问题:外部存储访问冲突。如果多个DMA通道同时启动,或者计算阵列同时发起了非对齐的数据访问,DDR控制器就会产生bank冲突,实际带宽瞬间掉一半。我们在调试一个视频模型时发现,明明带宽估算是足够的,但跑起来整体帧率就是上不去,最后用硬件性能计数器一看,DDR的读带宽利用率只有34%,因为三个DMA通道反复抢同一个bank区间。后来在编译器调度时给DMA通道加了起始地址偏移和bank分配的互斥策略,带宽利用率才恢复到70%以上。

3. 软件侧的工具链:把浮点运算图变成硬件能跑的指令流

硬件能获取的性能上限是固定的,决定实际性能的就是编译器和Runtime。很多人低估了工具链的工作量,觉得“能编译跑通就行”,但实际上工具链的深度直接决定芯片的生死。我常说一个观点:AI芯片的竞争力,一半在架构,一半在编译器。如果架构是90分而编译器只能发挥50分,那有效算力就是45分,而一个80分架构配一个80分编译器,有效算力是64分,后者反而赢。

3.1 编译器栈的四个层级:从计算图到循环级优化再到指令级

AI芯片的编译器通常分四层:

  • 模型级优化:拿到计算图后做算子融合、死代码消除、常量折叠。比如把Conv+BN+ReLU融合成单个算子,或者把相邻的Elementwise算子合并,减少中间张量的DDR读写。
  • 算子级优化:针对具体算子做计算变换。比如卷积可以转成GEMM,也可以用Winograd做变换减少乘法次数,或者用FFT做频域卷积(在滤波核较大时更划算)。
  • 循环级优化:这是最核心也最繁琐的一层。编译器要把算子内部的循环做分块、向量化、并行化、重排序,把循环的访问模式映射到硬件的数据通路和缓冲结构上。
  • 指令级调度:生成底层指令序列,安排指令发射顺序、同步点、DMA预取,处理指令流水线冒险。

很多人会问:为什么不能像GPU那样,直接用现有的TVM或MLIR做一套适配就完了?理论上确实能这么干,但AI芯片的一大特点就是各家硬件差异极大。有的芯片支持动态形状、有的只支持静态形状;有的PE阵列是大块SIMD,有的是众多小核MIMD风格。TVM的调度模板是为GPU的线程结构设计的,换到自定义DSA上,你得自己写大量的调度原语和代码生成后端,这活本质上就是在做一个行业级编译器,难度和工作量都不小。

3.2 SIMT与SIMD的路线之分:这是个架构决策,不只是软件风格

GPU走的是SIMT路子,一组线程在硬件上按锁步方式执行,但线程是独立的,允许存在分支发散。AI芯片则常见两种风格:

  • SIMD风格:一个大PE核内包含多个ALU向量单元,一条指令控制所有单元做同样的操作。优点是指令开销小,控制逻辑简单;缺点是对数据排列要求很高,遇到不规则访问时效率暴跌。
  • SIMT风格:每个PE可以有自己的执行流,但同一组PE共享取指和调度。优点是可以容忍一定程度的负载不均衡和条件分支;缺点是控制逻辑复杂,指令带宽开销大,片上存储消耗高。

我们那两颗芯片恰好走的是两条路线。端侧那颗是SIMD风格,编译器洗数据洗得很痛苦,任何padding、stride不规则的算子都要通过数据搬运指令去解决;云端训练那颗则更接近SIMT风格,写kernel的时候像在写微型GPU内核,但控制流的复杂性导致验证工作量大增。

对做编译器的人来说,这个选择带来的差异是巨大的:SIMD架构需要强大的内存对齐分析和向量化pass,SIMT架构则需要更复杂的调度器来管理并发执行单元。如果架构师和编译器负责人不是从一开始就坐在一起定这条路线,后面几乎一定出大坑。

3.3 算子映射的典型流程:以卷积为例的tiling与数据搬移

我以3x3卷积为例,说说在硬件上实际的映射流程。假设输入特征图是CxHxW,输出是C'xH'xW',片内SRAM只能容纳输入的一个横条带和三倍输出通道的权重切片。那么软件要做的事情大致是:

  1. 按输出通道切分:一次处理C_tile个输出通道对应的权重切片。
  2. 按空间位置切分:一次处理H_tile行、W_tile列的空间范围,确保输入patch和输出patch都放得下。
  3. 生成DMA搬运描述符:把输入patch从DDR搬到SRAM输入缓冲,把权重切片搬到权重缓冲。
  4. 发射计算指令:让MAC阵列对输入patch执行滑窗卷积,累加结果暂存在输出缓冲。
  5. 搬运回写:当前tile计算完后,把输出缓冲的数据搬回DDR。

这几个步骤每一步都有调度讲究。比如第3步要在第4步计算上一个tile的同时预取,否则计算会等搬运;第4步内部还要按行安排循环顺序,保证每个输入行缓冲从SRAM里只读一次,重复利用。我们实际调优时发现,tile的行高H_tile如果取得太小,行缓冲复用效率极低,搬运量能占到总耗时的一半;取太大又会溢出SRAM。最终是通过一个成本模型去搜索最优tile参数,而不是拍脑袋定一个固定值。

下面是一个极简的伪代码示意,描述编译器生成一个卷积tile的计算规划:

# 伪代码:卷积tiling调度示意 for co_tile in range(0, C_out, T_co): for h_tile in range(0, H, T_h): for w_tile in range(0, W, T_w): # 双缓冲DMA:预取下一个tile的数据到SRAM dma_wait(next_buffer_ready) dma_start(next_tile_desc) # 异步搬运 swap_buffer(current, next) # 计算当前tile for ci in range(C_in): for kh in range(3): for kw in range(3): acc += input_sram[ci][h+kh][w+kw] * weight_sram[co_ci][kh][kw] # 回写输出 dma_start(writeback_desc)

实际工程里的调度器会结合硬件寄存器资源、DMA通道数量、缓冲冲突等条件,把这个伪代码展开成真正的指令序列,并在指令之间插入合适的等待与同步。

3.4 调度器与存储分配:为什么编译器写得好等于多出一个架构师

如果把算子编译器拆开,一半的工作在“循环变换”,另一半在“存储分配”。存储分配说的是:哪些数据在什么时间必须放在某块SRAM空间里,什么时候可以释放、覆盖。这个问题的难度不亚于写一个寄存器分配器,因为所有tile数据都存在生命周期。

举一个常见的坑。编译器一开始比较简单,它会按算子依次分配SRAM空间:当前算子的输入、权重、输出各占一块独立空间。但模型很深时,中间张量一个接一个产生,如果每次分配都用全新空间,那2MB的SRAM可能在第5层就爆了。所以存储分配器需要做“张量生命周期分析”,让前一层释放的空间被后一层复用。我们刚开始没有做这个分析,跑MobileNet时只能把tile放得特别小,性能惨不忍睹。后来实现了基于存活性分析的存储复用,同样一颗芯片,MobileNet的端到端性能直接提升了40%。这个40%不是任何硬件改动换来的,纯粹是软件优化。

4. 软硬件接口:驱动、Runtime与一次完整推理的调用链

谈完编译器和硬件架构,终于要落到连接两者的那根轴心——Runtime和驱动。这根轴最容易被忽视,但它是芯片真正能被“用起来”的关键。一个企业级AI芯片项目里,Runtime团队和驱动团队往往是最后才组建的,但恰恰是他们决定芯片发布时是“能亮”还是“能用”。

4.1 设备侧Runtime的核心职责:指令队列与异步机制

设备侧Runtime的职责可以概括成一句话:把主机端下发的算子指令安全、高效地喂给硬件执行单元,并管理所有硬件资源。这里最核心的是指令队列。硬件通常提供一组寄存器,软件往里写指令描述符,芯片内部的微码或硬件状态机再从队列里取指令执行。

要做到安全和高效,指令队列通常设计为环形缓冲,也就是Ring Buffer。它的好处是:不用每次下发指令都切换用户态和内核态,主机侧Runtime只需维护头部和尾部指针,连续写入一批指令后只需要通知一次硬件“有新指令”。硬件取完指令自然会更新尾部指针,两边通过共享内存和门铃机制通信。

这里有一个安全性的讲究:驱动必须严格校验指令描述符里的地址偏移、长度、ID范围。我们遇到过内部测试时,某个算子生成的描述符长度超出了SRAM边界,直接导致硬件状态机跑飞。后来在硬件设计里增加了地址保护机制,越界访问会被截断并产生中断,但在此之前,驱动侧的校验仍然是第一道防线。

4.2 主机侧五件事:一次推理在主机侧的完整准备流程

从主机侧看,一次典型的推理调用大约要经历五个步骤:

  1. 模型加载与图编译:读取模型文件,经过图编译器和算子编译器,生成可执行的二进制指令流。
  2. 显存与缓冲分配:为模型权重、输入输出张量分配设备内存描述符。这个环节要注意内存对齐,通常对齐到64字节或更高,否则DMA效率骤降。
  3. 算子指令构建:把编译好的指令序列打包成指令描述符,写入Ring Buffer。
  4. 门铃触发:写硬件寄存器通知芯片侧有新的执行批次。
  5. 结果同步与回收:等待推理完成事件,回收输出张量,处理错误状态。

很多小白上手时会困惑:为什么每次调用都有这么大的固定开销?其实在真实的高性能推理引擎里,第1步和第2步会被缓存下来:图编译一次生成可执行文件持久化保存,内存缓冲通过内存池复用。真正每次推理只走第3到第5步。前两部的缓存设计,就是推理引擎性能能跑起来的关键之一。

4.3 同步与异步:事件机制怎么避免“空等”

AI芯片的推理过程天然是异步的:主机侧把指令交给硬件之后,理论上就可以去执行下一个任务,而不需要干等硬件算完。但异步也带来了复杂的同步问题。Runtime里必须有严谨的事件机制。

我们用到的模型是:主机侧创建一个事件对象,当硬件完成某个批次的所有算子时,会通过门铃或MSI中断更新这个事件的状态。推理引擎可以对这个事件执行“等待”或“轮询”。为了减少中断开销,在高吞吐场景下我们通常使用轮询,也就是主机侧在等待时间内会主动检查事件状态;低延迟场景才使用中断唤醒。

同步粒度也要仔细设计。如果每个算子都同步一次,性能基本废掉;如果一批算子做完才同步,又可能要承担延迟变长的代价。我们最终的做法是:以“一次完整推理”作为同步点,但在流式多batch场景下,用事件流机制让多个推理并行流水。调同步粒度是Runtime性能优化里最磨人的部分,没有通用方案,只能根据硬件特性和业务负载反复调。

4.4 一次完整推理的任务清单与常见接口设计错误

下面是我整理的,执行一次端到端推理时,软硬件两层各环节的任务清单。这个东西看着简单,但每一个环节都有它的坑:

阶段软件动作硬件动作常见坑
模型解析算子图融合、内存规划无动态形状导致内存规划失败
算子编译循环切分、存储分配无tile参数与SRAM容量不匹配
指令下发写Ring Buffer、更新门铃取指、译码描述符未对齐导致硬件异常
DMA搬运生成搬运描述符、地址计算外部存储读写地址跨界、bank冲突
计算执行等待搬运完成、发射计算MAC阵列流水计算同步粒度太粗导致空等
结果回写等待计算完成、生成回写描述符输出缓冲写回DDR回写与下一轮预取冲突
事件通知更新事件、唤醒等待任务门铃/中断中断频率过高拖垮CPU

这表格里每一行我都踩过至少一次。举个印象最深的:第一版驱动里,门铃寄存器写操作的时序没做内存屏障,CPU先写了Ring Buffer里的描述符,然后才写门铃,但由于编译器重排或CPU写缓冲的乱序,硬件在极短的时间内读到的是未写完的描述符,导致偶发性的解析失败。这个问题在本地测试十次不一定出现一次,一上服务端高并发压测就频繁发生。后来加了一条wmb()内存屏障,问题就消失了。这就是典型的软硬件接口时序问题,纯硬件的人很难想到,纯软件的人也不会往这个方向排查。

5. 真实项目中踩过的坑:量化误差、未知算子与布局不匹配

芯片开发到了系统集成阶段,各种“不期而遇”的问题就开始密集出现了。这些问题的共性是:在单独的模块测试时一切都正常,一旦把模型跑起来、性能测起来,问题就暴露了。我想写三个交叉领域的坑,它们都直接跟软硬件协同设计相关。

5.1 量化误差不是算出来的,是测出来的

AI芯片为了效率和存储带宽,普遍把权重和激活量化到INT8甚至INT4。量化感知训练在模型侧会模拟量化误差,但真正到了芯片上跑,问题要复杂得多:不同层之间的数据分布、不同layout转换带来的截断、还有硬件累加器的位宽,都会影响最终数值。

我们有一版在仿真里精度损失评估是0.4%,结果芯片回来后跑一个检测模型,mAP直接掉了3个点。排查下来发现,累加器的位宽设计是INT32,但是编译器在融合两个concat分路时,把中间结果先截断到INT8再送进下一层,这个中间截断在仿真评估工具里没有被建模出来。后来我们在编译器中间表示里增加了“数值精度传播”的分析pass,所有中间张量的精度都被显式标注,任何截断操作都在图上可视化呈现,才把这类问题压住。

量化这件事,我唯一的建议是:尽早建立端到端的bit-accurate仿真链路,让模型数据从图编译、算子仿真到硬件RTL仿真全程跑同一份数值,而不是各模块各自为政。链条越早打通,后面流的泪越少。

5.2 未知算子与降级路径:Fallback机制的第一个版本几乎都会炸

任何AI芯片的工具链都不可能覆盖所有算子。模型里出现一个不支持的算子时,需要有降级机制,通常是把该算子的计算放到主机CPU上执行,结果再传回设备侧。听起来很简单,但有一个隐蔽的性能陷阱:降级路径的数据搬运。

我们早期版本对降级算子的处理是:把设备侧DDR上的整个输入张量搬到主机内存,CPU算完后搬回设备侧。这个方案在公司内部一个小模型上跑通时没人觉得异样,直到有人试了一个类似UNet的模型,里面有几个不常见的上采样算子。结果一次前向传播里,这几个算子来回搬运了差不多2GB的数据,推理时间从几十毫秒暴涨到几十秒。

问题的根不在“不支持算子”,而在降级路径没有考虑数据局部性。后面的解决办法是:在图上做算子分区,把连续的CPU算子归并成一个子图,整块落在一个中间张量处理单元上批量执行,只在子图边界做一次数据搬运。经过这么改,那种混合模型的性能从几十秒降回了一百毫秒以内。这个优化现在回头看像是“显然应该这样做”,但第一版没人想到,原因就是大家默认降级是临时保底方案,没把它当成一个正式的软件路径来设计。

5.3 布局不匹配:NCHW、NHWC与内部格式的三角关系

模型框架里常用的张量布局是NCHW或NHWC,但AI芯片内部为了MAC阵列的访存连续性,往往希望数据是以某种定制化的channel-block布局存放的。编译器在图层面上要根据算子的实际运行位置,插入必要的“布局转换”算子。布局转换本身也是个算子,也要占用算力和带宽。

我们有一版工具链的布局转换实现是基于DMA+逐元素拷贝的,转换一个4D大张量的耗时甚至比真正的卷积算子还高。后来算法同事提了一个建议:尽可能把布局转换融合进卷积算子的tiling过程中,比如在DMA搬运输入patch时,直接在地址映射阶段完成channel重排。这个功能需要DMA引擎支持“地址重映射表”,属于软硬件协同设计的一个典型案例——没有硬件级的重映射支持,这个融合就做不到,只能靠多搬几次数据来硬扛。

现在很多芯片在DMA设计阶段就会加上各种“数据重排模式”,比如按固定间隔跳变、按通道翻转、按块交错。我建议做架构的朋友一定要重视DMA的地址变换能力,它虽然不直接贡献TOPS,但对真实模型性能的影响远大于多算一两个TOPS。

5.4 理论带宽的两成:从性能计数器找瓶颈

最后聊一个通用的排查方法。很多性能问题看起来毫无头绪,但几乎所有AI芯片都会提供一组性能计数器:指令发射数、计算周期数、DMA读写字节数、等待周期数、SRAM读写冲突次数等等。性能调优的第一步不是猜,而是读计数器。

我们有次跑一个Transformer类的模型,平均利用率只有35%,而且无论怎么调tile参数都提不上去。后来看了计数器发现,两个数据:一个是DMA通道的利用率极低,每一次搬运的数据量都很小;另一个是权重SRAM的bank冲突次数高得离谱。这两条线一交叉就明白了:tile切得太碎,每次搬运头开销占比太大;同时权重是按通道连续存放的,而计算阵列访问同一bank的频率过高。解决方案是把权重在预编译阶段做一次“channel交错重排”,让两个相邻输出通道的权重分布到不同bank。改完之后利用率直接到68%。

硬件性能计数器是芯片里最值得投资的调试设施,但它也要求在软硬件接口文档里定义清楚:什么事件、在什么条件下计数、如何清零。如果没有这一层,很多疑难问题根本没有办法定位。

6. 从软硬件设计的角度看AI芯片的未来方向

芯片行业变化很快,但软硬件协同设计这个主题只会越来越重要。我在这里说说我在实际项目里感受到的、正在发生的一些方向性变化。

6.1 DSA的边界:为什么推理和训练很难用同一颗芯片

DSA意味着硬件是为特定计算模式定制的。推理模型常见的是小batch、低延迟、权重固定,硬件可以针对稀疏性、权重复用做深度优化;训练则是大batch、高吞吐、需要反向传播和梯度更新,内存访问模式完全不同。把这两者硬塞进同一个架构,往往是两头不讨好。

我们内部讨论过能不能做一颗统一的训练推理芯片,结论是:可以,但效率很难同时极致。后来业界很多厂商选择的路线也是推出一款训练芯片和一款推理芯片,共用同一套工具链和Runtime,让上层用户无感。这个策略给我们做软硬件的人带来的启示是:工具链的迁移成本比芯片本身的成本更值得重视。只要你软件接口设计得足够统一,用户从推理芯片迁移到训练芯片的代价就能大幅降低。

6.2 可重构的边界:灵活性不是免费的,最终由编译器来买单

可重构架构,比如可以在编译阶段配置计算单元互联、存储分区的芯片,听起来很美好:让硬件的灵活性适应不同模型。但代价是“配置”这个动作本身要花时间,而且配置完的硬件形态,它的编译映射问题远比固定架构复杂。

我们在调查这类方案时最大的感受是:任何一个硬件的“可配置旋钮”,都会给编译器增加一个优化维度。旋钮越多,编译搜索空间越大;搜索空间一大,要么编译时间爆炸,要么靠启发式规则草草选择一个次优配置。可重构架构要落地,核心是先保证编译器能够高效地探明配置空间,否则硬件灵活性就是个只能供演示的摆设。

6.3 从chiplet到系统级协同:软硬件边界还在往后移

先进封装和chiplet技术让AI芯片的形态进一步分化,可能存在一个计算die、一个SRAM die、一个IO die组合起来的系统。这意味着软硬件设计的边界不再只是芯片内部,而可能延伸到整个多die系统:数据在die之间的传输带宽、片上互联的拥塞管理、跨die的内存一致性问题,都要进入编译器和Runtime的考虑范围。

还有一个趋势是“算法与芯片的深度耦合”。随着大模型普及,稀疏注意力、混合专家等结构开始进入主流,未来芯片可能在架构里就会内置对某些结构化稀疏的支持。而对这些结构的编译优化,又会成为工具链的亮点。可以说,接下来几年AI芯片的竞争,会更明显地表现为软硬件联合创新的竞争,谁的工具链把硬件能力释放得更充分,谁的方案在真实负载上跑得更快,谁就赢。

最后说一点我个人的体感。这个行业里,硬件团队和软件团队之间的沟通成本高得惊人。最初几个月,我们为了一个同步语义,双方开会吵了三天,最后写进接口文档才算了结。回过头看,AI芯片的软硬件设计,最难的不是某一个具体的技术点,而是让两个知识体系完全不同的人,能够在同一个抽象层次上对齐概念、把问题描述清楚。如果你正准备进入这个领域,我的建议是,先别急着看那些高深的架构论文,先把指令集手册、tile调度流程和Runtime的同步模型吃透。这三样东西理清楚了,你也就站在了AI芯片世界里最关键的十字路口上。

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

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

立即咨询