AI芯片性能瓶颈:访存墙与软硬件接口协同设计实践
AI 芯片的软硬件设计8访存墙与软硬件接口的协同博弈很多团队拿到一块新的 AI 加速芯片第一眼都盯着峰值算力TOPS 多少、TFLOPs 多少、是不是又翻了一倍。但我这几年的经验是真正决定一个模型跑得快不快的往往不是算力而是访存路径。算力就像洗碗机的转速带宽就像厨房的水管——水管粗度不够转速再快也是空转。这个系列聊到第 8 篇我想集中讲一讲我在多个 AI 芯片项目里的核心感受算力堆高不难难的是让软件真正把数据喂给计算单元也就是“访存墙”和软硬件接口的协同设计。这篇文章会覆盖内存带宽瓶颈的建模原理、加速器侧的数据流设计、算子库与编译器的适配逻辑以及一个从 18% 硬件利用率优化到 47% 的真实案例。如果你是刚入行的芯片验证工程师、做 AI 框架或算子库的软件开发者或者是准备做硬件选型的算法团队这篇文章应该能帮你少走不少弯路。我会尽量把原理讲清楚也把我在实际项目中踩过的坑和排查思路写出来而不是只给一个“性能调优秘籍”。1. 访存墙为什么算力堆得再高也跑不出标称性能1.1 用 Roofline 模型给“墙”画个边界我们常听到“内存墙”这个词但到底墙有多高绝大多数人没有量化概念。我最常用的是Roofline 模型把硬件的峰值算力P比如 256 TOPS和峰值带宽B比如 4 TB/s放在一起画两条线两线的交点对应的“操作强度” I P / B。对上面这个例子I 256e12 / 4e12 64意思是每个字节的数据搬进计算单元时至少要提供 64 次有效运算计算单元才能跑满。如果模型某个算子的操作强度只有 20那无论峰值算力多高实际性能都被带宽封顶在20 × 4TB/s 80 TOPS利用率只有 31%。这个模型极好用因为它在写代码之前就能告诉你优化方向是提高数据复用、降低搬运量还是换一种数据布局。我记得第一次用这个模型分析一个卷积网络时结论非常扎眼模型里很多小卷积层的操作强度只有 10~15就算计算阵列利用率是 100%整机也就能发挥出两三成的峰值算力。我个人的习惯是拿到硬件规格书先不急着跑 AI 模型而是先用一组微基准测出真实的可持续带宽——注意是“可持续”不是 HBM 理论峰值。实测里经常遇到的情况是连续读能跑得很高但一旦随机访问或带跨步就掉一半甚至更多。这直接决定了后面算子库开发时该用什么样的数据组织和搬运策略。1.2 数据搬运次数软硬件设计都要回答的“题眼”访存墙的本质是数据搬运次数太多。每个卷积算子输入特征图、权重、中间结果都要从 DRAM 搬进片上存储如果每次计算前都去 DRAM 取数那带宽再高也经不起折腾。解决思路只有一个把数据留在片上尽量多次复用。但这“多次复用”到底该在硬件上做还是软件上做不同芯片设计流派给出完全不同的答案。有些加速器硬件里集成了专用数据流调度器编译器只需要指出“这是卷积”硬件自己决定怎么搬数据还有些加速器走的是“软件定义搬运”的路线编译器把数据切块、缓冲、双缓冲全部显式表达硬件只是忠实地执行 DMA 命令。前者的优点是编程简单缺点是遇到非标准卷积或新算子时硬件调度器跟不上后者的优点是灵活缺点是对编译器要求极高一个切块参数不对性能就可能断崖下跌。我在自己做的一块模拟加速器项目上选择了折中方案硬件提供灵活的片上缓冲区描述符和 DMA 引擎软件通过描述符来控制数据流。这样做的好处是卷积这类高频算子可以被算子库压榨到极致当一个新结构出现时软件也能通过组合原子操作来适配不需要等下一代芯片流片。2. 硬件侧设计数据流架构与片上存储给软件留的“接口语义”2.1 数据流拓扑不是选一个字母那么简单加速器中最经典的问题之一是选数据流方向权重固定Weight Stationary、输入固定Input Stationary、输出固定Output Stationary还有行流水Row Stationary。如果只看论文感觉只是排列组合真做起来才会意识到这个选择直接决定了软件侧该怎么描述计算以及片上存储该怎么分区。我以卷积为例说明。假设输入通道是 128输出通道是 128卷积核是 3×3。如果选权重固定那么权重一旦从 DRAM 搬到片上缓冲区就要被尽量多的输入像素复用这时候输入特征的搬运方式必须是“一行一行推送”且每一行的水平复用窗口要覆盖 3 个卷积点。软件侧需要计算的切块参数就不仅是 NCHW 的 batch、channel、height、width还有“沿宽度方向一个 tile 内的输出像素数”这个参数决定权重被复用的次数。如果选输入固定思路反过来输入特征大部分时间待在片上权重以流形式进入。这种方式对权重大小和通道数异常敏感适合权重较小或可压缩的稀疏模型。而输出固定最容易被编译器建模因为输出矩阵的累加结果一直留在片上寄存器里但它对片上累加空间的容量要求很高。我的实际经验是没有哪个方案在所有场景都最优。高性能算子库最后都要做成“多数据流模板并存根据 shape 自动选择”。所以硬件团队在做微架构时千万别把数据流方向用硬连线焊死否则软件再怎么优化也只能戴着镣铐跳舞。一个可配置的数据流控制器哪怕只是支持“权重固定/输入固定”两种模式都能给后面的算子优化留下巨大空间。2.2 片上存储层次与 DMA 描述符软硬件真正的“握手面”片上存储怎么分层是我在所有芯片项目里最看重的一个设计点。常见做法是最靠近计算阵列的是寄存器文件和 L0 缓冲区接下来是一块比较大的 L1 SRAM或者叫 shared memory再往外才到 DRAM。有些芯片还有 L2 缓存专门用来承接算子间复用和中间结果驻留。这里的接口语义要非常明确。以 DMA 描述符为例我建议至少包含以下字段字段含义设计注意点src_address / dst_address源与目标地址必须支持任意对齐否则软件要填 padtensor_shape搬运的张量维度至少支持到 4D否则复杂 layout 要拆多条命令stride 数组各维度的 stride这是处理 NCHW/NHWC/自定义布局的关键是否触发中断搬运完成同步方式尽量用完成队列而不是逐次中断优先级与其他 DMA 的仲裁关系低优先级搬运应可被抢占这些字段看起来是硬件细节但在软硬件协同的项目中这就是软件工程师眼里的“硬件 API”。我见过一个项目硬件为了省寄存器把 stride 的粒度限制成固定倍数结果软件层不得不把所有张量 pad 成对齐形状浪费了大量片上空间和搬运带宽。教训是硬件上每做一个“简化”都要跟软件团队一起过一遍完整算子映射流程而不是只看寄存器面积。2.3 双缓冲与同步原语算得慢可以忍等数据不能忍数据流设计里我特别想强调“双缓冲”。简单说就是 DMA 在往缓冲区 B 搬运下一块数据的同时计算单元在处理缓冲区 A 中的数据两块缓冲区交替使用搬运和计算重叠把访存延迟藏起来。做得好硬件利用率能上来一大截做不好所谓的“双缓冲”只会在同步点频繁卡住。问题往往出在同步原语上。硬件如果只提供全局同步栅栏barrier那一个计算单元先算完也得等所有单元都到齐才能进入下一轮这种“木桶效应”会让细粒度的双缓冲形同虚设。更合理的做法是提供基于描述符的依赖链DMA 命令可以显式指定“等上一组计算命令完成后再启动”不同计算核心之间的同步画成有向无环图。软件映射阶段把每一轮迭代的依赖关系提前算好硬件按顺序执行而不是靠轮询等待。我在模拟项目里试过两种方案用纯栅栏同步时小算子重叠率只有 40%改成描述符依赖链后同样代码的重叠率到了 75% 以上。虽然增加了硬件设计复杂度但收益非常明显。3. 软件侧适配算子库、编译器与运行时如何吃透硬件特性3.1 算子库承担的是“硬件模板化”的脏活说到 AI 芯片软件栈很多人会高估编译器的能力低估算子库的价值。实际开发中卷积、矩阵乘、归一化这类高频算子几乎都是手写加自动调优出来的。编译器负责把模型翻译成计算图但最终落到加速器上的内核往往是从算子库里挑出来的模板。写算子库的时候最核心的动作是把数据流模板固化成代码对内做循环切分、向量化、乒乓缓冲对外暴露统一接口。一个典型的卷积算子模板切分逻辑大概长这样// 伪代码卷积算子模板的切分流程 void conv2d_tile(Input, Weight, Output, tile_params) { for (int n 0; n tile_params.block_n; n) { for (int oc 0; oc tile_params.block_oc; oc) { // Load weight tile to SRAM (double-buffered) dma_load(weight_tile[oc], src_weight offset, sync_flag); for (int oh 0; oh tile_params.tile_h; oh) { // Load input tile row to SRAM dma_load(input_tile[n][oh], src_input in_offset, sync_flag); // Compute mac array compute_mac(input_tile, weight_tile, output_acc, tile_params); } } } }但模板化最大的坑是“参数组合爆炸”。block_oc 取 16 还是 32tile_h 取 4 还是 8不同张量形状下性能能差出好几倍。我现在的做法是引入自动调优框架把模板参数范围定义好用一个短时基准测试在一组随机 shape 下搜索再把搜索结果固化成本地算子缓存。这套流程不能完全替代人工经验但至少能把“玄学调参”变成“半自动化找最优区”。3.2 编译器眼中的硬件抽象从循环变换到张量布局编译器要真正用好硬件特性它眼中的“硬件”得是一个干净的模型。我常用的是“分层抽象”最底层是一组类汇编指令LOADA、STOREA、MMA、SYNC 等中间层是带形状的循环结构最上层才接 AI 框架的计算图。每次硬件微架构调整都尽量只改底层指令的语义不让上层感知。循环变换是编译器发挥威力的第一站。同一个卷积可以拆成 NCHW、NHWC、NC1HWC0 等不同布局不同布局直接决定了访存的连续性和向量化宽度。我建议算法和编译团队早点在“图优化阶段”就把布局转换算子插入进去而不是让每个算子各自做 layout 猜测。否则就会看到一种很常见的性能事故前一个算子输出 NCHW后一个算子只能高效处理 NHWC中间被迫插入一个昂贵的 transpose搬运量直接翻倍。算子融合是另一件性价比极高的事。卷积后面通常跟着偏置加、ReLU、甚至 BatchNorm。如果逐个算子都写回 DRAM再读出来做下一次计算带宽成本能占到整体延迟的一半以上。融合的思路是把这些操作合并成一个内核中间结果只存在于片上寄存器或 L0 缓冲。我见过不少团队在这块嫌麻烦觉得“让硬件缓存兜着就够了”但结果是 L2 缓存命中率飘忽不定远不如显式融合稳定。3.3 运行时调度的隐性成本Launcher、量化缩放和负载均衡很多人把性能问题都归到算子和编译其实运行时Runtime也会吃掉大量性能。我印象很深的一次排查一个很小的全连接层在加速器上跑了 12 微秒但软件侧 CPU 提交任务、等待完成通知的开销占了 8 微秒。算力再快也架不住“启动指令比执行指令还慢”。这种启动开销的解法有几个方向。一是批量提交把多个算子的描述符一次性写入命令队列而不是每算子调用一次驱动。二是减少中断用完成队列累积状态计算单元跑完一组算子在队列里写一个标记CPU 批量轮询而不是处处同步。三是量化缩放处理很多量化模型的 per-channel 缩放因子需要实现为逐元素乘法如果运行时把这个操作当作独立算子下发又会多一次访存正确做法是把它融合进卷积的 epilogue 里。负载不均衡也是被低估的问题。用固定 tile 划分输出通道时不同通道组的数据分布差异会导致某些计算核心先干完活等着别人。解决思路是动态调度或持久化线程块。可在很多 AI 加速器上硬件原生并不支持动态领取任务这种情况下软件只能靠更小的静态 tile 来平滑负载但小 tile 又意味着更多的重复搬运——这个平衡点只能靠实测。4. 一个真实的调优案例从 18% 的硬件利用率到 47%4.1 基线 Profiling先确认瓶颈到底在哪我在一个模拟项目上做过一个典型的三层卷积网络使用一块自行设计的 AI 加速器峰值算力 128 TOPS片上有 2MB SRAM外部带宽标称 1.2TB/s。第一版算子库写完后我们用硬件计数器跑了一遍“卷积层-01”和“卷积层-02”结果非常不理想卷积层-01 的硬件利用率只有 18%卷积层-02 更低才 11%。官方标称 128 TOPS跑起来跟个玩具似的。排查的第一件事就是用 Roofline 算理论边界看看到底是“墙”还是“人祸”。逐层统计 DRAM 读写流量后发现卷积层-01 实际搬运数据量是同形状下理论最小搬运量的 2.8 倍。问题不在硬件而是软件实现把大量中间结果一遍遍写进 DRAM完全没有利用好片上 2MB SRAM。4.2 关键优化动作与实测结果我按“先访存、后计算、再同步”的顺序做了几轮优化每一轮都记录芯片硬件计数器的输出轮次优化动作算子平均利用率备注基线直译式映射无分块18%DRAM 流量巨大第 1 轮引入 tile 切分 片上累积26%中间结果不再反复读写 DRAM第 2 轮双缓冲 描述符依赖链35%计算与 DMA 重叠度提升第 3 轮权重重排Weight Repacking42%消除权重读取的跨步访问第 4 轮算子融合ConvBNReLU47%再降一轮 DRAM 流量每一轮的收益都不像“锦上添花”而是层层递进。做第 1 轮切块时最大的阻力不是代码而是怎么设计片上缓冲区的分配。2MB SRAM 要同时装输入 tile、权重 tile 和输出累加缓冲我花了整整两天调 tile 尺寸最后发现瓶颈竟然是整数倍对齐问题——tile 高度选 5 的时候硬件矩阵单元的边界利用率比选 8 低了三成。第 3 轮的权重重排是我特别想强调的一个细节。原始权重布局是按输出通道顺序连续存储卷积计算时每个输出通道需要读取三行权重跨步很大带宽利用率极低。我们把权重原地重排成“卷积核行优先”的格式算法表现完全没变但 DMA 的跨步访问变成了连续访问带宽效率直接涨了 20%。这类布局优化在文档里往往只写一句“优化了数据布局”但实际效果比很多花哨的高级 API 都来得大。4.3 剩下的 53% 去哪了对高利用率要保持敬畏调完 47%团队里有人问能不能再往上拉一拉我们把硬件计数器全开继续往下抠。结果发现剩下的一半时间里大约有 25% 被片上 SRAM 的 bank 冲突吃掉——不同计算核同时访问同一 bank 的不同地址发生流水线停顿还有 20% 是因为模型里参数量和 tile 尺寸不匹配导致权重反复被换入换出真正算得贼快的部分其实只有一半。所以你现在应该明白47% 的利用率在真实 AI 芯片上已经算很健康了。业界很多号称数百 TOPS 的芯片跑真实模型能到 35% 就算调得很不错。官方标称的峰值 TOPS 可以在最理想条件下测出来但真实网络结构千变万化数据复用机会、权重规模、算子形状都不同利用率很难逼近理论值。对“98% 利用率”这种数字要保持警惕要么是测试条件极特殊要么是统计口径有水分。5. 性能剖析与全链路 Debug先测后调的实操方法5.1 从硬件计数器到运行时 Trace三条信息缺一条都不行我调试 AI 芯片性能问题时依赖三类数据缺一条都容易误判第一类是计算侧数据各计算核心的实际运行周期、空闲周期、停顿周期。这能看出负载均衡和同步等待情况。第二类是访存侧数据DRAM 读写字节数、DMA 命令数量、bank 冲突次数、总线占用率。这能看出带宽和布局问题。第三类是软件侧数据CPU 提交任务的耗时、驱动调用次数、等待同步的时间。这是最容易漏掉的很多“硬件利用率低”的假象其实是 CPU 启动路径太慢。这三类数据要能在时间轴对齐。光看平均数字不够我经常会把一个算子的执行时间画成一条流程瀑布CPU 提交用了多久、DMA 搬运用了多久、计算阵列真实计算用了多久、中间等待了多久。这个瀑布图一出瓶颈立刻现形。有一次我以为算子是访存瓶颈结果是 CPU 写命令队列时加锁太频繁一个算子要交 30 次小命令光锁等待就占了 60% 延迟。5.2 典型定位流程先算边界再看数据最后看微结构我推荐一套可复现的排查流程。第一步永远是用 Roofline 模型算理论边界明确这个算子在当前硬件上到底有没有“可能”达到 80% 以上。如果理论边界就是 50%那后面所有努力都是在 50% 这个上限里求最优别浪费情绪。第二步看 DRAM 流量实际搬运量除以理论最小搬运量这个比值超过 1.5说明布局或切块有问题接近 1.0再去看计算单元的微结构停顿。第三步才是计算/同步细节bank 冲突、写后读依赖、同步栅栏粒度。这套流程看着简单但我在实际项目里见过太多人一上来就盯着汇编级微操作优化忙了大半天最后发现瓶颈在更上层——类似“指令集映射根本不合理”这种问题。逆着水流找源头永远比在漩涡里打转高效。5.3 一个容易踩的坑极简基准测试不能代表真实模型很多人喜欢用一个大的矩阵乘作为基准跑出很高的利用率然后宣布芯片性能优秀。这个测试本身没问题但它严重偏向“高复用、规则形状、大矩阵”的场景。真实模型里大量算子的形状都不是这种规则大块而是千奇百怪1×1 卷积、3×3 深度卷积、动态形状的注意力矩阵。一个加速器的大矩阵乘可能跑得飞快但换到深度卷积时数据复用系数可能只有矩阵乘的五分之一。我自己就会准备一套“形状压缩测试集”里面不放任何标准大模型而是几十个形状各异的算子宏。每次芯片小改版本都先跑这套测试集看哪些算子退化得厉害。这套做法能从设计早期就暴露软硬件接口的不匹配比等模型集成完再全线调试要省一个量级的时间。6. 下一代的软硬件协同方向从能效比到可编程性的再平衡6.1 小芯片、统一内存与近存计算的工程现实很多下一代方案都在做小芯片chiplet设计把计算芯粒、内存芯粒、IO 芯粒分开制造再封装在一起。好处是良率和灵活性但对软硬件协同设计提出了新问题跨芯粒访问的带宽和延迟远高于单芯粒内编译器必须知道哪些数据应该待在哪个芯粒的本地存储里。软件侧的抽象层次会从“内存”上升到“存储域”这对算子库和运行时都是不小的改动。存内计算和近存计算也是常被提起的方向。但说实话我在评估这类架构时第一反应不是看它省了多少搬运功耗而是问软件接口怎么定义如果算力埋在存储阵列里编译器要如何描述矩阵乘法的“原位计算”当前工具链对这类架构的抽象程度还不够成熟什么时候编译器和算子库能像今天控制 SRAM 这样精细控制存算阵列这类芯片才真正到了大爆发的时候。否则就会变成“硬件很美好软件不感冒”的尴尬状态。6.2 稀疏性与结构化让软件不再为一个零值付全价稀疏性是 AI 芯片绕不开的话题。现实模型中大量权重和激活都是零如果硬件对零值也一视同仁地搬运和计算能耗和带宽都白花了。但“稀疏”这件事硬件和软件必须商量好颗粒度。细粒度标量稀疏利用率最高但索引开销巨大压缩格式五花八门结构化稀疏比如 2:4 稀疏——每四个连续值里最多两个非零则让硬件可以用固定模式跳过零值控制逻辑简单很多。我在软硬件协同上倾向于先支持结构化稀疏。原因是编译器好映射权重做一次离线重排把非零值紧凑排列运行时无需动态判断每个元素。激活稀疏更难因为它依赖输入数据需要在计算过程中跳过无效块对硬件调度器的要求高出一截。但无论哪种稀疏最重要的原则是一致的别让软件为一个零值付出完整代价。如果硬件做不到这一点那软件还不如花精力把数据压缩在片上多放几层。6.3 可编程性的再平衡硬件是“工具”不是“立法者”做软硬件协同设计时间长了我有一个越来越强烈的观点硬件团队不应该把自己当成“立法者”规定软件必须怎么做事而应该把自己当成“工具制造者”把高效的原语和安全的默认值提供给软件并保留足够的控制权由软件按需使用。一套好的加速器指令集应该像乐高积木一样由少量可组合的原子操作构成而不是封装一堆“大而全”的专用算子。芯片的专用指令永远只能覆盖当时的模型热点但模型在快速演进——注意力机制、混合专家、多模态大模型每一轮都会冒出新的算子需求。专用硬件如果做得很深但很窄很快就过时做得宽而薄把通用计算、高效访存和灵活数据流组合好反而能在好几年里保持竞争力。我个人的体会是软硬件协同设计的“协同”二字最终落在两个团队的沟通质量和抽象层次的选择上。硬件要理解软件的痛点是数据流软件要理解硬件的边界是物理资源。一个团队如果能坐在一起把“算子→数据流→存储→同步”这条映射链路捋通比各自闷头定义一套新架构要有效得多。最后再分享一个实操经验拿到一块新芯片别急着点灯、跑 Hello World、跑模型先写一个微基准把片上 SRAM 的各类切块边界和 DMA 带宽响应测清楚。边界数据就像地图上的等高线有了它后面无论做算子库还是编译器都不至于在设计空间里瞎撞。踩过几次坑之后我才真正理解那句话——AI 芯片的性能不是设计出来的是软硬件一起“榨”出来的。