flowchart TD
subgraph TokenizerLayer["TokenizerManager (多进程)"]
Tok["并行 Tokenizer 编解码"]
end
subgraph SchedulerLayer["SRT Scheduler"]
Batch["ScheduleBatch 动态合批容器"]
Radix["Radix Tree 索引空间"]
end
subgraph ModelLayer["ModelRunner (TP / CUDA Graph)"]
Graph["CUDA Graph 捕获执行图"]
Kernels["FlashInfer / PagedAttention 算子"]
end
Tok --> SchedulerLayer --> ModelLayer
第 3 课:SGLang SRT 源码地图:Scheduler、ScheduleBatch 与 ModelRunner
参考答案版:包含完整代码实现与问答题深度参考解析。建议先独立完成练习题后再展开核对。
本课目标与通过标准
本课产出:能沿 SGLang SRT 的真实 event loop 追踪请求从 waiting queue 到 prefill/decode batch、TP worker、ModelRunner 和结果处理,并与 vLLM V1 的职责切分做源码级比较。
通过要求:唯一代码填空题通过全部断言;Q1~Q3 都能沿源码对象给出因果链;能指出一个正确性不变量、一个性能边界和一个需要 benchmark 才能确认的结论;总分至少 8/10。
源码版本与阅读方法
本课程使用固定 commit 的永久链接保证行号可复现,同时链接 current docs 供核对最新变化。阅读时先画调用图和状态所有权,再进入分支;不要从大文件第一行机械顺读。
建议阅读顺序:
| 顺序 | 源码入口 | 关注对象 |
|---|---|---|
| 1 | python/sglang/srt/managers/scheduler.py::run_event_loop |
normal/overlap 两条主循环如何选择 |
| 2 | get_next_batch_to_run、get_new_batch_prefill、update_running_batch |
waiting、chunked request、running batch 怎样迁移 |
| 3 | managers/schedule_batch.py::Req、ScheduleBatch、NextBatchPlan |
请求状态与 GPU batch tensor 的边界 |
| 4 | managers/tp_worker.py::TpModelWorker.forward_batch_generation |
scheduler 如何调用模型 worker |
| 5 | model_executor/model_runner.py::forward、_forward_raw、sample |
attention backend、CUDA graph、模型与 sampler 在哪里接合 |
源码快照:SGLang 9deb6952afa4。
核心心智模型
1. 它是什么,解决什么问题
SGLang SRT(SGLang Runtime)是由加州大学伯克利分校团队开发的高性能大语言模型推理引擎。与纯通用批处理推理系统不同,SGLang 最初专为复杂的结构化生成、多轮对话与智能体(Agent)复杂逻辑树探索而生。 在复杂大模型程序(如 ReAct Agent、Tree-of-Thought、自我纠错长思维链)中,系统面临严重的复杂前缀重复计算与小批量自回归的 CPU 驱动瓶颈:
- 传统系统仅能缓存固定的 System Prompt,无法高效捕捉在会话中途分叉、并发探索的多路径前缀;
- 在单步 Decode 过程中,PyTorch Eager 模式下的 Python 循环与 Kernel 发射延迟高达数百微秒,严重压制 GPU 算力利用率。
SGLang SRT 提出了以 RadixAttention(基数树动态前缀感知)与 多尺寸 CUDA Graph 捕获分发引擎为核心的运行时体系(图 3-1 与图 3-2)。它解决的核心工程问题是:通过树状结构全自动追踪任何历史 Token 前缀,并利用高效的分层合批容器(ScheduleBatch)与 CUDA Graph,将多轮交互与复杂 Agent 工作流的 TTFT 与推理吞吐推向极致。
2. 它如何工作
SGLang SRT 的分层协作与批处理推进时序如图 3-1 与图 3-2 所示:
图 3-1:SGLang SRT 运行时分层交互与 ScheduleBatch 容器拓扑
flowchart LR
Req["接收批次请求"] --> RadixMatch["Radix Tree 快速最长前缀检索"]
RadixMatch --> FormBatch["构造混合批次 (Prefill + Decode ScheduleBatch)"]
FormBatch --> GraphExec{"命中捕获尺寸的 CUDA Graph?"}
GraphExec -->|是| FastRun["重放 CUDA Graph (超低调度开销)"]
GraphExec -->|否| EagerRun["回退 Eager 动态形状前向执行"]
FastRun & EagerRun --> Sample["多项式采样与树节点更新"]
图 3-2:SGLang SRT 混合批处理与 CUDA Graph 捕获分发时序流程
- 三大核心抽象分工解耦(图 3-1):
Req(单请求持久业务实体):拥有单请求完整的跨步语义状态,包括输入/输出 Token 列表、采样参数、停止条件,以及在 Radix Tree 中的当前挂载节点;ScheduleBatch(调度器层批量控制视图):由Scheduler在 CPU 侧负责编排维护的动态批次容器。负责请求优先级排序、Prefill 与 Decode 区分聚合,以及向物理内存池请求分配 KV Slot;ForwardBatch(设备端执行描述符):由TpModelWorker/ModelRunner消费的 GPU 物理张量集合。包含打平的 Input Tokens、序列长度偏移数组seq_lens、PagedAttention 物理查找表指针,以及与 CUDA Graph 对齐的批次元数据。
- 两阶段批处理决策(Prefill 优先与 Decode 轮转,图 3-2): 在调度器核心函数
get_next_batch_to_run()中,状态机遵循严谨的转移次序:- 第一步:合并已就绪的 Prefill:上一个 step 中刚刚执行完前向 Prefill 的请求,在当前轮首先被合并进入活跃的
running_batch集合; - 第二步:新 Prefill 准入判定:若 WAITING 队列中存在可接纳的新请求,利用 Radix Tree 检索最长公共前缀后,优先构造
prefill批次; - 第三步:Decode 批次推进:若无新 Prefill,则将存活的
running_batch请求打包为decode批次下发执行。
- 第一步:合并已就绪的 Prefill:上一个 step 中刚刚执行完前向 Prefill 的请求,在当前轮首先被合并进入活跃的
- CUDA Graph 捕获与回放(图 3-2): ModelRunner 在启动阶段预先针对一系列典型的批次大小(如 Batch Size = 1, 2, 4, 8, 16, 32, 64, 128)录制捕获了静态的 CUDA Graph。当本轮
ForwardBatch的请求数恰好命中捕获尺寸时,直接重放(Replay)整张图,彻底绕过 PyTorch 的 CPU 解释与底层驱动启动开销;若尺寸未命中,则平滑回退至 Eager 动态前向模式。
3. 正确性条件与常见误区
禁止跨层合并三大对象(
Req/ScheduleBatch/ForwardBatch): 初学者常试图将三者合为一个大对象。这一做法是致命的:Req是长周期 CPU 业务对象,而ForwardBatch包含了大量与具体 step 强绑定的物理 GPU Tensor。若将其混为一谈,会导致庞大的中间激活与张量随长请求一直驻留显存引发 OOM,且彻底破坏了 Overlap 异步调度流水线的隔离性。异步 Overlap 调度中的写后读(WAR)屏障: 在 SGLang 的 Overlap 模式中,CPU 主线程在准备当前轮 Batch 元数据时,GPU 正在并发读取上一个轮次的输入 Buffer。系统必须通过 Stream Barrier(CUDA Event / 内存栅障)保证 GPU 完全完成读取后,CPU 才能向共享内存复写新的输入张量,杜绝发生脏读与内存撕裂。
Radix Tree 引用计数与修剪互斥不变量: 只要某个树节点对应的 KV 块正被某个活跃
Req读取,其引用计数必须严格大于 0。LRU 驱逐算法只能修剪引用计数归零的闲置叶子节点,严禁误删活跃节点。
4. 性能与工程取舍
CUDA Graph Padding 开销 vs CPU 发射延迟: 为了提高 CUDA Graph 命中率,系统通常需要把不成整数倍的请求向上对齐填充(Padding)到最近的静态尺寸桶。Padding 带来了一定程度的无用计算,但消除了数十微秒的 Python 解释开销,在小 Batch Decode 场景下利远大于弊。
Prefill 优先策略对 TTFT 与 ITL 的博弈:
get_next_batch_to_run默认优先调度新到达的 Prefill 请求,这极大优化了首字到达时间(TTFT);但代价是正在生成的 Decode 请求会被暂时挂起一轮,导致字间生成延迟(ITL)产生短暂尖峰。因此,SGLang 在长序列场景下引入了 Chunked Prefill 与动态优先级调节来平抑这种抖动。
具体演示:NextBatchPlan 批次选择决策演算
考虑调度循环中的一个经典决策时刻:
- 当前运行队列:
running = ["d0"](包含 1 个正在进行自回归的请求); - 上一轮刚刚完成 Prefill 计算的请求:
completed_prefill = ["p_done"]; - 等待队列中新到达、且通过 Radix 匹配准备执行 Prefill 的请求:
new_prefill = ["p_new"]。
调度决策推导:
合并上一轮成果:
running.extend(completed_prefill) \implies running = ["d0", "p_done"]。此时持久运行集合已经原子扩充为 2 个请求;判定运行批次:
- 检测到
new_prefill非空,根据“新 Prefill 优先以压低首字时延”策略,本次调度计划选出执行batch_to_run = ("prefill", ["p_new"]); - 最终返回计划字典:
{"batch_to_run": ("prefill", ["p_new"]), "running_batch": ["d0", "p_done"]}。
- 检测到
后继演进: 在下一个 step 中,若没有更新的 Prefill 进入,系统将执行
batch_to_run = ("decode", ["d0", "p_done"]),正式对两路请求并发进行自回归解码。状态机转移清晰无歧义。
实践任务:唯一代码填空题
补齐 NextBatchPlan 的教学版选择逻辑:先合并上轮完成的 prefill,再优先运行新 prefill,否则运行 decode。
只能修改 TODO/______ 位置,不得删除断言或放宽通过条件。
Code
def choose_sglang_batch(running, completed_prefill, new_prefill):
running = list(running)
# TODO:完成 prefill 的请求进入 running;新 prefill 优先于 decode。
______
if new_prefill:
batch_to_run = ("prefill", list(new_prefill))
elif running:
batch_to_run = ("decode", list(running))
else:
batch_to_run = None
return {"batch_to_run": batch_to_run, "running_batch": running}
assert choose_sglang_batch(["d0"], ["p_done"], ["p_new"]) == {
"batch_to_run": ("prefill", ["p_new"]),
"running_batch": ["d0", "p_done"],
}
assert choose_sglang_batch(["d0"], [], []) == {
"batch_to_run": ("decode", ["d0"]),
"running_batch": ["d0"],
}检查方法
运行断言;补一个三者皆空的用例。再说明真实源码在哪些条件下不会简单优先新 prefill。
Q1
Req、ScheduleBatch、ForwardBatch 三者为何不能合并成一个类?
你的答案:
Q2
为什么 get_next_batch_to_run 返回 plan 时还要返回更新后的 running_batch?
你的答案:
Q3
如何从源码判断性能问题在 scheduler CPU、batch preparation 还是 ModelRunner/kernel?
你的答案:
评分规则
- 代码 4 分:主路径 2 分、边界条件 1 分、能映射回源码对象 1 分;
- Q1~Q3 各 2 分:必须包含对象、状态变化、正确性或成本链;
- 一票否决:把论文峰值写成普遍生产结论;把源码支持写成所有模型/硬件可用;混淆算法正确性与性能;只背类名而说不清状态所有权。