nvrtc教程(全篇无废话)

前言

文章使用的cuda版本:12.4
nvrtc(nvidia runtime compilation),是一个用来实现代码运行时动态编译.cu文件的库。使用这个库并不需要配置cuda环境(但仍需要cuda toolkit),因此对于那些死活配置不好cuda环境的(比如我),这可以是另一种使用kernel的办法(但没有直接nvcc来得方便)。
关于nvrtc的使用,如果有能力的话可以参考官方文档

准备工作

首先需要安装cuda toolkit,配置好include目录(默认目录应该是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\include)和lib目录(默认目录应该是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\lib\x64)以及链接lib(干脆把lib目录下所有lib都链接了吧)。

需要的头文件:

#include<stdio.h>
#include<stdlib.h>
#include<nvrtc.h>
#include<cuda_runtime.h>
#include<cuda.h>

nvrtc.h的函数会返回一个值,可以用来判断函数是否成功运行,如果成功就会返回NVRTC_SUCCESS,返回类型为nvrtcResult,并且可以用nvrtcGetErrorString函数来获取这个错误的字符串形式。写一个简单的宏来实现这个功能

#define NVRTC_SAFE_CALL(x)\
    if(1)\
    {\
        nvrtcResult result = x;\
        if (result != NVRTC_SUCCESS)\
        {\
            printf("\nerror: " #x " failed with error %s\n", nvrtcGetErrorString(result));\
            system("pause");\
            exit(1);\
        }\
    }\

cuda.h和cuda_runtime.h的函数也有类似的返回值CUDA_SUCCESS,同样写一个宏函数:

#define CUDA_SAFE_CALL(x)\
    if(1)\
    {\
        CUresult result = x;\
        if (result != CUDA_SUCCESS)\
        {\
            const char *msg;\
            cuGetErrorName(result, &msg);\
            printf("\nerror: " #x " failed with error %s\n",msg);\
            system("pause");\
            exit(1);\
        }\
    }\

之后每次使用cuda的函数,都会调用这些宏,比如:

NVRTC_SAFE_CALL(nvrtcCreateProgram(&prog, source,  0,0,0,0));

此外,在主函数中应初始化cuda:

void CuInit()
{
	//指定设备并创建context
    CUdevice cuDevice;
    CUcontext context;
    CUDA_SAFE_CALL(cuInit(0));
    CUDA_SAFE_CALL(cuDeviceGet(&cuDevice, 0));
    CUDA_SAFE_CALL(cuCtxCreate(&context, 0, cuDevice));
}

把.cu文件转化为字符串

nvrtc要求输入的是字符串风格的.cu代码,比如下面这种:

const char *saxpy = "                                           \n\
extern \"C\" __global__                                         \n\
void saxpy(float a, float *x, float *y, float *out, size_t n)   \n\
{                                                               \n\
  size_t tid = blockIdx.x * blockDim.x + threadIdx.x;           \n\
  if (tid < n) {                                                \n\
    out[tid] = a * x[tid] + y[tid];                             \n\
  }                                                             \n\
}  

所以需要把.cu文件转化为字符串,写简单一个函数实现这个功能:

//当size参数没有缺省的话,size返回该字符串的长度
char* LoadFile(const char* path, int* size = 0)
{
    if(!size)
    {
        int _size;
        size = &_size;
    }
	FILE* fp = fopen(path, "rb");
	fseek(fp, 0, 2);
	*size = ftell(fp);
	fseek(fp, 0, 0);
	char* p = (char*)malloc(*size + 1);
	fread(p, 1, *size, fp);
	p[*size] = '\0';
	return p;
}

然后,就可以在主函数里面调用这个函数:

    int size;
    const char* path = "path/to/your/.cu";
    char* source = LoadFile(path, &size);

创建program并编译

获取字符串风格后的.cu文件后,调用nvrtcCreateProgram()创建program并调用nvrtcCompileProgram()编译program:

    nvrtcProgram prog; 
    //nvrtcCreateProgram()参数:
    //第一个为program
    //第二个为字符串风格的.cu
    //第三个是这个文件的文件名(只是一个供cuda内部使用的标识,可以通过cubin找寻这个标识获取这个文件,可以为0)
    //第四,五,六个分别为头文件的数目,include目录,include文件的文件名(具体怎么设置,靠大佬解答了)
    NVRTC_SAFE_CALL(nvrtcCreateProgram(&prog, source,  0,0,0,0));
    free(source);
    if(nvrtcCompileProgram(prog, 0,0) != NVRTC_SUCCESS)
    {
        size_t logSize;
        //nvrtcGetProgramLog获取错误日志,nvrtcGetProgramLogSize获取错误日志大小
        NVRTC_SAFE_CALL(nvrtcGetProgramLogSize(prog, &logSize));
        char *log = (char*)malloc(logSize);
        NVRTC_SAFE_CALL(nvrtcGetProgramLog(prog, log));
        printf("%s",log);
        free(log);
        system("pause");
        exit(1);
    }

编译为ptx文件

接着从program获取ptx代码:

    size_t ptxSize;
    NVRTC_SAFE_CALL(nvrtcGetPTXSize(prog, &ptxSize));
    char *ptx = (char*)malloc(ptxSize);
    NVRTC_SAFE_CALL(nvrtcGetPTX(prog, ptx));
    NVRTC_SAFE_CALL(nvrtcDestroyProgram(&prog));

获取kernel

    CUmodule module;
    CUfunction kernel;
    CUDA_SAFE_CALL(cuModuleLoadDataEx(&module, ptx, 0, 0, 0));//需要先把ptx加载为module才能获取kernel
    CUDA_SAFE_CALL(cuModuleGetFunction(&kernel, module, functionName));

运行函数

	//参数:
	//第一个为kernel
	//之后六个参数为grid和block的size
	//接着两个参数为共享内存的大小和工作流的指定
	//args即为传递的参数,等会讲;最后的0我也不知道
	CUDA_SAFE_CALL(cuLaunchKernel(kernel, grid[0],grid[1],grid[2], block[0],block[1],block[2], 0,0, args,0));

至此,我们可以把上述封装成头函数了:

#include<stdio.h>
#include<stdlib.h>
#include<nvrtc.h>
#include<cuda_runtime.h>
#include<cuda.h>

#define NVRTC_SAFE_CALL(x)\
    if(1)\
    {\
        nvrtcResult result = x;\
        if (result != NVRTC_SUCCESS)\
        {\
            printf("\nerror: " #x " failed with error %s\n", nvrtcGetErrorString(result));\
            system("pause");\
            exit(1);\
        }\
    }\


#define CUDA_SAFE_CALL(x)\
    if(1)\
    {\
        CUresult result = x;\
        if (result != CUDA_SUCCESS)\
        {\
            const char *msg;\
            cuGetErrorName(result, &msg);\
            printf("\nerror: " #x " failed with error %s\n",msg);\
            system("pause");\
            exit(1);\
        }\
    }\
   
char* LoadFile(const char* path, int* size = 0)
{
    if(!size)
    {
        int _size;
        size = &_size;
    }
	FILE* fp = fopen(path, "rb");
	fseek(fp, 0, 2);
	*size = ftell(fp);
	fseek(fp, 0, 0);
	char* p = (char*)malloc(*size + 1);
	fread(p, 1, *size, fp);
	p[*size] = '\0';
	return p;
}

void CuInit()
{
    CUdevice cuDevice;
    CUcontext context;
    CUDA_SAFE_CALL(cuInit(0));
    CUDA_SAFE_CALL(cuDeviceGet(&cuDevice, 0));
    CUDA_SAFE_CALL(cuCtxCreate(&context, 0, cuDevice));
}

CUfunction GetKernel(const char* path, const char* functionName)
{
    int size;
    char* source = LoadFile(path, &size);
    nvrtcProgram prog; 
    NVRTC_SAFE_CALL(nvrtcCreateProgram(&prog, source,  0,0,0,0));
    free(source);
    if(nvrtcCompileProgram(prog, 0,0) != NVRTC_SUCCESS)
    {
        size_t logSize;
        NVRTC_SAFE_CALL(nvrtcGetProgramLogSize(prog, &logSize));
        char *log = (char*)malloc(logSize);
        NVRTC_SAFE_CALL(nvrtcGetProgramLog(prog, log));
        printf("%s",log);
        free(log);
        system("pause");
        exit(1);
    }
    size_t ptxSize;
    NVRTC_SAFE_CALL(nvrtcGetPTXSize(prog, &ptxSize));
    char *ptx = (char*)malloc(ptxSize);
    NVRTC_SAFE_CALL(nvrtcGetPTX(prog, ptx));
    NVRTC_SAFE_CALL(nvrtcDestroyProgram(&prog));
    CUmodule module;
    CUfunction kernel;
    CUDA_SAFE_CALL(cuModuleLoadDataEx(&module, ptx, 0, 0, 0));
    CUDA_SAFE_CALL(cuModuleGetFunction(&kernel, module, functionName));
    return kernel;
}

void UseKernel(const char* path, const char* functionName, void** args, int* grid, int* block)
{
    CUfunction kernel = GetKernel("1.cu", "test");
    CUDA_SAFE_CALL(cuLaunchKernel(kernel, grid[0],grid[1],grid[2], block[0],block[1],block[2], 0,0, args,0));
}

args设置

cuLaunchKernel()要求的args是一个数组,包含了需要传递参数的指针,比如像这样的:

void* args[] = {&d_out, &a,&x,&b,&y};

如果要传递常量,那么只需在主存创建该变量,然后传递指向在主存上该变量的指针;如果要传递数组,那么首先要把在主存上的数组数据拷贝到显存上,接着传递指向在显存上该数组的指针。

此外,kernel返回类型必须void,这意味着返回值需要在参数列表里面,然后把结果从显存拷贝回内存。

举个例子,我要为下面的kernel传递参数(kernel前面记得加 extern “C”):

extern "C" __global__
void test(int* out, int a, int b, int* coord)
{
    out[0] = a*coord[0]+b*coord[1];
}                                                                        

那么,我在我的主函数里面要这么调用:

#include<stdio.h>
#include<stdlib.h>
#include<LoadCuda.h>
#include<cuda.h>

int grid[3] = {2,2,1};
int block[3] = {2,2,2};

int main()
{   
    CuInit();
    int a=1,b=2;
    int coord[] = {3,4};
    CUdeviceptr d_coord;//创建一个显存指针
    cuMemAlloc(&d_coord, 2*sizeof(int));//分配一块显存并让d_coord指向该显存
    cuMemcpyHtoD(d_coord, coord, 2*sizeof(int));//把主存上coord指向的数据传给d_coord指向的区域
    CUdeviceptr d_out;
    cuMemAlloc(&d_out, sizeof(int));//返回值也要分配显存
    void* args[] = {&d_out, &a,&b,&d_coord};
    UseKernel("1.cu", "test", args, grid, block);
    int out;
    cuMemcpyDtoH(&out, d_out, 4);//把数据传回主存
    printf("%d\n",out);
    system("pause");
    return 0;
}

至此,你应该学会如何基本使用nvrtc.h来加载kernel了

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值