4. 多流并发性能剖析工具链

多流并发写出来容易,跑得快却不一定。很多开发者把 kernel 拆到多个 stream 后,发现总耗时几乎没变,甚至更慢。原因通常不在“流”本身,而在于:某个环节把并发重新串了起来,或者 GPU 在等待中出现了大量空闲气泡。要定位这类问题,必须依赖工具链把时间轴和流级指标摊开来看。本节围绕三件工具能力展开:Nsight Systems 的时间线、CUDA Profiler 的流级指标,以及从这两者中识别串行化与气泡的方法。

一、Nsight Systems:把并发“画”在时间线上

Nsight Systems(nsys)是系统级时间线剖析器,它回答的核心问题是“这段时间里,CPU 在做什么、GPU 在做什么、谁在等谁”。对多流场景,它最有价值的是把每个 stream 的 kernel、memcpy、同步点按时间轴排布,让并发与串行一目了然。

典型采集命令如下,示例针对一个使用 4 条流做流水线的程序:

nsys profile \
  --trace=cuda,nvtx,osrt \
  --cuda-memory-usage=true \
  --sample=none \
  --output=multistream_report \
  ./pipeline_demo --streams=4 --iters=200

采集后用 nsys stats multistream_report.nsys-rep 或 GUI 打开 .nsys-rep。在时间线视图中,需要重点观察四类信息:

  • GPU 行(CUDA HW):所有 stream 的 kernel 与 memcpy 都汇聚在这一行,能直接看到是否真正重叠。
  • Stream 行:每条流单独一行,显示该流内部的执行顺序与间隔。
  • CPU 行:显示 launch、cudaMemcpyAsync、cudaStreamSynchronize 等 API 的调用时刻与耗时。
  • NVTX 标记:如果代码里插入了 nvtxRangePush/Pop,可以把“第 i 次迭代”这类逻辑区间标出来,便于对齐。

判断并发的第一眼,就是看 CUDA HW 行上的色块是否连续、是否有多条流的色块在时间上重叠。如果所有色块首尾相接、颜色单一,说明并发根本没发生。

二、CUDA Profiler:流级指标量化并发度

时间线是定性的,CUDA Profiler(ncu / nvprof 的继任者,以及 nsys 的统计视图)提供的是定量指标。对多流分析,最关键的几个流级/并发级指标是:

指标含义多流场景下的判读
Kernel 并发数(Concurrent Kernels)同一时刻在 GPU 上执行的 kernel 数量长期为 1,说明并发未生效
Stream 利用率各 stream 占用 GPU 时间的比例某条流占比过高,负载不均
GPU 空闲时间(Idle)GPU 无任何 kernel/memcpy 的时间即“气泡”,是优化重点
Memcpy 与 Kernel 重叠率拷贝与计算在时间上的重叠比例接近 0 说明拷贝串行在计算前
同步等待时间cudaStreamSynchronize / Event 等待耗时过长说明 CPU 侧同步点过密

用 ncu 采集流级指标时,可指定 metrics 集合:

ncu --metrics \
  sm__throughput.avg.pct_of_peak_sustained_elapsed,\
  gpu__time_duration.sum,\
  launch__grid_size,\
  launch__block_size \
  --launch-skip 20 --launch-count 40 \
  ./pipeline_demo --streams=4

注意 ncu 默认会串行化 kernel 以做精确计数,因此它更适合看单 kernel 的微观指标,而不是看并发时间线。并发时间线仍以 nsys 为准,两者配合使用:nsys 找“哪里串了”,ncu 看“串的那个 kernel 本身是否高效”。

三、识别串行化与气泡的四种典型模式

把时间线和指标对照,串行化与气泡通常表现为以下四种模式,识别它们就能快速定位根因。

模式一:默认流(legacy default stream)隐式同步。如果代码里混用了默认流和显式流,默认流会与所有阻塞流隐式同步,导致并发被强制串行。时间线上表现为:显式流的 kernel 之间总是被默认流的操作隔开,且 CUDA HW 行出现规律性空隙。修复方式是统一使用显式流,或编译时加 --default-stream per-thread。

模式二:同步点过密。每次迭代都调用 cudaStreamSynchronize 或 cudaDeviceSynchronize,CPU 被迫等待 GPU,GPU 在两次迭代之间出现气泡。时间线上 CPU 行出现长条等待,CUDA HW 行出现周期性空白。修复方式是减少同步频率,用 Event 做跨流依赖而非全设备同步。

模式三:拷贝与计算未重叠。如果先做一次大 H2D 拷贝,再启动 kernel,拷贝期间 GPU 计算单元空闲。时间线上 memcpy 色块与 kernel 色块首尾相接、无重叠。修复方式是分块拷贝、分块计算,用多流做流水线。

模式四:负载不均。4 条流中某条流的 kernel 特别长,其他流早早结束,GPU 尾部只剩一条流在跑。时间线上表现为尾部单色块。修复方式是重新划分任务粒度,让各流工作量接近。

一个实用技巧:在代码中用 NVTX 把每次迭代标记为 iter_i,然后在 nsys 时间线上按迭代对齐,能非常直观地看出“第 i 次迭代的气泡出现在哪个阶段”。

四、从时间线到优化动作的闭环

完整的剖析闭环是:先用 nsys 采集时间线,定位气泡和串行段;再用 ncu 对可疑 kernel 做微观分析;修改代码后重新采集,对比 GPU 空闲时间和并发 kernel 数是否改善。判断优化是否成功的量化标准可以设为:GPU 空闲时间占比从 X% 降到 Y% 以下,或平均并发 kernel 数从 1 提升到接近流数。

下面这张图把“时间线观察 → 指标判读 → 根因 → 修复动作”的对应关系整理成一张诊断图,便于在实际剖析时对照使用。

多流并发剖析诊断图:从时间线到修复动作 ① Nsight Systems 时间线观察 • CUDA HW 行色块是否重叠 • Stream 行内部间隔 • CPU 行同步等待长条 • memcpy 与 kernel 是否重叠 • NVTX 迭代标记对齐 输出:气泡位置 + 串行段 ② CUDA Profiler 指标判读 • Concurrent Kernels 长期=1 • GPU Idle 占比偏高 • Memcpy/Kernel 重叠率≈0 • 同步等待时间过长 • 各 Stream 利用率不均 输出:量化并发度与气泡占比 ③ 根因归类 A. 默认流隐式同步 B. 同步点过密 C. 拷贝与计算未重叠 D. 流间负载不均 E. 资源竞争(SM/带宽) 输出:可执行的修复方向 ④ 修复动作与验证标准 A 默认流同步 统一显式流 --default-stream per-thread 验证:并发数↑ B 同步点过密 Event 替代全同步 降低同步频率 异步回调 验证:CPU 等待↓ C 拷贝未重叠 分块拷贝+计算 多流流水线 Pinned 内存 验证:重叠率↑ D 负载不均 重划分任务粒度 动态调度 均衡各流工作量 验证:尾部气泡↓ 修改后重新采集 → 对比 GPU Idle 与并发 kernel 数 → 形成优化闭环

需要强调的是,工具给出的只是现象,根因判断仍要结合代码逻辑。例如时间线上出现气泡,可能是同步点,也可能是 CPU 侧 launch 太慢导致 GPU 等指令。此时应结合 CPU 行的 API 耗时来判断:如果 CPU 行上 cudaLaunchKernel 本身耗时很长,说明是主机端瓶颈,而非 GPU 并发问题。把时间线、流级指标和代码逻辑三者对齐,才能真正把多流并发调优做到位。