目录
- 前言
- 0. 简述
- 1. 串行处理与并行处理的区别
- 2. 并行执行
- 3. 容易混淆的几个概念
- 4. 常见的并行处理
- 总结
- 参考
前言
自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考
本次课程我们来学习下课程第一章——并行处理与GPU体系架构,一起来理解并行处理相关概念
课程大纲可以看下面的思维导图
0. 简述
本小节目标:理解并行处理的基本概念,理解 SIMD,以及编程中常见的并行处理方式
这节课我们来学习第一章的第 1 小节,理解并行处理,第一章节内容偏基础但相关知识在后面很多章节中都会涉及到。那我们为什么要讲并行处理和 GPU 体系架构呢?这是因为大家在用 tensorRT 做模型部署的时候其实不光是使用它的 API 把模型转换成推理引擎放到 GPU 上跑就行了,这个是远远不够的
我们还需要考虑很多其它的东西,比如说你的模型的推理引擎有没有充分的利用 GPU 资源,有没有把 CUDA Core、Tensor Core 都利用起来,模型的计算密度是多少,模型的可并行性是多少,有没有充分利用 GPU 资源去做并行化等等,那这些其实就是我们在做模型部署的时候需要考虑的,所以作为一个基础内容,我们会给大家展开去讲第一章节
第一章节的内容可能很多人在大学听到过一些类似的知识,那大家作为复习听一下就好了。第一章节我们分为三个部分讲解,第一部分是并行处理的简介,第二部分是 GPU 并行处理,第三部分是 GPU 体系架构
我们先来看第一部分,并行处理的简介,希望大家在学完这部分后能够去理解并行处理的一些基本概念,比如说理解什么叫 SIMD,还有在 C++ 编程中常用的并行处理方式都有哪些
1. 串行处理与并行处理的区别
OK,我们在讲并行处理之前,我们需要去想它和串行处理的区别是什么,串行处理(Sequential processing)其实就是指令或者代码块必须是依次执行的,也就是说你前面一条指令需要执行结束之后才能够执行下面一条语句
我们来看两个例子
a = b + c; // BB1
d = b + a; // BB2
我们先看第一个例子,这是两个赋值语句,第一句我们叫 BB1(Basic Block 1),第二个语句是 BB2(Basic Block 2)我们可以看到 BB2 语句中的 a 需要利用前面 BB1 语句的结果 a,这就是一个很典型的 data dependency 数据依赖的程序,像这种程序,我们必须依次执行 BB1 和 BB2,如果颠倒的话这个程序的结果就不一样了,这种情况下我们就必须要串行处理
if(a == b){ // COND3
c = d; // BB4
}else{
c = e; // BB5
}
我们再看第二个例子,这是一个很简单的分支程序,我们把它分成三个部分,第一个部分是 COND3 也就是条件语句,如果 a==b 条件成立则执行 BB4,如果不成立则执行 BB5,这个其实也是必须要串行执行的,为什么呢,因为我们编译器在执行的时候,我们不知道选择执行哪个语句,我们得需要知道 COND3 的结果才能够去执行 BB4 或者 BB5,所以这个也是有依赖的
所以总结来说就是当我们程序如果有一些依赖或者分支的地方,这些情况下必须让其进行串行,这里稍微给大家扩展一下,大家可能听过数据依赖其实有很多种类,我们第一个例子是 flow dependency,此外还有 Anti dependency、Output dependency、Control Dependency,大家感兴趣的可以自行查阅相关资料,这个在编译器做优化的时候其实还是很重要的东西
所以说我们串行程序它其实就是依次执行的一个程序,它的使用场景也是比较擅长去处理一些比较复杂的逻辑计算,比如大家在打开一个操作系统(Windows、Linux、Mac)的时候,它内部是有很多复杂的逻辑运算的,很多地方是需要串行执行
我们这里拿一个程序给大家去讲解一下,我们把这个程序给稍微抽象化,它有 5 个语句,statement1 到 statement5,如下图所示:
我们的 statement 语句它有可能是复制语句,也有可能是条件语句,也有可能是循环,还有可能是一个 I/O。我们给这个程序设置几个条件:
- statement2 依赖于 statement1
- statement5 依赖于 statement4
- statement3 是一个循环,跟所有的 statement 没有任何关系
- 有四个 core 可以使用
- 时钟周期
- statement1 和 statement2 需要 5 个时钟周期
- statement4 和 statement5 需要 8 个时钟周期
- statement3 需要 20 个时钟周期
那如果我们程序是按照上图串行执行,它的时钟周期就是 46 个 cycle,这个实在太慢了。我们既然有四个 core,那我们可以去想想充分利用这些 core 去把程序并行化
因此我们第一个策略如下图所示:
前面我们提到 2 和 1 是有依赖的,5 和 4 是有依赖的,3 是独立的,那这三个部分是不是可以分别分配到不同的 core 去执行呢,把 1 和 2 分配到一个 core,把 4 跟 5 分配到一个 core,把 3 分配到一个 core,这三个部分同时去执行,这样做其实对程序的运行结果没有任何影响,而它的时钟周期却只用了 20 个 cycle
我们继续优化,20 个 cycle 还是有点长,上面 20 个 cycle 中具有支配性的语句是 statement3,它是一个 for 循环,我们是不是有一些其它的策略可以给它做优化呢
我们说 statement3 这个循环可以分割成很多个子代码也就是子循环,每一个子代码块只要 5 个 cycle 就能执行完,那 statement3 就可以按照上图的分配方式,分配四个子代码块分别是 3-1、3-2、3-3、3-4,而刚好我们之前只用了三个 core,还有一个没用,我们现在就可以把空余的 core 给利用起来,把 3-3 和 3-4 放到另外一个 core 上让它去执行,那此时的执行时间就从 20 个 cycle 减少到了 16 个 cycle
那我们接着看还能不能优化呢,现在具有支配性的语句是 statement4 和 statement5,我们来看看这个能不能去优化。我们再给定一个假设条件,就是说 statement5 可以在 statement4 彻底结束之前就知道它所依赖的结果,也就是说 statement4 执行到中间环节的时候就已经把 statement5 所依赖的数据写回到寄存器或者写回到内存中,后面做的事情就跟 statement5 没有任何关系的,那也就意味着我们可以把 statement5 的执行时间往前调一点,如下图所示:
这样的话,我们 16 个 cycle 的时钟周期又能减少到 14 个
OK,我们来回顾一下,这个程序串行执行的时间一共是 46 个 cycle,我们进行了很多优化最终变成了 14 个 cycle,大概优化速度提升 3.5 倍左右,我们目前为止做的优化如下:
- 把没有数据依赖的代码分配到各个 core 各自执行(schedule,调度)
- 把一个大的 loop 循环给分割成多个小代码,分配到各个 core 执行(loop optimization)
- 在一个指令彻底执行完之前,如果已经得到了想要的数据,可以提前执行下一个指令(pipeline,流水线)
以上三个步骤其实只是并行处理中一个很简单的做法,真正做并行处理的时候其实要做的优化还有很多,我们管这一系列行为叫做 parallelization 并行化,我们管这个可以利用多核,多线程资源的程序叫做 parallelized program 并行程序
2. 并行执行
我们再回归正题,这个 parallel processing 并行执行又是指什么呢,它主要有以下几个特点:
- 指令/代码块同时执行
- 充分利用 multi-core 多核的特性,多个 core 一起去完成一个或多个任务
- 使用场景:科学计算,图像处理,深度学习等等
并行执行中多个 core 会一起去完成一个或多个任务,也就是说假设现在我们的 device 设备上有四个 core,我们可以让这四个 core 一起去执行一个 for 循环,我们把一个 for 循环分割成多个子块,让每一个 core 去完成一个部分,这个是完全 OK 的。或者我们程序很大时,我们可以让每一个 core 去分别完成这个程序的各个小任务,最终把每一个 core 的任务完成后我们再做一个同步处理,那这个也是可以的。
串行执行是针对于复杂逻辑运算的,而并行执行它非常擅长一些大规模的科学计算,比如天体预测,还有图像处理,深度学习这些东西其实也是有很多并行处理的概念在里面
上图是一个 Multi-core processor 多核处理器的架构,我们简单讲下,这个是偏计算机硬件的东西,从图中可以看到我们首先有一个 main memory 主存,主存上面有一个 L3 缓存,之后还有很多寄存器,那寄存器上面有很多 core,这些都是硬件层面的东西。在硬件之上我们会有一个自己的操作系统,然后在操作系统上有很多程序,也就是图中的 app 1、app 2 这些东西
说到并行处理,其实我们在做程序并行化的时候大部分时间处理的主要还是 loop 循环,这是因为我们发现执行时间长的程序耗时主要集中在两个方面,一个是 I/O 的内存读写,另一个就是 loop 的循环
I/O 的内存读写我们先抛开不谈,因为这里可扩展性的东西太多了,我们主要讲针对 loop 的并行处理,那 loop 的并行其实是一个很重要,也是很热门的一个课题,到目前为止已经有很多学者做了一些优化,也发表了很多论文,在图像处理还有深度学习中其实很多地方都用到了循环,比如我们在做分类任务时,我们把图片放到深度学习模型之前需要对这个图像进行预处理,包括 resize 到模型能接受的范围比如 256x256,包括通道切换比如 bgr2rgb,包括归一化比如 /255.0 等等,那这些都是需要用到循环的。还有我们在深度学习模型内部的一些卷积层或者全连接层,这里面其实也是要涉及到并行处理的
那这里我们以一个比较经典的 loop parallelization 为例给大家去讲解一下 CPU 里面是怎么对 loop 进行优化的,我们这里拿一个 4x4 的二维矩阵来进行一个讲解,默认来说它的一个遍历方式是 row major 行为主,它是先把一行的数据全遍历完之后再去遍历下一行
值得注意的是不同的编程语言它的遍历方式是不一样的,或者准确来说它的数据,二维数组在内存里面的存储方式是不一样的,比如大家经常使用的 C、C++、C# 这些语言的二维数组是以 row major 的方式进行存储的,所以我们也推荐在遍历的时候以 row major 进行遍历,但是有一些语言比如说 MatLab、R 这些语言的二维数组在内存中的存储方式其实是 column major 的
所以说针对不同语言,如果它默认存储方式是不同的话,那么也就意味着我们在进行优化的时候它对于 Reordering 的方式也是不同的,比如说对于 MatLab 语言,它存储方式默认是 column 的,但是你的编程代码里面如果遍历方式是 row major 的话,那我们可以把它修改成一个 column major 的遍历方式去完美的匹配它的一个存储方式,从而减少你的 cache miss
这个怎么理解呢,就是我们在 CPU 进行程序优化的时候,其实我们需要考虑如何尽量去减少你的一个 cache miss,比如存储数据的时候你需要考虑它的一些空间连续性或者时间连续性,比如一个元素访问到之后,它旁边的元素其实也很容易很大几率被访存,所以你把这一列的数据全都放一起访存的话,一起去放到 memory 里面去访存的话,其实它是可以减少你的 cache miss,当然再强调一遍,不同语言它的 reordering 的方式是不同的
第二个是 Vectorization 标量化,我们之前矩阵元素的遍历是一个个去遍历的,但是只要我们的指令集支持或者我们的寄存器支持的话,我们其实是可以让它四个元素一起去访问的,甚至说 16 个元素,32 个元素一起去访问,这个也是完全 OK 的,只要我们硬件支持,相比于我们原本一个个元素去访问而言,速度提升了很多
那下一个 Tiling,这个技术也是很重要的,如果说我们有一个 4x4 的数组,我们把它分割成四个 2x2 的数组的话,我们是不是可以每一个元素,每一个小部分都进行遍历呢,也就是每个部分独立的遍历或者独立的计算,这个是完全 OK 的。那如果说我们可以分割成四个小部分,那也就是意味着我们四个小部分它们的计算遍历等等其实是完全可以并行化的
这里我们给大家举了几个例子其实只是 loop parallelization 中很小的一部分,但是也是现在很常用的一些方式,比如这里的 Vectorization、Tiling 这些技术其实不光光在 CPU 里面会使用,其实在 GPU 的一些设计模式里面也完美的体现出来了这几个 loop 的优化,我们后面再给大家详细去讲,现在大家理解一下就好了
(双线性插值)
那在涉及到一些 DNN 或者图像处理的时候,也有一些比较常见的并行处理的例子,比如说双线性插值,我们在 resize 的时候会用到,用周围四个点的坐标值去计算中间点 P 值时也是可以做并行化的,因为一张图像有多个像素值,resize 时很多像素值都需要像这样进行计算插值,那这些执行其实是完全可以并行化的,因为我们在做某一部分的插值计算时并不会说依赖于它相邻的那一块区域的计算,它是完全可以去并行处理的
(卷积层)
那同样的卷积也是一样的,针对 5x5 的一个 input tensort 而言,用一个 3x3 的 kernel 进行卷积,那这个卷积的过程也是可以并行化的,通过滑动窗口我们一共需要滑动 9 次,做 9 次计算,但是这 9 次的计算中我们并没有说哪一个部分一定是依赖于另外一个部分的,所以说我们这 9 个部分它其实是完全可以并行化执行的
(全连接层)
同样的全连接层也是一样的,可以看到 input tensor 有 9 个元素,你的输出的 tensor 它一共有 A、B、C、D 四个元素,我们在做计算 A 的这个过程,它其实跟计算 B 的过程是没有任何依赖性的,那就是说计算 A、计算 B、计算 C、计算 D,它们这四个计算过程也是可以完全并行化的
3. 容易混淆的几个概念
我们讲了这么多并行处理,我们先稍微停一下,我们去讲一下大家在学习的过程中容易混淆的几个概念,首先第一个就是并行和并发,这两个词还是很像的,但在英语中这两个单词是完全不一样的,一个是 parallel 并行(物理意义上同时执行),一个是 concurrent 并发(逻辑意义上同时执行)
我们先说并发,并发是什么呢,并发它是逻辑意义上的同时执行,那什么叫逻辑意义呢,我们来看下面的例子:
现在我们只有一个 CPU,我们要让它去执行两个 task,我们先让它执行 task1,当 task1 执行到一半的时候我们切换到 task2,把 task2 做完一半后我们继续回到之前做 task1,之后我们再做 task2,这样一个 CPU 来切换的去做两个 task,但是实际上这个切换 task1 和 task2 的速度是非常非常快的,我们肉眼是无法识别的,所以我们能够感觉到这个 CPU 它是在同时做两个任务,那这个就是并发,逻辑意义上的同时执行
那我们再说并行,并行它就是物理意义上的同时执行,也就是说现在我们手上的 CPU 很多,我们有两个 CPU,我们让这两个 CPU 分别处理 task1 和 task2,并且让这两个 CPU 同时去执行,这个就是物理意义上的同时执行,这个就是 parallel
我们这个课程主要还是以并行处理为主,并发其实在做编译器的时候,大家可能会用到这个概念,但是做 GPU、CUDA 的话还是用并行处理
那么还有一个容易混淆的概念就是进程和线程,线程它是进程的一个子集,一个进程它可以有很多个线程,我们以一个例子来讲解,我们打开操作系统之后,我们去打开一个小程序,那其实在打开小程序的过程中操作系统为这个程序已经分配了一个进程,但是这个小程序它其实运行的速度可能会很慢,那么我们就可以把这个小程序的这个进程再给它分配很多个小的线程,去分别完成这个程序中的各个部分,这个是并行处理的一个解决方案,也就是进程和线程的关系
那另外一个就是多核和加速比的关系,这个也是大家容易混淆的一个概念,双核它的加速不一定就是两倍,比如我们只有一个核去处理一个程序,那么这个程序它的执行时间是 2ms,那么如果说我们现在有两个核去同时处理这个程序,那是不是这个程序的时间就会由 2ms 变成 1ms 呢,其实并不是的,我们这里以 Amdahl’s Law 来进行讲解
这是一个非常经典的图,也是在做并行处理中经常会遇到的一个图,横轴是处理器的个数,纵轴是加速比,我们可以看到,核个数和加速比并不是成线性增长的,它到了一定程度之后,它这个加速比其实就饱和了,就不会再继续增长了。也就是说我们在做并行处理的时候,其实我们有很多限制因素在里面
比如说我们回到之前的例子,我们看这里面 statement3 这个 for 循环我们可以分割,这个是完全 OK 的,但是 1 和 2,4 和 5 它们之间存在依赖关系,那如果有依赖关系的话,我们再怎么做并行,我们也不能够让 4 和 5 一起去执行,也不能让 1 和 2 并行执行,其实这个就是一个强制性的约束力,就是程序再怎么并行还是有个上限的
同时我们从图中还能看到 8 核的加速比有的时候也会差于 4 核,也就是说我们有的时候核越多其实并不是效率会越高,为什么呢,因为我们在做加速的时候,其实还是需要考虑它的数据传输,一些偏内存的读写,数据传输这个方面所消耗的时间也是需要去考虑的
4. 常见的并行处理
我们讲现在常用的并行处理方式都有哪些呢,刚才给大家稍微提了一下,并行处理自古以来都是一个非常非常热门的话题,有很多种方式,这里我们简单举几个例子:
- 编译器自动化并行优化
- GCC,LLVM,TVM,…
- 针对 for 循环的并行优化
- tile,fuse,split,vectorization,…
- 计算图优化
- CFG,HTG,MTG,…
- 数据流
- Dataflow compiler
- Dataflow architecture
- Polyhedral
- Polyhedral compiler
- HPC
- High performance computing
- …
首先编译器它就有很多自动化并行优化的一些方式,大家比较常见的 GCC、G++,它里面有很多自动并行优化的一些策略,还有做编译器的人可能经常听到的 LLVM,它们也有很多这种优化策略,还是大家做深度学习的编译器的时候,模型部署的时候,可能会经常听到 TVM 这个东西,它是针对深度学习模型 DNN 模型的一个编译器,这个里面也有很多自动化并行优化策略在里面
那我们还有很多针对 for 循环的并行优化,这个是一个很热门的话题,比如说 tile,fuse,split,vectorization 等等,到目前为止已经是很成熟的技术了,有很多地方都在用。
还有计算图优化,这个跟大家经常听到的 pytorch 或者是 tensorflow 里面的计算图是不一样的,我们可以把它理解为像是一个程序的流程图一样,就是 CFG、HTG、MTG 等等,这些都是 graph,我们针对这个程序的这个 graph 来进行优化,这个也是并行处理的一个方式
我们还有数据流,这个也是一个很老的概念了,我们有针对 Dataflow 的一个编译器,也有人在做 Dataflow 的硬件研发,这个还是挺常见的,现在很多 DNN 的模型,有很多硬件厂商会去把它制作成一个 Dataflow 的数据流的硬件,那大家感兴趣的可以查阅一下相关资料。
还有 Polyhedral,可以有针对 Polyhedral 这个编译器的开发,还有包括 HPC 超大规模计算的一些并行优化
总的来讲并行处理其实是一个很大的课题,很多人都在做,那我们这门课程的 TensorRT 我们可以把它理解为一个编译器,把一个模型输入到 TensorRT 里面给它做各种优化,优化成一个推理引擎,能够放到硬件上最优化你程序的执行时间
我们这门课程要讲 TensorRT,要讲 CUDA 编程的话,我们不得不提 SIMD 这个东西,因为太重要的,SIMD 全称 Single Instruction Multiple Data 也就是说我们同一条指令去执行多个数据,这个怎么理解,那我们先看一个简单的例子
我们现在要做一个乘法运算,A1 乘以 B1 赋值给 C1,同样我们还有 A2、B2、C2;A3、B3、C3;A4、B4、C4,那么我们这个过程其实在编译的时候,计算机需要处理的事情还挺多的,我们得需要先取指令,我们读取数据,还得做计算,再写回数据,这是针对一个,针对其它几个也是同样的操作,也就是这些同样的操作我们需要重复四次,那这是不是非常麻烦呢
OK,那针对这个我们其实有一个优化策略,如下图所示:
我们把 A1、A2、A3、A4 放在一起,B1、B2、B3、B4 也放在一起,C1、C2、C3、C4 也是一样的,那么这是不是也就意味着我们只需要读取一次数据,计算一次乘法,写回一次数据就行了,这个其实是可以的,我们管这种操作方式叫做 SIMD operation,它前面一个个取数据的操作方式我们叫 Scale operation,这个在编译器中也是比较常见的概念
在 CUDA 编程与 TensorRT 中以及 NVIDIA 在做 tensor core 的设计理念中都存在着 SIMD
我们从 NVIDIA 的官网中看 Tensor Core 的介绍,它很擅长去做 4x4 矩阵的乘加法运算,也就是图中的 D=AxB+C,如果我们依次去执行的话要执行 16 次,非常麻烦,Tensor Core 的设计理念就是允许多个数据放在一起去执行,快速计算 AxB+C,接下来的课程去针对 GPU 的体系架构我们会稍微扩展去讲一下,这个是很重要也是很核心的一个东西
值得注意的是准确来说在 CUDA 编程中它其实使用的并不是 SIMD 而是 SIMT(Single Instruction Multiple Thread),不再是多数据而是多线程,CUDA 编程中线程是一个很重要的概念,也就是说它会去充分利用它的多核资源去进行多线程化,用多线程化去把一个卷积操作,矩阵乘法操作这些东西进行优化处理,SIMT 和 SIMD 很像,它是 SIMD 的一个高级版,这个接下来我们也会再结合 CUDA 编程去讲
那最后我们去给大家稍微介绍一下比较常见的并行处理方式
CPU 上的并行处理
- OpenMP
- pthread
- MPI
- Halide
- 应用场景:图像预处理/后处理,Multi-task 模型
GPU 上的并行处理
- CUDA programming
- 应用场景:DNN 优化
我们这里主要针对模型部署的一些常见的并行处理方式,首先我们得先看我们并行处理的计算是要放到哪里去执行,我们是直接放在 GPU 上呢,还是放在 CPU 上,如果是 GPU 上的化,其实我们就用 CUDA 或者 TensorRT 是 OK 的,它的应用场景也是针对 DNN 进行的优化策略,那如果是放在 CPU 上的话,其实我们有很多种选项,我们可以选 OpenMP,可以使用 pthread、MPI、Halide 等等
那大家大学期间如果上过 C++ 并行处理课程的话,可能经常听到过 OpenMP,这个还是比较老的概念了,MPI 也是,其实它这些东西就是你在程序中如果声明了一个代码块,由 OpenMP 声明了代码块之后,那么你的这个程序就是这一部分可以进行并行化,它可以是指令级别的并行化,也可以是代码块的并行化,这个不是我们这门课程的重点就不展开讲了,感兴趣的可以查阅下相关资料
那么在 CPU 上的并行处理,它其实应用的场景主要是在图像的预处理和后处理上,就是说需要我们把数据放在 GPU 上之前,我们需要把这个图像给进行加工,让它加工成模型可以识别的一个格式,这个是在 CPU 上做的,其实现在有很多为了让预处理和后处理进行加速,也把它放在 GPU 上做,这个也是有的
还有就是做 Multi Task 的一个模型,也就是多任务模型,如果说我们的模型它既有分割又有检测,或者说我们有很多个分割,我们让它并行处理的话,其实也是可以用到这个 CPU 并行处理的概念,直接 OpenMP 或者 pthread 做并行处理,这个也是完全可以的
那我们这里再稍微扩展一下,就是我们刚才看到的图,multicore processor 在多核处理器,一般来说我们常见的就是同构架构,就是一个处理器里面它们有很多 core,它们的 core 其实都是一样的,我们有一个 CPU Core1、CPU Core2、CPU Core3、CPU Core4。但是我们在异构架构里面它不光是只有一个种类的核,如上图所示,我们可以有 CPU Core,也可以有 GPU Core,还可以有 DSP Core,也可以 Fast core,就是这些 core 是相互结合,相互联合起来去组成一个计算机硬件的
所以我们在做模型部署的时候,其实我们要想的东西很多,不光要去想这个东西是怎么能够在 GPU 上变快,我们还得去想它的 CPU 和 GPU 之间的通信是什么样子的,我们哪些地方可以放在 CPU 上去执行更快一点,我们哪些地方可以放在 DSP 上去执行,我们在做异步执行的时候怎么做等等,这些也是在异构架构中经常需要涉及到的理念
OK,以上就是第一部分的内容
总结
本次课程我们学习了并行处理的相关知识,首先我们讲解了串行处理和并行处理的区别,串行处理就是指令或代码必须依次执行,常用于一些比较复杂的逻辑运算;并行处理就是指令或代码块同时执行,常用于科学计算,图像处理等。之后我们讲解了 loop 循环优化的一些方案,比如 Vectorization、Tiling 等等,同时我们区分了几个容易混淆的概念,包括并行/并发、进程/线程等。最好我们简单介绍了一些常见的并行处理方式,重点是 SIMD 以及 CUDA 编程中非常重要的 SIMT。
OK,以上就是第 1 小节有关并行处理的全部内容了,下节我们来学习 GPU 并行处理的相关知识,敬请期待😄
参考
- Bilinear interpolation
- A guide to convolution arithmetic for deep learning
- Fully Connected Layer vs. Convolutional Layer: Explained
- Multicore Processing