CUDA HyperQ 并发内核执行深度解析:simpleHyperQ 示例实战指南
CUDA HyperQ 并发内核执行深度解析simpleHyperQ 示例实战指南【免费下载链接】cuda-samplesSamples for CUDA Developers which demonstrates features in CUDA Toolkit项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samplessimpleHyperQ 是 NVIDIA CUDA Samples 中用于演示CUDA Stream 多内核并发执行的经典入门示例位于 cpp/0_Introduction/simpleHyperQ。它通过在同一批 stream 中交错提交 kernel_A 与 kernel_B直观对比「支持 HyperQSM 3.5的设备」与「不支持 HyperQ 的旧设备」在并发能力上的差异。读完本文你将掌握 HyperQ 的底层原理、CUDA Stream 与 Event 的完整使用流程、基于时钟计数的内核计时方法以及如何通过运行结果定量判断设备是否真正发挥了并发执行能力。一、背景什么是 HyperQHyperQ 是 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默认 N32在每个 stream 中依次提交一对完全相同的内核kernel_A与kernel_B。这两个内核除了名字不同执行内容完全一致因此在同一个 stream 内kernel_B必然依赖kernel_Astream 内串行在不同 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);--nstreamsN可覆盖默认的 32 个 stream参数解析实现在 Common/helper_string.h支持--keyvalue形式findCudaDevice()实现在 Common/helper_cuda.h若命令行传入--deviceN则使用指定设备否则自动选择算力最高的设备基于 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.cucheckCudaErrors(cudaEventRecord(start_event, 0)); for (int i 0; i nstreams; i) { kernel_A1, 1, 0, streams[i](d_a[2 * i], time_clocks); total_clocks time_clocks; kernel_B1, 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)被放入默认 streamstream 0由于默认 stream 与所有非默认 stream 之间存在隐式同步stop_event 保证在所有已提交内核完成后触发这是 CUDA 中经典的等待全部完成模式。5. 结果收集与计时sum1, 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.cuExpected 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数学依据如下nstreams32、kernel_time10ms全串行32 对内核依次执行耗时约2 × 32 × 10ms 640ms下界 330ms 考虑了部分重叠完全并发HyperQ32 个 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创建/销毁 streamsimpleHyperQ.cu L164 / L225cudaMalloc/cudaFree设备内存分配/释放L158 / L232cudaMallocHost/cudaFreeHost页面锁定主机内存L154 / L231cudaEventCreate/cudaEventDestroy事件创建/销毁L169-L170 / L229-L230cudaEventRecord在 stream 中记录事件L184 / L195cudaEventSynchronize阻塞等待事件完成L207cudaEventElapsedTime计算两个事件间耗时毫秒L208cudaMemcpy设备到主机拷贝L203cudaGetDevice/cudaGetDeviceProperties获取设备信息L127-L128cudaDeviceGetAttribute查询时钟频率等属性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 --nstreams16 # 指定 stream 数量 ./build/simpleHyperQ/simpleHyperQ --device0 # 指定 GPU支持环境与 README 声明一致操作系统为 Linux 与 WindowsCPU 架构支持 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),仅供参考