在 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 什么时候算完,通常有两种做法:
-
阻塞等待 :调用
cudaStreamSynchronize()。CPU 此时只能"干等",啥也干不了,浪费性能。 -
轮询查询 :循环调用
cudaStreamQuery()。这会白白消耗 CPU 周期。
有了回调,你可以实现更优雅的操作:
-
逻辑衔接:GPU 算完第一阶段,回调函数立刻通知另一个 CPU 线程开始处理数据,或者触发 UI 更新。
-
多卡同步:在复杂的分布式计算中,用回调来触发跨节点的通信。
-
资源回收 :当 GPU 处理完一大块内存后,自动在回调里
free()掉主机端的缓存。
使用回调的"死亡禁忌"
虽然好用,但 CUDA 回调有两个非常严格的限制,不遵守的话程序会直接死锁或崩溃:
-
绝对不能在回调里调用 CUDA API : 回调函数是在驱动层的特殊线程中运行的。如果你在回调里又调用了
cudaMalloc或cudaMemcpy,极大概率会导致死锁(Deadlock)。 -
不要执行耗时太长的任务: 回调线程通常由驱动程序管理,如果你在里面写了个死循环或者大规模计算,会阻塞该流后续任务的调度,甚至拖慢整个驱动的响应速度。
代码示例
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