乐于分享
好东西不私藏

SGLang 源码解析:一次请求如何经过 RadixAttention、连续批处理与推测解码

SGLang 源码解析:一次请求如何经过 RadixAttention、连续批处理与推测解码

源码版本:sgl-project/sglang,本地提交 6b2c730bf793984c39f7f07b3c074ca05b059b00(2026-06-22)源码路径:D:\ai\source-code\sglang阅读目标:不是记住所有参数,而是建立一张从 API 请求到 GPU 执行、KV Cache、调度与分布式部署的完整地图。

很多人第一次接触 SGLang,会把它理解成“另一个能启动大模型的 OpenAI API 服务”。

这个理解没有错,但只看到了最外层。

真正决定推理服务性能的,不是接口长得像不像 OpenAI,而是下面这些问题:

  • 多个请求共享一段 System Prompt 时,已经计算过的 KV Cache 能不能复用?
  • 一个长 Prompt 进入以后,会不会把后面的短请求全部堵住?
  • Prefill 和 Decode 的计算特征不同,为什么还要放在同一批 GPU 上?
  • JSON Schema 是靠 Prompt“请求模型遵守”,还是在每一步采样时真正限制 token?
  • GPU 显存不足时,哪些 KV 可以淘汰,哪些正在使用、不能动?
  • 多个模型实例之间,路由器能不能把请求送回最可能命中缓存的 Worker?

SGLang 解决的正是这一层问题。

我更愿意把它理解成:

SGLang 是大模型推理请求的运行时与资源调度系统。模型负责计算下一个 token,SGLang 负责让大量请求更高效、更稳定地完成这件事。

下面不按目录逐个介绍模块,而是跟着一条请求走完整条链路。


一、先看全景:SGLang 不只是一个 Python Server

当前仓库至少可以分成五块:

部分
主要目录
解决的问题
SGLang Runtime
python/sglang/srt/
请求接入、分词、调度、KV Cache、模型执行和流式输出
SGL 前端语言
python/sglang/lang/
用生成、选择、并行等原语描述复杂 LLM 程序
内核库
sgl-kernel/
为注意力、量化、MoE 等路径提供 CUDA/C++ 优化算子
Model Gateway
sgl-model-gateway/
多 Worker 注册、路由、限流、重试、熔断、PD 协调和可观测性
工程体系
benchmark/
test/docker/docs/
压测、正确性、镜像、部署和文档

此外,仓库中还有 python/sglang/multimodal_gen/,用于图像、视频等生成模型。本文为了把一条主线讲清楚,重点讨论自回归语言模型的 SRT,不展开 Diffusion Runtime。

一条普通请求的大致路径如下:

客户端  ↓OpenAI 兼容 API / 原生 /generate  ↓TokenizerManager:校验、模板、分词、请求状态  ↓Scheduler:排队、前缀匹配、组批、显存预算  ↓TpModelWorker  ↓ModelRunner:模型前向、Attention Backend、CUDA Graph  ↓Sampler / Grammar:采样或受约束采样  ↓Scheduler:更新请求、缓存 KV、安排下一轮  ↓Detokenizer / TokenizerManager  ↓SSE 流式响应

大型部署还会在客户端与 Worker 之间增加 Model Gateway:

客户端  ↓SGLang Model Gateway  ├─ 普通 Worker  ├─ Prefill Worker  ├─ Decode Worker  └─ 其他模型或远程 OpenAI 兼容后端

这两张图先记住。后面的 RadixAttention、连续批处理、推测解码和 PD 分离,都只是这条主链上的不同优化。


二、第一站:HTTP Server 不负责推理,只负责接住请求

原生生成接口位于:

python/sglang/srt/entrypoints/http_server.py

generate_request() 处理 /generate 请求。流式请求会返回 StreamingResponse,不断输出 SSE 数据;非流式请求则等待最终结果。

这里有一个很重要的工程边界:

HTTP 层不直接调用模型,而是把请求交给 TokenizerManager。

OpenAI 兼容的 /v1/chat/completions/v1/responses 等入口,会先完成协议转换、Chat Template、Reasoning 与 Tool Call 等适配,最后也进入内部生成请求。

这样做的好处是,协议层可以变化,下面的调度和模型执行主链保持稳定。

TokenizerManager 做了什么

关键入口:

python/sglang/srt/managers/tokenizer_manager.pyTokenizerManager.generate_request()

从源码看,它主要完成:

  1. 规范化单请求与批请求参数;
  2. 生成并维护请求 ID 对应的状态;
  3. 校验 LoRA、输入长度和模型参数;
  4. 把文本转成 token IDs;
  5. 将 tokenized request 发给 Scheduler;
  6. 等待 Scheduler 返回 token;
  7. 处理客户端断开和请求取消;
  8. 将增量结果交还给 HTTP 层。

所以 TokenizerManager 不是一个简单的 tokenizer.encode() 包装器。它还是 API 世界和推理运行时之间的状态边界。

如果请求在进入 Scheduler 前就失败,代码会清理已经创建的请求状态,避免长期服务出现状态泄漏。这个细节不影响单次 Demo,却会影响一个服务能不能持续运行。


三、第二站:Scheduler 才是整个 Runtime 的心脏

Scheduler 的主循环位于:

python/sglang/srt/managers/scheduler.pyScheduler.event_loop_normal()Scheduler.event_loop_overlap()

普通调度循环可以压缩成五步:

接收新请求→ 更新等待队列→ 选择下一批请求→ 执行模型→ 处理结果并进入下一轮

对应的关键方法是:

process_input_requests()get_next_batch_to_run()run_batch()process_batch_result()

这看起来像一个普通事件循环,但困难在于:每个请求的位置都不同。

  • 有的请求刚到,需要处理几千个 Prompt token;
  • 有的请求已经进入 Decode,每轮只需要生成一个或少量 token;
  • 有的请求命中了几千个前缀 token;
  • 有的请求带 JSON Schema,需要先等待 Grammar 编译;
  • 有的请求显存不够,需要延迟、分块甚至回退;
  • 有的请求正在 PD 模式下等待 KV 从 Prefill 节点传到 Decode 节点。

Scheduler 的任务不是“把请求放进一个 batch”,而是在延迟、吞吐、显存和公平性之间不断重新组批。

Prefill 和 Decode 为什么必须区分

大模型推理分成两个明显不同的阶段。

Prefill

  • 一次处理整段输入;
  • 并行度高;
  • 计算量大;
  • 决定首 token 延迟的重要部分;
  • 会为输入 token 写入 KV Cache。

Decode

  • 自回归地逐步生成;
  • 单步计算量相对小;
  • 每一步都需要读取已有 KV;
  • 更容易受显存带宽和调度开销影响;
  • 决定持续输出速度。

如果把二者不加区分地排队,一个超长 Prefill 很容易让正在 Decode 的请求等待,用户就会感觉输出“卡住”。

连续批处理不是“一次凑齐一批”

离线训练中的 batch 通常开始和结束都整齐。

在线推理不是这样。请求随时到达,输入长短不同,输出也不知道什么时候结束。

SGLang 的 Scheduler 会在每一轮:

  • 移除已经完成的请求;
  • 保留仍需 Decode 的请求;
  • 从 waiting queue 中加入新请求;
  • 根据 token 和显存预算决定本轮规模;
  • 将 Prefill、Decode 或混合请求交给模型执行。

这就是 continuous batching 的关键:批次是动态变化的,不要求所有请求同时开始、同时结束。

Chunked Prefill 解决什么问题

如果一个请求带着 30K token 的长上下文进入,全部一次性 Prefill,可能占满本轮 token 预算。

Chunked Prefill 会把长输入拆成多段,让 Scheduler 有机会在段与段之间插入其他工作。它主要解决:

  • 长 Prompt 独占调度;
  • Prefill 峰值显存过高;
  • Decode 请求被长时间阻塞;
  • 单次 batch 很难装下超长输入。

代价是调度、缓存和边界管理更复杂。radix_cache.py 中甚至专门区分 unfinished request,保证分块过程中已经计算的 KV 可以插入并继续复用。


四、第三站:RadixAttention 复用的不是文字,而是可证明相同的计算前缀

这是 SGLang 最有辨识度的设计之一。

假设三个请求都使用同一段很长的 System Prompt:

请求 A = 系统提示词 + 用户问题 A请求 B = 系统提示词 + 用户问题 B请求 C = 系统提示词 + 用户问题 C

如果每次都重新计算系统提示词,GPU 会重复做大量相同的 Prefill。

模型在处理每个 token 时,会为后续 Attention 保存 Key 和 Value。只要前面的 token 序列和相关计算条件完全一致,对应 KV 就可以复用。

SGLang 用 Radix Tree 管理这些前缀:

root└─ 公共 System Prompt   ├─ 用户问题 A   ├─ 用户问题 B   └─ 用户问题 C

树的边不是原始字符串,而是 token ID 序列;节点的 value 指向 KV Cache 在显存池中的位置。

核心代码位于:

python/sglang/srt/mem_cache/radix_cache.pyRadixKeyRadixCache.match_prefix()RadixCache.insert()RadixCache.cache_unfinished_req()RadixCache.cache_finished_req()

1. 请求先找最长前缀

Req.init_next_round_input() 位于:

python/sglang/srt/managers/schedule_batch.py

它把当前完整 token 序列构造成 RadixKey,调用 tree_cache.match_prefix()

返回结果中最关键的是:

  • device_indices:已命中 KV 在 GPU 缓存池中的索引;
  • last_device_node:命中的最后一个树节点;
  • Host / Storage 命中信息:启用 HiCache 时使用;
  • cache_protected_len:当前请求正在保护的缓存长度。

2. 真正送进模型的只有未命中后缀

在 ScheduleBatch.prepare_for_extend() 中,输入不是完整 token 序列,而是:

完整输入[len(prefix_indices):]

这行语义非常关键。

Radix Tree 本身不会让模型变快。真正减少计算的是:Scheduler 拿到已命中的 KV 索引后,只把没有缓存的后缀送去 Prefill。

3. 请求结束后,把新结果插回树

cache_finished_req() 会将:

原始输入 token + 已生成 token

在已经提交 KV 的长度内插入 Radix Tree。

如果其中一部分早已存在,重复分配的 KV 会释放;没有见过的新后缀成为新分支。

对于 Chunked Prefill,cache_unfinished_req() 会在请求未结束时先保存已经完成的部分,并更新请求持有的缓存索引。

4. 正在使用的前缀不能被淘汰

每个树节点有 lock_ref

当请求引用某个前缀时,inc_lock_ref() 会沿父节点向上增加引用;请求完成或切换节点后,dec_lock_ref() 再释放。

只有没有被引用的叶子节点才能进入可淘汰集合。显存不足时,evict() 按配置的淘汰策略从这些节点释放 KV。

这说明 RadixAttention 不只是一棵查找树,它还参与显存生命周期管理。


五、为什么“看起来一样的 Prompt”仍然可能没有命中

这是实际部署中比“Radix Tree 是什么”更重要的问题。

源码显示,缓存匹配至少受下面几类条件影响。

1. Token IDs 必须一致

用户看到的文字一样,不代表送入模型的 token 一样。

以下变化都会改变 token 序列:

  • Chat Template 版本变化;
  • System、User、Assistant 消息顺序变化;
  • 多一个空格、换行或特殊 token;
  • 在 Prompt 中动态加入时间戳、请求 ID;
  • RAG 文档排序每次变化;
  • 截断长度不同;
  • tokenizer 或模型版本不同。

所以生产系统应该比较最终 token IDs,而不是比较页面上显示的字符串。

2. extra_key 不同会强制隔离

RadixKey 不只有 token_ids,还有 extra_key

OpenAI Serving 层会把 cache_salt 与 extra_key 组合;请求使用 LoRA 时,lora_id 也会进入这个逻辑命名空间。

因此,即使 token IDs 完全相同,只要 extra_key 不同,缓存也不会共享。

这是正确性和隔离能力,不是命中率 Bug。不同 LoRA、不同租户或明确不应共享状态的请求,本来就应该分开。

3. 最多只匹配到输入长度减一

Req._compute_max_prefix_len() 会将最大前缀长度限制为 input_len - 1

原因是最后一个输入 token 仍需要参与前向计算,才能得到下一 token 的 logits。所谓“整段输入都缓存了”,也不能简单理解为这次完全不执行模型。

4. Page Size 会影响可复用边界

当 page_size > 1 时,RadixKey.page_aligned() 会把可匹配长度向下对齐到完整页。

例如页大小为 4,两个请求共享 6 个 token,真正作为树节点复用的长度可能只有前 4 个;不足一页的尾部需要重新处理。

这是分页 KV 管理的代价:减少细粒度管理开销,但命中边界不再精确到每一个 token。

5. 特殊输入会主动关闭前缀复用

Req.init_next_round_input() 对 positional_embed_overrides 做了明确处理:

相同 token IDs 配上不同的位置向量,不能共享同一份 KV。

当前 kv_cache_builder.py 还会在使用 Transformers Backend 的多模态模型上关闭 Radix Cache,以避免多模态前缀错配。

这再次说明:缓存命中必须以计算等价为前提,不能为了命中率牺牲正确性。

6. 请求可能被路由到另一台 Worker

单个 Radix Tree 只知道本 Worker 的缓存。

如果请求 A 在 Worker 1 上建立缓存,请求 B 被轮询到 Worker 2,即使前缀相同,也不会命中 Worker 1 的 GPU KV。

这就是 Model Gateway 提供 cache_aware 路由策略的原因之一:大型部署不仅要让 Worker 内部会复用,还要尽量把相同前缀送到合适的 Worker。

7. 缓存可能已经被淘汰

缓存命中过一次,不代表会永久存在。

高并发、长上下文或较小的 KV Pool 都可能触发淘汰。要判断是“没有识别出相同前缀”,还是“识别过但已经被驱逐”,必须结合缓存命中率、token 使用率、队列和请求时间线一起看。


六、PagedAttention、RadixAttention 和连续批处理不是同一个概念

这三个名词经常被混在一起。

机制
解决的问题
Paged KV / PagedAttention
KV 在显存里怎样按页分配,减少连续大块内存需求和碎片
RadixAttention
不同请求之间怎样识别并复用相同前缀的 KV
Continuous Batching
不同进度的请求怎样动态进入和退出批次

可以把它们类比为:

  • Paged KV 是仓库货架;
  • Radix Tree 是“相同货物放在哪里”的索引;
  • Scheduler 是决定每一辆车何时进仓、取什么货的调度员。

只有三者配合,才能同时处理显存、重复计算和在线请求调度。


七、缓存感知调度:有缓存,还要让调度顺序利用它

前缀缓存并不是被动的。

在:

python/sglang/srt/managers/schedule_policy.py

Scheduler 会为 waiting queue 中的请求计算前缀命中,并支持按最长前缀等策略排序。

源码中还有一段 in-batch prefix caching 逻辑:

  • 如果多个等待请求在已有全局缓存中命中很少;
  • 但这些请求彼此共享较长前缀;
  • Scheduler 会避免它们全部同时冷启动;
  • 先让其中一个请求建立缓存,再让后续请求复用。

这背后的原则可以迁移到其他系统:

缓存不仅改变存储,也应该影响调度。否则请求虽然理论上可复用,执行顺序却可能让它们同时重复计算。

当然,最长前缀优先也不是永远最好。它可能让短前缀请求等待更久,所以生产环境仍要观察尾延迟和公平性,而不是只追求缓存命中率。


八、结构化输出不是“再写一句请返回 JSON”

SGLang 的受约束生成入口位于:

python/sglang/srt/constrained/

核心协调者是:

GrammarManager

当请求带有以下约束之一时:

  • JSON Schema;
  • Regex;
  • EBNF;
  • Structural Tag;

GrammarManager.process_req_with_grammar() 会从 Grammar Cache 获取已编译对象,或者把新规则放入编译队列。规则准备好以后,请求才进入正常 waiting queue。

在 Decode 阶段,Grammar Backend 会根据当前状态计算允许的 token 集合,并对 logits 应用 vocabulary mask。非法 token 在采样前就被排除。

当前源码中可以看到多种后端:

  • XGrammar;
  • Outlines;
  • llguidance;
  • Reasoner Grammar;
  • none

这里需要区分两件事:

  1. Prompt 约束是在语义层请求模型遵守格式;
  2. Grammar 约束是在采样层限制模型只能生成合法 token。

后者能显著减少 JSON 语法错误,但它仍不能保证字段内容真实。

例如:

{"price"-999999,"currency""火星币"}

它可能完全符合 JSON Schema,却不符合业务规则。

因此生产链路仍需要:

Grammar 保证语法→ Schema 保证结构→ 业务校验保证语义→ 高风险动作进入人工确认

九、推测解码:不是换掉大模型,而是减少串行 Decode 次数

普通自回归 Decode 每次通常只确认一个新 token:

目标模型前向→ 采样一个 token→ 再次前向→ 再采样一个 token

推测解码引入一个更便宜的 Draft 路径:

Draft 一次提出多个候选 token→ Target Model 并行验证→ 接受正确前缀→ 从第一个不接受的位置继续

目标模型仍然负责最终正确性。加速来自:一次 Target Model 验证可能确认多个 token,从而减少串行轮数。

当前版本在:

python/sglang/srt/speculative/spec_info.py

注册了多种算法,包括:

  • DFLASH;
  • EAGLE;
  • EAGLE3;
  • FROZEN_KV_MTP;
  • STANDALONE;
  • NGRAM;
  • 自定义插件算法。

不同算法的 Draft 来源并不相同:

  • NGRAM 可以利用上下文中已经出现的 token 模式;
  • EAGLE 系列使用专门的 Draft 机制和隐藏状态;
  • STANDALONE 使用独立 Draft Model;
  • DFLASH 走自己的候选与验证路径。

推测解码不是免费加速。它会增加:

  • Draft 计算;
  • 候选管理;
  • KV Cache;
  • 验证和拒绝采样;
  • Scheduler 状态复杂度。

是否值得开启,取决于接受率、输出长度、模型、硬件和请求分布。接受率低时,Draft 做了很多工作,却没有减少多少 Target 步数。

正确做法不是看到“推测解码更快”就打开,而是同时观察:

  • 接受长度;
  • 接受率;
  • 输出 token/s;
  • TPOT;
  • 显存占用;
  • P95 / P99 延迟。

十、ModelRunner:真正把调度结果变成 GPU 计算

Scheduler 决定“这轮运行谁”,模型执行层负责“怎样运行”。

关键路径:

python/sglang/srt/managers/tp_worker.pyTpModelWorkerpython/sglang/srt/model_executor/model_runner.pyModelRunner

TpModelWorker 负责张量并行 Rank、模型配置、分词器、随机种子和 ModelRunner 初始化。

ModelRunner 则管理:

  • 模型加载;
  • KV Cache 内存池;
  • Attention Backend;
  • CUDA Graph;
  • 量化路径;
  • TP / PP / EP / DP Attention 等并行配置;
  • Prefill、Decode 和其他 Forward Mode;
  • Draft / Target 相关执行状态。

下层的 sgl-kernel/ 提供自定义计算原语。它不是独立服务,而是 Runtime 在关键路径上调用的内核库。

这解释了为什么 SGLang 的性能不能归因于某一个算法:

请求调度减少空转,RadixAttention 减少重复 Prefill,Paged KV 管理显存,CUDA Graph 降低 Launch 开销,专用 Kernel 加速具体算子,推测解码减少串行步骤。

最终性能来自整条链路,而不是单点魔法。


十一、PD 分离:为什么要把 Prefill 与 Decode 放到不同节点

小规模部署通常使用统一 Worker:

同一组 GPU→ 做 Prefill→ 做 Decode

规模扩大以后,两种阶段的资源特征不同:

  • Prefill 更偏计算密集;
  • Decode 更依赖 KV 读取、显存容量和带宽;
  • 长 Prompt 会制造大块 Prefill 工作;
  • Decode 需要稳定的小步迭代,直接影响用户看到的输出节奏。

PD Disaggregation 将它们拆开:

请求→ Prefill Worker 计算输入并产生 KV→ 传输 KV 与元数据→ Decode Worker 接管生成→ 流式返回结果

当前源码用三个模式表示:

NULL:统一模式PREFILL:只承担 PrefillDECODE:只承担 Decode

关键目录:

python/sglang/srt/disaggregation/

仓库中可以看到 NIXL、Mooncake、MoRI、Ascend 和公共传输层等实现。Model Gateway 负责在更外层协调 Prefill 与 Decode Worker,并把响应重新合并为客户端看到的一条流。

PD 分离的收益是可以分别扩缩容、分别选择硬件,并减少两个阶段互相干扰。

代价也很明确:

  • KV 传输需要网络和专门后端;
  • 请求生命周期跨进程、跨节点;
  • 故障恢复与超时更复杂;
  • Prefill / Decode 比例配置不当会形成新的排队点;
  • 小流量场景可能得不偿失。

所以 PD 不是“生产环境必须开启”,而是规模与负载已经证明统一部署存在资源错配以后再考虑。


十二、并行策略:不要把 TP、DP、EP、PP 当成同一个开关

SGLang 支持多种并行维度,它们分割的对象不同。

策略
主要分割对象
常见目的
TP,Tensor Parallel
单层矩阵与张量
单卡放不下模型,或需要多卡共同完成一次前向
PP,Pipeline Parallel
不同模型层
将模型层分布到多个阶段
DP,Data Parallel
请求或副本
增加并发处理能力
EP,Expert Parallel
MoE Experts
将不同专家分布到不同设备
CP,Context Parallel
长上下文计算
分摊超长上下文的注意力工作

这些策略可以组合,但组合越多,通信、路由和故障面越大。

选型顺序应该是:

  1. 单卡能否放下模型和目标 KV Cache;
  2. 单实例是否达到目标吞吐;
  3. 瓶颈在计算、显存、通信还是排队;
  4. 再决定增加哪个并行维度。

“GPU 多就把所有并行都打开”通常不会自动得到最优结果。


十三、Model Gateway:从一台推理机走向一个推理集群

sgl-model-gateway/ 是一个 Rust 为主的控制面与数据面。

它解决的不是单个模型 Forward,而是 Worker 集群外层的问题:

  • Worker 注册与健康检查;
  • 普通、PD、gRPC 与 OpenAI 兼容后端路由;
  • Random、Round Robin、Cache Aware、Power of Two 等策略;
  • 限流与排队;
  • 重试和指数退避;
  • Worker 级熔断;
  • 多模型路由;
  • OpenTelemetry Trace、Prometheus 与结构化日志;
  • Chat History 和 MCP 工具调用等上层能力。

可以把两层边界记成:

SRT:一组 GPU 内,一条请求怎样高效完成Gateway:多组 Worker 之间,请求应该去哪里以及失败后怎么办

只有单机单 Worker 时,不必为了“架构完整”先上 Gateway。出现多副本、PD 分离、多模型、跨 Worker 缓存命中或统一限流需求后,它才开始体现价值。


十四、怎样判断服务真的跑得好:先看四类指标

SGLang 可通过 --enable-metrics 暴露 Prometheus 指标。

本文建议先看四组,而不是一上来收集所有指标。

1. 用户体验

  • sglang:time_to_first_token_seconds:首 token 延迟;
  • sglang:time_per_output_token_seconds:持续生成速度;
  • sglang:e2e_request_latency_seconds:端到端延迟。

2. 调度压力

  • sglang:num_running_reqs
  • sglang:num_queue_reqs
  • 排队时间和超时数量。

3. 资源与吞吐

  • sglang:token_usage
  • sglang:gen_throughput
  • Prompt / Generation token 总量;
  • KV Pool 使用情况。

4. 缓存

  • sglang:cache_hit_rate

当前 metrics_reporter.py 中,命中率不是“命中请求数 / 总请求数”,而是按有效命中 token 与总输入相关 token 计算。

这意味着两个请求都叫“命中”时,复用了 10 个 token 和复用了 4,000 个 token,价值完全不同。

不要单独优化命中率

缓存命中率提高,可能只是因为请求被强制集中到少数 Worker,结果队列更长。

因此至少把这些指标放在一起:

cache_hit_rate+ TTFT+ TPOT+ queue length+ token usage+ P95 / P99 latency

命中率只是原因线索,不是最终业务目标。


十五、最小启动与验证

以下命令用于理解路径,具体模型、CUDA、量化和并行参数必须按硬件调整。

1. 启动服务

python -m sglang.launch_server \  --model-path <MODEL_PATH> \  --host 0.0.0.0 \  --port 30000 \  --enable-metrics

2. 发送 OpenAI 兼容请求

curl http://127.0.0.1:30000/v1/chat/completions \  -H "Content-Type: application/json" \  -d '{    "model": "<MODEL_PATH>",    "messages": [      {"role": "system", "content": "你是一个代码审查助手,只输出明确结论。"},      {"role": "user", "content": "检查这段代码的空指针风险。"}    ],    "temperature": 0  }'

3. 查看指标

curl http://127.0.0.1:30000/metrics

重点搜索:

sglang:cache_hit_ratesglang:time_to_first_token_secondssglang:time_per_output_token_secondssglang:num_queue_reqssglang:token_usage

4. 验证 RadixAttention

准备一批请求:

  • System Prompt 固定且足够长;
  • User Prompt 不同;
  • 模型、Chat Template 和采样设置保持一致;
  • 先预热,再正式记录;
  • 不在共享前缀里加入时间戳或随机 ID。

对照两组服务:

A:默认启用 Radix CacheB:启动时增加 --disable-radix-cache

比较:

  • 缓存命中 token;
  • TTFT;
  • Prompt throughput;
  • 端到端延迟;
  • token usage;
  • P95 / P99,而不只看平均值。

仓库已经提供共享前缀压测入口:

benchmark/bench_in_batch_prefix/bench_in_batch_prefix.py

这个脚本会构造多组公共前缀和不同后缀,比较分批、提前建立前缀以及一次发送等场景。

本文没有填写一组“看起来很快”的性能数字,因为当前写作环境没有安装 SGLang 所需的 Torch/GPU 运行依赖。硬件、模型、Prompt 分布和并发度都会显著改变结果;不能复现的数字不应该冒充结论。


十六、什么时候值得用 SGLang

更适合:

  • 需要自托管大模型或多模态模型;
  • 有较高并发和明确延迟目标;
  • 大量请求共享 System Prompt、Few-shot、RAG 模板或多轮历史;
  • 需要 JSON Schema、Regex 等受约束输出;
  • 需要推测解码、量化、多 LoRA 或多种并行策略;
  • 需要从单机扩展到多 Worker、PD 分离或大规模 MoE;
  • 团队愿意维护 GPU、驱动、监控和压测体系。

不一定适合:

  • 业务只调用云端模型 API;
  • 请求量很低,单进程方案已经够用;
  • 团队没有 GPU 运维能力;
  • 只是本地跑一个小模型;
  • 还没有稳定业务请求,却先设计大型 PD 集群;
  • 只因为榜单数据选择引擎,没有用自己的负载做对照。

SGLang 降低的是推理执行成本,不会替你解决:

  • 模型本身不会的知识;
  • RAG 数据质量;
  • Prompt 业务逻辑;
  • 权限与产品边界;
  • 线上验收和用户价值。

十七、读源码的正确顺序

SGLang 代码量很大,直接从 scheduler.py 第一行开始很容易迷失。

建议按一条请求的生命周期阅读:

顺序
文件
先回答的问题
1
python/sglang/srt/entrypoints/http_server.py
请求怎样进入,流式响应怎样返回?
2
python/sglang/srt/managers/tokenizer_manager.py
文本怎样变成内部请求?
3
python/sglang/srt/managers/scheduler.py
请求如何排队、组批、执行和更新?
4
python/sglang/srt/managers/schedule_batch.py
单请求和一个 Batch 保存哪些状态?
5
python/sglang/srt/mem_cache/radix_cache.py
前缀怎样匹配、插入、锁定和淘汰?
6
python/sglang/srt/managers/tp_worker.py
Scheduler 怎样调用模型 Worker?
7
python/sglang/srt/model_executor/model_runner.py
模型、内存池、Backend 和 CUDA Graph 怎样组织?
8
python/sglang/srt/constrained/
结构化输出怎样进入采样过程?
9
python/sglang/srt/speculative/
Draft 与 Target 怎样协作?
10
python/sglang/srt/disaggregation/
Prefill 与 Decode 怎样跨节点交接?
11
sgl-kernel/
哪些关键算子下沉到 CUDA/C++?
12
sgl-model-gateway/
多 Worker 如何路由、限流和容错?

每读一个模块,都只问四件事:

  1. 输入是什么?
  2. 输出是什么?
  3. 它修改了哪些状态?
  4. 失败时由谁恢复?

这样读完以后,你得到的不是一张目录表,而是一套能迁移到其他推理引擎的工程框架。


最后总结

如果只记住 SGLang 的一句话,不要记“它支持很多模型和很多优化”。

记住这条主链:

协议层接住请求→ TokenizerManager 建立内部状态→ Scheduler 匹配前缀并动态组批→ RadixAttention 复用可证明相同的 KV→ ModelRunner 与 Kernel 完成 GPU 计算→ Grammar 或 Sampler 决定合法的下一个 token→ 结果回到 Scheduler 继续下一轮→ Gateway 在集群层选择 Worker、限流与容错

RadixAttention 解决重复 Prefill,Continuous Batching 解决在线请求组批,Paged KV 解决显存管理,Structured Output 解决采样合法性,Speculative Decoding 试图减少串行 Decode 步数,PD Disaggregation 解决大规模部署中的资源错配。

这些能力并不是彼此独立的功能标签。它们共同围绕一个目标:

在有限 GPU、变化请求和明确正确性边界下,让更多请求以可观察、可控制的方式完成推理。

这才是 SGLang 这个项目真正值得读懂的地方。


参考

  • SGLang 源码:https://github.com/sgl-project/sglang
  • SGLang 论文:https://arxiv.org/abs/2312.07104
  • 官方文档:https://docs.sglang.io/
  • 本文核对版本:6b2c730bf793984c39f7f07b3c074ca05b059b00