03 - 三种调度器与模型运行器
07 篇说「每段配一套匹配它的调度」。这一篇讲「配」具体是什么意思。
最难的是自回归那段。它要管的事很杂:显存怎么分给一堆同时在跑的请求、该挑哪几个凑成一批、装不下时踢谁出去、怎么少发点指令。这些写一套好几千行,而且属于「写对不难、写快极难」—— SGLang 在上面磨了两年。
你不想重写,但也不能直接用:SGLang 的调度器自带外壳,自己开 ZMQ 收请求、自己管分词器。而在多阶段流水线里请求是从上一个阶段的队列来的,分词早在预处理做完了。你要的只是它中间那块核心。
把一个自带外壳的东西剥出核心来用,两种做法差别巨大:
| 做法 | 省不省事 | 一年之后 |
|---|---|---|
| 抄一份改(分叉) | 省事 | 上游每次升级都要手工合并,最终再也跟不上。已有 SGLang 的下游项目这么栽过 |
| 只调公 开方法,不碰内部字段(组合) | 麻烦 | 上游升级基本无感 |
SGLang-Omni 选了后者,并写了三条纪律防止它慢慢滑向前者。
一、三种调度器接的是三类计算
07 篇分出的三类计算,在这里一一对应到三种调度器。它们对外是同一个接口,对内的结构完全不同:
三者对外接口完全一致 —— inbox、outbox、start()、stop()、abort(request_id)。Stage 因此不需要对调度器类型做任何分支,这是整套架构能不断加新模型而不塌的原因。
对内则完全不同:
| OmniScheduler | SimpleScheduler | Code2WavScheduler | |
|---|---|---|---|
| 接哪类计算 | 自回归 | 无状态一次性 | 有状态流式 |
| 内部循环 | 复用 SGLang 的事件循环:选批 → run_batch → 处理结果,跑在独立线程 | inbox.get() → fn(data) → outbox.put(),可选 batch_compute_fn 本地攒批 | 按消息类型分三支:new_request 初始化、stream_chunk 累积并解码、stream_done 冲出剩余 |
| 持有什么状态 | KV cache、树缓存、运行批、请求池 —— 最重 | 什么都不持有 | 每请求的因果偏移、注意力缓存、音频缓冲 |
| 中止时要做什么 | 归还 KV 页 |