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

2598

积分

0

好友

362

主题
发表于 5 天前 | 查看: 20| 回复: 0

一、CUDA中的事件

在众多编程语言或框架中,你可能都接触过“事件”这个概念。在 CUDA 编程中,事件同样扮演着核心角色,但其含义更贴近字面本身。它本质上是一种标志,用于密切监视设备的执行进度,是一个强大的同步工具。

同时,CUDA 事件也是执行精确计时的利器。它允许应用程序在代码的任何位置异步地记录事件,并查询这些事件何时完成。当事件之前的所有任务(或者可选地,特定流中的所有命令)都已完成时,该事件即被标记为完成。对于在流零(即默认流)中记录的事件,则是在所有流的所有先前任务和命令完成后才会完成。

因此,在 CUDA 编程中,事件身兼两职:既是高效的同步工具,也是测量执行时间的精密仪器。

二、CUDA事件主要应用场景

理解了事件的基本定义后,它在实际编程中有哪些用武之地呢?

  1. 性能测量
    事件可以精确测量内核函数的执行耗时,也常被用于测量涉及多个流、计算与数据传输重叠等复杂操作的整体性能。

    cudaEventRecord(start, 0);
    for (int i = 0; i < 2; ++i) {
        cudaMemcpyAsync(inputDev + i * size, inputHost + i * size,
                        size, cudaMemcpyHostToDevice, stream[i]);
        MyKernel<<<100, 512, 0, stream[i]>>>
                   (outputDev + i * size, inputDev + i * size, size);
        cudaMemcpyAsync(outputHost + i * size, outputDev + i * size,
                        size, cudaMemcpyDeviceToHost, stream[i]);
    }
    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    float elapsedTime;
    cudaEventElapsedTime(&elapsedTime, start, stop);
  2. 流的同步
    事件更重要的用途是控制流之间的依赖关系。这包括多流之间的同步、不同计算阶段的协调、多GPU间的数据依赖,以及在动态工作流中进行控制。

    __global__ void foo1(char *A){
      *A = 0x1;
    }
    
    __global__ void foo2(char *B){
    printf("%d\n", *B);    // *B == *A == 0x1 assuming foo2 waits for foo1
    // to complete before launching
    }
    
    cudaMemcpyAsync(B, input, size, stream1);    // Aliases are allowed at
    // operation boundaries
    foo1<<<1,1,0,stream1>>>(A);                  // allowing foo1 to access A.
    cudaEventRecord(event, stream1);
    cudaStreamWaitEvent(stream2, event);
    foo2<<<1,1,0,stream2>>>(B);
    cudaStreamWaitEvent(stream3, event);
    cudaMemcpyAsync(output, B, size, stream3);  // Both launches of foo2 andcudaMemcpy (which both read)
    // wait for foo1 (which writes) to complete before proceeding

    注:上面代码来自CUDA官网

三、流和流同步

虽然事件常用于流同步,但它与常规的流同步操作(如 cudaStreamSynchronize)有显著区别:

  1. 同步机制更灵活:事件可以实现跨多个流的精细同步控制,而流同步通常作用于单个流的内部。
  2. 控制精度更高:事件可以标记和等待一个具体的“点”(如某个内核完成),而流同步则需等待整个流的所有任务结束。
  3. 开销更低:在某些场景下,事件同步的开销低于完全同步一个流。
  4. 应用更广泛:事件除了同步,还具备计时功能,这是流同步不具备的。

通过这样的对比,能加深对两者适用场景的理解。

四、例程

结合以上理论,我们来看一个综合性的实战例程。它演示了如何利用事件进行多流间的同步,并对整个操作流程进行精确计时。

#include "cuda_runtime.h"
#include "device_launch_parameters.h"

#include <stdio.h>
#include <stdlib.h>

__global__ void kernelFunc(float* data, int num, float factor){
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < num) {
        data[idx] = data[idx] * factor + idx * 0.001f;
    }
}

int main(){
    const int N = 1 << 20;  // 100w
    const int numStreams = 4;
    const int chunkSize = N / numStreams;
    const size_t chunkBytes = chunkSize * sizeof(float);
    const size_t totalBytes = N * sizeof(float);

    printf("mul stream sync test...\n");

    float *hData = NULL;
    cudaMallocHost(&hData, totalBytes);

    for (int i = 0; i < N; i++) {
        hData[i] = (float)rand() / RAND_MAX;
    }

    float *dData=NULL;
    cudaMalloc(&dData, totalBytes);

    cudaStream_t streams[numStreams];
    for (int i = 0; i < numStreams; i++) {
        cudaStreamCreate(&streams[i]);
    }
    //1 cacl time
    cudaEvent_t startEvent, stopEvent;
    cudaEvent_t kernelEvents[numStreams];

    cudaEventCreate(&startEvent);
    cudaEventCreate(&stopEvent);

    for (int i = 0; i < numStreams; i++) {
        cudaEventCreateWithFlags(&kernelEvents[i], cudaEventDisableTiming);
    }

    cudaEventRecord(startEvent, 0);

    int threadsPerBlock = 256;
    int blocksPerChunk = (chunkSize + threadsPerBlock - 1) / threadsPerBlock;

    for (int i = 0; i < numStreams; i++) {
        int offset = i * chunkSize;

        cudaMemcpyAsync(&dData[offset], &hData[offset],
            chunkBytes, cudaMemcpyHostToDevice,
            streams[i]);

        kernelFunc << <blocksPerChunk, threadsPerBlock, 0, streams[i] >> >(
            &dData[offset], chunkSize, (float)(i + 1) * 0.5f);
        cudaGetLastError();

        cudaEventRecord(kernelEvents[i], streams[i]);
        cudaMemcpyAsync(&hData[offset], &dData[offset],
            chunkBytes, cudaMemcpyDeviceToHost,
            streams[i]);
    }

    for (int i = 0; i < numStreams; i++) {
        cudaStreamSynchronize(streams[i]);
    }

    // record finish timepoint
    cudaEventRecord(stopEvent, 0);
    cudaEventSynchronize(stopEvent);

    float totalTime = 0.f;
    cudaEventElapsedTime(&totalTime, startEvent, stopEvent);

    printf("sum time: %.3f ms\n", totalTime);

    //2 stream depended
    printf("\n stream sync :\n");

    cudaEvent_t syncEvent;
    cudaEventCreate(&syncEvent);

    kernelFunc << <blocksPerChunk, threadsPerBlock, 0, streams[0] >> >(
        dData, chunkSize, 2.0f);
    cudaEventRecord(syncEvent, streams[0]);

    for (int i = 1; i < numStreams; i++) {
        cudaStreamWaitEvent(streams[i], syncEvent, 0);
        kernelFunc << <blocksPerChunk, threadsPerBlock, 0, streams[i] >> >(
            &dData[i * chunkSize], chunkSize, 1.5f);
    }

    for (int i = 0; i < numStreams; i++) {
        cudaStreamSynchronize(streams[i]);
    }

    printf("sync finish \n");

    printf("\n event query:\n");

    cudaEvent_t queryEvent;
    cudaEventCreate(&queryEvent);

    kernelFunc << <blocksPerChunk, threadsPerBlock >> >(dData, chunkSize, 1.0f);
    cudaEventRecord(queryEvent, 0);

    int maxChecks = 100;
    int checkCount = 0;

    while (cudaEventQuery(queryEvent) == cudaErrorNotReady) {
        checkCount++;
        if (checkCount < maxChecks) {
            int dummy = 0;
            for (int j = 0; j < 1000; j++) {
                dummy += j;
            }
        }
        else {
            cudaEventSynchronize(queryEvent);
            break;
        }
    }

    printf("event query count: %d\n", checkCount);

    cudaEventDestroy(syncEvent);
    cudaEventDestroy(queryEvent);
    cudaEventDestroy(startEvent);
    cudaEventDestroy(stopEvent);

    for (int i = 0; i < numStreams; i++) {
        cudaEventDestroy(kernelEvents[i]);
        cudaStreamDestroy(streams[i]);
    }

    cudaFree(dData);
    cudaFreeHost(hData);
    cudaDeviceReset();

    printf("\n all finish!\n");
    return 0;
}

这段代码主要实现了多流间的同步,并计算了整体操作时间。如果你有前面的 [C/C++](https://yunpan.plus/f/25-1) 和 CUDA 基础,理解起来应该不难。如果遇到不清楚的地方,最好的方法是上机运行,通过添加打印日志来观察程序的执行流程。

五、总结

在之前学习 CUDA 流时,我们简要提到了事件。本文则对事件进行了深入展开,详细阐述了其工作原理、核心应用场景,并通过实际例程让大家更直观地理解事件的运行机制。掌握好事件这一工具,对于编写高效、协调的 [高性能计算](https://yunpan.plus/f/15-1) 程序至关重要。

希望这篇关于 CUDA 事件的实战解析对你有帮助。想了解更多 并行编程GPU计算 相关的深度内容,欢迎持续关注 云栈社区 的更新。




上一篇:跨界融合与个人品牌:Dan Koe对一人企业未来的核心洞察
下一篇:NotebookLM进阶教程:利用AI深度解析YouTube视频内容作为个人知识库
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-1-24 02:48 , Processed in 0.477770 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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