CUDA Streams 并发实战
基于 RTX 5090 (2 个 async copy engine) 和 A100-SXM4-80GB (3 个 async copy engine) 双平台实测。本文演示 CUDA Streams 如何实现 H2D + Kernel + D2H 重叠执行,并测量实际加速比。
1. 背景
1.1 Default Stream vs Explicit Streams
CUDA 的默认 stream(stream 0)是同步的——所有操作按提交顺序串行执行:H2D0 → Kernel0 → D2H0 → H2D1 → Kernel1 → D2H1。这意味着 GPU 的 compute engine 在数据传输时空闲,copy engine 在 kernel 执行时空闲——两个 engine 交替闲置,硬件利用率低下。
显式 stream(cudaStreamCreate)打破了这种串行约束。每个 stream 内部仍然保持顺序,但不同 stream 之间可以并发。当 stream 0 的 kernel 在执行时,stream 1 的 H2D 可以同时进行——GPU 的三个硬件引擎(2 个 copy engine + 1 个 compute engine)可以同时忙。
1.2 硬件并发能力
RTX 5090 有 2 个 async copy engine,理论最多可实现 2 路并发数据传输。加上 compute engine,最多 3 个操作可同时进行。A100-SXM4-80GB 有 3 个 async copy engine,理论上可实现 3 路并发数据传输(4 个操作同时进行)。但实际并发度都受限于:
- PCIe 带宽共享:多个 copy engine 共享同一 PCIe 链路(RTX 5090: Gen 5 ×16 ~63 GB/s;A100: Gen 4 ×16 ~32 GB/s 理论值)。如果多个 engine 同时做 H2D,带宽被均分。
- H2D 和 D2H 的方向冲突:H2D(写 GPU)和 D2H(读 GPU)共享 PCIe 双向带宽。同时做 H2D + D2H 时,双向带宽可能小于两个单向之和(nvbandwidth 实测:单向 56.3 GB/s,双向各 50.3 GB/s)。
- Kernel 执行时长:只有当 kernel 执行时间与数据传输时间大致匹配时,才能最大化重叠。如果 kernel 太短,数据传输追不上;如果 kernel 太长,copy engine 闲置。
1.3 测试策略
以下程序用 4 个 stream 各处理 256 MB 数据(含 H2D + kernel + D2H),对比串行和并发的总耗时。kernel 的循环次数 (K=1024) 经过调整,使单 stream 的计算时间与传输时间大致相当——这是最大化重叠的前提。
2. 对比测试程序
cat > stream_overlap.cu << 'EOF'
#include <cuda_runtime.h>
#include <stdio.h>
#define N (64 * 1024 * 1024) // 256 MB per buffer
#define K 1024 // dummy compute iterations
#define STREAMS 4
#define CHECK(cmd) do { \
cudaError_t e = cmd; \
if (e != cudaSuccess) { \
printf("Error: %s\n", cudaGetErrorString(e)); \
exit(1); \
} \
} while(0)
__global__ void dummy_kernel(float *d, int n, float s) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
float x = d[i];
for (int j = 0; j < K; j++)
x = x * s + 1.0f;
d[i] = x;
}
}
int main() {
float *h[STREAMS], *d[STREAMS];
cudaStream_t s[STREAMS];
for (int i = 0; i < STREAMS; i++) {
CHECK(cudaMallocHost(&h[i], N * sizeof(float)));
CHECK(cudaMalloc(&d[i], N * sizeof(float)));
CHECK(cudaStreamCreate(&s[i]));
}
cudaEvent_t start, stop;
CHECK(cudaEventCreate(&start));
CHECK(cudaEventCreate(&stop));
// === Test 1: Sequential (no streams) ===
CHECK(cudaEventRecord(start, 0));
for (int i = 0; i < STREAMS; i++) {
CHECK(cudaMemcpy(d[i], h[i], N * sizeof(float),
cudaMemcpyHostToDevice));
dummy_kernel<<<(N + 255) / 256, 256>>>(d[i], N, 1.0001f);
CHECK(cudaMemcpy(h[i], d[i], N * sizeof(float),
cudaMemcpyDeviceToHost));
}
CHECK(cudaDeviceSynchronize());
CHECK(cudaEventRecord(stop, 0));
CHECK(cudaEventSynchronize(stop));
float t_seq;
cudaEventElapsedTime(&t_seq, start, stop);
// === Test 2: Concurrent with streams ===
CHECK(cudaEventRecord(start, 0));
for (int i = 0; i < STREAMS; i++) {
CHECK(cudaMemcpyAsync(d[i], h[i], N * sizeof(float),
cudaMemcpyHostToDevice, s[i]));
dummy_kernel<<<(N + 255) / 256, 256, 0, s[i]>>>(d[i], N, 1.0001f);
CHECK(cudaMemcpyAsync(h[i], d[i], N * sizeof(float),
cudaMemcpyDeviceToHost, s[i]));
}
CHECK(cudaDeviceSynchronize());
CHECK(cudaEventRecord(stop, 0));
CHECK(cudaEventSynchronize(stop));
float t_async;
cudaEventElapsedTime(&t_async, start, stop);
float total = ((float)N * sizeof(float) * 3 * STREAMS)
/ (1024.0 * 1024.0 * 1024.0);
printf("Data: %.0f MB x %d streams = %.1f GB total\n",
(float)N * sizeof(float) / (1024 * 1024), STREAMS, total);
printf("Sequential: %7.2f ms (%.1f GB/s)\n",
t_seq, total / t_seq * 1000);
printf("Streamed: %7.2f ms (%.1f GB/s)\n",
t_async, total / t_async * 1000);
printf("Speedup: %.2fx\n", t_seq / t_async);
for (int i = 0; i < STREAMS; i++) {
CHECK(cudaFreeHost(h[i]));
CHECK(cudaFree(d[i]));
CHECK(cudaStreamDestroy(s[i]));
}
return 0;
}
EOF
nvcc -o stream_overlap stream_overlap.cu
./stream_overlap
3. 实测结果
RTX 5090 输出:
Data: 256 MB x 4 streams = 3.0 GB total
Sequential: 63.81 ms (47.0 GB/s)
Streamed: 27.01 ms (111.1 GB/s)
Speedup: 2.36x
3.1 为什么不是 3x
2 个 async copy engine + 1 个 compute engine 理论上可 3 路并发,但实际受限于:
- D2H 和 H2D 共享 copy engine:4 个 stream 的 8 次传输竞争 2 个 engine
- PCIe 半双工特性:同时 H2D + D2H 需要 PCIe 双向带宽(nvbandwidth 实测双向 ~50 GB/s vs 单向 ~56 GB/s)
- Kernel 执行时间不完美匹配:dummy kernel 时长可能不精确等于传输时间
4. A100 实测结果(官方 cuda-samples)
以下使用 NVIDIA 官方 cuda-samples 13.1 中的 simpleStreams、simpleHyperQ、simpleMultiCopy 在 A100-SXM4-80GB (CC 8.0, 3 copy engines) 上验证。
4.1 simpleStreams — 基础流并发
[simpleStreams] - GPU: NVIDIA A100-SXM4-80GB, 6912 Cores
> array_size = 16777216 (64.00 MB)
memcopy: 2.80 ms
kernel: 32.33 ms
non-streamed: 2.80 ms
4 streams: 2.57 ms
A100 的 4 stream 重叠比非重叠仅快 ~8%。这是因为 kernel 执行时间 (32.33 ms) 远大于 memcopy 时间 (2.80 ms),kernel 成为瓶颈后数据拷贝的重叠空间被压缩。
4.2 simpleHyperQ — 32 路硬件队列并发
A100 支持 HyperQ——32 个独立的硬件工作队列,允许多个 stream 真正并行执行:
[simpleHyperQ] - GPU: NVIDIA A100-SXM4-80GB
Expected serial: 0.330s - 0.640s
Expected concurrent: ~0.020s
Measured: 0.027s
实测 0.027s,接近理论并发时间。32 路 kernel 在执行时间上几乎完全重叠——HyperQ (32 硬件工作队列) 是 A100 数据中心 GPU 的标志性能力。消费级 GPU 通常不支持或仅有受限的 HyperQ 功能。
4.3 simpleMultiCopy — 多 copy engine 带宽
[simpleMultiCopy] - GPU: NVIDIA A100-SXM4-80GB
Memcpy H2D: 2.72 ms (6.17 GB/s)
Memcpy D2H: 1.96 ms (8.54 GB/s)
Kernel: 0.004 ms
Avg. serialized: 1.638 ms
Avg. 4 streams: 1.682 ms
Speedup: -0.044 ms (negative!)
为什么重叠反而更慢? Kernel 只有 0.004 ms——太轻量了。4 个 stream 的管理开销超过了重叠收益。这揭示了一个关键瓶颈:只有当 kernel 计算时间与传输时间相近时,重叠才有收益。本例中 kernel 比传输快 500 倍,GPU compute engine 在 99.8% 时间里空等数据。
关键推论:A100 有 3 个 copy engine 仍出现负加速比,说明 engine 数量不是瓶颈——kernel/传输比才是。始终用 Nsight Systems 确认重叠是否实际发生。
5. Nsight Systems 可视化 (概念)
在 Nsight Systems 中,stream 并发效果体现为时间线上各 stream 的操作互相交错:
Stream 0: [H2D ][Kernel ][D2H ]
Stream 1: [H2D ][Kernel ][D2H ]
Stream 2: [H2D ][Kernel ][D2H ]
Stream 3: [H2D ][Kernel ][D2H ]
Time: 0ms ──────────────────────────────── 27ms
没有 stream 的时序:
Default: [H2D0][K0][D2H0][H2D1][K1][D2H1][H2D2][K2][D2H2][H2D3][K3][D2H3]
Time: 0ms ──────────────────────────────────────────────────── 64ms
6. Stream 最佳实践
| 实践 | 说明 |
|---|---|
使用 cudaMemcpyAsync |
非阻塞拷贝是重叠的前提 |
| 每个 stream 独立 buffer | 避免 data hazard,每个 stream 有独立的 H2D/Kernel/D2H buffer |
pinned memory (cudaMallocHost) |
普通 pageable memory 无法异步 H2D |
| 先 issue 所有 work,再同步 | issue 阶段无阻塞,GPU 调度器自行安排并发 |
| Nsight Systems 验证 | 眼见为实——工具确认重叠是否真的发生 |