
不少朋友来问我AI芯片到底是不是把一堆矩阵乘法单元堆上去就完事了每次我都要反问一句——如果编译器写出来的程序只能让这台设备跑到三成算力你还会觉得它是个好设计吗在AI芯片的软硬件设计里真正容易翻车的不是那几个能乘能加的算力单元而是软件和硬件之间怎么分工、怎么对话。这个系列写到第4篇前面聊过架构、算子、存储相关的内容这篇我想把软硬件设计这件事收拢成四个实战层面一起聊透指令集契约、数据流与片上存储、编译器运行时、以及硅前验证与软硬协同调优。做AI芯片的工程师或者正在接触加速器项目的学生都能在这里拿到一套马上能用的方法论。1. 软硬件边界设计先把芯片与编译器的“契约”签明白1.1 为什么不能把AI芯片做成通用处理器很多人的直觉是AI芯片本质上就是一堆乘加器MAC比谁堆得快、堆得省电。问题是如果硬件只堆算力却不告诉软件这些算力该怎么用最顺那这个芯片就只是一堆很贵的硅片。通用CPU的做法是把指令集做得通用靠乱序执行、分支预测、大缓存去适配任意代码这些机制占了大量面积和功耗。AI负载不是任意的它由卷积、矩阵乘、注意力、归一化、激活这几类相对固定的算子组成而且计算量高度集中在规则循环上。做AI芯片的正确姿势是把有限面积尽量切给PE阵列和片上存储而不是浪费在投机执行上。但是话说回来“把大多数资源给算子”不等于“把所有东西焊死”。软硬件边界设计的第一步就是决定哪些东西由硬件固定实现哪些地方必须让软件能够重新配置。这个边界如果划得靠前芯片好写、软件难写算子稍微一进化就废划得靠后硬件通用性上来了面积和功耗又压不住。每次评审架构双方在这个问题上的争论时间通常最长因为谁都没有标准答案只有真实负载才能给出答案。1.2 算子特化哪些功能吃“硬加速”哪些留给软件组合我常用的方法是把一两组真实模型跑一遍统计算子耗时占比然后按帕累托原则去划分。矩阵乘、卷积、注意力这类吃时间的大户占总运行时间通常在60%到80%往往值得做专门的硬件执行单元而softmax、layernorm、GELU这类小算子单独看占比不高但如果它们频繁访问片外存储就会变成隐形瓶颈。对这些小算子硬加速的目标是“减少中间结果落地”比如提供在线softmax用的scale/max辅助寄存器让编译器把softmax融合进前面的矩阵乘里而不是做一整套完整指令。这里有一个很多人容易犯的错觉得算子越多越好每条都来个专用硬件。几个算子拼出来的面积和延迟会拖累主频最终总性能反而变差。我的习惯是先接触发频率和收益再决定哪条指令进指令集哪条只进运行库。下面这张表是我在一个AI加速器项目上做过的粗略分类可以参考。算子类型耗时占比建议策略矩阵乘 / 卷积 / 注意力高特化硬件执行单元做深流水layernorm / softmax / GELU中专用辅助单元 编译器融合各种损失函数 / 控制流低通用指令组合不占芯片面积1.3 数据布局与量化格式也是“软硬件契约”的一部分把接口搞错的地方往往不在指令本身而在数据约定。同一份张量硬件期望的是NHWC连续排布软件给的却是NCHW那编译器就得乖乖做transpose白白浪费一大块带宽。类似的还有量化格式FP16、BF16、INT8、FP8各自的指数位/尾数位都不一样硬件支持哪几种、转换单元放在哪里必须在指令集规范里写清楚否则编译器看到模型里出现了不支持的格式只能报警。别小看这些约定它们决定了软硬件之间的通信协议。协议越明确编译器越容易做离线优化运行时越简单。协议含糊软件就只能靠猜测去试探硬件行为结果通常是兼容性差的驱动和一堆难以排查的bug。2. 数据流架构与片上存储芯片里最贵的是带宽不是算力2.1 为什么“算得动”不等于“喂得饱”如果只盯着峰值算力很容易把设计带偏。厨房里主厨刀功再好传菜的速度跟不上客人面前一样上不了菜。AI芯片里那个“传菜走廊”就是存储系统。要判断一个设计是否平衡我会先算算术强度Arithmetic Intensity即每字节访存能支撑多少次浮点运算。拿一个MNK1024的GEMM来说FP16数据计算量约为2.1GFLOP2×1024^3。理想情况下A、B、C三个矩阵各占2MB片外总访存约6MB算术强度约350FLOP/Byte。如果芯片标称100TFLOPS按roofline模型片外带宽至少得100e12/350也就是接近286GB/s这已经远超单颗DDR的能力得靠HBM这类高带宽内存。如果编译器调度不好同一个数据被反复搬来搬去实际算术强度可能掉到50FLOP/Byte以下那时候再好的HBM也救不回来。这就是为什么“喂得饱”比“算得动”更关键算力容易堆高带宽却受封装、功耗、pad数量限制基本属于稀缺资源。设计时先按带宽估算能支持多少算力再决定PE阵列规模比反过来从算力倒推要稳得多。2.2 三种经典数据流输出固定、权重固定、行固定数据流的选择直接决定了硬件Buffer形状和软件循环怎么编排。输出固定Output Stationary把每个PE看作一个部分和的累加器输入和权重流经PE适合输出通道数不多、输出张量可以被片上SRAM装下的场景。权重固定Weight Stationary把权重留在PE里输入数据广播过去适合K很大的全连接层因为权重只加载一次、复用次数多。行固定Row Stationary把一个输出通道对应的部分和按行展开到PE阵列上同时让一行输入在PE间流动这样卷积和矩阵乘能共用一套硬件利用率更均衡。实际芯片不会只用一种数据流往往是给PE阵列配多个缓冲区和可配置的读取模式。软件编译算子的时候需要根据算子形状决定用哪个模式。这里我踩过的坑是把卷积的数据流设计直接搬到注意力上结果QK^T这一步的矩阵形状和卷积完全不同复用率上不去带宽开销瞬间拉满。后来改成按序列长度方向优先切分才把性能拉回来。2.3 片上存储分层与tiling编译器眼里的Buffer片上存储通常分四五级寄存器文件、每个核私有的SRAM、共享L2、HBM/DDR。每一级带宽差一个数量级价格也差一个数量级。编译器最重要的工作之一就是把大矩阵切成能放进片上SRAM的小块并在计算的同时安排好DMA搬运让片外数据像流水一样提前到位。# 在M、N、K上做三维分块使得两个tile能放进片上SRAM for i in range(0, M, TM): for j in range(0, N, TN): for k in range(0, K, TK): load(A[i:iTM, k:kTK] - SRAM_A) load(B[k:kTK, j:jTN] - SRAM_B) C[i:iTM, j:jTN] SRAM_A SRAM_B这段代码看起来只有三行真正难的是TM、TN、TK选多少。选太小循环嵌套太深指令和DMA启动开销把算力吃没了选太大SRAM放不下编译器只能做部分缓冲双缓冲失效计算和搬运不能重叠。通常取TM×TK×2字节和TK×TN×2字节都小于片上SRAM的一半另外一半留给输出缓冲和其他临时量这样才能保证双缓冲真正跑起来。建议设计硬件时就把SRAM的bank数给够否则一眼看过去带宽够实际访问会因为bank conflict再次腰斩。3. 编译器与运行时真正决定芯片上限的是软件能不能喂饱它3.1 编译器在AI加速器里做的不是“翻译”传统编译器是把高级语言翻译成指令序列AI编译器更像一个“计算图调度器”输入是模型结构输出是能在加速器上依次执行的一串内核配置。算子融合、内存规划、多核任务划分这些步骤每一项做不好都可能导致性能直线下降。而且这些步骤是离线完成的编译器多花一分钟优化运行时可以受益成千上万次所以优化可以做得很大胆不必担心编译时间。提到后端每个加速器都有一套自己的内核描述语言或微码硬件团队会强调通用性软件团队则希望指令粒度越粗越好。最终谈出来的形态通常是一套支持数据并行、流水并行的算子原语再由编译器把原语拼接成完整任务流。这里有个容易被忽视的坑IR设计如果只考虑当前模型家族下一代网络结构变了就得重写编译器前端。3.2 算子融合不是优化项是必选项在AI芯片上做融合核心收益是减少中间张量落回片外。以attention计算为例QK^T的结果矩阵尺寸是序列长度平方。序列长度2048时这个中间矩阵在FP16下就有8MB左右老老实实写回HBM再读出来一次来回就是16MB的流量。融合的做法是在片上滚动计算max和scale让softmax与后面的矩阵乘一起做中间根本不出片外这一下访存量就能降一个数量级。类似的融合组合还有ConvBNReLU、LinearGELU、LayerNormResidual量化。但融合不是“越狠越好”。我见过有人信心满满地把三个大算子强行融成一个内核结果中间复用距离太长寄存器放不下编译器被迫插入spill性能反而比分开跑还差。正确做法是先用profile看访存热点再按“热点优先、距离可控”的原则做融合每融一组都跑一次回归。注意融合的最终效果受片上SRAM容量限制判断标准是“中间张量是否可能全部留在片内”不是“算子编号有没有意义”。3.3 运行时调度与动态shape加速器执行任务时多核之间需要同步。硬件通常提供barrier、事件等原语运行时负责把大任务切成多个wave让不同核心尽量同时结束避免出现“部分核在等部分核在忙”的尾延迟。工作量不均匀时还需要动态负载均衡比如把大算子切成细粒度子任务按核心的空闲状态动态分发。另一个让编译器团队头疼的是动态shape。真实业务里句子长度、batch size经常变化如果每次变化都重新编译性能肯定崩。好一点的硬件会在启动内核时支持配置shape寄存器编译器只生成一个带参数的调度计划运行时把当前维度填进去。架构评审时如果发现硬件不支持动态shape我的建议是直接打回否则后面上线LLM服务时会非常痛苦。3.4 roofline模型与性能计数器调优时第一件事是用性能计数器看芯片到底卡在哪个环节。Compute stall高说明PE在空等Memory stall高说明访存没藏住NOC stall高说明片上网络堵了。把这些统计套进roofline模型里如果实际性能点在roofline下方很远多半是程序结构问题先改访存如果已经贴近roofline但离峰值算力还远说明当前算子的算术强度上限就这样再调软件很难有质的飞跃得换硬件方案。4. 性能模型、验证与调优实录硅前就开始“吵架”4.1 Cycle级仿真慢性能模型才是软硬件协同的主战场RTL仿真精确但是慢。一个几百层的模型在cycle级仿真上跑完一次可能要好几个小时根本支撑不了架构探索的迭代速度。我经手过的加速器项目通常会维护三套工具cycle级RTL仿真用来查功能和时序问题性能模型用C或Python模拟指令流和访存几分钟就能跑完一次典型负载FPGA原型则用来做软件栈的联合调试。验证方式精度速度主要用途Cycle级RTL仿真高慢模块功能、时序验证性能模型中快架构探索、瓶颈分析FPGA原型较高较快软件栈联调、集成测试这里要强调性能模型必须和RTL做校准不能各说各话。每月对一次关键算子的延迟和带宽统计公开数据挂在墙上双方都能看到差异才能避免硅后才发现模型错得离谱。4.2 软硬件协同调优的完整闭环调优不是研发收尾时才做的事情而是贯穿整个项目周期。先把标准benchmark和几个真实模型定成基线每轮优化后profile看瓶颈。优先调软件因为改tiling、改调度顺序、改融合策略的成本最低收益往往也最大确实动了硬件参数比如Buffer大小、PE数量、NoC带宽分配就要重新跑性能模型和回归。保存所有benchmark结果防止一个改好了另一个坏掉。常见的指标包括算子有效利用率、芯片峰值利用率、端到端吞吐、线延迟LLM场景。选指标时别只顾峰值利用率端到端吞吐才是用户感受最直接的参数。两个芯片峰值利用率相差5%但如果一个任务的启动开销大端到端可能反而慢30%这种现象多发生在小模型频繁加载的场景。4.3 常见问题速查表与排查思路下面这张表是我在调优过程中经常拿来对照的速查表每一条都对应过真实案例。现象可能原因排查方向kernel利用率低tiling太大或循环开销过高检查SRAM占用、循环层级memory stall偏高双缓冲没生效查DMA是否覆盖计算阶段多核利用率不均任务划分不均看wave统计做动态负载均衡内核数量过多融合不足用profile统计算子链长度片外带宽跑不满访存模式不连续检查数据布局和转置开销切换算子后等待长硬件配置开销高运行时做算子合并减少切换排查的核心原则是从上往下、先软件后硬件。大多数“芯片性能差”的问题最后都发现是软件没有理解硬件而非硬件真的跑不动。只有当软件已经把tiling、融合、调度都做到位指标依然不达标时才值得回头去动硬件参数。4.4 一点个人实践体会经历过几次从架构定义到硅前验证的完整流程之后我最大的体会是软硬件边界不是评审会上一次性定死的而是得靠真实负载、性能模型、编译器反馈三方一起逼出来的。硬件团队最初给的buffer大小一定会在某个算子的访存模式下被证伪编译器团队最初发的调度方案也一定会在某个硬件限制下被推翻。这不是项目失控恰恰是协同设计该有的样子。给新入行的朋友一个具体的建议接新项目别先翻架构文档先找项目里能跑通的最小编译流程demo半天跑通它你对整个软硬件接口的理解会超过闷头读三天文档。