首页 / 资讯中心 / 文章详情

AI芯片软硬件协同设计:从指令集到性能建模的工程实践

AI芯片软硬件协同设计:从指令集到性能建模的工程实践 ★ FEATURED ARTICLE
1. 从能跑就行到跑得高效AI芯片软硬件协同设计的核心命题做AI芯片这行的人都有一个共识一颗芯片流片回来真正决定它能不能用的不是峰值算力标称了多少TFLOPS而是软件栈能不能把硬件里那些MAC阵列、SRAM带宽、NoC通路给喂饱。我见过太多团队在架构仿真阶段数据漂亮得不行结果板子一上电实际推理吞吐只有理论值的30%不到。问题出在哪几乎无一例外都是软硬件设计的割裂。这个系列写到第四篇前几篇分别聊了架构选型、数据流设计、存算一体的取舍这次我想把焦点收窄到一个更具体、也更要命的话题上当硬件微架构已经定型软件侧到底该怎么做才能把利用率从能跑拉到跑得高效。这不是一个纯软件优化问题也不是一个纯硬件问题而是一个需要在设计早期就同步考虑的协同问题。如果你正在做AI加速器的指令集设计、编译器后端开发、算子库调优或者你是一个SoC架构师需要评估不同数据流方案对软件复杂度的影响那这篇内容应该能给你一些可以直接参考的思路。我会尽量把每个决策背后的为什么讲清楚而不是只丢一堆结论。2. 指令集设计别让编译器替你擦屁股2.1 为什么大多数AI芯片的ISA设计是失败的先说一个我观察到的现象很多AI芯片团队在设计指令集时习惯性地把硬件模块的接口直接暴露成指令。比如矩阵乘法单元有load A、load B、compute、store C四个接口那ISA里就设计四条指令。看起来一一对应很清晰但编译器后端工程师拿到这套ISA的时候会非常痛苦。原因很简单硬件模块的接口是微架构视角而编译器需要的是编程视角。编译器在做指令调度时需要知道哪些指令可以并行发射、哪些存在数据依赖、哪些可以重排。如果ISA只是把硬件端口翻译了一遍那编译器就必须自己去推导这些信息而它往往推导不出来。我参与过一个项目早期的ISA设计就是典型的端口映射风格。结果编译器团队花了三个月做指令调度优化最后发现瓶颈根本不在调度算法上而是ISA本身没有提供足够的语义信息让编译器做决策。后来我们重新设计了一套带有明确并行语义的指令格式编译器的调度效率直接提升了40%以上。2.2 一条好指令应该携带哪些信息从编译器友好的角度出发一条AI加速器指令至少应该携带以下几类信息计算语义这条指令做什么运算输入输出数据类型是什么累加精度如何并行约束这条指令能否与其他指令并行需要哪些资源槽位数据局部性提示操作数来自哪个存储层级是否需要预取同步语义是否需要等待某个屏障是否会产生副作用这些信息不需要全部编码在指令的二进制格式里但必须在ISA文档中有明确定义并且编译器能够通过某种方式获取。常见做法是在指令编码中留出几位作为调度提示位或者通过独立的元数据表来描述。注意调度提示位不要设计得太复杂。我见过一个设计用了8位来编码各种并行约束结果编译器后端为了填这8位写了一千多行逻辑维护成本极高。通常3到4位就足够表达大多数场景。2.3 定长指令 vs 变长指令的取舍这个问题在AI芯片领域争论了很久。定长指令的好处是译码简单、流水线容易设计、编译器对齐方便变长指令的好处是编码密度高、可以灵活扩展。我的经验是对于以矩阵运算为主的AI加速器定长指令几乎是唯一合理的选择。原因在于AI负载的指令流通常比较规整变长编码带来的密度优势有限但译码复杂度会显著增加流水线前端的面积和功耗。而且定长指令让编译器的指令打包bundling逻辑简单很多可以更容易地实现VLIW风格的静态调度。当然如果你的加速器需要支持非常多样化的算子类型比如同时要跑卷积、Transformer、图神经网络那可能需要在定长指令的基础上设计多套指令格式通过opcode的高位来区分。这种情况下译码逻辑会复杂一些但比纯变长指令还是好控制得多。3. 数据搬运的软件可见性什么时候该让编译器管什么时候不该3.1 显式搬运与隐式搬运的边界AI芯片设计中一个经典的争论是数据在存储层级之间的搬运应该由硬件自动完成比如cache还是由软件显式控制比如DMA指令这个问题的答案取决于你的目标负载特征。如果负载的数据访问模式非常不规则比如稀疏矩阵运算或者图计算那硬件cache的自动管理会更合适因为软件很难预测访问模式。但如果负载是规整的稠密矩阵运算那显式搬运几乎总是更优的选择。我做过一个对比实验同一个卷积算子在显式DMA和硬件cache两种方案下显式DMA的端到端延迟比cache方案低了35%功耗低了28%。原因在于cache的tag查找、替换策略、一致性维护都会带来额外开销而卷积的数据复用模式是完全可以静态分析的编译器可以在编译期就生成最优的搬运序列。但这里有一个前提编译器必须能够获取足够的信息来做这个分析。如果算子是以黑盒库的形式提供给编译器的那编译器就没有办法做跨算子的搬运优化。所以如果你选择了显式搬运的方案就必须同时提供一套让编译器能够看透算子内部数据流的机制比如通过中间表示IR暴露数据依赖关系。3.2 双缓冲与多缓冲的软件实现显式搬运方案中双缓冲是最基本的优化手段。原理很简单在计算当前数据块的同时预取下一块数据。这样计算单元和搬运单元可以并行工作隐藏搬运延迟。但双缓冲的实现细节有很多坑。首先是缓冲区大小的选择太小了隐藏不了延迟太大了浪费SRAM。我的经验公式是缓冲区大小 搬运延迟 × 计算吞吐 / 数据复用率这个公式看起来简单但实际中搬运延迟和计算吞吐都是变量取决于具体的硬件配置和负载特征。所以更实际的做法是在编译器中做一个自动的buffer sizing pass根据算子的计算图和目标硬件的参数自动计算最优的缓冲区大小。多缓冲三重或更多在大多数AI负载中收益递减很快。从单缓冲到双缓冲通常能带来30%到50%的提升但从双缓冲到三缓冲往往只有5%到10%。除非你的搬运延迟特别大比如涉及外部存储访问否则双缓冲就够了。3.3 搬运指令的粒度设计搬运指令的粒度是一个容易被忽视但影响很大的设计决策。粒度太细指令数量爆炸译码和调度开销大粒度太粗灵活性差容易出现搬运了但用不上的浪费。我倾向于推荐以tile为单位的搬运粒度也就是一次搬运一个计算单元能够处理的数据块。这样搬运和计算可以一一对应编译器的调度逻辑最清晰。但tile的大小应该是可配置的让编译器根据具体算子来调整。另外搬运指令最好支持stride和gather/scatter模式。虽然这些模式在稠密矩阵运算中用得不多但在处理边界条件、padding、或者特殊数据布局时会非常有用。如果硬件不支持编译器就只能退化成逐元素搬运性能会急剧下降。4. 算子库与编译器的分工谁该做什么4.1 手写算子库的不可替代性现在有一种流行的观点是编译器会搞定一切只要前端框架对接好后端编译器自动生成最优代码就不需要手写算子库了。这个观点在CPU和GPU领域有一定道理但在AI加速器领域我认为至少在未来几年内手写算子库仍然是不可替代的。原因在于AI加速器的微架构差异太大了。不同芯片的MAC阵列形状、SRAM bank划分、NoC拓扑都不一样编译器很难为每一种微架构都生成最优代码。而手写算子库可以针对特定硬件做深度优化比如手动安排数据在bank之间的分布来避免冲突手动插入同步指令来最小化流水线气泡。我参与过的一个项目编译器自动生成的卷积算子只能达到手写算子库60%的性能。后来我们分析发现差距主要来自两个方面一是编译器没有做好SRAM bank冲突避免二是编译器的指令调度没有充分考虑到硬件的双发射能力。这两个问题在编译器框架内解决起来都很复杂但手写算子库的工程师可以很自然地处理。4.2 编译器该聚焦在哪些优化上既然手写算子库承担了最底层的性能优化那编译器应该聚焦在哪些方面我的看法是算子融合把多个小算子合并成一个大算子减少中间结果的搬运开销。这是编译器最擅长的事情因为编译器可以看到整个计算图。内存分配自动管理片上存储的分配和回收最大化内存复用。手写算子库通常只关注单个算子内部的内存使用跨算子的内存复用需要编译器来做。布局转换自动插入必要的layout转换让相邻算子之间的数据格式匹配。手写算子库通常只支持固定的输入输出布局灵活性差。动态形状处理处理运行时才知道的张量形状生成自适应的执行计划。这是手写算子库很难做到的。所以一个合理的分工是编译器负责图级别的优化和调度算子库负责节点级别的极致性能。两者通过一个定义良好的接口来协作编译器在需要的时候调用算子库算子库在执行时向编译器反馈实际的资源使用情况。4.3 接口设计中的常见陷阱编译器和算子库之间的接口设计有很多细节需要注意。我踩过的一个坑是早期我们定义的接口只传递张量的形状和数据类型没有传递内存布局信息。结果编译器以为算子库会输出NCHW格式但算子库实际输出的是NHWC格式导致后续算子读到了错误的数据。后来我们在接口中增加了layout字段并且规定所有算子库必须显式声明输入输出的layout。这个改动看起来很小但避免了大量隐蔽的bug。另一个坑是错误处理。算子库在执行过程中可能会遇到各种异常情况比如内存不足、数据越界等。如果接口没有定义清晰的错误码和恢复机制编译器就无法做出正确的决策。我们的做法是在接口中定义了一套标准的错误码并且要求算子库在出错时返回尽可能多的上下文信息让编译器可以决定是重试、降级还是报错。5. 性能建模在流片之前就知道软件能跑多快5.1 为什么需要性能模型AI芯片的流片成本极高如果流片回来才发现软件跑不出性能那损失是巨大的。所以在设计阶段就需要一个性能模型能够预测给定硬件配置下软件能达到什么样的性能。性能模型不需要100%准确但需要能够捕捉到主要的性能瓶颈。我见过一些团队的模型过于简化只计算理论峰值算力结果完全无法反映实际性能。也见过一些团队的模型过于复杂试图模拟每一个时钟周期的行为结果模型本身跑得比实际芯片还慢。我的建议是性能模型的粒度应该和设计决策的粒度匹配。如果你在做架构探索需要比较不同MAC阵列形状的影响那模型可以粗一些只需要捕捉计算和搬运的overlap情况。如果你在做编译器优化需要评估不同调度策略的效果那模型就需要细一些要能够反映指令级的并行和依赖。5.2 一个实用的性能模型框架我常用的性能模型框架包含以下几个层次层次建模对象精度要求典型用途算子级单个算子的执行时间误差20%算子库优化图级整个计算图的端到端时间误差30%编译器调度评估系统级包含主机交互的完整流程误差50%系统方案选型算子级模型通常基于roofline模型扩展考虑计算吞吐、内存带宽、以及两者的overlap程度。图级模型在算子级模型的基础上加上算子间的依赖关系和调度开销。系统级模型则进一步考虑主机与加速器之间的通信开销、驱动层的延迟等。这个框架的关键在于每一层模型都可以独立验证和校准。算子级模型可以用微基准测试来校准图级模型可以用真实网络的profiling数据来校准系统级模型可以用端到端应用来校准。这样即使某一层模型有偏差也不会影响其他层的可信度。5.3 模型校准的实操经验模型校准是一个需要耐心的工作。我的经验是先校准计算吞吐用纯计算密集的微基准比如大矩阵乘来测量实际的计算吞吐和理论峰值对比得到计算效率系数。再校准内存带宽用纯访存密集的微基准比如大数组拷贝来测量实际的内存带宽得到带宽效率系数。然后校准overlap用计算和访存混合的微基准测量两者重叠执行时的实际性能得到overlap效率系数。最后校准调度开销用包含多个小算子的计算图测量算子间的调度开销。这四个系数确定之后性能模型的预测精度通常能达到70%到80%。对于架构探索和编译器优化来说这个精度已经足够做出正确的决策了。提示校准数据要定期更新。硬件配置、编译器版本、驱动版本的变化都可能影响这些系数。我通常会在每次重大版本更新后重新跑一遍校准流程。6. 软硬件协同验证别等到流片回来才发现问题6.1 验证左移的必要性AI芯片的验证通常分为几个阶段RTL仿真、FPGA原型验证、流片后硅验证。传统的做法是软件团队在RTL仿真阶段只做简单的功能验证等到FPGA阶段才开始做性能调优。但这种做法有一个很大的问题如果硬件微架构有缺陷等到FPGA阶段才发现修改成本已经很高了。我主张把软件性能验证尽可能左移。在RTL仿真阶段虽然仿真速度慢但可以用小规模的负载来验证关键路径的性能。比如用一个64x64的矩阵乘来验证MAC阵列的利用率和数据搬运的效率。如果在这个规模下性能就不达标那大规模下只会更差。6.2 仿真加速的实用技巧RTL仿真的速度是最大的瓶颈。一个中等规模的AI负载在RTL仿真器上可能需要跑几个小时甚至几天。为了提高效率我通常采用以下策略负载裁剪只保留计算图的关键路径去掉分支和不影响性能的部分。数据简化用随机数据代替真实数据减少数据准备的时间。并行仿真把不同的算子分配到不同的仿真实例上并行跑最后合并结果。混合仿真对性能关键的模块用RTL仿真对其他模块用行为级模型代替。这些技巧可以把仿真时间从几天缩短到几个小时让软件团队能够在设计早期就获得性能反馈。6.3 FPGA原型上的性能调优FPGA原型是软硬件协同验证的关键环节。虽然FPGA的运行频率通常远低于最终芯片但它的优势在于可以跑真实的软件栈包括驱动程序、运行时库、编译器生成的全部代码。在FPGA原型上做性能调优时需要注意几点频率缩放FPGA的频率和芯片频率不同需要做频率缩放来估算芯片上的性能。但缩放不是简单的线性关系因为有些瓶颈比如内存带宽在FPGA上可能不是瓶颈但在芯片上会是。资源限制FPGA的资源通常比芯片少可能需要裁剪硬件配置。裁剪后的性能数据需要谨慎外推。时序收敛FPGA的时序收敛问题可能导致某些路径无法达到目标频率这会影响性能测量的准确性。尽管有这些限制FPGA原型仍然是流片前最可靠的性能验证手段。我的经验是FPGA上的性能数据经过合理缩放后和最终芯片的实测性能通常能控制在20%以内的偏差。7. 写在最后一些个人体会做AI芯片的软硬件协同设计最深的体会就是平衡两个字。硬件团队希望微架构越简单越好软件团队希望硬件越灵活越好而最终的产品需要在性能、功耗、面积、开发成本之间找到一个平衡点。我自己的经验是在项目早期软件团队应该深度参与硬件架构的定义。不是说要软件工程师去设计电路而是说软件团队需要把编译器的需求、算子库的需求、运行时系统的需求清晰地传达给硬件团队。很多硬件设计决策在电路层面看起来无关紧要但对软件的性能影响巨大。另一个体会是不要追求一步到位。AI芯片的软硬件协同是一个迭代的过程第一版可能只能达到理论性能的50%但通过持续的优化和调优逐步提升到70%、80%甚至更高。关键是要建立一个能够快速迭代的验证和调优流程让每一次修改都能快速看到效果。最后分享一个小技巧在定义硬件寄存器接口时尽量留一些预留位和预留寄存器。这些预留资源在后期可能会派上大用场比如用于性能计数器、调试信息输出、或者运行时配置。我见过太多项目因为寄存器空间不够不得不重新流片代价太大了。
阅读完成 · 觉得有帮助?
咨询建站