深入理解GPU(二)硬件架构
上篇文章介绍了GPU的渲染管线,这是从渲染的流程层面介绍了GPU渲染的过程,本文的内容深入到GPU的硬件架构,从硬件层面介绍GPU的组成和工作原理。参考文献有多篇非常深入详细的文章,值得学习。
什么是GPU?
GPU的全称是Graphics Processing Unit,图形处理单元。最初的GPU是专门用于绘制图形图像和处理图元数据的特定芯片。

如上图所示,展示了GPU和CPU的硬件差异:
- CPU的核心数量少,每个核心都有控制单元,内存设计上是大缓存,低延迟。CPU擅长分支控制和逻辑运算,而不适合海量的数据计算。
- GPU则计算单元非常多,多个计算单元共享一个控制单元。内存设计上是追求高带宽,可以接受高延迟。GPU适合海量的数据并发计算的场景。
桌面GPU 物理架构
GPU的微观结构因不同厂商,不同架构都会有所差异,但是核心的部件,概念以及运行机制大同小异,桌面级的GPU产商有NVIDIA,AMD,移动端的GPU包括PowerVR,Mali和Andreno。以NVIDIA的桌面级GPU为例,历代的GPU包括Tesla,Fermi, Maxwell,Kepler和Turing架构。关于各代GPU的详细架构信息,推荐GPU架构系列文章,生动详细。
Tesla架构

以上是Tesla架构的总览图,Tesla架构虽然是早期的GPU架构,但是却包括了GPU该有的硬件结构,其所奠定的基础设施框架和设计思想却历久弥新。Tesla架构的硬件部分包括以下组件:
- Host Interface: 主要用于接受来自CPU端各种图形API(DX,GL,Vulkan等)发出的的渲染命令,Host Interface接受这些命令,并通过FrontEnd处理这些命令,同时Host Interface还负责将内存中的各种数据,包括顶点数据,贴图数据,buffer数据等,传入到GPU的显存中。
- Input Assembler: 负责将顶点数据根据顶点索引和图元类型进行简单的组装,并搭配上对应的顶点属性,然后传送给Vertex Work Distribution。
- Vertex,Pixel,Compute Work Distribution: 可以理解为包工头,负责将各自领域的工作分发到对应的工作单元去执行。
- TPC(Texture Processing Cluster): 包含一个纹理单元Texture Unit和两个负责计算的SM(Streaming Multiprocessor),下面会详细介绍SM的结构。
- Viewport/Clip/Setup/Raster/Zcull Block: 顶点着色器处理完,还只是输出了一堆裁剪坐标和等待光栅化插值的属性,这个模块就负责这些内容,流水线到目前还未开放编程部分。
- ROP(Raster Operations Process):这些模块负责对pixel shader处理后的像素进行测试和合并。同一个像素位置的深度/模板的写入,颜色混合,抗锯齿等都由该模块完成。
- L2 cache, Memory Controller和DRAM:每个DRAM搭配一个ROP,Memory Controller和L2 cache,从SM发来的数据吞吐的请求会在这里执行,同时还负责将最终的像素值写入到FrameBuffer中。

上面是一个TPC内部的详细结构,在Tesla架构中一个有8个这样的TPC单元。每个TPC内部包括有:
- Geometry Controller: 负责顶点属性在芯片内部的输入和输出,同时还负责几何着色器等增删顶点改变顶点拓扑结构等。顶点着色器和几何着色器是在SM里完成的,Geometry Controller会把顶点着色器的结果送入到Viewport/Clip/Setup/Raster/Zcull Block去进行光栅化,或者Stream Out输出。
- SMC(SM Controller): Tesla是统一的并行计算架构,顶点着色器,几何着色器,片元着色器和计算着色器等都由SM来进行计算,SMC的职责就是将这些任务拆分打包成Warp,然后派发给SM进行执行,同时SMC还负责协调SM和Texture Unit之间的操作。总结就是SMC主要负责对接外部资源和协调内部工作。
- Texture Unit:包含4个纹理地址生成器和8个滤波单元。与SM内的指令都是标量运算不同,纹理单元的指令源是纹理坐标,输出的是经过插值的纹理值,这些值都是向量,获取的数据会暂存到Tex L1 cache中备用。
- SM(Steaming Multiprocessor): 执行真正计算的单元,每个TPC包含两个SM,SM又包含以下内容:
- I-Cache: 指令缓存,一个SM需要执行的来自SMC分配的任务,并不是马上就能执行的,大量的执行是需要被缓存下来,分批执行的。
- C-Cache: 常量缓存,主要用于缓存常量数据。
- Shared Memory: SM 内的高速可编程内存,由同一 SM 内的所有线程共享,用于线程间通信和数据复用.
- MT(Multi Thread) issue: 负责把Warp任务拆分为一条条的执行派发执行,其对Warp的调度是GPU并行能力的关键。
- SP(Streaming Processor): 真正执行计算的地方,主要执行最基本的浮点型标量运算,包括add,multiply,multiply-add等,以及各种整型的运算。
- SFU(Special Function Unit): 执行复杂的运行,比如指数,对数,三角函数,属性插值,透视矫正等复杂的运算单元。
每个SM可以执行的可以是着色器程序,也可以是CUDA程序,这些高级语言会被首先编译成为中间指令,然后优化成为GPU的二进制指令。**ISA(Instruction Set Architecture)**主要包含有:
- 运算:浮点和整型的加法乘法,最大最小等。
- 流控制:分支,调用,返回,中断和同步等。
- 内存访问:包括内存的读取,写入,原子操作等。
SIMD和SIMT
SIMD指Single Instruction Multi Data,即单个指令,多数据。SIMT是Single Instruction Multi Thread,即单个指令,多个线程。关于SIMD和SIMT的区别可以参考该文章。
SIMD在多个数据上并行进行相同操作的硬件部件。典型地,SIMD将两个vector作为操作数,对于两个vector的操作数进行相同操作,下图展示了使用SIMD并行执行四个操作的计算。SIMD在多个寄存器中存储多个数据,然后GPU从寄存器中取出数据来执行相同的指令,进行相同的计算,其本质还是一个线程。
SIMT则类似于CPU中的多线程,每个线程都有自己独立的寄存器,ALU和data cache,但是所有的线程使用相同的指令,命令从单一的instruction cache广播到所有的SIMT的core,每个core的指令一样,数据不一样。SIMT是真正的并行,GPU中的并行处理就是基于SIMT的。
当SMC拿到一个着色器的所有指令之后,会将指令以32个线程为单位分发给SM, 负责执行这个着色器的所有的32个线程,称作一个Warp。指令存在了Instruction Cache里,warp scheduler会将数据存入寄存器,然后将指令派发给warp内的SP来执行。这里需要区分的是:warp的线程数量为32,并不一定表示执行的sp有32个,比如Tesla架构内的一个SM的SP只有8个,那么如何运行32个线程呢?答案就是每个sp执行四次。所以Warp其实是逻辑的概念,与实际物理层面的core的数量无关。

Warp Divergence
一个Warp内所有的线程都是执行的相同的指令,但是由于数据不同,不同的线程可能进入到不同的分支,GPU会将所有的分支都走一遍,如果当前的指令是true,但是当前线程的数据条件是false,那么此时线程会被遮掩(Mask out),其执行结果会被丢弃掉,即使只有一个线程进行了分支,那么其他所有的线程也必须等待它执行完毕,这个就是锁步执行(lock step)。在所有分支都走同一侧还好,如果两个分支都走,则会产生性能影响。另外,如果shader对应的像素数量达不到32个线程,那么仍然会占用一个warp来运行,其中部分的核心工作,另外的则不工作,warp的利用率会降低。所以为了提高warp的使用效率,尽可能的将线程并行的数量对齐32,同时保证每个warp内的分支都走同一边,避免分叉。
Stall和延迟隐藏(Latency Hiding)
GPU中的延迟Latency表示指令从开始到结束所消耗的clock cycle的数量,在GPU中硬件,计算比内存访问的速度要快几个数量级,所以延迟通常都是对主存的访问造成的。比如纹理采样,读取顶点数据,读取varing等,尤其是纹理采样,如果cache missing可能需要消耗几百个时钟周期。
GPU实现延迟隐藏的主要方式就是通过warp的切换来完成,一个SM中可能存下足够多的warp,这些warp可以是不同类型的,GPU在warp之间的切换几乎是无开销的,所以当一个Warp stall了,就会立马切换到下一个warp,等到之前的warp需要的数据准备好了,再切换回来继续执行。
GPU之所以能实现这个机制,得益于GPU中大量的寄存器,GPU中的寄存器数量要远超于CPU,SM中为每个warp的的线程在初始化的时候就会分配好所需的寄存器和local memory,当Warp发生切换时不需要保存和恢复寄存器状态,因此才能实现无成本切换,可以在一个cycle内完成。。
需要注意的是,如果Shader里变量多,占用的寄存器就会变多,留给warp切换的寄存器就会变少,自然分配给SM的warp的数量就会减少(这里可以想象成按照寄存器数量来分warp的数量),这样就会降低GPU延迟隐藏的能力,降低GPU利用率。

GPU内存

根据CPU-GPU是否共享内存,分为两种CPU-GPU的架构,左侧是分离式架构,CPU和GPU有独立的内存和缓存,通过PCIE等总线通讯,这种结构的缺点在于PCIE有着高延迟低带宽的特点,数据传输成为性能瓶颈。右侧是耦合式架构,CPU和GPU共享内存和缓存,移动端通常都是这种架构。
在内存管理方面,分离式架构中两者各自拥有独立内存,两者共享一套虚拟地址空间,必要时进行内存拷贝。耦合式结构中,GPU没有独立内存,与CPU共享系统内存,由MMU管理。GPU使用独立显存空间的好处是:GPU可以对Buffer和Texture进一步优化,实现对GPU更加友好的内存排布,显存中存储的数据可能并不是我们实际Upload上去的数据,所以在耦合式架构中,即使CPU和GPU使用同一片物理内存,仍然需要使用Mapbuffer来实现对数据的拷贝。如果对CPU和GPU使用相同的数据,GPU无法进行优化,反而可能降低性能。
GPU内存分类

如上图所示是GPU中内存的层次结构,速度最快的是寄存器,L1缓存是片上缓存(On-Chip Memory),每个shader的核心都有独立的L1缓存,其访问速度很快。L2缓存时所有的Shader核心共享的,属于片外缓存,距离shader核心略远,所以访问速度比L1要慢。DRAM是主存(可以叫做System Memory,或Global Memory,或Device Memory),这个内存最大,也是访问速度最慢的,FrameBuffer一般都是放在主存上的。
不同内存的访问速度如下所示:
- 寄存器内存(Register Memory): 访问速度最快,GPU中的寄存器数量很多。
- 共享内存(Shared Memory): 也是片上内存,和L1 Cache是同一个硬件单元,SharedMemory可以开发者控制,而L1是GPU控制的。Shared Memory访问速度很快,是一个Shader核心内所有的线程所共享的。
- 全局内存(Global Memory): 即主存,系统内存或者设备内存,数量最大,速度最慢。
- 局部内存(Local Memory): Local Memory是Global Memory的一部分,它是每个线程所私有的,主要用于处理寄存器溢出,或者超大的uniform数组,由于是Global Memory的部分,所以访问速度很慢。
- 常量内存(Constant Memory): 常量内存也是Global Memory的一部分,所以访问速度同样很慢,部分GPU会有Constant Cache用于缓存。
- 纹理内存(Texture Memory): 纹理内存也是Global Memory的一部分,访问速度也很慢,部分GPU会有Texture Cache。
Memory Bank和Bank Confict
为了能够提高内存访问性能,SharedMemory和L1Cache被设计为一个个的MemoryBank。Bank数量一般与warp大小或者CUDA core的数量对应,例如32个core就会把SystemMemory划分为32个Bank,每个bank包含多个cacheline,Bank可以理解为memory对外的窗口,窗口越多就越高效。
如果同一个warp中的不同线程访问的是不同的bank,则可以并行执行,最大化的利用带宽;如果访问的是一个bank中的同一个cacheline,那么可以通过广播机制同步到其他线程,一次访问可以获取全部数据;如果访问的是同一个bank中的不同cacheline,那么就需要阻塞等待,串行访问,这个叫做Bank Conflict,会影响性能。如果不同的线程对同一个cacheline有写入的操作,那么也必须要阻塞等待,等上一个线程写入完毕,才能执行后续的读取或者写入。
其他的重要概念
Pixel Quad

光栅化的基本单位是像素pixel,但是片元着色器执行的基本单位是Pixel Quad,也就是2x2的像素。其目的是为了计算ddx和ddy,从而选取贴图的mipmap级别。进行EarlyZ判断的最小单位也是Pixel Quad。
由于PixelQuad的存在,即使再小的三角形,哪怕只占用一个像素,也会最小执行四个像素的ps,因此会造成很大的Overdraw,所以我们需要尽可能的避免大量的小图元的绘制。
Early ZS
在传统的渲染管线中,像素首先执行ps计算,然后再通过深度/模板测试,判断是否需要写入/废弃该像素的结果,这样就会造成overdraw。于是现代GPU出现一个EarlyZ的技术,将深度/模板测试放在PS之前进行,这样可以提前将看不见的像素剔除掉,减少Overdraw。Early ZS执行的最小单位不是pixel,而是Pixel Quad。
AlphaTest是在PS之后,ROP之前,根据像素计算得到的Alpha值来判断是否需要保留该像素,因此AlphaTest会影响EarlyZ的优化,因为像素必须执行PS才能知道自己是否需要保留。如果在像素着色器里会修改深度,如使用AlphaToCoverage,同样会影响EarlyZ的执行。
Hiz Culling
Hierarchical Z-Culling(Hiz),是Nvidia GPU支持的粗粒度的硬件culling方案,通过低分辨率的Z-Buffer来做剔除,它的精确度是8x8的像素块。关于各种基于Z的Culling方案,可以参考这篇文章:渲染杂谈:early-z、z-culling、hi-z、z-perpass到底是什么?
Register Spliing 和 Active Warp
前文提到GPU中的寄存器有很多,但是数量是有上限的,GPU的核心在执行一个Warp的时候,会在一开始就把寄存器分配给每个线程,这里每个线程执行所需要的寄存器的大小是在shader编译完成之后就可以确定的,如果shader占用的寄存器过多,那么最终可以分配来执行的Warp数量就变少了,也就是Active Warp降低。Active Warp数量降低会影响延迟隐藏的能力,进而影响GPU的性能。同时Shader使用的寄存器过多会产生更加严重的问题就是Register Spilling:GPU会将寄存器文件存储到Local Memomry上,而Local Memory是主存的一部分,访问速度很慢。
GPU核心的乱序执行和保序
GPU的计算核心是乱序执行的,不同的Warp执行的耗时不一致,受到分支,cache miss等因素的影响,GPU会尽可能的填充任务到核心,但是同一个像素的写入顺序是保证的,先执行的DrawCall的像素一定是先写入到FrameBuffer的,GPU在每个阶段输出的结果也都是保序的。
参考文献
- 深入GPU硬件架构及运行机制
- GPU架构探秘之旅
- GPU 渲染管线和硬件架构浅谈
- A look at the PowerVR graphics architecture: Tile-based rendering
- A look at the PowerVR graphics architecture: Deferred rendering
- Understanding GPU Family 4
- Imagination’s PowerVR Rogue Architecture Explored
- Graphics cores: trying to compare apples to apples
- The Mali GPU: An Abstract Machine, Part 1 - Frame Pipelining
- The Mali GPU: An Abstract Machine, Part 2 - Tile-based Rendering
- The Mali GPU: An Abstract Machine, Part 3 - The Midgard Shader Core
- The Mali GPU: An Abstract Machine, Part 4 - The Bifrost Shader Core
- Forward pixel killing
- Tile-Based Rendering
- Arm Mali GPU Best Practices Developer Guide
- Mobile Hardware and Bandwidth
- ARM Unveils Next Generation Bifrost GPU Architecture & Mali-G71: The New High-End Mali
- Arm’s New Mali-G77 & Valhall GPU Architecture: A Major Leap
- Qualcomm’s mobile processor lines
- Adreno-tiling
- 移动设备 GPU 架构知识汇总
- GPU Framebuffer Memory: Understanding Tiling
- GPU 画像素的顺序是什么 (结合 Understanding Tiling 这篇文章,可以更加清晰的理解 GPU 光栅化的绘制顺序)
- 当我们谈优化时,我们谈些什么 (从硬件到优化,非常值得阅读)
- NVIDIA GPU Programming Guide
- Life of a triangle - NVIDIA’s logical pipeline
- 游戏性能优化杂谈 (陈文礼大佬的性能优化杂谈系列文章非常值得一读)
- 移动平台 GPU 硬件学习与理解
- GPU 分析工具随笔
- GPU 架构和渲染
- 再议移动平台的 AlphaTest 效率问题
- 试说 PowerVR 家的 TBDR
- mesa(开源的 opengl 实现。包含 freedreno,一个开源的 adreno 驱动。如果对驱动实现细节感兴趣,比如 early-z、lrz 的实现原理,强烈推荐阅读,这个是网上少有的代码级别的资料)

