GPU编程实战(基于Python和CUDA)

978-7-115-56091-9
作者: 布莱恩·图奥迈宁(Brian Tuomanen)
译者: 韩波
编辑: 吴晋瑜

图书目录:

详情

本书旨在引导读者基于 Python 和CUDA 的 GPU 编程开发高性能的应用程序,先后介绍了为什么要学习 GPU 编程、搭建 GPU编程环境、PyCUDA入门等内容,以及 CUDA 代码的调试与性能分析、通过 Scikit-CUDA 模块使用 CUDA 库、实现深度神经网络、CUDA 性能优化等内容。学完上述内容,读者应能从零开始构建基于 GPU的深度神经网络,甚至能够解决与数据科学和 GPU编程高性能计算相关的问题。 本书适合对GPU 编程与 CUDA编程感兴趣的读者阅读。读者应掌握必要的基本数学概念,且需要具备一定的 Python编程经验。

图书摘要

版权信息

书名:GPU编程实战(基于Python和CUDA)

ISBN:978-7-115-56091-9

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [美] 布莱恩•图奥迈宁(Brian Tuomanen)

译    韩 波

审  校 毛星云

责任编辑 吴晋瑜

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e56091”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


Copyright ©Packt Publishing 2018. First published in the English language under the title Hands-On GPU Programming with Python and CUDA (9781788993913).

All rights reserved.

本书由英国Packt出版社授权人民邮电出版社有限公司出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


本书旨在引导读者基于Python和CUDA的GPU编程开发高性能的应用程序,先后介绍了为什么要学习GPU编程、搭建GPU编程环境、PyCUDA入门等内容,以及CUDA代码的调试与性能分析、通过Scikit-CUDA模块使用CUDA库、实现深度神经网络、CUDA性能优化等内容。学完上述内容,读者应能从零开始构建基于GPU的深度神经网络,甚至能够解决与数据科学和GPU编程高性能计算相关的问题。

本书适合对GPU编程与CUDA编程感兴趣的读者阅读。读者应掌握必要的基本数学概念,且需要具备一定的Python编程经验。


感谢北卡罗来纳州立大学罗立分校电气与计算机工程专业的Michela Becchi教授和她的学生Andrew Todd,是他们于2014年带我进入GPU编程的世界。感谢Packt出版社的编辑Akshada Iyer,感谢她在本书撰写过程中给予我的极大支持。最后还要感谢Andreas Kloeckner教授为我们带来了优秀的PyCUDA库——在本书中,我们会经常用到这个库。

——Brian Tuomanen

自2014年以来,Brian Tuomanen博士一直从事CUDA和通用GPU编程方面的工作。他在美国西雅图华盛顿大学(University of Washington)获得了电气工程专业的学士学位,在攻读数学专业的硕士学位之前,从事过软件工程方面的工作。后来,他在哥伦比亚的密苏里大学攻读数学博士学位,在那里与GPU编程“邂逅”——GPU编程当时主要用于研究科学问题。Tuomanen 博士曾经在美国陆军研究实验室就通用GPU编程发表演讲,后来在美国马里兰州的一家初创公司负责GPU集成和开发方面的工作。目前,他在西雅图担任微软的机器学习专家(Azure CSI)。


衷心感谢家人给予我的全力支持!

——Vandana Shah

Vandana Shah拥有电子学士学位,还拥有人力资源管理MBA学位和电子工程硕士学位。她的研究领域为超大规模集成电路(Very Large Scala Integration,VLSI)。在审阅本书时,她已经提交了脑肿瘤检测的图像处理和深度学习交叉领域的电子学博士论文,待被授予博士学位。她感兴趣的领域为深度学习和嵌入式系统的图像处理。她拥有超过13年的研究经验,教过电子和通信专业的本科生和研究生的课程,并在IEEE、Springer和Inderscience等知名期刊上发表过多篇论文。在磁共振成像(Magnetic Resonance Imaging,MRI)图像处理领域,她获得了美国政府的资金支持,并开展了相关的研究。她一直致力于指导学生和研究人员,还为学生和教师进行软技能开发提供培训。她不仅在技术领域实力不俗,还擅长跳印度舞Kathak。


感谢大家选择这本用Python和CUDA进行GPU编程的入门指南。虽然这里的GPU指的是图形编程单元,但是本书不是用来介绍图形编程的,而是介绍如何对通用GPU编程,即GPGPU编程(General-Purpose GPU Programming)。在过去的十年中,人们发现GPU不仅可以用于渲染图形,同时也非常适合用于计算,尤其是吞吐量巨大的并行计算。为此,英伟达公司发布了CUDA工具包,以期让所有了解C编程的人能轻松步入GPGPU编程的世界。

之所以编写本书,是为了帮助大家尽快进入GPGPU编程的世界。为此,我们尽量为每一章提供有趣的例子和习题。我们尤其鼓励大家亲自输入相应的示例代码,并在你喜欢的Python环境(Spyder、Jupyter和PyCharm都是不错的选择)中运行它们。这样有助于你掌握所有必需的函数和命令,并获得编写GPGPU程序的第一手经验。

乍一看,GPGPU并行编程似乎是一项异常艰巨的任务,尤其是对那些只有CPU编程经验的人来说。你需要面对很多新的概念和惯例,这简直和从零开始没什么两样。这时,你一定要树立信念——只要付出努力,就一定能掌握GPGPU并行编程技术。请保持学习热情并持之以恒!学完本书,相信GPGPU并行编程技巧将变成你的“第二天性”。

祝编程愉快!

本书仿佛是专门为2014年的我而写的。那时,笔者正在攻读博士学位,出于研究需要,尝试开发一个基于GPU的模拟环境。在此期间,笔者疯狂阅读有关GPU编程的各种图书和手册,想尽快在这个领域找到一点感觉。不幸的是,大多数文献会不厌其烦地展示数不尽的硬件原理图和术语——随便翻开一页,几乎都是这些内容,真正实用的编程知识却廖廖无几。

本书适合那些想要实际进行GPU编程、不想被技术细节和硬件原理图绕晕的读者阅读。为此,我们将使用C/C++(CUDA C)语言对GPU进行编程,并通过PyCUDA模块将其内联到Python代码中。我们只需编写底层GPU代码,而其他烦琐的工作(例如编译、链接以及在GPU运行代码等)可以由PyCUDA代劳。

第1章“为什么要学习GPU编程”,介绍学习这个领域知识的动机、如何应用阿姆达尔定律,以及评估从串行编程切换到GPU编程后所能带来的性能提升。

第2章“搭建GPU编程环境”,解释如何在Windows和Linux系统下为CUDA编程搭建合适的Python与C++开发环境。

第3章“PyCUDA入门”,展示利用Python语言进行GPU编程时所需的基本技能。本章着重介绍如何使用PyCUDA的gpuarray类与GPU进行数据传输,以及如何使用PyCUDA的ElementwiseKernel函数来编译简单的CUDA内核函数。

第4章“内核函数、线程、线程块与网格”,介绍编写高效CUDA内核函数所需的基础知识。这些内核函数是在GPU上运行的并行函数。本章除了介绍如何编写CUDA设备函数(由CUDA内核直接调用的“串行”函数),还将介绍CUDA的抽象线程块/网格结构及其在启动内核函数方面所发挥的作用。

第5章“流、事件、上下文与并发性”,讲解CUDA流的概念。利用CUDA流,我们可以在GPU上同时启动多个内核函数并实现同步。本章介绍如何使用CUDA事件来计算内核函数的运行时间,以及如何创建和使用CUDA上下文。

第6章“CUDA代码的调试与性能分析”,填补纯CUDA C编程方面的一些空白,并展示如何使用Nsight IDE进行开发和调试,以及如何使用英伟达(后简称NVIDA)公司的性能分析工具。

第7章“通过Scikit-CUDA模块使用CUDA库”,介绍几种可以通过Python Scikit-CUDA模块使用的标准CUDA库,例如cuBLAS、cuFFT和cuSolver库。

第8章“CUDA设备函数库与Thrust库”,演示如何在代码中使用cuRAND和CUDA Math API库,以及如何使用CUDA Thrust C++容器。

第9章“实现深度神经网络”,介绍如何应用前面几章中介绍的知识,从零开始构建一个完整的深度神经网络。

第10章“应用编译好的GPU代码”,展示如何使用PyCUDA和Ctypes,实现Python代码与预编译的GPU代码之间的交互。

第 11 章“CUDA性能优化”,讲解非常底层的各种性能优化技巧,特别是与CUDA相关的技巧,例如向量化内存访问、原子操作、线程束洗牌和使用内联PTX汇编代码。

第12章“未来展望”,给出一些教育规划和职业规划方面的内容。当然,这些都是以扎实掌握GPU编程基础知识为前提的。

最后的“习题提示”针对各章的习题给出了解题思路。

这的确是一本实战性较强的技术书。你应先掌握一定的编程知识,才能更好地阅读本书。准确来说,你应该做到:

在Python语言方面具有中级编程经验;

熟悉标准的Python科学计算包,例如NumPy、SciPy和Matplotlib;

在某种基于C的编程语言(C、C++、Java、Rust、Go等)方面具有中级编程能力;

了解C语言动态内存分配的相关概念(尤其要了解C语言中mallocfree函数的用法)。

GPU编程主要适用于与科学或数学高度相关的领域,因此本书的很多(即使不是大多数)示例会用到一些数学运算。因此,你应具备大学一年级或二年级的数学知识或了解这部分内容,如下所示:

三角学(三角函数,如sin、cos、tan……);

微积分(积分、导数和梯度);

统计学(均匀分布和正态分布);

线性代数(向量、矩阵、向量空间和维数)。

如果你没学过上述内容,或者学完已经有一段时间了,也不用担心,因为本书穿插着介绍了一些关键的编程和数学概念。

此外,在本书中,我们只使用CUDA——它是NVIDIA硬件专有的编程语言。也就是说,在开始之前,你需要准备好以下一些特定的硬件:

具有64位x86架构的Intel/AMD处理器的PC;

内存容量不低于4GB;

入门级NVIDIA GTX 1050 GPU(Pascal架构)或更高级别的GPU。

书中的大多数(并非全部)的示例代码可以在各种配置水平较低的GPU上运行起来,但需要说明的是,我们只在使用GTX 1050的Windows 10操作系统和使用GeForce GTX 1070(简称GTX 1070)的Linux操作系统上进行了测试。关于软硬件的设置和配置的具体说明参见第2章。

下载相应的文件后,请确保使用最新版本的解压工具来提取示例代码。可用的解压工具如下:

对于Windows系统,请选用WinRAR/7-Zip;

对于macOS系统,请选用Zipeg/iZip/UnRarX;

对于Linux系统,请选用7-Zip/PeaZip。

您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e56091”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。

本书中有一些不同的文本样式,用以区别不同种类的信息。相应说明如下。

黑体:表示新术语和关键词。

代码段以如下格式显示:

cublas.cublasDestroy(handle)
print 'cuBLAS returned the correct value: %s' % np.allclose(np.dot(A,x),
y_gpu.get())

代码段中需要关注的某些特定代码会以如下形式显示:

def compute_gflops(precision='S'):

if precision=='S':
    float_type = 'float32'
elif precision=='D':
    float_type = 'float64'
else:
    return -1

命令行输入或输出内容会显示为如下格式:

$ run cublas_gemm_flops.py

警告或者重要的提示以这样的形式给出。

 

技巧以这样的形式给出。


事实表明,除了用于对视频游戏进行图形渲染,图形处理单元(GPU)还能为普通消费者提供一种进行大规模并行计算的捷径。现在,人们只要从当地的商店购买一块价值2000美元的现代GPU,并将其插入家中的PC,就能轻松获得强大的算力——在5年或10年前,只有顶级的企业和大学的超级计算实验室才有这种算力。近年来,GPU的这种开放的可及性已经在很多方面显现出来。实际上,我们只要留意一下新闻就可以发现——加密货币矿工使用GPU挖掘比特币等数字货币,遗传学家和生物学家使用GPU进行DNA分析和研究,物理学家和数学家使用GPU进行大规模的模拟,人工智能研究人员通过编写GPU代码来撰写剧本及创作音乐,谷歌和脸书等大型互联网公司使用带有GPU的服务器群来完成大规模的机器学习任务……类似的例子简直不胜枚举。

本书的编写初衷,就是帮助你快速掌握GPU编程。这样,无论最终目标是什么,你都可以尽快用上GPU的强大算力。

注意,本书旨在为你介绍GPU编程的核心要领,而不是赘述复杂的技术细节及GPU的工作原理。第12章会列举更多的资源,以帮助你了解细分,进而为学到的GPU新知识找到用武之地。在本书中,我们将使用CUDA。CUDA是NVIDIA公司的通用GPU(General-Purpose GPU,GPGPU)编程框架,早在2007年就有了第一个版本。CUDA是NVIDIA GPU的专有系统,是一个成熟、稳定的平台,使用起来比较方便,并提供了一套无与伦比的第一方数学加速和人工智能相关的代码库。在安装和集成方面,CUDA也是最便捷的。

目前CUDA编程领域出现了许多现成的标准化Python库,如PyCUDA和Scikit-CUDA,让从事GPU编程的程序员更容易上手。基于上述原因,我们在本书中选用了CUDA。

CUDA的发音通常是coo-duh,而非C-U-D-A!CUDA最初代表的是Compute Unified Device Architecture(计算统一设备架构),但英伟达公司已经放弃了这个首字母缩写词的初始含义,转而将CUDA作为全大写的专有名称来使用。

现在,我们先介绍阿姆达尔定律,由此开始GPU编程之旅。

阿姆达尔定律是一种简单而有效的方法,用于估计通过将程序或算法转移到GPU上可能获得的速度提升,进而帮助我们判断是否有必要通过重写代码利用GPU提升程序的性能。在此之后,我们将简要学习如何使用cProfile模块分析Python代码的运行情况,以找到代码的瓶颈。

在本章中,我们将介绍下列主题:

阿姆达尔定律;

将阿姆达尔定律应用到代码中;

使用cProfile模块对Python代码进行简单的性能分析。

学习本章之前,请先安装Anaconda Python 2.7。下载地址如下:

https://www.anaconda.com/download/

在深入挖掘GPU的潜力之前,我们首先要说明的是,与Intel/AMD公司的中央处理器(CPU)的算力相比,GPU的优势在哪里。GPU的优势并不在于拥有比CPU更高的时钟频率,也不在于单个内核的复杂性或特殊设计。与现代单个CPU内核相比,单个GPU内核其实很简陋,这方面它并不占优势,因为CPU内核应用了很多复杂的工程技术,比如通过分支预测来降低计算的延迟等。这里所谓的“延迟”,指的是执行一次计算从开始到结束所用的时间。

GPU的强大之处在于它比CPU拥有多得多的内核,这意味着其吞吐量有了巨大的进步。这里的“吞吐量”指的是可以同时进行的计算数量。下面让我们通过类比来进一步理解这到底意味着什么。GPU就像一条非常宽阔的城市道路,可以同时通过很多辆行驶较慢的汽车(高吞吐量、高延迟),而CPU就像一条狭窄的公路,只能同时容纳几辆汽车,但可以让每一辆车更快地抵达目的地(低吞吐量、低延迟)。

对于新发行的GPU设备,我们只需考察其内核数量,就能大体了解其吞吐量的提升情况。举例来说,Intel或AMD公司的CPU平均只有2~8个内核,而入门级、消费级NVIDIA GTX 1050 GPU则有640个内核,新的顶级NVIDIA RTX 2080 Ti则有4352个内核!因此,只要我们知道如何正确地并行化需要加速的程序或算法,就可以充分利用GPU巨大的吞吐量所带来的优势。所谓的“并行化”,指的是通过重写程序或算法,将其工作负载分割成更小的单位,以便同时在多个处理器上并行运行。下面让我们来思考一个现实生活中的例子。

假设你正在建造一所房子,并且已经准备好了所有的设计资料和建材。如果你只聘请1个工人的话,那么建造这座房子估计需要100小时。假设这所房子的建造方式比较特殊,即相关工作可以完美地分配给额外增加的每个工人——也就是说,聘请2个工人建造这座房子需要50小时,聘请4个工人需要25小时,聘请10个工人需要10小时。那么,建造房子的所需小时数等于100除以所聘请的工人数量。这就是一个可并行化任务的例子。

我们注意到,与1个工人独自建造(串行建造)房子相比,2个工人完成这项任务的速度是其2倍,而10个工人一起完成(并行完成)这项任务的速度则是其10倍——也就是说,如果N是工人的数量,那么建造速度将是原来的N倍。在本例中,N被称为任务的串行版本的并行加速比。

对于给定的算法,在开发并行化版本进行之前,通常我们需要先估计并行化的潜在加速比,以确定是否值得花费资源和时间来实现程序的并行化。现实生活中的例子比这个例子要复杂得多,我们显然很难对程序的所有部分完美地并行化。在大多数情况下,只有一部分程序可以被很好地并行化,而其余的部分则不得不串行运行。

接下来,我们介绍阿姆达尔定律。阿姆达尔定律实际上就是一个简单的数字公式,用于估计将串行程序的某些代码放到多个处理器上并行执行时可能带来的潜在速度增益。为了便于理解,我们沿用建造房子的例子来介绍该定律。

在前面的例子中,整个工期仅与房子的实际物理建造过程有关,但现在,我们把设计房子的时间也考虑进来。假设世界上只有一个人有能力设计你的房子——这个人就是你,并且设计房屋需要100小时。地球上没有人能与你的设计才华相提并论,所以这部分任务根本不可能由其他建筑师来分担——也就是说,不管你有什么样的资源,无论你能聘请多少人,设计这所房子都需要100小时。所以,如果你只聘请1个工人,建造这座房子所需要的全部时间就是200小时——你设计房子需要100小时,1个工人建造房子需要100小时。如果你聘请2个工人,则需要150小时——设计房子的时间仍然是100小时,而建造房子仅需要50小时。显然,建造房子的总时间的计算公式为100+100/N,其中N是聘请的工人数量。

现在回过头来想想,如果我们聘请1个工人,建造房子需要多长时间——它最终决定了聘请更多工人时的加速比,也就是说,这个过程变快了多少倍。如果我们聘请 1名工人,就会发现设计和建造房子所需的时间是相同的,即100小时。所以,我们可以说,设计房子的时间占比是0.5(50%),建造房子的时间占比是0.5(50%)——当然,这两个部分加起来是1,也就是100%。当增加工人时,我们希望对此进行比较——如果我们有2个工人,建造房子时间将减少一半。所以,与初始串行版本相比,这将花费原时间的0.5 + 0.5 / 2 = 0.75(75%),而0.75×200为150小时。我们可以看到,聘请更多工人的方法是行之有效的。此外,如果聘请N个工人,我们可以计算出N个工人并行施工所需时间占原时间的比例,具体计算公式为0.5 + 0.5/N

现在,让我们确定通过增加工人而获得的加速比。如果我们有2个工人,建造一所房子需要原时间的75%,那么可以用0.75的倒数来确定并行化的加速比——也就是说,加速比将是1/0.75,比只有1个工人时的速度约快1.33倍。在这种情况下,如果我们聘请N个工人,加速比将变为1/(0.5+0.5 /N)。

随着聘请更多的工人(N更大),0.5/N将接近于0,所以当并行化这个任务时,加速比是有一个上限的,即1/(0.5+0)=2。我们可以用估计的最大加速比除以原时间,来确定这个任务所需的绝对最小时间——200/2 = 100小时。

刚才用来确定并行编程中的加速比的原理叫作阿姆达尔定律。使用该定律时,只需要知道原始串行程序的执行时间中,可并行化的代码的执行时间所占比例(称为p),以及可用的处理器内核数量N

在这种情况下,无法并行化的代码的执行时间比例总是 1−p,所以我们只需要知道p

现在,我们可以用阿姆达尔定律来计算加速比(Speedup,用S表示)了,具体公式如下所示:

综上所述,阿姆达尔定律就是一个简单的公式,可以用于粗略地(非常粗略地)估计一个至少可以部分并行化的程序的潜在加速比。只要我们知道可以并行化的代码的运行时间占比(p)和运行并行化的代码的内核数量(N),就可以大致推断出是否值得为特定串行程序开发一个并行版本。

接下来,我们介绍一个非常经典的并行计算的例子,并且将在本书中多次用到这个例子—— 一个生成Mandelbrot集图形的算法。首先,让我们来定义Mandelbrot集。

对于给定的复数c,当时,我们可以定义这样一个递归序列,其中,而时,该序列可以表示为。如果n增加到无穷大,仍然以2为界,就说c是Mandelbrot集的元素。

回想一下,我们可以将复数表示在二维笛卡儿平面上,其中x轴表示实数分量,y轴表示虚数分量。因此,我们可以很容易地用一个令人印象深刻的(也是众所周知的)图形来可视化Mandelbrot集。这里,我们将在复笛卡儿平面上用较浅的阴影表示Mandelbrot集的元素,用较深的阴影表示不属于Mandelbrot集的元素,具体如图1-1所示。

图1-1

现在,让我们考虑一下如何用Python代码来生成Mandelbrot集。因为我们不可能检查每一个复数是否属于Mandelbrot集,所以必须选择检查的范围。同时,我们必须确定需要检查每个范围(由width、height确定)中的多少个点,还要在检查|zn|时,确定n的最大值(max_iters)。接下来,我们将编写一个函数,用它来生成Mandelbrot集的图形——就本例来说,我们通过连续迭代图形中的每一个点来完成该任务。

首先,我们需要导入NumPy库,它是本书中经常用到的一个数值运算库。具体来说,这里的功能是通过simple_mandelbrot函数实现的。我们先用NumPy的linspace函数生成一个充当离散复平面的网格(下面的代码应该是相当简单的):

import numpy as np

def simple_mandelbrot(width, height, real_low, real_high, imag_low,
imag_high, max_iters):
     real_vals = np.linspace(real_low, real_high, width)
     imag_vals = np.linspace(imag_low, imag_high, height)
     # we will represent members as 1, non-members as 0.
     mandelbrot_graph = np.ones((height,width), dtype=np.float32)
     for x in range(width):
         for y in range(height):
             c = np.complex64( real_vals[x] + imag_vals[y] * 1j )
             z = np.complex64(0)
             for i in range(max_iters):
                 z = z**2 + c
                 if(np.abs(z) > 2):
                     mandelbrot_graph[y,x] = 0
                     break
     return mandelbrot_graph

我们要添加一些代码,以便将Mandelbrot集的图形转储到一个PNG格式的文件中。为此,首先需要导入相应的库:

from time import time
import matplotlib
# the following will prevent the figure from popping up
matplotlib.use('Agg')
from matplotlib import pyplot as plt

现在,让我们添加一些代码来生成Mandelbrot集,将其图形转储到一个文件中,并使用time函数对两个操作进行计时:

if __name__ == '__main__':
     t1 = time()
     mandel = simple_mandelbrot(512,512,-2,2,-2,2,256, 2)
     t2 = time()
     mandel_time = t2 - t1
     t1 = time()
     fig = plt.figure(1)
     plt.imshow(mandel, extent=(-2, 2, -2, 2))
     plt.savefig('mandelbrot.png', dpi=fig.dpi)
     t2 = time()
     dump_time = t2 - t1
     print 'It took {} seconds to calculate the Mandelbrot
graph.'.format(mandel_time)
     print 'It took {} seconds to dump the image.'.format(dump_time)

现在,让我们运行这个程序(该程序也可以从本书配套资源的文件夹1下面的Mandelbrot0.py文件中找到),如图1-2所示。

图1-2

可以看到,生成Mandelbrot集耗时约14.62秒,转储图形耗时约0.11秒。我们是以逐点方式生成Mandelbrot集的。不同点的坐标值之间并不存在依赖关系,因此生成Mandelbrot集实际上就是一个可并行操作。相比之下,转储图形的代码则无法并行化。

现在,让我们用阿姆达尔定律来分析一下。就本例来说,如果将相应的代码并行化,我们可以得到多大的加速比?如上所述,该程序的两个部分总共用时约14.73秒。我们可以并行化生成Mandelbrot集的代码,也就是说,可并行化的那部分代码的执行时间占比为p=14.62/14.73≈0.99。因此,这个程序可并行化比例约为99%!

那么,我们可以获得多大的加速比呢?笔者目前用的是一台拥有640个内核的GTX 1050的笔记本电脑,使用阿姆达尔定律时,N将是640。所以,计算加速比的公式为

这个加速比的值无疑是非常好的,足以表明将算法并行化到GPU上的努力是非常值得的。记住,阿姆达尔定律只是给出了一个非常粗略的估计值!将计算任务转移到GPU上时,我们还要考虑其他因素,比如在CPU和GPU之间发送和接收数据所需的额外时间,或者转移到GPU上的算法只能部分并行,等等。

在前面的例子中,我们是通过Python中的标准time函数分别对不同的函数和组件进行计时的。虽然这种方法对于小型的程序来说比较好用,但对于调用许多不同函数的大型程序来说并不总是可行,因为大型程序中有些函数可能值得我们去并行化,但有些函数根本不值得这样做,甚至不值得在CPU上进行优化。本节的目标是,找到程序的瓶颈和热点——即使我们精力充沛,并且在每一个函数调用前后都应用了time函数,仍有可能会遗漏一些东西;或者,会有一些我们从未考虑到的系统调用或库调用,但或许正是它们在“扯后腿”。在考虑重写代码以在GPU上运行之前,我们首先要找出哪些代码需要转移到GPU上,且必须始终牢记美国著名计算机科学家Donald Knuth的忠告——“过早的优化是万恶之源”。

我们将借助性能分析工具来查找代码中的瓶颈和热点。利用这些工具,我们很容易找出程序中哪些代码最为耗时,以便对其进行相应的优化。

我们主要使用cProfile模块对示例代码进行性能分析,因为该模块是Python中的标准库函数。我们可以在命令行中用-m cProfile来运行该性能分析工具,用-s cumtime规定通过每个函数花费的累计运行时间来组织结果,然后用>运算符将输出重定向到文本文件。

上述方法适用于所有Linux Bash或Windows PowerShell命令行环境。

让我们运行图1-3所示的命令。

图1-3

现在,我们可以用自己喜欢的文本编辑器来查看文本文件的内容,如图1-4所示。记住,程序的输出显示在该文本文件的开头部分。

我们没有删除原示例中对time函数的引用,因此前两行的内容实际上是它们的输出。随后我们可以看到这个程序中各个函数调用的总次数以及这些函数的累计运行时间。

再往后是一个由程序中被调用的函数组成的列表,其中的函数按照累计运行时间由多到少的顺序进行排列。其中,第一行代表程序本身,第二行代表程序中的simple_mandelbrot函数。(注意,这里的累计运行时间与我们用time函数测得的时间是一致的。)在此之后,我们还可以看到许多与将Mandelbrot集图形转储到文件有关的库函数和系统调用,这些调用的耗时相对较少。因此,我们可以利用cProfile工具的输出结果来推断程序的瓶颈在哪里。

图1-4

与CPU相比,使用GPU的主要优势在于其吞吐量的提升,这意味着我们可以在GPU上同时执行比在CPU上更多的并行代码。但是,GPU无法对递归算法或不能并行化的算法进行加速。有些任务,比如建造房子,其中只有部分任务是可并行化的。我们无法加快设计房子的速度(因为在这个例子中,设计房子本质上是串行的),但是可以聘请更多的工人来加快建造房子的过程(在这个例子中,建造房子是可并行化的)。

我们还用建造房子这个例子介绍了阿姆达尔定律。通过这个定律对应的公式,我们可以大致估计一个程序的速度提升潜力——知道可并行化的代码的执行时间的占比,以及可以并行运行这些代码的处理器数量。然后,我们应用阿姆达尔定律分析了一个生成Mandelbrot集并将其图形转储到文件的小型程序,并得出“该程序非常适合在GPU上并行运行”的结论。最后,我们简单介绍了如何利用cProfile模块对代码进行性能分析。通过这个工具,我们可以找出程序的瓶颈在哪里,不需要显式地对函数调用进行计时。

至此,我们不仅掌握了一些基本的概念,还有了学习GPU编程的动力。在第2章中,我们将介绍如何在Linux或Windows 10操作系统中搭建GPU编程环境。同时,我们将继续深入探索GPU编程的世界,并为本章中的Mandelbrot集程序编写一个基于GPU的版本。

1.在本章的Mandelbrot集示例代码中有3个for语句,但是我们只能对前两个for语句实现并行化。请问为什么不能对所有for循环实现并行化?

2.在用阿姆达尔定律分析将一个串行CPU算法转移到GPU上的性能提升情况时,我们没有考虑哪些因素?

3.假设你获得了3个新型的绝密GPU的独家使用权,并且这3个GPU除内核数之外,其他方面都是一样的——第一个有131072个内核,第二个有262144个内核,第三个有524288个内核。在将Mandelbrot集示例代码并行化并转移到这些GPU上以生成512像素×512像素的图像时,第一个GPU和第二个GPU之间的计算时间会有差异吗?第二个GPU和第三个GPU之间呢?

4.在应用阿姆达尔定律考量某些算法或代码块的可并行性时,你能想到哪些问题?

5.我们为什么要使用性能分析工具?单靠Python的time函数来进行性能分析可以吗?

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e56091”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程