上一篇请移步【动手学深度学习PyTorch版】22续 ResNet为什么能训练出1000层的模型_水w的博客-CSDN博客

目录

一、深度学习硬件CPU 和 GPU

1.1 深度学习硬件

◼ 计算机构成

◼ 程序执行的原理

◼ 内存

◼ 存储器

◼ 中央处理器(CPU)

1.2 如何提升cpu的利用率?(如何使运算在cpu上进行的更快,特别是数值运算:矩阵乘法、线性运算等)

◼ 提升空间和时间的内存本地性

◼ 尽量使用多核并行计算

1.3 GPU

◼ GPU

◼ cpu和gpu的对比

◼ 如何提升GPU的利用率?

◼ CPU/GPU带宽

◼ 如何在CPU上进行高性能计算编程?

1.4 总结


一、深度学习硬件CPU 和 GPU

1.1 深度学习硬件

◼ 计算机构成

(1)CPU(处理器):除了运行操作系统和其他许多功能外,还能执行程序;通常由 8 个或者更多个核心组成。

(2)内存(随机访问存储,RAM):用于存储和检索计算结果,如权重向量和激活参数,以及训练数据。

(3)以太网:一个或者多个,速度从 1 GB/s 到 100 GB/s 不等。

(4)高速扩展总线(PCle):用于系统连接一个或者多个 GPU;服务器最多有 8 个加速卡,通常以更高级的拓扑方式连接,而桌面系统则有 1 个或 2 个加速卡,具体取决于用户的预算和电源负载的大小。

(5)持久性存储设备:为系统需要的训练数据和中间检查点需要的存储提供了足够的传输速度;如磁盘驱动器、固态驱动器,在许多情况下使用高速扩展总线连接。

◼ 程序执行的原理

程序执行的原理:在计算机上运行代码的时候,需要将数据转移到处理器(GPU 或者 CPU)上执行计算,然后将结果从处理器转移回到随机访问存储和持久访问存储器中。

CPU(处理器):除了运行操作系统和其他许多功能外,还能执行程序;通常由 8 个或者更多个核心组成。

CPU的芯片图:

◼ 内存

主要用于存储需要随时访问的数据

当想要从内存中读取一部分内容时,需要先将地址(信息的位置)发送到 RAM,然后可以选择只读取一条 64 位记录还是一长串记录(突发读取,burst read),

  • 向内存发送地址并设置传输大约需要 100 ns(细节取决于所用内存芯片的特定定时系数),每个后续传输只需要 0.2 ns;
  • 第一次读取的成本是后续读取的 500 倍;
  • 每秒最多可以执行一千万次随机读取;
  • 应该尽可能地避免随机内存访问,而是使用突发模式读取和写入;

当拥有多个存储体时,每个存储体大部分时候都可以独立地读取内存,

  • 如果随机操作均匀分布在内存中,有效的随机操作次数将高达 4 倍(突发读取的速度也快了 4 倍);
  • 由于内存对齐是 64 位边界,因此最好将任何数据结构与相同的边界对齐(当设置了适当的标志时,编译器基本上就是自动地执行对齐操作);

因为 GPU 的处理单元比 CPU 多得多,因此它对内存带宽的需要也更高,

  • 一种解决办法是使内存总线变得更宽,这样可以同时传输更多的信息(首要方法);
  • 另一种方法是在 GPU 中使用特定的高性能内存(GPU 虽然速度更快,但是它的内存通常比 CPU 的内存小得多,因为成本更高,价格昂贵,通常仅限于在高端服务器的芯片上使用)。

◼ 存储器

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

(1)硬盘驱动器(hard disk drive,HDD)

它包含许多旋转的盘片,这些盘片的磁头可以放置在任何给定的磁道上进行读写,

优点是相对便宜,:缺点:

  • 典型的灾难性故障模式
  • 相对较高的读取延迟:
    • 硬盘驱动器的转速大约为 7200 RPM(每分钟转速),如果速度再快一点,就会由于施加在碟片上的离心力而破碎;
    • 在访问磁盘上的特定扇区时,需要等待碟片旋转到位(可以移动磁头,但是无法对磁盘加速),因此可能需要 8 毫秒才能使用请求的数据;
    • 硬盘驱动器可以以大约 100 IOPs(每秒输入/输出操作)的速度工作,在过去二十年中这个数字基本上没变,带宽(大约为 100 – 200 MB/s)也很难增加(每个磁头读取一个磁道的比特,因此比特率只随信息密度的平方根缩放);
    • 对于非常大的数据集,HDD 正迅速降级为归档存储和低级存储;

(2)固态驱动器(solid state drives,SSD)

固态驱动器使用闪存持久存储信息以更快地访问存储的记录,

缺点是:

  • 固态驱动器以块的方式( 256KB 或更大)存储信息:
    • 块只能作为一个整体写入(而且块必须被读取、擦除,然后再重新写入新的信息),需要耗费大量的时间,导致固态驱动器在按位随机写入时性能非常差;
  • 存储单元磨损比较快:
    • 通常在几千次写入之后就已经老化了;
    • 不建议将固态驱动器用于交换分区文件或大型日志文件;
  • 带宽的大幅增加迫使计算机设计者将固态驱动器与 PCIe 总线相连接,这种驱动器称为 NVMe(非易失性内存增强):
    • 最多可以使用 4 个 PCIe 通道;
    • 在 PCIe4.0 上最高可达 8 GB/s;

◼ 中央处理器(CPU)

(1)组成:

  • 处理器核心(processor cores):用于执行机器代码

前端加载指令并尝试预测将采用哪条路径,然后将指令从汇编代码解码为微指令(汇编代码通常不是处理器执行的最低级别代码,复杂的微指令可以被解码成一组更低级的操作,然后由实际的执行核心处理,通常执行核心能够同时执行许多操作)。

高效的程序可以在每个时钟周期内执行多条指令,前提是这些指令可以独立执行。

为了提高吞吐量,处理器还可以在分支指令中同时执行多条代码路径,然后丢弃未选择分支的结果。

  • 总线(bus):连接不同组件

总线会因为处理器型号、各代产品和供应商之间的特定拓扑结构有明显不同。

  • 缓存(cache):相比主内存实现更高的读取带宽和更低的延迟内存访问

为了避免出现向 CPU 传输用于处理的数据不足的情况,应该尽量避免从内存中加载新数据,而是应该将数据放在 CPU 的缓存上。

添加缓存一方面能够确保处理器核心不缺乏数据,但同时也增加了芯片的尺寸,消耗了原本可以用来提高处理能力的面积。

  • 向量处理单元(vector processing unit):为高性能线性代数和卷积运算提供辅助

CPU在一个时钟周期内执行许多操作,是通过向量处理单元实现的(这些处理单元有不同的名称:在 ARM 上叫做 NEON,在 x86 上被称为 AVX2),常见的功能是能够执行单指令多数据操作(single instruction multiple data,SIMD)。

1.2 如何提升cpu的利用率?(如何使运算在cpu上进行的更快,特别是数值运算:矩阵乘法、线性运算等)

◼ 提升空间和时间的内存本地性

从图中我们可以看到,一个数据要参与计算,需要走很长的一条路。

如果要计算两个向量的和a+b,在计算之前需要准备数据,a 和b很有可能是放在主内存中的,如果想要将a和b进行相加,就需要将数据从主内存搬运到寄存器中(数据只有被搬运到寄存器中才能参与运算):主内存=>L3 cache(shared LLC)=>L2 cache=>L1 cache=>寄存器。

这条路里面,每一个地方的能其实是不一样的。这条路径中最快的是寄存器,寄存器可以认为是和主频一样快,

  • L1 访问延时:0.5ns (L1比寄存器要大,访问延迟比寄存器要高)
  • L2 访问延时:7ns (14 *L1 ,访问一次L2相当于访问14次L1)
  • 主内存访问延时:100ns (200 * L1 ,访问一次主内存相当于访问200次L1)

虽然cpu算的比较快、频率比较高,但实际上,实测下来,在实际测试的时候会发现远远没有达到CPU理论的算的速度,运算速度可能远低于理论值,这通常是由于内存访问速度过慢导致的。

内存访问太慢了,所以一般来说,我们所谓的加速一个比较大的关键点就是提升空间和时间的内存本地性,来使得我的缓存的效率更高。

具体来说,有两种办法:

  • 时间:重用数据使得它们在缓存里
  • 空间:按序读写数据使得可以读取

提升时间上的本地性:重用数据使得保持在他们的缓存里(计算时需要将数据从主内存搬运到寄存器中,如果计算完的数据不再使用,cpu会将数据从寄存器一直回退到主内存中,因此,对于重复使用的数据,就希望能够将重复使用的数据一直保持在寄存器或者是L1中,以便于下一次使用)。

提升空间上的本地性:按序读写数据使得可以预读取(cpu在读取内存的时候是一块一块地读,如果所要计算的数据如果在内存中是存储在一起的话,就希望下一次计算所使用到的数据和前一次计算所需要的数据是相邻的,这样能够提升cpu读取数据的效率)。

比如,例:

如果一个矩阵是按行存储的(在行上面的内存地址是连续的),访问一行会比访问一列要快,特别是当矩阵比较大的情况下。这是因为,

  • cpu一次读取64字节(缓存线),一个Float是4个字节,意味着一次性会读16个浮点数(一次性读取2行);
  • cpu在读完这16个浮点数(缓存线)之后,会提前预读取下一个(缓存线);

◼ 尽量使用多核并行计算

高端cpu有几十个核,比如:

加起来一共是64个核。

但是Intel比较有意思的是,假设从芯片上来说有4个物理核,但是从系统上你看到的核可能是有8个,因为它使用了超线程。把一个CPU超线程了两个核。

但是超线程对于计算密集型没有太多的用处,并行来利用所有核,超线程不一定提升性能,因为这2个超线程共享的是一个寄存器。

例:使用以下两种方式来计算a + b ,

左边使用循环对元素进行逐个相加,同样的道理,图的右边是使用计算框架(numpy等)进行加法运算,最终在运算的时候左边会比右边慢很多,慢个几百倍。

那是因为有2个原因:

  • 左边调用了n次函数(n是a的长度),每次调用它都是有开销的;
  • 右边很容易做并行(C++的并行实现,如下所示):
    • omp:C++中比较常用的并行方法;
    • 第一行omp的标记表示该循环可以被多个线程并行执行,可以很好地利用多核;

1.3 GPU

◼ GPU

这个是Ncidia Titan X的一个架构图, 我们可以认为一小块就是一个核。其实3080,3070,3090等等没什么区别,唯一区别就是放了不同的大核而已。

大核里面有很多小核,每个小核的每一个绿点,它其实我们可以认为是一个计算单元,可以在每一个绿点上开一个线程。所以GPU来说,每次可以上千次计算。

就算每一个绿点的计算能力比cpu弱,速度慢上4,5倍,但是计算单元数量多,最终还是快的。

◼ cpu和gpu的对比

表中的“/”表示一般型号的参数/高端型号的参数。CPU的核心数量一般来说是6核,好一点的是64。而显卡的核心数量一般来说是2K核,好一点的是4K个。

GPU的核心数量远多于CPU,所以就导致了GPU每秒能计算的浮点数(TFLOPS,可以用核心数量和主频的乘积来做一个简单的近似)就要远高于CPU。

每一次计算都需要从内存中读取数据,因此内存带宽也很重要,通常来说,如果达不到计算峰值一般是由于内存带宽的限制。

也就是说,GPU通过高的内核带宽,多核,来换取它的计算更快。核心数量和内存带宽的优势使得GPU在运算速度上要远快于CPU。但是另外付出的代价是,这也导致了GPU的内存大小不是很大,GPU的控制流很弱(CPU是做通用计算的,因此需要很强的控制流)。

◼ 如何提升GPU的利用率?

本质上与cpu一样,

(1)并行:使用数千个线程;

(2)内存本地性:

缓存更小,架构更简单(GPU为了节省面积将缓存做得比较小,这样做的好处是内存的带宽会更高一点);

(3)少用控制语句:支持有限,同步开销很大;

◼ CPU/GPU带宽

CPU和GPU并不是独立的,所有的任务都是运行在CPU上的,如果要在GPU上做运算,就会存在带宽的问题。

  • CPU和内存之间的带宽,可以认为CPU每个针脚会传输一定的数据,分配给内存的针脚越多,带宽就越大。
  • CPU和GPU之间的带宽:带宽不是很高,特别是在多个GPU的情况下,可能需要共享带宽。

因此不要频繁地在CPU和GPU之间传递数据:带宽限制,同步开销(这里的同步是由驱动决定的)。

◼ 如何在CPU上进行高性能计算编程?

(1)C++或者任何高性能语言:编译器成熟;
(2)Nvidia上用CUDA:编译器和驱动成熟;

(3)其他使用OpenCL(CUDA也支持OpenCL):质量取决于硬件厂商(编译器和驱动是根据硬件厂商决定);

1.4 总结

  • CPU:可以处理通用计算。性能优化考虑数据读写效率和多线程(多核);
  • GPU:使用更多的小核和更好的内存带宽,适合能大规模并行的计算任务;
  • 设备有运行开销,数据传输时要争取量大次少而不是量少次多;
  • 在训练过程中数据类型过小可能会导致数值的溢出(在推断过程中影响不大);