CUDA学习笔记一
参考
暂还只是初步的学习笔记,未成体系(可能有错漏)
CUDA概念
CUDA, Compute Unified Device Architecture 即统一计算设备架构
CUDA is a parallel computing platform and programming model created by NVIDIA.
GPU和CPU之间的主要区别在于设计思想的不同。CPU的设计初衷是为了实现在执行一系列操作时达到尽可能高的性能,其中每个操作称之为一个thread,同时可能只能实现其中数十个线程的并行化,GPU的设计初衷是为了实现在在并行执行数千个线程时达到尽可能高的性能(通过分摊较慢的单线程程序以实现更高的吞吐量)。
为了能够实现更高强度的并行计算,GPU将更多的晶体管用于数据计算而不是数据缓存或控制流 。 下图显示了 CPU 与 GPU 的芯片资源分布示例。
CUDA架构
在硬件层次上:
Graphics Processing Unit(GPU):图形处理单元。每个GPU包含多个GPC
Graphics Processing Cluster(GPC):图形处理集群。每个GPC包含多个TPC
Texture Processing Cluster(TPC):纹理处理集群。每个TPC包含两个SM
Streaming Multiprocessor(SM):流式多处理器。每个SM包含多个SMPB
SM Processing Block(SMPB or block):每个SM包含多个线程束
Thread Warp(warp):线程束。Wrap是GPU的基本执行单元,每个warp的32线程执行同一指令
Streaming Processors(SPs or CUDA Cores):流处理器或CUDA核心。执行浮点和整数运算、thread运行的基本单位
GPU上运行函数kernel对应一个Grid,每个Grid内有多个Block,每个Block由多个Thread组成
- 一个图形处理单元(Graphics Processing Unit)包含若干流式多核处理器(Streaming Mlultiprocessor,SM),GPU的内存是全局内存(global memory),可被该GPU上的所有线程访问
- 一个流式多核处理器(Streaming Mlultiprocessor,SM)包含多个线程处理器(Scalar Processors,SP),SM的内存是共享内存(shared memory),可被block内的所有线程可以访问
- 线程处理器(Scalar Processors,SP),最基本的计算单元,有自己的局部内存(local memory)和寄存器,只能被自己访问
抽象概念与具体硬件对应关系
为了更好的管理和执行thread,提出了线程束wrap:
- warp是硬件层面中SM对应执行线程的单位
- 线程束Wrap是GPU的基本执行单元,目前CUDA的warp的大小为32
- 一个指令会在wrap中的32个thread中并行执行,在划分blocksize的时候,一般都会设置成32的倍数
CUDA编程
CUDA编程并行计算整体流程
- 在GPU上分配显存 ,将CPU上的数据拷贝到显存上
- 利用核函数 完成GPU显存中数据的计算
- 将显存中的计算结果拷贝回CPU内存中
内核函数
在CUDA中,我们可以通过创建一个内核函数来实现并行化,所谓的内核函数,就是一个只能在GPU上执行而不能直接在CPU上执行的函数,通过__global__
标识
cpp
__global__ void function(parm1,parm2,...)
{
//code
}
global
这个关键字用来定义可以在主机端(CPU)调用的函数,但是这些函数运行在设备端(GPU)。__global__
函数只能被其他 __global__
或 __device__
函数调用,并且它们必须位于 .cu
文件的外部。__global__
函数的主要用途是执行并行计算任务
- 运行在设备端(GPU)上,但可以从主机端调用
- 通常用于编写并行计算的核心部分
- 只能由主机代码或另一个
__global__
函数调用
示例
c[[
__global__ void add(int *a, int *b, int *c) {
int index = threadIdx.x + blockIdx.x * blockDim.x;
c[index] = a[index] + b[index];
}
device
这个关键字用来定义设备端函数。这些函数只能由其他 __device__
或 __global__
函数调用,并且它们驻留在设备内存中。__device__
函数可以访问全局内存、常量内存、共享内存等
- 运行在设备端(GPU)上,不能从主机端直接调用
- 通常用于实现辅助功能,如数学运算、数据处理等
- 可以由
__global__
或__device__
函数调用
示例
cpp
__device__ int square(int x) {
return x * x;
}
__global__ void multiply(int *a, int *b, int *c) {
int index = threadIdx.x + blockIdx.x * blockDim.x;
// blockIdx.x:表示当前线程块在其一维网格中的位置(索引),从 0 开始
// blockDim.x:表示每个线程块中线程的数量(在一维的情况下)
// threadIdx.x:表示当前线程在其线程块中的位置(索引),从 0 开始
// blockIdx.x * blockDim.x 计算了当前线程块相对于整个数组的起始位置
//+ threadIdx.x 则是在当前线程块内的相对位置
c[index] = square(a[index]) * b[index];
}
host
__host__
是一个存储类修饰符,它用来声明可以在主机端(CPU)上执行的函数或变量。host 修饰符通常与 __device__
或 __global__
一起使用,以便声明可以在 CPU 和 GPU 上都执行的函数。
- 如果函数只需要在主机端执行,可以只使用
__host__
修饰符 - 如果函数需要在主机和设备端都能执行,则应使用
__host__ __device__
- 如果函数只在设备端执行,则使用
__device__
__host__
函数不能包含任何设备特定的指令或 API 调用,因为它们也必须能够在主机端执行
示例
cpp
// 定义一个可以在主机端和设备端都使用的函数
__host__ __device__ float square(float x) {
return x * x;
}
__global__ void addKernel(float *d_a, float *d_b, float *d_c, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
d_c[idx] = square(d_a[idx]) + square(d_b[idx]); // 可以被GPU或CPU执行
}
}
CUDA项目
在已安装VS和CUDA的前提下,打开VS,创建一个CUDA XX.X Runtime
项目
创建后默认有一个 kernel.cu
,是一个一维矩阵相加的示例代码(而且有详尽的注释),可以运行测试一下CUDA环境有无问题
运行结果如下
CUDA进阶
共享内存加速访存
一般将数据copy到GPU,默认使用全局内存,其读写速度特别慢,如果将数据放到线程块中读写更快的共享内存中,可以提升速度。
内存层次如下
- CPU可以读写GPU设备中的Global Memory、Constant Memory以及Texture Memory内存储的内容;主机代码可以把数据传输到设备上,也可以从设备中读取数据;
- GPU中的线程使用Register、Shared Memory、Local Memory、Global Memory、Constant Memory以及Texture Memory;不同Memory的作用范围是不同的,和线程、block以及grid有关;
- 线程可以读写Register、Shared Memory、Local Memory和Global Memory;但是只能读Constant Memory和Texture Memory;
CUDA内存读写速度比较
- 线程寄存器(~1周期)
- Block共享内存(~5周期)
- Grid全局内存(~500周期)
- Grid常量内存(~5周期)
但由于共享内存的大小有限,大概只有几十K,所以只能多次拷贝
分配共享内存的方式分为静态分配和动态分配
- 静态分配
cpp
__global__ void staticFun(int* d, int n)
{
__shared__ int s[64]; //在共享内存中进行静态分配
int t = treadIdx.x;
s[t] = d[t]; //将全局内存数据拷贝到申请的共享内存中,之后利用共享内存中的数据参与运算将会比调
//用全局内存数据参与运算快(由于共享内存有限,不能全部拷贝到共享内存,这其中就涉及到分批拷贝问题了)
__syncthreads();//需要等所有线程块都拷贝完成后再进行计算,如果不等待所有线程完成写入操作,可能读取到未初始化的数据
}
staticFun<<1,n>>(d, n);
- 动态分配
cpp
__global__ void dynamicFun(int *d, int n)
{
extern __shared__ int s[]; //在共享内存中进行动态分配
int t = threadIdx.x;
s[t] = d[t];
__syncthreads();
}
dynamicFun<<1, n, n*sizeof(int)>>(d, n); //动态申请需要在外部指定共享内存大小
其中
关键字__ shared __
用于申请共享内存(动态申请时要加上 extern
)
函数__syncthreads()
用于块内共享内存同步(块内不同线程之间同步)
利用stream增加文件IO吞吐量
CUDA的stream流,类似我们经常使用CPU时开多线程。
当我们使用GPU进行计算时,GPU会自动创建默认stream来执行核函数,默认流和CPU端的计算是同步的。(也即在CPU执行任务过程中,必须等GPU执行完核函数后,才能继续往下执行)
可以主动开启多个stream,类似CPU开启多线程。可以将大批量文件读写分给多个stream去执行,或者用不同stream分别计算不同的核函数。开启的多个stream之间是异步的,stream与CPU端的计算也是异步的。所以需要注意加上同步操作。
注意,受PCIe总线带宽的限制,当一个流在进行读写操作时,另外一个流不能同时进行读写操作,但是其他流可以进行数值计算任务(类似与CPU中的流水线机制)
调用cuBLAS库API进行矩阵计算
cuBLAS(CUDA Basic Linear Algebra Subroutines)是CUDA的一个高性能线性代数子程序库,用于执行基本的线性代数运算。它提供了类似于传统的BLAS(Basic Linear Algebra Subprograms)库的功能,但针对GPU进行了优化,可以显著提高矩阵运算的性能
cuBLAS的主要功能包括:
- 向量运算:如向量加法、向量点积等。
- 矩阵-向量乘法:计算矩阵与向量的乘积。
- 矩阵-矩阵乘法:计算两个矩阵的乘积。
- 转置:计算矩阵的转置。
- 缩放:按标量缩放向量或矩阵。
- 其他:还包括一些辅助函数,如设置向量元素等。
使用cuBLAS的基本步骤:
- 初始化cuBLAS句柄:创建一个句柄并初始化。
- 设置数据:将数据从主机复制到设备。
- 执行cuBLAS操作:调用相应的cuBLAS函数进行计算。
- 获取结果:将结果从设备复制回主机。
- 清理资源:释放设备上的数据和销毁cuBLAS句柄。
示例代码:
以下是一个简单的示例,演示如何使用cuBLAS执行矩阵-矩阵乘法:
注意链接cuBLAS库,可以使用CUDA安装时的属性配置表,如【Y9000P 2022 GTX3060 CUDA安装记录(六、VS项目配置)】
cpp
#include <cuda_runtime.h>
#include <cublas_v2.h>
#include <iostream>
// 定义矩阵的尺寸
const int M = 2;
const int N = 2;
const int K = 2;
int main() {
// 初始化cuBLAS句柄
cublasHandle_t handle;
cublasCreate(&handle);
// 创建设备上的数据
float* d_A, * d_B, * d_C;
cudaMalloc(&d_A, M * K * sizeof(float));
cudaMalloc(&d_B, K * N * sizeof(float));
cudaMalloc(&d_C, M * N * sizeof(float));
// 填充矩阵A和B
float h_A[M * K] = { 1.0f, 2.0f, 3.0f, 4.0f };
float h_B[K * N] = { 5.0f, 6.0f, 7.0f, 8.0f };
cudaMemcpy(d_A, h_A, M * K * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, K * N * sizeof(float), cudaMemcpyHostToDevice);
// 执行矩阵乘法
float alpha = 1.0f;
float beta = 0.0f;
cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, M, N, K, &alpha, d_A, M, d_B, K, &beta, d_C, M);
// 从设备复制结果到主机
float h_C[M * N];
cudaMemcpy(h_C, d_C, M * N * sizeof(float), cudaMemcpyDeviceToHost);
// 输出结果
for (int i = 0; i < M * N; ++i) {
std::printf("%f ", h_C[i]);
}
// 清理资源
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
cublasDestroy(handle);
return 0;
}
运行结果