try ai
科普
编辑
分享
反馈
  • 算术强度

算术强度

SciencePedia玻尔百科
核心要点
  • 算术强度是浮点运算次数与移动数据字节数的比值,从根本上表征了一个算法的性能潜力。
  • Roofline 模型将性能极限可视化,显示一个程序是内存受限(受数据速度限制)还是计算受限(受处理器速度限制)。
  • 对于内存受限的任务,提高性能需要通过减少数据移动来增加算术强度,主要通过数据复用技术(如分块)来实现。
  • 算术强度低的算法不仅速度更慢,而且由于从主内存移动数据的成本很高,其能耗也显著增加。

引言

在高性能计算领域,现代硬件的核心存在一个悖论:处理器的计算速度呈指数级增长,但它们从内存中访问数据的能力却未能跟上步伐。这种日益扩大的差距被称为“内存墙”,意味着即使是最强大的计算机,也常常在等待数据上花费比计算更多的时间。为了克服这一关键瓶頸,我们需要一种方法来理解和量化算法的计算工作与其数据需求之间的关系。正是在这里,算术强度的概念成为了一个不可或缺的工具。

本文对算術強度及其对算法设计和性能优化的深远影响进行了全面探讨。在第一章“原理与机制”中,我们将解构这一核心概念,解释它如何被用于极具影响力的 Roofline 模型中以诊断性能限制,并揭示数据复用等技术如何能显著提升效率。随后,“应用与跨学科联系”一章将展示算术强度的普遍 relevance,说明它如何指导从数值线性代数、计算物理到深度学习前沿等领域的优化策略。读完本文,你将建立一个强大的心智模型,用于分析计算性能和设计不仅更快,而且本质上更高效的软件。

原理与机制

想象一下,你置身于一个巨大的图书馆中,任务是撰写一份需要参考数百本书的报告。你有两种基本速度决定了你完成任务的快慢。第一种是你思考、阅读和写作的速度——你的“思考速度”。第二种是你走到书架、找到书并把它带回桌子的速度——你的“取书速度”。如果你的报告要求你每写一句话就要查阅一本新书,那么你大部分时间将花在走路而不是写作上。你的“取书速度”就是瓶颈。但是,如果你能收集一堆相关的书籍,并在需要返回书架前用它们工作数小时,那么你的进度将只受限于你的“思考速度”。

这个简单的类比正是现代高性能计算的核心。一台计算机,就像我们的图书管理员一样,受两种主要速度的支配。

计算机的两种速度

首先,是处理器的原始计算能力,即其​​峰值性能​​,我们称之为 PpeakP_{\text{peak}}Ppeak​。它以每秒浮点运算次数(FLOPS)来衡量,代表它理论上每秒可以执行多少次算术计算(如加法或乘法)。这是计算机的“思考速度”。对于现代处理器来说,这个数字可能高得惊人,达到每秒数万亿次运算(teraflops)。

其次,是处理器从主内存(DRAM)获取数据的速度。这是​​内存带宽​​,我们称之为 BmemB_{\text{mem}}Bmem​,以每秒字节数来衡量。这是计算机的“取数速度”。虽然这个数字也相当可观,但历史上计算速度的提升率超过了内存带宽的提升率。这种不断扩大的差距造成了一个被称为​​“内存墙”​​的根本性挑战:处理器常常因数据不足而“挨饿”,空闲地等待信息从内存中传来。

对于任何给定的任务,其总耗时 TTT 受限于这两项活动中较长的一项:计算所花费的时间和移动数据所花费的时间。我们可以用极其简洁的方式表达这一点:

T≥Total OperationsPeak PerformanceandT≥Total Bytes MovedMemory BandwidthT \ge \frac{\text{Total Operations}}{\text{Peak Performance}} \quad \text{and} \quad T \ge \frac{\text{Total Bytes Moved}}{\text{Memory Bandwidth}}T≥Peak PerformanceTotal Operations​andT≥Memory BandwidthTotal Bytes Moved​

一个合理的执行时间模型是,它由这两者中的瓶颈所决定。因此,如果一个程序执行 FFF 次浮点运算并移动 DDD 字节的数据,其时间 TTT 大约为:

T≈max⁡(FPpeak,DBmem)T \approx \max\left(\frac{F}{P_{\text{peak}}}, \frac{D}{B_{\text{mem}}}\right)T≈max(Ppeak​F​,Bmem​D​)

这立即告诉我们一些至关重要的信息。一个程序的性能不仅仅是其运行硬件的一个特性,它还关键性地取决于程序本身的性质。我们如何捕捉这种性质呢?

算术强度:计算的特征

要了解一个任务是受限于思考还是取数,我们需要知道它为获取的每一份数据执行了多少次思考。这个关键的比率就是我们所说的​​算术强度​​,用字母 III 表示。它从第一性原理定义为:

I=Floating-Point OperationsBytes MovedI = \frac{\text{Floating-Point Operations}}{\text{Bytes Moved}}I=Bytes MovedFloating-Point Operations​

算术强度是算法的一个基本属性。它告诉我们一个算法是与内存“话多”,不断请求新数据,还是“深思熟虑”,对它收到的每一份数据进行大量计算。

让我们来看两个经典的计算任务。首先,一个简单的“流式”操作,就像许多科学代码中使用的那样,我们用两个数组 BBB 和 CCC 的元素来更新数组 AAA:A[i]←B[i]+s⋅C[i]A[i] \leftarrow B[i] + s \cdot C[i]A[i]←B[i]+s⋅C[i]。对于每个元素,我们执行一次乘法和一次加法,即 222 次浮点运算(FLOPs)。为此,我们必须从 BBB 中读取一个元素,从 CCC 中读取一个元素,并向 AAA 写入一个元素。如果我们使用双精度数(每个8字节),那么从主内存读取和写入的数据量就是 8+8+8=248+8+8 = 248+8+8=24 字节。因此,算术强度为:

Itriad=2 FLOPs24 Bytes≈0.083 FLOPs/ByteI_{\text{triad}} = \frac{2 \text{ FLOPs}}{24 \text{ Bytes}} \approx 0.083 \text{ FLOPs/Byte}Itriad​=24 Bytes2 FLOPs​≈0.083 FLOPs/Byte

这是一个非常低的强度。这就像我们的图书管理员走到书架去取三本书,只为了写一个两个词的短语。

现在,让我们将此与一个不同的算法进行对比:乘以两个大的方阵,C←A×BC \leftarrow A \times BC←A×B。如果矩阵的大小为 n×nn \times nn×n,总操作数约为 2n32n^32n3。一个朴素的算法可能会为 CCC 的每个元素从内存中读取所需的行和列,这会导致巨大的数据流量。然而,一个聪明的算法可以将矩阵的块加载到快速的本地内存(缓存)中,并对其进行广泛的复用。在一个理想的“分块”算法中,我们只需从慢速的主内存中读取 AAA 和 BBB 的每个元素一次,并读写 CCC 的每个元素一次。总的数据移动量与矩阵的大小成正比,而不是与操作数成正比:完整更新大约需要 32n232n^232n2 字节。那么算术强度就是:

IGEMM≈2n3 FLOPs32n2 Bytes=n16 FLOPs/ByteI_{\text{GEMM}} \approx \frac{2n^3 \text{ FLOPs}}{32n^2 \text{ Bytes}} = \frac{n}{16} \text{ FLOPs/Byte}IGEMM​≈32n2 Bytes2n3 FLOPs​=16n​ FLOPs/Byte

这是一个了不起的结果!与流式 triad 操作不同,矩阵乘法的强度随问题规模的增长而增长。对于一个大小为 n=1024n=1024n=1024 的矩阵,其强度为 1024/16=641024/16 = 641024/16=64 FLOPs/Byte,比我们的 triad 示例高出近一千倍。这个算法是“深思熟虑”的;它为从主图书馆书架上取出的每个字节执行了大量的计算。一些算法,如矩阵向量乘法,其强度是恒定且低的,这使得它们天生更容易受到内存墙的影响。

Roofline 模型:通往最高性能的地图

现在我们可以将机器的属性(PpeakP_{\text{peak}}Ppeak​,BmemB_{\text{mem}}Bmem​)和算法的特性(III)统一成一个单一、优雅的图景:​​Roofline 模型​​。

我们程序的性能 PPP,以 FLOPS 衡量,不能超过处理器的峰值速度:P≤PpeakP \le P_{\text{peak}}P≤Ppeak​。这是我们的第一个限制,一个平坦的性能“屋顶”。

性能也受到我们提供数据速度的限制。对于我们获取的每个字节,我们可以执行 III 次操作。如果内存系统每秒提供 BmemB_{\text{mem}}Bmem​ 字节,它能支持的最大性能是 P≤Bmem×IP \le B_{\text{mem}} \times IP≤Bmem​×I。这是我们的第二个限制,一个倾斜的“屋顶”,其高度取决于算法的强度 III。

实际性能被困在这两个屋顶之下:

P≤min⁡(Ppeak,Bmem×I)P \le \min(P_{\text{peak}}, B_{\text{mem}} \times I)P≤min(Ppeak​,Bmem​×I)

这个简单的不等式给了我们一张强大的地图。在一张性能与算术强度的图上,这个边界的形状看起来像一个屋顶线。

该模型清晰地定义了两种计算模式:

  1. ​​内存受限(或带宽受限):​​ 如果一个算法的强度 III 很低,它将首先碰到倾斜的屋顶部分。其性能由内存带宽决定:P≈Bmem×IP \approx B_{\text{mem}} \times IP≈Bmem​×I。此时,处理器处于“饥饿”状态,等待数据。机器是“取数受限”的。许多常见的科学计算核心,如网格上的模板更新,在使用朴素实现时自然会落入这个范畴。

  2. ​​计算受限:​​ 如果一个算法的强度 III 很高,它将碰到平坦的屋顶部分。其性能仅受处理器峰值速度的限制:P≈PpeakP \approx P_{\text{peak}}P≈Ppeak​。机器是“思考受限”的,内存系统能够跟上。

这两种模式之间的交叉点发生在被称为​​机器平衡点​​或​​脊点​​的强度上,I⋆=Ppeak/BmemI_{\star} = P_{\text{peak}} / B_{\text{mem}}I⋆​=Ppeak​/Bmem​。这个值是硬件的一个特性。如果你的算法强度 III 小于 I⋆I_{\star}I⋆​,你就是内存受限的;如果大于它,你就是计算受限的。

这导致了一个令人惊讶而深刻的结论:如果你的程序陷入了内存受限的状态,使你代码的计算部分更高效(例如,通过减少 FLOPs 的数量)可能根本不会加快它的速度!。如果你已经把所有时间都花在去图书馆书架的路上了,学得再快也无助于你更快地完成报告。唯一的改进方法是减少你去书架的次数。

攀登 Roofline 的艺术:数据复用

Roofline 模型不仅能诊断问题,它還为我们指明了治愈之路。要为一个内存受限的核心实现更高的性能,我们必须增加其算术强度。由于浮点运算的次数通常由问题的定义所固定,增加 III 的唯一方法是减少从主内存移动的字节数。

实现这一点的关键是​​数据复用​​。我们必须像那位一次性收集一大堆书的图书管理员一样聪明。我们需要利用计算机的​​内存层次结构​​——一个由更小、更快的内存(称为​​缓存​​)组成的系统,它位于处理器和慢速主内存之间。当数据从主内存中取出时,它会被放置在缓存中。如果处理器很快又需要同样的数据(​​时间复用​​)或需要位于内存中附近位置的数据(​​空间复ouyong​​),它可以从快速的缓存中获取,而无需再次慢速地访问主内存。

巧妙的算法设计就在于最大化这种复用。实现这一目标的主要技术是​​分片​​或​​分块​​。

  • 对于矩阵乘法,算法不是处理整个矩阵,而是将小的方形子矩阵(片或块)加载到缓存中。然后,它在驱逐这些块之前,对它们执行所有必要的计算。这个简单的改变极大地减少了主内存流量,使其不再随工作量(O(n3)O(n^3)O(n3))扩展,而是随数据大小(O(n2)O(n^2)O(n2))扩展,从而将算术强度从一个常数提升到随 nnn 增长的值。
  • 对于随时间演化的模板计算,我们可以使用​​时间分块​​。我们不是为整个空间域计算一个时间步,而是为一个小的域块计算多个时间步。这使得该块的数据在多次更新中保持在缓存中的“热”状态,显著增加了复用并提高了强度。

通过使用分块等技术增加算法的算术强度,我们可以在 Roofline 图上沿 x 轴移动,向上攀登倾斜的屋顶,直到理想地达到平坦的峰值性能天花板。

超越速度:数据的能源成本

算术强度的重要性甚至比执行时间更深。移动数据不仅慢,而且在​​能源​​方面也极其昂贵。

考虑一次计算与为其移动数据的能源成本。一次浮点运算可能消耗几个皮焦(pJ)的能量。从最快的 L1 缓存访问该数据也需要类似的能量。但是,如果数据不在缓存中,必须从主内存(DRAM)获取,能源成本可能会高出100到200倍!。

这意味着算术强度低的算法不仅仅是慢,它还是一个耗电大户。它的大部分能源预算都花在了在芯片上以及往返于 DRAM 之间穿梭数据上。通过优化算法来提高其算术强度,我们不仅使其更快,而且还显著降低了其能耗。我们正在设计“多思考,少取数”的算法,这是为从手机到世界上最大的超级计算机等一切设备创建高效软件的基本原则。

通过这种方式,算术强度提供了一个优美、统一的原则。它将算法的抽象世界与硬件的物理限制联系起来,引导我们走向不仅更快,而且更优雅、更可持续的计算。它是帮助我们在处理器对计算的无尽渴求与供給它的世界有限速度之间的挑战性地形中导航的指南针。

应用与跨学科联系

熟悉了算術強度的原理和 Roofline 模型的优雅清晰之后,我们可能会问:这又如何?这是一个合理的问题。一项物理定律或一个数学概念获得其真正力量,并非仅来自其抽象之美,而在于其解释和预测我们周围世界的能力。算术强度也不例外。它是一把钥匙,解锁了对计算世界更深层次的理解,揭示了在科学和工程领域令人叹为观止的广阔图景中隐藏的统一性。

现在,让我们踏上穿越这片图景的旅程。我们将看到这个单一的概念——工作完成量与数据移动量的简单比率——如何作为一种通用的性能语言,指导着从解决基本方程的算法到正在重塑我们世界的人工智能的设计。

根基:重塑计算架构

几乎所有科学计算的核心都是强大而通用的线性代数规则。如果我们希望理解计算性能,这里是理所当然的起点。人们可能认为这些操作的速度仅取决于处理器的原始计算能力。但算術強度揭示了一个更微妙的真相。

考虑一个最基本的操作:两个向量的内积或点积。我们将对应元素相乘并求和。在像 Modified Gram-Schmidt 过程这样更复杂的算法背景下,这个操作会反复执行。让我们把处理器想象成一位主厨,其最快的内存(缓存)是操作台。主内存是走廊尽头的食品储藏室。对于一个非常长的向量的内积,厨师从储藏室取两个数字,将它们相乘,然后将结果加到操作台上的一个 running total 中。然后他们回到储藏室去取下两个数字。每一个短暂的计算瞬间,都伴随着一次去储藏室的长途跋涉。算術強度低得可怜。我们那能够完成惊人计算壮举的 multi-gigaflop 处理器,大部分时间都在等待数据。它陷入了严重的​​内存受限​​状态。这个 humild 的例子教给我们一个关键教训:对于许多简单的流式操作,性能几乎与处理器的峰值速度无关,而完全取决于内存系统的带宽。

现在,让我们让事情变得更有趣。科学领域的许多问题,从模拟结构到分析社交网络,都涉及“稀疏”矩阵,其中大部分元素为零。为了节省内存,我们只存储非零值及其位置。一种常见的格式叫做压缩稀疏行(CSR)。当我们执行稀疏矩阵向量乘法(SpMV)时,本质上仍是在执行一系列内积。然而,情况甚至更糟。因为非零元素“散布”在整个矩阵中,我们对输入向量的访问模式是不规则和不可预测的。这完全挫败了处理器智能预取数据或复用已在缓存中值的能力。我们处理的每个非零元素不仅需要从内存中获取其自身的值和列索引,还需要获取输入向量中的一个对应值。结果是算术强度比简单的内积还要低。SpMV 是内存带宽受限核心的典型例子,这一挑战推动了计算机架构和算法设计领域数十年的研究。

那么,我们是否注定要永远等待内存?完全不是。算术强度的概念不仅诊断了问题,它还指明了治愈之道。治愈方法是​​数据复用​​。解决方案是设计算法,在已经放在“操作台”(缓存中)的数据上执行尽可能多的工作。

这引领我们来到现代高性能计算中最重要的思想之一:​​分块​​。考虑使用 LU 分解求解一个大型线性方程组。一种朴素的方法是在处理完每一列后更新整个剩余矩阵——所谓的“秩-1 更新”。这是一个 Level-2 BLAS(基础线性代数子程序)操作,和内积一样,它为极少的计算流过大量数据,导致算术强度低。绝妙的替代方案是“分块”算法,这是 Level-3 BLAS 的基础。我们不是一次处理一列,而是处理一个“块”的列。我们将一个小的子矩阵(一个块)加载到缓存中,并用它来执行一次大型矩阵-矩阵乘法以更新矩阵的其余部分。缓存块中的每个元素在被丢弃之前被复用了数百或数千次。相对于移动的数据量,计算量急剧增加。算术强度显著提高,通常与块大小成正比。我们将一个内存受限的操作转变为一个​​计算受限​​的操作,最终释放了我们处理器的全部威力。这个单一的思想就是为什么现代数值库如 LAPACK 和 ScaLAPACK 效率如此惊人的原因。

模拟物理世界

有了这些基础思想,我们就可以转向模拟物理现实的宏大挑战。在这里,算术强度帮助我们 navigating 一个关键的设计选择:我们是存储预先计算的信息,还是动态地重新计算它?

想象一下我们正在使用计算流体动力学(CFD)模拟空气流过机翼。模拟的核心是使用像共轭梯度算法这样的迭代方法反复求解一个巨大的方程组。每一步都需要将一个数学算子——代表扩散的物理特性——应用于一个数值向量。一种方法是预先计算并存储这个算子作为一个巨大的稀疏矩阵,然后使用我们前面讨论的 SpMV 核心。这是“组装”方法。另一种是“无矩阵”方法,我们根本不构建矩阵。相反,每当我们需要应用算子时,我们都使用底层的物理模板(一个点与其邻居之间的局部关系)来重新计算其效果。

哪种更好?算术强度提供了答案。组装方法付出了沉重的内存代价,在每次迭代中从内存读取矩阵结构和值,导致强度低。无矩阵方法完全避免了这种流量。它做了更多的计算,但极大地减少了内存移动。结果是更高的算术强度,更有效地利用可用的内存带宽,并常常导致更快的解决方案。

这一原则在现代高阶有限元方法(FEM)中达到了顶峰,这些方法被用于从结构力学到电磁学等领域的极其精确的模拟。这些方法可以使用复杂的、弯曲的“单元”来模拟物理对象。一种称为​​和因子分解​​的关键技术允许用一系列小的、一维的矩阵乘积来应用算ator——这种计算成本的扩展比朴素方法要有利得多。这些无矩阵、和因子分解算子的算术强度可以非常高。事实上,对于具有简单、规则几何形状的问题,强度可以变得如此之大,以至于计算越过 Roofline模型的“脊点”并变得计算受限。对于更复杂的弯曲几何形状,我们需要在每个点加载几何信息,这增加了内存流量,并可能将核心推回到内存受限状态。因此,算术强度揭示了算法选择、数学结构和待解问题物理复杂性之间优美而微妙的相互作用。

算术强度的影响一直延伸到原子尺度。在计算化学中,科学家使用不同的方法来模拟分子的行为。两种主力方法是分子动力学(MD)和蒙特卡洛(MC)模拟。MD 通过计算原子间的力并积分牛顿定律来模拟原子的实际运动。MC 通过提出随机移动并根据系统能量的变化接受或拒绝它们来探索可能分子构型的空间。一个 MD 力核需要为每对相互作用的原子计算一个三维力向量。而一个 MC 核,另一方面,只需要计算由一次移动引起的标量能量差。这个看似微小的底层物理差异导致了不同的计算结构。分析表明,MC 能量计算的结构可以使其具有比 MD 力计算显著更高的算术强度。这一见解使开发人员能够针对每种方法专门定制优化,从而从像 GPU 这样的强大硬件中榨取每一滴性能。

计算与智能的前沿

当我们转向现代计算的前沿:量子化学、人工智能,甚至构建我们软件的工具时,算術強度的透镜同样强大。

在量子化学中,像自洽场(SCF)理论这样的方法涉及计算数量惊人的“电子排斥积分”(ERIs),这个数量形式上随着系统大小的四次方增长。一个关键的优化是“积分筛选”,我们用一个廉价的测试来估计一个积分的大小,如果它小得可以忽略不计,就跳过完整的、昂贵的计算。这极大地减少了总的浮点运算次数。但它对算術強度有什么影响呢?与直觉相反,它通常会降低算術強度。筛选检查本身需要内存访问(读取预计算的界限),但增加的 FLOPs 很少。它消除的工作——ERI 计算——是计算密集型的。因此,虽然总运行时间下降了(这是目标),但剩余工作的性质变得更加内存密集。这是一个深刻的洞察:目标并非总是最大化算術強度,而是将其用作一种诊断工具,以理解我们计算的特性并指导我们的优化工作。

也许今天没有哪个领域的性能比深度学习领域更关键了。像 MobileNet 这样的模型被设计用于在功率有限的设备(如智能手机)上高效运行。这些网络的一个关键组成部分是“深度可分离卷积”,它将标准卷积分解为两个阶段以减少总操作数。让我们分析第一阶段,即深度卷积,它将一个小滤波器独立应用于每个输入通道。我们可能认为一个被设计为计算上“轻量级”的操作不会给硬件带来压力。但算术强度分析揭示,这个操作是强烈的内存受限。相对于它为每个元素执行的少量计算,它读取大量数据(输入和滤波器)并写入大量数据(输出)。这解释了为什么AI硬件社区如此专注于开发具有巨大内存带宽和专用数据路径的芯片,因为它们对于全速运行即使是这些“高效”网络也是必不可少的。

最后,在一个 fascinating 的转折中,算术强度的概念正被构建到我们用来编程计算机的工具本身中。想象一个编译器需要决定你代码中的某个特定循环应该在主处理器(CPU)上运行,还是卸载到一个专门的加速器如 GPU 上。GPU 在原始 FLOPs方面可能强大得多,但数据必须首先通过像 PCIe 这样的连接发送给它,而这种连接有其自身的带宽限制。一个聪明的编译器可以分析循环来估计其算术强度。然后它可以使用一个性能模型,很像 Roofline 模型,该模型考虑了 CPU 和 GPU 的计算速度、它们各自的内存带宽以及 PCIe 数据传输的额外时间成本。通过比较预测的执行时间,编译器可以做出明智的、自动的决定。卸载的条件通常归结为一个简单的规则:如果循环的算术强度高于某个特定于机器的阈值,GPU 的计算优势就超过了数据传输成本,代码就会被卸载。编译器已经学会了用算术强度的思维方式来思考。

从最基本的矩阵运算到宇宙的模拟和人工智能的构建,算術強度提供了一个统一的框架。它是一个简单的比率,但它讲述了一个关于计算与数据移动之间持续、复杂舞蹈的深刻故事。掌握它就是为了获得对算法性能的深刻直觉,理解硬件的限制,并在追求更快、更强大计算的无尽探索中 unlocking new possibilities。