CUDA HyperQ 并发内核执行深度解析:simpleHyperQ 示例实战指南
【免费下载链接】cuda-samplesSamples for CUDA Developers which demonstrates features in CUDA Toolkit项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samples
simpleHyperQ 是 NVIDIA CUDA Samples 中用于演示CUDA Stream 多内核并发执行的经典入门示例(位于 cpp/0_Introduction/simpleHyperQ)。它通过在同一批 stream 中交错提交 kernel_A 与 kernel_B,直观对比「支持 HyperQ(SM 3.5+)的设备」与「不支持 HyperQ 的旧设备」在并发能力上的差异。读完本文,你将掌握 HyperQ 的底层原理、CUDA Stream 与 Event 的完整使用流程、基于时钟计数的内核计时方法,以及如何通过运行结果定量判断设备是否真正发挥了并发执行能力。
一、背景:什么是 HyperQ
HyperQ 是 NVIDIA 为 Kepler 及之后架构引入的硬件队列机制。传统 GPU 前端只有单一硬件工作队列,不同 stream 中提交的内核即使互不依赖,也可能因为排队顺序产生伪依赖(false dependency),从而无法真正并行。HyperQ 提供了多个独立硬件队列,允许来自不同 stream 的内核真正并发执行,从而提升 GPU 利用率。
README 对此给出了精确的量化描述:
- 支持 HyperQ 的设备(Compute Capability 3.5 及以上):可同时运行多达32 个内核;
- 不支持 HyperQ 的设备(SM 2.0 / SM 3.0):最多只能并发运行2 个内核(即一个 kernel_A 与一个 kernel_B)。
源码注释 simpleHyperQ.cu 进一步明确了本示例的演示目标:展示 HyperQ 如何让支持设备避免不同 stream 内核之间的伪依赖。示例配套的白皮书 doc/HyperQ.pdf 提供了更深入的硬件机制说明。
二、示例核心思想与程序结构
simpleHyperQ 的核心设计非常巧妙:它创建N 个 stream(默认 N=32),在每个 stream 中依次提交一对完全相同的内核kernel_A与kernel_B。这两个内核除了名字不同,执行内容完全一致,因此:
- 在同一个 stream 内,
kernel_B必然依赖kernel_A(stream 内串行); - 在不同 stream 之间,内核没有任何数据依赖,理论上可以完全并行。
通过测量整批 2×N 个内核的总耗时,即可判断设备的并发能力:
- 若全部串行执行,耗时约为
2 × N × kernel_time; - 若完全并发执行,耗时约为
2 × kernel_time(每个 stream 内部两段串行,所有 stream 并行); - 无 HyperQ 设备只能同时运行 1 个 A + 1 个 B,耗时介于两者之间。
整个程序由三个设备函数/内核和一个主函数构成,全部集中在 simpleHyperQ.cu 单文件中:
| 函数 | 作用 |
|---|---|
clock_block() | 设备函数,通过读取clock()寄存器忙等指定的时钟周期数 |
kernel_A/kernel_B | 两个完全相同的入口内核,包装clock_block(),便于在 profiler 时间线上区分 |
sum() | 单 warp 归约内核,将 2×N 个时钟计数累加为单个值,用于结果校验 |
main() | 设备选择、stream/event 创建、内核提交、计时与验证 |
三、内核耗时控制:基于时钟寄存器的忙等
要让并发效果可测量,内核必须有确定的运行时长。clock_block()利用 CUDA 提供的clock()内建函数读取 GPU 时钟周期数,通过模运算循环忙等:
__device__ void clock_block(clock_t *d_o, clock_t clock_count) { unsigned int start_clock = (unsigned int)clock(); clock_t clock_offset = 0; while (clock_offset < clock_count) { unsigned int end_clock = (unsigned int)clock(); // 利用 2^32 模运算避免时钟回绕问题: // end - start = end + 2^32 - start (mod 2^32) clock_offset = (clock_t)(end_clock - start_clock); } d_o[0] = clock_offset; }这段实现的关键点在于:clock()返回的 32 位时钟计数会回绕,源码通过无符号减法自动利用模算术正确处理了回绕场景(见 simpleHyperQ.cu)。内核以单线程单块(<<<1, 1>>>)方式启动,确保每个内核恰好占用一个 SM 的一个调度槽位,从而精确控制每个内核的硬件资源占用,便于并发调度。
目标时钟周期数由kernel_time(默认 10ms)与设备时钟频率clockRate换算得到:
clock_t time_clocks = (clock_t)(kernel_time * clockRate); // x86_64 等平台 clock_t time_clocks = (clock_t)(kernel_time * (clockRate / 100)); // ARM 平台ARM(__arm__/__aarch64__)平台之所以除以 100,是因为这些架构上内核耗时超过通道复位时间会导致挂起,注释对此有明确说明(simpleHyperQ.cu)。
四、主流程:Stream、Event 与并发提交
1. 参数解析与设备选择
main()首先解析命令行参数,然后选择计算设备:
int nstreams = 32; // 每个内核对占用一个 stream float kernel_time = 10; // 每个内核的目标运行时长(ms) if (checkCmdLineFlag(argc, (const char **)argv, "nstreams")) { nstreams = getCmdLineArgumentInt(argc, (const char **)argv, "nstreams"); } cuda_device = findCudaDevice(argc, (const char **)argv);--nstreams=N可覆盖默认的 32 个 stream;参数解析实现在 Common/helper_string.h,支持--key=value形式;findCudaDevice()实现在 Common/helper_cuda.h:若命令行传入--device=N则使用指定设备,否则自动选择算力最高的设备(基于 SM 数量 × 核心数 × 时钟频率估算的compute_perf)。
随后程序读取设备属性并检查算力,给出明确的硬件能力诊断输出:
if (deviceProp.major < 3 || (deviceProp.major == 3 && deviceProp.minor < 5)) { if (deviceProp.concurrentKernels == 0) { printf("> GPU does not support concurrent kernel execution (SM 3.5 or higher required)\n"); printf(" CUDA kernel runs will be serialized\n"); } else { printf("> GPU does not support HyperQ\n"); printf(" CUDA kernel runs will have limited concurrency\n"); } } printf("> Detected Compute SM %d.%d hardware with %d multi-processors\n", deviceProp.major, deviceProp.minor, deviceProp.multiProcessorCount);这段逻辑对应 README 中"SM 3.5 以上支持 HyperQ、SM 2.0/3.0 最多并发 2 个内核"的表述,并进一步区分了「完全不支持并发」与「支持并发但无 HyperQ」两种退化情形。
2. 内存分配
示例同时使用了页面锁定主机内存与设备内存,展示了两种分配方式:
clock_t *a = 0; checkCudaErrors(cudaMallocHost((void **)&a, sizeof(clock_t))); // 主机端 pinned memory clock_t *d_a = 0; checkCudaErrors(cudaMalloc((void **)&d_a, 2 * nstreams * sizeof(clock_t))); // 设备端cudaMallocHost分配页面锁定内存,配合cudaMemcpy可获得更高的拷贝带宽,本示例用它接收最终归约结果;- 设备内存
d_a为每个内核预留一个clock_t槽位(共2 × nstreams个),用于收集各内核实际消耗的时钟数。
3. Stream 与 Event 的创建
cudaStream_t *streams = (cudaStream_t *)malloc(nstreams * sizeof(cudaStream_t)); for (int i = 0; i < nstreams; i++) { checkCudaErrors(cudaStreamCreate(&(streams[i]))); } cudaEvent_t start_event, stop_event; checkCudaErrors(cudaEventCreate(&start_event)); checkCudaErrors(cudaEventCreate(&stop_event));N 个 stream 各自独立,互不阻塞;两个事件分别标记整批任务的起点与终点。
4. 并发提交内核
核心提交循环如下(simpleHyperQ.cu):
checkCudaErrors(cudaEventRecord(start_event, 0)); for (int i = 0; i < nstreams; ++i) { kernel_A<<<1, 1, 0, streams[i]>>>(&d_a[2 * i], time_clocks); total_clocks += time_clocks; kernel_B<<<1, 1, 0, streams[i]>>>(&d_a[2 * i + 1], time_clocks); total_clocks += time_clocks; } checkCudaErrors(cudaEventRecord(stop_event, 0));- 每个 stream 内先提交
kernel_A再提交kernel_B,形成 stream 内依赖; - 所有 stream 的提交在 CPU 侧一次性完成(异步),CPU 随即可以继续做其他工作;
cudaEventRecord(stop_event, 0)被放入默认 stream(stream 0),由于默认 stream 与所有非默认 stream 之间存在隐式同步,stop_event 保证在所有已提交内核完成后触发,这是 CUDA 中经典的"等待全部完成"模式。
5. 结果收集与计时
sum<<<1, 32>>>(d_a, 2 * nstreams); checkCudaErrors(cudaMemcpy(a, d_a, sizeof(clock_t), cudaMemcpyDeviceToHost)); checkCudaErrors(cudaEventSynchronize(stop_event)); checkCudaErrors(cudaEventElapsedTime(&elapsed_time, start_event, stop_event));sum内核利用cooperative groups的cg::thread_block与cg::sync()实现线程块内同步(代码中特意注明"为了简洁未做优化"),将 2×N 个时钟计数单 warp 归约到单个值。cudaEventElapsedTime给出以毫秒为单位的实测总耗时。
五、运行结果解读:如何判断并发效果
程序会在退出前打印三段对比数据(simpleHyperQ.cu):
Expected time for serial execution of 32 sets of kernels is between approx. 0.330s and 0.660s Expected time for fully concurrent execution of 32 sets of kernels is approx. 0.020s Measured time for sample = X.XXXs数学依据如下(nstreams=32、kernel_time=10ms):
- 全串行:32 对内核依次执行,耗时约
2 × 32 × 10ms = 640ms(下界 330ms 考虑了部分重叠); - 完全并发(HyperQ):32 个 stream 并行,每个 stream 内 A→B 两段串行,耗时约
2 × 10ms = 20ms; - 实测值越接近 20ms,说明 HyperQ 的并发调度越充分;实测值越大,说明设备并发能力受限(旧设备或资源不足)。
程序还以bTestResult = (a[0] >= total_clocks)作为回归验证:归约得到的实际总时钟数必须不小于理论请求的时钟总数,否则返回EXIT_FAILURE,确保内核确实跑满了预期时长(simpleHyperQ.cu)。
运行完成后释放全部资源:销毁 N 个 stream、两个 event,并释放 pinned 内存与设备内存。
六、涉及的 CUDA Runtime API 一览
README 明确列出本示例用到的全部 CUDA Runtime API,对应源码中的实际调用位置如下:
| API | 用途 | 源码位置 |
|---|---|---|
cudaStreamCreate/cudaStreamDestroy | 创建/销毁 stream | simpleHyperQ.cu L164 / L225 |
cudaMalloc/cudaFree | 设备内存分配/释放 | L158 / L232 |
cudaMallocHost/cudaFreeHost | 页面锁定主机内存 | L154 / L231 |
cudaEventCreate/cudaEventDestroy | 事件创建/销毁 | L169-L170 / L229-L230 |
cudaEventRecord | 在 stream 中记录事件 | L184 / L195 |
cudaEventSynchronize | 阻塞等待事件完成 | L207 |
cudaEventElapsedTime | 计算两个事件间耗时(毫秒) | L208 |
cudaMemcpy | 设备到主机拷贝 | L203 |
cudaGetDevice/cudaGetDeviceProperties | 获取设备信息 | L127-L128 |
cudaDeviceGetAttribute | 查询时钟频率等属性 | L132 |
七、构建与运行
示例的构建由 CMakeLists.txt 定义,并被 cpp/0_Introduction/CMakeLists.txt 的add_subdirectory(simpleHyperQ)纳入 0_Introduction 总构建。构建配置要点:
- 需要CMake 3.20+与
find_package(CUDAToolkit REQUIRED); - 默认 CUDA 架构列表为
75 80 86 87 89 90 100 110 120,与 README 声明的支持架构(SM 5.0 ~ SM 9.0)相对应; - 默认开启
-lineinfo(若设ENABLE_CUDA_DEBUG则改用-G以支持 cuda-gdb),并启用CUDA_SEPARABLE_COMPILATION; - 头文件搜索路径包含 Common 目录,其中 helper_cuda.h 提供
checkCudaErrors错误检查宏与设备选择逻辑,helper_functions.h 提供计时等辅助函数。
构建与运行方式(在仓库根目录下):
cmake -S cpp/0_Introduction/simpleHyperQ -B build/simpleHyperQ cmake --build build/simpleHyperQ ./build/simpleHyperQ/simpleHyperQ # 默认 32 个 stream ./build/simpleHyperQ/simpleHyperQ --nstreams=16 # 指定 stream 数量 ./build/simpleHyperQ/simpleHyperQ --device=0 # 指定 GPU支持环境(与 README 声明一致):操作系统为 Linux 与 Windows;CPU 架构支持 x86_64 与 armv7l;运行前需先安装对应平台的 CUDA Toolkit。
八、延伸思考:与仓库中其他 Stream 示例的关系
simpleHyperQ 属于 0_Introduction 中"CUDA Systems Integration, Performance Strategies"主题,其核心知识点——用多 stream 隐藏内核间延迟——在仓库中有多处进阶应用:
- simpleStreams 演示多 stream 并发基础用法;
- simpleMultiCopy 与 simpleHyperQ 展示 stream 在数据拷贝与内核执行上的重叠;
- streamOrderedAllocation 则进一步利用 stream 序内存分配实现更细粒度的并发控制。
从源码结构看,这些示例共同构成了一条从"stream 基础"到"stream 驱动性能优化"的学习路径,simpleHyperQ 是其中最能直观量化并发收益的一个——只需运行一次并对比"期望耗时"与"实测耗时"即可验证设备的真实并发能力。
【免费下载链接】cuda-samplesSamples for CUDA Developers which demonstrates features in CUDA Toolkit项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考