QQ三公机器人 Overlap Scheduler 在做什么

原文读到了,来自博客园「银光」的笔记(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 评论

发表评论