原文读到了,来自博客园「银光」的笔记(2026-09-18)[citation:9.4]。这篇笔记虽然篇幅不长,但抓到了一个在 LLM 推理引擎开发中极易踩、踩了还很难定位的坑。我帮你把核心机制展开说透。
---
一、Overlap Scheduler 在做什么
LLM 推理服务中,overlap(重叠调度) 的核心思路是:把 GPU 上的推理计算和 CPU 上的前后处理(tokenization、sampling、detokenization、KVCache 管理)并行起来。[citation:9.4]
正常时序(理想):
GPU: ── forward N ──→── forward N+1 ──→── forward N+2 ──→
CPU: post N-1 │ pre N │ post N │ pre N+1 │ post N+1 │
└── overlap ──┘
CPU 在 GPU 跑第 N 次 forward 时,同时处理第 N-1 次的后处理和第 N+1 次的前处理——计算和调度在时间上重叠,隐藏调度延迟。
---
二、"一行代码"的精确位置
那行要命的代码在 sample 阶段:[citation:9.4]
就这一行
if args.is_greedy.all():
...
args.isgreedy 是一个 GPU 上的 tensor(通常是 torch.Tensor),if args.isgreedy.all() 要判断它的值——这个判断必须把数据从 GPU 拷贝回 CPU。
而GPU → CPU 的任何数据拷贝,都会触发隐式的 cudaStreamSynchronize——CPU 线程停下来,等当前 CUDA stream 里所有排队的操作彻底完成,才能拿到返回值。
---
三、为什么这一行就毁了 overlap
阻塞链
┌─────────────────────────────────────────┐
│ GPU 正在执行 forward N 的推理 │
│ CPU 本来应该在 overlap 区间做 post/pre │
│ │
│ CPU 执行到 args.is_greedy.all() │
│ ↓ │
│ 触发隐式 cudaStreamSynchronize │
│ ↓ │
│ CPU 阻塞,等 GPU 上所有任务完成 │
│ ↓ │
│ 等 forward N 跑完才继续 │
│ ↓ │
│ 等拿到结果后,overlap 窗口已关闭 │
│ CPU 从头开始处理 post/pre │
│ 下一轮 cudaGraphLaunch 无法提前发起 │
└─────────────────────────────────────────┘
原笔记用 nsys 工具对比的时序是关键:[citation:9.4]
| 状态 | 时序 | overlap 效果 |
|------|------|:----------:|
| 正常 overlap | N 推理 → N+1 推理 → N+2 推理 → 全流程在执行推理 | ✅ 完美 |
| 触发同步后 | N 推理 → cudaStreamSync → cudaGraphLaunch(N+1) → N+1 推理 | ❌ 完全消失 |
中间插了个 cudaStreamSynchronize,就把异步流水线变成了串行执行。
---
四、两种触发方式的严重性分级
原笔记区分了两种案例,它们的杀伤力完全不一样:[citation:9.4]
| 案例 | 触发方式 | 阻塞位置 | 代价 |
|:---:|---------|---------|:----:|
| 1 🔴 | GPU → CPU 方向(.all() 判 tensor) | sample 阶段,推理刚完成 | overlap 完全消失——CPU 等 GPU 所有任务完成才拿到数据,然后才去做 post/pre,中间的 overlap 时间窗口全浪费了 |
| 2 🟡 | CPU → GPU 方向(非 pinned memory 拷贝) | 下一轮 launch 时 | 只损失 cudaGraphLaunch 这一小段——因为 cudaGraphLaunch 本身是异步的,发完 GPU 就开始跑,CPU 捡起 post/pre 继续 overlap |
本质差异:案例 1 阻塞时,GPU 上的推理已经完成,但 CPU 必须要等它传回才能知道结果;案例 2 阻塞时,CPU 只是在等拷贝完成,拷完就能发 lauch,GPU 收到 launch 就开始跑推理。
原笔记给了张很清晰的 nsys 图——第二张图里箭头 1 是 cudaGraphLaunch,箭头 2 才是真正的同步等待(等上一轮推理完成通知)。这段间隔越长,overlap 效果越好。 案例 1 的问题在于同步来得太早,这段间隔几乎没了。[citation:9.4]
---
五、overlap 的有效窗口公式
原笔记给了一个很工程化的总结:
overlap 的有效窗口 = cudaGraphLaunch 执行完 → 出现阻塞 [citation:9.4]
窗口越长,overlap 效果越好;窗口被同步切断,overlap 就失效。
换句话说:
阻塞出现得太早(像案例 1,在 sample 阶段)→ 窗口被截断,overlap 完全消失
阻塞只挡住了 cudaGraphLaunch(案例 2)→ 只损失一小段,GPU 计算继续 overlap
理想的阻塞位置:在下一轮,等上一轮推理的完成事件——那时 overlap 该干的事已经干完了
---
六、工程启示:怎么修?
现场排查
如果怀疑某行代码破坏了 overlap:
用 nsys 或 nsight systems 抓 timeline,看 cudaStreamSynchronize 在哪出现
检查该同步点是否在 GPU → CPU 数据路径上
查那附近有没有 .item()、.all()、.tolist() 等 GPU tensor 转 CPU 的操作
修复方向
❌ 有问题的写法:GPU tensor 判断触发隐式同步
if args.is_greedy.all():
...
✅ 修复方案 1:把判断前移到 CPU 上做(在进 GPU 前就决定好)
greedy_flag = True # CPU 变量
... 整个推理循环用 CPU 变量判断,不再读 GPU tensor
✅ 修复方案 2:如果用异步流,需要显式管理 stream 事件
把需要下发的数据提前用 pinned memory + non_blocking 传入
stream = torch.cuda.Stream()
with torch.cuda.stream(stream):
# 异步拷贝
sourcetensor.pinmemory()
gputensor.copy(sourcetensor, nonblocking=True)
根本原则:推理hot path 上不要出现任何 GPU → CPU 的数据拷贝判断。所有决策需要的布尔值/标量,要么在 CPU 上提前算好传进去,要么异步传递不阻塞。
0 评论