标题:一二三四五六七八九十一二三四五六七八九十
标题:摩尔的预言 唯有CUDA才是终极的CPU
作者:小熊在线-宁道奇
关键词:Intel AMD CPU GPU CUDA Pentium 处理器 摩尔 out-of-order 乱序执行 GeForceFX GPGPU Dirichlet Voronoi Shader BrookGPU GFLOPS stream kernel DirectX OpenGLARB OpenGL NV3x x86 API warp block grid PTX Ct
正文:
【→开篇前言:终极的CPU←】
开篇前言:终极的CPU
现在很多人在谈论GPGPU和CUDA这样的通用计算技术,几乎很多硬件的评测,都要跟这个扯上点关系。但是那些泛泛而谈的文章根本没说到点儿上。在NVIDIA发布GT200系列显示芯片之后,这种基于GPU的通用进算更High到了前所未有的高潮。就连Intel也放胆,标榜其下一代独立显卡Larrabee有多么超强的通用计算能力。今天小编我就来跟大家深入到CUDA的内部,为大家诠释为什么说:唯有NVIDIA CUDA才是终极的CPU。
让我们先回溯过去的一段时间,早在2003年,Intel和AMD都在不遗余力的为生产出性能强大的处理器而打拼着。由于两大公司的激烈竞争,在短短几年的时间里,处理器的主频时钟速度有了显著的提升。尤其是到了Intel的Pentium 4时代,处理器的时钟频率狂飙到了一个前所未有的高度。
傻快傻快的奔腾4
但是随着制造工艺技术的制约,这种时钟频率的提升很快就碰到了壁垒。从2001年至2003年,Pentium 4处理器的时钟频率从最初的1.5GHz提升到了3GHz。然而从2003年到2005年,整整2年的时间里,处理器的时钟频率提升的速度骤然放缓,只是从3GHz增加到3.8GHz。
处理器频率大战在2005年画上了休止符
在最后,Intel到了强弩之末之时仍然不肯放弃。他们充分的优化了处理器的架构,使之可以运行在更高的频率上,Prescott核心就是这种造神运动的产物。不过Intel所面对的问题已经不再是工业制造技术的问题了,芯片制造商所面对的是最简单最基本的物理定律的壁垒。一些评论家甚至提出摩尔定律也将就此失效。不过事实远非如此,只是摩尔定律最初的意思被很多人曲解罢了。摩尔定律所述的是有关在一定面积内的晶体管的集成度。只是长期以来,处理器晶体管集成度的提高都会伴随着性能的提升和时钟频率的提高罢了。这必然很容易让无知的人将摩尔定律的本源所曲解。在后来,情况又变得更加复杂了。随着处理器架构的革新,晶体管的集成度也越来越高,并且正向着一条死胡同儿发展。
【→摩尔定律与运算效能←】
摩尔定律与运算效能
CPU制造商正在尽力寻找解决问题的方法时,GPU制造商却迎来了前所未有的机会,显示核心比处理器更试用摩尔定律。
GPU的机会来了
为什么Intel不能在CPU中运用相同的设计,让处理器跑的更快呢?其实原因非常简单,处理器的设计宗旨就是要从指令流中得到最高的处理效能。由此处理器的设计师们使用了很多附加的措施,例如将数据分为浮点、整数等不同的数据类型,嵌入随机存储装置,使用分支预测技术等等。而优秀的处理器架构就能在同一时间处理更多条指令,就是传说中的并行处理。由此,Pentium处理器中引入了超标量执行的概念,在某些情况下,每个时钟周期可以处理2条指令。而在Pentium Pro处理器时代,又迎来了out-of-order乱序执行的概念。为了更好的利用执行单元,处理器可以乱序执行指令,进一步提高指令处理的效能。可是问题是指令流是连续的,它会大大制约并行执行单元的效率。因此盲目的在处理器中增加并行执行单元是毫无用处的,他们可能在绝大多数时间处于空闲状态。这也是目前困扰多核心处理器的一大问题。
反过来说,GPU在运作时就非常之简单了。它的工作就是要生成一组多边形,同时产生一组像素填充到多边形中去。多边形和像素之间的处理操作是独立的,因此他们能使用并行处理的思想,在处理流水线中尽可能的加入更多的并行执行单元来加快处理的速度。这就意味着GPU将会有更多的执行处理单元处于忙碌状态,这会比CPU具备更高的处理效能。
简单而强大的GPU
GPU与CPU另一个不同之处就是记忆体。GPU与显存之间的联系极为亲密。当GPU要读取一个材质单元时,需要使用几个时钟周期读取临近的材质单元。并且当写入一个像素的时候,也需要花费几个时钟周期参考临近的单元。这种机制对于内存来说是非常有利的,理论上讲它可以更加合理的利用显存带宽。GPU与CPU不同之处在于,GPU不需要庞大的高速缓存。缓存的目的仅仅是加速材质的处理速度。几KB的空间就能包含所需要的三线和双线过滤文件。
【→横空出世:GeForce FX←】
横空出世:GeForce FX
CPU与GPU是完全两个独立的世界,这种分离持续的时间相当长。我们使用CPU来进行办公、互联网冲浪等应用。而GPU仅仅是为了更快的绘制出漂亮的图像。但是一个理念的出现,彻底的改变了传统。GPU具备了可编程性。这让CPU感到了一种前所未有的恐慌。第一款被称作可编程的GPU是NVIDIA的NV20,就是传说中的Geforce3,不过它远远不能对CPU的地位构成威胁。它能够处理的程序指令还非常有限,大约在10条左右,另外它所能处理的数据类型也非常之有限,只能处理9或12bit固定长度的数据。
传说中的GeForce FX
当NVIDIA的NV30发布的时候,摩尔定律再次得到了印证,不仅仅是晶体管数量的增加,计算单元的数量也成倍增长。同时这也使得显卡芯片更具灵活性。因此NVIDIA的NV30具备许多革命性的突破。虽然游戏发烧友可能并不会像跪拜Voodoo 6000那样将一块GeForce FX当作自己最心爱的收藏,但是它真的让GPU看上去不仅仅是一颗图形加速器。
从NV30起,GPU开始支持单精度浮点运算,即使它并不完全支持IEEE754标准,但这消息却让许多程序员感到振奋人心。另外它还支持超过数以万记的指令处理。这一点是最吸引研究人员的地方,他们可以通过一些途径找到突破芯片生产工艺技术的壁垒,制造出性能更为强大的芯片。
【→降临凡间:GPGPU技术←】
降临凡间:GPGPU技术
使用图形加速器的架构设计思想,运用到数学计算中去,这种考量并不是进期才萌生而出的。事实上,追溯到20世纪90年代,就有一些研究员萌生了一些想法。只是简单的利用基于GPU硬件布线级别的功能,比如利用光栅和Z-buffers来计算弹道轨迹或绘制Voronoi图表。这也算是对GPU最为原始的通用计算应用点子。
Voronoi图,又叫泰森多边形或Dirichlet图,它是由一组由连接两邻点直线的垂直平分线组成的连续多边形组成。N个在平面上有区别的点,按照最邻近原则划分平面;每个点与它的最近邻区域相关联。Delaunay三角形是由与相邻Voronoi多边形共享一条边的相关点连接而成的三角形。Delaunay三角形的外接圆圆心是与三角形相关的Voronoi多边形的一个顶点。Voronoi三角形是Delaunay图的偶图。
使用Voronoi图来绘制世界人口的密度
在2003年,GPU中又出现了另一个新的高级概念——Shader。这是GPU发展史上的又一次重大革新。这次GPU的硬件已经可以进行矩阵计算。在此后的1年时间里,SIGGRAPH美国计算机绘图专业组一直致力于GPU通用计算方面的研究工作。由此他们率先提出了一个名为GPGPU的概念,意思是具有通用计算用途的GPU。这可以说是GPU发展最具创意的里程碑。由此2003年年底,第一个通用计算程序BrookGPU也随之到来。
BrookGPU是斯坦福大学一个相当有趣的项目,它可以为你展现当前GPU的强大性能,虽然目前GPU尚不能完全取代CPU,但是这个编译器为GPU模拟CPU的一般应用提供了一个运行环境。
BrookGPU是一个编译器和实时系统,可以为当前GPU提供简单的类似C的编程环境。一个运行在NVIDIA GeForce FX 5900 Ultra的shader程序运算速度可达20 GFLOPS,相当于10GHz的Pentium 4,而且图形系统的内存带宽为25.3 GB/sec,相比Pentium 4只有5.96 GB/sec。
早在2003年,要想发挥GPU的运算优势,我们只有两种图形API可供选择:Direct3D 或 OpenGL。因此,研究人员想利用GPU强大的处理能力不得不去学习这些API的用法。问题是搞GPU通用计算开发的研究人员一般并不是专业的绘图程序员。这对于他们来说是一项非常严密而复杂的技术。需要熟悉3D应用程序中的Shader着色单元、材质单元、帧等等各种各样的概念。另外还要精通并行处理程序方面的技术例如:流处理,内核,分布式计算,集群计算等等。所以第一个难点就是类比GPU和CPU之间的差异,在两个完全不同的世界中,我们要寻找到相似的、共通的东西。
stream流——你可以把数据想象成会流动的液体,他们会在GPU中不断的穿梭往来。GPU使用纹理来描绘出场景。而在经典的CPU编程中,我们可以将它类比成一个数组。
kernel内核——它的功能将适用于每一个独立的stream流。它相当于是pixel shader像素着色单元。从概念上讲,它能够实现一个非常典型的程序内部循环,它适合处理较长较大的数据。
从一个应用程序的内核中的流读取执行结果,这一步就相当于在材质单元中进行渲染。很显然,处理器中并没有渲染这个概念,不过这可以看作是对内存的访问。
控制本地内存的写入地点,即分散操作。这可以使用vertex shader顶点着色单元来完成,因为pixel shader像素着色单元在处理时,不能修改像素的坐标值。
【→通用计算:鼻祖BrookGPU←】
通用计算:鼻祖BrookGPU
BrookGPU LOGO
上面的这张图片极具现代艺术气息。不过在通用计算业界,这张图片却代表了一场异常澎湃的革命。Brook程序制定了一套基于C语言的扩展指令集——“C with streams”。具体来说,Brook的目的就是将所有的3D API都封装起来,在应用程序中,充分的发挥GPU的并行处理能力,协助CPU完成繁重的计算任务。为此,Brook带有一个编译器,以*.br文件包含有C++代码和扩展代码,并且可以生成标准的C++代码。不会拘泥于任何3D API的限制,可以适用于多种3D API。无论是DirectX、OpenGL ARB、OpenGL NV3x还是x86。
BrookGPU 实现架构
Brook具备几个技术优势。首先让GPGPU走出了心里阴影,让程序员们知道使用GPU做通用计算并不是一件难事。事实上,当Brook发布的时候,当时就有许多IT网站发表社论,直接了当的说:CPU已死,GPU的性能太过强大,很快将会改变整个计算世界的格局。炒作这番言论已经是5年之前的事儿了,在五年之后的今天,这件事情仍然没有发生。这让我们明确了一点:GPU绝对不会取代CPU。不过从另一个方面看,CPU的发展史,并入了GPU的老路。现代处理器越来越看重并行处理效能,它们的内部集成了越来越多的处理核心,同时还在不断扩展着多线程技术和SIMD处理单元。而目前的GPU正好相反,朝着更庞大,更灵活的方向发展。GPU开始支持单精度浮点运算,开始支持整数运算,很快GPU就将充分支持双精度计算。这种发展的态势非常明显,无论是GPU还是CPU的发展路线,两者都会在某一时刻相交在一起。因此,最终将发生什么事情呢?GPU将吸纳CPU的功能,成为一个数学协处理器?目前,Intel和AMD都在紧锣密鼓的致力于这方面的研究。但在这期间,整个计算机和图形界的技术趋势也在悄悄地发生着变化。
让我们回到最初的话题,Brook最初的优势就是要普及GPGPU,不让3D API限制GPU的应用。Brook的出现,大大简化了迈入GPU通用计算的门槛,使得很多开始刚刚学习编程的人也能驾轻就熟。不过在另一方面,Brook仍然有很长的路要走,需要更多的完善,它才能使GPU通用计算成为可以信赖的高效能计算技术。
现在Brook发展所面临的问题已经不是跨API,跨平台方面的技术性问题了。它所面对的是目前繁多的3D API,要想将所有的API都整合到一起,所需的工作量相当庞大。但是随着3D技术的不断发展,真正的问题还是兼容性,这个问题已经发展到他们无法掌控的局面。考虑到各大GPU厂商之间的竞争,很多大公司都会在GPU中加入自己的特色技术,会有各种不同版本的驱动程序。即使是DirectX API也未在硬件方面完全统一过规格。针对游戏而优化的专门驱动,可以让游戏跑的更顺畅,而这些对于Brook的开发人员而言,却等于n个晚上的通宵工作。这使得想统一GPU通用计算的API变成了一件非常困难的事情。因此相当长的一段时期以来,Brook仍然只是高级研究员和程序员的玩物,并未得到更广泛的普及。
【→战神诞生:CUDA API←】
战神诞生:CUDA API
Brook至今仍未成功的主要原因,就是它并没有得到ATI和NVIDIA的足够重视。因为这两个巨人都看到了新的契机,在技术背后蕴含着更为广阔的市场。在这个市场已经不在那么关注他们的图形性能。
由此Brook的研究人员班底,很快就加入了在美国Santa Clara的开发团队,与NVIDIA全球的工程师一起,从事这个全新领域的研究工作。主旨就是提供一套软件和硬件相结合的整体高性能计算解决方案——CUDA。
NVIDIA开发人员众所周知的一个GPU的秘密:他们不想依赖任何的图形API接口,仅仅通过驱动程序与硬件通信。这就意味着可以解决所有Brook遇到的技术实现问题。由此CUDA(Compute Unified Device Architecture)通用计算架构就此诞生,可以说它是一个基于GPU软件层的通信技术。
CUDA API
从上图中,你也可以看到CUDA提供了两种不同类型的API接口:
1、一种是高级的API:CUDA运行的API
2、另一种是低级API:CUDA的驱动API
高级的API是在低级API之上所执行的,每一个调用的函数在运行的时候都可以被驱动API细分为若干个基本的指令。需要注意的是这两个API是互斥的,程序员只能使用其中的一个,这两个API的功能不可能被混在一起。这里所指的高级API也只是相对而言的。即使在程序处在空闲的时候,低等级的API仍然提供了一些基本的功能,例如做程序的初始化或者管理链接等等。但是不要指望这些API会变得很抽象,要想利用好CUDA的特性,对NVIDIA的GPU还需有些比较全面的认识才可以。
驱动API结合了更多的管理功能,它会请求GPU做很多处理工作。但它的好处是更加的灵活,它可以给程序员更多额外的控制。无论如何,这两种API都可以沟通OpenGL和Direct3D的资源。这会体现出许多好处,CUDA可以用来生产资源,比如生成几何图形,在程序中进行材质贴图等等,同时这些也可以传递到传统的图形API来生成。3D图形API也可以将渲染后的结果发送到CUDA进行后续处理。CUDA本身就是基于图形芯片,而这种图形芯片也具备通用计算的能力。这里有许多交互性的例子,在GPU的显存中存储数据将更具优势,系统可以绕过速度相对较慢的PCI-Express总线,直接调用显存中的数据。
另一方面需要指出的是,针对这种在显存内的资源共享来说,图形数据并不总是短小精悍的,并且也会给程序员带来一些头痛的问题。例如,转换分辨率或者颜色深度时,图形数据就有优先权。因此,如果在缓冲中的资源需要增加的时候,驱动程序会毫不犹豫的将应用程序分配给CUDA来执行。这样CUDA计算和图形处理就不会产生冲突。对于数据的分配和管理,CUDA还有待于更进一步完善。尤其是当我们的系统中有几个GPU的时候,我们首先就无法使用SLI模式了,我们只能用一颗GPU来完成显示工作。不过这也是避免系统混乱的最好办法。
最后再说一下CUDA的软件层,目前CUDA的2.0版提供了两个标准的数学运算库——CUFFT(离散快速傅立叶变换)和CUBLAS(离散基本线性计算)的实现。这两个数学运算库所解决的是典型的大规模的并行计算问题,也是在密集数据计算中非常常见的计算类型。开发人员在开发库的基础上可以快速、方便的建立起自己的计算应用。此外,开发人员也可以在CUDA的技术基础上实现出更多的开发库。
CUBLAS是一套函数库,它是基于模块化的运行在GPU上的线性计算函数库。它支持NVIDIA的GPU的计算资源。该库在API层次上是能够“自给自足”,即不必与CUDA直接交互。应用程序使用CUBLAS库的基本模型是在GPU内存空间中创建矩阵和矢量对象,使用数据填充它们,调用一定顺序的CUBLAS函数,最后把结果从GPU内存上载到主机。要实现这个过程,CUBLAS提供了一些帮助函数,用于在GPU空间中创建和销毁对象,以及对这些对象写入和读出数据。
【→通用计算:名词解析←】
通用计算:名词解析
在深入CUDA之前,让我们先了解一些基本的概念和定义,这些字眼都贯穿于NVIDIA的说明文档始终。古怪的NVIDIA工程师,选用的都是一些比较深奥的术语,凡人很难理解。首先,我们来在CUDA里定义thread线程的概念。因为这里所指的线程,与传统的“CPU线程”是有所区别的,同时也不是我们在GPU文章里所指的“线程”。在GPU中,线程是最基本的元素,它贯穿于数据处理的始终。与CPU中的线程不同,CUDA的线程是非常轻巧微小的,这就意味着,单独的线程处理起来会非常的简单快速。
第二个在CUDA说明文档中经常看到的词叫做warp,生硬的翻译成中文应该叫做轻纱织物。不要试图从字面理解warp的概念,因为它仅仅是一种象征性的比喻,一个由NVIDIA自创的术语罢了。NVIDIA的意思是CUDA的整个处理工作,就像是一架织布机,织物在织布机内快速的来回穿过。
NVIDIA的文档中规定:在CUDA中的一个warp,是由32个线程组成的。这也是SIMD处理中,数据的最小封包单位。CUDA采用的是多处理并行架构,它的主旨就是尽量能并行处理更多的数据。
并行计算很像是中国古代的纺车
通常warp这个封装尺寸,并不总能满足程序员的需要,因此在CUDA中还可以使用一种称作block块的容器来封装线程。如果程序员需要,可以在一个block块内包含64至512个线程。
最后,我们就使用一种叫做grid栅格的容器,将许多个block块封装起来。这种数据机制的优势就在于可以同时在GPU中处理多个block块。这种方式将GPU所有硬件资源都紧密的联系在一起。在稍后的文章中,我们还会对它进行详细的剖析。
在栅格中的块,可以完全的抽象化,不受任何约束。并且应用程序中的内核可以在一个单一的会话中调用海量的线程,而不用担心这些固定的资源。CUDA在运行的时候,会为你考虑好一切事情。这就意味着这种架构非常容易扩展。如果硬件可以利用的资源非常稀少,那么在执行块的时候,就会尽可能的顺序执行。如果它有非常庞大的处理单元,那么它就可以使用更多并行模式处理数据。这就意味着,这些代码可以适用于入门级的GPU,也可以适用于高端发烧级的GPU,甚至未来的GPU也不在话下。
另外当CUDA API运行的时候,还会涉及到使用指定的CPU,这里叫做host主机,与GPU类似,他们都被认为是一些device设备。上述这些,都是CUDA中的一些基本的概念,希望这些纯技术YY的东西没有吓跑各位读者,上帝与你们同在阿门……
【→架构解析:从硬件角度看CUDA(上)←】
架构解析:从硬件角度看CUDA(上)
如果你是一位小熊在线的忠实读者,那么你对NVIDIA最新的GPU架构技术一定并不陌生。如果你不经常看小熊在线的文章,那么我还是建议你多做一些功课,去了解一下NVIDIA最新的显卡特色技术。因为NVIDIA的CUDA架构与他们的GPU架构稍有不同,并且在这篇文章中,将为读者揭示一些NVIDIA从未提及的技术细节。
GPU的处理核心架构
从上面的这张架构图我们可以看到,NVIDIA的Shader核心是由若干个材质处理单元(TPC)的群组所构成的。例如一个8800GTX,它内部就有8个群组,而8800GTS内部就有6个群组。事实上,每一个群组都是由1个材质处理单元和2个流多重处理器构成的。而处理器又由一个前端的读取/解码单元,一个指令发送单元,一个由八个计算单元组成的组,和2个SFU超级功能单元所组成,他们在处理指令方面都属于SIMD单指令多数据流。同样这类指令也适用于所有warp中的线程。NVIDIA这种并行模式叫做SIMT单指令多线程执行单元。需要指出的是,后端的操作,比前端操作要高出2倍的时钟周期。在实际中,部分执行指令有可能有2倍长。16路的SIMD,就只能相当于8路的处理效能。
流多重处理器在运作模式下,允许每个时钟周期内,在流水线的前端,让一个warp处在准备执行的状态。在一个warp中的32个线程中的应用指令,在流水线的后端,需要用4个时钟周期来处理。但是这种操作在流水线的前端,仅仅需要1/2的时钟周期就可以完成,从这一点来看,仅仅需要2个时钟周期,就可以完成操作。
因此为了保证流水线前后端的速度一致,使得硬件的利用率最大化,CUDA推出了一种交替类型的指令周期。每个典型的指令使用一个时钟周期,而SFU的指令在另一个周期。
每个多重处理器也有一些确定数量的资源,以便可以更精确的榨取它们的性能。它们有一些很小的存储区域,这些区域被叫做共享内存。每一个多重处理器的共享内存仅有16KB大小。这并不是一个高速缓存,这是由程序自由管理的存储区域。这方面与Cell处理器上的SPU处理单元有些类似,可以进行本地的数据存储。这些技术细节是非常独特而有趣的,并且你可以在CUDA的文档中找到相关更详细的软件与硬件技术说明。这些存储区域并不是为了pixel shaders而设计的,他们主要针对的就是CUDA。
【→架构解析:从硬件角度看CUDA(下)←】
架构解析:从硬件角度看CUDA(下)
这个存储区域为block块中的线程提供了一种信息沟通的途径。它最重要的作用就是强调限制级别。所有的线程都保证被封装到一个block块中,这就保证了多重处理器可以有效的处理这些任务。反过来说,指派block块到不同的多重处理器中,是非常不确定的。这就意味着,在执行的时候来自不同block块中的线程是无法进行通信的,使用这种存储区是一件非常复杂的事儿。但是设计这些复杂的存储区,对于整体架构来说也是值得的。因为某些特殊的线程,可能会贸然访问主内存,这也许会产生很多冲突,而这种共享存储区可以快速的链接寄存器。
block块内部架构
这些共享的存储区并不是多重处理器唯一可以访问的存储设备。很显然,他们可以使用显存,但是相对于共享存储区来说,显存的带宽和速度都不如前者。因此,这种机制可以抑制对内存过于频繁的访问。NVIDIA也提供了多重处理器的高速缓存,可以存储常量和纹理,大致相当于每个多重处理器能分配到8KB的空间。
多重处理器内部架构
同时,多重处理器也具备8192个寄存器。在多重处理器中所有激活的block块中所有的线程,都可以通过这些寄存器共享信息。不过激活的warps总数被限制在24个,也就是768个线程。例如8800GTX最多可在同一时间处理12288个线程。在这方面加以限制,是为了不让GPU的资源消耗的太多,可以更合理的分配计算任务。
针对CUDA优化过的程序从本质上讲就是在block块的数量与他们的尺寸之间找到一种平衡。在一个block块中加入更多的线程,有利于提高内存潜伏期的效率,但是与此同时可以使用的寄存器数量就要少了。如果block块中的线程数太多了,比如达到512个线程的水平,那么整个流水线的执行效能就大大降低了。这仅仅够喂饱一个多重处理器的,浪费了256个线程的处理能力。因此NVIDIA建议每个block块使用128至256个线程,这是最为折中的办法,它会在内核的潜伏期与寄存器数量之间找到最好的平衡点。
【→架构解析:从软件角度看CUDA←】
架构解析:从软件角度看CUDA
从软件的角度来看,CUDA是由C语言的扩展组成,这种架构当然要追溯到BrookGPU的时代,并且许多API的编写工作,都是从BrookGPU继承来的。CUDA扩展和延伸了许多BrookGPU的函数和变量。这里有一个非常典型的关键字__global__,它是一个前缀,在它后面的内容意味着是一个内核。这个功能将被CPU和执行单元所调用。__device__,标记有这个关键词的程序将会被GPU执行。但是仅仅在GPU中被调用。最后再介绍一个关键词__host__,这个前缀是可选的,它会指派CPU调用一个函数,并且由CPU执行它,换言之,它就像是一个非常传统的CPU执行函数。
对于__device__ 和 __global__ 函数来说,也有一些限制条件。他们不能被递归,也就是说他们不能自己调用自己。他们不能拥有可变数目的自变量。最后,要说一下__device__关键词,它必须驻留在GPU的显存空间内。理论上来说,绝对不可能获得它的物理地址。变量在控制存储区域的时候,也会有一些限制。__shared__是一个变量的前缀,当程序中出现这个关键词的时候,就表明变量要保存在流多重处理器中的共享存储区中。这与通过 __global__关键词调用会有些不同。这是因为执行配置中规定,在调用时,要指明grid栅格容器的大小,并指明它在哪个内核中,并且要为每一个block块指派所包含线程的尺寸。我们可以看看下面的这个例子:
__global__ void Func(float* parameter);
which will be called as follows:
Func<<< Dg, Db >>>(parameter);
其中Dg是grid栅格的尺寸,而Db是定义block块,这两个变量是CUDA的一种新的类型。
CUDA API其本质上来讲是由各种操作显存的函数组成的。cudaMalloc用来分配内存,cudaFree用来释放内存,cudaMemcpy用来互相拷贝内存和显存之间的数据。
在最后,我们将介绍一下CUDA程序的编译方式。这是非常有趣的,整个编译过程需要几个阶段。首先,所有的代码都要让CPU来处理,这些都会从文件中提取,并且他们都会通过标准的编译器。用于GPU处理的代码,首先要转换成中间媒介性语言——PTX。中间语言更像是一种汇编程序,并且能够中和潜在的无效代码。在最后的阶段,中间语言会转换成指令。这些指令会被GPU所认同,并且会以二进制的形式被执行。
PTX中间媒介语言
【→检验真理:实践CUDA←】
检验真理:实践CUDA
一旦你仔细阅读过NVIDIA的文档,你就会对CUDA的效能产生深刻的印象。有什么比自己亲自动手尝试写一个CUDA程序更让人兴奋的事儿呢?即使一切理论,一切技术,看起来都是那么的完美,但是实践起来,所有让人头疼的问题也就会立刻显现出来。如果你想自己动手尝试写一个CUDA程序,或者想更深入的了解它,那么阅读官方的帮助文档是最有效的手段。
事实上,着手写一个小项目,其实也挺简单的。网络上有许多高品质的免费工具可以参考使用。比如Visual C++ Express 2005,它具有非常完善的开发环境,几乎可以满足我们所有的需要。
最难的部分就是找一个比较简单的题材,让程序编写和实现起来并不那么十分的困难,同时又能充分发挥出CUDA的并行处理效能。
最终我们采用了一个代码块,它可以截取一个地图的高度值,并且通过相应的计算输出成普通的地图。由于编程技术有限,我们就不加入一些繁杂的细节功能了。我们只是安排了一些简单的循环:处理每一个地图上的像素,并且我们采用了矩阵式的算法,比较相邻的像素的颜色信息,使用一些稍微复杂一些的公式,来生成像素的颜色值。这类程序基本上可以充分发挥出CUDA的并行处理优势,下面我们就来做一个CUDA运算效能的简单测试。
我们的测试软件分为CPU和CUDA两个版本。两个程序算法之间相差并不大,我们并没有针对CPU架构重新开发新的算法。
测试图
测试图
最后咱们先别着急比较CPU版本和CUDA版本之间的测试结果,还是了解一下手头的编程工具CUDA SDK。由于我们第一次尝试编写CUDA的程序,因此我们并没有抱很高的期望。并且我们在编程方面也不够专业,在核心算法部分的代码也没有针对CPU做一些适当的优化。所以直接比较他们的结果,会让我们非常的震撼。
【→意料之中:惊人的测试结果←】
意料之中:惊人的测试结果
不过,我们并没有处理的措施,也没有处理的时间,只能任凭程序编写完成后,看看CUDA是否真的有很大的计算优势。程序的测试和开发,都是在一台笔记本电脑上完成的。它的配置为Core 2 Duo T5450处理器和一颗GeForce 8600M GT的显卡。操作系统为Vista。这台笔记本的配置并不算高端,远远没有现在的高端台式机速度快。但是测试结果非常有趣,因为这台笔记本的GPU处理器能力远远不能与NVIDIA的主打高端GPU产品相比,没有夸张的散热片,没有GTX 280那样的强悍配置,但是这颗仅集成有70M晶体管的CUDA GPU,却显示出了惊人的能力。这与我们预想的测试结果相吻合。
采用分辨率为2048x2048像素的图片,测试结果如下
CPU 1 thread: 1419 ms
CPU 2 threads: 749 ms
CPU 4 threads: 593 ms
GPU (8600M GT) blocks of 256 pixels: 109 ms
GPU (8600M GT) blocks of 128 pixels: 94 ms
GPU (8800 GTX) blocks of 128 pixels / 256 pixels: 31 ms
从测试结果来看,CUDA GPU的处理效能确实非常的强大。不过细心的读者也会发现,懒惰的程序员并没有针对CPU的线程进行优化,没有修改CPU内部的执行线程。最理想的测试环境,是处理器内部运行的线程数量同CUDA一样多。不过CPU内部仅有两个核心,我们只能运行2个独立的线程,他们的运行效能是非常强大的。最后,公平的说,四个线程的版本,确实出乎意料的快。而实际的测试成绩和我们脑海中所期待的,没有什么差别。
理论上来说,由于处理器创造了额外的线程,所以处理的效能会稍稍损失一些。但现实恰恰相反如何解释这种情况呢,也许是跟Windows的线程调度程序有关,多线程使得处理器始终处在忙碌的状态下,充分的利用了处理器的资源。
在任何情况下,结果都是线程的数量越多越好。当处理更小的材质规格时(512X512以下),处理效能提升越显著,并且在四个线程版本中,这种情况更加明显。不过GPU的处理效能仍然要比CPU快的多,8600GT要比双线程版本的运算效能快3倍以上。
CUDA工具
另一个让人吃惊的测试结果就是:即使是最慢的GPU运算结果,也是最快的CPU运算结果的6倍。对于一个未经人事的CUDA处女作程序来说,这个结果相当挑逗人。同时也请各位读者注意,如果想得到更好的测试结果,那么我们就要使用更小的blocks块,反之也是如此。
这里我来做一个简单的解释,我们的程序每个线程使用了14个寄存器,并且每个blocks块中封装了256个线程。由此,每个blocks块至少需要3584个寄存器,在一个多重处理器中,768个线程就已经饱和了。
在这种情况下,三个blocks块将会占用10572个寄存器。但是多重处理器仅有8192个寄存器,在整个CUDA的流水线中,也仅仅能跑动2个blocks块。
稍微改动一下程序,一个blocks块包含128个线程,每个线程处理1个像素。那么每个blocks块我们只需需要1792个寄存器。如果用总过8192个寄存器,除以1792的话,那么可以看出我们可以在系统中同时跑4个blocks块。
在实践中,多重处理器的最佳的线程数为512个,而NVIDIA官方声称理论上最大为768。但是在GPU中执行的许多blocks块通过访问存储区进行数据通信就会更加的灵活,由此就不仅仅受限于寄存器的个数了。当操作更长的指令时会有延迟。系统能发布延迟指令,让另一个blocks块等待处理的结果,直到有可用的数据结果为止。四个blocks块的确会使系统具备更好的潜伏期,特别是当我们的程序造访若干个存储区的时候。
【→结果分析:CUDA还可以跑的更快←】
结果分析:CUDA还可以跑的更快
最后,尽管我们刚才说这个简单的CUDA程序测试并不是一次赛马。但是我们仍然无法抗拒8800GTX的诱惑力。我们还是忍不住在8800GTX上运行了这个程序,结果更是异常的惊人。8800GTX的通用计算结果,相当于移动版8600GT的三倍。而且测试的环境非常公平,并没有针对任何GPU做blocks块优化。
有贪心的读者也许并不满意这个结果,可能在期盼他们之间更大的性能差距,比如说4倍甚至更高。因为8800GTX具备128个ALU,要比移动版8600GT的32个ALU高出很多,并且他们的时钟频率也有较大的差异,8800GTX的核心频率为1.35GHz,而移动版8600GT仅有950MHz。但是在实际跑程序的时候,他们的性能差距并没有理论设想的那么大。
小编想,这里最有可能的一个假说就是:受限于显存的访问速度。更确切的说,对于最初的图片文件访问,像是在一个CUDA的多维数组阵列。这个名词听起来真是非常的深奥和复杂,其实只是一个纹理存取和运算的方法。它有着以下几个优势:
1、可以访问速度更快的纹理缓存
2、与CPU的数据操作方式不同,我们可以将数据封装在一个包里,不必用多边形处理图像文件。
由此,我们也可以利用免费的像素过滤机制给像素添加地址,例如在[0,1]的区间之内。而不必使用[0,宽度],[0,高度]这样冗长的数据格式。作为一个小熊在线忠实的读者,长期关注显卡频道的网友们都会知道,移动版8600具备16个材质处理单元,而8800GTX具备32个材质处理单元。因此他们两种体系架构之间有将近一倍的规格差距。并且由于两块显卡的核心频率不同,他们之间的性能差距也会拉大:
(32 x 0.575) / (16 x 0.475) = 2.4
这个比率接近3倍,这与我们实际的测试结果相吻合。这也就是为什么虽然他们的规格相差数倍,但是测试结果他们仅仅相差3倍。归根结底,都是由于G80材质处理单元中的ALU,成为了整个CUDA运算的瓶颈。与线程的数量和blocks块的设置并无太大关系。
性能分析工具
另外,还有更加令人鼓舞的成果。因为这是我们第一次编写CUDA程序,但是我们的运行环境并不是十分的理想。首先我们使用的是一台装有Vista操作系统的笔记本电脑,这就意味着我们只能使用还处在测试阶段的CUDA SDK 2.0版本。并且我们的NV显示驱动也仅仅是v174.55,这个测试驱动也是测试版。尽管如此,我们在编写、运行程序时,没有遇到任何的不愉快。只是在初次运行程序时,我们的程序有不少bug,程序企图访问已经分配的显存空间。显示器开始狂闪,接着就黑屏了。直到Vista自动重启了显示驱动服务,电脑的桌面才重新回到我们的眼帘。
但不得不承认,其实这也不足为奇,这仅仅是程序的一个小BUG,跟无数Windows应用程序一样,都是可以通过简单修复后补完的。
最后,我来谈谈对NVIDIA的一些小牢骚。NVIDIA CUDA所有文档,都在一本厚厚的百科全书大全中,我并没有找到一本稍微薄一些的快速入门指南。或者是一步步CUDA从入门到精通之类的文档,也没有针对Visual Studio的快速环境配置指南。在有不明白不理解的名词,登录NVIDIA的CUDA官方网站,居然没有一个关键词搜索框,我只能一篇一页的通读所有CUDA官网的文章。这里确实有失人性化。希望NVIDIA给广大的初学者,开更多方便之门,把CUDA的入门门槛降的再低一些就好了。
不过这些都是小问题,SDK中也有比较完成的程序例子可供各位参考探究。你可以从中找到如何构建一个迷你型CUDA应用程序的方法。对于初学者来说,这已经是一个不错的入门指南了。
【→最终结论:CUDA才是终极的CPU(上)←】
最终结论:CUDA才是终极的CPU(上)
NVIDIA自从发布CUDA之后,就推出了GeForce 8800系列显示卡。在当时,NV承诺的性能是极端的诱人,但是我们要时刻保持警惕,别被商家给忽悠了。毕竟这是个先河,还从来没有人搞过GPGPU。而且目前可以用的资源还太少,对于GPGPU的研究也仅仅是在初期阶段。不过我要说的是,GPGPU的潜力是无限的。
早在2007年初,还仅仅只有一个试用版本的SDK,但是CNUDA的更新也十分的频繁,这证明NVIDIA正在专注通用计算事业。今天的CUDA发展的很好,SDK已经有2.0的测试版本可以使用了,而且它支持目前主流的操作系统,如Windows Vista、XP、Linux和Mac OS X。并且NVIDIA也推出了专门的CUDA官方网站。支持多语言,全世界的开发人员都可以参与到其中来。
从个人的层面上讲,小编我对CUDA的印象是给予非常的肯定和鼓励。如果你非常熟悉GPU的架构和特性,那么你的智商也足以学会简单编程,通过API去实现自己的CUDA应用程序。并且,你不必费心的去考虑硬件的细节,只需要善用好这几千个并行线程就可以实现伟大的程序。我们一开始编写这个程序的时候,还不敢确定CUDA有多强的性能,通过这个小程序,我们发现它确实要比CPU的性能强大许多。
因此,CUDA并不是研究员勾引消费者去购买GeForce显卡的一种骗人的把戏。CUDA的实用性是任何C语言程序员都可以读懂的。即使是普通的电脑爱好者,只要在这方面投入一些时间和精力,也会步入这个并行计算的殿堂。不过我觉得NVIDIA应该提供更多,更好品质的开发帮助文档,来帮助用户入门。
【→最终结论:CUDA才是终极的CPU(下)←】
最终结论:CUDA才是终极的CPU(下)
NVIDIA的CUDA会成为未来的一个主流的API,主要就是因为一个字:便捷。我们都知道,未来的IT领域是并行计算的天下。无论是硬件、软件都会朝着这个方向发展,这是业内技术的大趋势。目前的这些开发范例,也还只是创建一些线程,不过开发人员一定要小心的处理共享资源,因为现在的处理器也朝着多核心的方向发展着。
但是在数年以后,处理器的核心数,也不可能达到几百个。不过目前NVIDIA是第一个做到了这些的厂商。不过一枝独秀的狭义性也显现出来,目前也仅仅有NVIDIA的GF8、9系列和高端的Quadro/Tesla才支持CUDA程序。
Tesla高端计算解决方案
虽然可以运行CUDA的NVIDIA显示卡遍及世界各个角落,不过NVIDIA也并不满足现状。毕竟它的竞争对手们也不会袖手旁观。AMD也提供了自己的SDK,名叫Stream Computing。尽管至今没有一个实际的东西,Intel也发布了自己的解决方案——Ct。这三个竞争者,就像三国一样,三足鼎立,互相促进又互相牵制。另外在暗中伺机窥探的还有一个大佬,微软。他们也推出了自己的API,印有“微软出品”的字样相信这更容易让众多的开发者接受。
AMD Stream Computing
NVIDIA仍然有很多机遇与挑战。虽然在技术方面CUDA有许多先天的优势,但是想要说服系统平台开发商,和崇尚兼容性,稳定性为首要素的软件开发人员来支持CUDA也不是一件容易的事儿。不过随着CUDA SDK 2.0的发布,相信会有更多的应用程序开发商愿意支持这项技术。同时小编我也深信,在未来的计算机系统中,GPU的性能大幅超越了CPU,而处理器仅仅起到一个调度协调的作用,真正具有强大运算性能的部件已经远非CPU可比,而CUDA才是终极形态的CPU。