CUDA Kernel Launch 开销测量

基于 RTX 5090 实测。测量空 kernel 的 launch 延迟,以及不同 grid/block 配置对 launch 开销的影响。这是判断”计算该放 CPU 还是 GPU”的关键数据。


1. 为什么关心 Launch 开销

每个 CUDA kernel 的启动都有固定开销:

CPU 提交命令 → CUDA Driver 处理 → GPU 调度 → 执行 → 完成通知

如果 kernel 计算量 < launch 开销,GPU 加速反而变成减速。阈值通常在 10-50 μs 量级,但不同 GPU 差异显著。


2. 测试程序

cat > launch_lat.cu << 'EOF'
#include <cuda_runtime.h>
#include <stdio.h>

#define CHECK(c) do {                                      \
    cudaError_t r = c;                                     \
    if (r != cudaSuccess) {                                \
        printf("Error: %s\n", cudaGetErrorString(r));      \
        exit(1);                                           \
    }                                                      \
} while(0)

__global__ void empty() {}

int main() {
    cudaEvent_t s, e;
    float t;
    CHECK(cudaEventCreate(&s));
    CHECK(cudaEventCreate(&e));

    // === Single empty kernel ===
    CHECK(cudaEventRecord(s, 0));
    empty<<<1, 1>>>();
    CHECK(cudaEventRecord(e, 0));
    CHECK(cudaEventSynchronize(e));
    CHECK(cudaEventElapsedTime(&t, s, e));
    printf("Empty kernel (1 thread): %.2f us\n", t * 1000);

    // === Average over 10000 launches ===
    for (int i = 0; i < 100; i++) empty<<<1, 1>>>();
    CHECK(cudaDeviceSynchronize());

    CHECK(cudaEventRecord(s, 0));
    for (int i = 0; i < 10000; i++)
        empty<<<1, 1>>>();
    CHECK(cudaEventRecord(e, 0));
    CHECK(cudaDeviceSynchronize(e));
    CHECK(cudaEventElapsedTime(&t, s, e));

    printf("Average (10k iters): %.3f us\n", t * 1000 / 10000);

    // === Different block counts ===
    int blocks[] = {1, 32, 256, 1024, 4096, 16384};
    printf("\n%-8s | %-15s\n", "Blocks", "Time (us)");
    printf("---------|----------------\n");
    for (int i = 0; i < 6; i++) {
        CHECK(cudaDeviceSynchronize());
        CHECK(cudaEventRecord(s, 0));
        for (int j = 0; j < 1000; j++)
            empty<<<blocks[i], 256>>>();
        CHECK(cudaEventRecord(e, 0));
        CHECK(cudaEventSynchronize(e));
        CHECK(cudaEventElapsedTime(&t, s, e));
        printf("%-8d | %-15.2f\n", blocks[i], t);
    }

    CHECK(cudaEventDestroy(s));
    CHECK(cudaEventDestroy(e));
    return 0;
}
EOF

nvcc -o launch_lat launch_lat.cu
./launch_lat

3. RTX 5090 实测

Empty kernel (1 thread): 14363.71 us     ← 首次 launch,含 driver 初始化
Average (10k iters):     2.647 us       ← 稳定后每次 launch 开销

Blocks   | Time (us)
---------|----------------
1        | 2.34           ← 1000 次 empty kernel 总时间
32       | 2.35
256      | 2.36
1024     | 2.38
4096     | 2.40
16384    | 8.24           ← 16k blocks 触发额外调度开销

3.1 关键解读

指标 说明
首次 launch 14.4 ms 含 CUDA driver 初始化、context 建立
稳定 launch 2.6 μs 正常 kernel 提交延迟
block 数 < 4k ~2.4 μs 调度负载可忽略
block 数 16k ~8.2 μs 大量 block 的硬件调度队列开销

4. CPU vs GPU 决策边界

以 RTX 5090 的 2.6 μs launch + ~10 μs PCIe H2D/D2H = ~13 μs 总开销为基准:

假设: CPU 单核 4 GHz, 4 FLOPs/cycle = 16 GFLOPS
      GPU RTX 5090 ≈ ~100 TFLOPS (BF16 Tensor Core)

不同计算密度下 GPU 胜出的最小数据量:
  算数密度 1 op/el   → GPU 胜出 > 200K 元素 (13μs × 16GFLOPs = 208K ops)
  算数密度 10 ops/el  → GPU 胜出 > 20K 元素
  算数密度 1000 ops/el → GPU 胜出 > 200 元素 (矩阵乘法等)
场景 数据量 推荐 原因
逐元素 ReLU < 100K CPU launch + PCIe > 计算
向量内积 > 10K GPU 计算开始主导
矩阵乘法 1024×1024 任何 GPU 计算时间远大于开销

经验法则:GPU kernel 总时间 < 20 μs 则 CPU 可能更快;批量处理是 GPU 高效的关键——一次传输 100K 元素比 100 次 1K 传输高效 100×。


5. 影响 Launch 开销的因素

因素 影响
CUDA context 初始化 首次 launch ~10-15 ms,后续 ~2-3 μs
Block 数量 < 4096 无影响,> 16k 开始显著
Shared memory 大小 每个 block 的 shared mem 分配有微小开销
Stream 并发 多 stream 的 launch 可 pipeline,但单个 launch 延迟不变
CUDA Graph 显著降低:通过预录制消除单次 launch 开销

5.1 用 CUDA Graph 消除 Launch 开销

对于重复执行的小 kernel,CUDA Graph 将多次 launch 合并为一次:

cudaGraph_t graph;
cudaGraphExec_t instance;

// 录制
cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal);
for (int i = 0; i < 1000; i++)
    kernel<<<1, 256, 0, stream>>>();
cudaStreamEndCapture(stream, &graph);

// 实例化
cudaGraphInstantiate(&instance, graph, NULL, NULL, 0);

// 执行(一次 launch 替代 1000 次)
cudaGraphLaunch(instance, stream);

6. 与 PCIe 延迟的关系

总 GPU 操作延迟 ≈ Launch 开销 + PCIe 传输时间:

操作 延迟
Kernel launch 2.6 μs
PCIe H2D (1 KB) 11.1 μs (含 ~10 μs PCIe TLP 往返)
PCIe D2H (1 KB) 10-12 μs
nvbandwidth Host↔Device 621 ns (纯 PCIe 链路延迟)

PCIe 有一次性的 ~10 μs 往返开销(TLP 打包/解包),之后每字节传输接近线速。这是为什么批量传输比逐次小传输高效几个数量级


参考