二. CUDA编程入门-CUDA中的线程与线程束

目录

    • 前言
    • 0. 简述
    • 1. 执行一下我们的第一个CUDA程序
    • 2. CUDA中的grid和block
    • 3. block和thread的遍历(traverse)
    • 4. nvcc编译器
    • 5. Makefile部分
    • 6. 执行我们的第二个CUDA程序
    • 7. Makefile添加的部分
    • 总结
    • 参考

前言

自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考

本次课程我们来学习下课程第二章——CUDA编程入门,一起来学习 CUDA 中的线程与线程束

Note:线程束杜老师之前也讲过,感兴趣的可以看看 CUDA编程之基础,内存模型和线程束,另外也有一篇关于 CUDA 编程的教程 CUDA编程入门极简教程,非常建议大家看看

课程大纲可以看下面的思维导图

在这里插入图片描述

0. 简述

本小节目标:
1. 理解在 CUDA 中一维二维三维的 grid,block 的写法,以及遍历 thread 的方法

2. 理解 .cu 和 .cpp 中相互引用时的注意事项,理解 Makefile 的写法

今天我们来讲解第二章节 CUDA 编程入门,从这一章节开始,我们会结合代码以及 PPT 里面的各种知识点,一起来把整个知识框架给搭建起来,第二章节内容主要分为以下五个部分:

  • CUDA 中的线程与线程束
  • 使用 CUDA 进行 matmul 计算
  • 共享内存以及 bank conflict
  • 使用 CUDA 进行预处理/后处理
  • stream 和 event

首先第一个要讲的是 CUDA 中的线程和线程束,它们是 CUDA 中的一个很基本的概念,我们会教大家如何去理解 thread 和 block 这个概念,同时 CUDA 的 thread id 它这个索引是如何计算的

第二个是如何使用 CUDA 进行矩阵乘法的一个计算,这部分我们会根据 CPU 和 GPU 两个实现来进行一个速度比较,同时也会在 GPU 中使用各种不同策略的核函数来进行一个矩阵乘法的计算比较,来告诉大家如何去选择一个比较好的方式去加速

第三个也是比较重要的概念,共享内存以及 bank conflict,共享内存是大家在写核函数的时候经常会遇到的一个问题,就是如何去高效利用你的共享内存,以及 bank conflict 它什么时候会发生,它为什么会发生,以及发生之后我们如何去解决它

第四个部分是偏实际操作的一些东西,比如我们在利用 TensorRT 进行一个模型部署的时候,其实我们需要考虑如何利用 CUDA 来对 TensorRT 的一些预处理和后处理来进行一个加速,比如 resize、crop 等等这些东西它其实都是可以用 CUDA 来进行加速的,甚至像 3D 点云这些东西其实也是可以考虑 CUDA 来进行加速的,

第五个部分 stream 和 event,这个也是 CUDA 中的一个基本概念,我们会教大家如何利用 CUDA 写一个多流的程序来提高你的并发性

OK,我们来看第一章第一小节,CUDA 中的线程和线程束,这一章结束后希望大家能够理解 CUDA 中的一维、二维、三维的 Grid 和 Block 它的写法是什么样子的,以及如何去计算你的 thread id 的索引

1. 执行一下我们的第一个CUDA程序

源代码获取地址:https://github.com/kalfazed/tensorrt_starter

我们来看第一个案例 2.1-dim_and_index,包含的文件内容如下:

在这里插入图片描述

Makefile、compile_commands.json 以及 Makefile.config 是用于编译过程中环境变量和一些参数的设置,src 文件夹下是我们的源文件,它包含一个 print_index.cu 的 CUDA 文件

这个案例是一个非常简单的打印索引的 CUDA 程序,具体实现我们先不看,先执行 make 编译一下,输出如下所示:

在这里插入图片描述

Note:我们也可以用 nvcc 相关指令进行编译,指令如下所示:

nvcc print_index.cu -o print_index.cu.o

nvcc 是 cuda 的编译器,定义和 gcc、g++ 是一样的

编译完成之后多了一个 trt-cuda 可执行文件,执行后输出如下:

在这里插入图片描述

程序会将各个维度上的 block idx 和 thread idx 给打印出来,我们来稍微简单看一下里面的部分代码,如下所示:

__global__ void print_idx_kernel(){
    printf("block idx: (%3d, %3d, %3d), thread idx: (%3d, %3d, %3d)\n",
         blockIdx.z, blockIdx.y, blockIdx.x,
         threadIdx.z, threadIdx.y, threadIdx.x);
}

void print_two_dim(){
    int inputWidth = 4;

    int blockDim = 2;
    int gridDim = inputWidth / blockDim;

    dim3 block(blockDim, blockDim);
    dim3 grid(gridDim, gridDim);

    /* 这里建议大家吧每一函数都试一遍*/
    // print_idx_kernel<<<grid, block>>>();
    // print_dim_kernel<<<grid, block>>>();
    // print_thread_idx_per_block_kernel<<<grid, block>>>();
    print_thread_idx_per_grid_kernel<<<grid, block>>>();

    cudaDeviceSynchronize();
}

主要有以下几个点和以往的 C/C++ 程序不同:

  • 文件的后缀名是 .cu
  • <<<>>> 符号的出现
  • __global__

2. CUDA中的grid和block

上面的程序里面其实有很多我们不清楚的概念,现在我们先简单介绍下 CUDA 编程中的一些基本概念,首先我们来看 CUDA 编程中的 grid 和 block,它们是大家在写 CUDA 程序时第一个要接触的概念,我们看下面这张图:

在这里插入图片描述

CUDA编程中的grid和block的概念启动一个kernel的时候需要指定grid和block

首先我们计算机里面有一个 host 和 device,我们可以认为 host 就是你的 CPU,device 就是你的 GPU,其实我们一般在写一个程序的时候肯定是从 CPU 开始执行的,然后执行到一定程度之后开始调用你的 device 做一些计算,这个时候我们的入口就是这个 kernel,kernel 它是一个函数,是一个以线程为单位来进行计算的一个函数,我们管它叫核函数

我们之前讲过 CUDA 编程它其实里面有大量的 thread,成千上万个 thread 来进行一些处理,这所有的 thread 它其实都是在执行同一个核函数,同一个 kernel。一般来说一个 kernel 它会分配一个 Grid,一个 Grid 里面有很多个 block,而一个 block 里面又有很多个 threa,它是一个层级的架构。

所以大家可以这么认为,grid 和 block 它其实都是属于大量 thread 的一个组合,它之所以这样去划分,比如把 Grid 分为好几个多维的 block,block 分为好几个多维的 thread,它其实就是为了能够帮助你进行一个快速的索引。另外值得注意的是 grid 和 block 它其实是属于逻辑意义上的东西,它并不是说在 GPU 硬件上实际存在的东西,比如说我们 GPU 里面有 SM,我们的 SM 它其实里面是没有 grid 和 block 这些东西的,它就是在编程里面方便使用的一个逻辑上的概念

grid 和 block 它是什么样的关系呢?block 里面有很多的 thread,每一个 thread 它都会有一个自己的 register 和一个自己的 local memory。同时我们可以看到每一个 block 里面的 thread 它们其实都是在共享一个 shared memory。我们一个 grid 里面的很多个 block 它其实都是在访问同一 global memory,constant memory 和 texture memory

这里面其实可以看到它又分了好几个 memory,这个在之前的课程中也讲过,它其实是分层级的一个 memory,就是离你的计算单元越近的 memory 越快,所以我们知道你的 register 是最快的,其次是你的 shared memory,再往下走就是 global memory 这些东西,所以说我们再写 CUDA 程序的时候就要想如何去高速的利用 shared memory,这个我们在之后的小节中会详细讲,这里先跳过

那我们接着看,一般来说:

  • 一个 kernel 对应一个 grid
  • 一个 grid 可以有多个 block,一维~三维
  • 一个 block 可以有多个 thread,一维~三维

我们需要注意一下,其实当我们定义 grid 和 block 的时候,我们可以给分配一维二维或者三维的,上图给展示的是一个二维的

3. block和thread的遍历(traverse)

我们先从一维去考虑

在这里插入图片描述

比如说我们现在有一个数组,它有 8 个数据,现在我们想给它切分成两个 block 来进行访问,一个 block 我们有 4 个线程分别去访问不同的地址,如上图所示

那我们可以这么写:

void print_one_dim(){
    int inputSize = 8;
    
    int blockDim = 4;
    int gridDim = inputSize / blockDim;
    
    dim3 block(blockDim);
    dim3 grid(gridDim);
}

首先 input_size 等于 8,block_dim 等于 4,说明一个 block 是 4 个线程;之后 grid 是你的 input_size 除以 block_dim 等于 2,所以我们 dim3 block 它传入的参数就是 4,dim3 grid 传入的参数就是 2

我们看一下 dim3 是什么:

struct __device_builtin__ dim3
{
    unsigned int x, y, z;
#if defined(__cplusplus)
#if __cplusplus >= 201103L
    __host__ __device__ constexpr dim3(unsigned int vx = 1, unsigned int vy = 1, unsigned int vz = 1) : x(vx), y(vy), z(vz) {}
    __host__ __device__ constexpr dim3(uint3 v) : x(v.x), y(v.y), z(v.z) {}
    __host__ __device__ constexpr operator uint3(void) const { return uint3{x, y, z}; }
#else
    __host__ __device__ dim3(unsigned int vx = 1, unsigned int vy = 1, unsigned int vz = 1) : x(vx), y(vy), z(vz) {}
    __host__ __device__ dim3(uint3 v) : x(v.x), y(v.y), z(v.z) {}
    __host__ __device__ operator uint3(void) const { uint3 t; t.x = x; t.y = y; t.z = z; return t; }
#endif
#endif /* __cplusplus */
};

typedef __device_builtin__ struct dim3 dim3;

dim3 它其实是 CUDA 内部的一个 struct,它里面有一个 int 类型的一个 x、y、z,所以就代表我们可以给设定为一维、二维、三维,根据你传入参数的不同,它会初始化成不同维度的一个 dim,这个是完全 OK 的

如果说一维是可以的,那么我们来看二维怎么定义:

void print_two_dim(){
    int inputWidth = 8;

    int blockDim = 2;
    int gridDim = inputWidth / blockDim;

    dim3 block(blockDim, blockDim);
    dim3 grid(gridDim, gridDim);

    /* 这里建议大家吧每一函数都试一遍*/
    // print_idx_kernel<<<grid, block>>>();
    // print_dim_kernel<<<grid, block>>>();
    // print_thread_idx_per_block_kernel<<<grid, block>>>();
    print_thread_idx_per_grid_kernel<<<grid, block>>>();

    cudaDeviceSynchronize();
}

比如我们现在有一个 8x8 的二维数组,共 64 个数据,然后现在我们设定它的一个 blockDim 的宽度是 2,那就是一个 block 里面有 2x2 个线程(因为 block 设定为二维的),然后一个 grid 里面有 4x4 个 block

我们回到 print_one_dim 函数里面,我们看它的 thread id 是怎么打印出来的

__global__ void print_idx_kernel(){
    printf("block idx: (%3d, %3d, %3d), thread idx: (%3d, %3d, %3d)\n",
         blockIdx.z, blockIdx.y, blockIdx.x,
         threadIdx.z, threadIdx.y, threadIdx.x);
}

因为你的 grid 和 block 它可以是三维,也可以是二维,也可以一维,所以你的 block id 和你的 thread id 它对应的其实是有各个维度的,当我们在进行索引的时候,它是先从你的 z 去索引,再去访问你的 y,最后再去访问你的 x。

运行打印输出如下:

在这里插入图片描述

每一个不同的维度它都有个自己的索引,因为数据一共是 8 个,所以它一共有 8 个不同的输出,在这里大家需要注意一下,之前我们说每一个 thread 它都会执行一个 kernel,所以你的 thread 它有自己的 id,所以它执行的结果它完全是不一样的。之后每一个 block 它都有一个自己的 thread,thread id 它是从 0~3 的一个个变化,你的 block id 它也是从 0~1 这样的变化

核函数需要注意一下,前面一定要写一个 __global__ 来表示它是一个核函数,这个大家写的时候需要注意

我们再看另外一个核函数 print_dim_kernel

__global__ void print_dim_kernel(){
    printf("grid dimension: (%3d, %3d, %3d), block dimension: (%3d, %3d, %3d)\n",
         gridDim.z, gridDim.y, gridDim.x,
         blockDim.z, blockDim.y, blockDim.x);
}

它是把你 grid 和 block 各个维度上的一个 dim 的大小给打印出来,我们执行一下输出如下:

在这里插入图片描述

因为你的 grid dim 和 block dim 的大小其实并不会根据你的 thread id 的变化而变化,所以它的结果都是一样的,grid 先是 z 再是 y 再是 x,维度是 1x1x2。block 也是先 z 和 y 方向,之后 x 方向是 4,维度是 1x1x4

我们现在这个索引,它其实是各个维度上的一个索引,这样只是看起来比较好看,但是如果我们希望这个索引它是按照一个一维的方式展现出来,我们应该怎么做呢?

我们看另一个核函数,print_thread_idx_per_block_kernel

__global__ void print_thread_idx_per_block_kernel(){
    int index = threadIdx.z * blockDim.x * blockDim.y + \
              threadIdx.y * blockDim.x + \
              threadIdx.x;

    printf("block idx: (%3d, %3d, %3d), thread idx: %3d\n",
         blockIdx.z, blockIdx.y, blockIdx.x,
         index);
}

这里索引计算这里需要大家记住一个诀窍,就是 thread 它有 x、y、z 维度,那我们先去访问你 z 维度上的 threadIdx 之后乘上你的 blockDim 的 x 和 y,然后再是你的 threadIdx.y 乘上 blockDim.x 那一行的数据,之后再是你 threadIdx.x 的一列

这么看可能看起来有点麻烦,我们结合图去理解一下:

在这里插入图片描述

我们假设 block 是三维的,先遍历 z 方向,每一个 z 方向它都有 blockDim.x * blockDim.y 这些量级的 thread,找到了 z 方向是第几个之后我们再看 y 方向,y 方向就是具体看它在哪一行,所以 threadIdx.y * blockDim.x,找到在哪一行之后,我们再去找它是哪一列也就是 threadIdx.x,最终我们就能找到图中红色点线程的一个索引了

我们执行一下输出如下:

在这里插入图片描述

可以看到我们现在已经能够以一维的方式去打印了

我们看另一个核函数,print_thread_idx_per_grid_kernel

__global__ void print_thread_idx_per_grid_kernel(){
    int bSize  = blockDim.z * blockDim.y * blockDim.x;

    int bIndex = blockIdx.z * gridDim.x * gridDim.y + \
               blockIdx.y * gridDim.x + \
               blockIdx.x;

    int tIndex = threadIdx.z * blockDim.x * blockDim.y + \
               threadIdx.y * blockDim.x + \
               threadIdx.x;

    int index  = bIndex * bSize + tIndex;

    printf("block idx: %3d, thread idx in block: %3d, thread idx: %3d\n", 
         bIndex, tIndex, index);
}

我们如果说不是想要在一个 block 里面去寻找你的 thread id,而是在一个 grid 的空间下去寻找你的 thread,怎么办呢?这个我们也是结合着图去看一下可能会比较好理解:

在这里插入图片描述

grid 它可以是多维的 block,我们把图中每一个小方块给理解为一个 block,我们现在需要从这么大的空间里面去找这个红色的点,我们怎么做呢?我们肯定是先需要去找它的这个 block 到底是在哪一个,我们需要找 block id,它跟 thread id 的索引是一样的,先从 z 寻找之后,再去寻找你的 y 之后再去寻找你的 x,找到了你的 block id 之后,你再去寻找你的 thread id,也先是 z 再去 y 最后 x

找到之后怎么做呢?你的 index 它等于 bIndex * bSize + tIndex,而 bSize 也就是每个 block 的大小,它就等于 blockDim.z * blockDim.y * blockDim.x,,这样子就能够把你的 thread id 在 Grid 的空间下的索引给找出来

我们执行一下输出如下:

在这里插入图片描述

那这个小节的案例我们基本上看完了,这里注意一下有一个 cudaDeviceSynchronizesynchronize 是同步的意思,在 CUDA 编程中有几种同步的方式:

  • cudaDeviceSynchronize:CPU 与 GPU 端完成同步,CPU 不执行之后的语句,直到这个语句以前的所有 cuda 操作结束
  • cudaStreamSynchronize:跟 cudaDeviceSynchronize 很像,但是这个是针对某一个 stream 的。只同步指定的 stream 中的 cpu/gpu 操作,其他的不管
  • cudaThreadSynchronize:现在已经不被推荐使用的方法
  • __syncthreads:线程块内同步

如果我们把 cudaDeviceSynchronize 注释掉再去执行会发现终端上没有任何打印,这是因为我们核函数的执行它其实是异步的,我们 CPU 不会去等待它执行完成,所以我们如果要得到对应的结果需要做同步处理,确保 CPU 在执行下面的指令时当前核函数已经执行完成

我们最后看一个核函数,print_cord_kernel

__global__ void print_cord_kernel(){
    int index = threadIdx.z * blockDim.x * blockDim.y + \
              threadIdx.y * blockDim.x + \
              threadIdx.x;

    int x  = blockIdx.x * blockDim.x + threadIdx.x;
    int y  = blockIdx.y * blockDim.y + threadIdx.y;

    printf("block idx: (%3d, %3d, %3d), thread idx: %3d, cord: (%3d, %3d)\n",
         blockIdx.z, blockIdx.y, blockIdx.x,
         index, x, y);
}

我们之前在一个 grid 中找 thread id 时写得比较冗长,我们既需要计算 block 索引又需要计算 thread 索引,之后还需要拼在一起,最后才能得到整体上的一个全局索引,这个还是比较麻烦的,我们一般很少会这么用

我们一般怎么做呢?一般按照上面的方法计算 blockIdx.x * blockDim.x + threadIdx.x,同理 y 方向也是,这样就能找到对应点的 (x,y) 的坐标

我们执行一下输出如下:

在这里插入图片描述

这样它就能把你 4x4 大小的一个图像中的每一个索引打印出来,一般来说这个是我们比较常用的方式

第一个案例中还有一些关于二维的其它例子,大家可以自己尝试下

4. nvcc编译器

上面我们对第一个 CUDA 案例的内容进行了简单分析,下面我们来看下这个 CUDA 程序是怎么完成编译执行的呢?其中的 Makefile 文件里面又包含了哪些内容?下面我们一起来看下

我们都知道 C++ 程序的编译器是 g++,那么 CUDA 程序的编译器是什么呢?是 nvcc,它是 g++ 的一个扩展版,但是如果你用默认的 g++ 去编译 CUDA 程序时是会出错的,这是因为 CUDA 程序里面有很多标识符比如 __global__dim3 等等 g++ 是无法识别的,因此编译 CUDA 程序我们需要用它对应的编译器 nvcc

下面我们对比了 g++ --help 和 nvcc --help,来看下两个编译器有什么不同:

在这里插入图片描述

g++nvcc 是两种不同的编译器,分别用于编译 C++ 程序和 CUDA C/C++ 程序。虽然两者都支持命令行选项来控制编译过程,它们的功能和用途有着显著的差异。以下是对它们输出内容的一般性对比,以及各自的特点介绍。(from ChatGPT)

g++ (–help 输出内容)

g++是 GNU Compiler Collection (GCC) 的一部分,专门用于编译 C++ 程序。当你使用 g++ --help 命令时,它会输出一系列的选项和用法说明,这些内容主要涵盖:

  • 编译选项:控制编译阶段的行为,如优化级别(-O1, -O2, -O3, -Os等)、调试信息生成(-g)等。
  • 链接选项:指定链接行为,如静态链接(-static)、链接库搜索路径(-L)和链接特定库(-l)。
  • 预处理选项:控制预处理器行为的选项,如定义宏(-D)、包含路径(-I)等。
  • 警告控制:开启或禁用特定类型的警告(如-Wall, -Wextra, -Werror等)。
  • 语言选项:指定使用的 C++ 标准版本(如-std=c++11, -std=c++14, -std=c++17等)。
  • 其他杂项:包括文件输出控制、调试和优化辅助工具等。

nvcc (–help 输出内容)

nvcc 是 NVIDIA CUDA Compiler,用于编译 CUDA C/C++ 程序。nvcc 的命令行界面设计用于接受类似于传统 UNIX 编译器的选项,同时提供了许多专门针对 CUDA 编程的选项。使用 nvcc --help 时,你会看到:

  • CUDA 相关编译选项:用于控制 GPU 代码生成的选项,如指定 GPU 架构(-arch)、代码生成目标(-code)等。
  • 包装选项nvcc实际上是一个包装器编译器,它可以调用gcc/g++来处理 C/C++ 源文件。因此,它允许指定传递给这些编译器的选项(如-Xcompiler)。
  • 优化和调试:提供了用于优化 CUDA 代码和生成调试信息的选项(如-O2, -G, -lineinfo等)。
  • 库和链接选项:与g++类似,nvcc也支持指定链接选项,如链接 CUDA 运行时库等。
  • 预处理和宏定义:支持预处理器选项,允许定义宏、包含路径等。
  • 警告和错误处理:控制编译时警告和错误显示的选项。

异同点总结

  • 相同点:两者都支持预处理、编译、链接等编译阶段的控制,包括优化级别、调试信息生成、警告控制等。
  • 不同点nvcc 专注于 CUDA C/C++ 程序的编译,提供了许多专门的 CUDA 相关选项,如 GPU 架构选择、CUDA 代码生成等。而 g++ 主要用于普通 C++ 程序的编译,支持各种 C++ 标准。
  • 包装器角色nvcc 具有包装器的角色,能够调用 g++gcc 来编译 C/C++ 代码部分,并将这部分与 CUDA 代码整合在一起。

nvcc 编译时常见的编译选项有:(from ChatGPT)

指定GPU架构和代码生成

  • -arch=sm_xx:指定目标设备的计算能力。xx 是一个数字,比如 50 表示计算能力 5.0。这个选项让 nvcc 知道生成的代码应该针对哪种类型的 GPU。
  • -code=sm_xx,compute_xx:指定具体的 GPU 代码(sm_xx)和虚拟架构代码(compute_xx)。这允许在不同的 GPU 上有更好的兼容性和性能。

优化和调试

  • -O0, -O1, -O2, -O3:设置优化级别。-O0 表示无优化(方便调试),-O3 表示最高优化级别。
  • -G:启用调试模式,生成调试信息。
  • -lineinfo:生成行信息,有助于性能调优和调试,而不需要像 -G 那样降低优化级别。

预处理器选项

  • -Dname:定义宏 name
  • -I:添加头文件搜索路径。

包装器选项

  • -Xcompiler:后面紧跟要传递给底层 C/C++ 编译器的选项,如 -Xcompiler -fPIC

其他常用选项

  • -c:编译源文件但不链接。生成目标文件(.o文件)。
  • -o:指定输出文件的名称。
  • -v:在编译时显示详细信息,帮助诊断问题。
  • --use_fast_math:使用快速数学库。这可以提高性能,但可能牺牲一些精度。
  • --ptxas-options=-v:显示 PTX/SASS 汇编优化信息,有助于深入了解代码是如何被优化的。

这些选项可以组合使用,以满足特定的编译需求和性能优化目标。

5. Makefile部分

OK,我们下面来看第一个案例的 Makefile 文件,来看它是如何通过 nvcc 编译我们的 CUDA 程序的,Makefile 完整内容如下:

CONFIG        :=  ../../config/Makefile.config
CONFIG_LOCAL  :=  ./config/Makefile.config

include $(CONFIG)
include $(CONFIG_LOCAL)

BUILD_PATH    :=  build
SRC_PATH      :=  src
CUDA_DIR      :=  /usr/local/cuda-$(CUDA_VER)

KERNELS_SRC   :=  $(wildcard $(SRC_PATH)/*.cu)

APP_OBJS      +=  $(patsubst $(SRC_PATH)%, $(BUILD_PATH)%, $(KERNELS_SRC:.cu=.cu.o))  

APP_DEPS      +=  $(KERNELS_SRC)

CUCC          :=  $(CUDA_DIR)/bin/nvcc

CUDAFLAGS     :=  -Xcompiler -fPIC 

INCS          :=  -I $(CUDA_DIR)/include \
                  -I $(SRC_PATH) 

LIBS          :=  -L "$(CUDA_DIR)/lib64" \

ifeq ($(DEBUG),1)
CUDAFLAGS     +=  -g -G -O0
else
CUDAFLAGS     +=  -O3
endif

ifeq ($(SHOW_WARNING),1)
CUDAFLAGS     +=  -Wall -Wunused-function -Wunused-variable -Wfatal-errors
else
CUDAFLAGS     +=  -w
endif

all:
	$(MAKE) $(APP)

update: $(APP)
	@echo finished updating $<

$(APP): $(APP_DEPS) $(APP_OBJS)
	@$(CUCC) $(APP_OBJS) -o $@ $(LIBS) $(INCS)
	@echo finished building $@. Have fun!!

show: 
	@echo $(BUILD_PATH)
	@echo $(APP_DEPS)
	@echo $(INCS)
	@echo $(APP_OBJS)

clean:
	rm -rf $(APP)
	rm -rf build

# Compile CUDA
$(BUILD_PATH)/%.cu.o: $(SRC_PATH)/%.cu
	@echo Compile CUDA $@
	@mkdir -p $(BUILD_PATH)
	@$(CUCC) -o $@ -c $< $(CUDAFLAGS) $(INCS)

.PHONY: all update show clean 

这个 Makefile 文件是用来编译 CUDA 程序的构建脚本,涉及多个关键部分,下面逐一解析它们的作用和含义:(from ChatGPT)

配置文件引入

CONFIG        :=  ../../config/Makefile.config
CONFIG_LOCAL  :=  ./config/Makefile.config

include $(CONFIG)
include $(CONFIG_LOCAL)

这部分指定了两个配置文件的路径,通过 include 指令,Makefile 将读取并执行这些配置文件中定义的指令,允许你在这些配置文件中设置一些变量或者编译标志等。

基本路径设置

BUILD_PATH    :=  build
SRC_PATH      :=  src
CUDA_DIR      :=  /usr/local/cuda-$(CUDA_VER)

这里定义了构建目录(build)、源代码目录(src)和 CUDA 安装路径。

源文件和目标文件

KERNELS_SRC   :=  $(wildcard $(SRC_PATH)/*.cu)

APP_OBJS      +=  $(patsubst $(SRC_PATH)%, $(BUILD_PATH)%, $(KERNELS_SRC:.cu=.cu.o))
  • KERNELS_SRC 使用 wildcard 函数列出了所有在源代码目录下扩展名为 .cu(CUDA 源文件)的文件。
  • APP_OBJS 通过 patsubst 函数将 KERNELS_SRC 中的源文件路径从 src 目录转换到 build 目录,并将扩展名从 .cu 改为 .cu.o(即,目标文件)。

编译器和编译标志

CUCC          :=  $(CUDA_DIR)/bin/nvcc

CUDAFLAGS     :=  -Xcompiler -fPIC
  • CUCC 变量指向 CUDA 编译器 nvcc 的路径。
  • CUDAFLAGS 定义了传递给 CUDA 编译器的标志。在这里 -Xcompiler 标志用于将 -fPIC 选项传递给底层的 C/C++ 编译器,以生成位置无关代码(PIC)。

包含目录和库目录

INCS          :=  -I $(CUDA_DIR)/include \
                  -I $(SRC_PATH) 

LIBS          :=  -L "$(CUDA_DIR)/lib64" \
  • INCS 定义了编译器查找头文件的目录。
  • LIBS 指定了链接器查找库文件的目录。

调试和警告选项

ifeq ($(DEBUG),1)
CUDAFLAGS     +=  -g -G -O0
else
CUDAFLAGS     +=  -O3
endif

ifeq ($(SHOW_WARNING),1)
CUDAFLAGS     +=  -Wall -Wunused-function -Wunused-variable -Wfatal-errors
else
CUDAFLAGS     +=  -w
endif

这部分根据 DEBUGSHOW_WARNING 变量的值来调整编译标志,以支持调试信息的生成和警告信息的显示。

目标定义

  • all 是默认目标,它依赖于变量 $(APP)(这里没有显示定义,可能在包含的配置文件中定义)。
  • update$(APP) 目标用于编译和链接应用程序。
  • show 目标用于打印出一些变量的值,以便于调试 Makefile。
  • clean 目标用于清理构建产物。

编译规则

$(BUILD_PATH)/%.cu.o: $(SRC_PATH)/%.cu
	@echo Compile CUDA $@
	@mkdir -p $(BUILD_PATH)
	@$(CUCC) -o $@ -c $< $(CUDAFLAGS) $(INCS)

这个规则用于编译 .cu 文件:它会为每个源文件创建对应的 .cu.o 对象文件。这包括创建构建目录,然后使用 nvcc 编译器和前面定义的编译选项来编译源文件。

特殊目标

.PHONY: all update show clean
  • 声明 allupdateshowclean 为伪目标,这意味着它们不对应文件名,即使存在同名文件,也总是执行相关的命令。

Makefile 的分析到这里就结束了,这个还是需要大家对 Makefile 有一些基本的了解

博主之前有一些关于 Makefile 的笔记,大家感兴趣的可以看看:Makefile

6. 执行我们的第二个CUDA程序

我们来看第二个案例 2.2-cpp_cuda_interactive,包含的文件内容如下:

在这里插入图片描述

相比于第一个案例,这个多了一个 main.cpp 源文件,print_index.hpp 和 utils.hpp 两个头文件,我们希望 main.cpp 作为接口,通过 g++ 去编译调用 print_index.cu 中的核函数

main.cpp 中的内容如下:

#include <stdio.h>
#include <cuda_runtime.h>
#include "print_index.hpp"

void print_one_dim(int inputSize, int blockSize){
    int gridSize = inputSize / blockSize;

    dim3 block(blockSize);
    dim3 grid(gridSize);

    print_idx_device(block, grid);
    // print_dim_device(block, grid);
    // print_thread_idx_per_block_device(block, grid);
    // print_thread_idx_device(block, grid);
}

void print_two_dim(int inputSize, int blockSize){
    int gridSize = inputSize / blockSize;

    dim3 block(blockSize, blockSize);
    dim3 grid(gridSize, gridSize);

    // print_idx_device(block, grid);
    // print_dim_device(block, grid);
    // print_thread_idx_per_block_device(block, grid);
    print_thread_idx_device(block, grid);
}


int main(){
    int inputSize;
    int blockSize;

    /* one-dimention test */
    inputSize = 32;
    blockSize = 4;
    print_one_dim(inputSize, blockSize);
        
    /* two-dimention test */
    // inputSize = 8;
    // blockSize = 4;
    // print_two_dim(inputSize, blockSize);
    return 0;
}

我们 main.cpp 中没有核函数的具体实现,只有几个接口函数,通过这几个接口函数调用我们的 CUDA 核函数完成相应的计算,这个其实是 CUDA 编程中非常常见的一种方式,通过 host 函数去调用 device 函数

值得注意的是我们对于所有的 CUDA API 的调用都套上了一个 CUDA_CHECK 函数,它的定义如下:

#ifndef __UTILS_HPP__
#define __UTILS_HPP__

#include <cuda_runtime.h>
#include <system_error>

// 一般cuda的check都是这样写成宏
#define CUDA_CHECK(call) {                                                 \
    cudaError_t error = call;                                              \
    if (error != cudaSuccess) {                                            \
        printf("ERROR: %s:%d, ", __FILE__, __LINE__);                      \
        printf("CODE:%d, DETAIL:%s\n", error, cudaGetErrorString(error));  \
        exit(1);                                                           \
    }                                                                      \
}

#endif //__UTILS__HPP__

这个代码段定义了一个非常有用的宏 CUDA_CHECK,其主要用途是简化 CUDA API 调用的错误处理。CUDA 编程中,几乎所有的 CUDA API 函数调用都会返回一个 cudaError_t 类型的状态码,表示调用是否成功。正确的错误处理对于调试和确保 CUDA 应用程序稳定运行至关重要。

这个是 CUDA 编程中很常见的一种错误处理的方式,我们后续会详细介绍,这里先简单过一下。

7. Makefile添加的部分

第二个案例完整的 Makefile 内容如下:

下面依旧是一个 Makefile 文件,请你帮我找出其中与上一个 Makefile 文件添加的部分,并进行详细分析
CONFIG        :=  ../../config/Makefile.config
CONFIG_LOCAL  :=  ./config/Makefile.config

include $(CONFIG)
include $(CONFIG_LOCAL)

BUILD_PATH    :=  build
SRC_PATH      :=  src
CUDA_DIR      :=  /usr/local/cuda-$(CUDA_VER)

CXX_SRC       +=  $(wildcard $(SRC_PATH)/*.cpp)
KERNELS_SRC   :=  $(wildcard $(SRC_PATH)/*.cu)

APP_OBJS      :=  $(patsubst $(SRC_PATH)%, $(BUILD_PATH)%, $(CXX_SRC:.cpp=.cpp.o))
APP_OBJS      +=  $(patsubst $(SRC_PATH)%, $(BUILD_PATH)%, $(KERNELS_SRC:.cu=.cu.o))  

APP_DEPS      :=  $(CXX_SRC)
APP_DEPS      +=  $(KERNELS_SRC)
APP_DEPS      +=  $(wildcard $(SRC_PATH)/*.hpp)


CUCC          :=  $(CUDA_DIR)/bin/nvcc
CXXFLAGS      :=  -std=c++11 -fPIC
CUDAFLAGS     :=  -Xcompiler -fPIC 

INCS          :=  -I $(CUDA_DIR)/include \
                  -I $(SRC_PATH) 

LIBS          :=  -L "$(CUDA_DIR)/lib64" \

ifeq ($(DEBUG),1)
CUDAFLAGS     +=  -g -O0 -G
CXXFLAGS      +=  -g -O0
else
CUDAFLAGS     +=  -O3
CXXFLAGS      +=  -O3
endif

ifeq ($(SHOW_WARNING),1)
CUDAFLAGS     +=  -Wall -Wunused-function -Wunused-variable -Wfatal-errors
CXXFLAGS      +=  -Wall -Wunused-function -Wunused-variable -Wfatal-errors
else
CUDAFLAGS     +=  -w
CXXFLAGS      +=  -w
endif

all:
	$(MAKE) $(APP)

update: $(APP)
	@echo finished updating $<

$(APP): $(APP_DEPS) $(APP_OBJS)
	@$(CUCC) $(APP_OBJS) -o $@ $(LIBS) $(INCS)
	@echo finished building $@. Have fun!!

show: 
	@echo $(BUILD_PATH)
	@echo $(APP_DEPS)
	@echo $(INCS)
	@echo $(APP_OBJS)
	@echo $(APP_MKS)

clean:
	rm -rf $(APP)
	rm -rf build

ifneq ($(MAKECMDGOALS), clean)
-include $(APP_MKS)
endif

# Compile CXX
$(BUILD_PATH)/%.cpp.o: $(SRC_PATH)/%.cpp
	@echo Compile CXX $@
	@mkdir -p $(BUILD_PATH)
	@$(CC) -o $@ -c $< $(CXXFLAGS) $(INCS)

# Compile CUDA
$(BUILD_PATH)/%.cu.o: $(SRC_PATH)/%.cu
	@echo Compile CUDA $@
	@mkdir -p $(BUILD_PATH)
	@$(CUCC) $(ARCH) -o $@ -c $< $(CUDAFLAGS) $(INCS)

.PHONY: all update show clean 

我们来看下有了 C++ 之后的 CUDA 程序的 Makefile 文件需要添加哪些东西:(from ChatGPT)

C++ 源文件的处理

CXX_SRC       +=  $(wildcard $(SRC_PATH)/*.cpp)
APP_OBJS      :=  $(patsubst $(SRC_PATH)%, $(BUILD_PATH)%, $(CXX_SRC:.cpp=.cpp.o))
APP_DEPS      :=  $(CXX_SRC)
  • 新增对 .cpp 文件的处理,包括查找源码、转换为对象文件的路径,并添加为依赖。

C++ 编译选项

CXXFLAGS      :=  -std=c++11 -fPIC
  • 定义了 C++ 的编译选项,包括使用 C++11 标准和生成位置无关代码。

编译 C++ 源文件的规则

$(BUILD_PATH)/%.cpp.o: $(SRC_PATH)/%.cpp
	@echo Compile CXX $@
	@mkdir -p $(BUILD_PATH)
	@$(CC) -o $@ -c $< $(CXXFLAGS) $(INCS)
  • 新增了一个规则来编译 .cpp 文件为 .cpp.o 对象文件,使用的是 $(CC) 编译器(通常指向 gccg++),这使得 Makefile 能够处理 C++ 源文件。

相比于第一个案例的 Makefile,这个版本通过引入对 C++ 源文件的支持以及条件编译选项的扩展,大大增强了其灵活性和通用性。这些改进使得 Makefile 能够更好地适应复杂的项目结构,处理不同类型的源文件,并提供更精细的构建控制

OK,以上就是第一小节有关 CUDA 中的线程和线程束的全部内容了

总结

本次课程我们学习了 CUDA 编程中的一些基础概念,首先通过执行我们的第一个 CUDA 程序案例我们了解了 CUDA 中的 Grid 和 Block 的概念,它们是为了我们能够快速索引而产生的逻辑意义上的东西,物理意义上并不存在。之后我们通过分析第一个案例了解了各自情况下的 idx 索引的计算,然后我们简单的聊了一下 nvcc 以及 Makefile,并通过第二个案例了解了同时包含 C++ 和 CUDA 程序的 Makefile 该如何编写。总之,这里只聊了 CUDA 编程中一些基本的东西,更多的知识比如 warp 线程束还有 Makefile 的规则等等内容可能需要大家额外补充了。

OK,以上就是第 1 小节有关 CUDA 中的线程与线程束的全部内容了,下节我们来学习使用 CUDA 来进行矩阵乘法的加速,敬请期待😄

参考

  • CUDA编程之基础,内存模型和线程束

  • CUDA编程入门极简教程

  • https://github.com/kalfazed/tensorrt_starter

  • Thread and block heuristrics in CUDA programming

  • Makefile

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/464259.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[C语言]——VS实用调用技巧

一.什么是bug bug本意是“昆⾍”或“⾍⼦”&#xff0c;现在⼀般是指在电脑系统或程序中&#xff0c;隐藏着的⼀些未被发现的缺陷或问题&#xff0c;简称程序漏洞。 “Bug” 的创始⼈格蕾丝赫柏&#xff08;Grace Murray Hopper&#xff09;&#xff0c;她是⼀位为美国海军⼯…

MQ组件之RabbitMQ学习

MQ组件之RabbitMQ入门 同步调用和异步调用 在微服务架构中&#xff0c;服务之间的调用有同步调用和异步调用两种方式。 我们使用OpenFeign去调用是同步调用&#xff0c;同步调用的缺点很明显&#xff0c;在下图的场景中&#xff0c;支付完成后需要调用订单服务、仓库服务、短…

MyBatisPlus 之二:SpringBoot 快速整合 MyBatisPlus 详细步骤

SpringBootMyBatisPlus Spring Boot 结合 MyBatis Plus 是一种常见的 Java 后端开发框架组合&#xff0c;能够快速构建高性能、易于维护的 CRUD 应用程序。以下是 Spring Boot 集成 MyBatis Plus 的基本步骤 一、快速体验 注意&#xff1a;下面版本 idea2020 SpringBoot2.* …

node.js快速入门-day03

个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大二在校生 &#x1f921; 个人主页&#xff1a;坠入暮云间x &#x1f43c;座右铭&#xff1a;给自己一个梦想&#xff0c;给世界一个惊喜。 &#x1f385;**学习目标: 坚持每一次的学习打卡 文章目录 web服务器创建…

Rocket MQ 从入门到实践

为什么要使用消息队列&#xff0c;解决什么问题&#xff1f;&#xff08;消峰、解藕、异步&#xff09; 消峰填谷 客户端》 网关 〉 消息队列》秒杀服务 异步解耦 消息队列中的重要概念理解。&#xff08;主题、消费组、队列&#xff0c;游标&#xff1f;&#xff09; 主题&…

phpstudy搭建简单渗透测试环境upload-labs、DVWA、sqli-labs靶场

好久没有做渗透相关的试验了&#xff0c;今天打开phpstudy发现很多问题&#xff0c;好多环境都用不了&#xff0c;那就卸载重装吧&#xff0c;顺便记录一下。 小皮下载地址&#xff1a; https://www.xp.cn/download.html 下载安装完成 一、下载搭建upload-labs环境 github…

LeetCode每日一题[C++]-310.最小高度树

题目描述 树是一个无向图&#xff0c;其中任何两个顶点只通过一条路径连接。 换句话说&#xff0c;一个任何没有简单环路的连通图都是一棵树。 给你一棵包含 n 个节点的树&#xff0c;标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表&#xff08;每一个…

数字多空策略(实盘+回测+数据)

数量技术宅团队在CSDN学院推出了量化投资系列课程 欢迎有兴趣系统学习量化投资的同学&#xff0c;点击下方链接报名&#xff1a; 量化投资速成营&#xff08;入门课程&#xff09; Python股票量化投资 Python期货量化投资 Python数字货币量化投资 C语言CTP期货交易系统开…

【深度学习模型移植】用torch普通算子组合替代torch.einsum方法

首先不得不佩服大模型的强大之处&#xff0c;在算法移植过程中遇到einsum算子在ONNX中不支持&#xff0c;因此需要使用普通算子替代。参考TensorRT - 使用torch普通算子组合替代torch.einsum爱因斯坦求和约定算子的一般性方法。可以写出简单的替换方法&#xff0c;但是该方法会…

【C#】【SAP2000】SAP2000中批量修改指定荷载工况下所有Frame对象的温度荷载

if (build true){// 连接到正在运行的 SAP2000cOAPI mySapObject (cOAPI) System.Runtime.InteropServices.Marshal.GetActiveObject("CSI.SAP2000.API.SapObject");cSapModel mySapModel mySapObject.SapModel;// 获取所有框架单元的总数int numberFrames 0;str…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Search)

搜索框组件&#xff0c;适用于浏览器的搜索内容输入框等应用场景。 说明&#xff1a; 该组件从API Version 8开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 子组件 无 接口 Search(options?: { value?: string, placeholder?: Reso…

[论文精读]Dynamic Coarse-to-Fine Learning for Oriented Tiny Object Detection

论文网址&#xff1a;[2304.08876] 用于定向微小目标检测的动态粗到细学习 (arxiv.org) 论文代码&#xff1a;https://github.com/ChaselTsui/mmrotate-dcfl 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&…

网站安全监测:守护网络空间的坚实防线

随着互联网技术的飞速发展和广泛应用&#xff0c;网站已成为企业、机构和个人展示形象、提供服务、传递信息的重要平台。然而&#xff0c;与此同时&#xff0c;网站也面临着日益严重的安全威胁。黑客攻击、数据泄露、恶意软件等安全问题频发&#xff0c;给网站运营者带来了巨大…

FFplay使用滤镜添加字幕到现有视频显示

1.创建字幕文件4k.srt 4k.srt内容: 1 00:00:01.000 --> 00:00:30.000 日照香炉生紫烟2 00:00:31.000 --> 00:00:60.000 遥看瀑布挂前川3 00:01:01.000 --> 00:01:30.000 飞流直下三千尺4 00:01:31.000 --> 00:02:00.000 疑是银河落九天2.通过使用滤镜显示字幕在视…

ping和telnet的区别

ping是ICMP协议&#xff0c;只包含控制信息没有端口&#xff0c;用于测试两个网络主机之间网络是否畅通 telnet是TCP协议&#xff0c;用于查看目标主机某个端口是否开发。 总结&#xff1a;ping是物理计算机间的网络互通检查&#xff0c;telnet是应用服务间的访问连通检查&am…

GPU密集型计算性能优化的方法和技术

对GPU密集型计算进行性能优化的方法和技术多种多样。通过一些优化策略和技术需要综合考虑应用程序的具体需求、所使用的GPU硬件、以及编程模型和库的选择。通过不断地分析和调整&#xff0c;可以实现GPU计算性能的持续提升。以下是一些常用的优化策略和技术&#xff1a; 算法优…

Oracle 部署及基础使用

1. Oracle 简介 Oracle Database&#xff0c;又名 Oracle RDBMS&#xff0c;简称 Oracle Oracle系统&#xff0c;即是以Oracle关系数据库为数据存储和管理作为构架基础&#xff0c;构建出的数据库管理系统。是目前最流行的客户/服务器&#xff08;client/server&#xff09;或…

监视和内存观察

监视和内存观察 5.监视和内存观察5.1 监视5.2 内存 5.监视和内存观察 在调试的过程中我们&#xff0c;如果要观察代码执行过程中&#xff0c;上下文环境中的变量的值&#xff0c;有哪些方法呢&#xff1f; 这些观察的前提条件一定是开始调试后观察&#xff0c;比如&#xff1…

金枪鱼群优化算法TSO优化BiLSTM-ATTENTION实现风力发电功率预测(matlab)

金枪鱼群优化算法TSO优化BiLSTM-ATTENTION实现风力发电功率预测&#xff08;matlab&#xff09; TSO-BiLSTM-Attention金枪鱼群算法优化长短期记忆神经网络结合注意力机制的数据回归预测 Matlab语言。 金枪鱼群优化算法&#xff08;Tuna Swarm Optimization&#xff0c;TSO)是一…

upload-labs第一关

上一篇文章中搭建好了upload-labs环境&#xff0c;接下来进行第一关的尝试&#xff0c;我也是第一次玩这个挺有意思。 1、第一关的界面是这样的先不看其他的源码&#xff0c;手动尝试下试试。 2、写一个简单的php一句话木马 3、直接上传&#xff0c;提示必须要照片格式的文…