启动开销
影响 中
2026-08-06
小 kernel 过多,launch 开销主导整体耗时
这个问题在推理场景和小 batch 训练里特别常见 —— 算子拆得很细,每个 kernel 都只处理一点点 数据,最后时间全花在「发起」而不是「计算」上。
现象长什么样
- 时间线放大后是成百上千个宽度只有几微秒的小块
- 相邻 kernel 之间有宽度相当稳定的空隙(这一点很关键,是固定开销的特征)
- 把所有 kernel 耗时加起来,明显小于这段区间的墙上时间
- CPU 侧某个线程一直忙于调用 CUDA API
为什么会这样
一次 kernel launch 的成本大致包含:
| 阶段 | 典型量级 |
|---|---|
| CPU 侧 API 调用 + 参数打包 | 几微秒 |
| 命令写入队列、驱动处理 | 几微秒 |
| GPU 侧取指、调度、启动 | 亚微秒到几微秒 |
这些成本基本与数据量无关。所以当 kernel 本体是 500 微秒时,它可以忽略; 但当 kernel 本体只有 3 微秒时,开销和计算量级相同,效率直接腰斩。
还有一个容易忽略的点:如果 CPU 侧下发速度赶不上 GPU 消费速度,GPU 就会饿死 —— 此时时间线上的空隙不是 GPU 在忙,而是它在等活干。
怎么确认是这个原因
先看总量对比:
nsys profile --stats=true -o launch_check ./your_app
在输出的 cuda_gpu_kern_sum 里关注两个数:kernel 总数、以及平均耗时。
# 直接看 kernel 数量级和平均耗时
nsys stats --report cuda_gpu_kern_sum launch_check.nsys-rep
判据比较直接:
- 平均 kernel 耗时 < 10 微秒,且 kernel 数以万计 → 基本可以确认是这个问题
- 平均耗时在几百微秒以上 → launch 开销可以忽略,去查别的方向
解法
-
CUDA Graph。 把一段固定的 kernel 序列录制成 graph,一次提交整段, 省掉每个 kernel 的 CPU 下发成本。对形状固定的推理图收益最明显。
-
算子融合。 把 elementwise 链、激活、归一化之类的相邻小算子合并成一个 kernel, 既省 launch 又省显存往返。
-
增大每次的工作量。 提高 batch、把循环搬进 kernel 内部,让单个 kernel 变「胖」。
-
确认 CPU 侧不是瓶颈。 如果是下发跟不上,先优化 CPU 侧逻辑(比如去掉每步的同步、 减少 Python 开销),否则换成 graph 也只是把问题挪个位置。
注意 CUDA Graph 的前提是拓扑和形状基本不变。动态 shape 频繁变化时反复重建 graph, 开销可能反而更大。
kernel launchCUDA Graphkernel fusion启动开销小算子overhead