前置知识:从 Transformer 到 SGLang 关心的那几件事
谁该读这一篇? 完全没接触过 LLM 推理引擎、但希望快速跟上后续章节的工程师。 前置阅读: 无(这就是起点)。 耗时: 50 分钟 学完能: 1. 用自己的话讲清楚 token、KV cache、Prefill / Decode 两阶段、TTFT / TPOT 的物理含义; 2. 算出一个 7B / 70B 模型在 H100 上的 KV cache 上限和单步耗时量级; 3. 解释为什么"前缀缓存"是 SGLang 最大的卖点之一; 4. 看到一个 LLM 服务化的请求,能在脑子里画出"从 HTTP 进来 → token 出去"的全链路; 5. 后续读 RadixAttention 时不会被"radix tree、page、block、token slot"这些词卡住。
1. Token 是什么,为什么这是一切的起点
LLM 不读字符,也不读单词,读的是 token:一段被分词器(tokenizer)切出来的离散单元。 对中文,token 通常对应 1-2 个汉字;对英文,一个 token 平均 ~0.75 个单词。
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
print(tok.encode("你好,世界"))
# → [108386, 3837, 99489] # 3 个 token
print(tok.encode("Hello, world!"))
# → [9707, 11, 1879, 0] # 4 个 token
后续所有性能讨论都按 token 计:吞吐 = token/s,时延 = token 间隔(毫秒)。
2. 一个请求的两阶段:Prefill 和 Decode
[输入 prompt: 1024 token] ──Prefill──▶ KV cache (1024 个 token 的 K/V)
│
▼
采样 1 个新 token
│
─────Decode 循环─────
▲ │
│ 采样 1 个新 token
│ │
└──────┘
直到 EOS 或达到 max_tokens
- Prefill:把整段 prompt 一次喂给模型,算出每层每个 token 的 Q/K/V,把 K/V 存进 cache。一次完成。
- Decode:每次只算一个新 token 的 Q/K/V(拿前面所有 token 的 K/V 当 attention 的 K 和 V),把新算的 K/V 追加进 cache。多次循环。
两个阶段的计算模式截然不同:
| 维度 | Prefill | Decode |
|---|---|---|
| 一步喂进模型的 token 数 | N(prompt 长度) | 1 |
| 算子主要瓶颈 | 算力(matmul 大) | 显存带宽(每步只算 1 token,矩阵很瘦) |
| GPU 利用率 | 高 | 低(除非 batch 起来) |
| 影响的 SLO | TTFT(首 token 时延) | TPOT(token 间隔) |
这两个阶段为什么不能合一?因为它们的算力/带宽配比不同; 把 prefill 和 decode 混着调度 + 切分(chunked prefill / disaggregated)是 vLLM、SGLang 这类引擎共同的核心议题。后面
02-core-concepts/05-token-attention.md和05-distributed/04-disaggregated.md会展开。
3. TTFT vs TPOT:用户感知的两个时延
| 指标 | 含义 | 用户感知 |
|---|---|---|
| TTFT(Time To First Token) | 从请求到第一个 token 输出的时间 | "卡了多久才开始打字" |
| TPOT(Time Per Output Token) | 平均每个后续 token 的间隔 | "打字快不快" |
| E2E latency | TTFT + (output_len - 1) × TPOT | 整体响应时间 |
| Goodput | 满足 SLO 的请求 token/s | 容量规划的硬指标 |
业务对 chat 类需求通常是 TTFT < 500 ms、TPOT < 50 ms。
SGLang 的几乎所有优化都在调这两个数:
- RadixAttention → 降 TTFT(命中前缀就不用再 prefill)
- Cache-aware scheduling → 降 TTFT(让命中前缀的请求优先排队)
- 压缩 FSM → 降 TPOT(约束生成时 jump-ahead 多吐 token)
- Speculative decoding → 降 TPOT
4. KV Cache:必须存在的原因 & 算它有多大
朴素实现的灾难:每生成一个新 token,前面所有 token 的 K、V 都要重新计算一遍。 长 prompt + 长 output 的复杂度是 $O(N^2)$。
KV cache 把每层 attention 的 K 和 V 存下来,新 token 只算自己的 Q/K/V 然后拼接。 代价是显存 —— 这个显存有多大?
其中 $h_{\text{kv}}$ 是 KV head 数(GQA 模型下远小于 attention head 数),$d_{\text{head}}$ 是每个 head 的维度。
Llama-3-70B(H100,fp16)举例:
- 80 层,KV heads = 8,head_dim = 128,fp16 (2 bytes)
- 每 token 每层:$2 \times 8 \times 128 \times 2 = 4096$ bytes = 4 KiB
- 整模型每 token:$80 \times 4 \text{ KiB} = 320$ KiB
- 一张 80 GB H100 装完权重(140 GB → 必须 TP=2)后剩 ~20 GB 给 KV cache → 单卡 ~65k token 上限
- 一个 batch 装 64 路请求、每路 1k token = 64 × 320 KiB = 20 GB —— 恰好用满
这里有个关键认知:KV cache 是显存里最大的一块,几乎所有推理引擎的设计都围绕"怎么把 KV 装得更密、怎么复用更多"展开。
5. Prefix Cache:SGLang 真正的特色
业务场景:一个 chat agent 经常有相同的 system prompt(几百到几千 token); RAG 系统的同一份检索结果会喂给不同问题;多轮对话的历史也是天然共享前缀。
朴素做法:每个请求自己存自己的 KV,前缀 token 也算 N 遍 prefill。 Prefix cache:把已经算过的 KV 存住,新请求来的时候做"最长前缀匹配",命中就跳过对应的 prefill。
请求 A: "system: 你是助手 ... user: 1+1=?" (prompt 1500 token)
请求 B: "system: 你是助手 ... user: 2+2=?" (prompt 1500 token)
└────共享 1490 token────┘└── 各自 10 ──┘
朴素: A 跑 1500 prefill,B 跑 1500 prefill = 3000 token prefill
prefix cache:A 跑 1500 prefill,B 命中 1490 后只跑 10 = 1510 token prefill
命中率高的服务下,prefix cache 能把 TTFT 降一个数量级。
关键工程问题:
- 怎么组织这些"已经算过的 KV"?vLLM 用 hash 表(block 级),SGLang 用 radix tree(token 级)。
- 显存满了怎么淘汰?LRU?引用计数?
- 命中之后怎么排队?让命中的请求优先 → SGLang 的 cache-aware scheduling。
这三个问题就是 02-core-concepts/ 整一章的内容。
6. Continuous Batching:和静态 batch 的区别
朴素静态 batch:N 个请求一起发,所有请求做完才能进下一批。最慢的一个拖死整批。 连续批处理(continuous batching):每"一步"(iteration)都重新决定这一步喂哪些请求 —— 谁结束就让谁走,新进来的可以加入。
静态 batch(4 路):
step 1: A B C D ──┐
step 2: A B C D │ D 在 step 3 已完成但要等到全部完成
step 3: A B C D ──┘
step 4: A B ── ── C 在 step 5 完成
step 5: A ── ── B 在 step 6 完成
...
连续批:
step 1: A B C D
step 2: A B C D D 完成 → 出去
step 3: A B C E E 新加入
step 4: A B E C 完成 → 出去
...
GPU 利用率显著提高。vLLM 把这个做成 v0 的核心招牌;SGLang 在这个基础上更进一步加了 cache-aware 排序。
想看这个机制的最早论文:Orca (OSDI '22);在 vllm-learning 里有 vllm-learning/02-core-concepts/02-continuous-batching.md 详细展开,本书不重复。
7. 三种 Parallelism:TP / PP / DP(一句话各自)
| 并行 | 切什么 | 通信代价 | 典型场景 |
|---|---|---|---|
| TP(Tensor Parallel) | 一层内部的矩阵切多卡,AllReduce 拼回来 | 高(每层 1-2 次 AllReduce) | 单模型放不下单卡(70B 必须 TP≥2) |
| PP(Pipeline Parallel) | 把不同层放到不同卡 | 中(点对点 send/recv) | 模型超大、跨节点 |
| DP(Data Parallel) | 多副本,每副本独立 | 低(只在 router 层) | 单卡能装下,要扩 QPS |
SGLang 还有 EP(Expert Parallel,MoE 专家切分)和 CP(Context Parallel,长上下文切分),后续 05-distributed/ 展开。
8. 一个请求在 SGLang 内部的旅程(鸟瞰)
sequenceDiagram
participant U as 客户端
participant H as HTTP Server
participant T as TokenizerManager
participant S as Scheduler
participant W as ModelWorker (GPU)
participant D as DetokenizerManager
U->>H: POST /v1/chat/completions
H->>T: 转发请求
T->>T: tokenize prompt
T->>S: GenerateReqInput (token ids)
S->>S: match_prefix → 命中 1490 token
S->>S: 加入 waiting queue,按 cache-aware policy 排序
S->>W: ScheduleBatch (prefill + 已 decode 的请求合并)
W->>W: forward(prefill) → 新 KV 写入 radix tree
W->>W: sampler → 1 个新 token
W->>S: BatchTokenIDOut
S->>D: 转发 token id
D->>D: 增量 detokenize → 文本片段
D->>T: 文本片段
T->>H: SSE chunk
H->>U: data: {"choices": [...]}
Note over S,W: decode 阶段重复 N 次直到 EOS
这就是后面所有源码章节的"主干"。卡住的话回到这张图。
9. 你需要熟悉的几个名词(卡片速查)
| 名词 | 一句话 |
|---|---|
| Forward pass | 模型从输入算到输出的一次前向 |
| Logits | 最后一层输出的、对每个 vocab token 的未归一化分数 |
| Sampling | 把 logits 变成下一个 token(greedy / top-k / top-p / temperature) |
| Page / Block | KV cache 的连续单元,常见 16 / 32 / 64 token 一块 |
| Page table / Block table | 每个请求一张"逻辑 KV → 物理 page"的映射表 |
| Radix tree | 把所有缓存的前缀按"公共前缀分裂"组织成的多叉树(SGLang 用) |
| Hash table / 块级前缀 | 把前缀按 block 切,每块算 hash 存 dict(vLLM 用) |
| Continuous batching | 每一步重新决定本步喂哪些请求 |
| Cache-aware scheduling | 调度时把"前缀命中长度"作为优先级权重 |
| CUDA Graph | 把一串 GPU 操作录下来一次提交,省 launch 开销 |
| FlashAttention / FlashInfer | 高效 attention kernel,SGLang 默认走 FlashInfer |
| Constrained decoding | 强制输出符合 JSON Schema / regex / grammar |
| Compressed FSM | SGLang 的快速受约束解码方案 |
| DSL(Domain-Specific Language) | SGLang 自己的前端语言,写 gen / select / fork |
10. 自检
写下答案再看后文:
- 一个 7B 模型在 fp16、H100 单卡上跑,prompt = 2k、output = 512、batch = 32,KV cache 占多少显存?还能装多少 batch?
答案
Llama-2-7B 参数:32 层、num_kv_heads=32、head_dim=128(无 GQA)。 每 token 每层 KV:`2 × 32 × 128 × 2 = 16 KiB`;32 层 = 512 KiB/token。 单请求总 token = 2048 + 512 = 2560;KV = `2560 × 512 KiB ≈ 1.25 GB`。 batch=32 → `40 GB` KV。 H100 80 GB 装完权重 14 GB 后剩 ~66 GB,扣掉 workspace(~5-10 GB)剩 ~55 GB。 所以这个配置已经接近上限,再加 batch(如 64)会触发抢占。GQA 模型(如 Llama-3,KV head=8)能装 4× 更多。- 为什么 prefill 阶段瓶颈是算力,decode 是带宽?画一下 Q × K^T 的形状变化你就懂了。
答案
Prefill:Q shape = `[N, d]`(N 个新 token),K^T = `[d, N]`,QK^T = `[N, N]` —— 是个**大方阵**,算力(FLOPs)∝ N²。GEMM 充分利用 Tensor Core,瓶颈在算力。 Decode:Q shape = `[1, d]`,K^T = `[d, L]`(L 是历史长度),QK^T = `[1, L]` —— 是个**瘦矩阵**,FLOPs 极小,但每步必须把整段 K/V 从 HBM 读出来。瓶颈在显存带宽。 这就是为什么 continuous batching 在 decode 阶段收益最大:把多请求的 Q 拼成 `[B, d]` 与同一份 K/V 共算,攒一次 K/V 读多次用。- 同一个 system prompt(3000 token)被 100 个用户复用,prefix cache 能省多少 prefill 算力?
答案
无缓存:每用户 prefill 3000 token + user_query(设 100 token)≈ 3100 token,100 次 ≈ 310k token prefill。 有缓存:第一次完整 prefill 3100 token;之后 99 次只 prefill 100 token user_query → 共 `3100 + 99×100 = 13000` token。 节省 ≈ `(310000 - 13000) / 310000 ≈ 95.8%` 的 prefill 算力。 实际 TTFT 改善还要看长度平方关系(Attn 是 O(N²)):`(100/3100)² ≈ 0.1%` 的 prefill **时延**,TTFT 降到原来的 ~3%。- 静态 batch 和 continuous batching 在 GPU 利用率上的差异,主要因为什么?
答案
静态 batch:N 路请求生成长度不一,**最慢的拖死全 batch**。后期只剩 1-2 路在 decode,其它 slot 空转。平均利用率 = ∑生成长度 / (N × 最长生成长度),常 < 50%。 Continuous batching:每"一步"(iteration)重新决定本步喂哪些请求,谁完成谁出去、谁新进来谁加入。slot 不会空转。平均利用率可到 85-95%。 核心使能:**iteration 级调度** + **paged/radix KV**(让请求随便加进 / 出 batch 而不破坏内存布局)。- 你能在 5 秒钟里画出 §8 那张时序图吗?
答案
关键节点(5 秒能画出来的核心 6 步): Client → HTTP → **TM**(tokenize)→ **Scheduler**(match_prefix + 队列)→ **Worker**(forward)→ **Scheduler**(出 token id)→ **Detokenizer**(增量解码)→ **TM**(SSE chunk)→ Client。 决策点:进 Scheduler 后是否命中 RadixCache?是否触发 prefill 还是直接 decode?显存满了是否要 evict? 后续所有源码章节都是这条主干的展开。11. 下一步
- 继续打基础 →
01-overview/01-what-is-sglang.md(SGLang 是什么、和 vLLM 不同的几个点) - 直接进核心 →
02-core-concepts/01-radix-attention.md - 对比 vLLM 视角学 → 先看
vllm-learning/01-overview/00-prerequisites.md复习相同基础,再回来读 §5 的 SGLang 特色对比。 - 动手 →
07-hands-on/01-setup.md,把 SGLang 装起来 + 起 server,对照本章的概念看 metrics。
上游源码起点:
sglang/python/sglang/launch_server.py