GPU精粹2——高性能图形芯片和通用计算编程技巧 流式编程 1

http://book.csdn.net/
现代的GPU,在计算历史中第一次把数据并行、流式计算平台放入几乎每台台式计算机和笔记本电脑中。一些最近的学术派研究论文——以及本书的其他章节——演示了这些流式处理器有能力加速范围很广的应用程序,而不仅仅是它们本来所针对的实时渲染。然而,要利用这个计算能力需要使用一个对很多程序员来说是陌生的,完全不同的编程模型。本章探寻了在CPU和GPU编程之间最基本的差别之一:存储器模型,它不像传统的基于CPU的程序,基于GPU的程序对什么时间、什么地点和怎么访问存储器有一些限制。本章提供了GPU存储器模型的概览,并解释了基本的数据结构,如多维数组、结构体、列表和稀疏数组在数据并行编程模型中是如何表达的。
33.1  流式编程
现代的图形处理器能够加速的东西远不止实时计算机图形。关于在图形处理单元(GPGPU)上通用计算的新兴领域的近期工作已经显示了GPU能够加速流体力学、高阶图像处理、真实感渲染,甚至计算化学和生物等大量应用程序(Buck等2004,Harris等2004)。除了实时渲染的目的之外,使用GPU的关键是将它视为一个流式的、数据并行的计算机(参见本书的第29章,以及Dally等2004)。在GPU程序中构建计算和访问存储器的方式非常受到流式计算模型的影响。同样,在讨论基于GPU的数据结构之前首先简要这个模型。
比如GPU这样的流式处理器在编程上与CPU串行处理器方式完全不同。大多数程序员熟悉的编程模型是他们可以在程序的任何地方写到存储器中的任何位置。相反,当对一个流处理器编程时,以更加结构化的方式访问存储器。在流式模型中,程序被表示成对数据流的连续操作,如图33-1所示。流(即一个数据的有序数组)中的元素由核(即一个小的程序)中的指令处理。核在流中的每个元素上操作,并把结果写入一个输出流。
图33-1  以数据依赖图给出的流式程序
流式编程模型的约束让GPU以并行的方式运行核,因此同时可以处理许多数据元素。这种数据并行性的保证来自于确定在一个流元素上的计算不会影响在相同流的其他元素上的计算。结果,能在核计算中用到的值只能是那个核的输入和全局存储器的读取。除此之外,GPU要求核的输出是独立的:核不能执行对全局存储器的随机写入(换句话说,它们只可能写到输出流的一个流元素位置)。这个模型给予的数据并行性是GPU的速度能比串行处理器更快的基础。
节点是核,边是数据流。核并行地处理所有数据元素,并把它们的结果写给输出流。
下面有两个样例代码片断,演示了该如何把一个串行的程序转变成一个数据并行的流式程序。第一个样例演示了在串行处理器上循环过一个数组(例如,图像中的像素)。注意循环体中的指令每次只作用于一个数据元素:
for (i = 0; i < data.size(); i++)
  loopBody(data[i])
下一个例子演示了同样的代码段,用流式处理器的伪代码写成:
inDataStream = specifyInputData()
kernel = loopBody()
outDataStream = apply(kernel, inDataStream)
第一行指定了数据流。在我们的图像示例中,流是图像中的所有像素。第二行指定了计算的核,这只是来自第一个样例的循环体。最后,第三行把核应用到输入流中的所有元素,并把结果存到输出流中。在图像示例中,该操作会处理整个图像并产生一个新的,变换过的图像。
目前的GPU片段处理器还有一个超出了前面描述的流式模型的编程限制。目前的GPU片段处理器是单指令、多数据(SIMD)的并行处理器。这在传统上的意思是所有的流元素(片段)必须由相同的指令序列处理。最近的GPU(支持像素着色器 3.0 [Microsoft 2004a])稍微放松了这个严格的SIMD模型,允许使用变长循环和有限的片段级分支。然而,由于硬件基本仍是SIMD,因此分支在片段间必须有空间一致性才能高效地运行(更多的信息请参见本书的第34章)。目前的顶点处理器(顶点着色器 3.0[Microsoft 2004b])是多指令,多数据(MIMD)的机器,因此能比片段处理器更高效地运行核分支。虽然灵活性比较差,但片段处理器的SIMD结构非常高效和划算。
因为现在几乎所有的GPGPU计算都在比较强大的片段处理器上执行,所以基于GPU的数据结构必须适合于片段处理器的流和SIMD编程模型。因此,在本章中的所有数据结构都用流来表示,而且在这些数据结构上的计算都是SIMD、数据并行核的形式。
33.2  GPU存储器模型
相对于串行微处理器的主存储器、cache和寄存器,图形处理器有它们自己的存储器体系结构。然而,这个存储器体系结构是针对加速图形操作设计的,适合于流式编程模型,而不是通用、串行的计算。而且,图形API,如OpenGL和Direct3D更进一步地把这个存储器限制成只能使用图形专用的图元,如顶点、纹理和帧缓冲区。本节给了一个在当前GPU上的存储器模型的概览,以及基于流的计算如何适合于它。
33.2.1  存储器体系结构
图33-2演示了CPU和GPU的存储器体系结构。GPU的存储器系统建立了现代计算机存储器体系结构的一个分支。GPU与CPU类似,有它自己的cache和寄存器来加速计算中的数据访问。然而,GPU自己的主存储器也有它自己的存储器空间——这意味着在程序运行之前,程序员必须明确地把数据复制入GPU存储器。这个传输传统上是许多应用程序的一个瓶颈,但是新的PCI Express总线标准可能使存储器在CPU和GPU之间共享在不远的将来变得更为可行。
图33-2  CPU和GPU的存储器体系结构
33.2.2  GPU流类型
与CPU存储器不同,GPU的存储器有一些用法的限制,而且只能通过抽象的图形编程接口来访问。每个这样的抽象可以想像成不同的流类型,各流类型它自己的访问规则集。GPU程序员可以看到的3种流类型是顶点流、帧缓冲区流和纹理流。第4种流类型是片段流,在GPU里产生并完全消耗。图33-3演示了一个现代GPU的流水线,3个用户可访问的流,以在流水线中它们可以被用到的地点。
图33-3  现代GPU中的流
图注:GPU程序员可以直接访问顶点、帧缓冲区和纹理。片段流由光栅器产生,并被片段处理器消耗。它们是片段程序的输入流,但完全是在GPU内部建立和消耗的,所以对程序员来说是不能直接访问的。
1. 顶点流
顶点流通过图形API的顶点缓冲区指定。这些流容纳了顶点位置和多种逐顶点属性。这些属性传统上用作纹理坐标、颜色、法线等,但是它们可以用作顶点程序的任意输入流数据。
顶点程序不允许随机索引它们的输入顶点。直到最近,顶点流的更新还只能通过把数据从CPU传到GPU来完成。GPU不允许写入顶点流。然而,最近的API增强已经使GPU可能写入顶点流。这是通过“复制到顶点缓冲区”或“渲染到顶点缓冲区”来完成的。前一项技术,把渲染结果从帧缓冲区复制到顶点缓冲区中;在后一项技术中,渲染结果直接写到顶点缓冲区中。最近增加的GPU可写顶点流功能使GPU第一次可以把来自流水线末端的结果接入流水线的开始。
2. 片段流
片段流由光栅器产生,并被片段处理器消耗。它们是片段程序的输入流,但是因为它们完全是在图形处理器内部建立和消耗,所以它们对程序员来说是不能直接访问的。片段流的值包括来自顶点处理器的所有插值输出:位置、颜色、纹理坐标等。因为有了逐顶点的流属性,传统上使用纹理坐标的逐片段值现在可以使用任何片段程序需要的流值。
片段程序不能随机访问片段流。允许对片段流随机访问会在片段流元素之间产生依赖,因此打破了编程模型的数据并行保证。如果算法要求对片段流的随机访问,那么这个流必须首先被保存到存储器,并转换成一个纹理流。
3. 帧缓冲区流
帧缓冲区的流由片段处理器写入。它们传统上被用作容纳要显示到屏幕的像素。然而,流式GPU计算用帧缓冲区来容纳中间计算阶段的结果。除此之外,现代的GPU可以同时写入多个帧缓冲区表面(即多个RGBA缓冲区)。目前的GPU在每个渲染遍中最多可以写入16个浮点标量值(可以预计这个数值在未来的硬件上会增加)。
片段或顶点程序都不能随机地访问帧缓冲区的流。然而,CPU通过图形API可以直接读写它们。通过允许渲染遍直接写入任意一种类型的流,最近的API增强已经开始模糊帧缓冲区、顶点缓冲区和纹理的区别。
4. 纹理流
纹理是惟一一种可以被片段程序和Vertex Shader 3.0 GPU顶点程序随机访问的GPU存储器。如果程序员需要任意地索引入一个顶点、片段或帧缓冲区流,它们必须首先将它转换成一个纹理。纹理可以被CPU或GPU读取和写入。GPU通过直接渲染到纹理而非帧缓冲区,或把数据从帧缓冲区复制到纹理存储器来写入纹理。
纹理被声明为1D、2D或3D流,并分别用1D、2D或3D地址寻址。一个纹理也可以声明为一个立方图,它可以被当成6个2D纹理的数组。
33.2.3  GPU核的存储器访问
顶点和片段程序(核)是现代GPU的驮马。顶点程序在顶点流元素上操作,并将输出送到光栅器。片段程序在片段流上操作,并把输出写入帧缓冲区。这些程序的能力由它们能执行的运算操作和它们能访问的存储器所定义。GPU核中可用的多种运算操作接近于在CPU上可用的操作,然而有很多的存储器访问限制。如同先前描述的,大部分这些限制是为了保证GPU必需的并行性以维持它们的速度优势。然而,其他的限制是进化中的GPU体系结构造成的,几乎将无疑地会在将来的世代中得到解决。
下面是在支持Pixel Shader 3.0和Vertex Shader 3.0功能(Microsoft 2004 a,b)的GPU上,顶点和片段核的存储器访问规则列表:
●   没有CPU主存访问;没有磁盘访问。
●   没有GPU栈或堆。
●   随机读取全局的纹理存储器。
●   读取常量寄存器。顶点程序可以使用常数寄存器的相对索引。
●   读/写临时寄存器。
–寄存器对正在处理的流元素来说是局部的。
–没有寄存器的相对索引。
●   从流输入寄存器中流读取。
–顶点和读取顶点流。
–片段和读取片段流(光栅化的结果)。
●   流写入(只在核的尾端)。
–写入地点由该元素在流中的位置决定。
不能写入计算出的地址(即不能散列)。
–顶点核写到顶点输出流。
最多可以写入12个四分量的浮点值。
–片段核写到帧缓冲区流。
最多可以写入4个四分量的浮点值。
从前述的规则集和在33.2.2小节中描述的流类型浮现出的另一个的访问模式是:指针流(Purcell等,2002)。指针流源于可以使用任意输入流作为纹理读取地址的能力。图33-4演示了指针流只是值为存储器地址的流。如果指针流是从纹理中读取的,那么这个能力叫做依赖纹理(dependent texturing)。
图33-4  用纹理实现指针流
 

你可能感兴趣的:(GPU精粹2——高性能图形芯片和通用计算编程技巧 流式编程 1)