TRT1-CUDA_driver_Api-1 驱动、Check、Context、Memory

// CUDA驱动头文件cuda.h
#include  <cuda.h>
#include  <stdio.h>
#include <string.h>

int main(){    /* 
    cuInit(int flags), 这里的flags目前必须给0;
        对于cuda的所有函数,必须先调用cuInit,否则其他API都会返回CUDA_ERROR_NOT_INITIALIZED
        https://docs.nvidia.com/cuda/archive/11.2.0/cuda-driver-api/group__CUDA__INITIALIZE.html
     */
    CUresult code=cuInit(0);  //CUresult 类型:用于接收一些可能的错误代码
    if(code != CUresult::CUDA_SUCCESS){
        const char* err_message = nullptr;
        cuGetErrorString(code, &err_message);    // 获取错误代码的字符串描述
        // cuGetErrorName (code, &err_message);  // 也可以直接获取错误代码的字符串
        printf("Initialize failed. code = %d, message = %s\n", code, err_message);
        return -1;
    }
    
    /* 
    测试获取当前cuda驱动的版本
    显卡、CUDA、CUDA Toolkit

        1. 显卡驱动版本,比如:Driver Version: 460.84
        2. CUDA驱动版本:比如:CUDA Version: 11.2
        3. CUDA Toolkit版本:比如自行下载时选择的10.2、11.2等;这与前两个不是一回事, CUDA Toolkit的每个版本都需要最低版本的CUDA驱动程序
        
        三者版本之间有依赖关系, 可参照https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html
        nvidia-smi显示的是显卡驱动版本和此驱动最高支持的CUDA驱动版本
        
     */

    
    int driver_version = 0;
    code = cuDriverGetVersion(&driver_version);  // 获取驱动版本
    printf("CUDA Driver version is %d\n", driver_version); // 若driver_version为11020指的是11.2

    // 测试获取当前设备信息
    char device_name[100]; // char 数组
    CUdevice device = 0;
    code = cuDeviceGetName(device_name, sizeof(device_name), device);  // 获取设备名称、型号如:Tesla V100-SXM2-32GB // 数组名device_name当作指针
    printf("Device %d name is %s\n", device, device_name);
    return 0;
}

cuInit是一个 driver api 用来返回的是调用成功或者失败

curesult 是一个接受类型。

可以看到如果没有经历cuInit那么返回的类型会是3,这个cunit枚举类型中的3对应着的是没有初始化。

 作为保险,可以自定义cudacheck

// 使用有参宏定义检查cuda driver是否被正常初始化, 并定位程序出错的文件名、行数和错误信息
// 宏定义中带do...while循环可保证程序的正确性
#define checkDriver(op)    \
    do{                    \
        auto code = (op);  \
        if(code != CUresult::CUDA_SUCCESS){     \
            const char* err_name = nullptr;     \
            const char* err_message = nullptr;  \
            cuGetErrorName(code, &err_name);    \
            cuGetErrorString(code, &err_message);   \
            printf("%s:%d  %s failed. \n  code = %s, message = %s\n", __FILE__, __LINE__, #op, err_name, err_message);   \
            return -1;   \
        }                \
    }while(0)

通过这种check就可以快速告诉我需要查找什么问题。但是这样的check对于一些返回不是整数的效果就不是很好了(因为return-1)。

所以需要进一步完善check这个功能:
 

#define checkDriver(op)  __check_cuda_driver((op), #op, __FILE__, __LINE__)

bool __check_cuda_driver(CUresult code, const char* op, const char* file, int line){

    if(code != CUresult::CUDA_SUCCESS){    
        const char* err_name = nullptr;    
        const char* err_message = nullptr;  
        cuGetErrorName(code, &err_name);    
        cuGetErrorString(code, &err_message);   
        printf("%s:%d  %s failed. \n  code = %s, message = %s\n", file, line, op, err_name, err_message);   
        return false;
    }
    return true;
}

这里返回的是一个bool类型的值。很明显,这种结构的返回之更加的友好。

在runtime中也可以用这种,与上面的没什么大的区别。

#define CHECK(call)\
{\
  const cudaError_t error=call;\
  if(error!=cudaSuccess)\
  {\
      printf("ERROR: %s:%d,",__FILE__,__LINE__);\
      printf("code:%d,reason:%s\n",error,cudaGetErrorString(error));\
      exit(1);\
  }\
}

CUDA 的运行时函数和驱动程序函数在执行后会返回一个 cudaError_t 类型的值,用于指示函数执行的结果。这个返回值可以是 cudaSuccess(函数执行成功)或者其他不同的错误代码,表示函数执行时发生了错误。

CUDA 运行时函数包括对设备内存的分配、数据传输、核函数的调用等操作,而驱动程序函数涉及到 CUDA 驱动程序的管理和配置。无论是运行时函数还是驱动程序函数,它们都会在执行后返回一个 cudaError_t 类型的值。

CUcontext:

context是一种上下文,关联对GPU的所有操作。

/*

CUcontext实际上是一个指向CUDA上下文对象的指针。通过使用该指针,可以对CUDA上下文进行创建、配置、销毁和管理等操作。

例如,在先前的代码示例中,我们声明了一个名为context的指针变量,并使用cuCtxCreate函数将创建的上下文对象的地址赋值给该指针:

CUcontext context;
cuCtxCreate(&context, CU_CTX_SCHED_AUTO, 0);

然后,我们可以通过该指针进行进一步的操作,如释放上下文资源:

cuCtxDestroy(context);

*/

每个线程都有一个栈结构来存储context ,栈顶是当前使用的context,对应有push、pop等操作。所有的api都以当前的context为操作目标。

没有context:

cuMalloc(device , &ptr , 100);
cuFree(device ,ptr);
cuMemcpy(device , dst ,src,100);

有context:
ceCreateContext(device , &context);
cuPushCurent(context);
cuMalloc(&ptr , 100);
cuFree(ptr);
cuMemcpy(dst , src ,100);
cuPopCurrent(context);

可以看到加入了context之后系统更加简洁,并且对于整体的封装性也更好。

而由于高频的操作,所以对于设备而言,基本是一个线程固定访问一个显卡的,就是只是用一个context,很少会用到多个context。在这种情况下再一个个push、pop就很麻烦,所以推出了cuDevicePrimaryCtxRetain(),他是一个给他设备id,自动配置context,就是一个显卡对应一个primary context.不同的线程,只要设备id一样,那就是一样的primary context。

而且context线程安全。

用context:
ceCreateContext(device , &context);
cuPushCurent(context);
cuMalloc(&ptr , 100);
cuFree(ptr);
cuMemcpy(dst , src ,100);
cuPopCurrent(context);

用primaryctxretain:
cuDevicePrimaryCttxRetain(device &context)
cuMalloc(&ptr , 100);
cuFree(ptr);
cuMemcpy(dst , src ,100);

而且所有的runtime api都是基于cuDevicePrimaryCtxRetain的。

int main(){

    // 检查cuda driver的初始化
    checkDriver(cuInit(0));

    // 为设备创建上下文
    CUcontext ctxA = nullptr;                                   // CUcontext 其实是 struct CUctx_st*(是一个指向结构体CUctx_st的指针)
    CUcontext ctxB = nullptr;
     CUdevice device = 0;
    checkDriver(cuCtxCreate(&ctxA, CU_CTX_SCHED_AUTO, device)); // 这一步相当于告知要某一块设备上的某块地方创建 ctxA 管理数据。输入参数 参考 https://www.cs.cmu.edu/afs/cs/academic/class/15668-s11/www/cuda-doc/html/group__CUDA__CTX_g65dc0012348bc84810e2103a40d8e2cf.html
    checkDriver(cuCtxCreate(&ctxB, CU_CTX_SCHED_AUTO, device)); // 参考 1.ctx-stack.jpg
    printf("ctxA = %p\n", ctxA);
    printf("ctxB = %p\n", ctxB);
    /* 
        contexts 栈:
            ctxB -- top <--- current_context
            ctxA 
            ...
     */

    // 获取当前上下文信息
    CUcontext current_context = nullptr;
    checkDriver(cuCtxGetCurrent(&current_context));             // 这个时候current_context 就是上面创建的context
    printf("current_context = %p\n", current_context);

    // 可以使用上下文堆栈对设备管理多个上下文
    // 压入当前context
    checkDriver(cuCtxPushCurrent(ctxA));                        // 将这个 ctxA 压入CPU调用的thread上。专门用一个thread以栈的方式来管理多个contexts的切换
    checkDriver(cuCtxGetCurrent(&current_context));             // 获取current_context (即栈顶的context)
    printf("after pushing, current_context = %p\n", current_context);
    /* 
        contexts 栈:
            ctxA -- top <--- current_context
            ctxB
            ...
    */
    

    // 弹出当前context
    CUcontext popped_ctx = nullptr;
    checkDriver(cuCtxPopCurrent(&popped_ctx));                   // 将当前的context pop掉,并用popped_ctx承接它pop出来的context
    checkDriver(cuCtxGetCurrent(&current_context));              // 获取current_context(栈顶的)
    printf("after poping, popped_ctx = %p\n", popped_ctx);       // 弹出的是ctxA
    printf("after poping, current_context = %p\n", current_context); // current_context是ctxB

    checkDriver(cuCtxDestroy(ctxA));
    checkDriver(cuCtxDestroy(ctxB));

    //更推荐使用cuDevicePrimaryCtxRetain获取与设备关联的context
    //注意这个重点,以后的runtime也是基于此, 自动为设备只关联一个context
    checkDriver(cuDevicePrimaryCtxRetain(&ctxA, device));       // 在 device 上指定一个新地址对ctxA进行管理
    printf("ctxA = %p\n", ctxA);
    checkDriver(cuDevicePrimaryCtxRelease(device));
    return 0;
}

 首先是先拿到了ctxA和ctxB的地址。然后由于cuCtxCreate会自动做一个push,所以闲谈出来的是b的地址,然后重新pushctxA进去,拿到ctxA的地址。

然后最重要的就是这个

 checkDriver(cuDevicePrimaryCtxRetain(&ctxA, device));       // 在 device 上指定一个新地址对ctxA进行管理
    printf("ctxA = %p\n", ctxA);
    checkDriver(cuDevicePrimaryCtxRelease(device));
    return 0;

cuDevicePrimaryCtxRetain 函数用于增加设备的主上下文的引用计数,以便继续使用已经创建的上下文。而 cuDevicePrimaryCtxRelease 函数用于减少设备的主上下文的引用计数。

在代码中,通过 cuDevicePrimaryCtxRetain 获取设备关联的主上下文,并将其地址赋给 ctxA 变量。接着打印了 ctxA 的地址,和预期结果相符。

最后,通过 cuDevicePrimaryCtxRelease 减少设备的主上下文的引用计数,来释放这个上下文。

destroy只是干掉了context的create部分,不是真的干掉了ctxA。

Memory


int main(){

    // 检查cuda driver的初始化
    checkDriver(cuInit(0));

    // 创建上下文
    CUcontext context = nullptr;
    CUdevice device = 0;
    checkDriver(cuCtxCreate(&context, CU_CTX_SCHED_AUTO, device));
    printf("context = %p\n", context);

    // 输入device prt向设备要一个100 byte的线性内存,并返回地址
    CUdeviceptr device_memory_pointer = 0;
    checkDriver(cuMemAlloc(&device_memory_pointer, 100)); // 注意这是指向device的pointer, 
    printf("device_memory_pointer = %p\n", device_memory_pointer);

    // 输入二级指针向host要一个100 byte的锁页内存,专供设备访问。参考 2.cuMemAllocHost.jpg 讲解视频:https://v.douyin.com/NrYL5KB/
    float* host_page_locked_memory = nullptr;
    checkDriver(cuMemAllocHost((void**)&host_page_locked_memory, 100));
    printf("host_page_locked_memory = %p\n", host_page_locked_memory);

    // 向page-locked memory 里放数据(仍在CPU上),可以让GPU可快速读取
    host_page_locked_memory[0] = 123;
    printf("host_page_locked_memory[0] = %f\n", host_page_locked_memory[0]);
    /* 
        记住这一点
        host page locked memory 声明的时候为float*型,可以直接转换为device ptr,这才可以送给cuda核函数(利用DMA(Direct Memory Access)技术)
        初始化内存的值: cuMemsetD32 ( CUdeviceptr dstDevice, unsigned int  ui, size_t N )
        初始化值必须是无符号整型,因此需要将new_value进行数据转换:
        但不能直接写为:(int)value,必须写为*(int*)&new_value, 我们来分解一下这条语句的作用:
        1. &new_value获取float new_value的地址
        (int*)将地址从float * 转换为int*以避免64位架构上的精度损失
        *(int*)取消引用地址,最后获取引用的int值
     */
    
    float new_value = 555;
    checkDriver(cuMemsetD32((CUdeviceptr)host_page_locked_memory, *(int*)&new_value, 1)); //??? cuMemset用来干嘛?
    printf("host_page_locked_memory[0] = %f\n", host_page_locked_memory[0]);
/*
CUresult CUDAAPI cuMemsetD32(CUdeviceptr dstDevice, unsigned int ui, size_t N);
N 是 number of element
*/

    // 释放内存
    checkDriver(cuMemFreeHost(host_page_locked_memory));
    return 0;
}

cuMemAllocHost:

 

 锁页内存则是一种特定于内存管理的机制,用于提高数据在内存和设备之间的传输性能。它涉及将内存页面锁定在物理内存中,以阻止操作系统进行页面交换和调度,从而减少数据传输的延迟和性能损失。锁页内存通常在涉及GPU加速计算、高性能网络传输等场景下使用,旨在优化数据访问和传输的性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值