CUDA Stream深度解析:Stream创建与销毁、默认Stream与自定义Stream、Stream同步机制、Stream优先级

各位同学,今天我们来聊聊CUDA Stream。说实话,Stream这个概念刚接触时容易让人迷糊——不就是个队列吗?但实际用起来,里面的门道可不少。我在做推理引擎优化时,曾经因为Stream没用好,导致GPU利用率只有30%,后来改完直接翻倍。今天就把这些经验掰开揉碎讲给你听。

Stream是什么?说白了就是GPU的任务流水线

想象一下,你只有一个厨房,但有两个厨师。一个负责切菜,一个负责炒菜。如果让他们串行工作,切完再炒,效率很低。但如果让他们并行——切菜的同时炒菜,那就快多了。CUDA Stream就是这个道理。

每个Stream本质上是一个独立的命令队列。同一个Stream里的操作按顺序执行,不同Stream之间的操作可以乱序、可以并行。我习惯把Stream看作GPU上的"线程"——虽然不完全准确,但这么理解有助于入门。

核心要点:Stream是GPU上执行异步操作的序列。不同Stream之间没有依赖关系,可以并发执行。

Stream的创建与销毁

创建Stream很简单,API就两行:

cudaStream_t stream;
cudaStreamCreate(&stream);

销毁也一样:

cudaStreamDestroy(stream);

嗯,这里要注意:销毁Stream时,它会等待所有已提交的任务完成。我曾经踩过这个坑——在Stream里提交了大量任务后立刻销毁,结果程序卡住了好久。后来才明白,销毁操作会隐式同步。

小技巧:如果你确定Stream里的任务已经全部完成,可以先调用cudaStreamSynchronize(stream)显式同步,再销毁。这样销毁操作会立即返回。

另外,创建Stream时还可以指定标志:

cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking);

cudaStreamNonBlocking这个标志很重要。它表示这个Stream不会与默认Stream同步。我后面会详细讲。

默认Stream vs 自定义Stream

每个CUDA程序启动时,都会自动创建一个默认Stream。你没显式指定Stream时,所有操作都跑在默认Stream上。

默认Stream有个特点:它会与设备上的所有其他Stream隐式同步。说白了,默认Stream是个"霸道总裁"——它执行时,其他Stream都得等着。

举个例子:

// 默认Stream操作
kernel1<<<grid, block>>>();

// 自定义Stream操作
cudaStream_t stream;
cudaStreamCreate(&stream);
kernel2<<<grid, block, 0, stream>>>();

cudaStreamSynchronize(stream);

这段代码里,kernel1和kernel2不会并行执行。因为默认Stream会阻塞自定义Stream。这就是为什么我建议:如果你要用多Stream,最好把默认Stream的操作也放到自定义Stream里,或者用cudaStreamNonBlocking标志。

Stream类型 特点 适用场景
默认Stream 隐式同步,阻塞其他Stream 简单串行任务
自定义Stream(普通) 独立执行,可并行 多任务并发
自定义Stream(NonBlocking) 不与默认Stream同步 需要完全独立的任务

Stream同步机制

同步是Stream编程里最容易出错的地方。我总结了几种同步方式:

  1. cudaStreamSynchronize:阻塞CPU,直到指定Stream里的所有操作完成。
  2. cudaDeviceSynchronize:阻塞CPU,直到设备上所有Stream的操作完成。
  3. cudaEvent:更细粒度的同步,可以在Stream之间插入事件。

Event同步是我个人比较喜欢的方式。它允许你精确控制依赖关系:

cudaEvent_t event;
cudaEventCreate(&event);

// Stream A 执行
kernelA<<<grid, block, 0, streamA>>>();
cudaEventRecord(event, streamA);

// Stream B 等待 Stream A 完成
cudaStreamWaitEvent(streamB, event, 0);
kernelB<<<grid, block, 0, streamB>>>();

你看,这样Stream B就会等Stream A的kernelA执行完才开始。但注意,cudaStreamWaitEvent不会阻塞CPU,它只是告诉GPU:"嘿,Stream B先别动,等这个事件信号来了再继续。"

避坑指南:我曾经在项目里用Event同步,但忘了检查Event是否已经被记录。结果程序跑起来后,Stream B一直在等一个永远不会到来的事件——死锁了。所以记住:cudaEventRecord必须在cudaStreamWaitEvent之前调用。

Stream优先级

CUDA支持给Stream设置优先级。高优先级的Stream会优先获得计算资源。创建时指定:

int priority_high, priority_low;
cudaDeviceGetStreamPriorityRange(&priority_low, &priority_high);

cudaStream_t high_prio_stream, low_prio_stream;
cudaStreamCreateWithPriority(&high_prio_stream, cudaStreamNonBlocking, priority_high);
cudaStreamCreateWithPriority(&low_prio_stream, cudaStreamNonBlocking, priority_low);

优先级范围由硬件决定。你可以用cudaDeviceGetStreamPriorityRange查询。数值越小优先级越高(priority_high是较小的值)。

我一般在推理场景里这么用:

  • 高优先级Stream:处理实时性要求高的请求(比如在线推理)
  • 低优先级Stream:处理后台任务(比如模型预热、数据预处理)

但说实话,优先级的效果因GPU架构而异。在Turing架构上效果明显,在Volta上就一般。你想想看,硬件调度器实现不同,表现自然不一样。

个人经验:不要过度依赖优先级。如果两个Stream的任务量差距太大,优先级也救不了。我习惯先通过任务拆分来平衡负载,再用优先级做微调。

知识体系总览

下面这张图总结了Stream的核心概念和关系:

CUDA Stream 知识体系 默认Stream 隐式同步,阻塞其他 自定义Stream 独立队列,可并行 NonBlocking 不与默认同步 同步机制 cudaStreamSynchronize 阻塞CPU,等待Stream cudaDeviceSynchronize 阻塞CPU,等待所有 cudaEvent + WaitEvent 细粒度Stream间同步 Stream优先级 高优先级 → 实时推理 低优先级 → 后台任务

这张图把Stream的三大块——创建类型、同步机制、优先级——串起来了。你从默认Stream和自定义Stream出发,理解它们的区别;然后掌握三种同步方式;最后用优先级做精细调度。这三块掌握了,Stream就算入门了。

好了,今天的内容就到这里。记住:Stream是并发调度的基础,但用好它需要你对同步机制有清晰的认识。多写代码、多调试,慢慢就有感觉了。

一句话总结:Stream让GPU从"单车道"变成"多车道",但同步机制就是交通信号灯——用好了畅通无阻,用不好就是连环追尾。

想要更多资料,联系可可