GPU Kernel、异步 Launch、Stream 与融合 Kernel 全解
GPU Kernel、异步 Launch、Stream 与融合 Kernel 全解
本文目标:把"GPU 在跑 kernel"这件事从原理讲到工程——kernel 是什么、CPU/GPU 怎么协作、为什么 launch 是异步、
synchronize和stream怎么用、为什么要融合 kernel、怎么写/怎么调。读完你能回答面试官"你说的 kernel 是什么"“为什么 PyTorch op 测时间要 sync”“stream 是干什么的”“为什么要融合算子”。和前面几篇呼应:Megatron Timer 反复强调
cuda.synchronize(),本篇讲清它为什么是命门。
一、先澄清:此 kernel 非彼 kernel
“kernel” 有两个常见含义,别混:
| 操作系统内核 | GPU kernel | |
|---|---|---|
| 是什么 | OS 的核心,常驻 ring0 管硬件 | 一段在 GPU 上并行执行的函数 |
| 谁跑 | CPU | GPU 上成千上万个线程 |
| 例子 | Linux kernel、Windows kernel | vector_add、matmul、flash_attention |
本文讲的是 GPU kernel。一句话定义:
kernel 是你写的一段被编译成 GPU 指令、由 GPU 上成千上万个线程并行执行的函数。CPU 调用它时只是"派单",真正干活的是 GPU。
最小例子,矩阵加法 C = A + B,CPU 写一行 C = A + B,底层翻译成 CUDA kernel:
1 | // 这就是一个 kernel: 被 GPU 上几万个线程同时执行的函数 |
CPU 调用 vector_add<<<blocks, threads>>>(...):
- 立刻返回(异步),只是把这个 kernel 丢进 GPU 的执行队列。
- GPU 的 SM(流多处理器)拿到 kernel,启动成千上万线程,每个算一个
C[i],全部并行。
二、CPU/GPU 协作模型:派单与施工
把 CPU 当项目经理、GPU 当工地,最能记住:
1 | CPU(项目经理) GPU(工地) |
关键事实:
- CPU 提交 kernel(launch)极快(微秒级),它只填一个"派工单"丢进队列。
- GPU 跑 kernel 慢(毫秒~秒级),但成千上万线程并行。
- 因为 launch 快、GPU 慢,CPU 会一口气把一串 kernel 全丢进队列,GPU 按顺序慢慢跑。这就是"异步 pipeline"——CPU 和 GPU 重叠工作,GPU 不空等、CPU 不干等。
为什么必须异步(不异步就慢)
如果每次 launch 都同步等 GPU 跑完再返回,CPU 要傻等——GPU 算 1ms,CPU 也要等 1ms 才能发下一个 kernel。异步则 CPU 可以在 GPU 跑第 1 个 kernel 的同时把第 2、3、4 个都丢进队列,CPU 的 launch 时间和 GPU 的计算时间重叠。这是 GPU 编程高性能的基础:让 CPU 永远在 launch、GPU 永远在算,两者谁都不闲着。
三、异步 Launch 与 synchronize(Timer 的命门)
3.1 异步带来的"测不准"
回到 Megatron Timer 为什么必须 sync:
1 | start_time = time.time() # CPU 此刻 |
不 sync 时,time.time() 测的是 “CPU 提交 kernel 的时间”,而 GPU 上这个 kernel 可能还在队列里排队、甚至还没开始。测出来的值可能是真实 GPU 计算时间的零头——完全不可信。
3.2 torch.cuda.synchronize() 的作用
1 | torch.cuda.synchronize() # 阻塞 CPU, 等 GPU 把队列里所有 kernel 全跑完 |
调用后 CPU 卡住,直到 GPU 把当前 stream 上所有已 launch 的 kernel 全部执行完。这时再取 time.time(),才反映 GPU 真实完成时间。
所以 Megatron Timer 的 start/stop 都先 synchronize():
start前 sync:确保起点是"GPU 之前全干完了"的时刻,不被前面残留 kernel 污染。stop前 sync:确保终点是"这段 kernel 真跑完"的时刻。
3.3 sync 的代价
synchronize() 会让 CPU 停下来等 GPU,打破异步重叠——CPU 不能在 GPU 跑时继续 launch 后续 kernel,于是 CPU 和 GPU 串行化,性能下降。所以:
- 生产训练不要乱插 sync(除非必要)。
- 测时间时才 sync(且要能用 log_level 关掉,见 Megatron Timer 的 DummyTimer)。
- 需要拿 GPU 结果到 CPU 用时才 sync(如
loss.item()打印、判断是否 NaN、控制流分支)。
面试金句:“CUDA op 异步 launch,CPU 侧时间戳测的是提交时间不是计算时间,必须
synchronize()等队列排空才可信;但 sync 会打破 CPU/GPU 重叠拖慢训练,所以只在测时间、取结果到 CPU、做控制流时用,生产里要尽量少。”
3.4 何时该 sync(经验)
| 场景 | 要不要 sync |
|---|---|
| 测一段 op 耗时 | 要(前后各一次) |
loss.item() 打印 / 检查 NaN |
要(隐式 sync:.item()/.cpu() 会触发) |
| 控制流:if 某条件 launch 不同 kernel | 要(拿条件值到 CPU) |
| 正常前向/反向一串 op | 不要,保持异步 |
print 一个 GPU tensor |
隐式 sync |
注意 .item()、.cpu()、.tolist()、对 GPU tensor 做 print、if 判断 GPU tensor 值——这些操作会隐式触发 sync,是训练慢的常见暗坑。推理/训练代码里循环内 loss.item() 每步都 sync 一次,会显著拖慢。
四、CUDA Stream 与 Event:谁先谁后、能不能并行
4.1 Stream(流)是什么
stream 是 GPU kernel 的执行队列。同一 stream 里的 kernel 严格按 launch 顺序串行执行;不同 stream 的 kernel 可以并行执行(如果 GPU 资源够)。
1 | stream A: [k1] → [k2] → [k3] (串行) |
- 默认所有 op 走 default stream(stream 0),全串行。
- 想让两段独立计算重叠(如"算梯度"和"搬数据"重叠),把它们放不同 stream:
1
2
3
4
5
6
7
8
9s1 = torch.cuda.Stream()
s2 = torch.cuda.Stream()
with torch.cuda.stream(s1):
# 这些 kernel 进 s1 队列
grad = model(x)
with torch.cuda.stream(s2):
# 这些 kernel 进 s2 队列, 可和 s1 并行
data.copy_(host_data)
torch.cuda.synchronize() # 等两条都跑完
4.2 典型用途:计算/通信重叠、H2D/计算重叠
这是 stream 最有价值的场景:
- 计算与通信重叠:一边 GPU 算梯度(stream A),一边 NCCL all-reduce 上一段梯度(stream B,通信 kernel 也在 GPU 上但走不同 stream),两者并行——这就是 Megatron DDP “bucket ready 触发 async all-reduce 与反向 overlap” 的底层。
- Host-to-Device 搬数据和计算重叠:下一 batch 的数据 H2D 拷贝(stream B)和当前 batch 前向(stream A)并行,CPU/PCIe 传输藏进算力里。
- prefill/decode 分流:vLLM、推理引擎里有时把 prefill 和 decode 放不同 stream 重叠。
4.3 Event(事件):跨 stream 同步
不同 stream 要同步"等 A 这步完成 B 才开始",用 event:
1 | e = torch.cuda.Event() |
Event 还可用来测 GPU 时间(比 time.time()+sync 更精细):
1 | start = torch.cuda.Event(enable_timing=True) |
Event 记录的是 GPU 时间线上的点,不受 CPU launch 异步干扰,是测 GPU kernel 耗时的正确姿势。Megatron Timer 用 time.time()+sync 是更简单粗粒度的版本,精细 profile 用 Event 或 nsys/ncu。
面试金句:“stream 是 kernel 执行队列,同 stream 串行、跨 stream 可并行;计算/通信重叠、H2D/计算重叠靠多 stream。跨 stream 同步用 Event,Event 也能精确测 GPU kernel 时间(不受 launch 异步干扰)。”
五、Grid / Block / Thread:kernel 的并行结构
一个 kernel 启动时,要告诉 GPU 派多少线程、怎么组织。CUDA 用三层结构:
1 | Grid ──► 一堆 Block ──► 每个 Block 一堆 Thread |
- Thread:最小执行单元,每个 thread 跑一份 kernel 代码、算一个/几个元素。
- Block:一组 thread,同一 block 内 thread 可共享 shared memory、可
__syncthreads()同步。一个 block 跑在一个 SM 上。 - Grid:所有 block 的集合,跨 SM 调度。
典型配法:每个 thread 算一个输出元素,blockDim=256,gridDim=ceil(N/256)。
关键:算力靠"喂饱 SM"
GPU 有几十上百个 SM,每个 SM 能同时跑成千上万线程。kernel 要派足够多的 thread 才能占满 SM——这叫** occupancy(占用率)。如果 kernel 只派几百个 thread(比如 batch=1 推理),SM 大量空闲,算力浪费,这就是 decode 阶段算力受限不足、访存受限为主**的根源。vLLM 用 continuous batching 把多个请求的 decode token 攒一起喂 GPU、用 CUDA Graph 跳 launch,都是为了解决这个。
SM 内的存储层次(影响 kernel 性能)
1 | 寄存器 (per-thread, 最快, 几 KB) ← thread 私有, ~1 cycle |
写高性能 kernel 的核心是:把热点数据放 shared memory/寄存器、少访问 global memory(HBM)。这就是 FlashAttention 的精髓——把 attention 的中间 矩阵不写回 HBM、用 shared memory/寄存器在线算 softmax,把 HBM 访存降到 。
显存篇提过"训练访存受限、推理 decode 访存受限"——kernel 跑多快,不是看你算多快,而是看你从 HBM 读数据快不快。访存受限下,优化方向是"少读 HBM"(融合 kernel、tile+shared memory、减少中间结果落地),不是"算更快"。
六、融合 Kernel:为什么要把多个算子合成一个
6.1 朴素 op 链的问题
一段 Y = activation(BiasAdd(MatMul(X, W))),朴素实现是三个独立 kernel:
1 | launch matmul → GPU 算 → 写结果到 HBM |
三个问题:
- 多次 HBM 读写:中间结果
matmul 输出被写 HBM 又被下一 kernel 读 HBM,带宽浪费、延迟叠加。访存受限下这是性能杀手。 - launch 开销:每个 kernel launch 有固定 CPU 开销(几微秒)+ GPU 启动开销。算子小、launch 多时(decode 阶段每步几十个小算子),launch 开销占比极高。
- 不能跨 kernel 优化:每个 kernel 独立编译,没法把"读-算-写"合并优化。
6.2 融合的解法
融合 kernel(fused kernel):把多个算子合成一个 kernel,中间结果留在寄存器/shared memory,不落地 HBM,一次 launch 跑完。
1 | launch fused_kernel → 线程读 X, W → 算 matmul → 直接在寄存器里 add bias → 激活 → 写最终 Y |
收益:
- 少 HBM 读写(最大头,访存受限下决定性提速)。
- 少 launch 开销。
- 更好的数据局部性(在 shared memory 里 tile 复用)。
6.3 训练/推理里的融合 kernel 实例
- FlashAttention:融合
QK^T → mask → softmax → ×V,把 中间矩阵不落地 HBM。Megatron/vLLM 默认用。 - FusedMLP:融合
gate × up → activation → × down(SwiGLU 全融合),Megatronfusions/下。 - FusedLayerNorm + bias + residual:Megatron 把
residual = x + dropout(layernorm(x + bias))融成一步。 - Fused Adam(optimizer):把一阶/二阶矩更新 + 权重更新融成一个 kernel,Megatron/VLLM 用。
- Fused softmax / rope / cross_entropy:Megatron
csrc/、vLLMv1/sample/ops。
6.4 融合的代价与边界
- 实现复杂:手写 CUDA/Triton 工作量大、可维护性差。
- 通用性差:融合的特定 op 组合换形状/换激活可能要重写。
- 不一定都更快:compute-bound 的大 kernel(大 GEMM)本身 HBM 访存占比低,融合收益小;访存-bound 的小算子链融合收益大。
面试金句:“融合 kernel 解决两个问题:访存受限下减少中间结果落地 HBM(性能大头)、launch 受限下减少 launch 次数。FlashAttention/FusedMLP/FusedAdam 都是典型。算 compute-bound 的大 GEMM 融合收益小,访存-bound 的小算子链融合收益大——这就是为什么 decode 阶段(小算子多)特别需要融合和 CUDA Graph。”
七、CUDA Graph:把一串 launch 录成一次 replay
7.1 问题:launch 开销在 decode 里很重
推理 decode 阶段每步只产一两个 token,但每步要 launch 几十个 kernel(mlp/attention/norm/rope/sample…)。每个 launch 几微秒 CPU 开销 × 几十个 × 几百步 = CPU 被 launch 占满、GPU 等待 launch。这就是 launch-bound。
7.2 CUDA Graph 的解法
CUDA Graph:在"录制"阶段把一串 kernel launch 记录成一个图结构(依赖关系),"重放"阶段一次 cudaGraphLaunch 把整图丢给 GPU,CPU 只需一次 launch 调用,GPU 自己按图依赖调度。
1 | 录制(capture): 跑一遍 op 链 → 记录成 graph(节点=kernel, 边=依赖) |
收益:
- CPU launch 开销从几十次降到 1 次——decode 延迟大幅下降。
- GPU 自己管依赖,减少 CPU↔GPU 同步。
代价:
- 图的输入/输出地址固定,shape 要固定(变 shape 要重新抓图或抓多个图)。
- 要占额外显存放图。
- 动态控制流(if/while 依赖运行时数据)难进图。
7.3 工程实例
- vLLM:
v1/cudagraph_dispatcher.py+worker/gpu/cudagraph_utils.py——启动时为若干个固定num_tokens组合抓图,运行时dispatch_cg_and_sync_dp按实际 token 数选匹配图 replay。decode 受益最大,prefill(shape 多变)收益小。 - 静态形状训练 / inference:很多推理引擎对固定 shape 模型用 CUDA Graph 加速。
和融合 kernel 的区别:融合是"把多个算子合成一个 kernel"(减 HBM 访存+减 launch),CUDA Graph 是"把多个 kernel launch 合成一次提交"(只减 launch,不改 kernel)。两者互补——融合解决访存+launch,Graph 解决 launch。decode 慢两者都要用。
八、怎么写 kernel:Triton 与 CUDA C
8.1 两条路
- CUDA C/C++:原生 NVIDIA,性能最高、控制最细,但难写难调。Megatron
csrc/、vLLMcsrc/里大量手写 CUDA kernel。 - Triton:OpenAI 出的 Python DSL(
@triton.jit),编译成 GPU 代码,比 CUDA 易写、性能接近,FlashAttention 早期版、很多 fused kernel 现在用 Triton 写。生态火。
1 | # Triton 写个 vector_add, 比 CUDA 简洁 |
8.2 什么时候要自己写
- 框架没现成融合 kernel、且是性能热点(attention、mlp、optimizer、采样)。
- 特殊算子(MLA、MoE dispatch、量化)。
- 极致性能优化(cudnn/cutlass 调不动的特殊 shape)。
但优先用现成的(FlashAttention、cudnn、cutlass、Triton 模板),手写 kernel 是最后手段——调到最优极费时。
九、怎么调 kernel 性能:profiling
写完/优化 kernel 要测、要找瓶颈:
torch.cuda.Event:粗粒度测段 GPU 时间(前面讲过)。torch.profiler:导出 Chrome trace,看每个 kernel 的 wall-clock、占空比、launch 间隔。nvidia-smi:看 GPU 利用率、显存、温度(粗粒度,util 高不代表没瓶颈)。nsys(Nsight Systems):系统级 timeline,看 CPU launch 和 GPU kernel 的时间线、stream 并行情况、有没有气泡、sync 在哪卡住。最该会的工具。ncu(Nsight Compute):kernel 级细粒度,看每个 kernel 的算力/访存/占用率/瓶颈(compute bound 还是 memory bound)、给出优化建议。调单个 kernel 用它。
典型调优流程:
nsys看哪段有气泡、哪个 kernel 占时最长。- 对最耗时的 kernel 用
ncu看是 compute bound 还是 memory bound。 - memory bound → 减少 HBM 访存(融合、tile、shared memory、向量化读取);compute bound → 用 tensor core、减冗余计算。
- launch bound → CUDA Graph 或融合 kernel。
面试金句:“调 GPU 性能先 nsys 看 timeline 找最长/有气泡的 kernel,再 ncu 看单个 kernel 是 compute bound 还是 memory bound,对症下药——memory bound 减 HBM 访存(融合/tile),compute bound 上 tensor core,launch bound 用 CUDA Graph。
nvidia-smiutil 高不等于没瓶颈,可能正卡在访存。”
十、串起训练/推理:kernel 视角看性能
把这套知识用到你熟悉的训练/推理场景:
训练(Megatron)
- 前向/反向一串 kernel 异步 launch,CPU 不停发,GPU 不停算,两者重叠——这就是高性能的基础。
all-reduce/all-gather通信也是 kernel(NCCL kernel,在 GPU 上跑),放单独 stream 和计算 kernel 重叠(DP overlap)。- Timer 要
synchronize()才能测准各段 kernel 时间,但 sync 拖慢训练所以用 log_level 控制。 - 融合 kernel(FusedMLP/FlashAttention/FusedAdam)减访存+减 launch,是访存受限训练的核心优化。
推理(vLLM)
- prefill 算力受限:大 GEMM kernel,算得满,launch 开销占比小,CUDA Graph 收益小。
- decode 访存受限+launch 受限:每步几十个小 kernel、batch 小 SM 空闲——continuous batching 攒 token 喂饱 SM + 融合 kernel + CUDA Graph 跳 launch 三件套。
- 多 stream:prefill/decode 分流、KV 搬运和计算重叠。
- 异步 scheduler:CPU 调度下一步时 GPU 还在算上一步,靠的就是 launch 异步——CPU 在 launch 间隙做调度,不阻塞 GPU。
三种瓶颈(务必分清)
| 瓶颈类型 | 含义 | 优化方向 |
|---|---|---|
| compute bound | 算力打满,访存不是瓶颈 | 上 tensor core、减冗余计算、更高精度融合 |
| memory bound | 访存打满,算力空闲(最常见) | 减 HBM 读写、融合 kernel、tile+shared memory |
| launch bound | launch 开销占主导(小算子多) | CUDA Graph、融合 kernel |
大模型训练/推理绝大多数时间是 memory bound(显存篇讲过访存受限),所以"减访存"是优化主线——融合、tile、量化(少读权重)、KV cache 复用(少重算)全是减访存。
十一、面试速答清单
Q1:GPU kernel 是什么?和 OS kernel 区别?
GPU kernel 是一段在 GPU 上由成千上万线程并行执行的函数;CPU 调用它(launch)只是把派工单丢进队列,真正执行的是 GPU。OS kernel 是常驻 ring0 管硬件的操作系统核心,跑在 CPU。两者同名但完全不同。
Q2:为什么 PyTorch 测 op 时间必须 cuda.synchronize()?sync 的代价?
CUDA op 异步 launch,
time.time()测的是 CPU 提交 kernel 的时间,GPU 可能还没开始跑,数据不可信。synchronize()阻塞 CPU 等 GPU 队列排空才反映真实计算时间。代价是打破 CPU/GPU 重叠、串行化拖慢,所以只在测时间、取结果到 CPU(.item()/.cpu())、做控制流时用,生产尽量少。
Q3:stream 是什么?怎么用它做计算/通信重叠?
stream 是 kernel 执行队列,同 stream 串行、跨 stream 可并行。把独立的两段计算放不同 stream(如算梯度放 stream A、NCCL all-reduce 放 stream B)就能让它们并行——这是 DDP 反向 overlap 的底层。跨 stream 同步用 Event,Event 也能精确测 GPU kernel 时间。
Q4:为什么要融合 kernel?收益和代价?
朴素 op 链每个 kernel 把中间结果写 HBM、下个 kernel 又读 HBM,访存浪费+延迟叠加;launch 多次开销大。融合把多算子合成一个 kernel,中间结果留寄存器/shared memory 不落地 HBM、一次 launch 跑完。收益是减访存(memory bound 下决定性)+减 launch;代价是难写难维护、通用性差。FlashAttention/FusedMLP/FusedAdam 是典型。
Q5:CUDA Graph 解决什么?和融合 kernel 区别?
解决 launch bound——把一串 kernel launch 录成图,运行时一次
cudaGraphLaunch提交,CPU 只调一次 launch。区别:融合是"多算子合成一个 kernel"(减访存+减 launch),Graph 是"多 kernel launch 合成一次提交"(只减 launch,不改 kernel)。decode 阶段 launch 受限两者互补都要用。vLLM 为固定 num_tokens 抓图、按 token 数 dispatch 选图。
Q6:memory bound / compute bound / launch bound 怎么区分、怎么优化?
memory bound 是访存打满算力空闲(大模型最常见),优化靠减 HBM 读写(融合/tile/shared memory/量化);compute bound 是算力打满,优化靠 tensor core/减冗余计算;launch bound 是小算子多 launch 开销主导,靠 CUDA Graph/融合。先 nsys 看 timeline 找最长/有气泡的 kernel,再 ncu 看单 kernel 是 compute 还是 memory bound 对症下药。
nvidia-smiutil 高不代表没瓶颈,可能正卡访存。
Q7:为什么 decode 阶段 GPU 利用率低、特别需要优化?
decode 每步只产一两个 token,kernel 规模小、batch 小,SM 喂不饱(occupancy 低)、算力空闲,同时几十个小算子 launch 开销占比高——既 memory bound 又 launch bound。所以 vLLM 用 continuous batching 攒 token 喂饱 SM、融合 kernel 减访存、CUDA Graph 跳 launch 三件套。prefill 是大 GEMM 算力受限,这些优化收益小。
Q8:FlashAttention 融合了什么、为什么快?
融合
QK^T → mask → softmax → ×V,把 attention 中间 矩阵不写回 HBM、用 shared memory/寄存器在线分块算 softmax,把 HBM 访存从 降到 。memory bound 下减访存是决定性收益,还顺带省了 显存。这是融合 kernel 减访存最经典的例子。
十二、一张图收口
1 | CPU(派单) ─launch(kernel)─► [GPU stream 执行队列] ─► SM 上成千线程并行跑 kernel |
主线一句话:GPU kernel 是被成千线程并行执行的函数、CPU 异步 launch 派单进 stream 队列、要测准或拿结果才 sync(sync 拖慢所以要少用);高性能靠"喂饱 SM + 少读 HBM + 少 launch"——融合 kernel 解决访存+launch、CUDA Graph 解决 launch、多 stream 做计算通信重叠;大模型绝大多数时间 memory bound,所以"减访存"是优化主线。 把 launch 异步、sync、stream、融合、三种瓶颈讲顺,GPU 性能这关就稳了。
参考资料
- NVIDIA CUDA C++ Programming Guide(grid/block/thread、stream、event、memory hierarchy)
- NVIDIA Nsight Systems / Nsight Compute 文档
- FlashAttention: Fast and Memory-Efficient Exact Attention Dao et al., 2022
- Triton: https://triton-lang.org
- 与本文 Megatron Timer、显存计算法则、vLLM 源码精读、RDMA 篇交叉对照
