cuda编程笔记(38)--CUDA 异步回调

在 CUDA 中,大部分操作(如内核启动 kernel<<<>>>cudaMemcpyAsync)都是异步的。这意味着 CPU 把任务丢给 GPU 后,会立刻继续执行后面的代码,而不会原地等待 GPU 算完。

异步回调(Stream Callback) 允许你在 CUDA 流(Stream)的任务队列中插入一个 CPU 函数

执行逻辑:顺序与阻塞

  • 先来后到 :只有当流中排在它前面的所有任务(Kernel、拷贝等)都跑完,回调才会执行。

  • 精准执行 :你调用几次 cudaStreamAddCallback,它就执行几次。

  • 后浪被挡 :回调函数会阻塞该流中后续的任务。也就是说,回调函数没跑完,流里排在它后面的 Kernel 别想开始。

核心 API:cudaStreamAddCallback

这是实现该功能的灵魂函数。它的原型大致如下:

cpp 复制代码
extern __host__ cudaError_t CUDARTAPI cudaStreamAddCallback(cudaStream_t stream,
        cudaStreamCallback_t callback, void *userData, unsigned int flags);
  • stream: 关联的 CUDA 流。

  • callback: 你在 CPU 上定义的函数指针。

  • userData: 传给回调函数的参数(比如某个对象的指针)。

  • flags: 目前必须传 0(保留参数)。

callback是一个函数指针,定义如下,必须按照下面的函数形式定义回调函数

cpp 复制代码
/**
 * Type of stream callback functions.
 * \param stream The stream as passed to ::cudaStreamAddCallback, may be NULL.
 * \param status ::cudaSuccess or any persistent error on the stream.
 * \param userData User parameter provided at registration.
 */
typedef void (CUDART_CB *cudaStreamCallback_t)(cudaStream_t stream, cudaError_t status, void *userData);

cudaStreamAddCallback 以后可能会被删除。如果被删了,就不要再用它了。

如果没有回调,CPU 想知道 GPU 什么时候算完,通常有两种做法:

  1. 阻塞等待 :调用 cudaStreamSynchronize()。CPU 此时只能"干等",啥也干不了,浪费性能。

  2. 轮询查询 :循环调用 cudaStreamQuery()。这会白白消耗 CPU 周期。

有了回调,你可以实现更优雅的操作:

  • 逻辑衔接:GPU 算完第一阶段,回调函数立刻通知另一个 CPU 线程开始处理数据,或者触发 UI 更新。

  • 多卡同步:在复杂的分布式计算中,用回调来触发跨节点的通信。

  • 资源回收 :当 GPU 处理完一大块内存后,自动在回调里 free()主机端的缓存。

使用回调的"死亡禁忌"

虽然好用,但 CUDA 回调有两个非常严格的限制,不遵守的话程序会直接死锁或崩溃:

  1. 绝对不能在回调里调用 CUDA API : 回调函数是在驱动层的特殊线程中运行的。如果你在回调里又调用了 cudaMalloccudaMemcpy,极大概率会导致死锁(Deadlock)

  2. 不要执行耗时太长的任务: 回调线程通常由驱动程序管理,如果你在里面写了个死循环或者大规模计算,会阻塞该流后续任务的调度,甚至拖慢整个驱动的响应速度。

代码示例

cpp 复制代码
#include <cstdio>
#include <cuda_runtime.h>

// 1. 定义回调函数
// 注意:必须符合这个特定的签名,且使用 CUDART_CB 宏
void CUDART_CB MyCallback(cudaStream_t stream, cudaError_t status, void *userData) {
    char* message = (char*)userData;
    printf("【回调通知】:%s (状态码: %d)\n", message, status);
    // 警示:这里千万不能调用任何 cudaMalloc/cudaFree 等 API!
}
__global__ void kernel(float *d_data,int N){
    int tid=blockIdx.x*blockDim.x+threadIdx.x;
    if(tid>=N) return;
    d_data[tid]+=1;
}
int main() {
    cudaStream_t stream;
    cudaStreamCreate(&stream);

    // 2. 启动 GPU 任务
    float *d_data;
    cudaMalloc(&d_data,1024*sizeof(float));
    cudaMemset(d_data,0,1024*sizeof(float));
    dim3 blocks(8),threads(128);
    kernel<<<blocks, threads, 0, stream>>>(d_data,1024);

    // 3. 添加回调
    const char* note = "任务已结束,数据已就绪";
    cudaStreamAddCallback(stream, MyCallback, (void*)note, 0);

    // 4. CPU 立刻去做其他极其重要的工作
    printf("CPU:我已经把任务和回调都安排好了,现在我要去打游戏了。\n");
    
    // 为了演示,让主线程别立刻退出(否则回调线程也会死)
    cudaDeviceSynchronize(); 
    
    cudaStreamDestroy(stream);
    cudaFree(d_data);
    return 0;
}
cpp 复制代码
CPU:我已经把任务和回调都安排好了,现在我要去打游戏了。
【回调通知】:任务已结束,数据已就绪 (状态码: 0)

与标准库线程配合

cudaStreamAddCallback 的设计极其高冷,它只允许由 CUDA 驱动程序(Driver)自己管理的一个内部隐藏线程 来执行回调函数。你无法给它传一个 std::thread::id 或者指定的线程句柄。

虽然你不能指定线程,但你可以把 CUDA 回调当成一个"发令员"。在回调里去通知你的 std::thread 起床干活。

最标准的做法是使用 C++ 的同步原语:std::condition_variable(条件变量)或信号量。

伪代码示例

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <cuda_runtime.h>

// 全局同步原语
std::mutex mtx;
std::condition_variable cv;
bool ready = false;

// 你的工作线程:负责处理计算后的重活
void worker_thread() {
    std::unique_lock<std::mutex> lock(mtx);
    std::cout << "Worker Thread: 正在等待 GPU 的信号..." << std::endl;
    
    // 等待回调发出信号
    cv.wait(lock, [] { return ready; });
    
    std::cout << "Worker Thread: 收到信号!开始在自定义线程中处理数据。" << std::endl;
}

// CUDA 回调函数:它是发令员
void CUDART_CB MyCallback(cudaStream_t stream, cudaError_t status, void *userData) {
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
    std::cout << "【CUDA 回调】:任务跑完了,正在拍醒主线程..." << std::endl;
    cv.notify_one(); // 通知 worker_thread
}

int main() {
    // 1. 启动你自己的线程
    std::thread t(worker_thread);

    // 2. 模拟 CUDA 流程
    cudaStream_t stream;
    cudaStreamCreate(&stream);
    // 这里可以启动 kernel
    
    // 3. 添加回调
    cudaStreamAddCallback(stream, MyCallback, nullptr, 0);

    // 4. 回收资源
    t.join();
    cudaStreamDestroy(stream);
    return 0;
}

新 API:cudaLaunchHostFunc

cudaLaunchHostFunc 支持 Stream Capture(流捕获,用于把一连串操作录制成 CUDA Graph),而回调函数不支持。如果你在录制 Graph 时调用这个 API,会直接报错。

对于CUDA Graph,我以前的文章介绍过,可以简单回顾

cuda编程笔记(29)-- CUDA Graph-CSDN博客

新 API (cudaLaunchHostFunc):完美支持。你可以把一个 CPU 函数也"录进" Graph 里,让它成为工作流的一环。

cpp 复制代码
extern __host__ cudaError_t CUDARTAPI cudaLaunchHostFunc(cudaStream_t stream, cudaHostFn_t fn, void *userData);

cudaHostFn_t如下

cpp 复制代码
/**
 * CUDA host function
 * \param userData Argument value passed to the function
 */
typedef void (CUDART_CB *cudaHostFn_t)(void *userData);

示例代码

流捕获的流程在CUDA Graph的文章有介绍,请自行学习

cpp 复制代码
#include <cstdio>
#include <cuda_runtime.h>

// 1. 定义回调函数
void CUDART_CB MyHostNodeFunc(void* userData) {
    int* val = (int*)userData;
    printf("【Graph 节点触发】主机函数正在执行,数值为: %d\n", *val);
}
__global__ void kernel(float *d_data,int N){
    int tid=blockIdx.x*blockDim.x+threadIdx.x;
    if(tid>=N) return;
    d_data[tid]+=1;
}
int main() {
    int data_to_pass = 1024;
    cudaStream_t stream;
    cudaStreamCreate(&stream);

    // 2. 启动 GPU 任务
    float *d_data,*h_out;
    h_out=(float*)malloc(1024*sizeof(float));
    cudaMalloc(&d_data,1024*sizeof(float));
    cudaMemset(d_data,0,1024*sizeof(float));
    dim3 blocks(8),threads(128);

    // --- 开始捕获 ---
    cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
    kernel<<<blocks, threads, 0, stream>>>(d_data,1024);
    cudaMemcpyAsync(h_out, d_data, 1024*sizeof(float), cudaMemcpyDeviceToHost,stream);
    cudaLaunchHostFunc(stream, MyHostNodeFunc, &data_to_pass);
    cudaGraph_t graph;
    cudaStreamEndCapture(stream, &graph);
    // --- 结束捕获 ---

    // 实例化并启动 Graph
    cudaGraphExec_t graphExec;
    cudaGraphInstantiate(&graphExec, graph, nullptr, nullptr, 0);
    
    // 执行 Graph,此时会依次运行 Kernel -> Memcpy -> MyHostNodeFunc
    cudaGraphLaunch(graphExec, stream);
    
    cudaStreamSynchronize(stream);

    // 清理资源
    cudaGraphExecDestroy(graphExec);
    cudaGraphDestroy(graph);
    cudaStreamDestroy(stream);
    
    cudaFree(d_data);
    free(h_out);
    return 0;
}
cpp 复制代码
【Graph 节点触发】主机函数正在执行,数值为: 1024
相关推荐
Better Bench2 小时前
《八十天环游地球》阅读笔记
笔记·读书笔记·八十天环游地球
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2026.04.21 题目:1722. 执行交换操作后的最小汉明距离
笔记·算法·leetcode
阿Y加油吧3 小时前
两道 LeetCode 题的复盘笔记:从「只会暴力」到「懂优化」
笔记·算法·leetcode
chudonghao4 小时前
[UE学习笔记][基于源码] 控制器、Pawn、相机的控制关系
笔记·学习·ue5
Qinn-4 小时前
【工作笔记】锁等待超时错误 排查
笔记
LeeeX!5 小时前
【OpenClaw最新版本】 命令行备忘录:高频操作与实战技巧
笔记·aigc·openclaw
羊群智妍5 小时前
2026免费GEO工具,AI搜索优化一步到位
笔记
Leah-5 小时前
Web项目测试流程
笔记·学习·web·测试·复盘
Qinn-6 小时前
【学习笔记】软考系统分析师计算机系统计算题考点
笔记