乱序执行在NPU/GPGPU中的价值与工程选择:从浅度调度到动态并行

发布时间:2026/9/6 1:32:38
乱序执行在NPU/GPGPU中的价值与工程选择:从浅度调度到动态并行 1. 为什么传统NPU/GPGPU刻意避开乱序执行做体系结构的人对乱序执行Out-of-Order Execution不会陌生它在CPU领域已经被研究了几十年从Tomasulo算法到寄存器重命名再到重排序缓冲ROB这套机制几乎统治了所有高性能处理器内核。但有意思的是当行业把视线转向NPU和GPGPU时乱序执行突然变成了一个少有人碰的领域大多数商用加速器都采用的是非常规矩的顺序发射In-Order Issue方案顶多加上一些多发射和流水线调度优化。这背后的原因并不复杂传统上大家默认三件事第一加速器的任务模型足够规则卷积、矩阵乘、逐元素运算这些负载执行节奏几乎是完全可预测的编译器只要做好静态调度就能获得很高的流水线利用率第二乱序执行的核心收益在于挖掘指令级并行ILP来掩盖延迟但NPU/GPGPU里大量的并行度来自数据级并行DLP和线程级并行TLP不需要靠复杂的硬件调度来榨取性能第三乱序执行需要消耗大量的面积和功耗寄存器堆端口、比较器网络、唤醒逻辑、选择逻辑这些东西在CPU里已经烧掉了不少晶体管放到以能效比为生命线的加速器里性价比很难看。这个判断在很长一段时间里都是成立的但最近几年情况开始起变化。一方面NPU的主网格阵列Main Grid Array规模在快速膨胀MAC阵列越来越大要求的输入数据供给速度和指令供给速度都远超从前。如果执行流水线在等待数据依赖时僵在原地即使阵列的计算能力再强实际的有效利用率FLOPs Utilization也上不去。另一方面负载开始变得不规整稀疏卷积、动态形状的Transformer解码、推荐系统的Embedding查询这些任务里指令之间的依赖关系不再那么规整静态调度的天花板变得肉眼可见。所以乱序执行该不该进入NPU/GPGPU这个问题已经从理论探讨变成了一个现实的工程选择题。要想把这个选择题做对先把乱序执行在加速器里到底能解决什么、代价是什么、边界在哪里研究清楚比直接拍脑袋上乱序方案要靠谱得多。需要先定一个基础认知NPU和GPGPU虽然都被叫做加速器但它们的执行模型差异其实非常大。GPGPU走的是SIMT单指令多线程路线成百上千个线程在一个指令流下运行乱序调度的对象是线程束Warp里的指令NPU则更偏向专用数据流架构指令编排的焦点是数据在MAC阵列里的流动方式。同一个乱序概念落到这两种架构里的形态完全不同不能一概而论。后面我会分别展开。2. 重新评估乱序执行的价值当并行度遇到结构瓶颈2.1 从FLOPs利用率说起判断一个加速器设计得好不好最直观的指标就是FLOPs利用率——也就是实际算出来的运算量除以硬件理论峰值运算量。对NPU来说理论峰值通常是拿MAC阵列的MAC数乘以主频再乘以2来算的。很多芯片手册上标的TOPS数据非常好看但真实应用跑到90%以上利用率的情况并不多见能在70%以上已经算优秀设计。利用率损耗主要出现在三个环节取指和译码阶段指令带宽不足导致执行单元饿肚子。数据供给阶段片上SRAM带宽不够或者AHB/AXI总线等待周期过长MAC阵列在空转。依赖等待阶段前一条指令的结果还没写回后续指令必须插入气泡。前两个环节可以通过加大指令Cache、增加SRAM端口数量、做多级缓存来缓解属于带宽类问题。第三个环节则属于延迟类问题是乱序执行最擅长解决的部分。举一个非常简单的例子。假设一个NPU的MAC阵列需要连续执行两条指令A指令把某个寄存器里的数据搬进片上缓存B指令要用缓存里的数据做矩阵乘。顺序执行时B只能等A彻底完成数据写入后才能发射如果数据加载延迟是10个周期那么这10个周期里MAC阵列只能干等。如果换成乱序执行在等待A完成期间调度器可以把后续几条与A没有依赖关系的指令比如另一路独立的卷积计算先发射出去让MAC阵列全程保持运转。在传统CPU里这个收益通常只有百分之十几到二十因为CPU的指令窗口本就有限。但对拥有庞大执行阵列的NPU来说哪怕只减少10%的空转周期换算成绝对算力收益也非常可观。尤其当MAC阵列规模达到数百TOPS级别时每1%的利用率提升对应的是真金白银的吞吐增量。2.2 并行度供给的边际递减效应有一种观点认为NPU的并行度足够高根本不需要乱序。这个说法只在负载极度规整、编译器能完美静态编排指令的前提下才成立。我们可以做一个推演。假设一个NPU有64个MAC单元每个周期可以执行64次乘加。为了让阵列满负荷运转每个周期都需要有64个可执行的操作数准备好。如果指令之间的依赖链很长或者访存延迟很高编译器再努力也没法让每个周期都凑满64个有效操作——因为指令窗口中可用的独立指令数不足。这时候硬件乱序调度器就变成了一个并行度缓冲池把不同时间窗口里的可并行指令动态地凑到一起发射。从利用率曲线来看当阵列规模小的时候静态调度已经能挖出大部分并行度乱序带来的增量收益不明显。但当阵列规模变大、每周期需要喂入的操作数变多时静态调度能挖出的并行度逐渐逼近极限乱序的动态挖掘能力就能把曲线往外推出一截。这个拐点在哪里每家芯片的具体情况不同但趋势是明确的——阵列越庞大越需要动态调度机制来匹配指令供给与执行能力。2.3 稀疏计算打破规则负载假设再看另一类推动因素稀疏计算。稀疏卷积、稀疏Transformer、剪枝后的模型推理这些负载的最大特点就是计算模式高度不规则。非零元素的位置不同导致每个处理单元PE的实际工作量和数据依赖关系完全不同。在做权重剪枝后同一个卷积层里有的输出通道需要计算10个输入通道有的只需要计算3个静态编译根本没法精确预知运行时的稀疏分布。在这个场景下乱序的核心价值变成了负载均衡。各个PE执行完当前任务的时间点完全错开如果执行流水线能动态调度那些已就绪的微指令让空闲的PE能及时领到新任务整体效率就能提升。反之如果所有PE都共用一条顺序指令流任何一个PE的拖延都可能拖慢全局节奏效率会很难看。从目前能看到的公开资料来看确实已经有部分自研NPU架构在设计阶段加入了浅度的乱序调度窗口比如发射4到8条指令的动态调度目的就是为了吸收不规则负载下的执行节奏抖动。这类设计不会像CPU那样做一个几百项的ROB而是用很浅的窗口换取大部分收益是一种非常务实的折中。3. 乱序执行在NPU中的实现形态从浅度调度到显式数据流3.1 浅度乱序指令窗口加动态发射NPU上最先落地的乱序形态通常是在顺序取指、顺序译码的基础上增加一个小规模的指令发射队列Issue Queue然后做动态发射。设计思路类似CPU里的顺序提交、乱序执行模型但规模小得多。基本流程如下指令仍由前端顺序取指和译码生成内部微操作uop。微操作被送入一个发射队列队列中的每一项记录它需要的源操作数是否已就绪。每周期检查队列中所有微操作的就绪状态挑出其中优先级最高的若干条发射到执行单元。执行完成后结果写回寄存器堆或直接送到下一级执行单元的旁路网络。提交环节可以保持顺序即指令完成执行后仍按原来的程序顺序更新架构状态比如写回寄存器、更新PC这样异常处理和调试都比较简单。这个方案的好处是不需要做完全的寄存器重命名可以在指令编码里显式规定哪些操作数需要旁路大大降低硬件复杂度。指令发射队列的条目数Entry Count是关键设计参数。太浅比如只有2到4项有效调度范围太小收益微弱太深比如64项以上唤醒逻辑和仲裁逻辑的面积功耗急剧上升ROI迅速变差。从实践角度8到16项是一个比较合理的设计区间既能覆盖常见的访存延迟和依赖间隔又不会让硬件规模失控。3.2 寄存器重命名在NPU里的改造CPU乱序执行的核心组件是寄存器重命名通过把架构寄存器映射到物理寄存器来消除WAR写后读和WAW写后写伪相关。NPU里需不需要答案是需要但形态不同。NPU的寄存器堆通常不是一组通用整数寄存器而是由标量寄存器控制参数、向量寄存器数据块和累加器构成的多层次结构。尤其累加器宽度可能达到32位甚至更高数据位宽大复制成本高直接做完整物理寄存器堆成本巨大。比较务实的一种做法是只对控制类标量寄存器做重命名数据类寄存器不做重命名通过在指令译码阶段做数据依赖检查禁止有反相关WAR/WAW的指令同时存在于发射窗口内。这样一来大部分伪相关问题被直接规避掉数据寄存器复制成本也省下来了。代价是发射窗口里部分指令对可能因为伪相关而不能同时驻留损失一部分潜在并行度。实际做下来这个改造的收益/成本比是非常划算的。因为NPU里的数据块寄存器通常占的程序执行时间比例很高但出现的频率不高通常一次矩阵乘的输入数据会连续在寄存器里停留很长时间重命名的需求并不像CPU那么频繁。3.3 提交逻辑与精确异常让步给的面积预算在CPU里精确异常是乱序执行的铁律每一次中断、异常、上下文切换都必须保证架构状态一致。但在NPU场景里这个需求可以大幅度放松。NPU上跑的负载通常在启动前就已经把输入数据和参数全部准备好了运行过程中极少发生需要精确回滚的事件。常见的异常类型包括访存越界、MAC阵列溢出保护、看门狗超时这些在绝大多数情况下更倾向于直接报错并整块重跑而不是回滚到某一精确指令点。因此NPU乱序执行的实现可以省略ROB直接采用发射后不管的策略。译码阶段如果检查到某条指令可能产生异常就保守地把后续指令的发射暂停等异常结果返回后统一处理。这个策略能省下ROB条目和配套的提交逻辑面积和功耗节约非常可观。省掉ROB带来的约束也很明显不能支持指令的投机执行speculative execution也就是不能跳过尚未确认的分支路径去执行后面的指令。但对NPU负载来说分支密度很低绝大多数代码是顺序执行的循环体展开投机收益聊胜于无舍弃它是合理的。4. 乱序GPGPU从Warp调度到独立线程调度4.1 SIMT模型下的乱序窗口是Warp还是线程GPGPU的乱序执行和NPU走的是完全不同的技术路线。经典NVIDIA GPU是SIMT架构以Warp线程束通常32个线程为单位取指、发射和执行。传统设计中一个Warp拥有独立的程序计数器所有线程在同一时刻执行同一条指令调度器只是按某种策略在多个Warp之间切换掩盖同一Warp内部的长延迟操作这叫线程级并行掩盖延迟并不需要乱序。从Volta架构开始NVIDIA做了一次重要调整把调度粒度从Warp细化到独立的线程。每个线程拥有自己的程序计数器和堆栈允许同一Warp里的不同线程各自执行不同指令甚至出现一个线程阻塞、其他线程继续推进的情况。这个设计在公开资料里叫做独立线程调度Independent Thread Scheduling。它本质上就是一种在线程粒度上的动态调度机制——在一定程度上打破了Warp内所有线程必须齐步走的束缚。从学术角度归类这条路线比NPU上的浅度乱序走得更远因为它实现了真正的指令级乱序效果不同线程的指令可以在不同时间点发射到执行单元。而传统的Warp调度器本质上是一个多线程顺序调度器谈不上乱序。4.2 协处理器上的动态并行GPU的程序生成程序GPGPU领域另一种和乱序思想关系密切的技术是动态并行Dynamic Parallelism。传统GPU内核由CPU侧的主线程发起GPU上所有并行任务的划分在启动前就已经由软件决定。而动态并行允许GPU内核里的代码动态启动子内核子内核的网格尺寸、共享内存大小等参数在运行时才能确定。这套机制需要GPU硬件具备程序生成程序的执行能力运行中的线程组可以创建新的任务并提交到任务调度器调度器再将新任务分发到空闲的SM流多处理器。从乱序执行的视角看这实际上是一种任务级乱序——任务不再是按照某个预设的静态顺序执行完毕而是由运行时的数据依赖驱动动态排队、动态发射。动态并行对硬件的挑战主要有两点一是任务调度器需要处理并发提交的子任务队列保证优先级和资源分配的合理性二是需要额外的内存管理机制来保存子内核的参数和上下文。很多早期的实现都出现过死锁问题触发条件通常是子任务占用了过多共享内存或寄存器资源导致父任务无法继续推进。这套机制的复杂度在GPGPU领域也算得上高投资高回报的典型代表了。4.3 调度器结构GPU乱序的硬件基石无论NPU还是GPGPU调度器都是乱序执行的核心硬件。GPGPU里的调度器设计可以给NPU不少参考。GPU SM内部的调度器每一级由以下组件构成活动Warp表Active Warp Table记录所有可参与调度的Warp以及它们的状态就绪、等待、阻塞等。优先级选择器Priority Selector每周期从就绪Warp中选择若干条指令发射。操作数收集器Operand Collector发射前把指令需要的源操作数从寄存器堆或旁路网络收集齐。执行流水线接口把带操作数的指令送入对应的执行单元。在乱序GPGPU中活动Warp表里还可以为每条指令记录它的源/目的寄存器依赖关系发射时进行依赖检查。这就从Warp间多线程调度进化成了Warp内指令级乱序调度硬件复杂度呈指数级上升但带来的灵活性也比浅度乱序高得多——尤其对不规则负载来说这个灵活性往往意味着数倍的利用率差距。5. 主网格阵列与MAC阵列的调度联动乱序的价值落点5.1 MAC阵列到底在等什么理解了指令侧的执行机制再来看数据侧的主网格阵列。NPU性能瓶颈经常出现在MAC阵列本身——大量的乘加单元密密麻麻排列在一起每个周期需要高吞吐的数据供应。MAC阵列的利用率直接取决于它能拿到的操作数。为了把MAC阵列喂饱NPU通常会安排一个或多个数据搬运引擎DMA把数据从DRAM搬到片上SRAM再从SRAM高效搬到MAC阵列。这个数据流动链路上的任何一个环节出现等待MAC阵列都会立刻空转。传统NPU里DMA引擎和MAC阵列控制器之间是松耦合的一般靠软件编译器或驱动预先编排时序来同步。但一旦负载中存在数据依赖很长的链比如递归神经网络的时间步、注意力机制里的softmax前后依赖软件编排就变得很难精确。这时如果MAC阵列控制器和DMA引擎之间存在一个乱序调度窗口DMA搬数、MAC计算、结果写回就能形成一个松散的动态流水线数据到位了就立刻算算完了立刻搬走而不是死板地等待编译器预设的时间点。这就是主网格阵列Main Grid Array与MAC阵列之间调度联动的意义所在。5.2 网格级乱序让PE不会空转再往下深一层看主网格阵列内部。主网格阵列通常由若干处理单元PE排列成二维网格每个PE内嵌若干MAC单元。网格阵列执行一个卷积层时会把输入特征图和权重切片分发到各个PEPE之间进行部分和传递如脉动阵列的数据流方式。当负载变得不规则比如稀疏卷积某些PE处理的区域内非零权重很少很快就完成了任务并进入空闲状态。而另一些PE因为承载了密集计算还在忙碌中。如果没有动态调度机制网格的同步屏障会强制所有PE等待最慢的那个完成空闲PE的算力就白白浪费了。引入网格级调度窗口后已经完成任务的PE可以从调度队列里领取下一块计算任务而不用等待整个网格完成屏障。这个机制可以理解为任务级乱序——不同PE上执行的任务顺序可以不同尽管程序逻辑上它们是顺序的。这种做法在FPGA上的软核处理器集群里很常见在高性能NPU里也逐渐成为热点设计方向。5.3 从数据流到指令调度设计哲学的转变把MAC阵列的数据流视角和指令调度的乱序视角放在一起能看到一个设计哲学的分岔点。传统NPU的核心设计哲学是数据流驱动计算既然数据移动的路径是清晰可预测的就把计算完全编排在数据流的骨架上。这种方式高效但刚性极强——一旦数据的到达时间和预设不一致cache miss、DMA拥塞、稀疏矩阵整个执行流水线就会打嗝。乱序执行的设计哲学则是指令流驱动计算硬件调度器根据当前数据就绪情况动态安排指令发射顺序让执行资源始终处于忙碌状态。这种方式更灵活但需要付出硬件调度逻辑的代价。对于新一代的NPU一个可能的中间路线是保留数据流的基本架构框架因为MAC阵列的数据流动习惯很难改变但在指令级和任务级加入一定深度的乱序调度缓冲让数据来不及到达时执行资源能转而执行其他已就绪任务。这个思路可以概括为数据流为主干乱序为软组织是目前工程实践中可行性最高的一种做法。从实际测试数字来看这种折中方案在稀疏模型上的利用率提升通常能达到15%到30%具体取决于稀疏度和规则程度。而在规则稠密负载上反而可能因为调度逻辑本身的开销造成2%到3%的轻微下降。所以做设计时一定要先想清楚目标负载偏向哪边。6. 乱序GPGPU的前沿动态与工程实践参考6.1 Volta以来的独立线程调度一个完整案例NVIDIA从Volta架构开始引入的独立线程调度是乱序概念在GPGPU领域最典型的落地案例。它允许Warp内各个线程独立推进各自维护程序计数器和调用栈。这意味着线程A在执行高延迟的全局内存加载时线程B可以继续执行后续算术指令。线程A和线程B之间的隐式同步屏障如Warp Vote、__syncwarp由硬件统一管理。分支发散branch divergence时的串行化执行可以更加灵活不需要所有线程都聚齐后再继续。这套机制对某些负载的增益非常显著。以图神经网络GNN的采样和聚合为例不同节点邻居数量差异很大导致各线程的内存访问延迟差异很大。独立线程调度能有效隐藏这些不均衡的延迟几乎可以视作一种轻量级的乱序执行。对比早先的Warp齐步走模型这类不规则负载的性能提升可以达到2到3倍。6.2 并发执行模型任务级乱序的现代例子另一个值得关注的方向是Turing架构引入的并发执行模型Concurrent Execution Model而后在Hopper架构中得到发展的线程块集群Thread Block Cluster机制。线程块集群允许程序员把一个网格里的线程块显式聚合成集群集群内的线程块可以直接通过分布式共享内存进行通信而不必经过全局内存。从调度的角度看线程块集群的粒度介于线程与网格之间相当于硬件在网格任务之间增加了动态调度维度——同一网格中的不同线程块集群可以在同一SM上并发执行也可以分布在不同的SM上调度器根据空闲资源动态放置。这种能力让GPU在运行多任务混合负载一个内核在做计算、另一个内核在做内存拷贝、还有第三组任务在做稀疏算子时内部资源的利用变得更加动态和高效。它虽然没有直接跳乱序执行的名号但在调度顺序可以被执行环境动态改变这个核心点上和乱序思想的本质是一致的。6.3 学术界对乱序GPU的研究结论学术领域的不少工作也在持续探索乱序/动态调度思想在GPGPU上的价值。一些研究组用模拟器对比过大型寄存器堆充分乱序发射的GPU核心与经典SIMT核心的性能差异结论大致如下在规则的高计算密度负载下乱序GPU相对经典SIMT核心的提升通常不超过10%但核心面积和功耗可能增加30%以上。在访存密集或不规则负载下乱序核心的优势会显著放大部分负载能获得30%到50%的端到端加速。混合设计如浅度乱序窗口Warp级并行掩盖能在较小硬件代价下获得中等收益是最具性价比的设计点。这些结论与NPU领域当前的实际设计倾向是一致——全乱序的收益边界通常不划算但适度乱序的调和策略正在成为兼顾效率和灵活性的新趋势。7. 工程落地中的权衡清单与实践建议7.1 什么样的NPU/GPGPU值得做乱序先明确一点不是所有加速器都需要乱序。做不做乱序取决于负载画像和硬件资源。这里有一个简单的自查框架目标负载的访存延迟是否高CPU侧和片上SRAM的往返延迟如果超过15到20个周期静态流水线就需要大量软件预取来掩盖乱序能替代这部分软件工作。指令之间的依赖链是否长如果运行的主要是层内计算密集算子卷积、矩阵乘依赖链较短静态调度基本够用如果涉及层间依赖、递归结构依赖链长乱序价值明显。阵列规模是否足够大当MAC阵列超过一定规模比如每周期需要喂入数百个操作数单靠软件静态编排已经很难保证满负荷动态调度几乎是必选项。负载规律的确定性如何如果运行的都是形状完全确定的模型编译器可以提前算出每一步的时序乱序带来的额外收益很低。面对动态shape、稀疏计算、多任务混跑乱序才放得开手脚。7.2 层次化乱序分而治之的工程策略与其在硬件上做一个大而全的乱序核心不如把乱序能力拆到不同层级各自应对各自的瓶颈。这是我个人最推崇的一种工程路径。指令级浅度乱序在标量控制处理器里加8到16项发射队列处理常规的依赖等待和分支延迟。任务级动态调度在网格执行单元和DMA引擎之间增加任务队列允许数据到达顺序与任务发射顺序不同吸收访存波动。MAC阵列级弹性执行为每个PE或PE组引入小型就绪队列已完成当前任务的PE直接从队列取下一个任务不必等待全局屏障。内核级并发调度在主机驱动和运行时里实现多内核动态并发让不同内核在同一芯片上穿插执行充分利用空闲资源。这四个层次可以独立实施也可以叠加组合。做第一层只需要比较小的改动投入产出比最高做第四层的负担相对较低基本是软件层面但需要对运行时系统做较大重构。7.3 要警惕的坑工程实践中有几个坑值得提前注意。第一个坑是把CPU乱序的成熟组件笨重地直接照搬。CPU的ROB条目数动辄上百寄存器堆端口开到8到10个这些条件NPU和GPGPU很难复制。直接照搬的结果大概率是面积爆炸、时序收敛困难。正确做法是精简发射队列深度只覆盖最长等待链的一部分延迟。第二个坑是忽略编译器软件的配合。如果编译器完全不了解硬件乱序窗口的存在依然生成过度保守的依赖关系标注或者故意插入了大量序列化屏障硬件的动态调度能力会被直接废掉。乱序窗口能否发挥作用很大程度取决于编译器的调度模型和发射队列中的依赖检查是否形成一个整体。第三个坑是验证难度被低估。乱序调度的状态空间比顺序调度的状态空间大好几个数量级。不规则的调度顺序、旁路网络上的数据竞争、提交顺序与异常处理的交互每一样都是验证噩梦。建议在架构验证阶段就引入形式化验证工具辅助检查而不是只靠仿真点上的随机测试。第四个坑是功耗墙。乱序调度器的功率密度通常很高尤其在唤醒逻辑和选择逻辑上。这些逻辑恰恰是动态功耗的主要来源。在大算力加速器里调度器的功耗占比如果超过5%就值得重新审视窗口深度和唤醒策略是否逼得太紧。必要时可以采用门控时钟、数据驱动的唤醒树来降低无效翻转。7.4 乱序设计可以分阶段演进最后说一个贴近现实的经验乱序能力的引入不一定非得一步到位。完全可以在第一代芯片里先做任务级动态调度和DMA弹性队列不涉及指令级乱序软件模型仍然是顺序的硬件只需要在任务分发和数据搬运层面做动态化跑通之后第二代再做指令级浅度乱序把标量控制处理器里的固定流水线留出调度窗口。这样做的风险可控每一代的收益都能被精确测量和验证。这几年我越来越确信一个判断加速器行业之前之所以能坚持顺序执行静态编排这么久是因为模型足够简单编译器足够聪明。但模型复杂度和算力规模持续堆上去之后纯静态方案能覆盖的比例只会越来越小。乱序不是用来炫技的它本质上是给加速器增加一种动态纠偏的能力让算力资源在负载不完美时也能闭环运转。从浅度窗口开始一点一点积累对这种能力的理解是如今做加速器设计最务实的思路之一。

相关新闻