为什么需要GPU

几年前我启动并主导了一个项目,当时还在谷歌,这个项目叫谷歌大脑。该项目利用谷歌的计算基础设施来构建神经网络。

规模大概比之前的神经网络扩大了一百倍,我们的方法是用约一千台电脑。这确实使深度学习取得了相当大的进展。用到相当多的

计算机。不久之后我发现,之前我并没意识到,用一千台电脑是一项非常昂贵的技术。因此,我和我的朋友,意识到,利用一种

不同的技术,仅用三台电脑,而非一千台,就可以做到这点,而秘诀就是利用GPU技术。

---Andrew Ng [The Big Talk:深度学习与在线教育]

GPGPU是最近几年才流行起来的Project,并且在科学计算上使用频繁。

深度学习在06年立项,当时的数据集小的可怜,只需要CPU仿真一下。

09年,Stanford的CV实验室公布ImageNet后,深度学习开始拥抱大数据。

浅层结构+大数据尚且依赖云计算,深度结构+大数据的需求则是云计算提供不了的。

12年,Hinton组的Alex开源了cuda_convnet,正式大规模使用GPU。衍生出Theano、Caffe等项目。

在今天,如果你要修改这些项目代码,包括自己的创新改良,不会CUDA是不行的。

这也是深度学习带来的一个全新领域,它要求研究者不仅要理论强,建模强,程序设计能力也要过硬,不能纸上谈兵。

单从这点来看,深度学习依然和传统机器学习学派分道而行(多使用CPU多线程、云计算)。

大规模分布式CPU云应该尽量避免,尽管面子大,唬人效果好,但毕竟计算效率低,平台组建麻烦。

高效的程序设计,应当从单机中,榨取更多的计算力,进而考虑多机联合。

CUDA物理计算体系

1.1 CUDA特性

CUDA可以说是介于NVIDIA Driver和DirectX等库的中间产物。

传统游戏开发者,无须考虑太多GPU硬件架构,毕竟A卡、N卡水火不容。

通过设备商提供的Driver,DirectX或者OpenCL可以将不同架构的硬件底层封装,提供给开发者共有的API。

而CUDA,则可以看成是Driver、DX之流的上级。

为了泛型编程(C、C++、Fortran多语言)、以及榨取更多的计算力,NVIDIA对OpenCL进行的改装,

贴合自己的GPU硬件架构,量身定做出CUDA。

较游戏程序员不同,CUDA程序员主要工作,就是把握硬件架构,在算法理论时间复杂度下,

将算法串行执行体系,改组为并行执行体系。违背硬件的并行设计,只会让你的程序跑的百倍之慢。

1.2 NVIDIA GPU三大经典架构(Fermi、Kepler、Maxwell)

与海贼王里CP9道力值 (道力可以反映一个人的战斗力,但不能完全决定一个人的实力)设定相似:

NVIDIA为它在不同成长阶段卖出的产品,规定了计算能力体系:

计算能力1.0是跑CUDA的最低条件,这一时期的代表作是8800GT家族。

Fermi架构的计算能力是2.0, Kepler是3.0,Maxwell是4.0。

1.2.1 年轻有为的Fermi(费米)

Fermi架构,也就是GTX 4XX系卡,这算是CUDA的一个重要转折点——它把流处理器(SP)改名为"CUDA核心"。

NVIDIA的[Fermi架构图]

NVIDIA的物理计算体系以分为SM、CUDA核心两大部分。

官方称SM为Stream Multiprocessors(流多处理器),民间多译为SM计算阵列。

物理计算体系对于程序员是透明的,需要调度NVIDIA提供的逻辑计算体系(线程Grid、线程Block、线程Thread)

对于一个指定好逻辑计算体系的任务,GPU会按照负载均衡的原则,将任务平均分至各个SM阵列,

对于每个SM阵列,就调度它手下那一伙CUDA核心干活。

由于每个SM阵列的CUDA核心有限,NVIDIA规定,Fermi架构,每个SM最多并行执行1024个线程。

当然,实际任务中,每个SM会分到几百万个线程,这时候,就只能小部分并行,然后再串行了。

Fermi 1.0架构,官方设计是16组SM,512SP,然而旗舰GTX480最后只弄出了15组,480SP,顺次阉割出GTX470、GTX460。

Fermi 2.0架构,旗舰GTX580,总算达到设计图要求,达到了16组,512SP,顺次阉割出了GTX570,GTX560。

1.2.2 老当益壮的Kepler(开普勒)

Kepler架构最大变化在于, 对每个SM阵列,将SP数量扩大到6倍,达到192SP。谓之曰SMX阵列。

每个SMX阵列,包含192个CUDA核心,单次并行吞吐量是2048个线程。

Kepler 1.0架构,官方设计是15组SM,2880SP,然而旗舰GTX580最后只弄出了8组,1536SP,顺次阉割出GTX570、GTX560。

Kepler 2.0架构,旗舰GTX680,总算达到设计图要求,达到了15组,2880SP,顺次阉割出了GTX670,GTX660。

特别版,GTX Titan Z,直接把两块GK110并在一起,合出了30组,5760SP,同时支持双精度浮点计算。

其阉割掉双精度之后,就是GTX690。

值得一提的是,GTX游戏卡直接把双精度阉割掉了,因为只有Tesla做科学计算的时候,才会用双精度浮点运算。

整体来看,Kepler并没有提升SM阵列数量(在产量情况下,GTX 5XX居然只造出了8组)。但是每个阵列的吞吐量扩大到2倍(1024-2048)。

速度也较之提升(CUDA核心扩大到6倍)。

在GTX 6XX系中,15组SM阵列,总算达到了Fermi的两倍吞吐量。

1.2.3 "长者"风范的Maxwell(麦克斯韦)

Maxwell架构是老黄的无奈之举。因为台积电把20nm工艺让给了ARM系(Apple和高通)。

还是基于28nm的Maxwell,继续在SM上大刀阔斧闹改革,将192SP降低为128SP,谓之曰SMM阵列。

Maxwell 最新架构,官方设计是16组SM,2048SP,为旗舰GTX980,顺次阉割出GTX970,GTX960。

特别版,GTX TitanX,24组SM,3072SP,较之TitanZ,阉掉了双精度浮点数支持。

TitanX是老黄在GTC 2015向DL界主推的一块民用卡,因为DL无需高精度浮点,用Tesla太奢侈。

(仔细分析老黄那自信满满进攻DL领域的表情,拿TitanX打游戏实在有点.....)

降低了SP数量之后,Maxwell得到了功耗和性能之间的权衡调整。

1.3 设备信息

编译CUDA Examples下的deviceQuery,你可以看到GPU的基本信息。

红字标出的,是设计并行程序时需要参考的几个重要设备参数。

CUDA逻辑计算体系

2.1 线程网格(Grid)、线程块(Block)、线程(Thread)、线程束(Warp)

2.1.1 内核函数

内核函数是并行计算中最基本的单元函数,其特点是:

统一的处理逻辑代码,分布并行掌控不同区域的数据,以此达到多区域数据联动并行执行。

NVIDIA为了CPU在逻辑上,能调度GPU计算的函数,规定了统一的格式。

以__global__限定符为始,声明:__global__ void helloworld()。

__global__意思为,GPU执行,CPU调用

调用时,需要分配  <<<线程块,块内线程数>>>。

如执行helloword,使用1个线程块,块内使用256个线程,则

helloworld<<<1,256>>>

2.1.2 线程网格(Grid)

线程网格在编程时并不存在,它只是抽象上的并行网格体系。

不同种类的内核函数,每种内核函数调度数个的线程块,这数个线程块逻辑上被判为一个Grid。

2.1.3 线程块(Block)

线程块是一个3D结构,强调3D坐标系时,需要以dim3类型声明三维大小。

dim3是个结构体, 成员x、y、z,代表方向轴尺度。

如helloworld<<<dim3(1,1,1),256>>>。

当然,大部分操作基本使用的是1D坐标系,线程块默认全部扩展到X轴上。

一般写成helloworld<<<1,256>>>。

通常在内核函数内,需要获取线程块编号,以便对数据集的不同区域处理,四大重要属性:

☻dim3 gridDim(不是指有多少Grid,而是指一个Grid有多少Block)

☻dim3 blockDim(不是指有多少Block,而是指一个Block有多少Thread)

☻dim3 blockIdx

☻dim3 threadIdx

对于1D坐标系,有int tid= (blockDim.x*blockIdx.x) + threadIdx.x;

tid指明当前线程的编号,是内核函数里最基本的控制变量。

int step=(blockDim.x * gridDim.x);

由于CUDA限制每个Block的线程数(2.0以上通常使用1024,以下通常使用512)

所以在常规元素分解模型中,通常把每个Block的线程数设置为常量(固定不动)

这时,有两个策略:

① 其一,不固定Block:

这种方法最为常用,由于CUDA对每个任务而言,对Block数量的限制很松,

如图:

这时候,可以采取为每个线程分配一个元素的方法,用

$BLOCKS=(N + THREADS - 1) / THREADS$

算出一个动态的Block数量的需求,这时候,for(i=tid;i<N;i+=step)等效于for(i=tid;i<N;i+=1)

因为这个循环根本不会执行第二次。

① 其二,固定Block数量:

这时,这时候,为了跑完全部的N个元素,有些Thread会启动人工循环。

i+=step会将元素坐标继续跳转,因为N必然大于step,你不能用+1来取剩余的元素吧?

这两种方法本质上是等效的,由于在物理执行时,同时并行线程最多大概是3072,

几百万、甚至几千万的Block会被CUDA扔到等待队列里,由CUDA自己安排自动循环,

有时甚至比你的人工循环更高效,所以,通常用①的方法,为了保持写法一致,step也会作为默认跳转量。

2.1.4 线程(Thread)

CUDA逻辑体系里最基本的执行单位,等效于CPU的线程。

内核函数一旦被指明了线程块大小,线程大小后,每个线程就分配到了一个内核函数的副本。

区别这些的线程的唯一方法就是线程编号tid,通过tid,让不同线程窥视数据集的不同部分。

用相同的逻辑代码,执行数据空间的不同子集。

2.1.5 线程束(Warp)

线程束对用户透明,它是NVIDIA强行规定的。目前显卡都固定为32。

逻辑上,线程束将32个线程编为一组。

一般微机系统,如8086,它的访存模式是串行的。每一个总线周期,吞一个字节进来。

NVIDIA的GPU在一个总线周期内,能够最大吞32*4=128字节。

前提是当个线程束内的线程,逐序访问显存,这特别需要设计数据存储形式。

使用线程束的目的是掩盖单个总线周期过长的问题,通常要跑500~600个T周期。

简单样例: HelloWorld、向量加法

3.1 CUDA程序基本结构

CUDA的编译器NVCC只能编译.cu文件。cu文件可以看作是h&cpp文件的混合体。

所有代码可以写在一个cu文件里,但是毫无可读性。

CUDA最大的亮点在于提供C(C++)接口,所以设计模式应该保持传统C程序风格。

☻风格一:将不同函数写进不同cu文件里,模块化。

看起来挺好的,实际操作起来比较麻烦。

NVCC编译器在生成程序时,不支持跨文件link,也就是说,#include一个cu文件,会导致重定义。

VS中会这么报错:

error LNK2005: "void __cdecl cudaStrcpy(char *,char *)" (?cudaStrcpy@@YAXPAD0@Z) 已经在 gpu_helloworld.cu.obj 中定义

所以,NVCC编译完cu文件后,要在项目属性里把冲突的cu文件从生成中排除,然后link。

☻风格二:h、cpp、cu混用

上图中指明了,h&cpp是由C编译器来编译的。C编译器里不允许#include一个cu文件(不支持)

否则VS会报错

main.obj : error LNK2001: 无法解析的外部符号 _threadIdx

解决方案:

若要引用cu里的函数,在main.cpp里外部extern声明一下,让VS转为NVCC编译器处理。

3.2 GPU版HelloWorld

3.2.1 内存与显存之争

——老板,我要4G显存的显卡。

——亲,这是我们最新款显卡GT810,4G显存哦,只要399。您看那个GTX960

才2G显存,跑游戏卡死了。

——好,我就买这个了。

买显卡,看显存大小,似乎已经成了电脑小白必带的标签。

显存是什么?显卡中最慢的外部存储体,焊在PCB板上,与GPU独立,想来几斤就有几斤。

☻DDRIII的内存,传输带宽是30~40GB\s,也就是说,每秒能访存30~40GB数据。

☻笔记本GDDR5的显存,带宽大概是80GB\s。

☻GTX960的带宽是148GB\s。

☻淘宝杂牌显卡,带宽大概是40~50GB\s。

☻GPU的Cache带宽大概是1.5TB\s,CPU的寄存器带宽大概是15TB\s。

☻GPU大概有50%的T周期浪费在访问显存上。作为GPU上最慢最庞大的存储体,显存带宽极为重要。

★★★结论:根据木桶原理,访存速度远远远远远小于并行处理速度,因而I/O瓶颈是GPU的最大问题。

★★★CPU不可以直接访显存,仅__device__、__global__函数有权访问,所以需要内存显存数据相互转移。

3.2.2 内存与显存大挪移

一般来说,一个CUDA程序必然少不了以下三步:

☻cudaMalloc:创建新的动态显存堆

☻cudaMemcpy:将主机(Host)内存复制到设备(Device)显存

☻显存处理完之后,cudaMemcpy:设备(Device)显存复制回主机(Host)内存,释放显存cudaFree

其中第三步最容易遗忘。要知道,CPU最后是无法使用显存中的数据的。

3.2.3 GPU设备选择

CUDA并行有三大境界

☻多台机器间并行

☻多路显卡间并行

☻单卡多线程并行

一般单机中需要操控多路显卡交火,比如AlexNet就是典型的双路交火结构。

不像孤岛危机之类的游戏那样,CUDA需要手动检测、切换设备,这是一切计算的开头准备工作。

下面的InitCUDA函数演示了单卡计算的设备选择:

#include "cuda_runtime.h"
#include "cstdio"
bool InitCUDA()
{
int cnt, i;
cudaGetDeviceCount(&cnt);
if (!cnt)
{
printf("No GPU Device\n");
return false;
}
for (i = ; i < cnt; i++)
{
cudaDeviceProp device;
if (cudaGetDeviceProperties(&device, i)==cudaSuccess)
if (device.major >= ) break;
}
if (i == cnt)
{
printf("GPU's Computing Capability must higher than 1.0\n");
return false;
}
cudaSetDevice(i);
return true;
}

InitCUDA()

只有计算能力>=1.0(8800GT以上)的N卡才能做CUDA计算,默认选择符合条件的第一块卡。

3.2.4 HelloWorld

GPU版HelloWorld,主要目的是演示CUDA基本程序框架:

☻将HelloWorld复制进显存

☻让GPU完成strcpy函数

☻将显存中的HelloWorld转回内存,并且打印

整体代码:

/****kernel.cu****/
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
__global__ void cudaStrcpy(char *des, char *src) /*内核函数*/
{
while ((*src) != '\0') *des++ = *src++;
*des = '\0';
} /****gpu_helloworld.cu****/
#include "device.cu"
#include "kernel.cu"
#include "cstring"
void helloworld(char *str1, char *str2)
{
InitCUDA();
char *dev_str1=, *dev_str2=;
int size = strlen(str1) + ;
cudaMalloc((void**)&dev_str1, size); /*cuda系函数必须放在cu文件里*/
cudaMalloc((void**)&dev_str2, size);
cudaMemcpy(dev_str1, str1, size,cudaMemcpyHostToDevice);
cudaStrcpy<<<,>>>(dev_str2, dev_str1); /*单线程块、单线程*/
cudaMemcpy(str2, dev_str1, size, cudaMemcpyDeviceToHost);
} /****main.cpp****/
#include "cstdio"
#include "cstring"
extern void helloworld(char *str1, char *str2);
int main()
{
char src[] = "HelloWorld with CUDA";
char *des = new char[strlen(src)+];
helloworld(src, des);
printf("%s\n", des);
}

3.3 GPU版向量加法

向量加法是CUDA 7.0在VS中提供的样例模板,演示了并行算法的经典trick:循环消除。

利用单个线程块中,多个线程并发执行,来消除循环。

时间复杂度估计,不能简单从O(n)迁移到O(1),因为GPU同时并行量存在限制。

即便是Kepler架构中拥有192SP的SM阵列,理论同时并行量也不过是2048。

/****kernel.cu****/
__global__ void kernel_plus(int *a, int *b, int *c)
{
int x = threadIdx.x;
c[x] = a[x] + b[x];
} /****gpu_vectoradd.cu****/
void vectorAdd(int *a, int *b, int *c,int size)
{
if (!InitCUDA()) return;
int *dev_a = , *dev_b = , *dev_c = ;
cudaMalloc((void**)&dev_a, size*sizeof(int));
cudaMalloc((void**)&dev_b, size*sizeof(int));
cudaMalloc((void**)&dev_c, size*sizeof(int));
cudaMemcpy(dev_a, a, size*sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(dev_b, b, size*sizeof(int), cudaMemcpyHostToDevice);
kernel_plus << <, size >> >(dev_a, dev_b, dev_c);
cudaMemcpy(c, dev_c, size*sizeof(int), cudaMemcpyDeviceToHost);
} /****main.cpp****/
extern void vectorAdd(int *a, int *b, int *c, int size);
int main()
{
int a[] = { , , , , }, b[] = { , , , , }, c[] = { };
vectorAdd(a, b, c,);
printf("{1,2,3,4,5} + {10,20,30,40,50} = {%d,%d,%d,%d,%d}\n",
c[], c[], c[], c[], c[]);
}

CUDA程序设计(一)的更多相关文章

  1. CUDA程序设计(三)

    算法设计:基数排序 CUDA程序里应当尽量避免递归,因而在迭代排序算法里,基数排序通常作为首选. 1.1 串行算法实现 十进制位的基数排序需要考虑数位对齐问题,比较麻烦.通常实现的是二进制位的基数排序 ...

  2. CUDA程序设计(二)

    算法设计:直方图统计 直方图频数统计,也可以看成一个字典Hash计数.用处不是很多,但是涉及CUDA核心操作:全局内存.共享内存.原子函数. 1.1  基本串行算法 这只是一个C语言练习题. #def ...

  3. CUDA从入门到精通

    http://blog.csdn.net/augusdi/article/details/12833235 CUDA从入门到精通(零):写在前面 在老板的要求下.本博主从2012年上高性能计算课程開始 ...

  4. CUDA从入门到精通 - Augusdi的专栏 - 博客频道 - CSDN.NET

    http://blog.csdn.net/augusdi/article/details/12833235 CUDA从入门到精通 - Augusdi的专栏 - 博客频道 - CSDN.NET CUDA ...

  5. 【CUDA开发】CUDA从入门到精通

    CUDA从入门到精通(零):写在前面 在老板的要求下,本博主从2012年上高性能计算课程开始接触CUDA编程,随后将该技术应用到了实际项目中,使处理程序加速超过1K,可见基于图形显示器的并行计算对于追 ...

  6. 从零开始山寨Caffe·贰:主存模型

    你左手是内存,右手是显存,内存可以打死显存,显存也可以打死内存. —— 请协调好你的主存 从硬件说起 物理之觞 大部分Caffe源码解读都喜欢跳过这部分,我不知道他们是什么心态,因为这恰恰是最重要的一 ...

  7. CUDA5.5入门文章:VS10设置

    原文链接:http://blog.csdn.net/augusdi/article/details/12205435 作者专栏:http://blog.csdn.net/augusdi/article ...

  8. 【CUDA并行程序设计系列(1)】GPU技术简介

    http://www.cnblogs.com/5long/p/cuda-parallel-programming-1.html 本系列目录: [CUDA并行程序设计系列(1)]GPU技术简介 [CUD ...

  9. 《CUDA并行程序设计:GPU编程指南》

    <CUDA并行程序设计:GPU编程指南> 基本信息 原书名:CUDA Programming:A Developer’s Guide to Parallel Computing with ...

随机推荐

  1. iOS开发之功能模块--用runtime给UIView类别拓展PressMenu工具

    这是个很有用的列别工具类,在聊天对话框添加和QQ一样的"复制.粘贴.取消"等选项,而且使用起来很方便,只要找到聊天泡泡内部的某个View,比如Label或者背景冒泡的UIImage ...

  2. PHP实现把文本中的URL转换为链接的auolink()

    转载:http://www.jb51.net/article/52916.htm 其实我在<把文本中的URL地址转换为可点击链接的JavaScript.PHP自定义函数>一文中介绍过PHP ...

  3. Orchard中如何配置远端发布

    Orchard中默认安装是有Blog功能的.下面介绍如何配置Remote Blog Publishing功能,使用Windows Live Writer客户端发布博客. 一,开启Remote Blog ...

  4. WinHEC 2015 深圳之行

    在上个月,本人有幸在MVP Team和MVP Lead的帮助下,参加了微软在深圳召开的WinHEC 2015大会. 在本次大会上,见证了微软和国内企业的合作,并且发布一些关于在Windows 10方面 ...

  5. Oracle创建表空间、创建用户以及授权

    Oracle安装完后,其中有一个缺省的数据库,除了这个缺省的数据库外,我们还可以创建自己的数据库. 为了避免麻烦,可以用’Database Configuration Assistant’向导来创建数 ...

  6. SQL SERVER 2012启动失败 because upgrade step 'SSIS_hotfix_install.sql' 失败

    有台数据库服务器(开发服务器),开发人员邮件告诉我,SSMS连接不了这台服务器,远程登录后,发现SQL SERVER的服务停止了,启动服务时报错,服务启动不了.检查错误日志发现下面一些信息 2015- ...

  7. socket-详细分析No buffer space available

    关键词:socket,tcp三次握手,tcp四次握手,2MSL最大报文生存时间,LVS,负载均衡 新年上班第一天,突然遇到一个socket连接No buffer space available的问题, ...

  8. C++STL - 函数模板

    模板主要是为了泛型编程,做到与类型无关 模板有函数模板和类模板,本文主要整理的是函数模板 1.函数模板定义 template<typename 类型形参1,typename 类型形参2,...& ...

  9. docker核心原理

    容器概念. docker是一种容器,应用沙箱机制实现虚拟化.能在一台宿主机里面独立多个虚拟环境,互不影响.在这个容器里面可以运行着我饿们的业务,输入输出.可以和宿主机交互. 使用方法. 拉取镜像 do ...

  10. Android 通过Uri获取Bitmap对象

    (转自:http://blog.sina.com.cn/s/blog_5de73d0b0100zfm8.html) private Bitmap getBitmapFromUri(Uri uri) { ...