趋近智
管理处理器寄存器是生成高性能代码的主要步骤之一,它发生在目标特定指令选择之后。寄存器分配是编译器的主要优化之一,但现代CPU、GPU和加速器中广泛存在的向量单元(SIMD)和专用矩阵乘法单元,给传统的标量寄存器分配带来了显著增加的复杂性。高效使用这些庞大且通常专用的寄存器文件,对于实现这些单元的全部吞吐量必不可少。
机器学习任务中常见的向量和矩阵操作,能够同时处理大量数据。硬件通过相应的庞大寄存器文件体现了这一点:
mma指令)。经典的图着色寄存器分配器(基于Chaitin或Briggs算法)构成了许多编译器的依据。它们建立一个干扰图,其中节点表示活跃范围,边连接相互干扰的范围,然后尝试使用等于可用物理寄存器数量的颜色对图进行着色。然而,将这些方法直接应用于大型向量/矩阵寄存器文件会遇到问题:
为了应对这些挑战,编译器采用更复杂的、为向量和矩阵寄存器量身定制的技术:
再具体化: 对于一个值(特别是常数或容易从其他值推导出的值,例如生成一个全零向量),分配器可以选择稍后重新计算(再具体化),而不是溢出和重新加载。这避免了对于易于重新生成的值产生的昂贵内存流量。编译器会识别其结果可以再具体化的指令,并权衡重新计算的成本与溢出/重新加载的成本。
活跃范围拆分和寄存器打包: 当向量寄存器包含多个独立的较小值,或当一个值仅在向量通道的子集中活跃时,分配器可能会拆分该活跃范围。这允许原始活跃范围的不同部分分配给不同的物理寄存器或独立溢出。反之,如果多个小的、不相互干扰的值可以放入单个向量寄存器中,它们可以打包在一起,从而降低整体寄存器需求。
优化溢出代码: 当溢出不可避免时,分配器必须生成高效的溢出代码。
寄存器分块: 这种技术将寄存器分配与循环分块优化(在第4章中讨论)紧密结合。内循环的结构使得一个计算块(例如,矩阵乘法的子块)的工作集能够放入可用的向量/矩阵寄存器中。对于GEMM (C+=A∗B),这通常意味着将C矩阵的一个块(Csub)保存在寄存器中(通常是累加器),并通过其他寄存器流式传输A和B的块。分配器的目的是最小化Csub块在迭代间的重新加载。
处理矩阵累加器: 针对矩阵单元的分配器需要特定的策略。在这些单元内累积的部分和极其有价值,且溢出代价高昂。分配器必须优先保持这些部分和驻留,通常通过仔细调度遍历矩阵块的外循环来实现。具体的指令(例如PTX mma、HLSL波矩阵内在函数)通常决定了操作数和累加器如何映射到寄存器文件。
阶段排序考量: 寄存器分配是在指令调度之前还是之后执行的经典困境,在向量/矩阵单元上变得更加突出。早期分配会限制调度器,而后期分配如果调度创建了高寄存器压力,可能会强制产生更多溢出。现代编译器通常使用迭代方法或集成调度和分配阶段,特别是对于性能关键的循环。
在GPU上,寄存器分配对占用率有直接而显著的影响。占用率指的是可以在流多处理器(SM)上并行驻留的活跃warp(线程组)的数量。每个SM都有一个庞大的物理寄存器文件,但它由在该SM上运行的所有线程共享。
编译器必须应对这种权衡。积极地分配寄存器可能会在线程内部实现更好的指令级并行,但会降低线程级并行(占用率)。反之,最小化寄存器使用会增加占用率,但可能由于溢出或展开减少而导致性能损失。GPU编译器通常使用启发式方法、分析数据或允许程序员提示(如CUDA中的__launch_bounds__)来指导这种平衡。
每线程分配的寄存器数量与在SM上可以并发运行的最大warp数量之间的关系,假设寄存器是限制因素。
考虑一个简化的矩阵乘法内循环(Cij+=Aik×Bkj),我们目标是保持一个4×4的C矩阵块在寄存器中。这需要16个累加器寄存器(标量或向量,取决于目标)。为了计算这个块,我们可能需要在最内层(k)循环的每次迭代中,为A的一个面板加载例如4个向量寄存器,为B的一个面板加载4个向量寄存器。
有效管理向量和矩阵寄存器不仅仅是应用标准分配算法。它需要充分了解目标架构的能力和限制,与指令调度和循环优化阶段的仔细交互,以及用于最小化溢出宽向量或专用矩阵数据所带来高成本的复杂策略。在此做出的选择,对于在现代异构硬件上将优化后的IR转换为高性能机器代码非常重要。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造