GPU全程是Graphics Processing Unit,图形处理单元。它的功能最初与名字一致,是专门用于绘制图像和处理图元数据的特定芯片,后来渐渐加入了其他很多功能。
GPU是显卡(Video card)最核心的部分,显卡还包括散热器、通讯元件、各类插槽等等。
得益于纳米工艺的引入,GPU可以将数以亿记的晶体管和电子器件集成在一个小小的芯片内。显卡不能单独工作,需要装载在主板上,结合CPU、内存、显存、显示器等硬件设备,组成完整的PC机。
TPC:Texture / Processor Cluster,纹理处理簇
SM:Stream Multiprocessor,流多处理器
SP:Streaming Processor,流处理器
SFU:Special Function Unit,特殊函数单元
MT Issue:Multithreaded isstruction fetch
早期GPU的渲染管线的深度测试是在像素着色器之后才执行,这样会造成很多本不可见的像素执行了耗性能的像素着色器计算。后来,为了减少像素着色器的额外消耗,
将深度测试提至像素着色器之前(下图),这就是Early-Z技术的由来。Early-Z技术可以将很多无效的像素提前剔除,避免它们进入耗时严重的像素着色器。Early-Z剔除的最小单位不是1像素,而是像素块(2*2)。
下述情况会导致Early-Z失效:
SIMD(Single Instruction Multiple Data)是单指令多数据,在GPU的ALU单元内,一条指令可以处理多维向量(一般是4D)的数据。比如,有以下shader指令:
float4 c = a + b
对于没有SIMD的处理单元,需要4条指令将4个float数值相加,汇编伪代码如下:
ADD c.x, a.x, b.x
ADD c.y, a.y, b.y
ADD c.z, a.z, b.z
ADD c.w, a.w, b.w
但是有了SIMD技术,只需要一条指令即可处理完成:
SIMD_ADD c, a, b
for(i=0;i
SIMT(Single Instruction Multiple Threads,单指令多线程)是SIMD的升级版,可对GPU中单个SM中的多个Core同时处理同一指令,并且每个Core存取的数据可以是不同的。
SIMT_ADD c, a, b这个指令会被同时送入在单个SM中被编组的所有Core中,同时执行运算,但a、b 、c的值可以不一样:
__global__ void add(float *a, float *b, float *c)
{
int i = blockIdx.x * blockDim.x + threadIdx.x;
a[i]=b[i]+c[i]; //no loop!
}
co-issue是为了解决SIMD运算单元无法充分利用的问题。例如下图,由于float数量的不同,ALU利用率从100%依次下降为75%、50%、25%:
为了解决着色器在低维向量的利用率低的问题,可以通过合并1D与3D或2D与2D的指令。例如下图,DP3指令用了3D数据,ADD指令只有1D数据,co-issue会自动将它们合并,在同一个ALU只需一个指令周期即可执行完。
但是,对于向量运算单元(Vector ALU),如果其中一个变量既是操作数又是存储数的情况,无法启用co-issue技术:
CPU 是一个具有多种功能的优秀领导者。它的优点在于调度、管理、协调能力强,但计算能力一般。
GPU 相当于一个接受 CPU 调度的 “拥有大量计算能力” 的员工。
CPU | GPU | |
---|---|---|
延迟容忍度 | 低 | 高 |
并行目标 | 任务(Task) | 数据(Data) |
核心构架 | 多线程核心 | SIMT核心 |
线程数量级别 | 10 | 10000 |
吞吐量 | 低 | 高 |
缓存需求量 | 高 | 低 |
线程独立性 | 低 | 高 |
根据CPU和GPU是否共享内存,可分为两种类型的CPU-GPU架构:
内存构架:GPU与CPU类似,也有多级缓存结构:寄存器、L1缓存、L2缓存、GPU显存、系统显存
它们的存取速度从寄存器到系统内存依次变慢。
由此可见,shader直接访问寄存器、L1、L2缓存还是比较快的,但访问纹理、常量缓存和全局内存非常慢,会造成很高的延迟。
Gpu内存分布在在RAM存储芯片或者GPU芯片上,他们物理上所在的位置,决定了他们的速度、大小以及访问规则:
sampler mySamp;
Texture2D myTex;
float3 lightDir;
float4 diffuseShader(float3 norm, float2 uv)
{
float3 kd;
kd = myTex.Sample(mySamp, uv);
kd *= clamp( dot(lightDir, norm), 0.0, 1.0);
return float4(kd, 1.0);
}
经过编译后的汇编代码:
:
sample r0, v4, t0, s0
mul r3, v0, cb0[0]
madd r3, v1, cb0[1], r3
madd r3, v2, cb0[2], r3
clmp r3, r3, l(0.0), l(1.0)
mul o0, r0, r3
mul o1, r1, r3
mul o2, r2, r3
mov o3, l(1.0)
在执行阶段,汇编代码会被GPU推送到执行上下文(Execution Context),然后ALU会逐条获取(Detch)、解码(Decode)汇编指令为二进制指令,并执行它们。
而对于SIMT架构的GPU,汇编指令有所不同,变成了SIMT特定指令代码:
:
VEC8_sample vec_r0, vec_v4, t0, vec_s0
VEC8_mul vec_r3, vec_v0, cb0[0]
VEC8_madd vec_r3, vec_v1, cb0[1], vec_r3
VEC8_madd vec_r3, vec_v2, cb0[2], vec_r3
VEC8_clmp vec_r3, vec_r3, l(0.0), l(1.0)
VEC8_mul vec_o0, vec_r0, vec_r3
VEC8_mul vec_o1, vec_r1, vec_r3
VEC8_mul vec_o2, vec_r2, vec_r3
VEC8_mov o3, l(1.0)
且Context以Core为单位组成共享的结构,同一个Core的多个ALU共享一组Context,
如果有多个Core,就会有更多的ALU同时参与shader计算,每个Core执行的数据是不一样的,可能是顶点、图元、像素等任何数据:
由于SIMT技术的引入,导致很多同一个SM内的很多Core并不是独立的,当它们当中有部分Core需要访问到纹理、常量缓存和全局内存时,就会导致非常大的卡顿(Stall)。
如果有4组上下文(Context),它们共用同一组运算单元ALU。
假设第一组Context需要访问缓存或内存,会导致2~3个周期的延迟,此时调度器会激活第二组Context以利用ALU。
当第二组Context访问缓存或内存又卡住,会依次激活第三、第四组Context,直到第一组Context恢复运行或所有都被激活。
延迟的后果是每组Context的总体执行时间被拉长了,越多Context可用就越可以提升运算单元的吞吐量。
NV shader thread group提供了OpenGL的扩展,可以查询GPU线程、Core、SM、Warp等硬件相关的属性。如果要开启次此扩展,需要满足以下条件:
OpenGL 4.3+;
GLSL 4.3+;
支持OpenGL 4.3+的NV显卡;
下面是具体的字段和代表的意义:
// 开启扩展
#extension GL_NV_shader_thread_group : require (or enable)
WARP_SIZE_NV // 单个线程束的线程数量
WARPS_PER_SM_NV // 单个SM的线程束数量
SM_COUNT_NV // SM数量
uniform uint gl_WarpSizeNV; // 单个线程束的线程数量
uniform uint gl_WarpsPerSMNV; // 单个SM的线程束数量
uniform uint gl_SMCountNV; // SM数量
in uint gl_WarpIDNV; // 当前线程束id
in uint gl_SMIDNV; // 当前线程束所在的SM id,取值[0, gl_SMCountNV-1]
in uint gl_ThreadInWarpNV; // 当前线程id,取值[0, gl_WarpSizeNV-1]
in uint gl_ThreadEqMaskNV; // 是否等于当前线程id的位域掩码。
in uint gl_ThreadGeMaskNV; // 是否大于等于当前线程id的位域掩码。
in uint gl_ThreadGtMaskNV; // 是否大于当前线程id的位域掩码。
in uint gl_ThreadLeMaskNV; // 是否小于等于当前线程id的位域掩码。
in uint gl_ThreadLtMaskNV; // 是否小于当前线程id的位域掩码。
in bool gl_HelperThreadNV; // 当前线程是否协助型线程。
利用以上字段,可以编写特殊shader代码转成颜色信息,可视化了顶点着色器、像素着色器的SM、Warp id,为我们查探GPU的工作机制和流程提供了途径,以便可视化窥探GPU的工作机制和流程。下面正式进入验证阶段,将以Geforce RTX 2060作为验证对象,加入扩展所需的代码,并修改颜色计算:
#version 430 core
#extension GL_NV_shader_thread_group : require
uniform uint gl_WarpSizeNV; // 单个线程束的线程数量
uniform uint gl_WarpsPerSMNV; // 单个SM的线程束数量
uniform uint gl_SMCountNV; // SM数量
in uint gl_WarpIDNV; // 当前线程束id
in uint gl_SMIDNV; // 当前线程所在的SM id,取值[0, gl_SMCountNV-1]
in uint gl_ThreadInWarpNV; // 当前线程id,取值[0, gl_WarpSizeNV-1]
out vec4 FragColor;
void main()
{
// SM id
float lightness = gl_SMIDNV / gl_SMCountNV;
FragColor = vec4(lightness);
}
接着修改片元着色器的颜色计算代码以显示Warp id:
// warp id
float lightness = gl_WarpIDNV / gl_WarpsPerSMNV;
FragColor = vec4(lightness);
// thread id
float lightness = gl_ThreadInWarpNV / gl_WarpSizeNV;
FragColor = vec4(lightness);
得到如下画面:
为了方便分析,用Photoshop对中间局部放大10倍,得到以下画面:
结合上面两幅图,也可以得出一些结论:
通过其那面介绍的逻辑管线层面和硬件执行层面,可以总结出: