《CUDA编程》4.CUDA程序的错误检测

在编写CUDA程序时,有的错误在编译过程中被发现,称为编译错误,有的在运行时出现,称为运行时刻错误,本章讨论如何排查运行时刻错误

1 一个检测CUDA运行时错误的宏函数

1.1 编写错误检查宏函数

《CUDA编程》3.简单CUDA程序的基本框架 中列举的函数,返回值是cudaError_t,只有在返回cudaSuccess时,才表示调用成功,否则返回一个错误代码,下面新建一个CUDA头文件并编写一个错误检查的宏函数:

①新建CUDA头文件
在这里插入图片描述
新建的文件是error_check.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 message: %s\n", cudaGetErrorString(error_code)); \
        exit(1); \
    } \
} while (0)

该段代码会检查返回值是否为cudaSuccess,如果不是,则返回错误代码的位置

1.2 把检查函数添加到CUDA程序中

这里以《CUDA编程》3.简单CUDA程序的基本框架中的代码例子为例,注意在头文件中添加#include "error_check.cuh",并为代码中分配内存的函数进行检查。

然后手动将39行的代码修改为 CHECK(cudaMemcpy(d_x, h_x, M, cudaMemcpyDeviceToHost));,修改后是错误代码,原本应该是cudaMemcpyHostToDevice

#include <cuda.h>
#include <cuda_runtime.h>
#include <math.h>
#include <stdio.h>
#include "error_check.cuh"


const double EPS = 1.0e-15;
const double a = 1.23;
const double b = 2.34;
const double c = 3.57;

// 希望 add 函数在 GPU 上执行
__global__ void add(const double* x, const double* y, double* z);
void check(const double* z, const int N);

int main(void) {
    const int N = 100000000; // 定义数组的长度为 10 的 8 次方
    const int M = sizeof(double) * N; // 每个数组所需的字节数

    // 分配host内存
    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] = a;
        h_y[n] = b;
    }

    //分配device内存
    double* d_x, * d_y, * d_z;
    CHECK(cudaMalloc((void**)&d_x, M));
    CHECK(cudaMalloc((void**)&d_y, M));
    CHECK(cudaMalloc((void**)&d_z, M));
    
    // 将数据从主机复制到设备上
    CHECK(cudaMemcpy(d_x, h_x, M, cudaMemcpyDeviceToHost));
    CHECK(cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice));

    const int block_size = 128;
    // 计算网格尺寸,确保所有元素都能被处理
    const int grid_size = (N + block_size - 1) / block_size;

    // 调用内核函数在设备中进行计算
    add << <grid_size, block_size >> > (d_x, d_y, d_z);

    // 将计算结果从设备复制回主机
    CHECK(cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost));
    check(h_z, N);

    // 释放内存
    free(h_x);
    free(h_y);
    free(h_z);
    CHECK(cudaFree(d_x));
    CHECK(cudaFree(d_y));
    CHECK(cudaFree(d_z));
    return 0;
}

__global__ void add(const double* x, const double* y, double* z) {
    const int n = blockIdx.x * blockDim.x + threadIdx.x;
    z[n] = x[n] + y[n];
}

void check(const double* z, const int N) {
    bool has_error = false;
    for (int n = 0; n < N; ++n) {
        if (fabs(z[n] - c) > EPS) {
            has_error = true;
        }
    }
    printf("Has error: %d\n", has_error);
}

运行结果如下:
在这里插入图片描述
指出了错误代码的信息,包括文件位置、行数、个数、和错误类型invalid argument,及代表该行函数出现了非法参数,正是由于我们手动修改导致的错误

PS: 大部分代码都可以使用该宏函数,除了cudaEventQuery(),因为它可能返回cudaErrorNotReady,但并不是代码出错了

1.3 使用该宏函数检查核函数错误

使用上述方法并不能捕捉核函数的错误,因为核函数不返回任何值,所以若想捕捉和函数的错误,应该在调用核函数之后使用如下语句:

CHECK(cudaDeviceSynchronize());
CHECK(cudaGetLastError());
  • 第一个语句是同步主机和设备,因为核函数的调用是异步的,使用该函数可以确保之前的CUDA操作全部完成,以便检查这些操作是否成功
  • 返回自上次调用 cudaGetLastError() 或者自程序开始以来最后一个 CUDA API 调用的错误代码。

依旧以上面的函数作为例子,手动的将block_size修改为1280,但我们知道该参数不能超过1024,所以会报错,代码如下:

#include <cuda.h>
#include <cuda_runtime.h>
#include <math.h>
#include <stdio.h>
#include "error_check.cuh"


const double EPS = 1.0e-15;
const double a = 1.23;
const double b = 2.34;
const double c = 3.57;

// 希望 add 函数在 GPU 上执行
__global__ void add(const double* x, const double* y, double* z);
void check(const double* z, const int N);

int main(void) {
    const int N = 100000000; // 定义数组的长度为 10 的 8 次方
    const int M = sizeof(double) * N; // 每个数组所需的字节数

    // 分配host内存
    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] = a;
        h_y[n] = b;
    }

    //分配device内存
    double* d_x, * d_y, * d_z;
    CHECK(cudaMalloc((void**)&d_x, M));
    CHECK(cudaMalloc((void**)&d_y, M));
    CHECK(cudaMalloc((void**)&d_z, M));
    
    // 将数据从主机复制到设备上
    CHECK(cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice));
    CHECK(cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice));

    const int block_size = 1280;
    // 计算网格尺寸,确保所有元素都能被处理
    const int grid_size = (N + block_size - 1) / block_size;

    // 调用内核函数在设备中进行计算
    add << <grid_size, block_size >> > (d_x, d_y, d_z);
    CHECK(cudaDeviceSynchronize());
    CHECK(cudaGetLastError());

    // 将计算结果从设备复制回主机
    CHECK(cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost));
    check(h_z, N);

    // 释放内存
    free(h_x);
    free(h_y);
    free(h_z);
    CHECK(cudaFree(d_x));
    CHECK(cudaFree(d_y));
    CHECK(cudaFree(d_z));
    return 0;
}

__global__ void add(const double* x, const double* y, double* z) {
    const int n = blockIdx.x * blockDim.x + threadIdx.x;
    z[n] = x[n] + y[n];
}

void check(const double* z, const int N) {
    bool has_error = false;
    for (int n = 0; n < N; ++n) {
        if (fabs(z[n] - c) > EPS) {
            has_error = true;
        }
    }
    printf("Has error: %d\n", has_error);
}

输出结果如下:
在这里插入图片描述
即表示配置错误,如果不使用该函数,则只能发现有一个错误,而不知道具体的错误信息。

PS: cudaDeviceSynchronize()非常消耗时间,所以一般不在内存循环中调用,否则会严重降低程序性能

2 用CUDA-MEMCHECK检查内存错误

CUDA提供了CUDA-MEMCHECK工具集,可以帮助你发现诸如越界访问、未初始化内存访问、内存泄漏等内存错误,从而提高代码的可靠性和性能。一共包含了4个工具:

  • memcheck:用于检测内存访问错误,包括越界访问、未初始化内存访问等,常见错误类型有:
    –Global Out-of-bounds:访问超出全局内存范围。
    –Local Out-of-bounds:访问超出局部内存范围。
    –Uninitialized Access:访问未初始化的内存。
    –Invalid Device Pointer:使用无效的设备指针
  • racecheck:用于检测数据竞争,即多个线程同时访问同一内存位置且至少有一个线程在写入,常见错误类型有:
    –Race Condition:多个线程同时访问同一内存位置且至少有一个线程在写入。
  • synccheck:用于检测同步错误,即线程之间的同步问题,常见错误类型有:
    –Barrier Synchronization Error:线程在屏障同步点出现错误。
    –Grid Synchronization Error:线程在网格同步点出现错误。
  • initcheck:用于检测未初始化内存的使用,常见错误类型有:
    –Uninitialized Memory Use:使用未初始化的内存。

以上4个工具都可由cuda-memcheck执行文件调用,其中调用memcheck时,可以简化,注意,只能对编译后的文件进行检查,通常是.out ,命令如下:

cuda-memcheck ./my_cuda_program.out

其他三个不可以简化

使用racecheck工具:

cuda-memcheck --tool racecheck ./my_cuda_program.out

使用synccheck工具

cuda-memcheck --tool synccheck ./my_cuda_program.out

使用initcheck工具

cuda-memcheck --tool initcheck ./my_cuda_program.out
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青石横刀策马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值