公众号记得⭐️,第一时间看推送不会错过。
大语言模型工程师需要的直觉,无需硬件手册。
最终,量化、推测解码和连续批处理等技术将不再像是一堆需要记忆的技巧。
几乎所有开始使用语言模型的人都会遇到一个困惑的问题。
你租用了一块顶级数据中心GPU。规格表显示它每秒可以执行接近一千亿次的算术运算。你加载了一个包含700亿个参数的模型,并开始生成文本。你观察利用率监控器,发现数值很高。一切看起来都很正常。
然后你统计生成的词元数量。你发现,对于一块标称每秒可执行一千亿次运算的芯片来说,每秒只有几十个词元。几乎没有任何算术能力被利用。
一切正常。你的代码没问题,驱动程序也没问题,购买更强大的GPU也几乎无济于事。你看到的正是这块硬件最重要的特性,一旦你清楚地认识到这一点,你读过的大多数性能优化建议就不再是一堆技巧,而是显而易见的。
本文将从基础做起,逐步构建这种直觉。我们将探讨GPU的外形设计原因、内存和计算能力之间持续的竞争关系,以及真正决定机器运行速度的因素。
本文无需CUDA知识。我们会列举一些实际芯片,并提供具体的数值供参考,但本文内容并不要求您记住这些数值,而且在所有情况下,数值的比值远比其来源数字本身更为重要。
但还是有个前提条件,你应该熟悉神经网络进行矩阵乘法运算的概念,并且知道模型权重是存储在内存中的数值。仅此而已。
一个不对称性几乎可以解释一切
首先,我们要从整个设计赖以存在的这个事实说起。
进行算术运算的成本很低。获取用于运算的数字的成本却很高。
你第一次读到这句话时会觉得不对劲。乘法听起来像是难点,而移动数字听起来又很简单。但在现代硬件上,情况恰恰相反,而且差距相当大。
我们可以用一个车间来形象地理解。算术单元就是工作台,成千上万个工作台挤在房间中央。数据存储在一个仓库里,仓库通过一条走廊与车间相连。
工作台消耗数据的速度远远超过走廊输送数据的速度。即使增加更多的工作台,情况也不会改变,因为走廊已经是极限了。这条走廊就是人们所说的内存带宽。
你之后遇到的所有性能优化技术,都是为了在每次经过这条走廊时,尽可能地提高数据处理效率。
为什么差距不断扩大
这并非暂时的工程技术缺陷,而是一个长期趋势。
在最近几代加速器中,算术能力的增长速度是内存带宽的数倍。每一代新芯片都能进行更多的数学运算,但能够获取的数据量却只略有增加。
因此,这种不平衡每代都在加剧,而不是缓解。随着时间的推移,减少数据移动的技术变得越来越重要,而原始的算术能力越来越无法准确反映芯片的实际性能。
为什么GPU拥有数千个简单的单元而不是少数几个复杂的单元
现在让我们来看看硬件是如何应对这种不对称性的。
CPU的设计目标是快速完成一个指令序列。它将大部分硅片用于实现这一目标。例如,使用大容量缓存来确保数据就在附近;使用预测硬件来猜测下一个指令;使用重排序逻辑来保证在等待期间也能继续工作。
所有这些都服务于单一的工作流程,并且都会占用大量的芯片面积。
GPU 的设计理念恰恰相反,因为它专为处理具有特殊属性的工作负载而设计。在图形处理中,每个像素都对不同的数据运行相同的短程序。在神经网络中,张量的每个元素都接受相同的处理。
当程序在数百万个数据元素上都相同时,就不再需要为每个元素配备一个控制器。一个控制器可以控制数千个运算单元,所有单元都对不同的值执行相同的指令。
这就是关键所在。GPU 删除了 CPU 中大部分的控制机制,并将节省下来的空间用于运算单元。
原始数量的对比非常明显。高端服务器 CPU 一次可以处理几百个线程,而数据中心 GPU 在类似的功耗预算下,每个时钟周期可以处理数万个线程。
关键在于,GPU 线程的性能要弱得多。它无法独立运行,而且自身运行速度也不快。它只是庞大而简单的机器中的一个通道。
在这里,你有一个术语值得学习,因为你到处都能看到它。线程不是逐个调度的。硬件会以固定的 32 个线程为一组来处理它们,这些线程同步运行并共享一条指令,这 32 个线程组成的组被称为一个线程束(warp)。这就是完整的定义,也是芯片实际运行的单元。
唯一会造成性能泄漏的地方
由于一个线程束(warp)共享一条指令,其中的 32 个线程不会在执行操作上产生分歧。
如果你的代码基于数据进行分支,而线程束中的一些线程选择一条路径,另一些线程选择另一条路径,硬件会按顺序执行这两条路径。它会关闭不在当前路径上的线程,这些线程会处于空闲状态,不产生任何输出。
线程束的运行时间等于两条分支路径的总和。
只有当线程束内部出现分歧时,才会产生性能损耗。不同的线程束选择不同的路径则不会产生性能损耗。实际上,这意味着在紧密的内部循环中使用数据相关的分支是值得避免的,而且当你调用库函数而不是编写自己的函数时,这种情况很少发生。
GPU 并不会缩短等待时间,而是让等待变得不可见
我们说过获取数据很慢。CPU 和 GPU 都面临着相同的物理延迟,但它们处理延迟的方式截然不同。
CPU 会尝试避免等待,它使用缓存和预测技术在请求数据之前就准备好数据。
GPU 会接受等待,并安排在等待期间始终执行其他任务。
其机制如下:芯片加载的工作负载远超其同时运行的能力。芯片上的单个计算单元可能驻留着 64 个线程束,但每个时钟周期只能执行一个。
当正在运行的线程束向主内存请求数据时,它会暂停。调度器不会等待它,而是选择另一个已准备就绪的线程束来运行。当该线程束也暂停时,调度器会选择第三个。
算术单元始终处于忙碌状态。每个线程束的大部分时间都处于等待状态,整个机器从未真正空闲过。
线程束之间的切换几乎不消耗任何资源。在 CPU 上,改变正在运行的线程意味着保存一个线程的状态并恢复另一个线程的状态,这需要数百个时钟周期。而在 GPU 上,每个驻留线程束的状态都已存储在芯片上的专用存储空间中。切换只需指向不同的存储空间,只需一个时钟周期。
这就是为什么 GPU 拥有海量的高速片上存储空间。它并非为了提升单个线程的速度,而是为了让成千上万个线程可以同时处于半完成状态,随时准备在数据到达时立即执行。
这就是为什么监控中的利用率数值具有误导性。它通常显示的是芯片上是否调度了任何任务,而不是运算单元是否在执行任何有用的操作。一个 100% “利用率”但数据匮乏的 GPU,看起来与一个满负荷运行的 GPU 完全相同。
实用版本
如果本节只能记住一个操作要点,那就记住这一点:
GPU 需要大量独立的工作才能高效运行。如果只给它一个小任务,它完成任务的速度大致与内存系统的速度相当,而且芯片的大部分资源都处于空闲状态。但如果给它一大批类似的工作,同样的硬件突然就显得速度飞快。
这并非偶然现象,而是设计本身的预期效果,这也是为什么在所有关于服务器性能的讨论中,批处理大小都会被提及的原因。
内存就像一个阶梯,每一层都比上一层慢得多
现在来说说几乎决定所有性能的部分。
数据并非仅仅“存在于内存中”。它存在于距离运算单元不同的位置,而最近和最远位置之间的差异巨大。
有四个值得了解的层级:
线程级存储。这是距离最近、速度最快的层级。每个线程在处理数据时都会将少量值保存在这里。访问几乎是瞬间完成的。
片上暂存区。每个计算单元都附带一个容量仅为几百 KB 的小型高速内存池。关键在于,如果您编写的是底层代码,那么您可以完全控制这个内存池。您可以决定哪些数据会存储在这里,以及数据会保留多长时间。
共享缓存。这是一个更大的内存池,容量为几十 MB,由整个芯片共享。它由硬件自动管理。您无法选择存储在这里的数据,但访问模式会影响其存储内容。
主内存。这是一个容量为几十 GB 的大型内存池,您的模型权重和激活值实际存储在这里。这就是人们常说的 VRAM。它位于处理芯片的外部,访问它是芯片运行速度最慢的操作。
随着您向下移动,两个方面会发生变化:内存池的容量会越来越大,访问它们也会变得越来越困难。
两端之间的差距可不小。读取线程已持有的值几乎不耗费任何成本。而访问主内存的成本则要高出数百倍。
你的权重位于最底层,这是最大的内存层,也是唯一足以容纳它们的内存层。
两种成本,而硬件只能处理其中一种
访问内存的成本体现在两个方面,而区分这两种成本正是本文后续内容的意义所在。
第一种成本是等待。你请求数据,数据到达需要一段时间。
硬件会为你处理这种等待,其机制与上一节所述完全相同。它同时处理数千个请求,因此当一个线程束等待时,其他许多请求都在被处理。等待是真实存在的,但几乎完全隐藏。
第二种成本是路径宽度。每秒只能传输一定数量的字节,无论请求重叠多少次,这个限制都不会改变。
这种成本是无法隐藏的。这是一个硬性上限,也是芯片将内存带宽作为主要指标进行宣传的原因。这就是为什么本文余下部分的所有计算都与带宽有关,而不是与等待时间有关。
这些层级的实际位置
梯形图显示了每一层级之间的距离。了解它们的物理位置也很有帮助,因为布局解释了为什么有些层级是共享的,而有些层级则不是。
GPU 不是一个巨大的算术单元池。它被划分为大约一百个独立的单元,每个单元都是一个小型的自包含机器。这些单元被称为流式多处理器(Streaming Multiprocessor,简称 SM),使用这个正式名称是值得的,因为你遇到的所有工具和文档都会使用它。
每个 SM 都拥有自己专属的顶层两层级副本。它拥有自己的线程寄存器文件、自己的共享内存和 L1 缓存,以及自己的线程束调度器,用于决定接下来运行哪个线程束。所有这些都不会与相邻的 SM 共享。
工作以线程块的形式到达,线程块是一批被分配给一个 SM 的线程,该 SM 会一直运行直到完成。在线程块内部,线程的处理方式与我们之前提到的线程束相同。举个例子就能具体说明划分方式。假设你启动一个包含 256 个线程的线程块,这是一个常见的选择。硬件会将其分成 8 个线程束(warp),每个线程束包含 32 个线程,调度器会从中选择执行这 8 个线程束。
你永远不会选择这种划分方式。你只需要选择批处理的大小,而分组到 32 个线程束的过程会在你不知情的情况下自动完成。
这就是为什么批处理大小几乎总是 32 的倍数。即使你请求 250 个线程,你仍然会得到 8 个线程束,但最后一个线程束只有 32 个线程通道中的 26 个在运行,而其余 6 个线程通道虽然轮到它们,却什么也不产生。
允许的最大线程块是 1024 个线程,这相当于 32 个线程束位于同一个 SM 上。
这就是共享内存的优势所在,也是它名称的由来。线程块中的每个线程都在同一个 SM 上,因此它们都可以读取相同的共享内存,彼此协作的成本几乎为零。
所有SM(流式存储器)下方是L2缓存,每个SM都可以访问它。L2缓存下方,完全位于芯片外部,横跨内存总线的是HBM(高显存内存),它存储着权重、键值缓存和激活信息。人们常说的全局内存或VRAM指的就是HBM。
这张图解释了你不断遇到的权衡取舍。任何能保存在单个SM(单层存储)内的数据都速度快且私密。任何需要整个芯片访问的数据都必须下传到L2或更低的缓存层,而下传过程是开销最大的部分。
这也赋予了这四个缓存层它们各自的名称,这些名称你在其他任何地方都会看到。每个线程的存储空间是寄存器文件。暂存区是共享内存,与L1缓存并列。共享缓存是L2缓存。主内存是HBM,它与人们通常所说的全局内存或VRAM相同。
本文的其余部分将使用这些名称。
反直觉的部分
在CPU上,存储空间的大小随着速度的降低而增大,呈平滑的金字塔形。寄存器很小,缓存很大,主内存很大。
在GPU上,这种形状被扭曲了。整个芯片上的寄存器文件加起来的大小与L2缓存大致相同。这很不寻常,并且直接源于上一节的内容。成千上万个未完成的线程需要地方存放它们的状态,因此芯片会分配相当大的面积来存储这些状态。
总而言之,内存系统并非单一整体,而是一个阶梯,性能优化几乎完全取决于尽可能长时间地将数据保存在高层。
每字节工作量决定一切
现在我们已经掌握了足够的信息来精确阐述核心思想。
以你在 GPU 上运行的任何操作为例。计算两项:它执行了多少次算术运算,以及它需要从主内存中读取多少字节才能执行这些运算。
用前者除以后者。这个比率就是该操作的每字节工作量,它可以在你进行任何测量之前预测你的性能。
它的正式名称是算术强度,你会看到它以这种方式使用。其核心思想很简单:每次从内存中读取数据能提取多少价值。
两端的操作
真正有用的问题不是操作读取了多少字节,而是它使用了读取到的每个值的多少次。
将数组中的每个元素乘以 2,每个值都只使用一次。取出元素,乘以 2,写回,之后就无需再访问它。每个取出的值只执行一次操作。
两个矩阵相乘则相反。在一个 1024 x 1024 的矩阵乘法中,从第一个矩阵中取出的每个值都会与第二个矩阵中的 1024 个不同的值相乘。一次取出,却要执行上千次操作。
单次取出带来的收益是上千倍的差异,而这仅仅是由于数据重用造成的。矩阵乘法是加速器真正擅长的运算之一,原因就在于此。
盈亏平衡点
每个芯片都有一个阈值,这个阈值可以通过一个简单的除法得出。
用芯片的峰值运算速率除以其峰值内存带宽。这样就能得到每字节运算量,在该数值下,两者正好达到平衡。
对于当前运行 16 位精度的数据中心 GPU 而言,这个数值约为每字节 300 次运算。
以H100 SXM5为例,它是大多数人租用的主力CPU。其数据手册显示,它拥有989 TFLOPS的密集BF16吞吐量和3.35 TB/s的内存带宽。
将两者相除,每秒989万亿次运算除以每秒3.35万亿字节,得出每字节295次运算,这就是300的由来。
在进行任何性能分析之前,这个数字本身就蕴含着很多信息。无论代码编写得多么出色,如果一个操作的每次读取字节的运算次数少于295次,就无法充分利用芯片的全部性能。
这个阈值也会随着硬件的升级而变化,但变化的方向可能与你预期的不同。H200使用了与H100相同的计算芯片,因此其算术性能上限仍然是989 TFLOPS,但其带宽提升至4.8 TB/s。这使得阈值降至每字节206次运算。
更低的阈值是一件好事。这意味着更多工作负载能够突破瓶颈,转而受计算能力限制,因此即使不增加额外的运算能力,带宽升级也能加快推理速度。
低于 300 时,性能受限于内存。增加运算能力并不会改变任何结果,因为运算单元已经处于待命状态。
高于 300 时,性能受限于运算能力。增加带宽并不会改变任何结果,因为数据到达的速度已经超过了处理速度。
性能阈值取决于硬件。而工作负载相对于该阈值的位置则取决于您自身。
这张图的名称是屋顶线模型。了解这个术语很重要,因为它是性能工程师讨论这类问题的标准框架,而且它正是上文所述的“双顶”概念。
为什么生成一个词元是最糟糕的情况?
将这个框架应用于语言模型推理,最初的难题就迎刃而解了。
文本生成是逐个词元进行的。每个新词元都需要模型完整地前向传播一次。每次前向传播都会读取模型中的每个权重一次。
首先,我们来看看单个权重实际执行的操作。
权重位于两个数字之间。它会乘以一个输入值,并将结果加到运行总和中,最终成为输出的一部分。
这就是权重的全部工作。一次乘法,一次加法,所以每次使用都会执行两次操作。
现在,让我们把规模扩大。一个拥有 700 亿个参数的模型大约需要执行 1400 亿次操作才能生成一个词元,因为每个权重都会执行两次操作。
接下来,我们来计算一下字节数。以 16 位精度表示,每个权重占用 2 个字节,因此读取所有权重意味着要移动 140 GB。
把这两点结合起来。1400 亿次运算由 140 GB 的流量驱动,相当于每字节一次运算。
盈亏平衡点大约在 300 左右。处理单个请求的运算量大约比这个阈值低三百倍。
这就是为什么运算单元处于空闲状态。你的设置没有问题。只是运算本身的工作量不足以维持它们的运行。
这个数字实际预测的是
一旦你接受了生成速度受内存限制这一事实,你的令牌速率就不再神秘,而变成了一个你可以在几秒钟内自己计算出来的公式。
一个 700 亿参数的 16 位精度模型大约占用 140 GB 的空间。高端 GPU 每秒从主内存读取大约 3.3 TB 的数据。
这可以得出两个数字和一个除法运算的结果。140 GB 的权重,以及每秒到达的 3300 GB 数据,这意味着读取一次权重需要 0.042 秒。假设是 42 毫秒。这是生成一个 token 所需的时间,因为在批处理大小为 1 的情况下,完整读取一次权重恰好等于生成一个 token 所需的时间。
现在把它转换成速率。如果每个 token 需要 42 毫秒,那么一秒钟可以生成 1000 除以 42 个 token,大约是 24 个。
所以大约每秒可以生成 24 个 token,而且无论软件多么巧妙,这个下限都无法改变。
下限取决于每个 token 需要读取的字节数除以字节到达的速度。其他的一切都只是细节。
预填充的行为方式恰恰相反
还有第二个阶段,它与上述情况截然不同。
当你发送一个提示时,模型会在生成任何内容之前一次性处理所有标记。这个阶段称为预填充,由于它同时处理多个标记,因此获取的每个权重都会被所有标记使用。
每字节的工作量会立即增加。预填充通常是计算密集型的,这就是为什么长提示和长输出相比,其性能消耗感觉不同的原因。
同一个模型有两个阶段,它们的瓶颈相反。性能讨论中的许多困惑都源于将它们视为单一因素。
你听说过的所有优化方法都改变着相同的比例
这就是框架的优势所在。一旦你将性能视为一个单一的比例,这些技术就不再像是一系列互不相关的技巧。
你只能改变两件事:增加每次获取的数据量,或者减少获取的字节数。以下每项技术都只改变其中之一。
批处理会增加工作量
同时处理多个请求,你只需读取每个权重一次,然后将其用于所有请求。
十个并发请求意味着每个字节的工作量增加十倍,而无需额外的内存开销。没有任何其他方法可以如此显著地提高服务吞吐量,而它唯一的代价是每个用户略微增加一些延迟。
盈亏平衡点的计算值得记住。在 16 位精度下,大约需要 300 个并发序列,生成才会达到计算瓶颈。低于这个数量,就会有多余的计算能力被浪费掉,这正是服务器系统努力保持批处理数据满载的原因。
融合操作减少字节数
链式逐元素运算是最明显的例子。
分别执行三个运算,每个运算都会从主内存读取输入并将输出写回。这意味着要进行六次内存往返,而实际耗时几乎可以忽略不计。
将它们融合为一个运算,中间值就无需离开芯片。一次读取,一次写入,运算结果完全相同。内存流量减少了三分之二。
这就是为什么支持运算融合的编译器在包含大量逐元素运算的模型上能显著提升速度,而对于纯矩阵乘法却无济于事的原因。
保持数据靠近内存也能减少字节数
共享内存正是为此而生的。
将一块数据加载到共享内存中,执行所有需要用到它的计算,然后继续执行下一个操作。只需支付一次费用,即可多次使用。
FlashAttention 就是一个著名的例子。普通的注意力机制会构建一个大型中间矩阵,将其写入主内存,然后再将其读回。 FlashAttention 将注意力机制以图块的形式计算,这些图块存储在片上内存中,因此中间数据完全不会访问主内存。
运算基本保持不变。内存流量大幅减少,运行时间也随之缩短。
量化直接减少了字节数
将权重存储在 8 位而不是 16 位,每个权重的大小都减半。
这使得每个标记需要读取的字节数减半,从而使每个字节的工作量翻倍,并大致将生成上限翻倍。同样的 700 亿参数模型的大小从 140 GB 降至 70 GB,理论最低速度从每秒约 24 个标记提高到约 48 个标记。
然而,代价是精度降低。精度降低会丢失信息,而损失多少很大程度上取决于模型和量化方法。这是一个真正的工程决策,并非唾手可得。
按顺序读取比想象中更重要
即使你从未编写过内核,也值得了解一个访问模式的细节。
内存并非一次只移动一个值。硬件读取的是固定大小的数据块。当线程束中的线程读取相邻地址时,它们的请求都集中在几个相同的数据块内,每个读取到的字节都会被使用,从而保证了你付费的带宽。
而当它们读取分散的地址时,每个线程会拉取一个完整的数据块,却只使用其中的几个字节。最终,你读取的数据量可能是实际所需数据的八倍。
实际结果是,内存布局本身就是一个性能决策。例如,如果沿错误的轴读取矩阵,或者使用分散存储相关值的数据结构,即使运算结果没有改变,也可能消耗掉大部分带宽。
小任务有其自身的性能下限
有一个瓶颈无法通过比率来衡量。
将工作发送到 GPU 会产生固定成本。CPU 必须准备和调度每个任务,无论任务大小,这都需要时间。
如果你的模型运行许多小操作,最终可能会花费更多时间在调度上而不是计算上。此时,你既不是内存瓶颈也不是计算瓶颈,而是开销瓶颈。
症状是 GPU 看起来空闲,而 CPU 却在努力工作。解决方法是减少操作数量并增大操作规模,或者捕获整个操作序列,以便将其作为一个单元重放。
如何判断你处于哪种情况?
只有当你能够确定自己处于哪种情况时,该框架才有用,而这有一个简单的方法。
在运行过程中测量两项指标:每秒实际从主内存移动的字节数,以及每秒实际执行的算术运算次数。将这两项指标与硬件的性能进行比较。
三种结果几乎涵盖了所有情况。
带宽接近上限,但运算能力远低于上限。您受限于内存。减少移动的字节数。提高批处理效率、量化、融合、修复布局。增加运算能力并无帮助。
运算能力接近上限,但带宽远低于上限。您受限于计算能力。这是一个不错的状态。现在的改进来自于降低精度、改进算法或提升硬件性能。
两者都远低于上限。您受限于开销,或者您的工作量太小,无法填满芯片。查看您启动了多少个独立的操作以及每个操作的大小。
注意首先应该检查哪里。对于语言模型服务,在几乎所有实际配置中,生成过程都受限于内存。如果您正在优化标记吞吐量,但尚未检查批处理大小、精度和键值缓存大小,那么这三者几乎总是会主导您尝试的任何其他方法。
硬件变化时哪些内容保持不变
规格变化很快。值得了解哪些部分需要重新学习,哪些部分不需要。
变化的是数值。内存容量、带宽和算术吞吐量都会随着每一代硬件的迭代而增长,其中算术吞吐量的增长速度最快。新的精度格式层出不穷,而且精度越来越低,因为缩小字节大小是突破瓶颈最直接的方法。
不变的是性能的构成。算术运算的成本仍然远低于数据传输的成本。内存容量的上限保持不变。每字节的运算量决定了性能的极限。
事实上,随着时间的推移,这套框架变得更加实用。由于算术能力的增长速度超过了带宽,盈亏平衡点也在不断提高。在旧硬件上受计算限制的工作负载,在新硬件上无需任何代码更改,就会变成受内存限制的工作负载。
这才是值得关注的方向。减少数据移动的技术将持续获得价值,而规格表上的峰值算术性能数据将始终是文档中最不具预测性的数据。
结论
在本文中,我们基于一个简单的不对称性——算术运算成本低,而数据传输成本高——构建了对GPU工作原理的直观理解。我们明白了为什么这种不对称性会导致芯片上集成数千个简单的算术单元,它们共享一个控制器,而不是几个复杂的算术单元。我们发现,GPU 并没有缩短数据等待时间,而是通过加载远超其实际运行能力的工作量,并在数据块之间进行瞬时交换,来隐藏等待时间。
我们沿着内存层级一路向下,从寄存器文件到共享内存和 L2 缓存,直至 HBM,每一层都比上一层更大,也更难访问。我们还了解了这些内存层级的位置:最上面的两层由每个 SM 私有,而下面的所有内存要么在整个芯片上共享,要么完全位于芯片之外。
这让我们得到了一个核心指标,即每个字节的运算量。每个芯片都有一个盈亏平衡点,在当前硬件上,以 16 位精度计算,大约是每个字节 300 次运算。低于这个值,性能受限于内存;高于这个值,性能受限于算术运算能力。
将这个指标应用于语言模型,就解释了最初的难题。生成令牌时,需要读取每个权重并对其执行大约两次操作,操作比率接近于 1,大约比盈亏平衡点低三百倍。预填充则完全相反,这就是为什么这两个阶段的行为如此不同。
最后,我们发现所有知名的优化都属于以下两种操作之一:批处理和分块会增加每次获取操作的工作量;融合、量化和更优的内存布局则会减少每次获取的字节数。
我们特意省略了编写 GPU 代码的底层细节,因为直觉必须先行。在此框架之前编写的内核代码往往会产生速度很快但作者无法解释的原因,以及速度很慢但作者找不到原因的代码。
(来源:内容来自半导体行业观察综合 )
上一篇:“卷”出来的银行中收数据? 有基层员工自掏腰包买保险、基金和理财
下一篇:长鑫天量IPO之后,又来长江,存储的好日子还有多久?长飞和光讯半年报差异巨大,光的后劲在哪里?黄金比特币为何强势大涨?