Programming Massively Parallel Processors Introduction Part
Outline of Programming Massively Parallel Processors Introduction Part
写在最前
本页面内容仅对于毫无基础的初学者想要快速了解可供参考,深度学习者仍建议自行阅读Programming Massively Parallel Processors书籍原文.
在本页面的所有内容中,我不会对CUDA代码进行详细介绍,我只会针对整体内容进行讲解,为读者构建基础的印象感知,同时方便温习使用.
以下内容为基于自身理解的梗概,内容难免有错漏,欢迎各位阅读者批评指正!
再次声明,以下仅供参考
异构并行计算与现代CPU/GPU架构
首先,我们需要知道,现代计算机的两个基础部件:CPU与GPU
CPU,即Central Processing Unit,作为现代计算机的基础组成部分,是大部分计算控制的核心.在片上,它拥有极大的控制单元(Control Unit),以及多级缓存结构(Cache),这也是它延迟导向(Latency-oriented design)的设计思路的表现.片上大的控制单元,使其及其擅长流程控制以及分支判断.相较而言,由于大量的缓存和控制占用,使之片上的计算单元(ALU,Arithmetic Logical Unit)占比较少,因而对于大规模计算,CPU通常难以胜任,尤其是现代图形处理、3D渲染、机器学习等领域中,CPU更多扮演控制的角色而非计算,计算的任务则主要交给GPU.
GPU,即Graphics Processing Unit,作为现代计算机的又一重大组成部分,承担起大规模并行计算的重任.其片上大量面积分配给ALU,而缓存与控制部分占比极少,这充分展现了GPU强大的计算能力.其与显存(DRAM)之间的通道数量较多,可以提供更高的内存带宽.以上两点,则是GPU吞吐导向(Throughput-oriented design)设计思路的展现.
在GPU中,所有的计算都是以线程(Thread)为最基础的单位在软件层面上进行的,每个线程都会负责一部分计算,这一点在下一部分会讲到.
尽管当下,多核CPU已经存在,同样提高了计算机的性能,但是,对于大规模的计算而言,CPU仅仅支持串行计算,其少量的ALU限制了它的及计算运用.对于GPU,大量的ALU为并行计算提供了可能.英伟达CUDA为GPU编程提供了高效快捷的工具,在如今人工智能高速发展、图形渲染要求更多的情境下,GPU异构并行计算成为处理大规模计算的主流方式.
下图为CPU/GPU的结构示意图
数据并行与GPU编程基础概念
请看如下图片灰度处理的例子
对于每一个图片像素,实际上都是由RGB(红绿蓝)三个值来决定的.对于灰度计算,我们有如下公式:
在实际应用中,我们总是会把二维结构一维线性化,以一维数组的形式储存.实际上,在CUDA编程和GPU计算中,这是很常见也是基本的操作.
对于输入数组,我们对每个像素用上述公式计算得到对应的输出,如下
对于串行计算,并行计算可以在几乎同一时间来完成计算,因为一位数组天然有可并行性.这是一种数据并行化的方法,就是转换数据的存储形式.
数据并行 vs 任务并行
数据并行并不是并行编程中唯一使用的并行类型。任务并行也被广泛应用于并行编程。任务并行通常通过应用程序的任务分解来体现。例如,一个简单的应用程序可能需要进行向量加法和矩阵-向量乘法。这两者各自就是一个任务。也就是说,如果这两个任务可以独立完成,那么就存在任务并行。在大型应用中,通常会有更多的独立任务,因此任务并行也更多。一般来说,我们主要通过数据并行来提升并行程序性能.
在GPU编程中,我们在软件层面将GPU分为grid-block-thread三个层次,每一个grid/block皆有xyz三个维度,因而形成了一个三位的结构.thread不存在方向和维度,因为thread已是基本的执行单元.
每个gird方向上支持的最大block数量,以及每个block方向上支持的最大thread数量,可以通过相关CUDA代码进行查询(由于设备不同,其返回数值有所不同).
下图可以看到形象的展示图
需要注意到,对于二维结构,通常用x方向代表水平轴方向,y方向代表垂直轴方向.三维结构的z轴与我们的认知基本一致.
正如上图图片灰度计算的例子相似,我们对于多维结构,一般会采用一维数组的方式存储.在CUDA中,一般采用行为主(Row-major)的展示形式
对应有如下的映射法则:
其中,
本部分书中讲到的矩阵乘法,图形模糊处理,在后续Parallel Concepts部分会有讲解,故不在这一部分讲述.
GPU计算架构和调度
在现代GPU编程中,除了如上的grid-block-thread结构,实际上,在硬件上,存在SM(Streaming Multiprocessor,流式多处理器)和warp(线程束)的概念.
在实际计算运行中,通常是每32个threads构成一个warp,这是GPU执行计算的最小调度单位.其中的32个线程是按索引连续的.
对于SM,通常包含了多个block(若干block),由于通常一个block包含了多个warp,所以也可以认为SM是多个warp的集合.SM中有控制单元和内存单元,所以负责基础的控制分流等操作.
通常,一个GPU上的SM是有限的,且支持的线程也是有限的,并不与之前代码中可以查看的数量一致.通常来说,一个SM能够支持2048threads,调度几十到几百block,以及含有一定内存的寄存器,这些所有的数量限制都会对实际SM可以含有的线程数造成影响,比如如果一个thread使用过多的寄存器内存,则寄存器会成为最大限制,导致一个SM中实际支持的线程数量达不到可支持的最大线程数.这个实际线程数和可支持的最大线程数的比率,我们叫做占用率(Occupancy)
在编程中,我们追求高占用率,这样可以充分利用SM,也就是说,尽量达到
在编程中,由于我们知道,最小的调度单位是一个warp,所以我们会尽量减少warp内的分支控制差异(Control Divergence),避免warp内的部分线程的等待(latency)
GPU内存结构与数据逻辑
对于GPU,不同内存层级之间,访问数据的速度是不一样的.
在GPU中,内存分为显存(DRAM)、共享内存(Shared memory)、常量内存(Constant memory)、寄存器内存(Register memory).在访问速度上,显存的IO是最慢的,最快的则是寄存器的IO,而共享内存和常量内存居中,但是都远远快于访问显存.
对于GPU内存访问,有一个极其重要的指标——计算密度(Computational intensity),单位FLOP/B,即每B内存读取执行了多少次浮点运算操作(Float-point Operation per Byte),这个指标越高,说明每次访问内存的数据得到了更高的利用,通常可以通过优化内存访问(减少内存访问)或是优化计算方法(增加数据利用率)来提高这个值.
其次,除了计算密度,峰值带宽决定了内存最快的读取速度,单位为GB/s.它与计算密度乘积则是计算吞吐量(Computational throughput),单位GFLOPS,代表着若干内存读取对应的每秒实际计算量.
三者关系如图
在实际编程运用中,为了减少内存重复读取,我们会采用tiling的方式来减少内存冲突,增加一次读取数据利用率,这一方法经常运用于如矩阵乘法等.再次声明,我不会在这一部分提供详细代码,因为后续内容会详细介绍tiling以及thread coarsening、memory coalescing、register tiling这些方法的综合运用.
需要注意的是,既然我们提到了内存合并(memory coalescing),我们需要知道,GPU的多级缓存和内存结构决定了对于连续的地址读取,其速度总是优于跳跃的内存读取,因为每次内存读取,GPU会尝试将一段连续的地址内容放入缓存,如果本身读取的数据就是连续的,那么可以直接从缓存拿去数据,提高数据访问速度,这是远远快于跳跃读取数据的.对于写入数据也是如此,因此,催生了Grid-Stride Loop方法的大量运用.
Appendix
尽管我们不提供详细的代码使用,但是在此附录,我会给出CUDA基本函数及其规范,供于参考,准确内容请访问CUDA官网.
对于代码,GPU上的部分叫做device,在CPU运行部分叫host
1 | /* |







