动手学深度学习12.4.硬件-笔记&练习(PyTorch)

以下内容为结合李沐老师的课程和教材补充的学习笔记,以及对课后练习的一些思考,自留回顾,也供同学之人交流参考。

本节课程地址:31 深度学习硬件:CPU 和 GPU【动手学深度学习v2】_哔哩哔哩_bilibili

本节教材地址:12.4. 硬件 --- 动手学深度学习 2.0.0 documentation

本节开源代码:...>d2l-zh>pytorch>chapter_optimization>hardware.ipynb


硬件

很好地理解算法和模型才可以捕获统计方面的问题,构建出具有出色性能的系统。同时,至少对底层硬件有一定的了解也是必不可少的。本节不能替代硬件和系统设计的相关课程。相反,本节的内容可以作为理解某些算法为什么比其他算法更高效以及如何实现良好吞吐量的起点。一个好的设计可以很容易地在性能上造就数量级的差异,这也是后续产生的能够训练网络(例如,训练时间为1周)和无法训练网络(训练时间为3个月,导致错过截止期)之间的差异。我们先从计算机的研究开始。然后深入查看CPU和GPU。最后,再查看数据中心或云中的多台计算机的连接方式。

也可以通过 图12.4.1 进行简单的了解,图片源自科林·斯科特的互动帖子,在帖子中很好地概述了过去十年的进展。原始的数字是取自于杰夫迪恩的Stanford讲座。下面的讨论解释了这些数字的一些基本原理,以及它们如何指导我们去设计算法。下面的讨论是非常笼统和粗略的。很显然,它并不能代替一门完整的课程,而只是为了给统计建模者提供足够的信息,让他们做出合适的设计决策。对于计算机体系结构的深入概述,建议读者参考 (Hennessy and Patterson, 2011) 或关于该主题的最新课程,例如Arste Asanovic

计算机

大多数深度学习研究者和实践者都可以使用一台具有相当数量的内存、计算资源、某种形式的加速器(如一个或者多个GPU)的计算机。计算机由以下关键部件组成:

  • 一个处理器(也被称为CPU),它除了能够运行操作系统和许多其他功能之外,还能够执行给定的程序。它通常由8个或更多个核心组成;
  • 内存(随机访问存储,RAM)用于存储和检索计算结果,如权重向量和激活参数,以及训练数据;
  • 一个或多个以太网连接,速度从1GB/s到100GB/s不等。在高端服务器上可能用到更高级的互连;
  • 高速扩展总线(PCIe)用于系统连接一个或多个GPU。服务器最多有8个加速卡,通常以更高级的拓扑方式连接,而桌面系统则有1个或2个加速卡,具体取决于用户的预算和电源负载的大小;
  • 持久性存储设备,如磁盘驱动器、固态驱动器,在许多情况下使用高速扩展总线连接。它为系统需要的训练数据和中间检查点需要的存储提供了足够的传输速度。

如 图12.4.2 所示,高速扩展总线由直接连接到CPU的多个通道组成,将CPU与大多数组件(网络、GPU和存储)连接在一起。例如,AMD的Threadripper3有64个PCIe4.0通道,每个通道都能够双向传输16Gbit/s的数据。内存直接连接到CPU,总带宽高达100GB/s。

当我们在计算机上运行代码时,需要将数据转移到处理器上(CPU或GPU)执行计算,然后将结果从处理器移回到随机访问存储和持久存储器中。因此,为了获得良好的性能,需要确保每一步工作都能无缝链接,而不希望系统中的任何一部分成为主要的瓶颈。例如,如果不能快速加载图像,那么处理器就无事可做。同样地,如果不能快速移动矩阵到CPU(或GPU)上,那么CPU(或GPU)就会无法全速运行。最后,如果希望在网络上同步多台计算机,那么网络就不应该拖累计算速度。一种选择是通信和计算交错进行。接下来将详细地介绍各个组件。

内存

最基本的内存主要用于存储需要随时访问的数据。目前,CPU的内存通常为DDR4类型,每个模块提供20-25Gb/s的带宽。每个模块都有一条64位宽的总线。通常使用成对的内存模块来允许多个通道。CPU有2到4个内存通道,也就是说,它们内存带宽的峰值在40GB/s到100GB/s之间。一般每个通道有两个物理存储体(bank)。例如AMD的Zen 3 Threadripper有8个插槽。

虽然这些数字令人印象深刻,但实际上它们只能说明了一部分故事。当我们想要从内存中读取一部分内容时,需要先告诉内存模块在哪里可以找到信息。也就是说,我们需要先将地址 (address)发送到RAM。然后我们可以选择只读取一条64位记录还是一长串记录。后者称为突发读取(burst read)。概括地说,向内存发送地址并设置传输大约需要100ns(细节取决于所用内存芯片的特定定时系数),每个后续传输只需要0.2ns。总之,第一次读取的成本是后续读取的500倍!请注意,每秒最多可以执行一千万次随机读取。这说明应该尽可能地避免随机内存访问,而是使用突发模式读取和写入。

当考虑到拥有多个物理存储体时,事情就更加复杂了。每个存储体大部分时候都可以独立地读取内存。这意味着两件事。一方面,如果随机读操作均匀分布在内存中,那么有效的随机读操作次数将高达4倍。这也意味着执行随机读取仍然不是一个好主意,因为突发读取的速度也快了4倍。另一方面,由于内存对齐是64位边界,因此最好将任何数据结构与相同的边界对齐。当设置了适当的标志时,编译器基本上就是自动化地执行对齐操作。我们鼓励好奇的读者回顾一下Zeshan Chishti关于DRAM的讲座

GPU内存的带宽要求甚至更高,因为它们的处理单元比CPU多得多。总的来说,解决这些问题有两种选择。首先是使内存总线变得更宽。例如,NVIDIA的RTX 2080Ti有一条352位宽的总线。这样就可以同时传输更多的信息。其次,GPU使用特定的高性能内存。消费级设备,如NVIDIA的RTX和Titan系列,通常使用GDDR6模块。它们使用截然不同的接口,直接与专用硅片上的GPU连接。这使得它们非常昂贵,通常仅限于高端服务器芯片,如NVIDIA Volta V100系列加速卡。毫不意外的是GPU的内存通常比CPU的内存小得多,因为前者的成本更高。就目的而言,它们的性能与特征大体上是相似的,只是GPU的速度更快。就本书而言,我们完全可以忽略细节,因为这些技术只在调整GPU核心以获得高吞吐量时才起作用。

存储器

随机访问存储的一些关键特性是 带宽 (bandwidth)和 延迟(latency)。存储设备也是如此,只是不同设备之间的特性差异可能更大。

硬盘驱动器

硬盘驱动器(hard disk drive,HDD)已经使用了半个多世纪。简单的说,它们包含许多旋转的盘片,这些盘片的磁头可以放置在任何给定的磁道上进行读写。高端磁盘在9个盘片上可容纳高达16TB的容量。硬盘的主要优点之一是相对便宜,而它们的众多缺点之一是典型的灾难性故障模式和相对较高的读取延迟。

要理解后者,请了解一个事实即硬盘驱动器的转速大约为7200RPM(每分钟转数)。它们如果转速再快些,就会由于施加在碟片上的离心力而破碎。在访问磁盘上的特定扇区时,还有一个关键问题:需要等待碟片旋转到位(可以移动磁头,但是无法对磁盘加速)。因此,可能需要8毫秒才能使用请求的数据。一种常见的描述方式是,硬盘驱动器可以以大约100IOPs(每秒输入/输出操作)的速度工作,并且在过去二十年中这个数字基本上没变。同样糟糕的是,带宽(大约为100-200MB/s)也很难增加。毕竟,每个磁头读取一个磁道的比特,因此比特率只随信息密度的平方根缩放。因此,对于非常大的数据集,HDD正迅速降级为归档存储和低级存储。

固态驱动器

固态驱动器(solid state drives,SSD)使用闪存持久地存储信息。这允许更快地访问存储的记录。现代的固态驱动器的IOPs可以达到10万到50万,比硬盘驱动器快3个数量级。而且,它们的带宽可以达到1-3GB/s,比硬盘驱动器快一个数量级。这些改进听起来好的难以置信,而事实上受固态驱动器的设计方式,它仍然存在下面的附加条件。

  • 固态驱动器以块的方式(256KB或更大)存储信息。块只能作为一个整体来写入,因此需要耗费大量的时间,导致固态驱动器在按位随机写入时性能非常差。而且通常数据写入需要大量的时间还因为块必须被读取、擦除,然后再重新写入新的信息。如今固态驱动器的控制器和固件已经开发出了缓解这种情况的算法。尽管有了算法,写入速度仍然会比读取慢得多,特别是对于QLC(四层单元)固态驱动器。提高性能的关键是维护操作的"队列",在队列中尽可能地优先读取和写入大的块。
  • 固态驱动器中的存储单元磨损得比较快(通常在几千次写入之后就已经老化了)。磨损程度保护算法能够将退化平摊到许多单元。也就是说,不建议将固态驱动器用于交换分区文件或大型日志文件。
  • 最后,带宽的大幅增加迫使计算机设计者将固态驱动器与PCIe总线相连接,这种驱动器称为NVMe(非易失性内存增强),其最多可以使用4个PCIe通道。在PCIe4.0上最高可达8GB/s。

云存储

云存储提供了一系列可配置的性能。也就是说,虚拟机的存储在数量和速度上都能根据用户需要进行动态分配。建议用户在延迟太高时(例如,在训练期间存在许多小记录时)增加IOPs的配置数。

CPU

中央处理器(central processing unit,CPU)是任何计算机的核心。它们由许多关键组件组成:处理器核心 (processor cores)用于执行机器代码的;总线 (bus)用于连接不同组件(注意,总线会因为处理器型号、各代产品和供应商之间的特定拓扑结构有明显不同);缓存 (cach)相比主内存实现更高的读取带宽和更低的延迟内存访问。最后,因为高性能线性代数和卷积运算常见于媒体处理和机器学习中,所以几乎所有的现代CPU都包含向量处理单元(vector processing unit)为这些计算提供辅助。

图12.4.3 描述了Intel Skylake消费级四核CPU。它包含一个集成GPU、缓存和一个连接四个核心的环总线。例如,以太网、WiFi、蓝牙、SSD控制器和USB这些外围设备要么是芯片组的一部分,要么通过PCIe直接连接到CPU。

微体系结构

每个处理器核心都由一组相当复杂的组件组成。虽然不同时代的产品和供应商的细节有所不同,但基本功能都是标准的。前端加载指令并尝试预测将采用哪条路径(例如,为了控制流),然后将指令从汇编代码解码为微指令。汇编代码通常不是处理器执行的最低级别代码,而复杂的微指令却可以被解码成一组更低级的操作,然后由实际的执行核心处理。通常执行核心能够同时执行许多操作,例如, 图12.4.4 的ARM Cortex A77核心可以同时执行多达8个操作。

这意味着高效的程序可以在每个时钟周期内执行多条指令,前提是这些指令可以独立执行。不是所有的处理单元都是平等的。一些专用于处理整数指令,而另一些则针对浮点性能进行了优化。为了提高吞吐量,处理器还可以在分支指令中同时执行多条代码路径,然后丢弃未选择分支的结果。这就是为什么前端的分支预测单元很重要,因为只有最有希望的路径才会被继续执行。

矢量化

深度学习的计算量非常大。因此,为了满足机器学习的需要,CPU需要在一个时钟周期内执行许多操作。这种执行方式是通过向量处理单元实现的。这些处理单元有不同的名称:在ARM上叫做NEON,在x86上被称为AVX2。一个常见的功能是它们能够执行单指令多数据(single instruction multiple data,SIMD)操作。 图12.4.5 显示了如何在ARM上的一个时钟周期中完成8个整数加法。

根据体系结构的选择,此类寄存器最长可达512位,最多可组合64对数字。例如,我们可能会将两个数字相乘,然后与第三个数字相加,这也称为乘加融合(fused multiply-add)。Intel的OpenVino就是使用这些处理器来获得可观的吞吐量,以便在服务器级CPU上进行深度学习。不过请注意,这个数字与GPU的能力相比则相形见绌。例如,NVIDIA的RTX 2080Ti拥有4352个CUDA核心,每个核心都能够在任何时候处理这样的操作。

缓存

考虑以下情况:我们有一个中等规模的4核心的CPU,如 图12.4.3 所示,运行在2GHz频率。此外,假设向量处理单元启用了256位带宽的AVX2,其IPC(指令/时钟)计数为1。进一步假设从内存中获取用于AVX2操作的指令至少需要一个寄存器。这意味着CPU每个时钟周期需要消耗 4×256 bit=128 bytes 的数据。除非我们能够每秒向处理器传输 2×109×128=256×109 字节,否则用于处理的数据将会不足。不幸的是,这种芯片的存储器接口仅支持20-40Gb/s的数据传输,即少了一个数量级。解决方法是尽可能避免从内存中加载新数据,而是将数据放在CPU的缓存上。这就是使用缓存的地方。通常使用以下名称或概念。

  • 寄存器 ,严格来说不是缓存的一部分,用于帮助组织指令。也就是说,寄存器是CPU可以以时钟速度访问而没有延迟的存储位置。CPU有几十个寄存器,因此有效地使用寄存器取决于编译器(或程序员)。例如,C语言有一个register关键字。
  • 一级缓存是应对高内存带宽要求的第一道防线。一级缓存很小(常见的大小可能是32-64KB),内容通常分为数据和指令。当数据在一级缓存中被找到时,其访问速度非常快,如果没有在那里找到,搜索将沿着缓存层次结构向下寻找。
  • 二级缓存是下一站。根据架构设计和处理器大小的不同,它们可能是独占的也可能是共享的。即它们可能只能由给定的核心访问,或者在多个核心之间共享。二级缓存比一级缓存大(通常每个核心256-512KB),而速度也更慢。此外,我们首先需要检查以确定数据不在一级缓存中,才会访问二级缓存中的内容,这会增加少量的额外延迟。
  • 三级缓存在多个核之间共享,并且可以非常大。AMD的EPYC 3服务器的CPU在多个芯片上拥有高达256MB的高速缓存。更常见的数字在4-8MB范围内。

预测下一步需要哪个存储设备是优化芯片设计的关键参数之一。例如,建议以向前 的方向遍历内存,因为大多数缓存算法将试图向前读取(read forward)而不是向后读取。同样,将内存访问模式保持在本地也是提高性能的一个好方法。

添加缓存是一把双刃剑。一方面,它能确保处理器核心不缺乏数据。但同时,它也增加了芯片尺寸,消耗了原本可以用来提高处理能力的面积。此外,缓存未命中 的代价可能会很昂贵。考虑最坏的情况,如 图12.4.6 所示的错误共享(false sharing)。当处理器1上的线程请求数据时,内存位置缓存在处理器0上。为了满足获取需要,处理器0需要停止它正在做的事情,将信息写回主内存,然后让处理器1从内存中读取它。在此操作期间,两个处理器都需要等待。与高效的单处理器实现相比,这种代码在多个处理器上运行的速度可能要慢得多。这就是为什么缓存大小(除了物理大小之外)有实际限制的另一个原因。

GPU和其他加速卡

毫不夸张地说,如果没有GPU,深度学习就不会成功。基于同样的原因,有理由认为GPU制造商的财富由于深度学习而显著增加。这种硬件和算法的协同进化导致了这样一种情况:无论好坏,深度学习都是更可取的统计建模范式。因此,了解GPU和其他加速卡(如TPU (Jouppi et al., 2017))的具体好处是值得的。

值得注意的是,在实践中经常会有这样一个判别:加速卡是为训练还是推断而优化的。对于后者,我们只需要计算网络中的前向传播。而反向传播不需要存储中间数据。还有,我们可能不需要非常精确的计算(FP16或INT8通常就足够了)。对于前者,即训练过程中需要存储所有的中间结果用来计算梯度。而且,累积梯度也需要更高的精度,以避免数值下溢(或溢出)。这意味着最低要求也是FP16(或FP16与FP32的混合精度)。所有这些都需要更快、更大的内存(HBM2或者GDDR6)和更高的处理能力。例如,NVIDIA优化了Turing T4 GPU用于推断和V100 GPU用于训练。

回想一下如 图12.4.5 所示的矢量化。处理器核心中添加向量处理单元可以显著提高吞吐量。例如,在 图12.4.5 的例子中,我们能够同时执行16个操作。首先,如果我们添加的运算不仅优化了向量运算,而且优化了矩阵运算,会有什么好处?稍后我们将讨论基于这个策略引入的张量核(tensor cores)。第二,如果我们增加更多的核心呢?简而言之,以上就是GPU设计决策中的两种策略。 图12.4.7 给出了基本处理块的概述。它包含16个整数单位和16个浮点单位。除此之外,两个张量核加速了与深度学习相关的附加操作的狭窄的子集。每个流式多处理器都由这样的四个块组成。

接下来,将12个流式多处理器分组为图形处理集群,这些集群构成了高端TU102处理器。充足的内存通道和二级缓存完善了配置。 图12.4.8 有相关的细节。设计这种设备的原因之一是可以根据需要独立地添加或删除模块,从而满足设计更紧凑的芯片和处理良品率问题(故障模块可能无法激活)的需要。幸运的是,在CUDA和框架代码层之下,这类设备的编程对深度学习的临时研究员隐藏得很好。特别是,只要有可用的资源GPU上就可以同时执行多个程序。尽管如此,了解设备的局限性是值得的,以避免对应的设备内存的型号不合适。

最后值得一提的是张量核 (tensor core)。它们是最近增加更多优化电路趋势的一个例子,这些优化电路对深度学习特别有效。例如,TPU添加了用于快速矩阵乘法的脉动阵列 (Kung, 1988),这种设计是为了支持非常小数量(第一代TPU支持数量为1)的大型操作。而张量核是另一个极端。它们针对 4×4 和 16×16 矩阵之间的小型运算进行了优化,具体取决于它们的数值精度。 图12.4.9 给出了优化的概述。

显然,我们最终会在优化计算时做出某些妥协。其中之一是GPU不太擅长处理稀疏数据和中断。尽管有一些明显的例外,如Gunrock (Wang et al., 2016),但GPU擅长的高带宽突发读取操作并不适合稀疏的矩阵和向量的访问模式。访问稀疏数据和处理中断这两个目标是一个积极研究的领域。例如:DGL,一个专为图深度学习而设计的库。

网络和总线

每当单个设备不足以进行优化时,我们就需要来回传输数据以实现同步处理,于是网络和总线就派上了用场。我们有许多设计参数:带宽、成本、距离和灵活性。应用的末端有WiFi,它有非常好的使用范围,非常容易使用(毕竟没有线缆),而且还便宜,但它提供的带宽和延迟相对一般。头脑正常的机器学习研究人员都不会用它来构建服务器集群。接下来的内容中将重点关注适合深度学习的互连方式。

  • PCIe,一种专用总线,用于每个通道点到点连接的高带宽需求(在16通道插槽中的PCIe4.0上高达32GB/s),延迟时间为个位数的微秒(5μs)。PCIe链接非常宝贵。处理器拥有的数量:AMD的EPYC 3有128个通道,Intel的Xeon每个芯片有48个通道;在桌面级CPU上,数字分别是20(Ryzen9)和16(Core i9)。由于GPU通常有16个通道,这就限制了以全带宽与CPU连接的GPU数量。毕竟,它们还需要与其他高带宽外围设备(如存储和以太网)共享链路。与RAM访问一样,由于减少了数据包的开销,因此更适合大批量数据传输。
  • 以太网 ,连接计算机最常用的方式。虽然它比PCIe慢得多,但它的安装成本非常低,而且具有很强的弹性,覆盖的距离也要长得多。低级服务器的典型带宽为1GBit/s。高端设备(如云中的C5实例。这进一步增加了开销。与PCIe类似,以太网旨在连接两个设备,例如计算机和交换机。
  • 交换机 ,一种连接多个设备的方式,该连接方式下的任何一对设备都可以同时执行(通常是全带宽)点对点连接。例如,以太网交换机可能以高带宽连接40台服务器。请注意,交换机并不是传统计算机网络所独有的。甚至PCIe通道也可以是可交换的,例如:P2实例就是将大量GPU连接到主机处理器。
  • NVLink ,是PCIe的替代品,适用于非常高带宽的互连。它为每条链路提供高达300Gbit/s的数据传输速率。服务器GPU(Volta V100)有六个链路。而消费级GPU(RTX 2080Ti)只有一个链路,运行速度也降低到100Gbit/s。建议使用NCCL来实现GPU之间的高速数据传输。

更多延迟

表12.4.1 和 表12.4.2 中的小结来自Eliot Eshelman,他们将数字的更新版本保存到GitHub gist

表12.4.1 常见延迟。

Action Time Notes
L1 cache reference/hit 1.5 ns 4 cycles
Floating-point add/mult/FMA 1.5 ns 4 cycles
L2 cache reference/hit 5 ns 12 ~ 17 cycles
Branch mispredict 6 ns 15 ~ 20 cycles
L3 cache hit (unshared cache) 16 ns 42 cycles
L3 cache hit (shared in another core) 25 ns 65 cycles
Mutex lock/unlock 25 ns
L3 cache hit (modified in another core) 29 ns 75 cycles
L3 cache hit (on a remote CPU socket) 40 ns 100 ~ 300 cycles (40 ~ 116 ns)
QPI hop to a another CPU (per hop) 40 ns
64MB memory ref. (local CPU) 46 ns TinyMemBench on Broadwell E5-2690v4
64MB memory ref. (remote CPU) 70 ns TinyMemBench on Broadwell E5-2690v4
256MB memory ref. (local CPU) 75 ns TinyMemBench on Broadwell E5-2690v4
Intel Optane random write 94 ns UCSD Non-Volatile Systems Lab
256MB memory ref. (remote CPU) 120 ns TinyMemBench on Broadwell E5-2690v4
Intel Optane random read 305 ns UCSD Non-Volatile Systems Lab
Send 4KB over 100 Gbps HPC fabric 1 μs MVAPICH2 over Intel Omni-Path
Compress 1KB with Google Snappy 3 μs
Send 4KB over 10 Gbps ethernet 10 μs
Write 4KB randomly to NVMe SSD 30 μs DC P3608 NVMe SSD (QOS 99% is 500μs)
Transfer 1MB to/from NVLink GPU 30 μs ~33GB/s on NVIDIA 40GB NVLink
Transfer 1MB to/from PCI-E GPU 80 μs ~12GB/s on PCIe 3.0 x16 link
Read 4KB randomly from NVMe SSD 120 μs DC P3608 NVMe SSD (QOS 99%)
Read 1MB sequentially from NVMe SSD 208 μs ~4.8GB/s DC P3608 NVMe SSD
Write 4KB randomly to SATA SSD 500 μs DC S3510 SATA SSD (QOS 99.9%)
Read 4KB randomly from SATA SSD 500 μs DC S3510 SATA SSD (QOS 99.9%)
Round trip within same datacenter 500 μs One-way ping is ~250μs
Read 1MB sequentially from SATA SSD 2 ms ~550MB/s DC S3510 SATA SSD
Read 1MB sequentially from disk 5 ms ~200MB/s server HDD
Random Disk Access (seek+rotation) 10 ms
Send packet CA->Netherlands->CA 150 ms
:label:table_latency_numbers

表12.4.2 NVIDIA Tesla GPU的延迟.

Action Time Notes
GPU Shared Memory access 30 ns 30~90 cycles (bank conflicts add latency)
GPU Global Memory access 200 ns 200~800 cycles
Launch CUDA kernel on GPU 10 μs Host CPU instructs GPU to start kernel
Transfer 1MB to/from NVLink GPU 30 μs ~33GB/s on NVIDIA 40GB NVLink
Transfer 1MB to/from PCI-E GPU 80 μs ~12GB/s on PCI-Express x16 link
:label:table_latency_numbers_tesla

小结

  • 设备有运行开销。因此,数据传输要争取量大次少而不是量少次多。这适用于RAM、固态驱动器、网络和GPU。
  • 矢量化是性能的关键。确保充分了解加速器的特定功能。例如,一些Intel Xeon CPU特别适用于INT8操作,NVIDIA Volta GPU擅长FP16矩阵操作,NVIDIA Turing擅长FP16、INT8和INT4操作。
  • 在训练过程中数据类型过小导致的数值溢出可能是个问题(在推断过程中则影响不大)。
  • 数据混叠现象会导致严重的性能退化。64位CPU应该按照64位边界进行内存对齐。在GPU上建议保持卷积大小对齐,例如:与张量核对齐。
  • 将算法与硬件相匹配(例如,内存占用和带宽)。将命中参数装入缓存后,可以实现很大数量级的加速比。
  • 在验证实验结果之前,建议先在纸上勾勒出新算法的性能。关注的原因是数量级及以上的差异。
  • 使用调试器跟踪调试寻找性能的瓶颈。
  • 训练硬件和推断硬件在性能和价格方面有不同的优点。

补充:

1. 提升CPU利用率

  • 在计算a+b之前,需要准备数据
    • 主内存 -> L3 -> L2 -> L1 -> 寄存器
      • L1访问延时:0.5 ns
      • L2访问延时:7 ns(14x L1)
      • 主内存访问延时:100 ns (200x L1)
  • 提升空间和时间的内存本地性
    • 时间:重用数据使得保持它们在缓存里
    • 空间:按序读写数据使得可以预读取
  • 举例:如果一个矩阵是按行存储的,那么访问一行会比访问一列要快。
  • 高端CPU有几十个核,并行来利用所有核,可以提升CPU利用率。
    • 超线程不一定提升性能,因为它们共享寄存器(Intel会用超线程将4个CPU处理成8个核,这种情况下4线程和8线程的计算性能差别不大)。

2. CPU vs GPU

CPU(一般/高端) GPU(一般/高端)
6/64 2K/4K
TFLOPS 0.2/1 10/100
内存大小 32 GB/ 1 TB 16 GB/ 32 GB
内存带宽 30 GB/s / 100 GB/s 40 GB/s / 1 TB/s
控制流

3. 提升GPU利用率

  • 并行:使用数千个线程
  • 内存本地性:缓存更小,架构更加简单
  • 少用控制语句:
    • 支持有限
    • 同步开销很大

4. CPU/GPU带宽

  • 不要频繁在CPU和GPU之间传数据:带宽限制、同步开销

5. CPU/GPU高性能计算编程

  • CPU:C++或者任何高性能语言
    • 编译器成熟
  • GPU:
    • Nvidia上用CUDA
      • 编译器和驱动成熟
    • 其他用OpenCL
      • 质量取决于硬件厂商

6. 总结

  • CPU:可以处理通用计算。性能优化考虑数据读写效率和多线程。
  • GPU:使用更多的小核和更好的内存带宽,适合能大规模并行的计算任务。

练习

  1. 编写C语言来测试访问对齐的内存和未对齐的内存之间的速度是否有任何差异。(提示:小心缓存影响。)

解:
概念解释:

  • 内存对齐访问​​:数据的内存地址是其大小(如4字节、8字节)的整数倍。例如,一个4字节的int类型变量存储在地址0x1000、0x1004等。
  • ​未对齐访问​​:数据的内存地址不是其大小的整数倍。例如,一个4字节的int类型变量存储在地址0x1001、0x1005等。

对齐访问通常更快,因为现代CPU的内存子系统(如缓存、内存控制器)针对对齐访问进行了优化。未对齐访问可能需要额外的时钟周期或引发硬件异常(在某些架构中)。

设计实验如下:
1)C语言代码如下,存为 memory_alignment_test.c 文件。
2)使用 gcc 编译代码,-O0:禁用编译器优化,确保测试准确性:
gcc -O0 -o memory_alignment_test memory_alignment_test.c
3)编译成功后,运行生成的可执行文件:
./memory_alignment_test
4)输出结果:
Aligned memory access time: 0.025735 seconds
Unaligned memory access time: 0.025872 seconds
结果表明,对齐内存访问的速度比未对齐内存访问快。

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define SIZE 1024 * 1024 * 10 // 10MB数据

int main() {
    // 分配对齐内存
    int* aligned = (int*)malloc(SIZE * sizeof(int));
    if (aligned == NULL) {
        printf("Memory allocation failed\n");
        return -1;
    }

    // 分配未对齐内存
    int* unaligned = (int*)((char*)aligned + 1);

    // 初始化内存
    for (int i = 0; i < SIZE; i++) {
        aligned[i] = i;
        unaligned[i] = i;
    }

    // 测试对齐内存访问速度
    clock_t start = clock();
    for (int i = 0; i < SIZE; i++) {
        int temp = aligned[i];
    }
    clock_t end = clock();
    printf("Aligned memory access time: %f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);

    // 测试未对齐内存访问速度
    start = clock();
    for (int i = 0; i < SIZE; i++) {
        int temp = unaligned[i];
    }
    end = clock();
    printf("Unaligned memory access time: %f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);

    // 释放内存
    free(aligned);

    return 0;
}
  1. 测试按顺序访问或按给定步幅访问内存时的速度差异。

解:
概念解释:

  • 按顺序访问​​:按顺序访问内存中的每个元素符合CPU缓存的工作原理,通常可以充分利用缓存的预读机制。
  • 按给定步幅访问​​:按给定的步幅访问内存会导致更多的随机内存访问,从而增加缓存未命中的概率。

通常按顺序访问的速度更快,因为顺序访问可以更好地利用内存带宽,因为数据是连续读取的。而步幅访问可能会导致内存带宽的利用率较低,因为每次读取之间有较大的间隔。

设计实验如下:
1)C语言代码如下,存为 stride_test.c 文件。
2)使用 gcc 编译代码,-O0:禁用编译器优化,确保测试准确性:
gcc -O0 -o stride_test stride_test.c
3)编译成功后,运行生成的可执行文件:
./stride_test
4)输出结果:
Stride=1 (Sequential) access time: 0.372 seconds
Stride=2 (Stride=2) access time: 0.383 seconds
Stride=64 (Stride=64) access time: 0.866 seconds
Stride=128 (Stride=128) access time: 1.699 seconds
结果表明,按顺序访问的速度比按给定步幅访问快。

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <x86intrin.h> // 确保 _mm_clflush 可用

#define SIZE (256 * 1024 * 1024) // 256MB
#define TOTAL_ACCESSES (100 * 1024 * 1024) // 固定访问100M次
#define CACHE_LINE_SIZE 64

void test_stride(volatile uint8_t* ptr, int stride, const char* label) {
    // 清空缓存
    for (size_t i = 0; i < SIZE; i += CACHE_LINE_SIZE) {
        _mm_clflush(ptr + i);
    }

    clock_t start = clock();
    for (size_t i = 0; i < TOTAL_ACCESSES; i++) {
        size_t idx = (i * stride) % SIZE; // 固定访问次数,但按步幅跳跃
        ptr[idx] = (uint8_t)(idx & 0xFF); // 写入操作
    }
    clock_t end = clock();
    double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
    printf("Stride=%d (%s) access time: %.3f seconds\n", stride, label, elapsed);
}

int main() {
    volatile uint8_t* data = (volatile uint8_t*)aligned_alloc(CACHE_LINE_SIZE, SIZE);
    if (!data) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    test_stride(data, 1, "Sequential");
    test_stride(data, 2, "Stride=2");
    test_stride(data, 16, "Stride=16");
    test_stride(data, 64, "Stride=64");
    test_stride(data, 128, "Stride=128");

    free((void*)data);
    return 0;
}
  1. 如何测量CPU上的缓存大小?

解:
Linux系统的命令行输入 lscpu,即可查看各级缓存的大小。
输出结果:
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 16896K

  1. 如何在多个内存通道中分配数据以获得最大带宽?如果有许多小的线程,会怎么布置?

解:
可以启用内存交错,通过将内存访问分散到多个通道上,从而提高内存带宽利用率。在多通道内存配置中,内存控制器可以在不同的通道之间交替分配内存请求,降低内存延迟并提高效率。

  1. 一个企业级硬盘正在以10000转/分的速度旋转。在最坏的情况下,硬盘读取数据所需的最短时间是多少(假设磁头几乎是瞬间移动的)?为什么2.5英寸硬盘在商用服务器上越来越流行(相对于3.5英寸硬盘和5.25英寸硬盘)?

解:
1)企业级硬盘的读取时间主要由以下因素决定:

  • 旋转延迟(Rotational Latency):即磁盘旋转到所需数据所在位置的时间。
  • 寻道时间(Seek Time):即磁头移动到所需磁道的时间。

假设磁头几乎是瞬间移动的(即寻道时间为0),则读取数据所需的最短时间主要由旋转延迟决定。
当转速为 10000转/分时,每转时间 = 60秒 / (10000转/分) = 6 ms
在最坏的情况下,磁头需要等待磁盘旋转半圈才能读取数据。因此,旋转延迟为 3 ms。
2)2.5英寸硬盘在商用服务器上越来越流行的原因主要是:

  • 更高的IOPS(每秒输入/输出操作数):2.5英寸硬盘的磁头移动距离更短,因此寻道时间更短,IOPS更高。在企业级应用中,如数据库和虚拟化环境,随机读写操作非常频繁。2.5英寸硬盘的高IOPS特性使其在这些场景中表现更好。
  • 更高的存储密度:2.5英寸硬盘的尺寸更小,可以在相同的机箱空间内安装更多的硬盘,从而提高存储密度。现代2.5英寸硬盘的存储容量已经接近甚至超过3.5英寸硬盘,例如,10TB的2.5英寸硬盘已经很常见。
  • 更低的功耗:2.5英寸硬盘的功耗通常比3.5英寸硬盘低,这可以显著降低服务器的总体功耗和散热需求。
  1. 假设HDD制造商将存储密度从每平方英寸1 Tbit增加到每平方英寸5 Tbit。在一个2.5英寸的硬盘上,多少信息能够存储一个环中?内轨和外轨有区别吗?

解:
1)背景知识:
一个2.5英寸的硬盘通常有多个磁盘(platters),每个磁盘上有多个磁道(tracks),每个磁道被划分为多个扇区(sectors)。磁道分布在磁盘的表面上,形成同心环。内轨和外轨的存储容量会有所不同,因为它们的周长不同。
2)存储信息量计算:
2.5英寸的硬盘的磁盘直径 d 为2.5英寸,半径 r 为1.25英寸,那么磁盘面积 S=π×r2≈4.91 平方英寸。
存储密度增加到每平方英寸5 Tbit,则单个磁盘的存储容量 C=S×5=24.55 Tbit。
内轨和外轨的存储容量跟其周长成正比,外轨半径为1.25英寸,内轨半径题目未指定,可以假设是0.625英寸,则:
外轨周长 Lo=2×π×ro≈7.85 英寸;
内轨周长 Li=2×π×ri≈3.93 英寸。
因此有:
外轨存储容量 Co=C×LoLo+Li≈17.35 Tbit;
内轨存储容量 Ci=C×LiLo+Li≈8.20 Tbit。

  1. 从8位数据类型到16位数据类型,硅片的数量大约增加了四倍,为什么?为什么NVIDIA会在其图灵GPU中添加INT4运算?

解:
1)从8位数据类型到16位数据类型,硅片的数量大约增加了四倍的原因有:

  • 更大的存储容量:16位数据类型需要存储的数据量是8位数据类型的两倍。因此,为了存储相同数量的16位数据,需要更多的存储单元。
  • 更多的计算资源需求:16位数据类型需要更多的计算资源来处理。
  • 更多的内存带宽和缓存需求:为了支持16位数据类型的高效处理,需要增加内存带宽和缓存容量,这也会导致硅片数量的增加。

2)NVIDIA在图灵GPU中添加INT4运算的原因主要是:

  • 提高推理效率:INT4运算可以显著提高推理任务的效率。在深度学习的推理阶段,通常不需要高精度的浮点运算,使用INT4可以减少计算资源的消耗,同时提高吞吐量。
  • 降低内存需求:使用INT4可以将模型的内存需求降低,从而在相同的硬件上支持更大的模型。
  • 支持量化技术:INT4运算支持量化技术,可以在保持模型精度的同时,显著提高计算效率。
  1. 在内存中向前读比向后读快多少?该数字在不同的计算机和CPU供应商之间是否有所不同?为什么?编写C代码进行实验。

解:
在内存中,向前读取(顺序读取)通常比向后读取(逆序读取)更快,因为CPU的缓存系统通常会预取内存中的数据,以提高数据访问效率。当程序顺序访问内存时,CPU会预测接下来要访问的数据,并提前将这些数据加载到缓存中。
设计实验如下:
1)C语言代码如下,存为 order_test.c 文件。
2)使用 gcc 编译代码,-O0:禁用编译器优化,确保测试准确性:
gcc -O0 -o order_test order_test.c
3)编译成功后,运行生成的可执行文件:
./order_test
4)输出结果:
Sequential read time: 0.257210 seconds
Reverse read time: 0.269498 seconds
结果表明,向前读的速度确实比向后读快。
不同的硬件架构(如台式机、服务器)可能会有不同的内存控制器设计,不同的CPU供应商在缓存设计和预取策略上也有所不同,因此顺序和逆序读取的速度差会有一定差异。

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <x86intrin.h> // 确保 _mm_clflush 可用

#define SIZE (256 * 1024 * 1024) // 256MB
#define TOTAL_ACCESSES (100 * 1024 * 1024) // 固定访问100M次
#define CACHE_LINE_SIZE 64

void test_stride(volatile uint8_t* ptr, int stride, const char* label) {
    // 清空缓存
    for (size_t i = 0; i < SIZE; i += CACHE_LINE_SIZE) {
        _mm_clflush(ptr + i);
    }

    clock_t start = clock();
    for (size_t i = 0; i < TOTAL_ACCESSES; i++) {
        size_t idx = (i * stride) % SIZE; // 固定访问次数,但按步幅跳跃
        ptr[idx] = (uint8_t)(idx & 0xFF); // 写入操作
    }
    clock_t end = clock();
    double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
    printf("Stride=%d (%s) access time: %.3f seconds\n", stride, label, elapsed);
}

int main() {
    volatile uint8_t* data = (volatile uint8_t*)aligned_alloc(CACHE_LINE_SIZE, SIZE);
    if (!data) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    test_stride(data, 1, "Sequential");
    test_stride(data, 2, "Stride=2");
    test_stride(data, 64, "Stride=64");
    test_stride(data, 128, "Stride=128");

    free((void*)data);
    return 0;
}
  1. 磁盘的缓存大小能否测量?典型的硬盘是多少?固态驱动器需要缓存吗?

解:
1)Linux系统命令行输入free -h可以查看磁盘的缓存大小,输出为80Gi。
2)典型的机械硬盘缓存大小范围为64MB到256MB。固态硬盘的缓存通常由DRAM颗粒组成,用于提高读写性能,常见的固态硬盘缓存大小为128MB到2GB。
3)固态硬盘通常需要缓存,原因如下:

  • 提高性能:缓存可以显著提高固态硬盘的读写速度,尤其是在处理大量小文件时。
  • 减少磨损:通过缓存,可以减少对闪存芯片的直接写入操作,从而延长固态硬盘的使用寿命。
  • 优化数据传输:缓存可以作为数据传输的缓冲区,提高数据传输的效率。
  1. 测量通过以太网发送消息时的数据包开销。查找UDP和TCP/IP连接之间的差异。
    解:
    没有用代码进行实际测量,仅理论计算了一下,如下:
    1)数据包开销的组成:
  • 以太网帧开销:包括一个14字节的头部(目的MAC地址、源MAC地址、类型字段)和一个4字节的尾部(CRC校验),总共18字节。
  • IP头部:IPv4头部通常是20字节(不包括选项字段),IPv6头部是40字节。
  • 传输层头部:
    • TCP头部:通常是20字节(不包括选项字段)。
    • UDP头部:固定为8字节。

2)由上可以计算出UDP和TCP/IP连接的总开销:

  • UDP数据包开销 = 以太网头部(14字节)+ IP头部(20字节)+ UDP头部(8字节)= 42字节。
  • TCP数据包开销 = 以太网头部(14字节)+ IP头部(20字节)+ TCP头部(20字节)= 54字节。

3)总结UDP和TCP/IP的差异如下:

  • UDP:
    • 开销小:每个数据包的开销为42字节,比TCP少12字节。
    • 速度快:UDP是无连接的,不需要建立连接和维护连接状态,因此传输速度快,但可能丢包。
    • 适合实时应用:由于UDP的低开销和快速传输特性,它适用于对延迟敏感的应用,如视频流、在线游戏和VoIP。
  • TCP/IP:
    • 开销大:每个数据包的开销为54字节。
    • 可靠性高:TCP是面向连接的,提供可靠的数据传输,包括错误检测、重传和顺序保证。
    • 适合需要保证数据完整性和顺序的应用,如Web浏览、文件传输和电子邮件。
  1. 直接内存访问允许CPU以外的设备直接向内存写入(和读取)。为什么要这样?

解:
直接内存访问(Direct Memory Access,DMA)是一种硬件特性,允许某些硬件子系统在不经过CPU干预的情况下直接访问系统内存,进行读取或写入操作。
这种机制设计的好处如下:
1)可以减轻CPU负担:

  • 高效数据传输:DMA允许硬件设备(如磁盘控制器、网络接口卡等)直接与内存交互,无需CPU逐字节地处理数据传输。这样可以显著减轻CPU的负担,使CPU可以专注于执行其他更重要的任务,如复杂的计算和程序逻辑。
  • 提高系统性能:通过DMA,数据传输可以在后台进行,CPU可以同时执行其他任务,从而提高系统的整体性能和效率。

2)可以提高数据传输速度:

  • 减少上下文切换:在没有DMA的情况下,CPU需要频繁地在用户态和内核态之间切换,以处理I/O操作。DMA减少了这种上下文切换的次数,从而提高了数据传输的速度。
  • 有效利用硬件特性:DMA控制器通常具有专门的硬件设计,能够以更高的速度和更低的延迟进行数据传输,比CPU直接操作内存更高效。

3)支持高带宽设备:

  • 满足高速I/O需求:现代计算机系统中,许多I/O设备(如高速网络接口、SSD硬盘等)的数据传输速率非常高。CPU可能无法及时处理这些高速数据流,而DMA可以有效地支持这些高带宽设备,确保数据传输的高效性。
  • 避免数据瓶颈:通过DMA,数据可以直接在设备和内存之间传输,避免了CPU成为数据传输的瓶颈。

4)增强系统安全性:

  • 减少数据拷贝:DMA可以直接将数据从设备传输到内存,减少了数据在系统中的拷贝次数,从而降低了数据被篡改或泄露的风险。
  • 隔离设备与CPU:DMA允许设备在不直接与CPU交互的情况下访问内存,这可以增强系统的安全性,防止恶意设备通过CPU间接访问系统资源。

5)支持复杂的I/O操作:

  • 多任务处理:DMA可以同时处理多个I/O操作,而不会阻塞CPU。这使得系统能够更高效地处理复杂的多任务环境,提高系统的响应速度和稳定性。
  • 支持DMA的设备可以独立于CPU运行,提高系统的灵活性和可扩展性。
  1. 看看Turing T4GPU的性能数字。为什么从FP16到INT8和INT4的性能只翻倍?

解:
NVIDIA T4 GPU 是一款基于 Turing 架构的数据中心 GPU,专为 AI 推理和多种工作负载优化。T4 GPU的性能数据如下:

  • FP32 单精度浮点性能:8.1 TFLOPS。
  • FP16 半精度浮点性能:65 TFLOPS。
  • INT8 整数性能:130 TOPS。
  • INT4 整数性能:260 TOPS。

从 FP16 到 INT8,性能翻倍是因为 INT8 的计算位宽减半,硬件可以并行处理更多的数据。从 INT8 到 INT4,性能再次翻倍,是因为 INT4 的位宽进一步减半,进一步提高了并行处理能力。

  1. 一个网络包从旧金山到阿姆斯特丹的往返旅行需要多长时间?提示:可以假设距离为10000公里。

解:
假设单程距离为10,000公里,那么往返总距离为20,000公里(​ m)。
考虑到光在光纤中的传播速度约为 m/s(真空中光速的2/3),则传播时间为:s,也即100 ms。
另外考虑其他延迟因素,实际的往返耗时大约在100-200 ms之间。

相关推荐
凤年徐12 分钟前
【数据结构初阶】顺序表的应用
c语言·开发语言·数据结构·c++·笔记·算法·顺序表
蹦蹦跳跳真可爱58918 分钟前
Python----目标检测(《SSD: Single Shot MultiBox Detector》论文和SSD的原理与网络结构)
人工智能·python·深度学习·神经网络·目标检测·计算机视觉
LeonDL1681 小时前
HALCON 深度学习训练 3D 图像的几种方式优缺点
人工智能·python·深度学习·3d·halcon·halcon训练3d图像·深度学习训练3d图像
半导体守望者2 小时前
英福康INFICON VGC501, VGC502, VGC503 单通道、双通道和三通道测量装置
经验分享·笔记·功能测试·自动化·制造
Timmer丿2 小时前
kafka学习笔记(三、消费者Consumer使用教程——配置参数大全及性能调优)
笔记·学习·kafka
Timmer丿2 小时前
kafka学习笔记(三、消费者Consumer使用教程——消费性能多线程提升思考)
笔记·学习·kafka
保持学习ing2 小时前
黑马Java面试笔记之 消息中间件篇(Kafka)
java·笔记·面试·kafka
只有左边一个小酒窝3 小时前
(三)动手学线性神经网络:从数学原理到代码实现
人工智能·深度学习·神经网络
@蓝莓果粒茶4 小时前
LeetCode第244题_最短单词距离II
c++·笔记·学习·算法·leetcode·职场和发展·c#
多多*4 小时前
蓝桥杯国赛训练 day1
java·开发语言·数据库·redis·缓存·职场和发展·蓝桥杯