ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LLM推理调度实现剖析:Overlap Scheduling优化GPU利用率

LLM推理调度实现剖析:Overlap Scheduling优化GPU利用率 最近在折腾 LLM 推理框架的调度层把一个简化版的 SGLang 调度器就叫它 Mini-SGLang拆开重写了一遍重点研究 Overlap Scheduling 的实现。这个项目说大不大但它把推理引擎里最容易出问题的两块——数据流和同步边界——全部暴露出来了。如果你也在做推理框架二次开发、想把 prefill 和 decode 重叠起来提高吞吐或者只是好奇 SGLang 这种级别的框架内部是怎么调度的这篇文章应该能帮你少走不少弯路。Mini-SGLang 的核心目标很简单用尽可能少的代码复现 SGLang 调度器的最关键行为——请求怎么进、怎么排队、怎么分到 worker 上、KV Cache 怎么流转、哪些地方必须同步等待。我在实现过程中最大的感受是调度器本身不难写难的是理清数据流的每一跳和同步边界的每一处成本。这篇文章就是把这两条线完整梳理一遍附上我实测中积累的排查经验。1. 项目概述为什么需要一个迷你版调度器1.1 起念从线上一个诡异的高延迟说起先说背景。前阵子我维护的一个推理服务出现了一个很典型的症状decode 阶段的 token 延迟偶尔飙到 3 秒以上但 GPU 利用率并不高显存也远没打满。直觉告诉我是调度层的问题但生产环境的框架封装得太厚日志里根本看不出是哪个环节卡住了。于是我自己动手搭了一个 Mini-SGLang把调度器的核心逻辑精简到几千行专门用来复现和观测这类问题。这里解释一下为什么选择 SGLang 作为参考对象。SGLang 是目前 LLM 推理框架里调度策略做得比较激进的一个尤其是它的 radix attention 缓存复用和连续批处理continuous batching调度在业界属于第一梯队。把一个复杂框架迷你化不是为了重复造轮子而是为了把黑盒变成白盒——当你能在一个可控的小型系统里随意加日志、改策略、模拟各种畸形负载时你对生产系统的理解会上一个台阶。Mini-SGLang 的定位就是一个教学级 实验级的调度器模拟器单机多卡部署、支持张量并行和简单的流水线并行、实现了连续批处理和 chunked prefill、最关键的是把 Overlap Scheduling 的核心路径prefill 与 decode 重叠、通信与计算重叠用清晰的数据流展现出来。适合三拨人刚入门推理框架想搞懂调度原理的、已经在做推理优化想验证新策略的、以及要排查线上调度问题但需要一个可控实验环境的。1.2 三个必须提前搞懂的概念在深入代码之前有三个概念是整个项目的基石也是后文所有讨论的前提。第一个是连续批处理Continuous Batching。传统静态批处理要求一个 batch 里的请求同时开始、同时结束任何一个请求拖后腿都会卡住整批。连续批处理把批的概念从物理层面解耦了调度器每一轮都重新决定哪些请求进 GPU 执行有请求结束了就从等待队列补充新请求进来GPU 永远在处理最新鲜的组合。Mini-SGLang 里就是一个简单的循环schedule() - execute() - schedule()每一轮调度都重新审视全局状态。第二个是分块预填充Chunked Prefill。一个超长 prompt 的 prefill 阶段如果一口气算完会独占 GPU 很长时间把其他请求的 decode 全部堵死。分块的做法是把长 prompt 切成若干块每块之间允许插入 decode 请求的执行形成交错执行。这是 Overlap Scheduling 的最基础形态。第三个是 KV Cache 管理。prefill 阶段产生的 Key/Value 张量需要缓存下来供后续 decode 使用这个缓存怎么分配、怎么复用、什么时候回收直接决定了调度器的资源视角。SGLang 用 radix tree 来管理 KV Cache 的共享前缀Mini-SGLang 我做了一个简化版用引用计数 空闲链表来管理先跑通逻辑再考虑前缀树的优化。2. 整体架构与调度模型拆解2.1 调度器的三层结构Mini-SGLang 的调度器我分成了三层每一层职责单一方便单独测试和替换实现。最上层是请求管理层Request Manager负责接收外部的推理请求维护三个队列等待队列waiting、运行队列running、已完成队列finished。等待队列里的请求经过 tokenizer 变成 token 序列加上一个唯一的请求 ID 后等待被调度。运行队列里的请求是当前正在参与前向计算的活跃请求每个请求都带有一份 KV Cache 的块引用列表。中间层是资源管理层Memory Manager这是调度器的心脏。它维护着一张全局的 KV Cache 块表每一块有固定的 token 容量Mini-SGLang 里我设成 16 个 token/块这是参考了 vLLM 早期版本的默认值块的状态有 free、allocated、referenced 三种。调度器每轮决策前都要向 Memory Manager 查询现在还能腾出多少块这个数字直接决定了能放多少请求进 running 队列。最下层是执行编排层Executor负责把调度器选中的请求集合转换成真正的 GPU 计算。Mini-SGLang 里执行层封装了模型的前向函数支持张量并行TP模式下的 multi-device 调用。这里有个容易忽略的细节调度器的决策是 CPU 侧的而模型执行是 GPU 侧的两者之间有一个异步的提交过程这个异步边界恰恰是后面讲 Overlap 的关键锚点。2.2 连续批处理的状态机每一轮调度的核心是一个状态机转移过程。我把一轮调度的逻辑梳理成了五步这五步在 SGLang 源码里也能找到对应实现只是那里被各种优化分支包围了扫描等待队列按请求到达时间排序逐个检查是否有足够的 KV Cache 块。预填充一个请求需要的块数 ceil(prompt_tokens / block_size)如果块不够停止扫描因为后面的请求需要更多块肯定也进不来。决策 prefill / decode 分组新进入的请求标记为 prefill 组已经在 running 队列里的请求标记为 decode 组。如果开了 chunked prefill还需要把长 prompt 的第一个 chunk 单独标记。向 Memory Manager 申请块为 prefill 请求分配 KV 块。注意 decode 请求不需要新分配块它们复用之前 prefill 阶段分配的块除非做 prefix caching 的扩展。组装 batch 并提交 Executor把 prefill 组和 decode 组拼成一个执行批次传给 Executor 执行一次前向。收集结果并更新状态采样出新 token判断请求是否结束释放 finished 请求的 KV 块。这个状态机看起来平淡无奇但 Overlap Scheduling 的入口就藏在第 4 步和第 5 步之间。传统实现里第 4 步是阻塞的提交一个 batch等 GPU 算完再进入下一轮调度。而重叠调度的思路是prefill 组和 decode 组可以拆成两次独立的提交让 decode 组先跑prefill 组后跑或者反过来利用 GPU 的流stream机制让两者在时间上交错。2.3 串行执行的问题在哪为了说清楚 Overlap 的价值我们先量化一下串行执行的代价。假设一个场景当前 running 队列里有 10 个 decode 请求每个请求 batch 内耗时 10ms等待队列里来了一个 2000 token 的 prefill 请求这个 prefill 单独算需要 40ms。串行模式下如果先跑 prefill 再跑 decode那么这轮总共耗时 40 10 50ms其中 40ms 里面 10 个 decode 请求完全没输出 token用户体验是卡了四倍时间。如果是先 decode 再 prefill虽然 decode 及时了但 prefill 请求的首 token 延迟TTFT就会增加一个 decode 周期。无论哪种优先级总有一个侧受损。更有意思的是 GPU 视角。40ms 的 prefill 是计算密集型的矩阵乘10ms 的 decode 是访存密集型的矩阵乘两者叠加执行时prefill 能把 decode 等待显存数据的时间填充起来反过来 decode 的访存间隙也能被 prefill 的计算顶上去。所以从硬件利用率角度看串行本身就是一种浪费——这正是 Overlap Scheduling 存在的根本理由。3. 数据流全景请求从进入到回包的完整旅程3.1 入口层请求接收与分词Mini-SGLang 的入口是一个简单的 HTTP server收到请求后立即做两件事分词和分配请求 ID。分词这一步在数据流里看似不起眼但它有一个隐藏的性能陷阱——tokenizer 是 CPU 操作如果放在调度循环里同步执行会阻塞整个调度线程。我的做法是维护一个独立的 tokenizer 线程池请求进来先丢进一个预处理队列调度器从预处理队列取的是已经分好词的样本。在数据流图上这一步标记为入口边界从这里开始请求从 JSON 结构变成了内部的ReqSample结构包含request_id、input_ids、sampling_params三个核心字段。input_ids是一维整数数组它就是后续所有计算的数据源头。分词还有个细节值得提input_ids不能直接拿去执行还需要在做 padding 时记录真实的 token 长度。Mini-SGLang 里我在ReqSample上存了valid_len字段padding 到 batch 内最大长度后执行时通过 attention mask 屏蔽 padding 部分。这个字段在调度器计算 KV 块需求时也要用到属于贯穿全程的元数据。3.2 调度层队列流转与优先级请求进入调度层后按waiting - running - finished的顺序流转。我在实现里给每个状态加了一个时间戳方便事后回放数据流。流转规则只有两条waiting 里有请求且块足够就转入 runningrunning 里的请求生成了终止 token 或达到最大长度就转入 finished。优先级策略这里多说一句。Mini-SGLang 默认用先来先服务FCFS但这在生产里是不够的真实 SGLang 提供了更丰富的策略。我实验后发现对 Overlap Scheduling 影响最大的是 prefill 和 decode 的优先级权重如果 decode 权重过高极端情况下 prefill 会被饿死新请求永远无法开始如果 prefill 权重过高decode 的延迟会持续恶化。一个比较稳的策略是给 decode 请求设置一个最长等待时间超过阈值后强制提升优先级。这个阈值我建议从 500ms 开始调太激进会造成调度抖动。调度层还负责一个容易忽略的数据流操作请求间的 KV 块共享。Mini-SGLang 简化了 radix tree但保留了块引用的概念——两个请求如果前缀完全一致比如系统提示词相同可以让它们引用同一批 KV 块的前缀部分。这个引用让数据流从请求独占块变成了块被多请求共享也带来了引用计数的管理负担回收时要等最后一个引用者结束。3.3 执行层张量数据流与 KV Cache执行层的数据流是整个系统里最复杂的一段。它接收的是调度器产出的一个 batch 描述结构每个请求的input_ids、valid_len、KV 块索引列表。这些数据要在 Executor 里拼装成 GPU 张量先按valid_len从大到小排序分组这能减少 padding 浪费然后 pad 到 batch 最大长度再拷贝到 GPU 显存。前向计算的数据流是一条严格的主链embedding - 各层 transformer - lm_head - sampling。每一层 transformer 内部又有两条支链attention 支链读取并更新 KV CacheMLP 支链做纯计算。这里有一个关键观察attention 支链是天然带读写依赖的——当前 token 的 Q 要与历史 K/V 做点积所以必须等历史 K/V 就位。而 MLP 支链只依赖当前 token 的输入和历史无关。这个差异对 Overlap 的意义在于如果某一层 attention 需要等待跨设备的 all-reduce 通信那么下一层的 MLP 计算理论上可以提前开始。前提是你能把算子依赖图拆开并在 CUDA 层面用多 stream 实现。Mini-SGLang 里我用 PyTorch 的多 stream 封装了这个逻辑虽然实际提升受限于 PyTorch 的算子粒度但足够验证思路了。3.4 出口层流式返回与块回收请求生成第一个 token 后就可以通过流式接口返回给客户端了。Mini-SGLang 用的是简单的 chunked HTTP response每 decode 一轮往连接里写一个 token。这里有一个数据流上的坑写响应是网络 IO一定不能放在调度线程里同步做。我踩过这个坑——把响应发送放在调度循环内结果网络慢的客户端直接拖慢了整个调度周期。正确的做法是把响应塞进一个 queue由独立的 sender 线程消费。请求结束后它的 KV 块需要释放。释放的逻辑在数据流里是引用计数归零每个块记录被哪些请求引用最后一个请求结束时ref_count变 0块归还给空闲链表。这个回收看起来简单但如果漏了某个分支比如请求因为异常被中断就会造成永久性显存泄漏。Mini-SGLang 里我加了一个周期性的 GC 扫描检查 running 队列里是否存在长时间没有进展的请求强制回收它们的块。4. 同步边界哪些地方必须停下来等4.1 同步的三种基本形态如果把数据流比作一条河流同步边界就是河上的闸门。闸门是必须存在的——没有闸门水位和流量就会失控——但每个闸门都有成本。在 Mini-SGLang 里我归纳出三种基本同步形态。第一种是调度器内部的显式同步CPU 侧 barrier调度线程必须等 Executor 返回这一轮结果才能进入下一轮决策。这是最粗粒度的同步也是开销最容易感知的。哪怕 GPU 算得再快只要这个 barrier 存在调度周期就不会小于 GPU 执行时间。第二种是通信同步GPU 侧 collective张量并行下每完成一个算子的局部计算就需要一次 all-reduce 把跨设备的中间结果合并。这类同步由 NCCL 库管理隐藏很深但它实实在在占用 GPU 时间。我在 profile 时看过TP4 规模下 all-reduce 能占到总时长的 10%15%这是必须正视的固定开销。第三种是资源边界同步CPU-GPU 间 memory fence调度器分配或释放 KV 块时必须确保 GPU 侧对应的显存操作已经完成。Mini-SGLang 里我用 CUDA event 来做这个 fence确保释放的块不会被正在执行的 kernel 读取。这类同步最容易出错因为它跨越了 CPU 和 GPU 两个执行域想当然地认为我改了逻辑GPU 就立即知道是典型的错误思维。4.2 张量并行下的 All-Reduce 边界张量并行是理解同步边界的绝佳样本。以 TP2 为例一个 transformer 层的 attention 输出在 device 0 和 device 1 上各算了一半要拼成完整的注意力结果必须做一次 all-reduce。这个 all-reduce 就是一道硬同步边界两个设备必须都算完自己的部分然后互相交换数据最后一起进入下一段计算。从数据流角度看all-reduce 边界的特征是短暂但昂贵它让每个设备在等待对方数据时处于空闲状态。为了减少这种空闲业界常用的手段是通信与计算重叠——把当前层的 all-reduce 和下几层的计算同时发出去。这个说起来简单做起来需要谨慎处理依赖关系因为下一层计算往往依赖 all-reduce 的结果。Mini-SGLang 里我用 CUDA events 实现了通信计算重叠的原型把 all-reduce 放到独立的通信 stream 上主计算 stream 继续推进不依赖该结果的部分。实测在有足够长计算链的情况下可以掩盖 30%50% 的通信延迟。但这个收益不是免费的多 stream 会引入事件同步的复杂度stream 之间如果忘记 wait数据错乱是必然的。4.3 批次边界与 KV Cache 生命周期批次边界是最容易被忽略的同步点。连续批处理框架下一个批次的生命周期是调度器确定请求集合 → Executor 执行一次前向 → 结果收集回来。下一个批次可能是完全不同的请求集合。这意味着每个批次的 KV Cache 访问模式都不同上一批释放的块这一批可能被另一个请求占用。这个切换动作要求调度器在 CPU 侧完成一系列内存簿记操作——更新块分配表、调整引用计数、整理空闲链表——然后才能安全地启动下一批。在 Mini-SGLang 里这个簿记耗时约 0.51ms单看不值一提但如果调度周期本身只有 20ms它占了 5% 的 CPU 时间。优化方向是把簿记操作与 GPU 执行重叠GPU 跑当前批次时CPU 提前规划好下一批次的块分配。这就是 Overlap Scheduling 在调度器层面的核心技巧。KV Cache 的生命周期还牵扯到一个数据一致性问题prefill 阶段写入的 KV 块decode 阶段必须读到完全相同的值。这里涉及一个隐含的同步边界——如果一个 KV 块同时被 prefill 写入和 decode 读取同一轮内必须保证写入完成才能读取。好在 GPU 前向计算天然保证了同一 batch 内的顺序性这个边界只在跨 batch 或跨设备的场景下才需要显式处理。4.4 同步边界的成本测算做调度优化之前先量化成本这是我的习惯。Mini-SGLang 里我加了一组计时探针统计一轮调度中每个同步边界的耗时占比。下面是一组典型数据TP2batch16平均 token 长度 512同步边界平均耗时占总周期比例可否消除调度器 barrierCPU 等待 GPU3.2 ms12%部分可重叠all-reduce每层 2 次 × 12 层4.5 ms17%可部分重叠内存簿记块分配/释放0.8 ms3%可与 GPU 并行响应发送网络 IO1.5 ms6%应异步化GPU 纯计算16.2 ms62%不可消除这个表告诉我一个反直觉的结论同步开销约 38%比很多人想象的高得多。也解释了之前生产环境里 GPU 利用率不高但延迟高的现象——GPU 在等同步而不是在算。提升吞吐的关键不是把算子写得再快一点而是把同步边界的时间填满。5. Overlap Scheduling 的实现要点5.1 预填充与解码的重叠Overlap Scheduling 最核心的一条路径是 prefill 与 decode 的重叠。我在 Mini-SGLang 里实现了两版一版是时间片轮转一版是主从批次对比下来差异很大。时间片轮转最简单把 prefill 请求切成小 chunk比如 256 token 一个每个调度周期只处理一个 chunk其余时间给 decode。这个方案实现难度低但坏处是 chunk 太小导致 prefill 的计算效率下降矩阵维度太小无法充分利用 tensor core。我实测 256 token chunk 的 prefill 效率只有整段 prefill 的 60% 左右。主从批次是我最终采用的设计把 batch 分成主批次decode 组和从批次prefill 组主批次正常执行从批次在独立的 CUDA stream 上执行。用一个简单的 CUDA event 控制从批次的 kernel 与主批次的 kernel 在计算资源上自动共享 GPU因为没有数据依赖可以真正并行跑。实测在单卡 A100 上这个方案能把 GPU 利用率从 68% 提到 82%同时 decode 的 P99 延迟只增加了 8%。代价是显存占用上升——prefill 的中间激活与 decode 的激活同时驻留显存峰值多了大约 15%。5.2 通信与计算的重叠前面提到 all-reduce 是张量并行下的硬同步边界那么把它和计算重叠就是 Overlap Scheduling 的第二个主战场。Mini-SGLang 里的实现思路是把一层 transformer 的计算拆成两个阶段——阶段一算 attention 的本地部分阶段二算 MLP——将 attention 的 all-reduce 和下一层的 embedding/attention 本地计算重叠。具体代码逻辑是attention 本地计算 - 发出 all-reduce通信 stream- 不等结果先推进到下一个 stage 的本地部分 - 等 all-reduce 事件 - 取回结果继续。这里有一个重要的前提模型的层与层之间必须存在可以提前计算的独立切片。比如 MLP 的上投影只依赖输入 token 的 embedding不依赖 attention 结果那就可以在 attention all-reduce 等待期间先算它。不是所有模型结构都支持这种拆分我在实现受限的模型上会退化为普通的同步 all-reduce。关于收益我要说句实在话通信计算重叠在 TP 规模小2/4时收益有限因为 all-reduce 本身耗时短重叠窗口太小。但在 TP8 或者跨机场景下通信延迟放大重叠收益会非常显著。如果你只是单机双卡做测试不用太纠结这一层优化把精力放在 prefill/decode 重叠上性价比更高。5.3 多流调度与算子排布多流multi-stream是实现重叠的技术底座。Mini-SGLang 里我维护了三个 CUDA stream主计算 stream、prefill stream、通信 stream。它们的协作关系是主计算 stream 跑 decode 请求的算子这是优先级最高的路径。prefill stream 跑 prefill 请求的算子通过事件的 wait 与主计算 stream 解耦——prefill 不需要等 decodedecode 也不等 prefill二者完全异步。通信 stream 跑 all-reduce 等集合通信操作由 NCCL 管理。关键三个 stream 在必要处用 CUDA events 建立依赖。比如第 N 层 MLP 的输入依赖第 N 层 attention 的 all-reduce 结果所以通信 stream 的事件要插入到主计算 stream 的对应位置。算子排布operator placement是容易被忽略的细节。如果你只是把算子丢到不同 stream 上GPU 上的实际执行顺序往往不符合你的直觉。我踩过的坑是两个 stream 上的小算子交替调度反而造成频繁的 kernel 切换性能不升反降。解决方案是对算子做批化batching把多个小 GEMM 合并成一个大 GEMM减少 kernel 启动次数。这个优化在 Mini-SGLang 里直接体现在 prefill stream 的处理上——我把一个 chunk 的多个 token 的 Q 矩阵拼接后一次性计算。5.4 一个可落地的调度策略配置最后给一套我反复调试验证过的配置可以直接作为 Mini-SGLang 或同类框架的起步参数。注意这些数字依赖具体硬件和模型不要照搬但可以作为调参的起始点。参数推荐值说明block_size16 token太小则块表开销大太大则显存碎片多max_running_requests由可用显存 / block_size推算至少留 20% 显存给激活值prefill_chunk_size512 或 1024 token256 效率太低2048 又可能卡住 decode 太久decode_max_wait300~500 ms超过后强制优先 decode避免饥饿TP 规模先单卡验证再上 TP2不要在 TP4 时做通信重叠收益不明显CUDA stream 数3主计算 prefill 通信超过 4 个 stream 管理成本大于收益这里还要提醒一个调度策略的全局原则Overlap 不是免费的。你把 prefill 和 decode 重叠了GPU 算力确实用得更满但每个请求的总圈数变多了——因为 GPU 同时在服务两类请求。最终效果是系统的总吞吐提升但单请求的延迟曲线会变胖。如果你的服务是面向实时交互的比如聊天助手要在吞吐和延迟之间做权衡如果是离线批量推理则可以激进地把重叠开到最大。6. 常见问题与排查实录6.1 死锁与资源饥饿Overlap Scheduling 引入多流和异步之后最常见的 bug 就是死锁。我在 Mini-SGLang 里踩过的一个典型死锁是prefill stream 在等待一个由主计算 stream 产生的事件而主计算 stream 又在等待 prefill stream 释放的显存块。两边互相等GPU 空转调度线程永久阻塞。排查这种问题的办法只有一个在所有 stream 的等待点插入带超时的 wait并记录等待时的调用栈。我写了一个包装函数给每个wait_event加上 5 秒超时超时后打印当前所有 stream 的状态和最近执行的 kernel 列表。有了这个工具绝大多数死锁在 10 分钟内就能定位。资源饥饿是另一种软死锁prefill 请求永远抢不到 KV 块因为 decode 请求把块都占满了而且 decode 完成得很慢。我的对策是引入抢占式回收当 prefill 在等待队列里停留超过阈值比如 2 秒调度器强制把一些 decode 请求的块压缩回收腾挪给 prefill。这个策略的本质是为新请求挤出存活空间虽然会牺牲少量 decode 的连续性但避免了整体系统的僵化。6.2 显存波动与碎片化Block 级别的 KV Cache 管理在长时间运行后会遇到碎片化问题。现象是显存明明还有不少空闲块但分配时找不到一段连续的空闲区间。因为我在实现里用连续数组模拟显存池block 释放后留下的空洞如果不整理新请求的大块分配就会失败。解决办法有两个层次。第一层是尽量分配小块把一个请求的 KV 块列表打散到多个位置用链表而不是连续数组来组织。SGLang 本身就用块表来做这种散列分配我的实现也是复制这个思路。第二层是必要时做块整理Mini-SGLang 里加了一个 defrag 触发条件——当空闲块总数超过 30% 但连续大块不足时触发一次整理把活跃请求的块搬移到显存池的低地址侧。整理期间要暂停调度所以不能频繁触发。6.3 同步开销反而拖垮吞吐这是最容易让人困惑的问题明明做了 Overlap吞吐反而下降了。我遇到的一个典型案例是prefill 和 decode 各自跑都很快但重叠后总吞吐下降了 10%。分析后发现罪魁祸首是两套注意力 kernel 同时执行导致的 L2 cache 争抢——decode 的 KV 访问模式是高频随机读prefill 是连续大矩阵乘混在一起把 cache 的局部性打碎了。解决这个问题的思路不是取消重叠而是错峰重叠让 prefill 的算子在 decode 的算子间隙执行而不是严格同时执行。具体做法是在 kernel 层面给 prefill stream 的算子加一个小的延迟启动比如 50100 微秒让 decode 的关键算子先拿到 GPU 资源。这个技巧听着有点粗糙实测确实有用在 cache 争抢严重的模型上恢复了 8%12% 的吞吐。6.4 排查工具与方法论最后分享一下我的排查工具链这些在 Mini-SGLang 的代码仓库里都有对应的实现。核心是三件套时间线日志timeline log、事件探针event probe、显存快照memory snapshot。时间线日志记录每个请求在每个阶段的进入和离开时间——进入调度器、拿到 KV 块、进入 Executor、产出第一个 token、结束。数据流异常时时间线日志能直接告诉你卡在哪个环节。事件探针是分布在各同步边界上的计时点汇总后能生成我前面 4.4 节那种成本分布表这是优化方向的依据。显存快照是在显存分配和释放时记录块表的完整状态排查泄漏和碎片问题必备。方法论上的建议是做调度优化一定要建立对照组。Mini-SGLang 里我保留了三个档位的开关——纯串行模式、时间片轮转模式、完整 Overlap 模式——任何改动都在这三个模式上跑同一组基准数据。没有对照你就分不清吞吐提升到底来自重叠调度还是来自某个巧合的缓存命中。写在最后的一些体会做完 Mini-SGLang 这个项目我最大的感触是调度器的本质就是在不确定性中做权衡——请求到达时间不确定、GPU 空闲时机不确定、显存释放时刻不确定所有优化手段都是在和这些不确定性博弈。Overlap Scheduling 不是把一切都异步化就完事了它需要你精确知道哪些同步边界可以拆、哪些不能拆以及拆了之后用什么机制来保证正确性。我自己在实践中的一个小习惯是每加一个重叠逻辑先问三个问题——数据依赖是否清零了有无可能产生等待环最坏情况下的副作用是什么三个问题都能回答再上手改代码。希望这篇文章能帮你在自己的调度器上少踩几个坑也欢迎在实际项目中验证这些方法后回来交流。
返回列表