经典模型 Roofline Model

Roofline Model的核心量有三个。第一是运算强度,也称算术强度,记为 $I$,定义为每从主存搬运一字节数据所做的浮点运算数,单位 FLOPs/Byte。第二是硬件的计算峰值 $P_{\text{peak}}$,单位 FLOPs/s。第三是主存带宽峰值 $B_{\text{peak}}$,单位 Byte/s。

Roofline Model给出的性能上界是 $P \le \min\big(P_{\text{peak}},; B_{\text{peak}}\cdot I\big)$。

于是乎我们就有下面的图片(来自Wikipedia),也就是一个折线: roofline

假设我们的程序非常简单,只需要在CPU上执行,而且不考虑分布式计算。上面讨论的带宽即CPU-Mem的带宽,其数量级大概是几百GB/s。(8通道DDR4-3200的理论速度是200GB/s,8通道DDR5-6400的理论速度是400GB/s,目前的CPU大多支持到最多8个内存通道。)上面讨论的计算峰值的数量级大概在10TFLOPS。(我们非常粗略地用一颗比较贵的CPU来估计:AMD Threadripper 9995wx,最理想的状态下为 96 核 * 5.4 GHz (Max Clock) * 16 (AVX512) * 2 (FMA) = 16.6 TFLOPS)而对于GPU,参考H100-SXM的数据,显存带宽为3.35TB/s,算力在bf16条件下为1979TFLOPS。

当然,我们不能认为计算峰值和带宽峰值都能跑到理论峰值而没有任何bubble。CPU确实是有这么高的计算峰值,但即使没有受访存限制,程序也很难跑到这个峰值,比如说你没法保证你的程序只有乘加操作并且全部都用AVX跑满了并且不需要做任何padding。内存这块也是一样的,确乎是有这么大内存带宽,但你也没法保证这个带宽无时无刻不是完全跑满的,除非你设计了一套完美的prefetch+cache机制。所以实际上一个真实程序会落在Roofline Model的折线的下面,而其性能的上限就是落在折线的线上。

此时我们谈到了cache,就引出了Roofline Model的一个理想假设,认为算术强度(Arithmetic Intensity)仅仅由算法(Kernel)本身决定,而没有考虑系统架构(Architecture)上的问题。比如说,一个数据局部性(locality)很差的程序,会频繁地发生缓存未命中。这意味着,尽管算法本身可能只需要读取N个字节的数据,但由于缓存的低效使用,CPU实际上与主内存之间来回传输了远超N个字节的数据量,这将导致实际的、在硬件上表现出来的算术强度远低于理论值。

当我们把Arch纳入Roofline Model的考虑的时候,Roofline Model的折线就将出现更多条低于前面理想假设的线。新产生一些墙比如说L2缓存墙,代表了“如果你的程序性能完全受限于L2缓存的带宽,那么它能达到的最大算术强度是多少”。算法决定了哪一条硬件限制(L1/L2/L3/DRAM/Compute)成为了当前程序的“实际roofline”。

当我们再考虑更复杂的场景下时,比如说考虑CPU+GPU的计算,此时实际上就是在原有的roofline基础上再添一些roof。比如下面这样(来自ASPLOS'25论文MoE-Lightning: High-Throughput MoE Inference on Memory-constrained GPUs):

roof.png

对于一个计算和数据都在GPU的任务,roofline是绿色线;对于一个计算和数据都在CPU的任务,roofline是蓝色线;对于一个计算在GPU,数据在CPU的任务,roofline是红色线。此时给出的洞察就是,如果该任务的计算强度特别低,那么与其将这个任务传给GPU算,不如直接在CPU上算完(蓝色roofline比红色的高),只有在计算强度跨过图中的P1点,才有传输到GPU上进行计算的必要。

有这样的roofline带来的insight是,在设计CPU+GPU的任务的时候,我们不再是naive地考虑我们只是希望靠这种异构计算的模式白嫖原本idle的CPU算力,而是希望解决在面临Memory Bound的时候把CPU-Mem这条闲置的带宽用起来。考虑到CPU的带宽和GPU的带宽只差一个数量级,但算力差两到三个数量级,对于Memory Bound的任务如果能用上这条带宽,那么其理论上的加速比是远比Compute Bound的时候用上CPU的算力带来的加速比可观的。但是代价是PCIe的瓶颈需要设计精妙的pipeline或者prefetch+cache来弥补。