SGLang 深拆 · Scheduler × Kernel

Scheduler 决定 GPU 吃什么,Kernel 决定 GPU 怎么吃。

如果只看 SGLang 的 benchmark,很容易把“快”理解成某个 attention kernel 很猛。其实不是。SGLang 真正厉害的地方是:上层 scheduler 负责把请求组织成对 GPU 最友好的工作负载,下层 kernel/backend 再把这批工作尽量以最少浪费跑完。这页就专门拆这两层。

Scheduler = admission + batching + cache + phase orchestration
Kernel = attention backend + graph replay + KV planning
高吞吐来自两层配合,不是单点魔法

一句话先说透

SGLang 不是“某个算子快”,而是“把在线 serving 的碎片化工作,压成更连续、更可预测、更可复用的 GPU 工作流”。

Scheduler 的职责

决定哪些请求进入 batch、prefill 与 decode 怎么穿插、哪些 prefix 能复用、KV cache 何时保留或释放、speculative worker 何时介入。

Kernel 的职责

把已经成型的 batch 真正落到 GPU 上,选择 FlashInfer / Triton / CUDA Graph / piecewise graph / paged KV 等低层执行路径。

系统精髓

上层尽量减少“坏 batch”,下层尽量减少“坏执行”。前者决定工作形状,后者决定执行效率。

Scheduler 的核心架构是什么样

从源码结构看,Scheduler 不是单纯的 FIFO 队列,而是一个把请求、缓存、batch、worker、speculative 路径、并行策略绑在一起的“在线推理控制器”。

入口对象
Scheduler 主体
python/sglang/srt/managers/scheduler.py:270 定义 class Scheduler。它直接依赖 ScheduleBatchPrefillAdderSchedulePolicyRadixCache 和 speculative 相关结构,说明它是整个 serving runtime 的编排中心,而不只是一个“排队器”。
批组织层
ScheduleBatch + PrefillAdder
ScheduleBatchschedule_batch.py:1250,负责把一轮要跑的请求整理成 GPU 可消费的 batch;prepare_for_extend()1503mix_with_running()1831,这两个点说明它既要准备 prefill/extend,又要跟正在运行的 decode batch 进行合流。
策略层
SchedulePolicy
schedule_policy.py:96 定义 class SchedulePolicycalc_priority()117。这层的意义不是抽象优雅,而是把“谁先跑”“怎么兼顾延迟和吞吐”从具体执行里分离出来。
缓存层
Radix / Chunk / SWA Tree Cache
scheduler.py:746-780 一段很关键:根据不同模式实例化 ChunkCacheSWAChunkCacheRadixCacheCppHiRadixCacheSWARadixCache。这说明 scheduler 一开始就在思考“prefix 怎么复用”,不是把 cache 当附属品。
推测执行层
Speculative Worker
scheduler.py:334 会解析 speculative_algorithm381601 注释明确写了会在 speculative decoding 下额外启动 draft worker。也就是说,推测解码不是外挂,而是 scheduler 内建的分支路径。

最本质的理解:Scheduler 的目标不是公平,而是把请求组织成 GPU 最赚钱的 batch。

Scheduler 具体怎么跑

收请求,先变成可调度对象。 新请求进入后,不会直接怼进 GPU,而是先进入 scheduler 维护的 waiting / running 语义里,准备和已有 batch、已有 cache 命中关系一起评估。
优先算 prefix 复用价值。 因为 Scheduler 直接绑定 RadixCachescheduler.py:183)和多种 tree cache 初始化逻辑(746-780),说明一个请求值不值得马上跑,取决于它能否复用已有 KV,而不是只看 arrival time。
构造这一轮 batch。 ScheduleBatch 负责把这轮能一起跑的请求打包,prepare_for_extend() 说明 prefill/extend 是单独准备的,mix_with_running() 则说明 SGLang 会想办法把新增工作和正在 decode 的工作合并,避免 GPU 出现空洞。
决定这一轮跑 prefill、decode,还是 speculative 路径。 scheduler.py:334 解析 speculative algorithm,381/601 启动 draft worker。也就是说 scheduler 不只是发 batch,还要选执行模式。
执行后更新 cache 与请求生命周期。 跑完并不是简单返回 token;还要维护 tree cache 状态、释放终止或中断请求的 KV。像 scheduler.py:3144-3162 这些位置就是显式的释放逻辑。

Scheduler 真正在优化什么

  • 减少小而碎的 batch
  • 提高 prefix 复用命中率
  • 把 prefill 和 decode 的资源冲突压到最低
  • 让 speculative decoding 在 accept 率高时真正赚到吞吐
  • 控制 KV cache 生命周期,避免内存浪费拖垮后续 batch

它不是在优化什么

  • 不是单纯追求单请求最短延迟
  • 不是机械 FIFO
  • 不是把所有请求都尽快塞进 GPU
  • 不是把 cache 命中留给底层 kernel 再说

Kernel 这层到底是什么逻辑

SGLang 的“kernel”不是单一 .cu 文件,而是一整条执行路径:attention backend 选择、KV 索引规划、workspace 复用、CUDA Graph 捕获与 replay,以及对 speculative/滑窗/cross-attention 的特殊处理。

FlashInfer 路径:默认追求最快

flashinfer_backend.py:5-6 直接写明:现在有 FlashInfer 和 Triton 两个 backend,FlashInfer 更快,Triton 更容易定制

  • 51-58 引入 BatchDecodeWithPagedKVCacheWrapperBatchPrefillWithPagedKVCacheWrapperfast_decode_plan,说明它不是手写一个 attention kernel 就完事,而是用 paged KV 的批式执行接口组织 decode/prefill。
  • 110 定义 global_workspace_buffer203-219 复用全局 workspace,目的就是减少每轮临时分配。
  • 1132-1144global_override_indptr_cpu 是个很工程化的点:为了绕掉一部分 host-to-device copy 开销,直接快路径覆盖 plan 所需的 indptr。
  • 后面大量 decode_wrappers / prefill_wrappers 逻辑说明,kernel 层已经按场景区分了 sliding window、cross attention、speculative 等路径,而不是一个大一统 kernel。

Triton 路径:可控和可定制

triton_backend.py 里把 decode_attention_fwdextend_attention_fwdextend_attention_fwd_unified 包进 backend,对自定义逻辑和调试更友好。

  • 显式维护 kv_indptrkv_indicesqo_indptrmask_indptr 等 metadata
  • 支持 sliding window / deterministic inference / split KV
  • 说明 SGLang 的内核思路不是押单一库,而是保留多 backend 路由能力

CUDA Graph / Piecewise Graph

cuda_graph_runner.py:40graph_capture454get_batch_sizes_to_capture(),后面还有 replay()。这说明 SGLang 会为常见 batch 形状提前 capture,再在运行时 replay,减少 kernel launch 和 Python/CPU 调度噪音。

model_runner.py:664-665 明确初始化了 piecewise CUDA graph。它的价值在于:在线 serving 的 batch 形状并不总固定,piecewise 图能在“不完全静态”的情况下也吃到 graph 的收益。

ModelRunner 是内核执行的总控

model_runner.py:285 定义 ModelRunner,它拿着 req_to_token_pooltoken_to_kv_pool_allocator339-340),说明执行层并不只看到 tensor,还直接掌握 request→token 和 token→KV 的映射资源。

这很关键:它让 kernel 执行不是“盲算”,而是和 serving cache 布局强绑定。

Kernel 层的本质不是把 attention 写得更炫,而是把 batch 的 memory layout、KV 访问方式、launch 开销、graph 复用 一起压缩到最省的形态。

Scheduler 和 Kernel 是怎么咬合的

它决定什么 如果做不好会怎样 典型源码锚点
Scheduler 请求分组、cache 命中、prefill/decode 穿插、speculative 是否启用 GPU 吃到碎 batch,prefix 复用低,延迟和吞吐一起烂 scheduler.py:270, 183, 334, 746-780
ScheduleBatch 把逻辑请求整理成一轮可执行 batch batch 形状差,kernel 即使快也救不回来 schedule_batch.py:1250, 1503, 1831
ModelRunner 承接 batch,组织 forward、KV pool、graph 路径 执行层和 cache 布局脱节,memory traffic 暴涨 model_runner.py:285, 339-340, 664-665
Kernel Backend paged KV、attention kernel、workspace、metadata、launch 优化 单步算得慢,或每步 overhead 太高 flashinfer_backend.py:5-6, 110, 203-219, 1132-1144
CUDA Graph 把高频 batch 形状 capture/replay CPU launch 开销吃掉吞吐收益 cuda_graph_runner.py:40, 454, 1250

真正的协同链路

请求进来 → scheduler 判定 cache / priority / phase → ScheduleBatch 组织 batch → ModelRunner 把 batch 映射到 KV pool 与 graph 路径 → backend 用合适 kernel 跑出来。

这里最容易被忽略的一点是:kernel 的好坏,严重依赖 scheduler 先给它一个“好问题”。如果上层让 GPU 每轮都面对随机长度、低命中、低合并度的碎请求,再牛的 kernel 也只能在烂输入上尽量少输。

一句最直白的话

SGLang 快,不是因为有“一个神级 kernel”;而是因为它有一套 让 kernel 持续工作在甜区 的 scheduler。

为什么这套组合能打出高吞吐

1. 把 prefix 复用提升到系统级

很多系统也有 KV cache,但 SGLang 的关键是 scheduler 自己就围绕 tree cache / radix cache 做决策,而不是让 kernel 被动处理已经定型的请求。

2. 把 prefill / decode 拆清楚再重组

prefill 和 decode 的资源画像不同。SGLang 通过 ScheduleBatch 的 prepare / mix 逻辑,把两种 phase 的冲突尽量压低,同时保留合流机会。

3. 用 paged KV 和元数据规划,降低 memory traffic 浪费

FlashInfer / Triton backend 都显式维护 KV 索引、indptr、workspace、wrapper metadata。这不是“代码复杂”,而是在买更低的访存浪费。

4. 用 CUDA Graph 把高频形状收敛成低开销重放

在线���务最烦的是每步都要 launch 一堆小 kernel。graph capture/replay 把这部分 CPU 发射成本压掉,尤其在 decode 场景更值钱。

5. speculative decoding 真赚的时候能被调度层兜住

推测解码不是 accept rate 高就行,还需要 scheduler 能组织 draft worker 和验证路径,不然收益会被同步与协调开销吃掉。

6. 多 backend 不是分裂,而是避免单一路径失灵

FlashInfer 追求极致性能,Triton 保留可定制性,piecewise graph 处理非完全静态形状。它的哲学是:不同硬件和模型,留多条最优路径。

最后压成一个公式

高吞吐 ≈ 好调度 × 高 cache 命中 × 低 launch 开销 × 高效 attention backend × 尽量稳定的 batch 形状

这也是为什么你如果只 benchmark 单个 kernel,通常只能解释 SGLang 为什么“局部快”;但解释不了它为什么在真实 online serving 里整体更猛。

引用依据

下面这些点都来自我实际抓到的官方文档或源码路径,不是拍脑袋总结。

  • python/sglang/srt/managers/scheduler.py:270class Scheduler
  • python/sglang/srt/managers/scheduler.py:183 — 引入 RadixCache
  • python/sglang/srt/managers/scheduler.py:334 — 解析 speculative algorithm
  • python/sglang/srt/managers/scheduler.py:381, 601 — 启动 speculative draft worker 注释
  • python/sglang/srt/managers/scheduler.py:746-780 — 初始化 Chunk/Radix/SWA/HiRadix 等 tree cache
  • python/sglang/srt/managers/scheduler.py:3144-3162 — aborted / release KV cache 相关逻辑
  • python/sglang/srt/managers/schedule_batch.py:1250class ScheduleBatch
  • python/sglang/srt/managers/schedule_batch.py:1503prepare_for_extend()
  • python/sglang/srt/managers/schedule_batch.py:1831mix_with_running()
  • python/sglang/srt/managers/schedule_policy.py:96, 117SchedulePolicycalc_priority()
  • python/sglang/srt/model_executor/model_runner.py:285, 339-340, 664-665ModelRunner、KV pool、piecewise CUDA graph
  • python/sglang/srt/model_executor/cuda_graph_runner.py:40, 454, 1250 — graph capture、batch size capture、replay
  • python/sglang/srt/layers/attention/flashinfer_backend.py:5-6 — FlashInfer 更快,Triton 更易定制
  • python/sglang/srt/layers/attention/flashinfer_backend.py:51-58 — paged KV wrappers / fast_decode_plan
  • python/sglang/srt/layers/attention/flashinfer_backend.py:110, 203-219 — 全局 workspace 复用
  • python/sglang/srt/layers/attention/flashinfer_backend.py:1132-1144 — override indptr 以减少 host-to-device copy
  • docs/developer_guide/benchmark_and_profiling.md — benchmark 分层:server / scheduler / model runner / kernel
  • docs/developer_guide/bench_serving.md — 在线 serving 的 TTFT、ITL、throughput 观测口径