TANKENQI.cn

November 8, 2024

并行计算 —— CUDA

BigData10.4 min to read

1 CUDA 简介

提供 CUDA 给出的应用开发库是 API 层的主要任务,大规模的并行计算问 题由它负责解决。CUDA 的核心是 CUDA C 语言,它包含对 C 语言的最小扩展集和一个运行时库,使用这些扩展和运行时库的源文件必须通过 nvcc 编译器进行编译。

2 CUDA 常用术语

3 CUDA 安装

3.1 硬件需求

机器需要具备 NVIDIA 的显卡

3.2 安装 Visual Studio

由于 cuda 开发工具依赖于Microsoft Visual C++ (MSVC) 编译器,而 Visual Studio 已经内置 MSVC,同时,CUDA 开发环境与 Visual Studio 紧密集成,可以让开发者在 Visual Studio 中直接编写、调试和管理 CUDA 项目,当然,Visual Studio 不是必须的,但是必须安装 MSVC。但如果你是新手,建议直接安装 Visual Studio

3.3 安装 CUDA 工具包

下载的 CUDA Toolkit 版本不能高于显卡自身支持的 CUDA 版本。本例中,则不能下载高于 12.6 版本的 CUDA Toolkit 工具包

默认情况下是全部勾选,其中,Visual Studio Integration 是为你的 VS 提供 CUDA 编程相关的模版等集成组件,如果你不想在 VS 中进行编程,可以不勾选,这里我保持勾选

根据自己安装的路径进行修改

Visual Studiobin 路径

CUDA 相关 binlib 路径

3.4 验证是否安装成功

# 先切换到cuda所在盘D:# 进入该路径cd D:\software\cuda\v12.6\extras\demo_suite

4 VS2022 下配置 CUDA 调试环境(方法一)

要求在安装cuda时,勾选了 cudavs 的集成组件

5 VS2022 下配置 CUDA 调试环境(方法二)

右键点击项目属性 → 配置管理器

与自己电脑 CPU 架构一致

右键点击项目属性 → 配置属性 → VC++ 目录 → 包含目录

添加包含目录:$(CUDA_PATH)\include

VC++ 目录 → 库目录

添加库目录:$(CUDA_PATH)\lib\x64

配置属性 → 链接器 → 输入 → 附加依赖项

将该目录下的库文件添加进去

默认路径为: C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.5\lib\x64

本例中路径为:D:\software\cuda\v12.6\lib\x64

cublas.libcublasLt.libcuda.libcudadevrt.libcudart.libcudart_static.libcufft.libcufftw.libcufilt.libcurand.libcusolver.libcusolverMg.libcusparse.libnppc.libnppial.libnppicc.libnppidei.libnppif.libnppig.libnppim.libnppist.libnppisu.libnppitc.libnpps.libnvblas.libnvfatbin.libnvfatbin_static.libnvJitLink.libnvJitLink_static.libnvjpeg.libnvml.libnvptxcompiler_static.libnvrtc-builtins_static.libnvrtc.libnvrtc_static.libOpenCL.lib

6 CUDA 编程测试

可以打开 VS 并创建一个 CUDA Runtime 工程,在 kernel.cu 文件中修改代码

也可以在命令行进行测试,这里我以在 GPU 服务器上为例进行演示

6.1 两个数组相加的简单例子

在服务器上,程序我已经帮大家写好了

/home/temp/user*/目录下

cd /home/temp/user1

serial.cu:串行程序

parallel_*.cu:并行程序

ls

#include <iostream> #include <math.h> #include <ctime> using namespace std;// 数组元素相加的函数 void add(int n, float *x, float *y) {	for (int i = 0; i < n; i++)		y[i] = x[i] + y[i];}int main(void) {	clock_t startTime, endTime;	startTime = clock();//计时开始	int N = 1 << 20; // 1M 元素	float *x = new float[N];	float *y = new float[N];	// 在主机端初始化数组	for (int i = 0; i < N; i++) {		x[i] = 1.0f;		y[i] = 2.0f;	}	// 执行函数 	add(N, x, y);	// 检查误差 (所有的值应该是 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;	endTime = clock(); // 计时结束 	cout << "The run time is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;	// 释放内存 	delete[] x;	delete[] y;	return 0;}
nvcc serial.cu -o serial ./serial

#include "cuda_runtime.h" #include "device_launch_parameters.h" #include <iostream> #include <math.h> #include <ctime> using namespace std;// 两个数组相加的核函数,指示符__global__告诉编译器该函数是运行在 // GPU 上,称为核函数 __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) {	clock_t startTime, endTime;	startTime = clock(); // 计时开始	int N = 1 << 20;	float *x, *y;	// 分配共享内存 – CPU 和 GPU 都可以访问 	cudaMallocManaged(&x, N * sizeof(float));	cudaMallocManaged(&y, N * sizeof(float));	// 在主机端初始化数组	for (int i = 0; i < N; i++) {		x[i] = 1.0f;		y[i] = 2.0f;	}	// 在 GPU 上执行核函数	add <<<1,1>>> (N, x, y);	// 等待 GPU 执行完毕 	cudaDeviceSynchronize();	// 检查误差 (所有的值应该是 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;	// 释放内存 	cudaFree(x);	cudaFree(y);	endTime = clock(); // 计时结束 	cout << "The run time is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;	return 0;}
nvcc parallel_1.cu -o parallel_1./parallel_1

从结果上看,可以发现程序耗时 1.5433s ,超过了单线程程序耗时 0.012454s

这其中大部分时间时间花在了 GPUAPI calls 上面,这是由于给 GPU 分配共享内存会额外消耗时间

我们可以使用 nvprof 工具进行分析

  1. 程序编译后在工程目录下生成可执行程序,如 CudaRuntime1.exe
  2. 进入到 CudaRuntime1.exe 所在目录
  3. 执行 nvprof CudaRuntime1.exe

附:如果报错(由于找不到cupti64_2024.3.1.dll,无法继续执行代码。重新安装程序可能会解决此问题)

则需要将 CUPTI 库所在路径添加到环境变量

默认是:C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6\extras\CUPTI\lib64

nvprof ./parallel_1

我们可以发现 api calls 耗时占据非常多

#include "cuda_runtime.h" #include "device_launch_parameters.h" #include <iostream> #include <math.h> #include <ctime> using namespace std;// 两个数组相加的核函数,指示符__global__告诉编译器该函数是运行在 // GPU 上,称为核函数 __global__ void add(int n, float *x, float *y){int index = threadIdx.x;int stride = blockDim.x;for (int i = index; i < n; i += stride)        y[i] = x[i] + y[i];}int main(void) {        clock_t startTime, endTime;        startTime = clock(); // 计时开始        int N = 1 << 20;        float *x, *y;        // 分配共享内存 – CPU 和 GPU 都可以访问         cudaMallocManaged(&x, N * sizeof(float));        cudaMallocManaged(&y, N * sizeof(float));        // 在主机端初始化数组        for (int i = 0; i < N; i++) {                x[i] = 1.0f;                y[i] = 2.0f;        }        // 在 GPU 上执行核函数        add <<<1,1>>> (N, x, y);        // 等待 GPU 执行完毕         cudaDeviceSynchronize();        // 检查误差 (所有的值应该是 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;        // 释放内存         cudaFree(x);        cudaFree(y);        endTime = clock(); // 计时结束         cout << "The run time is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;        return 0;}
nvcc parallel_2.cu -o parallel_2nvprof ./parallel_2

CUDA 核函数使用三尖括号语法 <<< >>> 指定。第一个参数代表线程块数 (block)、第二个参数代表每个线程块的线程数(thread),线程数最好是 32 的倍数,如 256

int blockSize = 256;int numBlocks = (N + blockSize - 1) / blockSize;add<<<numBlocks, blockSize>>>(N, x, y);

注:也可通过修改参数来控制并行度 , 例如:add<<<numBlocks/2, blockSize>>>(N, x, y)

同时,修改 add 函数(其中,blockIdx.x 表示线程块号)

__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];}
#include "cuda_runtime.h" #include "device_launch_parameters.h" #include <iostream> #include <math.h> #include <ctime> using namespace std;// 两个数组相加的核函数,指示符__global__告诉编译器该函数是运行在 // GPU 上,称为核函数 __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];}int main(void) {        clock_t startTime, endTime;        startTime = clock(); // 计时开始        int N = 1 << 20;        float *x, *y;            int blockSize = 256;        int numBlocks = (N + blockSize - 1) / blockSize;        add<<<numBlocks, blockSize>>>(N, x, y);        // 分配共享内存 – CPU 和 GPU 都可以访问         cudaMallocManaged(&x, N * sizeof(float));        cudaMallocManaged(&y, N * sizeof(float));        // 在主机端初始化数组        for (int i = 0; i < N; i++) {                x[i] = 1.0f;                y[i] = 2.0f;        }        // 在 GPU 上执行核函数        add <<<1,1>>> (N, x, y);        // 等待 GPU 执行完毕         cudaDeviceSynchronize();        // 检查误差 (所有的值应该是 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;        // 释放内存         cudaFree(x);        cudaFree(y);        endTime = clock(); // 计时结束         cout << "The run time is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;        return 0;}
nvcc parallel_3.cu -o parallel_3nvprof ./parallel_3

从结果上看,可以看到总耗时降低到了 0.260529sapi calls 花了 0.063782s

gridDim.x 表示线程格(grid,由多个线程块组成)中线程块的数量

blockIdx.x * blockDim.x + threadIdx.x // 线程号