Warp

我们知道,NVIDIA的GPU中线程调度的基本单位是Warp,一个Warp包含32个线程。

Warp的出现是为了隐藏指令执行的时延。

假设执行指令如下,

1
2
3
4
add r2, r0, r1 // r0 + r1 -> r2
add r5, r3, r4 // r3 + r4 -> r5
load r6 [r5] // mem[r5] -> r6
mul r8 r6, r7 // r6 * r7 -> r8

我们先看没有Warp时的执行情况(假设load指令执行需要3个时钟),

上图中,每个箭头代表一个执行的线程,不同的颜色代表不同的指令。因为ADD Unit有4个通道,也就是说可以同时进行4个加法操作,所以每个时钟周期可以同时完成4个线程的加法操作。我们可以称这样的执行方式为SIMT-4

可以看到,T3和T4没有可执行的指令,执行流水线中出现“气泡”。

现引入Warp。

Warp0中包含4个线程:线程0、1、2、3;
Warp1中包含4个线程:线程4、5、6、7;

如果上面的4条指令来自于顶点着色程序,那么每个线程对应于一个顶点的着色程序。也就是说,如果待渲染的顶点数为8,那么需要创建2个Warp,每个Warp渲染4个顶点(4个线程)。

在Warp0执行到load指令时,切换到Warp1便可以让计算资源尽可能的忙起来,而不像之前每遇到长延时指令时流水线必“卡顿”。

原先T3和T4没有硬件工作,而现在他们执行着Warp1的指令0和指令1.

执行效率得到了提升。

但是,如果执行Warp0的load指令时,发生了Cache Miss而导致3个时钟根本无法取回数据时,仍然会存在气泡存在,而这往往是比较常见的现象,如下图,

上图虽然只有T6和T7没有硬件工作,但真实的情况是,可能是数百个时钟。

在线程数量足够多的情况下,此时可以继续切换新的Warp来执行,和之前的做法一样。

这里,提供另外一种做法:增加一个Warp中的线程数量。

现在我们如果将一个Warp的线程数量从4提升到8,而硬件资源保持不变,执行情况会是这样,

这可以很大程度减少“气泡”出现的概率。

除此之外,还有一个好处是:Warp的后4个线程执行时不再需要取指和译码。如下图,

这样给取指和译码的优化提供了更多的可能,也就是你可以把取指和译码做的更加复杂,更进一步,可以把指令集设计的更加复杂,当然这并不一定是一个好的选择。

细心的你会发现,在T0时刻,只有ADD Unit工作,而另外两个硬件单元是空闲的,那怎样把他们也尽可能的用起来?

一种做法是每个时钟发射多条指令,也就是指令级并行。常见的做法有VLIW和超标量。

这里介绍另外一种NVIDIA的Fermi GPU的做法:同时发射两个Warp,如下图,

可以看到Warp1的指令0不需要等到T6再执行,在T4的时候就可以开始执行,从而提高了硬件利用率。

那到底Warp中线程数量的设置越大越好还是越小越好?

我们知道,NVIDIA的Warp大小是32个线程,AMD的Wavefront大小是64个线程,ARM的Quad大小是4/8/16个线程。其中Warp、Wavefront、Quad是不同厂商的不同叫法。

当Warp中线程数量设置较小时,可以减小遇到分支指令时的“惩罚”。下面我们介绍为什么。

影响SIMT执行效率的一个重要因素是:分支指令。假设一个包含分支指令的程序如下,

1
2
3
4
5
6
7
if (i > 0) {
r = a + b;
} else {
a = a + 1;
r = a + c;
}
r = r + 1;

如果Warp中的所有线程都满足i > 0,则分支判断后执行的指令为,

1
2
r = a + b;
r = r + 1;

如果Warp中的所有线程都不满足i > 0,则分支判断后执行的指令为,

1
2
3
a = a + 1;
r = a + c;
r = r + 1;

如果Warp中的线程1~10满足i > 0,而线程11~32不满足,那么执行的指令为,

1
2
3
4
r = a + b; // 线程1~10执行,线程11~32不执行
a = a + 1; // 线程1~10不执行,线程11~32执行
r = a + c; // 线程1~10不执行,线程11~32执行
r = r + 1;

可以看到,当一个Warp中的线程遇到分支指令执行到不同的分支时,执行效率会下降,不同分支中的指令数量较多的情况下效率下降尤其明显。

而当Warp中的线程数量较少时,线程走向不同分支的概率就会下降。

这就解释了为什么当Warp中线程数量设置较小时,可以减小遇到分支指令时的“惩罚”。

但Warp中线程数量设置较小时,会降低芯片面积的利用率。这个问题,读者可以自行思考一下。

头脑风暴之如何做自主可控GPU

整体思路

  1. 做好GPU产品
  2. 发展GPU生态

做好GPU产品

由于GPU技术壁垒高,如果从头开始做,势必跟不上时代。因此,

首先,参考和学习,然后吸收,再快速验证。
其次,自主创新。

参考学习

学习可以分为两个部分:基础知识和业界知识。

基础知识

  • 计算机体系结构
    • 指令集架构
    • 总线
    • 处理器
    • 存储系统
    • 其他
  • 计算机图形学
  • 算法
    • 人工智能
    • 机器学习
    • 高性能计算
  • 硬件知识
  • 其他

业界知识

  • 业界知名大会
  • 专利
  • 官方资料:博客、白皮书等
  • 论文
  • 人才引进
  • IP引进
  • 其他

自主创新

打好基础是自主创新的前提。如果不希望一直”望其项背”,那么自主创新是”弯道超车”的最好方式。这需要我们具备敏锐的洞察力以及快速学习和产品化的能力。

  • 在一些细分领域是否能够做的更好?
  • 对一些新出现的好的算法和技术(如图计算、RISC-V等),能否使用GPU支持或者让GPU更加高效或好用?

发展GPU生态

构建一个良性发展的生态,是GPU发展的必经之路。

GPU优化

通过阅读本文,你将知道:

  1. 什么是GPU优化
  2. 优化GPU的思路是怎样的
  3. 常见的优化GPU的方法有哪些

介绍

在做GPU优化之前,我们需要知道:

  1. 定位到瓶颈是前提。优化非瓶颈阶段,相当于做无用功
  2. 瓶颈总是存在的,而且通常是动态变化的。在GPU上运行某应用程序,总有一个阶段是瓶颈,而且随着运行不同的应用程序,瓶颈也是动态变化的
  3. 尽量不要过度优化。只需要优化到当前阶段不是瓶颈了即可
  4. 如果我们说应用程序处理阶段是瓶颈,代表着在一帧的渲染过程中,它大部分时间都是是整个流水线中最慢的阶段。
  5. 如果瓶颈已经不能再优化了,那么可以让其他阶段做更多的事情。类似于单位时间内可以渲染的帧的数量已经无法提升,那么可以提高每一帧的渲染质量。

瓶颈定位

瓶颈定位的思路,

  1. 为待测阶段设计若干测试用例,各测试用例在该阶段的工作量递减,其他阶段的工作量保持不变。如果帧率提升,那么该阶段很有可能就是瓶颈。
  2. 为待测阶段设计若干测试用例,各测试用例在该阶段的工作量不变,其他阶段的工作量递减。如果帧率保持不变,那么该阶段很有可能就是瓶颈。

GPU流水线可以大致划分为四个阶段,分别为:应用程序处理阶段、几何处理阶段、光栅化阶段和像素处理阶段。接下来,我们详细介绍每个阶段的可用的瓶颈定位方法。

应用程序处理阶段

  • 直接查看CPU的使用率
    • 如果CPU使用率维持在100%或者接近100%,则可以简单地认为应用程序处理阶段是瓶颈。
    • 这种方式有的时候不靠谱,因为有可能CPU是在等待GPU完成一帧的渲染。
  • 架空GPU的情况下查看CPU的使用率
    • 架空GPU方式可以用一个空的驱动程序。
    • 这种方式的缺点是:检测不到驱动程序的处理导致的瓶颈问题以及CPU和GPU交互导致的瓶颈问题。
  • 让CPU降频或超频运行
    • 如果降频导致性能相应地等比例降低,那可以认为应用程序处理阶段是瓶颈。超频是类似的。

几何处理阶段

  • 增加顶点属性
    • 增加顶点属性(如纹理坐标)相当于增加了顶点抓取的数据量,如果增加后GPU性能下降则可以认为顶点抓取便是瓶颈。
  • 增加染色程序的大小
    • 增加染色程序的长度之后,如果GPU性能下降则可以认为顶点处理是瓶颈。
    • 需要注意,要避免编译器优化掉添加的无效指令。

光栅化阶段

  • Shadow Map Generation这个功能使用的像素染色程序非常简单,使用这个功能的时候,光栅化和像素合并阶段都有可能成为瓶颈。
  • 在渲染小三角形比较多的场景时(如草地或树叶),光栅化可能会成为瓶颈。验证方法是:增加染色程序的大小。如果渲染一帧的时间没有增加,那么光栅化阶段便是瓶颈。

像素处理阶段

  • 降低屏幕分辨率
    • 如果把屏幕分辨率降低可以显著提升帧率,那么很有可能像素处理阶段便是瓶颈。
  • 增加片段染色程序的大小
    • 增加染色程序的长度之后,如果GPU性能下降则可以认为像素处理是瓶颈。
    • 需要注意,要避免编译器优化掉添加的无效指令。
  • 简化片段染色程序
    • 简化片段染色程序之后,如果一帧的渲染时间下降明显则可以认为像素处理是瓶颈。
  • 降低纹理大小
  • 修改缓冲区的位深度
  • 打开关闭混合
  • 改变混合模式
  • 渲染头发、草地、树叶等(像素染色程序简单

优化瓶颈

对于应用程序处理阶段,

  • 提高代码运行效率
  • 提高存储访问效率
  • 减少存储访问

对于几何处理阶段,

  • 优化染色程序,尤其是光照相关
  • 顶点预加载
  • 顶点数据压缩

对于光栅化阶段,

  • Early Z
  • 开启背面消隐

对于像素处理阶段,

  • Early Z
  • 开启背面消隐
  • 数据压缩
  • 像素和深度缓冲区合并
  • Forward Pixel Killing
  • Transaction Elimination

总结

  • GPU优化是找到GPU的瓶颈阶段并使得该阶段不再是瓶颈的过程,以及让非瓶颈阶段做更多有意义的事情的过程。
  • 瓶颈定位的思路:
    • 为待测阶段设计若干测试用例,各测试用例在该阶段的工作量递减,其他阶段的工作量保持不变。如果帧率提升,那么该阶段很有可能就是瓶颈。
    • 为待测阶段设计若干测试用例,各测试用例在该阶段的工作量不变,其他阶段的工作量递减。如果帧率保持不变,那么该阶段很有可能就是瓶颈。
  • 简单介绍了应用程序处理阶段、几何处理阶段、光栅化阶段和像素处理阶段的常见瓶颈定位和优化方法。

Arm发布Mali-G76 GPU

原文:Arm Announces Mali-G76 GPU: Scaling up Bifrost

两年前,Arm 发布了一个全新的 GPU 架构:Bifrost ,以及采用 Bifrost 架构的第一款 GPU:G71 。然而 G71 在 麒麟 960 和 Exynos 8895 上的表现都非常不佳。

而今年发布的 G72 在性能和效能方面的表现终于达到了之前 Bifrost 架构所承诺的样子。装载 G72 的麒麟 970 和 Exynos 9810 整体效能提升了 100% 。

今天 Arm 宣布了基于 Bifrost 架构的新一代 GPU IP:Mali G76. 它的目标很明确:赶超其他竞争对手。所以它在性能、功耗、面积方面做了很多优化。

Arm 承诺,集成了使用 TSMC 7 纳米工艺的 G76 的 SoC 在性能上可以提升 50%。

仔细对比我们会发现三个关键提升点,第一点是 30% 的性能密度提升。这意味着相同面积的芯片,性能可以提升 30%,或者保持性能不变的情况下,芯片面积可以变得更小。

由于功能单元的合并,Bifrost 微架构的效率提升了 30% 。效率是 Arm 当前尤其需要注重的,特别是在过去几年走了不少弯路的情况下,而且竞争对手高通在这期间在 3D 游戏领域发展迅猛。

最后,G76 在机器学习性能提升了 2.7 倍,这得益于往指令集中加入了特定的 8 位点乘(dot product)指令。

G76的一个很大的改动是:从 4 通道的SIMD单元扩宽为 8 。这意味着每个执行引擎中ALU单元的个数多了一倍。每个通道执行独立的FMA或者ADD/SF流水线。Warp的大小也从 4 变为 8 。总的来说,与其他GPU架构相比之前这种 4 通道设计属于非常窄的了。

这是一个非常有趣的变动,因为一般Warp的大小对于某个架构而言总是预定义好的,不会像Bifrost这样,中间改动这个值。长期存在的GPU架构中,尤其是PC领域的GPU,Warp的大小很多年都是不变的。NVIDIA自2006年的G80以来,Warp的大小一直是32 。AMD从之前的GCN架构开始至今,Warp的大小一直是64 。

在进一步介绍之前,我们回顾一下Arm的Warp大小采用4的原因。这在《Mali-G71 article from 2016》中有过介绍。

关于Warp的称呼不同的GPU厂商的叫法不一样,具体参看下表。

Bifrost的Warp的大小设置为4是比较有趣的,因为与其他厂商相比(通常16到32),小了很多。

Warp的设计从一定程度上反映了面对面积和性能而做出的不同权衡。如果Warp中线程数量为32,那么32个线程共用一套控制逻辑,而对Bifrost而言,需要8套控制逻辑。但从另一方面看,Warp中线程数量越多,我们越难填充满这些线程。

在ARM的GPU设计哲学中,更加注重避免执行卡顿,从Warp大小的设置就可见一二。更小的WARP大小意味着不同线程走向不同分支的概率更低。虽然分支处理很容易,但是这种现象会对性能产生一定影响。

Arm曾说,他们将Warp大小设置为4是为了在出现分支情况的时候尽量减少空闲ALU。理论上,这是一个合理的策略。假设你的程序中存在很多的分支语句,那么确实会有很多时候部分ALU闲置。架构师们在这个问题上花费了很多精力,特别是在桌面级GPU领域。但通常,他们一旦做出了某个选择,那么他们会一直坚持采用,因为开发者也会针对这个问题做自己的优化。

然而,更小的Warp意味着更多的控制逻辑,这样就增加了控制逻辑和ALU的比率。每一个SIMD的执行背后,都需要很多硬件单元来支持,如:缓存、调度单元、数量流控制以及其他硬件单元。这些单元通常功能固定,增加Warp的大小的时候,不需要增加这些支撑的硬件。这便是Mali-G76所作出的权衡。

Arm将4通道SIMD扩宽为8通道的结果是,提升了ALU和控制逻辑的比例。在G76中,在扩宽了SIMD宽度以及增加了执行引擎的数据吞吐量之后,执行核的面积与G72相比只增加了28%。站在整个GPU的角度,这是一个提升面积利用率的选择。

虽然Arm作出这次改变的原因在官方介绍中并未明显提及,但我们的猜测是:Bifrost原先的4通道SIMD的设计是过于热心了(overzealous),在真实代码中不同分支的情况并没有出现的如此频繁,以致于要采用如此少的SIMD通道来优化这样的问题。从Arm得到的说法是:现如今运行在GPU上的代码已经不像G71那时候了。

这样的改变可以在相同面积的芯片上放入更多的ALU。

但这样的改变也需要更加智能的编译器来支持。编译器需要确保8通道的SIMD能够更好的填充任务。而对于上层应用开发者而言,这个改变没有什么影响,因为他们只需要关注标准API以及ARM提供的驱动即可。

加宽SIMD通路的同时,还需要加大Cache以及数据通路。G76每个线程可用的寄存器数量为64,和G72一样,从这个角度来看,寄存器的压力保持不变。

为了匹配8线程的Quad,Arm将纹理带宽以及纹理单元的硬件个数都加了一倍。一个Shader Core可以同时输出2个像素以及2个纹素。新的纹理单元称为“Dual Texture Unit”以区分开之前的纹理单元。

这样做的结果就好像Arm将两个G72的核揉成了一个G76的核。在相同的时钟频率下,通用计算、纹素、像素的吞吐量都翻番了。理论上性能也会相应的提升一倍。整个芯片的面积反而比之前的两个核的面积要少很多。G76在和G72相同性能的情况下,面积是之前的66%,大大提高了面积的使用率。

Arm还在ALU里面加入了int8点乘的支持。这个操作在机器学习中是非常重要的。需要强调的是,虽然之前的Bifrost架构已经支持int8的数据类型(把4个int8打包成一个32位的数据),但G76是第一个把他们利用起来,可以在一个时钟周期内完成一个点乘操作。

这个优化的带来了2.7倍的机器学习的性能提升。这个数字当然只是一个近似值,实际取决于任务的情况。

Arm在机器学习上下了大注,所以明显提升机器学习的算力为他的客户提供了一个新的选择:高效地处理他们的神经网络。

Arm调研后发现,影响扩展性的另一个潜在因素是Tiler,也就是说当核扩展到一定数量后,Tiler会成为性能瓶颈。Tiler成为瓶颈的主要原因是在将Polygon List写回显存的时候速度限制。Arm解决这个问题的方法是从顺序写回变为乱序写回。Arm对这个实现方式持保密态度,大概是因为从顺序写回变为乱序写回不是一件简单的事情。

此外,Arm还详细介绍了该如何使用Tile Buffer,以使很多数据操作都避免访问显存。在某些特定情况下, 允许应用程序将深度缓存当颜色缓存使用。Arm这样做的原因是,存在很多开启多目标渲染而不使能MSAA的情形,不启用MSAA意味着深度的Tile缓存只有部分使用,而Multiple render target迅速占用颜色的Tile缓存。这样做的好处是避免了访问主存的频率,而访问主存通常是一个昂贵的操作。

G76的Local Storage机制也被优化了,主要优化的是处理寄存器溢出的逻辑。GPU会尝试将溢出的数据块组织到一起,以便将来可以更容易的访问到。

从性能上,我们可以说一个G76的核相当于两个G72的核。这也使得G76的配置中,可配置的最大核心数从之前的32个变为20个。

当把核心数都配置为最大,G76相比G72的最高性能提升为25%。需要指出的是,目前还没有使用G71或者G72的厂商将核心数配置为32个,最多的也只有20个G71,那便是Exynos 8895。

把执行核中的功能单元合并以提升GPU的PPA是十分亮眼的。

各大GPU IP提供商的竞争从未停止。尽管高通的Adreno 630在能耗效率上变现不佳,但仍然可以在明年发布的下一代中弥补,更别提性能上的提升,这将帮助高通重新获得GPU IP的领导地位。

总结一下,Mali-G76在相同面积和功耗的情况下性能提升了30%。这是一个不错的表现。尽管这能很大提升Mali系列GPU的竞争力,但是还不足以赶超其竞争对手。

微架构的变化方面,Arm将核合并的选择十分不错。当前Arm的核是可配置的,这是一把双刃剑。一方面,这给了客户更多选择,但也产生了不可避免的设计上的冗余。

Mali-G76证明了改进的方法之一可以是去掉冗余的控制逻辑。Arm推出了一款旗舰SoC,搭载了12个核,而我认为核数太多了。反观4核的Adreno 540、2核的Adreno 630、3核的Apple A11 GPU,不难理解为什么Mali在功耗和面积上落后这么多了。我期望Mali可以进一步将每个核中的计算资源提升,这样也许可以进一步提升性能,从而减小和其他竞争对手之间的差距。

Running CUDA Samples Based on GPGPU-Sim

CUDA Samples

CUDA Samples是示范CUDA编程概念的软件包。自CUDA 4.1开始,CUDA Toolkit搭载了CUDA Samples安装包。CUDA Samples的目录结构如下:

  • 0_Simple
  • 1_Utilities
  • 2_Graphics
  • 3_Imaging
  • 4_Finance
  • 5_Simulations
  • 6_Advanced
  • 7_CUDALibraries
  • EULA.txt
  • Makefile
  • bin
  • common

Incompatible GCC Version

如果你安装的是CUDA Toolkit 9.0, 它所兼容的GCC Version最高到 gcc-6. 如果你的环境是Ubuntu 19.10,那么在编译CUDA Samples之前,你需要降级GCC Version到6或以下,方法如下:

1
2
sudo apt install gcc-4.8
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 10

update-alternatives 更新/etc/alternatives目录下的gcc链接指向/usr/bin/gcc-4.8, 它的最后的参数10是指priority.

NOTE: 安装CUDA Toolkit时会进行GCC Compatibility检查,但是可以通过下面选项忽略这个检查:

1
sudo sh cuda_9.0.176_384.81_linux.run --override

Build Samples

构建CUDA Samples可以在顶层目录构建所有Samples, 也可以到单个目录下构建一个Sample. 为了能够让可执行文件能够链接到GPGPU-Sim的libcudart.so,需要这样运行构建命令:

1
make NVCCFLAGS="--cudart shared"

Run Samples

在 Run Samples 之前,需要配置GPGPU-Sim的仿真参数和libcudart.so的路径:

1
2
cp /path/to/gpgpu-sim/configs/tested-cfgs/SM2_GTX480/* /path/to/executables
source /path/to/gpgpu-sim/setup_environment

现在可以让CUDA Samples运行在GPGPU-Sim仿真器上了。

Documents

1
find /usr/local/cuda-9.0 -name '*.pdf' -printf '%f\n'

通过上面的命令可以找到介绍如何编写Samples以及相关的图形学原理的文档,亦包括CUDA编程的相关文档,可供初学者参考。

References:

0. GPGPU-Sim安装过程

图形渲染管线

通过阅读本文,你将知道:

  1. 图形渲染的基本过程是怎样的?
  2. 计算机图形学视角下的图形渲染管线是怎样的?
  3. OpenGL视角下的图形渲染管线是怎样的?
  4. GPU芯片设计者视角下的图形渲染管线是怎样的?

图形渲染过程

图形渲染过程可以类比于给模特拍照的过程。

首先我们需要选择一个模特,给模特化妆,选择合适的首饰服装。然后将各种物品以及模特安排在场景的合适位置。接着摄影师调整灯光亮度,设置好相机的各种参数(角度,曝光度等)。最后按下快门,经过打印之后一张图片就生成了。

总结下一张图片的生成过程,大致可以分为四步,分别为:

  1. 建模
  2. 场景构建
  3. 视景体选择
  4. 绘制

第一步 建模

建模是构建模型的过程。在上述场景中模特以及模特周围的物品都可以看成一个个的模型,选择模特、装扮模特、选择物品都属于建模的过程。

第二步 场景构建

场景构建是搭建我们要渲染的整个场景的过程。包含将各个模型放置在合适的位置以及设置好场景的全局属性(如光照、背景色等)。

第三步 视景体选择

视景体选择是选择场景展现范围和效果的过程。相机的不同位置、不同角度影响场景的展现范围;相机参数的设置影响场景的展现效果。

第四步 绘制

绘制是将图像最终展现到我们眼前的过程。虽然我们已经按下快门,选择了要展现的场景,但是由于打印颜色种类和质量的限制、打印机的设置或者洗照片的化学药剂的成分设置、纸张的长宽比和大小的不同,最终我们看到的图形也会千差万别。

计算机图形渲染

计算机图形是用计算机产生的图像。

在给模特拍照的例子中,人、物、场景都是现实中已经存在的,而在计算机图形渲染过程中,他们都需要计算机自己构建。

那怎样用计算机高效地渲染出真实图形呢?计算机图形学应运而生!

计算机图形学就是研究如何在计算机中表示图形,以及利用计算机进行图形的计算、处理和显示的相关原理和算法。

Computer Graphics is the art of science of producing graphical images with the aid of computer. - IEEE

计算机在图形渲染方面有其局限性。相对于图形渲染过程中的海量数据处理,计算机的计算能力是有限的;计算机图形显示设备是以离散形式显示图形的,比如分辨率为1080p的显示器,其包含的是1920x1080个像素点;计算机的计算精度有限等等。

由此便衍生出来很多优化算法,如:纹理、背面消隐、提前的深度模板测试等。

另外由于人眼的视觉特性,计算机也可以适当偷点懒。如帧率只需要达到34帧/秒,人眼便没有了卡顿感(游戏画面要求达到60帧/秒);人眼对运动中的画面的细节感知能力很低等。

纹理是最常用的优化措施。例如渲染一个游戏场景中的一棵树,与其使用成千上万个顶点构建这颗树的模型,不如直接使用一张纹理图片来代替。这样做可以极大的提高渲染效率。

从上述可知,计算机图形渲染的输入有两类,分别为:顶点输入和像素输入;计算机图形渲染的输出为:帧缓冲区中的图像。

帧缓冲区中的图像像素会由显示控制器以固有的频率读到显示器上进行显示。

计算机图形渲染分为实时渲染和非实时渲染。所谓实时,即快速响应。通常游戏画面的渲染都是实时渲染,计算机制作的电影画面的渲染都是非实时渲染。一个大型游戏的任何时刻截一张图都非常清晰,而电影画面中的高速运动物体的截图则是模糊的。

计算机图形学中并没有明确地定义图形渲染管线,其更多的是研究如何使用计算机更快更好的渲染图形,多偏向于算法方向。图形渲染管线将在后续章节以OpenGL为例进行讲解。

OpenGL渲染管线

渲染管线可分为可编程渲染管线和固定功能渲染管线。详细介绍请参看《GPU的渲染架构、渲染管线和渲染模式分类》
)

关于OpenGL渲染管线,我们可以在各种地方看到这个关键词:计算机图形学、OpenGL官方网站、各大GPU厂商的官网等等。你会发现他们说的都不太一样。

计算机图形学视角看OpenGL渲染管线

OpenGL最初的内部流程是一组有序的处理步骤,这些步骤被组织为一个双通道的渲染流水线。流水线阶段当然是固定的—也就是说,它们无论接受到什么样的输入数据都会执行特定的操作—因此,这样的工作方式成为固定功能的OpenGL流水线。

图中,黑色方框部分一般称为几何流水线,Geometry Pipeline;橘色方框部分一般称为像素流水线,Pixel Pipeline。

图中,绿色方框部分是可编程的。应用程序通过使用Shader来控制这些阶段内所进行的操作。Shader指的是一些比较短的程序段,它们被加载到OpenGL程序中,并最终加入到OpenGL流水线的适当的处理单元中,替换掉流水线中原来的固定功能。

程序员视角看OpenGL渲染管线

使用OpenGL编写应用程序或渲染引擎的程序员所面对的是OpenGL标准API。

在介绍OpenGL标准API展示的渲染管线之前,我们先了解一下OpenGL中的坐标表示。

OpenGL坐标系

常见的坐标系有:

  • 建模坐标系。盒子是什么形状
  • 世界坐标系。把盒子放在整个场景中的哪个位置
  • 观察和投影坐标系。从哪个角度位置看这个盒子
  • 规范化设备坐标系。指定为一个设备无关的坐标系
  • 设备坐标系。将最终的图形显示到什么样的设备上

OpenGL渲染管线

OpenGL官方WIKI上将渲染管线分为7个步骤,分别为:

  1. Vertex Specification:指定顶点数据
  2. Vertex Processing:顶点处理
  3. Vertex Post-Processing:顶点后处理
  4. Primitive Assembly:图元装配
  5. Rasterization:光栅化
  6. Fragment Shader:片段着色
  7. Per-Sample Processing :逐样本处理

GPU设计厂商视角看OpenGL渲染管线

和程序员一样,GPU设计厂商面对的也是OpenGL标准API,不同的是,前者是如何使用它们,后者是如何实现它们。

在考虑如何实现他们之前,得先考虑该GPU是应用在什么场景。桌面端的GPU更注重性能,而功耗和面积则非重点考虑因素;移动端的GPU更注重功耗和面积,性能相较于桌面端的GPU则不那么高…

总而言之,GPU设计者需要根据应用场景在功耗、性能和面积组成的三角形中选择一个“完美”的点作为“质心”。

驱动程序

GPU设计厂商提供给用户的除了GPU显卡之外,还有驱动程序。驱动程序运行在CPU上。

通常,驱动程序会对OpenGL API进行预处理。OpenGL API会被逐个放到命令缓冲区(软件概念),当遇到一个Draw Terminator做特定处理,当遇到一个Frame Terminator做特定处理。

Draw Terminator通常是:

  1. glDrawArrays系列
  2. glDrawElements系列
  3. glBegin

Frame Terminator通常是:

  1. glClear
  2. glFlush
  3. glFinish
  4. SwapBuffers
  5. glXMakeCurrent/glXMakeContextCurrent
  6. SwapLayerBuffers
  7. glFrameTerminatorGREMEDY

这里顺便提一下OpenCL中的Frame Terminator:

  1. clFlush
  2. clFinish
  3. clWaitForEvents
  4. cl_gremedy_computation_frame

上述的“特定处理”,不同的GPU是完全不一样的。但他们的核心目的都是将OpenGL API转换成GPU硬件认识的,可以让GPU硬件高效工作的指令和数据。

GPU硬件渲染管线

上图是GeForce6800的硬件架构图。其处理流程为:从主机接收到驱动程序发送的指令和数据,先进行顶点着色。然后着色后的顶点进行图元装配,再对图元进行背面消隐、剪裁和光栅化。光栅化产生的片段送到片段着色器进行片段着色,此过程中有可能会用到纹理。着色后的片段再送到ROP单元进行深度模板测试、混合等操作,最后将像素写回帧缓冲区。

上述过程看着和OpenGL官网描述的渲染管线挺像的。其实不然。上图中“ZCull”模块可以提前进行深度模板测试。

理论上,我们甚至可以在OpenGL API和GPU硬件之间再做一层封装,使得GPU硬件和OpenGL描述的渲染管线的逻辑模型的关联很小。随着GPU架构的发展,GPU硬件和OpenGL描述的渲染管线的逻辑模型的关联也确实越来越小。

尝试设计过GPU的人,应该遇到过显示列表这个“坑”。站在程序员角度,显示列表这个特性无可厚非,把一堆OpenGL API放在GPU,方便随时调用。但是GPU设计者要实现这个功能时非常痛苦。

Khronos Group在2015年游戏开发者大会上推出了Vulkan。相对于 OpenGL,Vulkan™ 大幅降低了 CPU 在提供重要特性、性能和影像质量时的“API 开销”,而且可以使用通常通过 OpenGL 无法访问的 GPU 硬件特性。

由此可见,Khronos Group在API设计上,也是对GPU设计者越来越友好。或者说,Khronos Group中有越来越多的GPU设计者参与进来。应用程序开发者的学习成本会略微增加。

总结

  1. 计算机图形学视角看OpenGL渲染管线,是算法模型;程序员视角看OpenGL渲染管线,是逻辑模型;GPU设计厂商视角看OpenGL渲染管线,是硬件(架构)模型。
  2. 图形渲染管线可以分为顶点获取、顶点处理、图元生成、图元处理、片段生成、片段处理共六个阶段。

参考

CPU和GPU的区别

通过阅读本文你可以知道CPU和GPU的区别是什么。

结构

上图是CPU和GPU的结构对比图。通过对比可以看出:

  1. CPU的计算单元较复杂,但是个数较少;GPU的计算单元相对简单,但是个数很多
  2. CPU的控制单元较复杂;GPU的控制单元相对简单,分布在各计算核
  3. CPU的片上缓存较大;GPU的片上缓存较小,分布在各计算核

为何他们的结构会差别这么大?这还得从他们的设计目标说起。

设计

我们知道,GPU最开始是专门用来做图像渲染的。图像渲染有以下特点:

  1. 被渲染的场景由非常多的顶点组成
  2. 顶点组成的场景会被光栅化成非常多的片段
  3. 对每个顶点或片段的处理逻辑都是一样的

这类数据可以称作流数据。流数据具有数据量大和每个数据分量的处理逻辑类似的特点。NVIDIA的GPU中有一个概念是Stream Processor,即流处理器。流处理器处理的数据即为流数据。

所以,GPU的设计目标是提高数据吞吐量。

CPU要处理的任务从一开始就特别复杂,它得保证具有复杂控制逻辑的程序快速运行,而且还得支持多任务。多任务要求CPU的运行频率较高,这样才能让用户感觉多个任务是在并发执行。

所以,GPU的设计目标是低时延。

总结

  1. CPU需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转和中断的处理。这些都使得CPU的内部结构异常复杂。而GPU面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境。
  2. CPU是几个教授组成的,而GPU是很多个小学生组成的。
  3. CPU采用基于低延时的设计;GPU采用基于高吞吐量的设计。

参考

https://www.cnblogs.com/biglucky/p/4223565.html

Vulkan学习计划

本文记录的是个人的Vulkan学习计划,仅供参考。

制定Vulkan学习计划的思路是:

  1. 找学习资料
  2. 根据学习资料掌握Vulkan整体脉络
  3. 根据整体脉络制定学习计划
  4. 执行学习计划

找学习资料有一个原则:找第一手资料或权威的资料。这是为了避免走弯路。

资料整理

官网

  1. 官网:https://www.khronos.org/vulkan/

  2. 说明文档主页:https://www.khronos.org/registry/vulkan/

    1. 说明文档:https://www.khronos.org/registry/vulkan/specs/
  3. 示例程序:https://github.com/KhronosGroup/Vulkan-Samples

  4. 学习指引:https://github.com/KhronosGroup/Vulkan-Guide

NVIDIA

  1. NVIDIA Vulkan:https://developer.nvidia.com/Vulkan
  2. 培训资料:https://developer.nvidia.com/nvidia-vulkan-developer-day

AMD

  1. AMD Vulkan:https://www.amd.com/zh-hans/technologies/vulkan

IMAGINATION

  1. IMAGINATION Vulkan:https://www.imgtec.com/developers/vulkan/

Qualcomm

  1. 高通 Vulkan:Qualcomm Adreno Vulkan - Qualcomm Developer Network
  2. How to Start Using Adreno SDK for Vulkan

ARM

  1. ARM Vulkan:https://developer.arm.com/solutions/graphics/apis/vulkan

学习计划

待添加

GPU性能指标

通过阅读本文,你将知道GPU的性能指标都有哪些以及他们是如何计算出来的。

GPU性能指标

在GPU信息数据库-GPU Specs Database,可以查找到几乎所有GPU的信息。GPU信息中有一栏代表的是GPU的性能参数:Theoretical Performance。

通过阅读这一栏数据我们可以知道:

  1. 这些数据都是理论上的峰值
  2. 性能指标一般有:纹理填充率像素填充率浮点处理能力

理论上的峰值

所谓理论上的峰值可以理解为理想情况下的峰值,GPU在实际运行过程中几乎都跑不出该峰值。

在得出这个理论上的峰值的过程中,做了很多假设。

假设一,一个时钟完成一次处理。如:一个时钟完成一个像素的渲染,实际上一个时钟是完不成一个像素的渲染任务的,反而根据我们写的片段染色程序的复杂度,可能需要上百甚至更多的时钟。

假设二,数据已准备好。如:像素渲染的输入数据总是可以立马获取到,实际并非如此。

假设三,任务调度和逻辑控制不消耗时钟。

至于这个理论上的峰值是如何计算得来的,请参看下面章节的介绍。

纹理填充率

纹理填充率 = 纹理单元运行的时钟频率 x 纹理单元的个数 x 每个时钟纹理单元可以处理的纹素个数(理论值)

比如NAVIDA的GeForce GT 430

  • 纹理单元运行的时钟频率为:700 MHz
  • 纹理单元的个数:16个
  • 每个时钟纹理单元可以处理的纹素个数:1个

所以它的纹理填充率为11.20 GTexel/s。

像素填充率

像素填充率 = ROP运行的时钟频率 x ROP的个数 x 每个时钟ROP可以处理的像素个数

比如NAVIDA的GeForce GT 430

  • ROP运行的时钟频率:700 MHz
  • ROP的个数:4个
  • 每个时钟ROP可以处理的像素个数:1个

所以它的像素填充率为2.800 GPixel/s

浮点处理能力

浮点处理包含半精度、单精度和双精度浮点的处理。

下面以单精度浮点处理能力为例:

单精度浮点处理能力 = 渲染核运行的时钟频率 x 渲染核的个数 x 每个渲染核包含的单精度浮点处理单元的个数

比如NAVIDA的GeForce GT 430

  • 渲染核运行的时钟频率:1400 MHz
  • 渲染核的个数:96个
  • 每个渲染核包含的单精度浮点处理单元的个数:2个(这个数值是逆推出来的,原网页没有该信息)

所以它的单精度浮点处理能力为268.8 GFLOPS

注意:每个渲染核包含的单精度浮点处理单元的个数是逆推出来的。在本例中逆推出来是2,实际上还有可能是1,因为如果该单精度浮点处理单元支持乘加操作的话,一个浮点乘法和一个浮点加法,是两个浮点操作。

总结

  1. GPU的性能指标包含:纹理填充率、像素填充率和浮点处理能力
  2. 纹理填充率的计算方式为:纹理单元运行的时钟频率 x 纹理单元的个数 x 每个时钟纹理单元可以处理的纹素个数(理论值)
  3. 像素填充率的计算方式为:ROP运行的时钟频率 x ROP的个数 x 每个时钟ROP可以处理的像素个数
  4. 单精度浮点处理能力的计算方式为:渲染核运行的时钟频率 x 渲染核的个数 x 每个渲染核包含的单精度浮点处理单元的个数
  5. GPU性能指标的数值都是理论值
  6. 知道了GPU性能指标的计算方式,我们可以从GPU厂商公布的数据中逆推得到一些有用的信息

参考

GPU的渲染架构、渲染管线和渲染模式分类

通过阅读本文你将能够知道:

  1. 统一渲染架构和分离式渲染架构是什么以及二者的区别
  2. 固定功能渲染管线和可编程渲染管线是什么以及二者的区别
  3. 立即渲染模式和基于TILE的渲染模式是什么以及二者的区别

NVIDIA GPU历史简单介绍

NVIDIA在1999年推出GeForce 256时,提出了GPU这个概念。

随后NVIDIA陆续推出了GeForce 2系列、GeForce 3系列、GeForce 4系列、GeForce 5系列、GeForce 6系列、GeForce 7系列和GeForce 8系列的GPU。

在推出GeForce 3系列GPU时,NVIDIA采用了可编程的渲染管线。此前的GPU都是固定功能的渲染管线。

在推出GeForce 8系列GPU时,NVIDIA采用了统一渲染架构。此前的GPU都是分离式渲染架构。

也是在推出GeForce 8系列GPU时,NVIDIA开始给GPU的架构命名。架构名都是以在科学发展历史中做出过突出贡献的人的名字来命名。GeForce 8系列的架构名称为Tesla,这是NVIDIA第一个采用统一渲染的架构。

随后NVIDIA又陆续推出了FermiKeplerMaxwellPascalVoltaTuring、Ampere等架构。

Fermi架构是NVIDIA第一个支持通用计算的架构。

在上面的介绍中出现了一些概念:可编程渲染管线、固定功能渲染管线、统一渲染架构、分离式渲染架构,在下面的章节中将详细介绍这些概念。这里提出来是为了让大家看到这些概念出现的时间顺序:

  1. 固定功能渲染管线
  2. 可编程渲染管线
  3. 分离式渲染架构
  4. 统一渲染架构

固定功能渲染管线和可编程渲染管线

一开始,GPU设计者想要完成顶点渲染这个功能,那么他会选择一个顶点光照算法,如Phong,然后使用硬件实现该算法。这样做的好处很明显:速度快。但一旦你选择了Phong光照算法,那么这个GPU就只支持这个算法,因为GPU的硬件已经固定死了。即使出现了一个更好的光照算法,你也无法更新。这个时候的GPU几乎所有的算法都是硬件加速实现,更准确地说,是把硬件加速的图形算法单元整合在一起组成一个GPU。

然后,可编程渲染管线应运而生。顶点着色和片段着色是可编程的,着色程序都运行在一个微处理器上。通常,这个微处理器被称为Shader Core。用户可以通过编写不同的染色程序自定义图形渲染的效果,这是一件令人兴奋的事情!

分离式渲染架构和统一渲染架构

由于顶点着色程序对精度要求较高,而片段着色程序要求较低,并且一般情况下,少量的顶点会生成大量的片段,所以GPU设计者设计了两类Shader Core:一类专门处理顶点着色程序,称为顶点着色器;另一类专门处理片段着色程序,称为片段着色器。并且顶点着色器的数量少于片段着色器的数量。这便是分离式渲染:顶点着色和片段着色在各自专门的硬件单元中进行。

慢慢地会发现,处理小三角形比较多的场景时,顶点着色器利用率很高,而部分片段着色器空闲;处理大三角形比较多的场景或者渲染纹理较多的场景时,顶点着色器部分空闲,而片段着色器利用率很高。

那是不是可以设计一个Shader Core,它既可以做顶点着色,也可以做片段着色以提高硬件资源的利用率?答案是肯定的。

统一渲染由此诞生!

基于统一渲染架构,Shader Core被挖掘出了更多的使用方法,比如通用计算。如果GPU被用来做通用计算,那么GPU中的图形算法硬件加速器(比如光栅化)是不工作的。

立即渲染模式和基于TILE的渲染模式

芯片架构通常要考虑三个核心要素:功耗、性能和面积。我们可以简记为PPA(Power、Performance、Area)

一个好的GPU架构需要针对GPU产品的应用场景,在PPA组成的三角形中选择一个好的平衡点。比如移动端的GPU更加注重功耗和面积,而桌面端的GPU更加注重性能。

立即渲染模式的GPU侧重于性能,而基于TILE的渲染模式的GPU侧重于功耗。因此,前者常见于桌面级GPU,而后者常见于移动端GPU。

二者的详细介绍,请参看我的另一篇博客《基于TILE的渲染》。

总结

  1. 如果顶点/片段着色算法是使用固定的硬件加速器实现,那么该GPU采用的就是固定功能的渲染管线;如果顶点/片段着色算法是可编程替换的,那么该GPU采用的就是可编程的渲染管线。
  2. 如果GPU中存在两种类型的Shader Core,一种只能运行顶点着色程序,另一种只能运行片段着色程序,那么该GPU就是分离式渲染架构;如果GPU中的所有Shader Core既可以运行顶点着色程序,也可以运行片段着色程序,那么该GPU就是统一渲染架构。
  3. 如果GPU渲染逻辑中,顶层遍历元素是图元,那么该GPU采用的是立即渲染模式;如果GPU渲染逻辑中,顶层遍历元素是TILE,那么该GPU采用的是基于TILE的渲染模式。

参考