zoukankan      html  css  js  c++  java
  • SIMD数据并行(三)——图形处理单元(GPU)

    在计算机体系中,数据并行有两种实现路径:MIMD(Multiple Instruction Multiple Data,多指令流多数据流)和SIMD(Single Instruction Multiple Data,单指令流多数据流)。其中MIMD的表现形式主要有多发射、多线程、多核心,在当代设计的以处理能力为目标驱动的处理器中,均能看到它们的身影。同时,随着多媒体、大数据、人工智能等应用的兴起,为处理器赋予SIMD处理能力变得愈发重要,因为这些应用存在大量细粒度、同质、独立的数据操作,而SIMD天生就适合处理这些操作。

    SIMD结构有三种变体:向量体系结构、多媒体SIMD指令集扩展和图形处理单元。本文集中围绕图形处理单元( Graphics Processing Unit, GPU)进行描述。

    图形处理单元(GPU)

    0. 写在前面

    GPU 独立于 CPU 而发展,拥有自己的一套体系结构,并在发展中形成了自己的专门术语。加上不同设计厂商之间使用的术语往往不太一样,容易给 GPU 初学者带来名词分辨上的困扰。

    鉴于 NVIDIA GPU 的成功,本文只使用 NVIDIA 官方术语进行表述。我自己在看《计算机体系结构:量化方法研究》这本书时,也存在着不少困惑,有些名词感觉前后表达的意思不容易对上号,甚至多看几遍之后,困惑愈深。不过最终我还是理了一个基本不差的脉络出来,如果有读者发现我表述中差错的地方,敬请指正。

    1. 简介

    谈到 GPU,人们首先想到的可能是「显卡」,进而想到「英伟达(NVIDIA)」。的确, GPU 的祖先便是图形加速器,用于辅助 CPU 做图形处理的工作,而黄仁勋创办的英伟达正是 GPU 领域的佼佼者,不断引领着 GPU 架构的发展革新。

    我们知道,图形显示是由一个个细小的像素组合而成,在计算机的世界中,像素的变化即意味着一组固定宽度比特位所表示的参数的变化。因此,对一副图形进行变换,本质上是对成千上万个像素的参数进行计算的结果,而这其中必然存在大量的数据并行度。

    GPU 天生是处理并行问题的好手,在它的体系结构中融合了线程并行、指令并行、SIMD 数据并行的多种并行处理形式,它可以概括为是一个由多个多线程 SIMD 处理器组成的 MIMD 处理器。

    GPU 处理数据并行任务能有很好的效能比,但处理控制为主的任务则不够经济,因此 GPU 一直以来是作为 CPU 的外设而存在的。随着异构处理器的兴起,二者常常被集成在同一个处理器芯片中,相辅相成。同时,GPU的计算能力已不局限于图形处理,而是正在被迁移到更多的数据并行计算场景。

    2. 架构组成

    了解 GPU 比较适合先从一个大的角度看下去,然后往下深入到局部的部件。正所谓「窥一斑而现全身」,从图1可以看到,GPU 几乎是由众多相同的部件复制组合而成。

     

    图1 Fermi GTX 480 GPU 的平面图

    从硬件抽象的角度看:

    • GPU,与 CPU 经总线连接通信,并通过片外 DRAM 交换数据。GPU 的主体由多个(16/32···)相同的流式多处理器组成。

    • 流式多处理器(Streaming Multiproeessor,SM),它是具有独立PC的完整处理器。每个流式多处理器拥有多个(16···)独立 SIMD 线程,称为「warp」。各 warp 不会同时执行,而是依据每个 warp 的忙闲状态进行恰当的调度,这也正是 GPU 设计的基本思想:通过足够多的线程切换来隐藏访问 DRAM 的延迟。

    • warp,每个 warp 有自己的 PC,执行 SIMD 指令完成计算和访存。SIMD 指令功能单元含有多个(16/32···)并行的线程处理器(或者说车道)

    • 线程处理器,它执行一个 warp 中 SIMD 指令对应的单个元素,可根据谓词寄存器或者遮罩位状态,决定执行或忽略对应元素的操作。同一个 warp 中的多个车道不能同时执行不同指令。

    从程序抽象的角度看:

    • 网格,在 GPU 上运行,由一个或多个线程块构成的代码。

    • 线程块,在流式多处理器上执行的向量化循环,由一个或多个 SIMD指令线程构成。

    • CUDA 线程,SIMD 指令线程的垂直抽取,对应于车道所执行的单个元素。

    GPU 拥有两层硬件调度程序:

    • 线程块调度器(NVIDIA 称 Giga 线程引擎),将线程块指定给流式多处理器,确保线程块被分配给其共享存储器拥有相应数据的流式多处理器。

    • warp 调度器,流式多处理器内部的SIMD线程(warp)调度程序,由它来调度应该何时运行哪个warp。一个流式多处理器中可能同时存在两个 warp 调度器。

    图2 warp 调度器分配指令给 warp 执行(图中含两个warp 调度器)

    从存储器的角度看:

    • 全局存储器,由GPU所有流式处理器共享,它是位于流式处理器片外的 DRAM。流式处理器间不能直接进行通信,但可以使用全局存储器中的原子存储操作进行协调。系统处理器(CPU)和 GPU 之间的数据交换也是通过全局存储器。

    • 共享存储器,位于流式处理器内部,这一存储器由单个流式处理器内部所有 warp 共享,但无法被其它流式处理器访问。流式处理器内的 warp 可以通过共享存储器进行通信。

    • 局部存储器,各 warp 在片外 DRAM 获得的一个专用部分,它不能在同一流式处理器的各 warp 间共享。局部存储器用于栈帧、溢出寄存器和不能放在寄存器中的私有变量。

    • 寄存器,大量存在于 GPU中。由于每一个 warp 都是一个独立的线程,因此各 warp 都分配有独立的向量寄存器组,寄存器组中的每一个向量寄存器有N个元素(16/32···),N 为 GPU 的向量长度。

    • 缓存,在 GPU 中的容量不如 CPU,因为 GPU 不依赖大型缓存来包含应用程序的整个工作集,而是使用较少的流式缓存,依靠大量的线程来隐藏访问 DRAM 的长延迟。CPU 中用来放置cache的芯片面积在 GPU 中替换为计算资源和大量的寄存器,用来执行大量的 SIMD 线程。GPU 中的缓存要么被看作带宽滤选器,以减少对全局存储器的要求,要么被看作有限几种变量的加速器,这些变量的修改不能通过多线程来隐藏。

     

    图3 GPU 存储访问关系

    图4 显示了以上软硬件之间的层级对应关系:

    图4 GPU 软硬件间的层级对应关系

    3. GPU的条件分支

    相比于向量体系结构,GPU提供了更多的硬件机制来实现 warp 中的分支处理。GPU 用于处理分支的硬件有:谓词寄存器、分支同步栈、指令标记、内部遮罩。

    当条件分支较简单时,编译器将代码编译成仅使用谓词寄存器的条件执行指令。谓词被设置为1的车道将执行操作并存储指令;其它车道将不执行和存储结果。

    当面对更复杂的控制流时,编译器将混合使用谓词寄存器和 GPU 分支指令。特殊的指令和标记将用于这些分支指令,在执行到这些分支指令时,先向分支同步栈中压入一个栈项(主要为遮罩),随后联合谓词寄存器的内容更新遮罩,在跳转的目标分支中通过遮罩来决定对应车道是否执行操作和存储;当分支结束,则弹出并恢复栈项。整个过程类似于函数调用过程中的现场保存和恢复机制,因此可实现多层分支的嵌套。

    由于同一 warp 内的不同车道不能同时执行不同指令,使得 GPU 必须计算所有的分支,而各分支执行过程中必然存在部分车道执行、其它车道空闲的情况。这带来了 GPU 工作效率的下降。比如,对于相同长度的路径,if-then-else的工作效率为50%;对于嵌套的分支语句,这一效率将进一步降低。幸运的是, GPU 可以在运行时,在遮罩位为全0或全1时,略过 then 或 else 部分。


    编程利器——CUDA

    当  GPU 的计算潜力与一种简化 GPU 编程的编程语言相结合,使人们对 GPU 计算的兴趣大增。

    编写并行程序天生要比编写顺序程序复杂得多,因为编译器只能自动并行化一些简单的并行情况,大部分时候都需要编程者自己给出并行化的信息,并进行合理的调度,以使得并行化的效能最大化。

    CUDA (Compute Unified Device Architecture,计算统一设备体系结构)就是 NVIDIA 开发的一种与 C 类似的语言和编程环境,通过克服异质计算 及多种并行带来的双重挑战来提高 GPU 程序员的生产效率。

    CUDA 并不只为 GPU 编程,而是把 CPU 和 GPU 当作一个整体来编程。CUDA 为系统处理器(主机)生成 C/C++,为 GPU(设备)生成 C/C++ 方言。

    CUDA 将 CUDA 线程作为编程原型,表示最低级别的并行。编译器和硬件可以将数以千计的 CUDA 线程聚合在一起,利用 GPU 中的多种并行类型:多线程、MIMD、SIMD、指令并行。

    关于 CUDA 语言的具体语法以及编程技巧,是一个比较大的内容,本文只介绍其概念,不做赘述。后期等信号君学习了相关的内容,可以分一个系列再来和大家探讨。


    物理指令集抽象——PTX指令集

    与大多数系统处理器不同,NVIDIA 编译器的指令集目标是硬件指令集的一种抽象,NVIDIA 称之为 PTX 指令集。

    编译器生成的指令与 PTX 指令一一对应,但一个 PTX 指令可以扩展到许多机器指令,反之亦然。PTX 使用虚拟寄存器,而不是实际的物理寄存器。从 PTX 指令集到 GPU 物理指令集的这一层抽象解释由优化程序实现。优化程序在 warp 之间划分可用的寄存器,它还会清除死亡代码,将指令打包在一起,并计算分支发生发散的位置和发散路径可能会聚的位置。

    之所以要使用 PTX 指令集,而不是直接使用物理指令集,是为了解决软硬件工程中挥之不去的兼容性问题。PTX 的存在为编译器提供了一种稳定的指令集,可以实现各代GPU 之间的兼容性。不同的 GPU 只需要适配相应的优化程序即可实现代码兼容,这就给了 GPU 硬件非常大的变化空间。

    PTX 抽象指令集的这一思想可以类比于 x86微体系结构。众所周知,Intel 为了实现当代内核指令对以往版本内核代码的兼容,在硬件上做了一个解释层,将复杂指令翻译成一组微指令,然后再进行真正的指令译码操作。它们的区别在于:x86 的这一转换是在运行时以硬件方式实现,而 PTX 指令转换为物理指令是在载入时以软件实现的。


    延迟优化 & 吞吐量优化

    在研究计算机体系结构时,延迟和吞吐量是经常会被提及的两个指标。「延迟」表示从发出一条指令/请求,到得到结果所经历的时间;「吞吐量」表示单位时间内,完成的指令/请求数。

    绝大部分的CPU 为了通用和低延迟做了更多优化,我们称之为基于延迟优化的处理器。GPU为了高并发、高流水牺牲了通用性和延迟,我们称之为基于吞吐量优化的处理器。

    GPU 依赖于流式多处理器中的硬件多线程以隐藏访存延时,但这并不意味着延迟不存在。例如,流式多处理器中的某个 warp 发起外部 DRAM 访问,由于没有缓存支持,这一访问结果可能需要几十个 cycle 后才能有效,于是该 warp 只能选择等待,但在它等待的间隙,warp 调度器将执行处理器内的其它线程。尽管单个线程的访存发生大量的延迟,但从流式多处理器整体而言,它的吞吐量还是很大。

    基于延迟还是吞吐量来优化计算机系统并没有一个绝对的优劣标准,而是从计算机系统的应用场景出发来考量。拿 GPU 的主要工作图形处理来讲,一般的图形都是以几十帧每秒的速率切换,因此 GPU 只需要以 ms 级的速率完成一幅图像内容的处理即可满足要求,这时候单个线程的延迟就显得无关紧要了。

    有一点值得注意的是:低延迟不意味着低吞吐量;高吞吐量也不意味着高延迟。延迟与吞吐量之间也没有绝对的限制关系。一个计算机系统它可以是低延迟的同时,具备高的吞储量。


    参考资料

    【1】John L. Hennessy,David A. Patterson . 计算机体系结构:量化研究方法:第5版[M].北京:人民邮电出版社,2013. (原名《Computer Architecture:A Quantitative Approach》)

    【2】David A. Patterson,John L. Hennessy . 计算机组成与设计:硬件/软件接口:第5版[M].北京:机械工业出版社,2015. (原名《Computer Organization and Design:The Hardware/Software Interface》)

    ·END·


    你可能还感兴趣:

    SIMD数据并行(一)——向量体系结构

    SIMD数据并行(二)——多媒体SIMD扩展指令集

    计算机系统中与存储有关的那些事

    现代处理器与代码性能优化

    翻译 | 浅析算法复杂度分析

    关于代码执行效率优化的一次内部分享

     

    欢迎来我的微信公众号做客:信号君

    专注于信号处理知识、高性能计算、现代处理器&计算机体系 

     

    技术成长 | 读书笔记 | 认知升级

    幸会~

  • 相关阅读:
    C++链式队列基本操作
    C++链栈基本操作
    C++顺序栈基本操作
    C++链表基本操作
    C/C++/JAVA
    C++操作链表
    How Many Maos Does the Guanxi Worth
    Heavy Transportation
    Frogger
    Til the Cows Come Home(Dijkstra)
  • 原文地址:https://www.cnblogs.com/ncdxlxk/p/10165553.html
Copyright © 2011-2022 走看看