GPU高性能编程CUDA入门

ops/2024/9/25 10:47:54/

CMake中启用CUDA支持

最新版的 CMake(3.18 以上),只需在LANGUAGES 后面加上 CUDA 即可启用。
然后在 add executable 里直接加你的 .cu文件,和 .cpp 一样。

cmake minimum required(VERSION 3.10)set(CMAKE_CXX_STANDARD 17)
set(CMAKE BUILD TYPE Release)project(hellocuda LANGUAGES CXX CUDA)add executable(main main.cu)

CUDA和C++的关系就像C++和C的关系一样,大部分都兼容,因此能很方便的重用C++现有的任何代码库,引用C++头文件等

第一个程序

将CPU以及系统的内存成为主机(host)。而将GPU及其内存成为设备(device)。
在GPU设备上执行的函数通常称为核函数(kernel)。
没有核函数,只考虑在主机运行的CUDA代码和标准的C在很大程度上是没有区别的。

#include <iostream>__global__ void kernel(void){}int main(void){kernel<<<1,3>>>();printf("Hello,World!\n");return 0;
}

而当遇到具有__global__修饰符的函数时,编译器就会将该函数编译为在device上运行。在此例子中,函数kernel()将被交给编译device代码的编译器,而main()函数将被交给host编译器。
__inline__ 内联函数
__forceinline__ 强制内联
__noinline__ 禁止内联优化
__host__ 定义支持CPU调用
__device__ 定义支持GPU调用, 可以与__host__ 同时修饰

而对kernel()函数的调用语句则使用了一种尖括号和两个数值的方式,这里的<<<1, 3>>>

  • 参数一:表示设备在执行款核函数时使用的并行线程块的数量。
  • 参数二:需要多少个线程格(Grid)(一格表示N个线程块的集合)

参数传递

以下代码展示了如何像核函数传递参数并取得返回结果

#include "stdio.h"__global__ void add(int a, int b, int *c){*c = a+b;
}int main(){int c;int *dev_c;HANDLE_ERROR(cudaMalloc((void**)&dev_c, size(int)));add<<<1,1>>>(2,7,dev_c);HANDLE_ERROR(cudaMemcpy(&c,dev_c,sizeof(int),cudaMemcpyDeviceToHost));printf("2+7 = %d\n",c);cudaFree(dev_c);return 0;
}

上述代码说明,cuda可以像调用C函数那样将参数传递给核函数;当设备执行任何有用的操作时,都需要分配内存,例如将计算值返回给主机

cudaMalloc()函数:(注意,分配内存的指针不是该函数的返回值,这点与malloc()不同)

  • 参数一: 一个指针,指向用于保存新分配内存地址的变量。注意,由于C语言中,指针传递是本身也是值传递的,所以为了使指针本身的值(不是指针地址指向的值)可以改变,因此在传递时要使用双重指针void**,这样做的主要原因还是因为分配内存的指针最终不是通过函数返回,而是直接改变参数值导致的(如果传的是一重指针,则改变的是pd指向的内存空间的数据,而不是pd本身,所以pd也就不能指向GPU的内存了)。
  • 参数二:分配内存的大小

CUDA中对设备指针的使用限制总结如下:

  • 可以将cudaMalloc()分配的指针传递给在设备上执行的函数
  • 可以在设备代码中使用cudaMalloc()分配的指针进行内存读写操作
  • 可以将cudaMalloc()分配的指针传递给在主机上执行的函数
  • 不能在主机代码中使用cudaMalloc()分配的指针进行内存读写操作

在主机代码中,可以通过调用cudaMemcpy()来访问设备上的内存。这个函数调用的行为类型与标准C中的memcpy(),只不过多了一个参数来指定设备内存指针究竟是源指针还是目标指针。如,当最后一个参数为cudaMemcpyDeviceToHost时,代表运行时源指针是一个设备指针,而目标指针是以个主机指针。此外还有参数cudaMemcpyHostToDevicecudaMemcpyDeviceToDevice等,如果源指针和目标指针都是位于主机上,那么可以直接调用标准C的memcpy()函数。

查询设备

对于拥有多个支持CUDA的设备,需要通过某种方式来确定使用的是哪一个设备。

int count;
HANDLE_ERROR(cudaGetDeviceCount(&count));

在调用cudaGetDeviceCount()后,可以对每个设备进行迭代,并查询各个设备的相关信息。CUDA runtime将返回一个cudaDeviceProp类型的结构,其中包含了设备的相关属性。可以利用cudaGetDeviceProperties()来获得i号设备的属性:

#include <iostream>int main(){cudaDeviceProp prop;int count;cudaGetDeviceCount(&count);for(int i =0 ; i< count; i++){cudaGetDeviceProperties(&prop, i);//对设备的属性执行某些操作}std::cout<<count<<std::endl;
}

在知道了每个可用的属性以后,接下来就可以进行一些具体的操作,如:

std::cout<<prop.major<<std::endl;

设备属性的使用

根据在cudaGetDeviceCount()cudaGetDeviceProperties()中返回的结果,我们可以对每个设备进行迭代,来找到我们期望的某些达到要求的设备。但是这种迭代操作执行起来有些繁琐,因此CUDA runtime提供了一种自动方式来执行这个迭代操作。首先,找出希望设备拥有的属性并将这些属性填充到一个cudaDeviceProp结构。

cudaDeviceProp prop;
memset(&prop, 0 , sizeof(cudaDeviceProp));
prop.major = 1;
prop.minor = 3;

之后,将该结构传递给cudaChooseDevice(),这样CUDA runtime运行时将查找是否存在某个设备满足这些条件,并返回一个设备ID,我们可以将这个设备ID传递给cudaSetDevice()。随后,所有的设备操作都将在这个设备上执行。

#include <iostream>
using std::cout;
using std::endl;int main(){cudaDeviceProp prop;memset(&prop, 0 , sizeof(cudaDeviceProp));prop.major = 1;prop.minor = 3;int dev;cudaGetDevice(&dev);cudaChooseDevice(&dev, &prop);cout<<"ID:"<<dev<<endl;
}

CUDA C并行编程

基于GPU的矢量求和

#define N 10
int main(){int a[N],b[N],c[N];int *dev_a, *dev_b, *dev_c;//在GPU上分配内存,注意这里要知道为什么使用void**cudaMalloc( (void**)&dev_a, N*sizeof(int));cudaMalloc( (void**)&dev_a, N*sizeof(int));cudaMalloc( (void**)&dev_a, N*sizeof(int));...//创建a,b数组并赋值//将数组a,b复制到GPUcudaMemcpy(dev_a, a, N*sizeof(int),cudaMemcpyHostToDevice);cudaMemcpy(dev_b, b, N*sizeof(int), cudaMemcpyHostToDevice);add<<<N,1>>>(dev_a, dev_b, dev_c);//将数组c从GPU复制到CPUcudaMemcpy(c, dev_c, N*sizeof(int), cudaMemcpyDeviceToHost);...//显式结果//释放GPU上分配的内存cudaFree(dev_a);cudaFree(dev_b);cudaFree(dev_c);return 0;
}

看一下核函数的调用:
add<<<N,1>>>(dev_a, dev_b, dev_c);
尖括号中的两个数值将传递给runtime,作用是告诉runtime如何启动核函数:我们将每个并行执行环境都称为一个线程块(Block),对于此例,将有N个线程块在GPU上运行(N个运行核函数的副本)。

如何在代码中知道当前正在运行的是哪一个线程块?答:利用变量blockIdx.x
threadIdx.x 读取线程
blockDim.x 获取当前线程数量

add()函数:

__global__ void add(int *a, int *b, int *c){int tid = blockIdx.x;  //计算机该索引处的数据if(tid < N)c[tid] = a[tid] + b[tid];
}

当启动核函数时,我们将并行线程块的数量指定为N。这个并行线程块集合就称为一个“线程格(Grid)”。因此,此例表示我们想要一个一维的线程格,其中每个线程格包含N个线程块,每个线程块的blockInx.x的值都是不同的,cuda会为每个设备代码副本提供不同的blockInx.x。

需要注意的一点是:在启动线程块数组时,数组每一维(N)的最大数量不能超过65535。这是一种硬件限制,如过启动的线程块数量超过了这个限制,那么程序将运行失败。

线程协作

【并行计算】CUDA在现代C++中如何运用?看这一个就够了!
《GPU高性能编程CUDA实战 CUDA By Example》


http://www.ppmy.cn/ops/115747.html

相关文章

spark 面试题

spark 面试题 1、spark 任务如何解决第三方依赖 比如机器学习的包&#xff0c;需要在本地安装&#xff1f;--py-files 添加 py、zip、egg 文件不需要在各个节点安装 2、spark 数据倾斜怎么解决 spark 中数据倾斜指的是 shuffle 过程中出现的数据倾斜&#xff0c;主要是由于…

微服务——网关登录校验(一)

1.网关登录校验 微服务中的网关登录校验是微服务架构中常见的一种安全机制&#xff0c;用于在请求到达微服务之前&#xff0c;对用户的身份进行验证&#xff0c;确保只有合法的用户才能访问相应的服务。 在微服务架构中&#xff0c;每个微服务都是独立部署的&#xff0c;它们之…

Linux常用命令大全

Linux系统中拥有大量的命令&#xff0c;这里列举一些常用的命令&#xff0c;覆盖了文件管理、系统监控、网络操作、用户管理。 文件和目录管理 ls&#xff1a;列出目录内容。 ls -l 列出当前目录下所有文件和目录的详细信息。 cd&#xff1a;更改当前目录。 cd /home/user 进…

文字转换(中文转英文、英文转中文)

1.环境 Python 3.10.14 2.完整代码 from transformers import pipeline, AutoModelWithLMHead, AutoTokenizermodelName "./damo/opus-mt-zh-en" # 模型名 model AutoModelWithLMHead.from_pretrained(modelName) tokenizer AutoTokenizer.from_pretrained(mo…

人工智能开发实战推荐算法应用解析

内容导读 个性化推荐思路推荐算法分类推荐效果评估 一、个性化推荐思路 推荐系统能为你提供个性化的智能服务&#xff0c;是基于以下事实认知&#xff1a;人们倾向于喜欢那些与自己喜欢的东西相似的其它物品&#xff0c;或倾向于与自己趣味相投的人有相似的爱好&#xff0c;…

基于PHP+MySQL组合开发的在线客服源码系统 聊天记录实时保存 带完整的安装代码包以及搭建部署教程

系统概述 随着互联网技术的飞速发展&#xff0c;企业与客户之间的沟通方式日益多样化&#xff0c;在线客服系统作为连接企业与客户的桥梁&#xff0c;其重要性不言而喻。然而&#xff0c;市场上现有的在线客服系统往往存在成本高、定制性差、维护复杂等问题。针对这些痛点&…

【自动化测试】Appium Server如何安装和Appium Server安装困难的原因和解决方法以及常见的一些安装失败的错误和解决方法

引言 Appium Server安装过程时常出现问题&#xff0c;以下是安装Appium Server过程一些原因、常见错误和解决方法 文章目录 引言一、Appium Server如何安装1.1 Node.js 安装1.2 使用NPM安装Appium1.3 验证Appium安装1.4 运行Appium Server1.5 使用Appium Desktop&#xff08;可…

Git常用指令

目录 前言1、git commit -m "write your description"2、git commit --amend3、git push -f4、git push origin master前言 在学校从来没使用过git版本管理,只会简单的把代码从github上拉下来,或者简单的把本地的代码push到自己github账号的仓库里面,但实际工作中…