深夜食堂开张了。你是这家餐厅的主厨,面前同时来了三份外卖订单。如果厨房只有一个灶台,你只能一道菜一道菜地炒——先做麻辣香锅,再做糖醋里脊,最后做清蒸鲈鱼,第一位顾客等到打瞌睡。可如果厨房里有三四个灶台呢?你可以同时开火:一个灶台爆炒,一个灶台炖汤,一个灶台蒸鱼。这就是GPU世界里CUDA Stream正在做的事——让并行成为习惯,让等待成为例外。 不过,灶台多了也会带来新烦恼:蒸鱼需要等汤底熬好才能淋汁,炒菜需要等配菜切完才能下锅。这时候,你需要一个“服务铃”——CUDA Event。它不参与做菜,只是在关键节点“叮”一声,告诉另一位厨师:“我这边好了,你可以继续了。”而那些动辄几百个步骤的宴席,与其现场指挥,不如提前画好一张完整流程图——这就是CUDA Graph的用武之地。 这篇文章将带你以“主厨视角”走进CUDA的并行调度厨房:从多流并发原理、流优先级、Event事件同步,到流间依赖管理、CUDA Graph、异步流水线和多GPU调度。全程没有晦涩的公式堆砌,只有厨房里的烟火气。
掌握CUDA Stream与Event,等于拿到了GPU并行调度的高级驾照。无论是大模型推理加速、多卡分布式训练,还是超高吞吐的数据管线,你都能从“能跑”进化到“榨干每一分算力”。
可视化演示
交互展示CUDA多流并发、事件同步机制和CUDA Graph调度策略,理解GPU并行计算的核心调度原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
多流并发执行机制 多个Stream独立并行执行Kernel GPU资源动态分配提升利用率 Stream[i] ∥ Stream[j] 异步执行最大化吞吐量
一、多流并发原理
想象你是深夜食堂的主厨,面前排着三份订单。如果只有一个灶台,你只能一锅一锅地炒:先做A,再做B,最后做C,顾客等得花儿都谢了。可要是厨房里能多加两个灶台呢?三个炉子同时开火,三份订单齐头并进,出餐速度一下就上来了。GPU的情况也差不多——它内部藏着成百上千个计算核心(SM,即流式多处理器),默认情况下却只用一个“灶台”干活,着实有点暴殄天物。
这个“灶台”在CUDA里叫Stream(流)。流是GPU上的一条工作队列,同一个流里的任务按先进先出的顺序执行;不同流里的任务,只要没有依赖关系,GPU就尽可能让它们同时干。默认流是程序启动时自动创建的那条队列,如果你不显式创建新流,所有任务都挤在这条单行道上,哪怕GPU明明有上百个空闲计算核心也只能干瞪眼。
创建新流用cudaStreamCreate,然后把kernel放到指定流上执行(比如kernel<<<gridDim, blockDim, 0, stream>>>)。要真正享受到多流加速,得满足两个条件:一是GPU还有空闲的计算资源(SM没被占满、显存带宽没饱和),二是不同流的任务之间没有数据依赖。满足之后,GPU的调度器会像一位忙而不乱的大厨,把空闲的锅分配给等待中的订单。
所以,多流并发的要义,不是把灶台从一个“变”成八个,而是把GPU里本来就有的灶台真正用起来。下次看到GPU利用率只有30%的时候,先问问自己:是不是一直让所有任务挤在默认流这条单行车道上?
- T₁、T₂两个任务各自单独执行所需的时间,可以理解为“两道菜的烹饪时长”。
- T₍串行₎ = T₁ + T₂串行模式总耗时等于两道菜一道一道做的时间之和,灶台从头到尾只服务一个订单。
- T₍并行₎ = max(T₁, T₂)并行模式总耗时等于最慢那道菜的时间,因为两个灶台同时开工,谁慢谁决定最终收工时间。
- S = T₍串行₎ / T₍并行₎加速比衡量“多灶台”相对“单灶台”快了多少倍。如果两道菜耗时一样,S≈2;如果一道菜独占几乎全部时间,S会接近1。
二、流优先级机制
深夜食堂来了位老顾客,他的订单必须优先出餐。你会怎么办?当然是在订单上贴一张“加急”标签,吩咐灶台先做他的菜。CUDA流的优先级,就是给不同灶台贴的“加急标签”。它不改变任何一道菜的做法,但改变了它们在上菜队列中的排队顺序。
CUDA中,创建流时可以指定优先级(cudaStreamCreateWithPriority),优先级数值越小表示优先级越高。高优先级流里的kernel会被调度器优先安排到空闲SM上执行。需要说明的是,低优先级流的任务并不会被强制打断(GPU通常在kernel边界进行调度切换),但在同等条件下,高优先级任务总是先上车。
这个机制最适合“一边做紧急的小任务,一边做不紧急的大任务”的场景。比如自动驾驶的感知线程需要毫秒级响应,它在一个高优先级流上跑;而离线建图、点云预处理在低优先级流上慢慢跑。这样即使GPU已被大任务塞满,紧急任务也能尽快获得计算资源,而不需要拆掉整个厨房重来。
用好优先级的前提是想清楚“谁等得起”。把等得起的粗活丢进低优先级流,把等不起的细活交给高优先级流,厨房的翻台率才能真正上一个台阶。
- T₍到达₎与T₍完成₎任务提交到流里的时刻、kernel真正执行完毕的时刻。
- L = T₍完成₎ - T₍到达₎任务从提交到完成的总延迟,相当于顾客从下单到拿到菜的等待时间。
- L₍高优₎ ≪ L₍低优₎高优先级流的任务延迟远小于低优先级流,这正是“加急标签”的效果——加急单总是先进厨房,普通单只能往后排。
三、事件同步机制
多灶台开火后,新的问题来了:灶台A的汤已经炖好,灶台B的菜还等着这锅汤来调味。怎么通知B?在CUDA里,我们摇响“服务铃”——Event(事件)。Event不参与做菜,它只在某个流执行到关键节点时“叮”一声,告诉其他流:“我这边好了,你可以继续了。”
Event是GPU上的一个标记点,它记录“某个流执行到此处”的时间戳。最常用的API组合是:cudaEventCreate创建事件,cudaEventRecord把事件记录到指定流上(相当于按下铃铛),另一个流通过cudaStreamWaitEvent等待这个事件(相当于听见铃声才继续)。此外,cudaEventSynchronize让CPU线程阻塞等待GPU执行到该Event,cudaEventElapsedTime则用两个Event精确测量GPU耗时。
这里的关键认知是:Event不是“让任务变快”,而是“让协作有节奏”。没有Event,多流就像几个各做各的厨师,最后可能出现“菜已经出锅但配菜还没切”的混乱;有了Event,依赖关系就变成了精确的时间约定。服务铃不是用来多响的,而是用在该响的时刻。
把Event插在真正需要衔接的关键节点,多流协作才有章法。否则就是满厨房铃铛乱响,厨师们反而手忙脚乱。
- t₍record₎Event被记录到流上那一刻的GPU时间,也就是“铃声响起的时刻”。
- t₍wait₎等待流的后续任务开始执行的时间。等待流的任务不会在t₍record₎之前启动。
- E ≺ W事件的偏序关系,读作“E先于W”。它保证“先发生的事一定发生在后”,GPU调度器据此安排依赖,避免数据竞争。
四、流间依赖管理
一个复杂菜单往往不是单线流程:佛跳墙可能要等“腌制”和“炒糖色”两条支线都完成,才能进入下一步。餐厅里的多灶台协作,本质上是一张依赖图。CUDA流间依赖管理,就是帮你把这张图画清楚、执行对。
多流依赖的核心API是cudaStreamWaitEvent(stream, event):让指定的流停下来,等event对应的那个时间点到了再继续。组合多个这类调用,就能搭出fork-join结构——一个流分出多路并行子任务,各路完成后汇合到依赖点上。比如“切割数据→两个流分别处理一半→事件合并→进入下一阶段”,这在数据并行计算中非常常见。
依赖图设计中最要命的是死锁:流A等流B的事件,同时流B等流A的事件,两个流都在那儿干瞪眼,GPU直接卡死。避免死锁要遵循两条基本原则——保持依赖无环(DAG,即有向无环图)、不要在同一个流里等待自己刚记录的事件。把依赖关系画成一张图,先找环,再造流,这就是多灶台协作的“排菜谱”工程。
依赖清晰了,多流并发才能既快又稳。记住:环是死锁的温床,无环是并行的底线。
- sᵢ、sₖ两个不同的流,可以理解成两个灶台(i是事件来源灶台,k是等待方灶台)。
- eⱼ来源流上记录的第j个Event,也就是来源流完成到某个进度的“铃声”。
- D = {(sᵢ, eⱼ, sₖ)}所有跨流依赖关系的集合。只要这个集合不构成环,调度就是安全的。
五、CUDA Graph调度
假设餐厅每天要接待几百桌客人,每桌的菜单几乎一样。与其每桌都口头吩咐一遍,不如把整个出餐流程画成一张标准流程图,厨房照着跑就行。CUDA Graph就是这张“标准流程图”——把一系列kernel及其依赖关系一次性捕获成图,之后想跑多少遍就按图执行。
传统方式下,每次调用kernel(放一个任务上灶台)都需要CPU和GPU来回“打招呼”(launch开销)。当kernel又短又多时,这种打招呼开销甚至超过kernel本身的计算时间。CUDA Graph先把一系列kernel及其依赖关系在流上“捕获”(capture)成一张图,然后实例化(instantiate)并一次性提交,GPU就能以极低的启动开销重放整个图。实例化后的图在GPU内部已经排好版,无需CPU逐次下发指令。
Graph特别适合AI推理场景——LLM逐层执行几十个短kernel,每层之间还有数据依赖。把这些kernel捕获成Graph后,单次推理的启动开销从“M次launch”变成“M次调度”的零头。当然,Graph不是银弹:如果每个kernel都重得像大佛跳墙,launch开销占比反而不高,Graph的收益就会非常有限。它是“小任务海量调用”场景下的厨房加速器。
下次写AI推理引擎时,如果发现kernel数量爆炸但每个都很短,记得掏出Graph这张“预绘流程图”,让GPU按图索骥、一气呵成。
- N被替换成Graph的kernel数量(大模型里几十上百层算子)。
- T₍launch₎传统方式下每个kernel的启动开销,包含CPU下发命令、GPU解析等耗时。
- T₍launch^graph₎Graph实例化后的启动开销,因为整个图已经预编译,这个值小到可以忽略。
- T₍节省₎ ≈ N × T₍launch₎节省的总时间约等于所有kernel的launch开销之和。
六、异步流水线工程
观察一下高档日料店的出餐线:备料师傅切好食材推到寿司师傅面前,寿司师傅捏好放到传菜口,传菜员立刻端走。没有谁等谁,每个人都只盯着自己这一步。这就是流水线。CUDA里的异步流水线也是同一个逻辑——让数据加载、计算、输出三个阶段像齿轮一样咬合转动,而不是一辆火车头拖着所有车厢走。
最简单的工程形态是双缓冲:准备两块显存buffer,流A把下一批数据从内存拷贝到GPU(数据加载),流B同时用上一批数据跑计算kernel(计算)。两个流各司其职、互不阻塞。更进一步可以拆成“加载→预处理→主计算→输出”四级流水线,每级分配一个流,中间用Event衔接进度。这样每时每刻,GPU要么在算,要么在等数据搬运工送来下一批料,几乎不会闲着。
流水线的核心指标是“周期时间τ”——每完成一个批次所需的最短间隔。公式告诉我们:流水线虽好,但启动第一段任务和收尾最后一段任务时,总有(k-1)个τ的“空泡”。任务批次n越大,空泡占比越小,流水线效率越高。所以流水线特别适合“大批量、持续涌入”的场景,比如训练循环里不断到来的数据batch。
流水线是“让所有人忙起来”的艺术。CPU在等GPU时,GPU在等数据时,都会产生空泡。用双缓冲和事件衔接把空泡填满,整条出餐线就永远热气腾腾。
- n任务批次数,比如要处理的数据块数量。
- k流水线级数,也就是拆成了几个阶段(加载、计算、输出……)。
- τ每一级的处理时间(假设各级均衡)。
- (n + k - 1) × τ首批任务要逐级填满流水线(多耗k-1个τ),之后每个τ出一批结果。
七、多GPU多流调度
等到餐厅开成连锁,光靠一个厨房就不够了。每家分店(GPU)都有自己的灶台(Stream),但分店之间要互相借调食材(跨设备数据),还得统一下班时间(全局同步)。多GPU调度,就是连锁餐厅的“中央调度室”。
多GPU环境首先要认清拓扑:GPU之间是走PCIe还是NVLink,带宽差别巨大(NVLink可达900GB/s,PCIe 4.0仅约32GB/s)。任务切分时,要尽量把需要频繁通信的数据放在同一块GPU上,用流把每块GPU内部的依赖安排明白;跨设备通信则通过点对点传输(如cudaMemcpyPeerAsync)挂在对应的事件后面,保证数据到达后再开始计算,而不是盲目地全量同步。
负载均衡是另一个重点。不同GPU的算力可能不同(比如两卡型号不一致),一味平均切分会让快的卡等慢的卡。简单有效的方式是按比例切分加动态调整:先用理论峰值估算比例,再根据实际吞吐微调。跨设备通信和同步点越少越好,因为每一条虚线箭头都可能拉低整体节奏。分店再多,食材转运的物流线要是堵了,整个连锁体系照样瘫痪。借助流和Event把每家分店的节奏调齐,才能让N块GPU真正拧成一股绳。
多GPU调度就是在“算得快”和“传得勤”之间找平衡。流和Event是把每家分店节奏调齐的节拍器,而拓扑感知和动态负载,则决定了整个连锁体系能走多快。
- W总工作量,可以理解为所有分店要做的菜的总量。
- N₍GPU₎与ηGPU数量和平均利用效率。η代表“每块GPU真正用来算账的时间占比”,数值越低说明空转越多。
- W / (N · η)理想切分下的计算时间——注意利用率越低,这个值越大,相当于分店越多但摸鱼的员工也越多。
- T₍通信₎跨设备数据传输与同步等待的额外时间,拓扑越差、切分越碎,这一项越高。
AI 应用场景
LLM推理服务加速:CUDA Graph + 多流
阶段:模型推理 / 部署优化
解决的问题:LLM每层有几十个短kernel和中间张量依赖,传统逐个launch时CPU-GPU通信开销可占单token耗时的30%以上。用CUDA Graph捕获整个解码过程的kernel序列,启动开销锐减80%-90%;再用多个Event管理并发请求流之间的依赖,避免互相踩踏。
具体案例:vLLM的Continuous Batching引擎中,把静态shape的prefill/decoding kernel用CUDA Graph缓存,推理服务吞吐提升约1.5-2倍,TTFT(首个token延迟)显著下降。事件同步用于调度多个并发请求流,确保每个请求的解码依赖顺序不被破坏。
训练数据管线:异步双缓冲
阶段:模型训练 / 数据管线
解决的问题:CPU上的数据加载(取数、增广)与GPU上的前反向计算天然串行,GPU经常“饿肚子”等数据。用多流+双缓冲能隐藏数据搬移时间,让GPU在上一批数据计算的同时,下一批数据已经在后台拷贝到显存。
具体案例:训练ResNet-50时,默认配置下GPU每步要等约15ms数据加载;在自定义训练循环中使用torch.cuda.Stream把“下一批数据拷贝到GPU”与“当前批前向反向”重叠后,训练吞吐提升约18%。这与PyTorch DataLoader提高prefetch_factor的思路殊途同归,但粒度更细、控制力更强。
多卡分布式训练:梯度分片通信
阶段:分布式训练 / 并行策略
解决的问题:数据并行下,每步训练结束后需要梯度all-reduce(跨卡通信)。若每块卡算完立即同步,通信期间其他卡都在等待,浪费严重。NCCL的all-reduce内部用多个Stream把梯度切块后分片通信,计算与通信通过Event精确对齐——先算完的梯度先发,不用等整层算完。
具体案例:在8卡A100上,采用梯度分片流水+事件同步后,相比“整层算完再all-reduce”的方式,训练端到端加速约20%。每块GPU内部的多个Stream分别负责计算、梯度切分、通信三件事,彼此的节奏由Event统一协调。
Python 演示
import torchimport time SIZE = 2048 def log(msg): print(f"[{time.strftime('%H:%M:%S')}] {msg}") def main(): if not torch.cuda.is_available(): print("当前环境无CUDA GPU, 仅演示代码逻辑") return device = torch.device('cuda', 0) log("准备两份大矩阵 (两道菜的食材)") a1 = torch.randn(SIZE, SIZE, device=device) b1 = torch.randn(SIZE, SIZE, device=device) a2 = torch.randn(SIZE, SIZE, device=device) b2 = torch.randn(SIZE, SIZE, device=device) log("串行模式: 先做菜1, 再做菜2") torch.cuda.synchronize() t0 = time.time() c1 = torch.matmul(a1, b1) c2 = torch.matmul(a2, b2) torch.cuda.synchronize() serial_time = time.time() - t0 log(f"串行耗时: {serial_time:.3f}s") s1 = torch.cuda.Stream(device=device) s2 = torch.cuda.Stream(device=device) log("多流模式: 两个灶台同时开工") torch.cuda.synchronize() t0 = time.time() with torch.cuda.stream(s1): c3 = torch.matmul(a1, b1) with torch.cuda.stream(s2): c4 = torch.matmul(a2, b2) torch.cuda.synchronize() parallel_time = time.time() - t0 log(f"多流耗时: {parallel_time:.3f}s") log(f"加速比: {serial_time / parallel_time:.2f}x") start_evt = torch.cuda.Event(enable_timing=True) end_evt = torch.cuda.Event(enable_timing=True) log("Event计时: 记录两个流的完成时刻") torch.cuda.synchronize() start_evt.record() with torch.cuda.stream(s1): c5 = torch.matmul(a1, b1) with torch.cuda.stream(s2): c6 = torch.matmul(a2, b2) end_evt.record() torch.cuda.synchronize() elapsed = start_evt.elapsed_time(end_evt) log(f"Event计时结果: {elapsed:.2f}ms") assert c1.allclose(c3) and c2.allclose(c4) if __name__ == "__main__": main()
s1 = torch.cuda.Stream(device=device)创建新流,相当于新开一个灶台,不同流上的CUDA操作可以并行执行。with torch.cuda.stream(s1):作用域内的所有CUDA操作都会提交到s1流上执行,而不是默认流。torch.cuda.synchronize()等待所有流上的任务全部完成,相当于等厨房里所有灶台熄火再计时。start_evt = torch.cuda.Event(enable_timing=True)创建支持计时功能的Event对象,enable_timing=True才允许计算耗时。start_evt.record()在默认流上记录起点时间戳,按下厨房的计时器。end_evt.record()在多流工作完成后记录终点时间戳,再次按下计时器。elapsed = start_evt.elapsed_time(end_evt)计算两个事件之间的毫秒耗时,比用CPU的time.time()更精确,因为它测的是GPU时间线。
常见误区
误区“我创建了8个Stream,把任务随便丢进去,GPU应该8倍速了吧!”
正解多流只是提供了并行的“可能性”,就像厨房里摆了8个灶台,但不代表每个灶台都有厨师和食材。任务要真正并行,需要GPU有空闲计算资源(SM、显存带宽),且任务之间没有数据依赖。如果所有任务都在抢同一批SM,或者互相有先后要求,多流不会加速,反而可能因为调度开销让整体变慢。正确做法是先看GPU占用率,再决定开几个流。
为何常见:把“多流”误解为“多GPU”,以为计算资源是独立扩大的;实际上GPU底层资源固定,多流只是让这些资源别闲着,而不是凭空多出资源。
误区“我不碰默认流,只用自定义流,它们之间肯定互不干扰,可以完全并行。”
正解默认流(legacy default stream)有隐式同步行为——它会让所有之前提交到其他流的操作先执行完,再运行自己;同样,其他流也要等默认流执行完。也就是说,只要默认流还在参与调度,整条时间线就会出现“强制串行”的关卡。要让多流真正并行,需要确保所有操作都提交到显式创建的流上,不依赖默认流。
为何常见:从CPU多线程思维迁移,以为互不依赖的任务天然并行,没意识到GPU流有隐式同步规则。默认流是CUDA遗留的历史包袱,新手最容易在这里栽跟头。
误区“Event开销很小,我在每个kernel后面都插一个Event,再让其他流逐个等待,肯定最稳妥。”
正解Event本身是轻量的,但cudaStreamWaitEvent会强制一个流停下来等待另一个流的时间点,这不只是“标记”,而是给GPU调度器套上枷锁。过度使用会造成虚假依赖和调度气泡(bubble),就像厨房里每道菜都要摇一次铃,厨师们反而被铃声绑住了手脚。正确做法是只在真正的数据依赖处使用Event,让调度器保留最大的自由空间。
为何常见:把同步当作“保险”,潜意识里认为多同步=更正确。但同步本身就是一种串行化成本,用多了会让多流并行的收益化为乌有。
误区“把kernel用Graph包一层,代码变复杂了,难道还能比手动launch更快?”
正解Graph最大的收益不是kernel本身变快,而是大幅降低kernel launch的CPU-GPU往返开销。对几百个短小kernel的调用(如LLM逐层推理),传统launch有大量CPU空转等待;Graph把整个依赖图一次性下发,GPU内部自行调度,启动开销降低一个数量级。但也要注意:如果kernel本身是大规模计算(比如单次矩阵乘法就占几十毫秒),launch开销占比很小,Graph收益就有限,不应盲目引入。
为何常见:直觉上以为优化只能围绕“缩短计算时间”,忽略了调度开销在短kernel场景下才是真正的瓶颈。Graph解决的问题是“指令下发”而不是“执行效率”。
面试题
Q1. CUDA中默认流(legacy default stream)与自定义流的关系是?
- A.与自定义流完全并行互不干扰
- B.默认流隐式同步致其他流串行← 正确答案
- C.默认流优先级更高抢占自定义流
- D.自定义流必须显式等待默认流完成
答案:B。默认流有隐式同步规则,它会让其他流的操作等待自己,反之亦然。这相当于厨房里最老的那个灶台有“独占通行证”,其他灶台都得让它先走。A把默认流当普通流是错的;C在CUDA规范里没有依据;D方向反了,是默认流让其他流等它。
Q2. 多流并发能加速的根本前提是?
- A.创建出足够多的流数量
- B.GPU还存在空闲的计算资源← 正确答案
- C.每个流绑定不同的CPU线程
- D.使用更高版本的CUDA版本
答案:B。多流只是调度手段,真正加速靠的是GPU资源并行不冲突。就像厨房摆了8个灶台,但如果只有2个厨师,多出来的灶台也是摆设。A是常见误解,流多不代表算力多;C的CPU线程数量与GPU流并行无关;D与CUDA版本关系不大,核心是资源是否空闲。
Q3. 关于流优先级,下列说法正确的是?
- A.优先级数值越大代表级别越高
- B.高优先级可打断已执行的kernel
- C.调度时会优先执行高优先级流← 正确答案
- D.所有GPU都支持八级以上的优先级
答案:C。CUDA中优先级数值越小越高,A反了;GPU通常在kernel边界调度,不会打断已在执行的kernel,B错;优先级是否支持及级数因GPU架构而异,D太绝对。C正确:高优先级流中的任务在同等条件下会先被调度到空闲SM上,就像加急单插队到普通单前面。
Q4. Event在CUDA中可以用于哪些用途?
- A.记录流中某操作的完成时间点← 正确答案
- B.让另一个流等待该时间点再继续← 正确答案
- C.直接修改kernel内部的计算逻辑
- D.测量两个时间点之间的GPU耗时← 正确答案
答案:ABD。Event本质是GPU上的时间戳与同步标记,能记录完成点(A)、让另一流等待(B)、测量耗时(D)。但Event运行在GPU调度层,无法进入kernel内部修改计算逻辑(C),这就像服务铃只能通知“菜好了”,不能改变厨师做菜的手法。
Q5. cudaStreamWaitEvent(stream, event)的作用是?
- A.让event等待stream执行完成
- B.让stream等待该事件后再执行后续← 正确答案
- C.立即触发event并通知所有流
- D.将stream中所有待执行任务取消
答案:B。这个API让指定的流“停下来等Event时间点到达再继续”,方向是stream等event。A把依赖关系颠倒了;C的“立即触发”是cudaEventRecord的职责;D完全不是同步API的功能。就像服务员对厨师说“听到铃声再炒下一道菜”——是厨师(stream)在等铃(event)。
Q6. 为什么CUDA Graph能显著降低kernel启动开销?
- A.移除了kernel计算直接输出结果
- B.将多个kernel启动合并为一次通信← 正确答案
- C.让kernel改在CPU端执行计算
- D.默认关闭所有流之间的同步
答案:B。CUDA Graph先把kernel序列捕获成图,再一次性下发到GPU,把M次CPU-GPU往返合并成一次。kernel本身仍在GPU上计算,A和C荒谬;Graph并不默认关闭同步,它只是把明确的依赖关系固化在图里,D错。就像把整个宴席的流程写成一张菜单传给厨房,而不是每道菜都去后厨口头交代一遍。
Q7. 下面哪些做法有助于避免多流死锁?
- A.构建依赖图时确认不存在有环结构← 正确答案
- B.让每个流只等待真正依赖的Event← 正确答案
- C.流A和流B互等对方的Event再执行
- D.让流等待自身内部已记录的Event
答案:AB。死锁的根源是循环等待。A从结构上保证无环,B从依赖粒度上避免虚假等待,都是正确做法。C是典型的AB互等死锁:流A等B的Event、B等A的Event,两个灶台互相等对方摇铃,永远僵持;D是自己等自己,同样不可能等到。多流依赖设计要像画菜谱流程图一样,先检查有没有环。
Q8. 在异步流水线中,双缓冲(double buffering)的核心思想是?
- A.用两份显存交替存储过程数据
- B.一个流拷贝数据时另一流同时计算← 正确答案
- C.将数据分两份各用一半GPU资源
- D.让两个GPU同时计算同一份数据
答案:B。双缓冲的精髓是传输与计算重叠:一块buffer被计算kernel占用时,另一块buffer正在接收下一批数据。A只说对了一半——交替存储只是手段,B才是目的;C把数据切分误解为并行策略;D涉及多GPU,与双缓冲概念不在同一层面。就像日料店里寿司师傅捏当前一贯时,备料师傅已经在切下一批鱼了。
Q9. 在多GPU多流调度中,需要考虑哪些因素?
- A.各GPU的拓扑位置和通信带宽← 正确答案
- B.跨设备通信带宽对任务切分影响← 正确答案
- C.每个GPU上流的数量必须完全相等
- D.全局同步点设置的频繁程度← 正确答案
答案:ABD。多GPU调度像管理连锁餐厅分店,拓扑(NVLink/PCIe)决定食材转运速度(A);通信带宽影响任务怎么切分(B);同步点越少越好,否则快的分店总在等慢的(D)。C是错误认知——每个GPU上流的数量完全可以根据负载动态调整,不需要强制相等。
Q10. 下列哪种操作属于“Event用于时间测量”的正确用法?
- A.用两个Event记录后计算时间差← 正确答案
- B.用CPU端的clock测GPU时间
- C.用cudaStreamWaitEvent测量延迟
- D.统计一个流中Event出现的总次数
答案:A。正确做法是cudaEventRecord记录起点和终点,再用elapsed_time计算差值,这是GPU时间线上的精确测量。B的CPU时钟测不到GPU端真实执行时间;C的WaitEvent是同步机制,不是计时工具;D的统计次数毫无计时意义。就像计时要用专门的秒表,而不是数服务员摇了几次铃。