[CUDA] CUDA to DL

时间:2023-03-08 15:45:17

又是一枚祖国的骚年,阅览做做笔记:http://www.cnblogs.com/neopenx/p/4643705.html

这里只是一些基础知识。帮助理解DL tool的实现。

最新补充:我需要一台DIY的Deep learning workstation.

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

  • CUDA的广泛应用造就了GPU计算专用Tesla GPU的崛起。
  • 随着显卡的发展,GPU越来越强大,而且GPU为显示图像做了优化。在计算上已经超越了通用的CPU。如此强大的芯片如果只是作为显卡就太浪费了,因此NVidia推出CUDA,让显卡可以用于图像计算以外的目的
  • 目前只有G80、G92、G94、G96、GT200、GF100、GF110、GK100、GK104、GK107平台(即GeForce 8~Gecorce GTX780Ti)的NVidia显卡才能使用CUDA,工具集的核心是一个C语言编译器。G80中拥有128个单独的ALU(Arithmetic Logic Unit,算术逻辑单元),因此非常适合并行计算,而且数值计算的速度远远优于CPU。

Tesla GPU

Ref: http://bbs.gpuworld.cn/forum.php?mod=viewthread&tid=199
A. GeForce系列GPU追求“速度”,并不会对“数据正确性”进行“再确认”,因为“显示”就算有1%~2%的错误,也无伤大雅,反正刷屏速度60Hz,肉眼也分辨不出,错就错了也无所谓
B. 但对于“运算”需求,是容不得丝毫错误的,必须达到99.999999999....%正确率的,就有非常高的要求,这也是Tesla GPU必须采用ECC显存,以确保运算正确率的原因,导致价格比GeForce高出很多。

如果,你对“数据正确性”要求不高,那 GeForce 卡绝对会让你很开心。但如果你的计算结果容不下一点点错误,那还是得咬着牙选择 Tesla 专业卡,否则你还要花更多成本去面对难以预期的风险。

[CUDA] CUDA to DL

最后的caffe性能对比,自觉脑补。

OpenCL

OpenCL(全称Open Computing Language,开放运算语言)是第一个面向异构系统通用目的并行编程的开放式、免费标准,也是一个统一的编程环境,便于软件开发人员为高性能计算服务器、桌面计算系统、手持设备编写高效轻便的代码,而且广泛适用于多核心处理器(CPU)、图形处理器(GPU)、Cell类型架构以及数字信号处理器(DSP)等其他并行处理器,在游戏、娱乐、科研、医疗等各种领域都有广阔的发展前景。


CUDA到底是个什么?

为了泛型编程(C、C++、Fortran多语言)、以及榨取更多的计算力,NVIDIA对OpenCL进行的改装,贴合自己的GPU硬件架构,量身定做出CUDA。

较游戏程序员不同,CUDA程序员主要工作,就是把握硬件架构,在算法理论时间复杂度下,将算法串行执行体系,改组为并行执行体系。(能并行的并行化)

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

  1. 1. 计算能力1.0是跑CUDA的最低条件,这一时期的代表作是8800GT家族。
  2. 2. Fermi架构的计算能力是2.0,
  3. 3. Kepler是3.0,
  4. 4. Maxwell是4.0。

GPU会按照负载均衡的原则,将任务平均分至各个SM阵列 <---- Stream Multiprocessors(流多处理器),民间多译为SM计算阵列

对于每个SM阵列,就调度它手下那一伙CUDA核心干活。流处理器(SP)改名为"CUDA核心"。

2.

由于每个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。

3.

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做科学计算的时候,才会用双精度浮点运算。

4.

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太奢侈。

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int deviceCount;
cudaGetDeviceCount(&deviceCount);
int dev;
for (dev = ; dev < deviceCount; dev++)
{
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
if (dev == )
{
if (/*deviceProp.major==9999 && */deviceProp.minor = &&deviceProp.major==)
printf("\n"); }
printf("\nDevice%d:\"%s\"\n", dev, deviceProp.name);
printf("Total amount of global memory %u bytes\n", deviceProp.totalGlobalMem);
printf("Number of mltiprocessors %d\n", deviceProp.multiProcessorCount);
printf("Total amount of constant memory: %u bytes\n", deviceProp.totalConstMem);
printf("Total amount of shared memory per block %u bytes\n", deviceProp.sharedMemPerBlock);
printf("Total number of registers available per block: %d\n", deviceProp.regsPerBlock);
printf("Warp size %d\n", deviceProp.warpSize);
printf("Maximum number of threada per block: %d\n", deviceProp.maxThreadsPerBlock);
printf("Maximum sizes of each dimension of a block: %d x %d x %d\n", deviceProp.maxThreadsDim[],
deviceProp.maxThreadsDim[],
deviceProp.maxThreadsDim[]);
printf("Maximum size of each dimension of a grid: %d x %d x %d\n", deviceProp.maxGridSize[], deviceProp.maxGridSize[], deviceProp.maxGridSize[]);
printf("Maximum memory pitch : %u bytes\n", deviceProp.memPitch);
printf("Texture alignmemt %u bytes\n", deviceProp.texturePitchAlignment);
printf("Clock rate %.2f GHz\n", deviceProp.clockRate*1e-6f);
}
printf("\nTest PASSED\n");
getchar();
}

Comment:

deviceProp.name为GPU名字,如果没有GPU则会输出 Device Emulation 
deviceProp.totalGlobalMem返回的是全局储存器的大小,对大数据或一些大模型计算时显存大小必须大于数据大小,如图返回的是2GB的存储大小, 
deviceProp.multiProcessorCount返回的是设备中流多处理器(SM)的个数,流处理器(SP)的个数SM数×每个SM包含的SP数,其中帕斯卡为每个SM,64个SP,麦克斯韦为128个,开普勒为192个,费米为32个, 
deviceProp.totalConstMem返回的是常数储存器的大小,如同为64kB 
deviceProp.sharedMemPerBlock返回共享储存器的大小,共享存储器速度比全局储存器快, 
deviceProp.regsPerBlock返回寄存器的数目; 
deviceProp.warpSize返回线程束中线程多少; 
deviceProp.maxThreadsPerBlock返回一个block中最多可以有的线程数; 
deviceProp.maxThreadsDim[]返回block内3维度中各维度的最大值 
deviceProp.maxGridSize[]返回Grid内三维度中各维度的最大值; 
deviceProp.memPitch返回对显存访问时对齐时的pitch的最大值; 
deviceProp.texturePitchAlignment返回对纹理单元访问时对其参数的最大值; 
deviceProp.clockRate返回显存的频率;


附另一个可能是不错的链接,关于cuda programming:http://blog.****.net/augusdi/article/details/12833235

只看一些基础概念:

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

2.1.1 内核函数

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

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

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

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

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

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

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

helloworld<<<,>>>

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数量的限制很松,

如图:

[CUDA] CUDA to DL

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

BLOCKS=(N+THREADS−1)/THREADSBLOCKS=(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周期。


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

cudaMalloc:创建新的动态显存堆

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

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

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

一个例子:

/* 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);
}

另一个例子:

/* 向量加法是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[]);
}

Ref: http://blog.****.net/qiexingqieying/article/details/51734347

首先对这些框架进行总览。

库名称

开发语言

速度

灵活性

文档

适合模型

平台

上手难易

Caffe

c++/cuda

一般

全面

CNN

所有系统

中等

TensorFlow

c++/cuda/Python

中等

中等

CNN/RNN

Linux, OSX

MXNet

c++/cuda

全面

CNN

所有系统

中等

Torch

c/lua/cuda

全面

CNN/RNN

Linux, OSX

中等

Theano

python/c++/cuda

中等

中等

CNN/RNN

Linux, OSX

接下来将对这些框架进行分别介绍。

Caffe

第一个主流的工业级深度学习工具。
它开始于2013年底,由UC Berkely的Yangqing Jia老师编写和维护的具有出色的卷积神经网络实现。在计算机视觉领域Caffe依然是最流行的工具包。
它有很多扩展,但是由于一些遗留的架构问题,不够灵活且对递归网络和语言建模的支持很差。

TensorFlow

Google开源的其第二代深度学习技术——被使用在Google搜索、图像识别以及邮箱的深度学习框架。
是一个理想的RNN(递归神经网络)API和实现,TensorFlow使用了向量运算的符号图方法,使得新网络的指定变得相当容易,支持快速开发。
缺点是速度慢,内存占用较大。(比如相对于Torch)

MXNet

是李沐和陈天奇等各路英雄豪杰打造的开源深度学习框架,是分布式机器学习通用工具包DMLC 的重要组成部分。
它注重灵活性和效率,文档也非常的详细,同时强调提高内存使用的效率,甚至能在智能手机上运行诸如图像识别等任务。
Torch
Facebook力推的深度学习框架,主要开发语言是C和Lua。
有较好的灵活性和速度。
它实现并且优化了基本的计算单元,使用者可以很简单地在此基础上实现自己的算法,不用浪费精力在计算优化上面。核心的计算单元使用C或者cuda做了很好的优化。在此基础之上,使用lua构建了常见的模型。
缺点是接口为lua语言,需要一点时间来学习。
Theano
2008年诞生于蒙特利尔理工学院,主要开发语言是Python。
Theano派生出了大量深度学习Python软件包,最著名的包括BlocksKeras
Theano的最大特点是非常的灵活,适合做学术研究的实验,且对递归网络和语言建模有较好的支持,缺点是速度较慢。
Finally, 到此位置,应该对DL TOOL有了一定的理解。

我需要一台DIY的Deep Learning Workstation.

Goto: [GPU] DIY for Deep Learning Workstation