想象你是一家繁忙中餐厅的后厨总管。周末晚市客人爆满,你不会等热菜师傅把一道菜做完才接下单——你会把订单分给热菜、凉菜、炖汤几位师傅,每位师傅按自己手头的小纸条顺序做事。更妙的是,传菜员往备料台送食材时,炒菜师傅根本不用干等:食材边到,锅边在炒。这套“多线并进 + 定时器标记”的管理方法,硬是把一堆“等待时间”变成了“做菜时间”。 CUDA 里的流(Stream)和事件(Event),就是 GPU 世界里的“师傅分工”和“厨房定时器”。流决定一串 GPU 任务的先后归属,事件用来记录“某个时刻是否已经到来”。一旦你掌握这两件武器,就能让 GPU 一边疯狂算数,一边在后台悄悄搬数据,把硬件每一分算力都榨干。 这篇文章从一个后厨比喻出发,带你逐步理解默认流、多流并行、事件同步、精确计时,以及最激动人心的“计算与传输重叠”。你会发现,CUDA 的异步世界并没有那么神秘,一顿晚市高峰过去,你也就全熟络了。
学会这一篇,你就能避开“默认流隐式阻塞”这些暗坑,学会用多流和事件把 GPU 利用率真正拉满,让训练、推理、实时处理项目的性能更上一层楼。
可视化演示
展示CUDA多流并行执行中的数据拷贝、内核计算和设备到主机传输,通过Event标记实现同步,直观理解异步执行带来的计算重叠。
本节配有交互动效(进入视口后自动加载,约 3KB)。
CUDA多流并行执行 多流H2D→Kernel→D2H异步执行,计算与传输重叠优化 Event记录同步点,精确计时性能分析 CUDA性能优化的核心基础,实现异步多任务并行
流的概念与默认流
后厨里,炖汤的砂锅、凉菜台的拌碗、热菜灶的炒锅,各归各的师傅。每位师傅面前都有一张“订单小条”,他只需要按小条排好的顺序做菜,做完一道再下一道。CUDA 里的“订单小条”就是流(Stream):它是一个由 GPU 操作构成的队列,规定了操作在设备端的执行次序。同一流中,前一个操作没完成,后一个操作不会开始。
流是 CUDA 异步执行的基本调度单位。任何内核启动、异步内存拷贝、事件记录,都可以放进一个流。放入同一流中的操作被保证按提交顺序在设备端严格串行执行;而不同流之间没有先后约束,GPU 硬件可以并行调度它们。这相当于给一大堆“待办任务”划出了几条互不干扰的流水线,每条线上的人只按自己的节奏干活。
当你没有显式创建流时,所有操作都跑进一个隐式存在的“默认流”(也常被称为 NULL 流,句柄为 0)。默认流最特殊之处是它和一些非默认流之间存在隐式同步:当默认流中的操作被启动时,GPU 可能先让其他流中的操作完成,再执行默认流,仿佛主厨一插队,全场都要停下来让路。这个“隐式阻塞”是初学者最容易踩到的性能陷阱之一。
你可以想象每位师傅只盯着自己的小纸条:切配、下锅、装盘。这就是流的意义——对内严格排序,对外互不干扰,然后整个后厨才能同时开火。理解了这个“纪律”,再看后面多流并行,就顺理成章了。
- 操作集合花括号里装的 Op_1 到 Op_n 表示流中的一系列 GPU 操作,比如内核执行、异步拷贝、事件记录,它们是构成流的基本元素。
- 顺序记号符号 \prec 表示“先于”,Op_i \prec Op_{i+1} 表达同一流中第 i 个操作必须等第 i+1 个操作前面的所有工作完成。
- 流内严格串行因此一条流是“一个接一个”的队列,不是一堆乱飞的散任务;这是流定义中最核心的承诺。
创建与使用流
每次来一桌大订单,经理就会给这桌开一条新的出菜流水线。在 CUDA 中,创建一条流只需要一个函数调用:cudaStreamCreate(&stream),用完再 cudaStreamDestroy(stream)。新流一诞生,你就能把内核、拷贝往里面塞。释放流时,CUDA 会等待流中所有已提交操作完成,因此不必担心“任务还没做完,流就被注销”。
使用流的标准姿势是“创建流 → 把操作放进流 → 销毁流”。关键的 API 有三个:内核启动时通过 <<<grid, block, sharedMemSize, streamId>>> 四个参数中的最后一个指定流;cudaMemcpyAsync(dst, src, size, kind, stream) 将内存搬运作为异步操作加入到流中;cudaStreamCreate 与 cudaStreamDestroy 负责流的生命周期管理。若第四个参数省略或填 0,则默认在 NULL 流中执行。
这种“四参数启动”和“Async 拷贝”是把操作提交给 GPU 的入口。调用完这些函数后,CPU 侧并不会等 GPU 执行完,而是立刻返回——这就是异步执行。CPU 可以继续发布下一条命令,不会干等 GPU。也是因此,我们可以把很多互不依赖的小任务“一次性撒出去”,让 GPU 自己慢慢消化。
经理开好新流水线后,还会顺手在单子上写“食材先送,菜后炒”——这就是异步拷贝 cudaMemcpyAsync 的定位。它不会让 CPU 站在库房门口等卸货,而是把搬运任务交给对应流,排队执行。你只需要保证流还活着,剩下的运输细节全部由 CUDA 调度。
- G(grid 网格维度)告诉 GPU 总共需要启动多少个线程块,相当于决定这道菜要做多少份。
- B(block 线程块维度)每个线程块里有多少个线程,相当于每个餐盘里盛多少食材。
- Sh(动态共享内存)额外给块内共享内存预留的字节数,一般没有特殊需求时可以填 0。
- S(流句柄)目标流的标识;为 0 或 NULL 时表示放入默认流,也就是走“主厨那条线”。
多流并行执行
后厨为什么快?不是一位师傅有三头六臂,而是十几位师傅同时开火。多流并行就是把不同任务放进不同流,让 GPU 上多个执行引擎同时运转。但这有一个前提:流之间的操作必须互相独立。如果流 A 要读流 B 的输出,那你还是得通过事件或同步让它们排队,否则就会产生数据竞争。
GPU 的调度器会在多个流之间自动寻找可以并行的操作:例如两个内核正好都只占一半的 SM(流多处理器),它们就能同时运行。但并发度并不是“流数量越多越好”。真正的并发上限由硬件资源决定:SM 的数量、每个 SM 可以驻留的线程块数、以及每个内核占用的寄存器和共享内存,共同决定了 GPU 到底能同时容纳多少个内核。开再多的流,多出来的也只能排队。
多流还有一个常见坑叫“虚假依赖”。比如默认流和自定义流交替使用,或在自定义流之间混用同步版拷贝,都会让两条本可并行的流隐式地排起队来。表面上看代码确实开了 4 条流,实际运行却是一条条串过去的。想真正并行,核心是让流与流之间的操作互不等待,让每个流都拥有自己独立的资源通道。
多流并行的主厨思想可以这样记:你的“任务划分”决定了有几位师傅愿意同时开工;而“灶台数量”决定了真正能同时站几位师傅。好的并行设计,就是在硬件允许的范围内,让每一位师傅都有活干、且互不抢灶。
- N_streams你创建并启动的流的条数。数量再多也只是候选,不等于真实并发。
- N_SM × B_cap / B_kernel一个 GPU 能同时承载的“内核实例数”。例如有 16 个 SM、每个 SM 驻留 8 个块,而一个内核占用 2 个块,那么可并发容纳 64 个这种内核。
- min 取最小实际并发度是取两者较小的值。它代表资源“短板”,像餐厅只有 4 个灶眼时,5 道菜同时点也只能一次炒 4 道。
事件机制与同步
厨师做菜时,传菜员按一下铃,大家就知道“这道菜可以端走了”——这个铃铛就是 CUDA 里的事件(Event)。事件不搬运数据,也不执行计算,它只是一个轻量标记,用于表示“流中某个位置到了”。当它前面的所有操作都完成的那一刻,事件状态就变为“已完成”。
CUDA 事件的基本用法是四步:先用 cudaEventCreate(&event) 创建事件对象;然后用 cudaEventRecord(event, stream) 把事件记录到某条流里;之后可以用 cudaEventSynchronize(event) 阻塞等待它完成;用完用 cudaEventDestroy(event) 销毁。如果你不想阻塞,还可以用 cudaEventQuery(event) 非阻塞询问:事件完成返回 cudaSuccess,没完成返回 cudaErrorNotReady,非常适合做轮询。
事件还有一个妙用:在流之间当“旗语”。如果流 B 要等流 A 的某个阶段结束,可以让流 B 调用 cudaStreamWaitEvent(stream_B, event_A),这样流 B 会一直等到事件 A 发生后才继续。这比把两个内核硬塞进同一流要灵活得多,也是构建复杂依赖关系(DAG)的基本积木。
事件就像后厨里那排“菜品出单号”:每道工序完成,就在出单号上盖一个章,别的主厨看见章才敢动下一道工序。看似只是小小的铃铛,却是整个异步配合体系里的润滑剂。
- 记录起点事件 E_s在流中提交一个记录操作;只有它前面的操作都完成,E_s 才会被标记为发生。
- 中间操作 K你要计时的内容,比如一个内核,它在流里排在两个事件之间。
- 记录终点事件 E_e同样插在流队尾,当它前面的任务全部完成后,该事件也被标记完成。
- 时间差 T读取两个事件各自的时刻,相减得到这段操作的耗时。
事件精确计时
经理想量化一位师傅炒一盘菜到底要多久,最简单的方法是在开始时按一下秒表,结束时再按一下秒表。CUDA 事件就是 GPU 自带的高精度秒表。使用事件计时的好处是:时间戳由 GPU 时钟记录,不经过 CPU 调度,测量结果非常稳定,也不会把 CPU 启动内核的开销混进来。
标准的计时模板是这样的:创建 start 与 end 两个事件;在目标流中先 cudaEventRecord(start, stream);接着启动要计时的内核;然后 cudaEventRecord(end, stream);调用 cudaEventSynchronize(end) 等 end 事件完成;最后 cudaEventElapsedTime(&ms, start, end) 拿到毫秒单位的耗时。整个过程看起来就像在菜品的开始端和结束端各夹一个计时夹。
需要特别注意三点:第一,start 和 end 必须记录在同一条流里,否则时间差没有意义;第二,cudaEventSynchronize 之后才能读取耗时,因为事件尚未完成时,时间差还不完整;第三,为了排除首次启动的冷启动效应,建议让同一个内核跑多轮,取平均时间。这样得到的数字才能真正反映 GPU 上的计算开销。
精确计时的意义在于“分清每道工序究竟占了多少灶台时间”。没有这个秒表,你做任何优化都像闭着眼睛炒菜——不知道自己改完是快了还是慢了。事件计时是性能优化最基础也最可靠的度量工具之一。
- T_end 与 T_startCUDA 事件内部分别记录了记录瞬间的 GPU 时间戳,精度约为微秒级。
- 时间差结束事件的时间戳减去开始事件的时间戳,得到中间阶段的原始耗时。
- 转换为毫秒CUDA 的
cudaEventElapsedTime返回毫秒单位,因此要乘以 10^{-3},也就是把微秒缩小 1000 倍。
流同步方式
后厨里,经理有时候只要等某一位师傅把菜做完,就可以继续下一道工序;但晚上打烊时,他必须等全厨房所有菜都上齐,才能宣布结束营业。CUDA 里的“等”也是分粒度的,选错了粒度,要么等不够,要么等太多。
CUDA 提供了几种主要的同步 API:cudaStreamSynchronize(stream) 只等待指定流中所有操作完成,其他流继续执行;cudaDeviceSynchronize() 等待当前设备上所有流的所有操作完成;cudaStreamWaitEvent(stream, event) 则让一条流等待另一条流中的某个事件信号,是一种更细粒度的“握手”。选择哪一种,取决于你真正需要等的是什么。
比显式同步更加隐蔽的是“隐式同步”。默认流中的部分操作、同步版 cudaMemcpy、锁页内存的分配与释放、cudaDeviceSynchronize 调用、甚至某些版本的设备端 printf,都可能触发与其它流的隐式同步。隐式同步会让 GPU 暂时放弃并行调度,把所有流“拉平”到同一个时间点上,这对性能的破坏往往是致命的。
同步的选择就像后厨的“等菜上齐”指令:等的人少,耽误时间少;一等全店,所有窗口都得停一停。性能调优的关键思路,就是尽量把同步粒度缩小到“只等必要的对象”,把不必要的等待从关键路径上拿掉。
- t_stream只等待一条流的完成时间,影响范围最小,如同只叫住一位师傅问进度。
- t_device等待当前设备所有流完成,影响范围是整个 GPU,如同要求全厨房暂停。
- t_host+device还包含主机侧上下文或跨设备的等待,代价最高,如同整家店关门歇业等客人全部离开。
计算与传输重叠
后厨最精彩的配合,是传菜员推着食材进备料台的同时,炒菜师傅已经开始炒下一道菜。CUDA 里的“计算与传输重叠”,就是让 GPU 的访存引擎(负责数据搬运)与计算引擎(负责内核算力)同时开工。很多加速应用最核心的技巧,都在这一节。
要实现重叠,需要三个条件。第一,把同步的 cudaMemcpy 换成异步的 cudaMemcpyAsync,并放入指定流;第二,使用锁页内存(cudaMallocHost 或 cudaHostAlloc),否则异步拷贝会退化成同步行为;第三,把不同阶段分配到不同流中,让流 A 负责搬运、流 B 负责计算。更经典的工程做法是“双缓冲流水线”:GPU 计算第 i 帧数据时,主机同时准备第 i+1 帧的传输,两块数据轮流交接,让 GPU 几乎不空闲。
当计算时间和传输时间相差不大时,重叠的效果非常显著。串行执行时,总耗时等于两者的和;重叠执行时,总耗时会压缩到两者的最大值。如果计算远大于传输,加速比接近 1 倍;如果两者相当,理论最高可以获得接近 2 倍的提速,这正是许多推理引擎“吞吐翻倍”的秘密来源。
主厨的高效不是因为他炒得有多快,而是因为他永远不在等食材——食材还在路上时,锅里的菜已经开始滋滋作响。这正是 CUDA 流与事件最值得掌握的实战技巧:让“等待”从运行时间里消失,让计算和传输真正并肩奔跑。
- T_c 与 T_t分别表示计算阶段和传输阶段的耗时,一个代表炒菜时间,一个代表备料时间。
- 串行总耗时同一时间只能做一件事时,先备料再炒菜,总耗时是两者相加。
- 重叠总耗时备料与炒菜同时开工,总耗时为两者中的较大者。
- 提速收益串行时间与重叠时间的比值就是加速比;两者接近时,理论上接近 2 倍。
AI 应用场景
大模型推理服务中的连续批处理
阶段:模型推理与在线服务
解决的问题:大语言模型推理时,单个请求的生成是串行的,GPU 经常处于“吃几口就停一下”的状态。如果只用同步拷贝和单条流,输入请求的预处理、GPU 推理、输出回传会依次等待,GPU 利用率很低。用 CUDA 流把不同请求的预处理和推理拆分到多条流中,一个请求在排队时,另一个请求的输入已经在后台搬运,GPU 就能像后厨流水线一样停不下来。
具体案例:在推理框架中维护一个“请求流池”,每个请求绑定一条流。前端拿到用户文本后,先用 cudaMemcpyAsync 把 token 输入搬进显存,放入流 1;同时流 2 正在执行上一个请求的 decode 内核。通过事件记录输出阶段,再异步拷贝回主机。实测在并发请求数较多时,GPU 利用率可从 40% 提升到 80% 以上。
深度学习训练中的数据预取流水线
阶段:数据加载与训练循环
解决的问题:训练时 CPU 负责读图片、做增强、传输到 GPU,GPU 负责前向与反向。传统做法是每轮迭代同步等待数据到位,GPU 空转时间较长。AI 工程师用多流实现“双缓冲预取”:当前迭代在 GPU 上计算的同时,下一个小 batch 的图片已经在另一条流上做 H2D 拷贝,数据搬运不再占用训练的关键路径。
具体案例:在 PyTorch 自定义训练循环中,通过 CUDA Stream 把 to(device) 操作放入非默认流,并与当前计算流用事件连接。每个 epoch 的训练时间缩短约 20%-35%,尤其在数据增强较重、图片较大的视觉任务上效果明显。
实时音视频多路并行处理
阶段:音视频采集与 AI 实时处理
解决的问题:安防或直播场景中常常有几十路视频流需要同时做目标检测。每路视频都要经历上传、预处理、推理、回传。如果所有路共用一条流,任何一路的慢操作都会拖住全局;把不同路映射到不同流,各路处理就能在 GPU 上自然交错,互不阻塞。
具体案例:某视频分析平台接入 8 路 1080p 摄像头,每路分配一条 CUDA 流做缩放、推理、结果回传。通过事件同步保证同一路的帧处理顺序,跨路之间不等待。最终整体延迟从 85ms 压到 45ms,GPU 显存占用反而下降,因为不再需要一次性把所有帧都塞进显存。
Python 演示
import threadingimport time class CudaStream: """模拟一条 CUDA Stream: 按提交顺序执行任务.""" def __init__(self, name): self.name = name self.tasks = [] # (描述, 耗时秒) def add_task(self, desc, cost): self.tasks.append((desc, cost)) def run(self): for desc, cost in self.tasks: time.sleep(cost) def main(): s1 = CudaStream("stream1") s1.add_task("copy H2D", 0.2) s1.add_task("compute", 0.6) s1.add_task("copy D2H", 0.2) s2 = CudaStream("stream2") s2.add_task("copy H2D", 0.2) s2.add_task("compute", 0.6) s2.add_task("copy D2H", 0.2) # 串行执行: 像同一流里排队 start = time.time() for s in (s1, s2): s.run() serial = time.time() - start # 并行执行: 像两个独立流同时跑 start = time.time() threads = [threading.Thread(target=s.run) for s in (s1, s2)] for t in threads: t.start() for t in threads: t.join() parallel = time.time() - start print("serial : %.2f s" % serial) print("parallel: %.2f s" % parallel) print("speedup : %.2f" % (serial / parallel)) if __name__ == "__main__": main()
self.tasks = []模拟流内部的“操作队列”,所有加入该流的任务都按顺序排队。def run(self): ... time.sleep(cost)模拟一个内核或一次拷贝所占据的 GPU 执行时间。for s in (s1, s2): s.run()串行模式:两条流挨个执行,相当于没有启用多流并行。threading.Thread(target=s.run)把每条流放进独立线程,相当于让 GPU 上的每条流各自推进。for t in threads: t.start()所有流同时开工,对应 CUDA 中的多流并行启动。for t in threads: t.join()等待所有流结束,对应cudaDeviceSynchronize()。serial / parallel计算加速比,直观展示重叠带来的收益。
常见误区
误区我经常用 `cudaMemcpy` 在 CPU 和 GPU 之间搬数据,以为只要调用完它,CPU 就可以立刻干别的事,反正拷贝在后台自动进行。
正解只有 cudaMemcpyAsync 才是异步的,普通的 cudaMemcpy 是同步阻塞调用,CPU 会一直等到拷贝完成才返回。想让拷贝和计算重叠,必须使用异步版本,并且配合锁页内存和流。比喻来说,普通 cudaMemcpy 是传菜员站在库房门口等你签字验收,cudaMemcpyAsync 才是他放下货转身就走。
为何常见:文档里“拷贝很快”给人的错觉,以及很多老式教程代码只在单流场景里使用同步拷贝,让人误以为拷贝总是非阻塞的。
误区我一次把三个内核都提交到同一条流里,以为它们会像多个厨师一样同时炒菜,这样肯定比一个个来要快很多。
正解同一流严格保证顺序执行,前一个内核全部完成,后一个内核才会开始。想让内核并行,必须把它们放到不同的流中。三条内核挤在一条流里,就像三位师傅共用一张订单小条,结果只能排队做菜。
为何常见:人们把“CPU 上提交多个任务”的并发直觉带到了 GPU 流上,忽略了流本身就是“一个队列”的本质。
误区我开 32 条流跑一个小内核,总比开 4 条流快吧?反正多开不要钱。
正解并行上限由硬件决定:SM 数量、每个 SM 能驻留的线程块数、每个内核占用的资源(寄存器、共享内存)。当流超过硬件承载能力后,多出来的流只能排队,不仅没有加速,还可能因为调度开销变慢。就好比后厨只有 4 个灶眼,你安排 8 位师傅也站不下。
为何常见:现代 CPU 的“超线程越多越好”经验被错误迁移到 GPU 流上,实际上 GPU 并发要靠资源和任务粒度共同匹配。
误区我在代码里只用了流 A,调用 `cudaDeviceSynchronize()` 时,以为它只会等流 A 完成,流 B 可以在后台继续跑。
正解cudaDeviceSynchronize() 会等待当前设备上所有流的所有操作完成,它不是一个“流级”接口。如果你只想等一条流,应该用 cudaStreamSynchronize(stream)。用设备级同步来等单条流,就像打烊时把全厨师傅都叫停,只为等一道甜品。
为何常见:函数名里带 “Device” 容易被忽略,很多初学者把所有同步需求都用一个设备级同步解决,最后发现性能下降。
误区我觉得事件和缓存差不多,应该是 GPU 里的一块区域,用来存放中间结果或临时数据。
正解事件不存储任何用户数据,它只是一个“状态标记”加“时间戳”的轻量对象。它记录的是“流执行到某个位置了没有”,以及“到达那一刻的 GPU 时钟读数”。把它理解为后厨的按铃或盖章更合适,而不是一个仓库。
为何常见:名字 “Event” 容易让人联想到“事件队列”或“事件缓冲区”,但在 CUDA 里它更像一个信号灯,而不是容器。
面试题
Q1. 关于 CUDA 流,最准确的说法是?
- A.GPU 上一条有序执行的操作队列← 正确答案
- B.一种用于存储数据的专用显存区域
- C.一个用于线程间通信的内建消息队列
- D.一条连接 CPU 与 GPU 的物理总线
答案:A。CUDA 流本质上是 GPU 操作的有序队列,同一流内按顺序执行,不同流之间则可并行。它不是显存区域、消息队列或物理总线,就像后厨的订单小条不是灶台也不是仓库。
Q2. 默认流(NULL 流)的操作,说法正确的是?
- A.默认流内操作没有固定的执行顺序
- B.默认流可能与其他流产生隐式同步← 正确答案
- C.默认流永远和其他流完全并行执行
- D.默认流上无法使用事件记录或同步
答案:B。默认流的最大特点是会与部分非默认流产生隐式同步,可能让并发变串行。默认流内部仍然严格有序,也可以用事件。这就像主厨一旦插队,全场都得让路。
Q3. 同一流中连续提交两个内核,它们会如何执行?
- A.一定并行执行以充分利用 GPU
- B.一定按照先后顺序严格执行← 正确答案
- C.是否能并行由 SM 空闲数决定
- D.后一个内核会先于前一个启动
答案:B。同一流内的所有操作被保证按提交顺序串行执行,前一个内核完全结束后后一个才开始。流内没有并行,只有不同流之间才可能并行。
Q4. 要在流 s 中启动名为 kernel 的内核,正确写法是?
- A.kernel<<<G, B, 0, s>>>(……)← 正确答案
- B.cudaStreamLaunch(kernel, s, ……)
- C.kernel<<<s>>> 然后在流中执行
- D.cudaLaunchStream(kernel, s, 0)
答案:A。内核启动最多支持四个配置参数:网格维度、块维度、动态共享内存、流句柄。第 4 个参数就是流 s,填 0 或不填表示默认流。CUDA 并没有 cudaStreamLaunch 这类 API。
Q5. cudaMemcpyAsync 与 cudaMemcpy 的核心差异是?
- A.前者速度一定比后者快
- B.前者可放入指定流并立即返回← 正确答案
- C.前者只能用于设备到设备拷贝
- D.前者会等待拷贝在 CPU 侧完成
答案:B。cudaMemcpyAsync 把拷贝作为异步任务提交到指定流,CPU 不等待拷贝完成;cudaMemcpy 是阻塞同步版本。两者速度并不是绝对的快慢关系,Async 版本价值在于能与其他计算重叠。
Q6. 哪些因素会限制多个流真正并行执行的程度?
- A.GPU 中流多处理器 SM 的数量← 正确答案
- B.每个 SM 可驻留的线程块上限← 正确答案
- C.创建流时使用的变量名长度
- D.单个内核占用的寄存器与共享内存← 正确答案
答案:ABD。并发并行受 SM 数量、每个 SM 能容纳的线程块数量以及内核本身资源占用共同限制。变量名长度只是源码层的符号,不影响 GPU 执行。
Q7. 关于 cudaEventQuery(event) 的作用,正确的是?
- A.阻塞等待事件所在流全部执行完
- B.非阻塞地查询事件是否已完成← 正确答案
- C.将事件重新记录到它所在的流中
- D.销毁事件并释放占用的 GPU 资源
答案:B。cudaEventQuery 是非阻塞查询接口:如果事件已完成,返回 cudaSuccess;尚未完成则返回 cudaErrorNotReady。你可以把它当成看一眼“菜品出单号”盖没盖章,不用站在那干等。
Q8. 使用 CUDA 事件做内核计时,正确做法是?
- A.在目标流中先后记录 start 与 end← 正确答案
- B.在两个不同的流中分别记录两个事件
- C.等待 end 完成后调用 ElapsedTime← 正确答案
- D.记录事件后立即读取 elapsed 时间
答案:AC。计时的两个事件必须记录在同一条流中,才有先后可比性;且必须等 end 事件完成后再读耗时。跨流计时或不等完成就读时间,都会得到无意义或偏小的结果。
Q9. 关于隐式同步,以下描述正确的是?
- A.默认流从来不会影响其他流执行
- B.默认流操作可能与全设备同步等待← 正确答案
- C.隐式同步只发生在显存分配时
- D.隐式同步会提升内核并行效率
答案:B。默认流中的操作、同步拷贝、锁页内存分配等都可能触发隐式同步,让多个流被迫对齐,破坏并行。隐式同步通常会降低效率,而不是提升。
Q10. 以下关于 CUDA 流与事件的说法,正确的是?
- A.多流可用来重叠计算与数据传输← 正确答案
- B.同一流内的操作按顺序严格执行← 正确答案
- C.流的数量越多,加速效果一定越好
- D.事件可以在不同流之间充当同步信号← 正确答案
答案:ABD。多流最典型的用途是让拷贝与计算重叠;同一流内有序是流的基本语义;事件配合 cudaStreamWaitEvent 可以做流间同步。但流过多时受硬件资源制约,不会“无限加速升”。