Home CUDA Features 4.18:CUDA Dynamic Parallelism(CDP2)
Post
Cancel

CUDA Features 4.18:CUDA Dynamic Parallelism(CDP2)

本文逐节对应 CUDA Programming Guide v13.3 的 4.18 CUDA Dynamic Parallelism。本章描述 CUDA 12.0+ 默认的 CDP2;很多旧教程讲的是 CDP1,尤其是 device-side cudaDeviceSynchronize,不能直接套用。

4.18.1 引言

4.18.1.1 概览

CUDA Dynamic Parallelism(CDP)允许正在 GPU 上执行的 thread 发射新的 grid。递归细分、数据依赖的自适应并行和不规则任务生成可以在设备端做决策,避免先把控制权和判定数据交还 CPU。

CUDA 12.0+ 默认使用 CDP2;计算能力 9.0+ 只支持 CDP2。较旧设备可用编译宏 -DCUDA_FORCE_CDP1_IF_SUPPORTED 选择 legacy CDP1,但 CDP1 计划在未来移除。

CDP 提供表达能力,不保证比扁平化 kernel、persistent kernel、work queue 或 device graph 更快。每个 child launch 都有运行时与资源管理成本。

4.18.2 执行环境

4.18.2.1 Parent 与 Child Grid

发起 device-side launch 的线程属于 parent grid,新 invocation 是 child grid。关系 properly nested:parent grid 在它所有线程发起的 child work 完成前,不会被视为完成。因此 host 等待 parent 完成,也间接等待其后代完成。

proper nesting 不表示 parent thread 可以在任意位置同步 child 并读取结果。CDP2 删除了 device-side cudaDeviceSynchronize;child 结果在 parent 结束前要由 tail launch kernel 消费。

4.18.2.2 CUDA Primitive 的作用域

Device Runtime 提供一组类似 Runtime API 的设备端接口,但能力有限。由 device API 创建的 stream/event 对创建它们的 grid 内所有线程可见,只在该 grid scope 有效。

以下跨界使用均未定义:

  • 把 host 创建的 stream handle 传入 kernel 使用;
  • parent grid 创建 stream 后让 child grid 使用;
  • 把一个 grid 创建的 event handle 交给另一个 grid;
  • grid 结束后继续使用其 device-created CUDA object。

4.18.2.3 Streams 与 Events

同一显式 stream 中 child launch 有序,不同 stream 可建立 event 依赖。stream/event handle 虽由某一线程创建,但 grid 内其他线程可使用,所以对象表是 grid-scope 而不是 thread-scope。

grid 退出时,所有由其发射的工作按 proper nesting 隐式完成,包含各 stream 的依赖。event handle 不保证跨 grid 唯一,数值相同也不能越域使用。

CDP2 还提供特殊 stream:

  • cudaStreamTailLaunch:当前 grid 及其 child 工作完成后执行,用于接续和消费 child 结果;
  • cudaStreamFireAndForget:派生工作不按普通同 stream 顺序等待发起点,但仍受 execution environment 规则管理。

4.18.2.4 顺序与并发

多个 parent 线程向同一显式 stream 发射时,最终顺序取决于线程实际调度;若业务要求确定顺序,先用 block/grid 可用同步选出单一发射者或建立事件顺序。

device-side 隐式 NULL stream 的范围是 thread block:同一 block 多线程向 NULL stream 发射会有序,不同 block 的 NULL streams 可以并行。想让同一 block 的多个发射并发,应创建显式 named streams。

不同 stream 只允许并发,不保证并发。程序不能让两个 child grid 互等,并假设硬件一定同时驻留来解除死锁。

4.18.3 内存一致性

parent/child 共享 global 和 constant storage,但各自有独立 shared 与 local memory。映射内存可用相同 pointer;texture 可读共享 backing,但有独立一致性边界。

Memory spaceParent/child 使用同一指针
global
mapped
local
shared
texture是,只读访问路径

4.18.3.1 Global Memory

child launch 时是明确一致性点:发射线程在 launch 之前可见的 global writes 对 child 可见。其他 parent 线程先写的数据,需要先通过 __syncthreads() 等让发射线程看到,才能保证 child 看见。

launch 之后 parent 与 child 并发访问同一位置,没有额外同步就是 race。CDP2 parent 不能显式等待 child,因此 child 修改不能保证被继续运行的 parent 线程看到。

需要在 parent grid 完成前消费 child 结果时,把 consumer kernel 发射到 cudaStreamTailLaunch

1
2
3
4
__global__ void parent(State* s) {
  child<<<child_grid, child_block>>>(s);
  consume_child<<<1, 256, 0, cudaStreamTailLaunch>>>(s);
}

tail kernel 不是 parent thread 的继续执行,而是一个后继 grid,因此资源和逻辑应按新的 kernel 边界组织。

4.18.3.2 Mapped Memory

zero-copy/mapped host memory 由 parent 和 child 通过 global pointer 访问。它遵守相同 launch 可见性和 race 规则,还要考虑 system scope 与主机并发访问。host 不能因为 child 已发射就提前读取结果,应等待顶层 parent 完成或相应 host-visible 事件。

4.18.3.3 Shared Memory

每个 grid 的每个 block 有自己的 shared memory。parent shared pointer 不能传给 child,child 也不可能访问 parent block 的 shared lifetime。需要共享的数据必须先写入 global/mapped memory。

编译器和 runtime 会拒绝一些可识别的 shared pointer 参数,但不要依赖所有非法情况都能静态检测。

4.18.3.4 Local Memory

局部数组、寄存器溢出和线程栈位于 parent thread 的 local memory,不属于 child。把设备函数局部变量地址传给 child 是错误的,即使 C++ 类型只是普通 pointer。

安全规则是:传给 child 的存储显式来自 global heap(合适的设备分配/设备端 new)或全局 __device__ storage。

4.18.3.4.1 Texture Memory

child launch 时,launch 前对 texture backing global memory 的写可反映到 child texture 读取。child 写 global backing 后,parent 的 texture path 在 parent grid 内不保证看到新值;同样应由 tail kernel 继续消费。parent/child 并发对 backing 写与 texture 读可能不一致。

4.18.4 编程接口

4.18.4.1 基础

Device code 使用普通 <<<grid, block, shared, stream>>> 语法发射 child。编译通常需要 relocatable device code(-rdc=true)并链接 cudadevrt

launch 后检查 device-side cudaGetLastError() 只能报告提交相关错误;child 执行期错误最终仍需在上层同步/错误检查中观察。

4.18.4.2 C++ Device Runtime

Device Runtime API 是逐线程调用,允许位于分歧分支,不要求整个 block 同时调用,因此本身不会因为只有一个线程 launch 就造成 collective deadlock。大量线程各自发射 child 会产生大量 launch,应主动聚合。

4.18.4.2.1 Device-Side Kernel Launch

Dg/Db 是 grid/block dim3Ns 为每 block 动态 shared memory 字节,S 必须是同一 grid 内创建的合法 stream,省略时用对应 NULL stream。

device-side launch 对发起线程异步返回。child 可能很快开始,也可能直到 parent 到达隐式 launch synchronization point 才开始。独立 stream 并发仍是机会性的。

child 继承全局设备配置,例如 cache config 与 device limits;不能在 child launch 参数中把它变成另一 device 的工作。

4.18.4.2.2 Events

device-created event 可记录 child stream 进度并让另一 device-created stream wait。event 不能跨 grid 传递,且只表达其合法 scope 内的依赖。

4.18.4.2.3 同步

CDP2 没有 parent thread 显式等待 child 的通用 API。若一个 parent thread 要依赖其他 parent 线程发射的 child,可用 grid 内共享 stream/event 组织 child 之间的顺序,但 parent thread 本身仍不能因此直接读取 child 结果。

可见 child 修改的合法设备端后继是 tail launch grid;最终 host 可在 parent completion 后读取。

4.18.4.2.4 Device Management

device runtime 只能控制当前执行 device,不支持 device-side cudaSetDevice()cudaGetDevice() 与 host 所见 ordinal 一致。可用 cudaDeviceGetAttribute 按 ID 查询属性,但 device runtime 不提供整体 cudaGetDeviceProperties()

4.18.5 编程指南

4.18.5.1 性能

CDP 适合每次 child 有足够工作量、并行形状只有设备端才能有效决定的场景。若每个数据元素都发射一个极小 child,launch/跟踪开销会压倒计算。

常见替代方案:单 kernel 内循环、全局 work queue、persistent kernel、prefix sum 后批量发射、CUDA Graph 条件节点或 device graph launch。应以端到端时间和代码复杂度比较。

4.18.5.1.1 启用 CDP 的 Kernel 开销

链接/使用 device runtime 会启用 dynamic launch 跟踪软件,其开销可能影响同时运行的 kernel,即使某个 kernel 自己没有 launch child。因此 benchmark 要比较未链接 CDP 的基线,而不只是比较 parent 内某个分支。

4.18.5.2 实现限制

嵌套深度、pending launches、device runtime 内存、stream/event 数和参数存储都受实现资源限制。语义得到保证,不表示规模可以无限增长。

4.18.5.2.1 Runtime

Device runtime 会为 launch tracking 预留内存,尤其是 pending grid launch pool。每个 pending launch 的配置和参数要保存到 child 完成。

host 可通过 cudaDeviceSetLimit(cudaLimitDevRuntimePendingLaunchCount, n) 调整固定 launch pool。提高它增加内存占用,降低它可能使发射在压力下失败。应用要检查 launch error 并控制任务生成速率。

4.18.5.3 兼容与互操作

CDP2 函数不能 launch CDP1 函数,反之亦然;调用图混用会在 module load 得到 cudaErrorCdpVersionMismatch。计算能力 9.0+ 上 device-side cudaDeviceSynchronize 不可用。

旧设备上强制 CDP1 的代码也不能引用 CDP2 的 tail/fire-and-forget stream。Fatbin/JIT 在不同架构上可能暴露版本不匹配,部署测试必须覆盖实际 GPU。

4.18.6 从 PTX Device Launch

这一节主要面向编译器和语言实现者。低层需要 cudaGetParameterBuffer 取得参数区,按 ABI 布局写参数,再用 cudaLaunchDevice 提交 kernel pointer、参数 buffer、grid/block、shared bytes 和 stream。

4.18.6.1 Kernel Launch API

4.18.6.1.1 cudaLaunchDevice

PTX 需按 32/64 位 address size 声明正确函数签名。实现位于 cudadevrt,必须链接。无参数 kernel 的 parameter buffer 可为 null。

4.18.6.1.2 cudaGetParameterBuffer

调用传入最大参数对齐和总字节数。当前实现返回 64 字节对齐 buffer,虽然 alignment 参数当前可能被忽略,仍应传真实要求保证未来兼容。

4.18.6.2 Parameter Buffer Layout

参数按 CUDA kernel ABI 顺序放入 buffer,每个参数 offset 对齐到自身 alignment,最终 buffer 覆盖全部参数。编译器实现不能简单无 padding 地拼接字段;结构体、向量类型和指针宽度尤其需要按目标 ABI 计算。

总结:CDP2 让 GPU 动态生成嵌套 grid,并用 tail launch 替代 parent 内显式等待;正确设计必须同时处理 grid-scope 对象、launch 时一致性、global-only 数据传递和有限 launch pool。

上一篇:4.17 Extended GPU Memory · 下一篇:4.19 CUDA Interoperability

This post is licensed under CC BY 4.0 by the author.

CUDA Features 4.17:Extended GPU Memory(EGM)

CUDA Features 4.19:CUDA 与图形及外部 API 互操作