找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4068

积分

0

好友

559

主题
发表于 19 小时前 | 查看: 5| 回复: 0

夕阳帆船航行于海面

一、CUDA中图的应用

图在各类算法中有着广泛的应用,而并行编程本身也可以被视为一种特定的算法处理模式。在了解了CUDA图的基本概念之后,我们接下来需要深入探讨的是如何在CUDA中构建图。掌握图的构建原理与过程,是为后续图的实例化、生成与执行打下坚实基础的关键一步。只有这样,我们才能将图的概念真正融入到并行编程的实践中。

二、CUDA中图的创建

在CUDA中,图的创建指的是在定义或创建阶段,程序需要定义图中各个操作的描述以及它们之间的依赖关系。更直白地说,就是根据预先设定的节点说明和操作描述,来创建具体的图节点并确立它们之间的依赖。这就像是先在纸上画出流程图及其关联,然后再将这幅“图”映射到计算机中。

CUDA提供了两种主要的图创建方式:

  1. 直接使用API创建
    CUDA提供了诸如 cudaGraphCreate()cudaGraphAddNode() 等接口来直接创建图并添加节点。更详细的API使用方法,建议查阅官方文档以深入学习。

    // Create the graph - it starts out empty
    cudaGraphCreate(&graph, 0);
    
    // Create the nodes and their dependencies
    cudaGraphNode_t nodes[4];
    cudaGraphNodeParams kParams = { cudaGraphNodeTypeKernel };
    kParams.kernel.func         = (void *)kernelName;
    kParams.kernel.gridDim.x    = kParams.kernel.gridDim.y  = kParams.kernel.gridDim.z  = 1;
    kParams.kernel.blockDim.x   = kParams.kernel.blockDim.y = kParams.kernel.blockDim.z = 1;
    
    cudaGraphAddNode(&nodes[0], graph, NULL, NULL, 0, &kParams);
    cudaGraphAddNode(&nodes[1], graph, &nodes[0], NULL, 1, &kParams);
    cudaGraphAddNode(&nodes[2], graph, &nodes[0], NULL, 1, &kParams);
    cudaGraphAddNode(&nodes[3], graph, &nodes[1], NULL, 2, &kParams);
  2. 通过流捕获的方式
    CUDA还支持通过捕获现有的、基于流的API调用来创建图。相比于直接创建,这种方式更像是“重用”已有的工作流来生成图,这更贴近现实场景:我们既可以从头绘制一幅新图,也可以在现有草图的基础上整合完善。主要接口包括 cudaStreamBeginCapture()cudaStreamEndCapture()

    cudaGraph_t graph;
    
    cudaStreamBeginCapture(stream);
    
    kernel_A<<< ..., stream >>>(...);
    kernel_B<<< ..., stream >>>(...);
    libraryCall(stream);
    kernel_C<<< ..., stream >>>(...);
    
    cudaStreamEndCapture(stream, &graph);

    上述代码示例来源于官方文档。

三、流捕获创建图的说明

使用流捕获来创建图时,有以下几点需要特别注意:

  1. 跨流的依赖和事件
    当使用 cudaEventRecordcudaStreamWaitEvent 在流捕获中处理跨流依赖时,如果等待的事件记录在同一个被捕获的图中,那么这种处理是有效的。类似地,在捕获模式下记录事件,相当于捕获了图中的一个节点集。
    当一个流等待一个捕获事件,但自身尚未进入捕获模式时,它会被置为捕获模式,并在其下一个操作项处处理对该捕获事件节点的依赖。此时,这两个流将被纳入同一张图中。
    如果流捕获中存在跨流依赖,则必须在调用 cudaStreamBeginCapture 的同一流中调用 cudaStreamEndCapture。对于基于事件的依赖,事件也必须将任何其他流连接回原始流。调用 cudaStreamEndCapture 后,所有被捕获到同一图中的流都将退出捕获模式。如果无法将流重新加入原始流,将导致整个捕获操作失败。
    需要注意的是,当一个流退出捕获模式时,该流中下一个未捕获的操作项(如果存在)仍将依赖于最近一个先前的未捕获操作项,尽管中间的操作项已被移除。

  2. 特殊的操作与限制
    在流捕获期间,某些操作是被禁止或不被处理的。例如,同步或查询正处于捕获状态的流或事件的执行状态是无效的;同样,当流处于捕获模式时,查询或同步包含该活动流捕获的更广泛句柄(如设备或上下文)的执行状态也是无效的。
    在捕获同一上下文中非 cudaStreamNonBlocking 创建的流时,尝试操作传统流(legacy stream)是无效的。向传统流添加工作会创建对被捕获流的依赖,而查询或同步传统流则等同于查询或同步那些被捕获的流。
    通常,当一个依赖关系试图连接已捕获的内容和未捕获且已排队的内容时,CUDA会返回错误。但在流进入或退出捕获模式的转换时刻,会移除转换前后立即添加到流中的操作项之间的依赖关系,这是一种特殊情况。
    试图通过等待一个来自不同捕获图的事件,来合并两个独立的捕获图,是无效的。同样,在没有指定 cudaEventWaitExternal 标志的情况下,从一个被捕获的流中等待一个未捕获的事件,也是无效的。
    目前,少数向流中排队异步操作的API不支持在图中使用,如果在流被捕获期间调用它们(例如 cudaStreamAttachMemAsync),将会返回错误。

  3. 操作失效
    在流捕获期间尝试任何无效操作,都会使正在进行的捕获图变为无效状态。与之关联的流和事件也将保持无效,并持续返回错误,直到 cudaStreamEndCapture 被调用以退出捕获模式,届时函数将返回错误值和 NULL 图句柄。

  4. 内省捕获
    可以使用 cudaStreamGetCaptureInfo 函数来检查活动的流捕获操作。它允许获取捕获状态、捕获的唯一(每个进程)ID、底层图对象以及流中要捕获的下一个节点的依赖关系(边)数据。这些依赖信息可用于获取流中最后捕获的节点的句柄。

四、应用实例

下面给出两个创建图的简单例程:

  1. API直接创建示例

    void cudaGraphsManual(float  *inputVec_h,
                          float  *inputVec_d,
                          double *outputVec_d,
                          double *result_d,
                          size_t  inputSize,
                          size_t  numOfBlocks)
    {
       cudaStream_t                 streamForGraph;
       cudaGraph_t                  graph;
       std::vector<cudaGraphNode_t> nodeDependencies;
       cudaGraphNode_t              memcpyNode, kernelNode, memsetNode;
       double                       result_h = 0.0;
    
       cudaStreamCreate(&streamForGraph);
    
       cudaKernelNodeParams kernelNodeParams = {0};
       cudaMemcpy3DParms    memcpyParams     = {0};
       cudaMemsetParams     memsetParams     = {0};
    
       memcpyParams.srcArray = NULL;
       memcpyParams.srcPos   = make_cudaPos(0, 0, 0);
       memcpyParams.srcPtr   = make_cudaPitchedPtr(inputVec_h, sizeof(float) * inputSize, inputSize, 1);
       memcpyParams.dstArray = NULL;
       memcpyParams.dstPos   = make_cudaPos(0, 0, 0);
       memcpyParams.dstPtr   = make_cudaPitchedPtr(inputVec_d, sizeof(float) * inputSize, inputSize, 1);
       memcpyParams.extent   = make_cudaExtent(sizeof(float) * inputSize, 1, 1);
       memcpyParams.kind     = cudaMemcpyHostToDevice;
    
       memsetParams.dst         = (void *)outputVec_d;
       memsetParams.value       = 0;
       memsetParams.pitch       = 0;
       memsetParams.elementSize = sizeof(float); // elementSize can be max 4 bytes
       memsetParams.width       = numOfBlocks * 2;
       memsetParams.height      = 1;
    
       cudaGraphCreate(&graph, 0);
       cudaGraphAddMemcpyNode(&memcpyNode, graph, NULL, 0, &memcpyParams);
       cudaGraphAddMemsetNode(&memsetNode, graph, NULL, 0, &memsetParams);
    
       nodeDependencies.push_back(memsetNode);
       nodeDependencies.push_back(memcpyNode);
    
       void *kernelArgs[4] = {(void *)&inputVec_d, (void *)&outputVec_d, &inputSize, &numOfBlocks};
    
       kernelNodeParams.func           = (void *)reduce;
       kernelNodeParams.gridDim        = dim3(numOfBlocks, 1, 1);
       kernelNodeParams.blockDim       = dim3(THREADS_PER_BLOCK, 1, 1);
       kernelNodeParams.sharedMemBytes = 0;
       kernelNodeParams.kernelParams   = (void **)kernelArgs;
       kernelNodeParams.extra          = NULL;
    
       cudaGraphAddKernelNode(
          &kernelNode, graph, nodeDependencies.data(), nodeDependencies.size(), &kernelNodeParams);
    
       nodeDependencies.clear();
       nodeDependencies.push_back(kernelNode);
    
       memset(&memsetParams, 0, sizeof(memsetParams));
       memsetParams.dst         = result_d;
       memsetParams.value       = 0;
       memsetParams.elementSize = sizeof(float);
       memsetParams.width       = 2;
       memsetParams.height      = 1;
       cudaGraphAddMemsetNode(&memsetNode, graph, NULL, 0, &memsetParams);
    
       nodeDependencies.push_back(memsetNode);
    
       memset(&kernelNodeParams, 0, sizeof(kernelNodeParams));
       kernelNodeParams.func           = (void *)reduceFinal;
       kernelNodeParams.gridDim        = dim3(1, 1, 1);
       kernelNodeParams.blockDim       = dim3(THREADS_PER_BLOCK, 1, 1);
       kernelNodeParams.sharedMemBytes = 0;
       void *kernelArgs2[3]            = {(void *)&outputVec_d, (void *)&result_d, &numOfBlocks};
       kernelNodeParams.kernelParams   = kernelArgs2;
       kernelNodeParams.extra          = NULL;
    
       cudaGraphAddKernelNode(
          &kernelNode, graph, nodeDependencies.data(), nodeDependencies.size(), &kernelNodeParams);
    
       nodeDependencies.clear();
       nodeDependencies.push_back(kernelNode);
    
       memset(&memcpyParams, 0, sizeof(memcpyParams));
    
       memcpyParams.srcArray = NULL;
       memcpyParams.srcPos   = make_cudaPos(0, 0, 0);
       memcpyParams.srcPtr   = make_cudaPitchedPtr(result_d, sizeof(double), 1, 1);
       memcpyParams.dstArray = NULL;
       memcpyParams.dstPos   = make_cudaPos(0, 0, 0);
       memcpyParams.dstPtr   = make_cudaPitchedPtr(&result_h, sizeof(double), 1, 1);
       memcpyParams.extent   = make_cudaExtent(sizeof(double), 1, 1);
       memcpyParams.kind     = cudaMemcpyDeviceToHost;
    
       cudaGraphAddMemcpyNode(&memcpyNode, graph, nodeDependencies.data(), nodeDependencies.size(), &memcpyParams);
       nodeDependencies.clear();
       nodeDependencies.push_back(memcpyNode);
    
       cudaGraphNode_t    hostNode;
       cudaHostNodeParams hostParams = {0};
       hostParams.fn                 = myHostNodeCallback;
       callBackData_t hostFnData;
       hostFnData.data     = &result_h;
       hostFnData.fn_name  = “cudaGraphsManual”;
       hostParams.userData = &hostFnData;
    
       cudaGraphAddHostNode(&hostNode, graph, nodeDependencies.data(), nodeDependencies.size(), &hostParams);
    }
  2. 流捕获创建示例

    void cudaGraphsUsingStreamCapture(float  *inputVec_h,
                                      float  *inputVec_d,
                                      double *outputVec_d,
                                      double *result_d,
                                      size_t  inputSize,
                                      size_t  numOfBlocks)
    {
       cudaStream_t stream1, stream2, stream3, streamForGraph;
       cudaEvent_t  forkStreamEvent, memsetEvent1, memsetEvent2;
       cudaGraph_t  graph;
       double       result_h = 0.0;
    
       cudaStreamCreate(&stream1);
       cudaStreamCreate(&stream2);
       cudaStreamCreate(&stream3);
       cudaStreamCreate(&streamForGraph);
    
       cudaEventCreate(&forkStreamEvent);
       cudaEventCreate(&memsetEvent1);
       cudaEventCreate(&memsetEvent2);
    
       cudaStreamBeginCapture(stream1, cudaStreamCaptureModeGlobal);
    
       cudaEventRecord(forkStreamEvent, stream1);
       cudaStreamWaitEvent(stream2, forkStreamEvent, 0);
       cudaStreamWaitEvent(stream3, forkStreamEvent, 0);
    
       cudaMemcpyAsync(inputVec_d, inputVec_h, sizeof(float) * inputSize, cudaMemcpyDefault, stream1);
    
       cudaMemsetAsync(outputVec_d, 0, sizeof(double) * numOfBlocks, stream2);
    
       cudaEventRecord(memsetEvent1, stream2);
    
       cudaMemsetAsync(result_d, 0, sizeof(double), stream3);
       cudaEventRecord(memsetEvent2, stream3);
    
       cudaStreamWaitEvent(stream1, memsetEvent1, 0);
    
       reduce<<<numOfBlocks, THREADS_PER_BLOCK, 0, stream1>>>(inputVec_d, outputVec_d, inputSize, numOfBlocks);
    
       cudaStreamWaitEvent(stream1, memsetEvent2, 0);
    
       reduceFinal<<<1, THREADS_PER_BLOCK, 0, stream1>>>(outputVec_d, result_d, numOfBlocks);
       cudaMemcpyAsync(&result_h, result_d, sizeof(double), cudaMemcpyDefault, stream1);
    
       callBackData_t hostFnData = {0};
       hostFnData.data           = &result_h;
       hostFnData.fn_name        = “cudaGraphsUsingStreamCapture”;
       cudaHostFn_t fn           = myHostNodeCallback;
       cudaLaunchHostFunc(stream1, fn, &hostFnData);
       cudaStreamEndCapture(stream1, &graph);
    }

    以上代码均来自官方文档,读者可以在此基础上进行完善和运行。需要注意的是,图功能需要CUDA 11及以上版本支持。如果你想运行上面的代码,请确保更新到相应的CUDA版本。由于图API本身也在不断迭代更新,如果在编译时遇到问题,请检查是否使用了不匹配的API接口。

五、总结

在CUDA图的应用中,创建是基础。只有正确、合理地掌握了图的构建方法,才能对图后续的各种高级应用有全面的掌控力。正所谓万丈高楼平地起,熟练掌握图的创建,就是在为构建高性能的并行计算应用打下坚实的根基。对于希望深入GPU并行计算领域的 C++ 开发者而言,理解并熟练运用CUDA图是一个重要的技能提升方向。




上一篇:国产开源工作流EsFlow:零代码拖拽,8张表搞定企业审批流程
下一篇:GESP C++三级真题精讲:二进制回文串的两种解法与位运算技巧
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-16 21:26 , Processed in 0.606321 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表