
背景Overlap Scheduling 是大模型推理加速的重要手段。从应用视角来说Overlap Scheduling 适用于有高 QPS 压力的场景吞吐和 TTFT 通常会更好。如果是低 QPS 的场景输入压力有限Overlap Scheduling 的吞吐不会更好并且会有 TTFT 升高的风险。这个结论不需要深入理解 Overlap Scheduling可以通过端到端的黑盒测试得到而本文想要做的是白盒分析知其所以然。本文试图解答以下问题Overlap Scheduling 是什么Overlap Scheduling 为什么会导致 TTFT 上涨有什么优化手段Overlap Scheduling 消除了 GPU 空闲气泡系统吞吐和 TTFT 就一定会更好吗在解答的过程中也会给读者展示分析和解决问题的方法在其他性能优化分析场景下也能应用。注本文的示例代码主要是 mini-sglang 的 Overlap Scheduling 实现提供的灵感在介绍基础概念时也以 mini-sglang 为样板。2. 概念理解定义Overlap Scheduling 是一种将 CPU 调度开销与 GPU 计算重叠执行的优化技术mini-sglang 通过双 CUDA Stream 机制实现 CPU 和 GPU 并行工作有效隐藏 CPU 延迟提高 GPU 利用率和系统吞吐。核心价值Overlap Scheduling 的核心目标是让 CPU 的调度操作与 GPU 的推理计算同时进行。由于 CPU 资源相对廉价且通常不是瓶颈而 GPU 算力昂贵且稀缺因此该技术的核心价值在于让瓶颈硬件GPU保持持续的满负荷计算状态避免其因等待 CPU 指令而 “空转”。技术手段能够并行工作本质上是让 GPU 计算可以异步执行多流是实现 GPU 异步计算的关键手段。对于熟悉传统 CPU 后台开发的工程师来说这类似于经典的 “生产者 - 消费者” 模型外部消息响应线程在遇到计算密集型任务时通常会将计算任务丢到工作线程里待计算完成后再返回给消息响应线程而消息响应线程则通过轮询回包队列或者事件触发的方式将计算结果回给外部请求者。mini-sglang 就是采用这类思路实现的CUDA 的工作流就是 CPU 里的工作线程。扩展本章介绍的是调度的重叠大模型推理中还有其他重叠譬如数据传输和计算的重叠、核函数启动和计算的重叠。无论哪种重叠其本质都是为了掩盖非瓶颈环节的延迟从而让昂贵的瓶颈硬件GPU得到最充分的利用。3. 核心技术点如前所述Overlap Scheduling 的本质技术是异步执行核心有两点创建异步环境让推理计算可以在单独的流里执行置位同步信号推理计算开始前需要确保数据已经准备好调度线程处理结果前需要确保推理已经完成mini-sglang 的实现很简洁关键代码如下1创建推理流# Engine 对象的 __init__ self.stream torch.cuda.Stream() ... # Scheduler 对象的 __init__ self.engine_stream_ctx torch.cuda.stream(self.engine.stream)2任务切换到推理流执行并且确保推理之前数据准备完成with self.engine_stream_ctx: # 切换到推理流 self.engine.stream.wait_stream(self.stream) # 等待调度流完成元数据准备 ongoing_data (forward_input, self._forward(forward_input))3处理结果之前确保推理流的数据已拷贝到 CPUdef _process_last_data(self, last_data, ongoing_data): if last_data is None: return batch, (_, next_tokens_cpu, copy_done) last_data[0].batch, last_data[1] copy_done.synchronize() # 等待 GPU→CPU 数据拷贝完成 # 处理采样结果...4. 原型代码基于 mini-sglang 的思路我们可以自行实现一份 demo 代码import torch import time import queue # 模拟各 CPU 阶段耗时秒 PRE_PROCESS_TIME 0.03 # 前处理CPU: 接收请求、准备 Metadata, Tokenization POST_PROCESS_TIME 0.02 # 后处理CPU: Sample, De-tokenization TOTAL_REQUESTS 10 # 测试总请求数 class OverlapEngine: def __init__(self): self.engine_stream torch.cuda.Stream() self.request_queue queue.Queue() self.results_queue queue.Queue() def _pre_process(self, req_id): 模拟前处理 time.sleep(PRE_PROCESS_TIME) def _post_process_normal(self, req_id): 模拟 normal 模式的后处理 time.sleep(POST_PROCESS_TIME) def _post_process_overlap(self, last_data): 模拟 overlap 模式的后处理 if last_data is not None: _, sync_event, _ last_data sync_event.synchronize() time.sleep(POST_PROCESS_TIME) def _mock_forward(self): 模拟 GPU 推理 dummy_tensor torch.randn(1000, 1000, devicecuda) for _ in range(1000): dummy_tensor torch.matmul(dummy_tensor, dummy_tensor) return dummy_tensor def _gpu_inference_async(self, req_id): 模拟异步 GPU 推理 with torch.cuda.stream(self.engine_stream): result self._mock_forward() sync_event torch.cuda.Event() sync_event.record(self.engine_stream) return req_id, sync_event, result def _gpu_inference_blocking(self): 模拟同步 GPU 推理 result self._mock_forward() torch.cuda.synchronize() return result def run_normal(self): 模拟运行 normal 模式 start_time time.time() for i in range(TOTAL_REQUESTS): self._pre_process(i) self._gpu_inference_blocking() self._post_process_normal(i) return time.time() - start_time def run_overlap(self): 模拟运行 overlap 模式 start_time time.time() last_data None for i in range(TOTAL_REQUESTS 1): ongoing_data None if i TOTAL_REQUESTS: self._pre_process(i) ongoing_data self._gpu_inference_async(i) self._post_process_overlap(last_data) last_data ongoing_data return time.time() - start_time # 预热 dummy_tensor torch.randn(1000, 1000, devicecuda) torch.matmul(dummy_tensor, dummy_tensor) engine OverlapEngine() print(f开始测试 {TOTAL_REQUESTS} 个请求...) duration_normal engine.run_normal() throughput_normal TOTAL_REQUESTS / duration_normal duration_overlap engine.run_overlap() throughput_overlap TOTAL_REQUESTS / duration_overlap print(- * 30) print(fNormal 模式耗时: {duration_normal:.4f}s | 吞吐: {throughput_normal:.2f} req/s) print( fOverlap 模式耗时: {duration_overlap:.4f}s | 吞吐: {throughput_overlap:.2f} req/s ) print(f提升效率: {((throughput_overlap/throughput_normal)-1)*100:.2f}%)运行效果如下开始测试 10 个请求... ------------------------------ Normal 模式耗时: 1.2408s | 吞吐: 8.06 req/s Overlap 模式耗时: 0.8476s | 吞吐: 11.80 req/s 提升效率: 46.39%从原型代码的运行结果看到有 46.39% 的吞吐提升收益那么收益的来源是哪儿呢下面做进一步拆解。5. 收益和代价分析5.1. Overlap Scheduling 模式推理时序图图15.2. 吞吐收益分析在 Overlap Scheduling 模式下主线程持续向推理流提交任务以最大化 GPU 占用率。根据 GPU 推理耗时与 CPU 调度耗时的相对比例系统将呈现两种运行状态1GPU 饱和态Compute Bound当单次 GPU 推理耗时显著大于 CPU 调度开销时任务调度形成完美的 GPU 流水线。现象 任务 N1 的启动触发点早于任务 N 的结束时间。结果 GPU 无需等待任务 N 结束后无缝衔接任务 N1GPU 利用率达到 100%。2GPU 饥饿态CPU Bound当单次 GPU 推理耗时小于 CPU 调度开销时GPU 流水线出现断流。现象 任务 N 提前结束但任务 N1 尚未由 CPU 准备就绪。结果 GPU 被迫进入空转等待状态算力出现浪费。无论处于饱和态还是饥饿态Overlap Scheduling 模式的系统吞吐量均高于 Normal 模式。然而在 GPU 饥饿场景下CPU 成为了瓶颈是否可以进一步优化针对 GPU 饥饿态时的 CPU 瓶颈可通过任务解耦让 CPU 任务并行执行基于N 后处理与启动 N1之间无数据依赖的特点让二者可以并行执行从而消除 CPU 的串行等待时间让系统进入 GPU 饱和态。因为 python 的 GIL 限制导致无法使用多核但这里的 CPU 任务是 IO 密集型的不会消耗太多 CPU应该可以做到不错的效果。后文会再结合 TTFT 优化进一步分析最后会给出一份并行化改造的示例代码以及分析过度并行化会因破坏 Continuous Batching 带来的负面影响。5.3. TTFT 代价分析通常我们讲 Overlap Scheduling 收益时都是在谈论它如何提升 GPU 利用率让系统吞吐更高但这是有代价的它可能会让 TTFT 升高。请求的首 token 推理计算量不会因为调度重叠而减少反而可能因重叠需要导致一次请求的处理过程中插入了非本请求的时间消耗最终导致 TTFT 耗时增加。实际上在我们的业务中也确实出现了这样的现象在仅有 Prefill 推理的场景下后文简称为“纯 Prefill 场景”开启 Overlap Scheduling 的耗时要比不开启的 Normal 模式耗时多 30%。下面的几个章节将基于这个命题展开在 Overlaping Scheduling 模式下TTFT 上涨的底层原因是什么以及如何让 TTFT 不增加。6. TTFT 耗时分析实验代码前述的原型代码每个请求只处理一次推理和我们的纯 Prefill 场景一致给原型代码加上 nvtx 和请求耗时统计同时加上 QPS 控制确保公平然后通过调整前处理、推理、后处理三个阶段的耗时来模拟 GPU 饱和、GPU 饥饿的场景观察 NSYS 和平均耗时即 TTFT。代码较长为免影响阅读体验放到 githubhttps://github.com/cswuyg/tools/blob/main/overlap_scheduling/overlap_scheduling_gpu_bound.pyhttps://github.com/cswuyg/tools/blob/main/overlap_scheduling/overlap_scheduling_cpu_bound.py运行nsys profile --tracecuda,osrt,nvtx python3 demo_overlap.py7. GPU 饱和场景 TTFT 分析1实验代码特点CPU 耗时 GPU 耗时并且请求出现积压。2通过 nsys 看到 GPU 泳道没有空隙。图23实验结果开始测试 5 个请求 (QPS1000)... ------------------------------ Normal 模式 - 总耗时: 3.4863s | 平均延迟: 2125.93ms | 队列等待: 1428.68ms | 吞吐: 1.43 req/s Overlap 模式 - 总耗时: 2.5646s | 平均延迟: 1618.12ms | 队列等待: 713.20ms | 吞吐: 1.95 req/s 吞吐提升: 35.94% | 延迟降低: 23.89%请求积压并且 GPU 饱和场景下Overlap Scheduling 的吞吐和 TTFT 都更好。我们再通过 NSYS 分析观察 Overlap 模式下不含队列等待时间的 TTFT 耗时组成以请求 2 为例图3请求 2 的耗时包括 6 个步骤请求 2 的前处理请求 2 的 cuda launch请求 1 的 后处理请求 3 的前处理请求 3 的 Kernel Launch请求 2 的后处理因推理耗时非常长所以 3、4、5、6 四个步骤的时间主要是请求 1 和请求 2 的推理耗时也就是说请求 2 的 TTFT 包含了请求 1 的推理时间此时如果要降低 TTFT需要减少每次推理的计算量或者是加大请求之间的间隔避免前请求影响到后请求。在实际的业务应用中本案例属于系统压力过大GPU 饱和使用请求在队列里等待。这时候 Overlap 模式的 TTFT 比 Normal 好因为高压力下Normal 的队列等待会更严重。分析结论纯 Prefill 推理GPU 用满的场景下当前请求的 Prefill 耗时会包含前一个请求的部分 Prefill 耗时导致 TTFT 耗时升高但因为 GPU 使用充分因此在相同的高 QPS 压力下Overlap Scheduling 比 Normal 模式的 TTFT 低。8. GPU 饥饿场景 TTFT 分析1实验代码特点CPU 耗时 GPU 耗时并且请求出现积压此时 CPU 调度处理阻塞了 GPU 任务的执行很多请求在等待但是 GPU 用不起来。2通过 nsys 看到 GPU 泳道出现明显的间隙Normal 模式的间隙更大。图43实验结果开始测试 5 个请求 (QPS1000)... ------------------------------ Normal 模式 - 总耗时: 2.0446s | 平均延迟: 1214.26ms | 队列等待: 805.36ms | 吞吐: 2.45 req/s Overlap 模式 - 总耗时: 1.2969s | 平均延迟: 875.86ms | 队列等待: 411.88ms | 吞吐: 3.86 req/s 吞吐提升: 57.65% | 延迟降低: 27.87%我们再通过 nsys 分析观察耗时的组成图5请求 2 的耗时包括 6 个步骤请求 2 的前处理请求 2 的 cuda launch请求 1 的 后处理重叠请求 2 的 GPU 计算请求 3 的前处理请求 3 的 Kernel Launch请求 2 的后处理这 6 个步骤只有 1、2、6 是请求 2 相关的其他是 Overlap 插入的这导致请求 2 的 TTFT 时间增加了。此时如果要降低 TTFT应该让步骤 6 尽快的执行不要在 3、 4、5 后面等待。在实际的业务应用中本案例属于调度开销占比过高GPU 使用不充分。结论纯 Prefill 推理GPU 用不满的场景下当前请求的 Prefill 耗时会包含前一个请求的后处理和后一个请求的前处理耗时TTFT 耗时升高但因为 GPU 使用相对充分因此在相同的高 QPS 压力下 Overlap Scheduling 比 Normal 模式的 TTFT 低。9. 现有架构下优化 TTFT9.1. 纯 Prefill 场景上述两节的分析纯 Prefill 场景下GPU 无论是饱和还是饥饿Overlap Scheduling 模式下 TTFT 都是增加的怎么才能让 TTFT 不增加呢从上一节的 NSYS 分析图我们得到结论当 CPU 时间里的 3、4、5 步骤和 GPU 推理时间正好重叠的时候TTFT 耗时就不会增加。将上述 GPU 饥饿实验代码里的前处理和后处理时间做修改让 CPU 时间和 GPU 时间尽量接近再运行测试效果如下Normal 模式 - 总耗时: 2.3839s | 平均延迟: 476.78ms | 吞吐: 2.10 req/s Overlap 模式 - 总耗时: 1.4150s | 平均延迟: 497.96ms | 吞吐: 3.53 req/s 吞吐提升: 68.47% | 延迟降低: -4.44%可以看到 Overlap 模式的 TTFT 耗时已经和 Normal 非常接近因为我们的 cuda 任务耗时不稳定所以要做到延迟不降低需要尝试很多次这里略过。结论纯 Prefill 推理场景下当 CPU 处理时间和 GPU 推理时间正好重叠时TTFT 耗时不会增加。这个结论理论上可达实际上几乎无法做到我们只能尽量的缩短差距首先CPU 计算任务的耗时我们无法通过参数调节我们只能通过调节推理计算量来对齐时间而推理计算量又和请求序列长度相关我们可以用 chunked prefill 来调节单次推理的计算长度但 chunked prefill 又需要付出 TTFT 升高的代价陷入死结。9.2. Prefill 和 Decode 混合场景在 Prefill 和 Decode 混合的情况下可以实现 TTFT 不增长。还是以上一节的 GPU 饥饿场景 NSYS 图为例我们修改请求的推理时间请求 1 和 请求 3 是 decode 请求耗时很短而请求 2 是 prefill 请求耗时很长。这时候就可以保证1请求 2 Kernel Launch 之后可以马上使用 GPU2请求 2 的推理时间就可以覆盖住请求 3 的 前处理和 Kernel Launch使得请求 2 推理完成之后马上就可以执行回包处理。这样请求 2 的 TTFT 就没有任何的插入时间修改前面的实验代码def _mock_forward(self, req_id): torch.cuda.nvtx.range_push(fGPUInference-Req{req_id}) dim 15000 dummy_tensor torch.randn(dim, dim, devicecuda) result torch.matmul(dummy_tensor, dummy_tensor) result torch.matmul(dummy_tensor, dummy_tensor) result torch.matmul(dummy_tensor, dummy_tensor) torch.cuda.nvtx.range_pop() return result def _gpu_inference_async(self, req_id): with torch.cuda.stream(self.engine_stream): if req_id in [2]: result self._mock_forward_prefill(req_id) else: result self._mock_forward(req_id) sync_event torch.cuda.Event() sync_event.record(self.engine_stream) return req_id, sync_event, result再次运行然后通过 nsys 看效果图6可以看到请求 2 的推理时间重叠了其他请求的 CPU 处理时间此时请求 2 的 TTFT 耗时和 Normal 模式一样。此时时序图如下图7从时序图很容易就可以发现GPU 进入了“饥饿态”也就是保住了 TTFT但是吞吐下降了。结论Prefill 和 Decode 混合场景下当 Prefill 请求的前后都是 Decode 请求时可以确保 Prefill 请求的 TTFT 不会增加但 GPU 出现了空转付出了系统吞吐下降的代价。10. CPU 并行计算改造尝试在前面分析到 GPU 饥饿场景时我们提到过让 CPU 并行加速看起来可以让 GPU 不断流也可以优化 TTFT本节还是通过实验来模拟并行改造后的效果。10.1. 理论分析1并行改造之前的时序图图8GPU 饥饿场景下的时序如上图GPU 流存在气泡CPU 的阻塞了 GPU 的运行。2并行改造之后的时序图图9如上图所示将所有 CPU 任务都异步化保证 GPU 不断流。10.2. 实验验证1实验代码在 TTFT 耗时分析实验代码基础上做改造新增两个工作线程让前处理、启动、后处理都通过队列衔接让它们都可以并发执行。代码较长为免影响阅读体验放到 githubhttps://github.com/cswuyg/tools/blob/main/overlap_scheduling/overlap_scheduling_multi_cpu_thread.py2NSYS 观察图10可以看到 GPU 流水线没有气泡用得非常满。3实验数据开始测试 5 个请求 (QPS1000)... ------------------------------ Normal 模式 - 总耗时: 2.0174s | 平均延迟: 1206.19ms | 队列等待: 802.71ms | 吞吐: 2.48 req/s Overlap 模式 - 总耗时: 1.1415s | 平均延迟: 773.87ms | 队列等待: 250.32ms | 吞吐: 4.38 req/s 吞吐提升: 76.73% | 延迟降低: 35.84%和 GPU 饥饿场景下的实验数据合并表格中的优化比例都是相对于 Normal 模式的方案 \ 指标吞吐提升TTFT 降低CPU 单线程 (GPU 饥饿场景)57.65%27.87%CPU 3 线程 (并行改造)76.73%35.84%可以看到高 QPS 压力下如果 GPU 流水线出现气泡可以使用 CPU 并行吞吐和 TTFT 都有很大的优化。看起来很完美到这里就结束了吗不是的在现实环境下有两个点我们的模拟实验没考虑到Python 的 GIL 限制。如果前处理和后处理有比较重的 CPU 计算会因为 python 无法使用多核导致 CPU 并行失效。更高频的 Kernel Launch 会影响动态批处理。原本攒一批请求再一次执行我们将 CPU 处理改造为多线程并行之后会变成多批执行。请求量没有改变但 GPU 需要做的计算次数更多了导致系统吞吐下降。影响最大的是第二个点下面展开分析。11. Overlap Scheduling CPU 并行带来的性能退化11.1. 理论分析动态批处理是契合 GPU 硬件特性增大系统吞吐的重要优化策略但和 Overlap Scheduling 在一起时Overlap Scheduling 会降低动态批处理的批次大小导致系统吞吐下降。Overlap Scheduling 让 CPU 调度任务更早的执行使得多个 GPU 任务可以连续的执行GPU 流水线不会断流但当 CPU 调度任务太早执行时会削弱动态批处理。CPU 调度任务执行得更早、更快、更高频就会导致连续的多个请求被更高频的组装成多个 CUDA Stream 任务对于 CUDA Stream 来说背后的请求总数并没有改变但 GPU 任务数变多了GPU 需要执行更长的时间。此时我们会观察到GPU 的流水线没有空闲气泡但系统的吞吐大幅度下降。注这里仅分析纯 Prefill 场景下的 TTFT因此不涉及连续批处理Continuous Batching仅是动态批处理Dynamic Batching。11.2. 实验验证我们继续修改前面的实验代码增加动态批处理处理逻辑来模拟 Overlap Scheduling 过度并行导致的性能下降。1实验代码代码较长为免影响阅读体验放到 githubhttps://github.com/cswuyg/tools/blob/main/overlap_scheduling/overlap_scheduling_with_dynamic_batching.py启动nsys profile --tracecuda,osrt,nvtx --gpu-metrics-devices0 python3 overlap_scheduling_with_dynamic_batching.py2实验结果开始测试 100 个请求 (QPS50)... -------------------------------------------------------------------------------- Normal 模式 - 总耗时: 2.3594s | 平均延迟: 366.42ms | 请求队列等待: 123.12ms | 吞吐: 42.38 req/s | 推理次数: 10 Overlap 单线程模式 - 总耗时: 2.4227s | 平均延迟: 509.54ms | 请求队列等待: 99.22ms | 吞吐: 41.28 req/s | 推理次数: 12 Overlap 三线程模式 - 总耗时: 10.9589s | 平均延迟: 4620.92ms | 请求队列等待: 13.53ms | 吞吐: 9.12 req/s | 推理次数: 66 -------------------------------------------------------------------------------- 单线程 vs Normal - 吞吐提升: -2.62% | 延迟降低: -39.06% 三线程 vs Normal - 吞吐提升: -78.47% | 延迟降低: -1161.09% 三线程 vs 单线程 - 吞吐提升: -77.89% | 延迟降低: -806.88%可以看到 CPU 单线程和三线程 Overlap Scheduling 模式的吞吐和 TTFT 都变差了因为动态批处理的批次太碎了。3观察 NSYS图 11我们可以看到GPC Clock Frequency 在做 Normal 模式计算时频率升高这意味着 GPU 的功耗管理系统检测到了更高的计算强度。SM Warp Occupancy 的黄色部分在三种模式下都持平因为用的是同一个 Kernel只是维度变化它所需要的计算资源是固定的。CUDA HW 部分可以看到 Normal 模式的 GPU 流水线有很大的空隙其他两种模式都是连续使用的。实验结果和我们的理论推导一致CPU 单线程和 3 线程的 Overlap Scheduling 虽然让 GPU 流水线不断流但是削弱了动态批处理GPU 的计算强度下降系统的吞吐和 TTFT 都变得更差。12. 权衡 CPU 并行和动态批处理的方案12.1. 基础方案回到文章开头的 Overlap Scheduling GPU 饥饿场景下的时序图CPU 调度只使用一个线程多个没有依赖的 CPU 任务串行执行这可能是权衡之后的方案图 12获取请求尽量的晚执行保障动态批处理尽量的获取更多请求付出 GPU 流水线有一定的空隙的代价获得更大的批处理但这种设计不是精准可控的使用多个线程执行 CPU 任务如果做到精准可控也有可能进一步优化。12.2. 精准控制 CPU 并行1理论分析上述的平衡方案也可以进一步优化我们仍然可以使用多线程来做 CPU 任务然后基于耗时统计让 Kernel Launch 在最合适的时机执行也就是尽量的多攒请求又不会导致 GPU 流水线断流。下面继续改造实验代码来验证。2实验代码通过预估当前任务的推理时间在当前任务结束前夕完成下一个批次任务的收集确保 batch size 足够大同时让 GPU 不要断流。当然实验代码没有很精细耗时预估不准所以还是有少量的 GPU 空隙还可以进一步优化。代码较长为免影响阅读体验放到 githubhttps://github.com/cswuyg/tools/blob/main/overlap_scheduling/overlap_scheduling_with_dynamic_batching_enhance.py启动nsys profile --tracecuda,osrt,nvtx --gpu-metrics-devices0 python3 overlap_scheduling_with_dynamic_batching_enhance.py3运行效果开始测试 100 个请求 (QPS50)... -------------------------------------------------------------------------------- Normal 模式 - 总耗时: 2.2206s | 平均延迟: 277.31ms | 队列等待: 95.61ms | 吞吐: 45.03 req/s | 推理次数: 12 Overlap 单线程模式 - 总耗时: 2.3685s | 平均延迟: 351.26ms | 队列等待: 69.72ms | 吞吐: 42.22 req/s | 推理次数: 17 Overlap 三线程模式 - 总耗时: 7.3986s | 平均延迟: 4553.77ms | 队列等待: 15.93ms | 吞吐: 13.52 req/s | 推理次数: 53 Overlap 三线程优化版 - 总耗时: 2.0589s | 平均延迟: 271.82ms | 队列等待: 15.64ms | 吞吐: 48.57 req/s | 推理次数: 14 -------------------------------------------------------------------------------- 单线程 vs Normal - 吞吐提升: -6.24% | 延迟降低: -26.67% 三线程 vs Normal - 吞吐提升: -69.99% | 延迟降低: -1542.13% 三线程优化 vs Normal - 吞吐提升: 7.85% | 延迟降低: 1.98% 三线程优化 vs 三线程 - 吞吐提升: 259.35% | 延迟降低: 94.03% | 推理次数减少: 73.58%可以看到CPU 三线程优化版本推理次数和 Normal 模式接近吞吐和 TTFT 也都好于 Normal 模式但还有优化空间看下图 NSYS 分析。4NSYS图 13图中可以看到优化版本的 GPU 流水线还是有少量的空隙。12.3. 总结Overlap Scheduling 里的 CPU 任务调度应该“恰到好处”的和 GPU 并行不能太早也不能太晚才能在 Overlap Scheduling 和 动态批处理之间取得平衡获取最大收益。当前 SGLang 和 vLLM 都没有把获取请求和 Kernel Launch 两个 CPU 任务拆分到独立工作线程甚至 SGLang 没有把推理后处理拆分到独立工作线程可能都是基于 Overlap Scheduling 和动态批处理的平衡考虑如果追求极致应该可以进一步优化但也应该考虑到这类优化是和业务场景强相关的如果一次推理的时间足够长单线程的 CPU 不是瓶颈那当前的实现也足够了。13. 附SGLang 早期实现的 Overlap SchedulingOverlap Scheduling 的剖析就基本完成了这里再附上我第一次分析 Overlap Scheduling 时的记录。在 SGLang 的早期版本里没有使用双流而是通过工作线程执行推理然后主线程等待工作线程的 Kernel LaunchGPU 任务都是异步执行的launch 完函数就返回完成后才继续往下执行其他 CPU 任务。这里工作线程和主线程本质上是串行执行的其实没有必要开启工作线程当前最新的 SGLang 也确实去掉了工作线程。图1414. 附其他信息1数据传输和计算的重叠。当前的推理时间不仅是推理时间还包括推理结果从 GPU 拷贝到 CPU的时间D2H这里也可以使用多流vLLM、SGLang 就是这么干的。2CPU 并行计算改造里的后处理线程。实际上 vLLM 就有这样的线程但 SGLang 没有它都在主线程里完成。3多推理一个 token。Overlap Scheduling 的异步实现会导致结束推理的请求不能及时终止它又进入下一轮推理。这要求在结果处理、清理结束请求资源时需要做容错处理同时会多推理一个 token这对纯 prefill 请求浪费占比较大。