Bootstrap

Cuda编程1:基础知识

一:核函数

在编写核函数时需要注意越界问题。如下代码,核函数中需要对线程索引进行约束,否则会计算出无用值。

/**
* 本代码为通过cpu和gpu计算两个数组内容相加。
* 数组共有1000个数据,使得每个线程处理一个加法。在分配线程时,每个block有128个线程,一个grid有(1000+127)/128=8个block。
* 8个block对应共有8*128=1024个线程,超出了1000位数据,所有要对核函数中的索引进行约束。
* if (index < N)
*   {
*       z[index] = x[index] + y[index];
*   }
*/
#include <stdio.h>
#include <math.h>

__global__ void vecAdd(const double *x, const double *y, double *z, const int N)
{
    const int index = blockDim.x * blockIdx.x + threadIdx.x;
    if (index < N)
    {
        z[index] = x[index] + y[index];
    }
}

void vecAddCpu(const double *x, const double *y, double *z, const int N)
{
    for (int i = 0; i < N; i++)
    {
        z[i] = x[i] + y[i];
    }
}

// 乘法计算
int main()
{
    const int N = 1000;
    const int M = sizeof(double) * N;
    double *h_x = (double *)malloc(M);
    double *h_y = (double *)malloc(M);
    double *h_z = (double *)malloc(M);
    double *result_cpu = (double *)malloc(M);
    for (int i = 0; i < N; i++)//初始化
    {
        h_x[i] = 1;
        h_y[i] = 2;
    }

    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 - 1) / block_size;

    vecAdd<<<grid_size, block_size>>>(d_x, d_y, d_z, N);
    cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost);

    vecAddCpu(h_x, h_y, result_cpu, N);//使用cpu计算
    bool error = false;
    for (int i = 0; i < N; i++)
    {
        if (fabs(result_cpu[i] - h_z[i]) > (1.0e-10))
        {
            error = true;
            printf("%f ------ %f", result_cpu[i], h_z[i]);
        }
    }
    printf("Result:%s\n", error ? "Errors" : "Pass");
    free(h_x);
    free(h_y);
    free(h_z);
    free(result_cpu);
    cudaFree(d_x);
    cudaFree(d_y);
    cudaFree(d_z);
    return 0;
}

二:线程索引

  1. 在cuda中,线程被分为三个维度,x , y , z。
  2. 在cuda编程中,会分为一个grid,多个block,多个thread。

1:index索引

__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);
}

2:维度

__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);
}

3:每个block中的线程索引

在这里插入图片描述

__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);
}

4:grid中的线程索引(唯一,很少用)

在这里插入图片描述

__global__ void print_thread_idx_per_grid_kernel()
{
	// 一个block中含有的线程数量
    int bSize = blockDim.z * blockDim.y * blockDim.x;

	// block的全局index号
    int bIndex = blockIdx.z * gridDim.x * gridDim.y +
                 blockIdx.y * gridDim.x +
                 blockIdx.x;
	
	// block内的线程索引
    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);
}

5:常用索引

__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; // x方向上唯一索引
    int y = blockIdx.y * blockDim.y + threadIdx.y; // y方向上唯一索引

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

三:cuda关键字

1:同步函数

在cpu将任务通过核函数时,由于异构运算,这时候cpu不会等待gpu执行完毕。故需要进行事件同步。

1:注意事项

在正常的cuda程序中,需要进行内存拷贝,即cudaMemcpy(),该函数会自动实现同步功能,故不需要再进行单独同步。若未进行拷贝操作,则需要单独执行同步函数。

2:关键字

1:cudaDeviceSynchronize();

作用对象: 该函数用于同步当前CUDA设备上的所有流。 使用场景:
当你使用多个CUDA流时,可能需要确保所有的流都已完成执行,然后再继续执行后续的CPU代码。cudaDeviceSynchronize()
会阻塞当前的CPU线程,直到当前设备上的所有流都已完成。

示例:

int main_test1()
{
    printf("Hello World!CPU\n");
    dim3 gridSize(2, 2);
    dim3 blockSize(3, 3);
    my_first_kernal<<<gridSize, blockSize>>>();
    cudaDeviceSynchronize(); // 等待gpu执行完毕,因为是异构运算,在执行后主机不会等待gpu执行完毕,所以需要cudaDeviceSyncronize()
    return 0;
}
2:cudaEventSynchronize();

作用对象: 该函数用于同步特定的CUDA事件,即 cudaEvent_t。 使用场景:
当你创建了一个CUDA事件,并在程序中使用该事件进行时间测量或其他目的时,你可能需要等待事件完成。cudaEventSynchronize()
会阻塞当前的CPU线程,直到指定的事件已完成。

示例:

cudaEvent_t myEvent;
cudaEventCreate(&myEvent);
// ... 在此处执行一些CUDA操作,然后记录事件
cudaEventRecord(myEvent, 0);
// ... 在此处可能需要等待事件完成
cudaEventSynchronize(myEvent);
3:cudaStreamSynchronize(stream);

作用对象: 同步指定的CUDA流。
使用场景: 当你使用多个流,并且只关心某个特定流的完成时,可以使用 cudaStreamSynchronize(stream)

示例:

cudaStream_t myStream;
cudaStreamCreate(&myStream);
// ... 在此处执行一些CUDA操作,并将其提交到myStream
// ... 在此处可能需要等待特定流完成
cudaStreamSynchronize(myStream);

4:cudaThreadSynchronize();

作用对象: 同步当前主机线程与当前设备之间的所有流。 使用场景:
在旧版本的CUDA中,用于同步主机线程与设备之间的操作。在较新的CUDA版本中,已被 cudaDeviceSynchronize() 替代。

2:常用关键字及函数

1. 线程分配关键字dim3
dim3 gridSize(2, 2);
dim3 blockSize(3, 3);
2. 设备内存分配函数

在device端分配空间。

cudaMalloc(void **devPtr, size_t size)

double *d_z;
cudaMalloc((void **)&d_z, M);//为device变量分配空间,M为分配的内存大小
3. 主机内存分配函数

在host端的pinned memory上分配空间。

cudaMallocHost(void **ptr, size_t size) 或 void * malloc(size_t __size) //省去内存和硬盘的交换时间

int *h_a;
cudaMallocHost((void **)&h_a, memsize);//常用
h_a = (int*)malloc(memsize)

补充说明:
1:对CUDA架构而言,主机端的内存被分为两种,一种是可分页内存(pageable memroy)和页锁定内存(page-lock或 pinned)。可分页内存是由操作系统API malloc()在主机上分配的,页锁定内存是由CUDA函数cudaHostAlloc()在主机内存上分配的,页锁定内存的重要属性是主机的操作系统将不会对这块内存进行分页和交换操作,确保该内存始终驻留在物理内存中。
2:GPU知道页锁定内存的物理地址,可以通过“直接内存访问(Direct Memory Access,DMA)”技术直接在主机和GPU之间复制数据,速率更快。由于每个页锁定内存都需要分配物理内存,并且这些内存不能交换到磁盘上,所以页锁定内存比使用标准malloc()分配的可分页内存更消耗内存空间。
出自:https://blog.csdn.net/dcrmg/article/details/54975432

4. 内存拷贝函数

cudaMemcpy(void *dst, const void *src, size_t count, cudaMemcpyKind kind),以同步的方式进行传输

//d_x,d_z为设备上分配的空间。h_x,h_z为主机上的空间。M为空间大小。
cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice);//cudaMemcpyHostToDevice从主机到设备拷贝数据
cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost);//cudaMemcpyDeviceToHost从设备到主机拷贝数据

cudaMemcpyAsync(),以异步的方式进行传输

5. 事件记录函数
cudaEvent_t start,stop_gpu; // 时间节点
cudaEventCreate(&start);
cudaEventCreate(&stop_gpu)
cudaEventRecord(start); // 记录第一个时间
//。。。
cudaEventRecord(stop_gpu); // 记录时间戳,通过减法算出gpu执行时间
cudaEventElapsedTime(&time_gpu, start, stop_gpu);    // 计算gpu花费时间
6. 内存释放函数

习惯:free顺序和malloc顺序反着来,后malloc的先free

cudaEventDestroy(start); // 回收事件资源
cudaEventDestroy(stop_cpu);
cudaFreeHost(h_a);//回收主机资源,由cudaMallocHost()分配
cudaFree(d_a);//回收设备资源,由cudaMalloc()分配

3:错误日志使用

1:cuda runtime API

cuda runtime API自带的错误信息提示不清晰,可以使用以下函数实现。
1: 方式1

cudaError_t error_code;
error_code = cudaGetLastError();//最新一次报错信息
if (error_code != cudaSuccess)
{
    printf("Errors Info:%s\n", cudaGetErrorString(error_code)); // 错误信息
    printf("File:%s\n", __FILE__);                              // 文件信息
    printf("LINE:%d\n", __LINE__);                              // 在第几行
}

2: 方式2
引入头文件,#include “error.cuh”

//error.cuh
#pragma once
#include <stdio.h>

#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(cudaEventCreate(&start));
CHECK(cudaEventRecord(start));
CHECK(cudaMemcpy(d_b, h_b, memsize, cudaMemcpyHostToDevice));

若发生错误,则会输出错误类型以及位置。

2:kernal函数

#define LAST_KERNEL_CHECK()          __kernelCheck(__FILE__, __LINE__)
inline static void __kernelCheck(const char* file, const int line) {
    /* 
     * 在编写CUDA是,错误排查非常重要,默认的cuda runtime API中的函数都会返回cudaError_t类型的结果,
     * 但是在写kernel函数的时候,需要通过cudaPeekAtLastError或者cudaGetLastError来获取错误
     */
    cudaError_t err = cudaPeekAtLastError();
    if (err != cudaSuccess) {
        printf("ERROR: %s:%d, ", file, line);
        printf("CODE:%s, DETAIL:%s\n", cudaGetErrorName(err), cudaGetErrorString(err));
        exit(1);
    }
}

调用方式:

MatmulKernal<<<grid_size, block_size>>>(d_matM, d_matN, d_matP, width);
LAST_KERNEL_CHECK();
CHECK(cudaMemcpy(matp, d_matP, size, cudaMemcpyDeviceToHost));
CHECK(cudaDeviceSynchronize());
;