作者丨xcyuyuyu@知乎(已授权)
来源丨https://zhuanlan.zhihu.com/p/507678214
编辑丨极市平台
1. 前言这是一份简单的CUDA编程入门,主要参考英伟达的官方文档进行学习,本人也是刚开始学习,如有表述错误,还请指出。官方文档链接如下:
https://developer.nvidia.com/blog/even-easier-introduction-cuda/
本文先从一份简单的C++代码开始,然后逐步介绍如何将C++代码转换为CUDA代码,以及对转换前后程序的运行时间进行对比,本文代码放在我的github中,有需要可以自取。
https://github.com/xcyuyuyu/My-First-CUDA-Code
本文所使用的CPU为i7-4790,GPU为GTX 1080,那就开始吧。
2. 一份简单的C++代码首先是一份简单的C++代码,主要的运行函数为add函数,该函数实现功能为30M次的for循环,每次循环进行一次加法。
// add.cpp #include #include #include // function to add the elements of two arrays void add(int n, float *x, float *y) { for (int i = 0; i < n; i++) y[i] = x[i] + y[i]; } int main(void) { int N = 1<<25; // 30M elements float *x = new float[N]; float *y = new float[N]; // initialize x and y arrays on the host for (int i = 0; i < N; i++) { x[i] = 1.0f; y[i] = 2.0f; } struct timeval t1,t2; double timeuse; gettimeofday(&t1,NULL); // Run kernel on 30M elements on the CPU add(N, x, y); gettimeofday(&t2,NULL); timeuse = (t2.tv_sec - t1.tv_sec) + (double)(t2.tv_usec - t1.tv_usec)/1000.0; std::cout << "add(int, float*, float*) time: " << timeuse << "ms" << std::endl; // Check for errors (all values should be 3.0f) float maxError = 0.0f; for (int i = 0; i < N; i++) maxError = fmax(maxError, fabs(y[i]-3.0f)); std::cout << "Max error: " << maxError << std::endl; // Free memory delete [] x; delete [] y; return 0; }
编译以及运行代码:
g++ add.cpp -o add ./add
不出意外的话,你应该得到下面的结果:

第一行表示add函数的运行时间,第二行表示每个for循环里的计算是否与预期结果一致。
这个简单的C++代码在CPU端运行,运行时间为85ms,接下来介绍如何将主要运算的add函数迁移至GPU端。
3. 把C++代码改成CUDA代码将C++代码改为CUDA代码,目的是将add函数的计算过程迁移至GPU端,利用GPU的并行性加速运算,需要修改的地方主要有3处:
1.首先需要做的是将add函数变为GPU可运行函数,在CUDA中称为kernel,为此,仅需将变量声明符添加到函数中,告诉 CUDA C++ 编译器这是一个在 GPU 上运行并且可以从 CPU 代码中调用的函数。
__global__ void add(int n, float *x, float *y) { for (int i = 0; i < n; i++) y[i] = x[i] + y[i]; }
那么修改后的add函数的调用也比较简单,仅需要在add函数名后面加上三角括号语法<<>>指定CUDA内核启动即可,<<>>称为执行配置(execution configuration),用于配置程序运行时的线程,后续会讲到,目前先将其设置为<<>>:
add<<<1, 1>>>(N, x, y);
2. 那么为了在GPU进行计算,需要在GPU上分配可访问的内存。CUDA中通过Unified Memory(统一内存)机制来提供可同时供GPU和CPU访问的内存,使用cudaMallocManaged()函数进行分配:
cudaMallocManaged(&x, N*sizeof(float)); cudaMallocManaged(&y, N*sizeof(float));
同时,在程序最后使用cudaFree()进行内存释放:
cudaFree(x); cudaFree(y);
其实就相当于C++中的new跟delete。
3. add函数在GPU端运行之后,CPU需要等待cuda上的代码运行完毕,才能对数据进行读取,因为CUDA内核启动时并未对CPU的线程进行固定,需要使用cudaDeviceSynchronize()函数进行同步。
4. 整体的程序如下所示:
// add.cu #include #include // Kernel function to add the elements of two arrays // __global__ 变量声明符,作用是将add函数变成可以在GPU上运行的函数 // __global__ 函数被称为kernel, // 在 GPU 上运行的代码通常称为设备代码(device code),而在 CPU 上运行的代码是主机代码(host code)。 __global__ void add(int n, float *x, float *y) { for (int i = 0; i < n; i++) y[i] = x[i] + y[i]; } int main(void) { int N = 1<<25; float *x, *y; // Allocate Unified Memory – accessible from CPU or GPU // 内存分配,在GPU或者CPU上统一分配内存 cudaMallocManaged(&x, N*sizeof(float)); cudaMallocManaged(&y, N*sizeof(float)); // initialize x and y arrays on the host for (int i = 0; i < N; i++) { x[i] = 1.0f; y[i] = 2.0f; } // Run kernel on 1M elements on the GPU // execution configuration, 执行配置 add<<<1, 1>>>(N, x, y); // Wait for GPU to finish before accessing on host // CPU需要等待cuda上的代码运行完毕,才能对数据进行读取 cudaDeviceSynchronize(); // Check for errors (all values should be 3.0f) float maxError = 0.0f; for (int i = 0; i < N; i++) maxError = fmax(maxError, fabs(y[i]-3.0f)); std::cout << "Max error: " << maxError << std::endl; // Free memory cudaFree(x); cudaFree(y); return 0; }
使用nvcc对程序进行编译并运行:
nvcc add.cu -o add_cuda ./add_cuda
或者使用nvprof进行速度测试:
nvprof ./add_cuda
不出意外的话,你会得到以下输出:

框出来的就是add函数在GPU端的运行时间,为4s。没错,就是比CPU端85ms还要慢,那还学个锤子。

好的回过头看看,问题出现在这个执行配置 <<>> 上。不急,先看一下一个简单的GPU结构示意图,按照层次从大到小可将GPU按照 grid -> block -> thread划分,其中最小单元是thread,并行的本质就是将程序的计算模块拆分成多个小模块扔给每个thread并行计算。

再看一下前面执行配置 `<<>>` 的含义,`<<>>` 应该写成 `<<>>` ,即表示函数运行时使用的block数量以及每个block的大小,前面我们将其设置为`<<<1,1>>>` ,说明程序是单线程运行的,那当然慢了~~。下面我们以单个block为例,将其改为`<<<1,256>>>`,add函数也需要适当修改:
__global__ void add(int n, float *x, float *y) { int index = threadIdx.x; // threadIdx.x表示当前在第几个thread上运行 int stride = blockDim.x; // blockDim.x表示每个block的大小 for (int i = index; i < n; i += stride) y[i] = x[i] + y[i]; }
修改的部分也比较好理解,不赘述了,接下来运行看看结果:

你看,开始加速了吧,4s加速到了77ms。

那么,`<<>>` 的两个参数应该怎么设置好呢。首先,CUDA GPU 使用大小为 32 的倍数的线程块运行内核,因此 `blockSize` 的大小应该设置为32的倍数,例如128、256、512等。确定 `blockSize` 之后,可以根据for循环的总个数`N`确定 `numBlock` 的大小(注意四舍五入的误差):
int numBlock = (N + blockSize - 1) / blockSize;
当然因为变成了多个`block`,所以此时add函数需要再改一下:
__global__ void add(int n, float *x, float *y) { int index = blockIdx.x * blockDim.x + threadIdx.x; int stride = blockDim.x * gridDim.x; for (int i = index; i < n; i+=stride) y[i] = x[i] + y[i]; }
这里index跟stride的计算可以参考上面GPU结构图以及下面的图(图取自An Even Easier Introduction to CUDA | NVIDIA Technical Blog),自行推算,较好理解。

搞定之后再编译运行一下:

看看,又加速了不是,通过提升并行度而加速,相比于CPU端(85ms)加速了接近一倍左右。
5. 结论以上仅是一份简单的CUDA入门代码,看起来还算比较简单,不过继续深入肯定有更多的坑,期待后面有时间继续学习。
本文代码:
GitHub - xcyuyuyu/My-First-CUDA-Code: The introduction to cuda, a simple and easy cuda project
https://github.com/xcyuyuyu/My-First-CUDA-Code
参考文献
[1] An Even Easier Introduction to CUDA | NVIDIA Technical Blog(https://developer.nvidia.com/blog/even-easier-introduction-cuda/)
本文仅做学术分享,如有侵权,请联系删文。
3D视觉工坊精品课程官网:3dcver.com
1.面向自动驾驶领域的多传感器数据融合技术
2.面向自动驾驶领域的3D点云目标检测全栈学习路线!(单模态+多模态/数据+代码) 3.彻底搞透视觉三维重建:原理剖析、代码讲解、及优化改进 4.国内首个面向工业级实战的点云处理课程 5.激光-视觉-IMU-GPS融合SLAM算法梳理和代码讲解 6.彻底搞懂视觉-惯性SLAM:基于VINS-Fusion正式开课啦 7.彻底搞懂基于LOAM框架的3D激光SLAM: 源码剖析到算法优化 8.彻底剖析室内、室外激光SLAM关键算法原理、代码和实战(cartographer+LOAM +LIO-SAM)
9.从零搭建一套结构光3D重建系统[理论+源码+实践]
10.单目深度估计方法:算法梳理与代码实现
11.自动驾驶中的深度学习模型部署实战
12.相机模型与标定(单目+双目+鱼眼)
13.重磅!四旋翼飞行器:算法与实战
14.ROS2从入门到精通:理论与实战
15.国内首个3D缺陷检测教程:理论、源码与实战
重磅!3DCVer-学术论文写作投稿 交流群已成立
扫码添加小助手微信,可申请加入3D视觉工坊-学术论文写作与投稿 微信交流群,旨在交流顶会、顶刊、SCI、EI等写作与投稿事宜。
同时也可申请加入我们的细分方向交流群,目前主要有3D视觉、CV&深度学习、SLAM、三维重建、点云后处理、自动驾驶、多传感器融合、CV入门、三维测量、VR/AR、3D人脸识别、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地、视觉竞赛、车牌识别、硬件选型、学术交流、求职交流、ORB-SLAM系列源码交流、深度估计等微信群。
一定要备注:研究方向+学校/公司+昵称,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,可快速被通过且邀请进群。原创投稿也请联系。
▲长按加微信群或投稿
▲长按关注公众号
3D视觉从入门到精通知识星球:针对3D视觉领域的视频课程(三维重建系列、三维点云系列、结构光系列、手眼标定、相机标定、激光/视觉SLAM、自动驾驶等)、知识点汇总、入门进阶学习路线、最新paper分享、疑问解答五个方面进行深耕,更有各类大厂的算法工程人员进行技术指导。与此同时,星球将联合知名企业发布3D视觉相关算法开发岗位以及项目对接信息,打造成集技术与就业为一体的铁杆粉丝聚集区,近4000星球成员为创造更好的AI世界共同进步,知识星球入口:
学习3D视觉核心技术,扫描查看介绍,3天内无条件退款
圈里有高质量教程资料、答疑解惑、助你高效解决问题
觉得有用,麻烦给个赞和在看~