CUDPP基本执行过程分析

  •     最近做一个k-D树的实现,中间过程要使用CUDPP,因为有一点不理解的地方,所以想看看CUDPP的具体实现,本文以CUDPP源码中自带的simpleCUDPP为例,就CUDPP的使用和运行过程作简单的分析。

        CUDA和数据等的初始化就不说了,相信使用CUDPP的同学都很清楚的。

        CUDPP本身是指CUDA Data-Parallel Primitives Library,翻译成中文也就是CUDA数据并行原语库。

     

    CUDPP初始化

        首先要讲的就是CUDPP的初始化:

            CUDPPHandle theCudpp;

            cudppCreate(&theCudpp);

        先定义了一个CUDPP句柄theCudpp,然后调用函数cudppCreate实例化一个CUDPP,并将句柄返回给theCudpp

        需要注意的是实例化必须要调用任何CUDPP函数之前,这也是很好理解的。另外在使用多GPU的应用中,每一个CUDA上下文都需要实例化一次CUDPP(因为每一个CUDA上下文使用一个独立的CUDPP实例)。现在我们来看看cudppCreate具体做了哪些事情。

        看下函数cudppCreate的具体实现:

            CUDPPResult cudppCreate(CUDPPHandle* theCudpp)

            {

                CUDPPManager *mgr = new CUDPPManager();

                *theCudpp = mgr->getHandle();

           return CUDPP_SUCCESS;

       }

        可见函数先在堆上定义了一个CUDPPManager对象,然后调用该对象的getHandle函数,从函数表面理解是返回一个句柄赋值给传进来的参数theCudpp所指向的句柄,紧接着返回。看到这里可能还有点糊涂,因为还是不明白CUDPPManager具体又是什么呢,getHandle返回的句柄又是什么意义,那么继续看源码:

    class CUDPPManager

    {

    public:

        //无关的函数先省了

     

        CUDPPHandle getHandle()

        {

            return reinterpret_cast<CUDPPHandle>(this);

        }

     

    private:

        cudaDeviceProp m_deviceProps;

    };

        看到这里,相信我们应该都会明白了,原来返回的句柄就是指向CUDPPManager的对象mgr本身的地址值,也就是说这个句柄实际上是一个指向CUDPPManager对象的地址。而这个对象具有一个cudaDeviceProp的成员变量,每一个CUDA上下文都需要实例化一次CUDPP的原因也就在于他的成员变量上(CUDPP会根据设备,也就是显卡的属性值自动的选择最优的参数进行原语操作)。这里还涉及到C++类型转化操作符reinterpret_cast的使用,简单地说它的一种用法是对指针和一个整形数(这也就是CUDPPHandle的实际类型size_t)进行互相转换,具体使用可以再查资料。

        总结一下CUDPP的初始化:实例化一个CUDPP,也就是生成一个CUDPPManager对象,将其地址(已经转化为size_t类型)返回给theCudpp保存。

     

    CUDPP配置

        初始化之后要做的工作就是配置CUDPP,也就是要指定自己要利用CUDPP做什么操作(ADDMULTIPLYMIN等等),操作的是什么类型的数据(CHARINTFLOAT等)要采用什么原语(SCANSORTSEGMENTED SCAN等)还有就是操作的有关选项(FORWORD还是BACKWORDINCLUSIVE还是EXCLUSIVE)。

        配置的代码相对简单也很好理解,下面列出来就好了。

    CUDPPConfiguration config;

    config.op = CUDPP_ADD;

    config.datatype = CUDPP_FLOAT;

    config.algorithm = CUDPP_SCAN;

    config.options = CUDPP_OPTION_FORWARD | CUDPP_OPTION_EXCLUSIVE;

    配置完毕,马上就要切入正题。

     

    生成CUDPP计划

    先贴代码:

    CUDPPHandle scanplan = 0;

    CUDPPResult res = cudppPlan(theCudpp, &scanplan, config, numElements, 1, 0);

        开始还是有点难以理解的,为什么前面生成了一个CUDPPHandle并且配置也完了,不是应该要进行操作了吗,怎么现在又来一个CUDPPHandle呢?语句的表面意思比较好理解,大致可以认为它是定义了一个CUDPPHandle然后生成一个cudppPlan并且将句柄赋值给scanPlan。那theCudppscanPlan是什么关系呢?我也是出于这个原因来看具体实现的。下面就看看cudppPlan做的工作吧。

         CUDPPResult cudppPlan(const CUDPPHandle cudppHandle,

                          CUDPPHandle        *planHandle,

                          CUDPPConfiguration config,

                          size_t             numElements,

                          size_t             numRows,

                          size_t             rowPitch)

    {

        CUDPPResult result = CUDPP_SUCCESS;

     

       //CUDPPManager的实现中可以看到下面两句是将cudppHandle转换回CUDPPManager对象指针

        CUDPPPlan *plan;

        CUDPPManager *mgr = CUDPPManager::getManagerFromHandle(cudppHandle);

     

       //这一段也很好理解,就是判断配置是否正确,具体可以看validateOptions函数实现。

        result = validateOptions(config, numElements, numRows, rowPitch);

        if (result != CUDPP_SUCCESS)

        {

            *planHandle = CUDPP_INVALID_HANDLE;

            return result;

        }

     

       //接下来就要真正开始创建plan了,创建基于配置参数config,生成不同类型的并行原语

        switch (config.algorithm)

        {

        case CUDPP_SCAN:

            {

                plan = new CUDPPScanPlan(mgr, config, numElements, numRows, rowPitch);

                break;

            }

     

         // ...

        //中间一些case以及default项就省了

         // ...

        }

     

       //最后不多解释了,将CUDPPPlan对象指针转换为CUDPPHandle赋值给planHandle,返回

        if (!plan)

            return CUDPP_ERROR_UNKNOWN;

        else

        {

            *planHandle = plan->getHandle();

            return CUDPP_SUCCESS;

        }

    }

     

        整个函数其实很简单,就是根据配置创建一个CUDPPPlan,然后将地址值转换为CUDPPHandle赋值给planHandle。接下来就以CUDPPRadixSortPlan为例看看是怎么建立起cudppHandleplanHandle之间的关系的。

        当然先要了解一下CUDPPRadixSortPlan的实现(具体实现省略):

    class CUDPPRadixSortPlan: public CUDPPPlan

    主要看这个继承关系,然后直接贴出CUDPPPLan的实现:

    class CUDPPPlan

    {

    public:

       //其他无关项省去

        CUDPPManager      *m_planManager;

    };

    以此可以看出,每一个CUDPPPlan有一个成员变量为CUDPPManager指针,指向CUDPP实例。

    再看CUDPPRadixSortPlan构造函数中一个语句(其他都是涉及一些具体实现的内容):

    allocRadixSortStorage(this);

        在函数allocRadixSortStorage在分配一些显存空间以后调用函数:initDeviceParameters,这也就是解析cudppHandleplanHandle关系的关键。initDeviceParameters具体实现就是根据planHandleCUDPPManager指针成员变量,设置在进行原语操作过程中的一些参数的最优值。至此,相信困扰我们的谜团已经解开了。OK,继续下一步骤--执行原语操作!

     

    执行原语

        直接看代码吧:

    res = cudppScan(scanPlan,pOutData,pInData, g_MyData.size);

    if(CUDPP_SUCCESS != res)

    std::cout<<"Operate ERROR!"<<std::endl;

        一句话,不解释!直接分析中间存在的导致混淆的一点小问题:在这个简单的cudpp例子中,整个过程计划中(按照配置config参数的设置)和最终执行的原语,都是scan操作,而实际上,在最后这一步执行原语的时候可以调用其他的原语操作,例如cudppReduce

    cudppSegmentedScan等等,只是在这些函数实现中会进行一次配置的判断,如果不匹配就会报错[相信CUDPP的设计者也是做了详尽的考虑,采用了这种最简洁高效的方式]

     

    清理

        清理工作是必须的,因为每一个CUDPPHandle都占用了一定的系统资源。需要清理的内容有两个类型:

    1CUDPP计划:cudppDestroyPlan(scanPlan)

    2CUDPP实例:cudppDestroy(theCudpp)

        清理顺序:最好是先清理CUDPP计划,然后清理CUDPP实例,当然在保证正确性的情况下顺序无关紧要。

        至此CUDPP的基本使用过程就完了,下面做个小结。

     

    小结

        以解答问题的方式来作总结吧,这也是我一开始不清楚的几个问题:

        CUDPP实例和CUDPP计划的功能分别是什么?两者有什么关系?

              CUDPP实例保存了一个CUDPPManager对象的指针,该对象保存了一个CUDA设备的属性值。CUDPP计划会根据配置参数config生成执行原语的最优参数并分配一系列临时显存用于执行原语操作,在计算最优配置参数的过程中需要结合CUDPP实例中的CUDA设备属性,这也是CUDPP实例和CUDPP计划的关系。

        一个利用CUDPP的应用可否具有多个CUDPP实例和CUDPP计划?如何配置?

        CUDPP的功能上可以看出在只具有一个CUDA设备的计算机上,只需要创建一个CUDPP实例,在具有多CUDA设备的计算机上可以实例化多个CUDPP,然后可以对不同的CUDPP计划采用不同的实例,以表示该计划运行于不同的CUDA设备上。

        CUDPP计划当然可以存在多个,不同的计划执行不同的并行原语,每个计划需要独立生成和销毁。

        配置也很简单了,可以基于一个CUDPP实例,生成多个CUDPP计划,每个CUDPP计划必须关联到一个并仅有一个CUDPP实例,可以说CUDPP实例和CUDPP计划的对应关系是1对多的关系。


         本文为个人见解,如有谬误,欢迎大家踊跃拍砖~~
CUDA NV12 转 BGR 是指在使用CUDA(一种由NVIDIA推出的并行计算平台和编程模型)时,将NV12格式的图像数据转换为BGR格式。NV12是一种常用的视频图像编码格式,属于YUV色彩空间的一种,其中Y代表亮度分量,UV代表色度分量。而BGR是一种三通道颜色模型,B代表蓝色,G代表绿色,R代表红色。 在CUDA中进行这样的转换,通常需要编写核函数(Kernel function),在GPU上并行处理图像数据。转换的主要步骤通常包括以下几个方面: 1. 分离亮度(Y)和色度(UV)分量。 2. 对色度分量进行上采样,因为NV12格式中色度分量的分辨率是亮度分量的一半。 3. 将YUV格式转换为RGB格式,这需要通过一系列的矩阵转换和颜色空间转换公式。 4. 在得到RGB格式数据后,可以将其顺序调整为BGR格式。 以下是一个简化的转换过程的伪代码示例: ```c // 假设已经分配了足够的内存空间用于存放Y分量、U分量、V分量以及最终的BGR分量 // YUV数据是按行存储的,其中Y分量占用的空间是U和V分量的两倍 unsigned char* yPlane, *uvPlane; unsigned char* bgrOutput; // 由于色度分量是UV复用的,需要先将UV分量分离 for (int y = 0; y < height; y += 2) { for (int x = 0; x < width; x++) { // Y分量存储 int indexY = y * width + x; // U分量存储 int indexU = (height + y) * (width / 2) + (x / 2); // V分量存储 int indexV = (height + y) * (width / 2) + (x / 2) + 1; // 这里是将UV分量转换为RGB分量的代码,通常涉及到一定的数学计算 // 例如使用U、V以及Y计算R、G、B // ... // 最后,将计算出的R、G、B值转为BGR格式并存储 // ... } } // 注意:实际的转换过程会涉及到更多的计算细节,包括色度分量的插值和色彩空间转换等。 ``` 在实际应用中,可以使用NVIDIA的CUDA Toolkit中提供的库函数,如`ippi`或`cudpp`等,来简化转换过程。同时,确保在计算过程中正确处理图像边界和内存访问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值