Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions 大模型教程/06-模型推理优化/02-KVCache.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ $$\text{KV Cache} = 2 \times L \times B \times n \times d \times \text{sizeof(dt

以 LLaMA-70B 为例($L=80$,$d=8192$,$n=4096$,$B=1$,FP16):

$$2 \times 80 \times 1 \times 4096 \times 8192 \times 2 \text{ bytes} = 10.7 \text{ GB}$$
$$2 \times 80 \times 1 \times 4096 \times 8192 \times 2 \text{ bytes} = 10{,}737{,}418{,}240 \text{ bytes} \approx 10.7 \text{ GB}$$

**单个请求的 KV Cache 就占用 10.7GB**!这是 LLM 推理显存紧张的主要原因。

Expand Down Expand Up @@ -281,7 +281,7 @@ GQA 在质量和效率之间取得了很好的平衡,被 LLaMA-2、Mistral 等

动态分配会导致内存碎片:

```
```text
|--KV1--| |--KV2--| |--KV3--|
^空隙^ ^空隙^
```
Expand Down
16 changes: 8 additions & 8 deletions 大模型教程/06-模型推理优化/03-解码策略.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ graph TD

**贪婪解码**(Greedy Decoding)每一步选择概率最高的 token:

$$x_{t+1} = \arg\max_{x} P(x | x_{1:t})$$
$$x_{t+1} = \arg\max_{x} P(x \mid x_{1:t})$$

其中 $x_{1:t}$ 表示已生成的前 $t$ 个 token,$P(x | x_{1:t})$ 是模型输出的下一个 token 的条件概率分布。$\arg\max$ 表示选择概率最高的那个 token。
其中 $x_{1:t}$ 表示已生成的前 $t$ 个 token,$P(x \mid x_{1:t})$ 是模型输出的下一个 token 的条件概率分布。$\arg\max$ 表示选择概率最高的那个 token。

### 特点

Expand All @@ -41,7 +41,7 @@ $$x_{t+1} = \arg\max_{x} P(x | x_{1:t})$$

贪婪解码有个致命弱点——它倾向于生成重复内容。这就像一个只会走「最短路」的人,一旦走进了死胡同,就会反复在同一个圈里转:

```
```text
The cat sat on the mat. The cat sat on the mat. The cat sat on the mat...
```

Expand Down Expand Up @@ -72,11 +72,11 @@ The cat sat on the mat. The cat sat on the mat. The cat sat on the mat...

为了比较不同长度的序列,通常用**长度归一化**的对数概率:

$$\text{score}(x_{1:t}) = \frac{1}{t^\alpha} \sum_{i=1}^t \log P(x_i | x_{1:i-1})$$
$$\text{score}(x_{1:t}) = \frac{1}{t^\alpha} \sum_{i=1}^t \log P(x_i \mid x_{1:i-1})$$

其中:
- $x_{1:t}$ 为候选序列,长度为 $t$
- $\log P(x_i | x_{1:i-1})$ 为每个 token 的对数概率,累加得到序列对数概率
- $\log P(x_i \mid x_{1:i-1})$ 为每个 token 的对数概率,累加得到序列对数概率
- $\alpha \in [0.6, 1.0]$ 为长度惩罚参数:$\alpha = 0$ 表示不做长度归一化,$\alpha = 1$ 表示完全归一化

背后的含义是:若不做长度归一化,Beam Search 会偏好短序列(短序列累积对数概率更大),除以 $t^\alpha$ 正是为了抵消这种偏差。
Expand Down Expand Up @@ -267,12 +267,12 @@ $$\text{Speedup} \approx \frac{1}{1 - \alpha}$$

**MTP**(Multi-Token Prediction)模型训练时预测未来多个 token:

$$\mathcal{L} = -\sum_{t=1}^T \sum_{k=1}^K \log P(x_{t+k} | x_{1:t})$$
$$\mathcal{L} = -\sum_{t=1}^T \sum_{k=1}^K \log P(x_{t+k} \mid x_{1:t})$$

其中:
- $T$ 为训练序列总长度
- $K$ 为同时预测的未来 token 数(如 $K = 4$)
- $P(x_{t+k} | x_{1:t})$ 为在位置 $t$ 预测未来第 $k$ 个 token 的概率
- $P(x_{t+k} \mid x_{1:t})$ 为在位置 $t$ 预测未来第 $k$ 个 token 的概率

用大白话讲,与标准语言模型只预测下一个 token($K=1$)不同,MTP 要求模型同时对未来 $K$ 个位置都做出准确预测,从而在推理时一步生成多个 token。

Expand All @@ -299,7 +299,7 @@ MTP 解码可以视为**自投机**:模型自己既是 draft 也是 target。

### 决策树

```
```text
需要确定性输出?
├── 是 → 贪婪解码或 Beam Search
└── 否 → 需要多样性?
Expand Down
12 changes: 6 additions & 6 deletions 大模型教程/06-模型推理优化/04-PagedAttention.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

### 碎片示例

```
```text
时刻 T1: |---Req1(1024)---|---Req2(512)---|---Req3(1024)---|---Free---|
时刻 T2: |---Req1(1024)---|-----Free-----|---Req3(1024)---|---Free---|
^Req2 结束
Expand Down Expand Up @@ -76,13 +76,13 @@ $$\text{Block size} = B \times H \times d_k \times 2 \text{ (K 和 V)}$$
- $d_k$ 为每个头的维度
- 因子 2 表示 K 和 V 各占一份

典型的 $B = 16$。例如 LLaMA-7B($H=32$,$d_k=128$,FP16),每块占用 $16 \times 32 \times 128 \times 2 \times 2 = 256$ KB。
典型的 $B = 16$。例如 LLaMA-7B($H=32$,$d_k=128$,FP16),每块占用 $16 \times 32 \times 128 \times 2 \times 2 = 262{,}144 \text{ bytes} = 256$ KB。

### 地址映射

每个请求维护一个**块表**(block table):

```
```text
请求 1 的块表: [物理块 7, 物理块 2, 物理块 15, ...]
请求 2 的块表: [物理块 3, 物理块 9, ...]
```
Expand Down Expand Up @@ -164,7 +164,7 @@ Beam Search 或并行采样需要从同一前缀生成多个分支。传统方
2. 当某个分支写入已共享的块时,才真正复制该块

示例:
```
```text
原请求: [块1, 块2, 块3]
分支后:
请求A: [块1, 块2, 块3] (共享)
Expand Down Expand Up @@ -198,7 +198,7 @@ CoW 使 Beam Search 的显存开销从 $O(k \cdot n)$ 降为 $O(n + k \cdot \Del
其中:
- $k$ 为 Beam 宽度(分支数)
- $n$ 为共享前缀的序列长度
- $\Delta$ 为每个分支新增的 token 数
- $\Delta$ 为每个分支在共享前缀之后新增的平均 token 数

从实际意义来看,共享前缀只存一份,只有真正“分叉”的新增块才需复制,显存节省显著。

Expand All @@ -222,7 +222,7 @@ CoW 使 Beam Search 的显存开销从 $O(k \cdot n)$ 降为 $O(n + k \cdot \Del
2. 若缓存命中,新请求直接引用已有的块
3. 新请求只需分配后续 token 的块

```
```text
前缀 "You are a helpful assistant..." 的块: [块10, 块11, 块12]

请求 A: [块10, 块11, 块12, 块20, 块21] (共享前缀,独有后缀)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ graph TD
**等待浪费**:批内请求的输出长度参差不齐,短请求先结束却必须等最长的请求完成——如同团建活动里跑最快的人必须在终点等最慢的人到齐才能开始下一环。

示例:
```
```text
请求 1: 输出 50 tokens,耗时 5s
请求 2: 输出 500 tokens,耗时 50s
请求 3: 输出 100 tokens,耗时 10s

静态批处理:必须等 50s,请求 1、3 的 GPU 利用率极低
```

**Padding 浪费**:长度对齐需要 padding,填充部分做无用计算
**Padding 浪费**:长度对齐需要 padding,填充部分产生无效的注意力计算开销

**延迟尖峰**:批次边界造成等待,新请求的延迟取决于批内最慢的请求。

Expand Down Expand Up @@ -77,7 +77,7 @@ $$\text{利用率} = \frac{\text{平均长度}}{\text{最大长度}} = \frac{(L_

### 调度流程

```
```text
迭代 1: [Req1, Req2, Req3] → 生成 token → Req1 结束
迭代 2: [Req2, Req3, Req4] → 加入新请求 Req4,生成 token
迭代 3: [Req2, Req3, Req4] → 生成 token → Req3 结束
Expand Down Expand Up @@ -123,7 +123,7 @@ Continuous Batching 需要处理两类请求:

结合 Chunked Prefill,可以将长 Prefill 分块,与 Decode 请求混合:

```
```text
迭代 1: [Decode: Req1, Req2] + [Prefill Chunk: Req3_part1]
迭代 2: [Decode: Req1, Req2] + [Prefill Chunk: Req3_part2]
迭代 3: [Decode: Req1, Req2, Req3] → Req3 Prefill 完成,加入 Decode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ graph LR

传统方法需要为每个序列预分配最大长度的连续KV Cache空间,导致显存碎片化。PagedAttention将KV Cache划分为固定大小的块(如16个token),通过块表(Block Table)管理非连续的物理内存:

```
```text
逻辑视图:[Token 0-15] [Token 16-31] [Token 32-47] ...
↓ ↓ ↓
物理块: Block 7 Block 2 Block 15 ...
Expand Down Expand Up @@ -255,16 +255,16 @@ def batch_qa(s, questions):
answers = []
for q in questions:
s += sgl.user(q)
s += sgl.assistant(sgl.gen(f"answer", max_tokens=100))
answers.append(s[f"answer"])
s += sgl.assistant(sgl.gen("answer", max_tokens=100))
answers.append(s["answer"])

return answers
```

## 性能对比

| 特性 | vLLM | SGLang |
|-----|------|--------|
|------|------|--------|
| KV Cache管理 | PagedAttention | RadixAttention |
| 前缀缓存 | 支持 | 原生优化 |
| 结构化输出 | 基础支持 | 原生支持 |
Expand Down Expand Up @@ -297,7 +297,7 @@ total = model_memory + kv_cache_per_seq * max_concurrent_seqs
- `num_params`:模型参数量(例如 7B 即 $7 \times 10^9$);乘以 2 是因为 FP16 每个参数占 2 字节。
- `kv_cache_per_token`:每个 token 的 KV Cache 大小;其中“$2$”分别对应 Key 和 Value 两个缓存,`num_layers` 为 Transformer 层数,`hidden_size` 为隐藏维度,末尾的“$\times 2$”表示 FP16 每个元素 2 字节。
- `kv_cache_per_seq`:单条序列的 KV Cache 总量,等于每 token 缓存量乘以最大序列长度 `max_seq_len`。
- `total`:总显存需求 = 模型权重 + 所有并发序列的 KV Cache 之和。该估算用于判断单卡可承载的最大并发数,即 $\text{max\_seqs} = (\text{GPU\_mem} - \text{model\_memory}) / \text{kv\_cache\_per\_seq}$。
- `total`:总显存需求 = 模型权重 + 所有并发序列的 KV Cache 之和。该估算用于判断单卡可承载的最大并发数,即 $\text{max\\_seqs} = (\text{GPU\\_mem} - \text{model\\_memory}) / \text{kv\\_cache\\_per\\_seq}$。

### 并发配置

Expand Down
Loading