vllm教程-02-核心原理

仓库地址:https://github.com/hhk-png/cycle-agent

02 核心原理

本章目标:深入拆解 vLLM 高性能背后的每一个关键机制。每个概念都按 为什么(motivation)→ 怎么做(mechanism)→ 具体例子 展开。


1. Transformer 推理基础:prefill 与 decode

为什么

LLM 生成文本本质是自回归(autoregressive):一次只预测下一个 token,把新 token 拼回输入,再预测下一个。这个过程天然分成了两个计算特征截然不同的阶段。

怎么做

  • Prefill(预填充):把整个输入 prompt 一次性喂给模型,并行计算出每个位置的注意力,并生成第一个输出 token。此时输入很长,是**计算密集(compute-bound)**的,能用 GPU 矩阵乘吃满算力。
  • Decode(解码):每步只输入"上一个 token",串行生成下一个 token,直到遇到结束符或达到最大长度。此时每个 token 的矩阵运算量很小,而权重 + KV cache 要从显存/内存搬到计算单元,所以是**内存带宽密集(memory-bandwidth-bound)**的。

例子

一个 8B 参数模型,权重 fp16 约 16GB。decode 每生成 1 个 token 至少要读完这 16GB 权重(假设 batch=1),即使计算量很小。A100 40GB 显存带宽约 1.5 TB/s,那么单 token 延迟下限 ≈ 16GB / 1.5TB/s ≈ 10ms 。这解释了为什么 decode 慢------瓶颈在"搬数据",不在"算"

关键洞察:既然 decode 受内存带宽限制,那么 batch 越大,每个 token 摊到的带宽成本越低,吞吐量(tokens/s)越高。这就是 continuous batching 的理论基础。

1.1 decode 为什么是带宽受限:定量推导

把上面的 16GB 例子做完整:设一个 8B 模型,fp16 权重 ≈ 16GB ;单卡显存带宽 B(A100 40GB ≈ 1.5 TB/s,H100 ≈ 3.35 TB/s)。decode 每步都要把整套权重从显存搬到计算单元------这是每步绕不开的"固定成本"。

复制代码
每步必须搬运 ≈ 16GB(权重)+ 本批新增的 KV

8B 模型每 token 的 KV 很小:2 × 32层 × 8 KV头 × 128 dim × 2B = 128 KB
(相比 16GB 权重,可忽略,见 §10)

batch = 1:每步搬 16GB,只产出 1 个 token
   单 token 延迟 ≈ 16GB / 1.5TB/s ≈ 10.7ms
   吞吐 ≈ 1 / 10.7ms ≈ 93 tokens/s

batch = 32:每步仍搬 16GB 权重(KV 增量 32×128KB ≈ 4MB,可忽略),产出 32 个 token
   单步耗时 ≈ 16GB / 1.5TB/s ≈ 10.7ms
   吞吐 ≈ 32 / 10.7ms ≈ 3000 tokens/s   ← 提高约 32 倍
   每个 token 摊到的带宽 = 16GB / 32 = 0.5GB → 平均每 token 延迟 ≈ 0.33ms

关键结论:

  1. 带宽是每步的"固定成本":batch 再大,权重也要完整读一遍;
  2. 吞吐 ≈ batch × (带宽 / 权重大小):batch 越大,摊销越薄,吞吐近似线性上升;
  3. 天花板 :这个线性增长不会无限持续------batch 大到一定程度后,要么算力 成为瓶颈(矩阵乘已满),要么 KV 显存不够(第 17 章附录 B 的显存墙)。

这就是为什么 vLLM 拼命增大有效 batch(continuous batching)减小每 token 搬运量(量化、KV 量化) 、以及一次多产 token(投机解码)------三招都是同一个公式在起作用。


2. KV Cache

为什么

注意力计算需要 query、key、value 三者。生成第 t 个 token 时,需要与前面所有位置的 K、V 做注意力。若每步都重算前面所有 token 的 K、V,成本随序列长度平方级 增长,完全不可行。所以要把每个位置算出的 K 和 V 缓存起来,decode 时只算新 token 的 K、V 并追加到缓存。

怎么做

KV cache 是张量,形状大致为:

复制代码
(num_layers, num_kv_heads, seq_len, head_dim)
  • num_layers:模型层数(每层一套 K/V);
  • num_kv_heads:KV 头数(MQA/GQA 下小于 Q 头数,8B 模型常为 8);
  • seq_len:序列长度(动态增长);
  • head_dim:每个头的维度(Llama 系列常为 128)。

显存膨胀公式:每个 token 的 KV 占用为

复制代码
每token字节数 = 2(K 和 V)× num_layers × num_kv_heads × head_dim × 每个元素字节数
             = 2 × num_layers × d_model × bytes_per_element   (当 num_kv_heads×head_dim = d_model)

fp16 下,Llama-70B(80 层,d_model=8192)每个 token 约 2.5 MB;一个 2048-token 的序列就要约 5 GB 显存。批量并发时,KV cache 往往是显存的最大消耗者,甚至超过模型权重。

KV cache 量化 :上面的 bytes_per_element 默认 fp16=2 字节。vLLM 支持 --kv-cache-dtype fp8(E4M3/E5M2)把 KV 压到 1 字节,每个 token 的 KV 显存直接减半,同样显存下并发近似翻倍------这是"省显存"与"省带宽"(decode 带宽受限)双收的关键旋钮,机制详见第 08 章 §2 与第 09 章 §9.3.9。

例子

text 复制代码
模型:Llama-70B,L=80 层,hidden=8192,fp16
每 token:2 × 80 × 8192 × 2B = 2,621,440 B ≈ 2.5 MB
一条 4096-token 的对话:≈ 10 GB
并发 64 条这样的对话:≈ 640 GB >> 单卡 80GB

这就是必须精细管理 KV cache 的原因,也直接引出 PagedAttention。


3. PagedAttention(分页注意力)

为什么

传统 serving 为每个请求预留连续的、按最大序列长度分配的内存块。序列长度动态变化导致:

  • 预留空间用不满 → 内部碎片
  • 不同长度请求交错 → 外部碎片,即使总量够也找不到连续块。

论文数据显示,这种方案 KV 利用率只有 20%--40%。

怎么做

借鉴操作系统虚拟内存分页

  1. 把 KV cache 逻辑上切成固定大小的块(block) ,默认 block_size = 16(16 个 token 一块);
  2. 每个块在物理显存里不要求连续 ,通过 block table(块表) 维护"逻辑块 → 物理块"的映射;
  3. 分配/释放按块进行,碎片只可能出现在最后一个块,浪费被压到最小;
  4. 块可以共享------多个序列指向同一物理块(前缀复用、并行采样),这是传统连续分配做不到的。

代码上这就是 vllm/attention/ 中 PagedAttention 内核做的事,调度层(vllm/engine/llm_engine.pyvllm/core/scheduler.py)负责块表的增删查改。

版本锚点(V0 vs V1) :上述 vllm/engine/llm_engine.pyvllm/core/scheduler.pyV0 引擎 的源码路径;vLLM 1.0 起 V0 已被移除,V1 引擎把这些职责迁到 vllm/v1/ 下(vllm/v1/core/sched/scheduler.pyvllm/v1/engine/ 等)。本文的源码坐标默认是"经典 V0 路径 + V1 等价物对照"------机制本身没有变,变的只是模块归属与工程实现(详见第 03 章 §1.1 与第 12 章 §10.1)。

例子(OS 分页类比)

复制代码
传统方式:请求 A 申请 100 页的连续内存,只用 20 页 → 80 页空转(内部碎片)
PagedAttention:请求 A 需要 20 个 token → 分配 2 个 block(16+4)
              物理块散落各处,block table 记录:
              logical_block[0] -> physical_block[7]
              logical_block[1] -> physical_block[3]
              释放时只释放已用块,其余块立刻可被请求 B 复用

收益 :KV 利用率从 ~30% 提升到接近 90%+,同样显存能服务数倍并发,吞吐随之提升。

3.1 块表的完整数值算例

把上面的 OS 类比换成一个能动手验算 的完整例子。设 block_size = 16(vLLM 默认),一条序列已经长到 50 个 token

text 复制代码
需要的逻辑块数 = ceil(50 / 16) = 4

逻辑块    对应 token 范围    物理块    说明
─────────────────────────────────────────────────────
block 0     tokens 0--15    phys[7]   满块(16/16)
block 1     tokens 16--31   phys[3]   满块(16/16)
block 2     tokens 32--47   phys[19]  满块(16/16)
block 3     tokens 48--49   phys[5]   残块(2/16)★

块表(block table)就是这条序列的 [7, 3, 19, 5]------它把逻辑位置 (第几个 16-token 段)翻译成物理块 id 。调度器只认这张表,attention 内核用 logical_pos // 16 找块、logical_pos % 16 找块内偏移。

内部碎片(internal fragmentation) :只有最后一个块填不满------50 - 3×16 = 2 个槽位被用掉,剩余 14 个槽位空置

text 复制代码
碎片 = 1 个残块 × (16 - 2) = 14 个空槽
浪费比例 = 14 / 64 ≈ 21.9%

内存利用率

text 复制代码
利用率 = 有用的槽位 / 已分配的槽位
       = 50 / (4 × 16)
       = 50 / 64
       ≈ 78.1%

(注意不是 50/48------50 个 token 至少需要 4 个块、即 64 个槽位;"3 个块"只装得下 48 个 token。)

为什么说分页"消灭了碎片"? 对比 naive 方案:naive 按 max_len 连续预留,假设 max_len = 2048

text 复制代码
naive 利用率 = 50 / 2048 ≈ 2.4%
分页 利用率 = 50 / 64     ≈ 78%

碎片被限制在最后一个块(最多 15/16 的空槽),其余块永远满装。50 个 token 到底,分页只浪费 14 个槽位,而 naive 浪费 1998 个槽位。这就是"同样显存能服务数倍并发"的数学来源。

补充:上面的 num_kv_heads 决定一个块里每个 slot 的字节数,进而决定一块能"装下多少上下文"------GQA/MLA 这类架构变体正是从这里影响 KV 显存的。想深入算每一块的具体字节数,见第 19 章《模型架构基础》与第 17 章附录 B。


4. Continuous Batching(连续批处理)

为什么

静态批处理(static batching)下,一批请求要"同生共死":要么整批都结束,要么新请求要等当前整批跑完才能进。批内请求长短不一,短的跑完了还得等长的 → GPU 出现空闲,吞吐低、尾延迟高。

怎么做

Continuous batching(又称 iteration-level scheduling,迭代级调度):在**每个解码步骤(iteration)**都检查批次:

  • 某个请求生成了结束符 / 达到 max_tokens → 立即移出批次;
  • 新到达的请求 → 立即做 prefill 并插入当前批次继续 decode;
  • 因此批次是一个动态集合,每步都在变化,GPU 始终满载。

在 vLLM 里,这个调度逻辑由 Scheduler 执行,每步通过 schedule() 决定本步要跑哪些请求、分配哪些 KV 块。

例子

复制代码
step 1: batch = {A}            # A 是 prefill
step 2: batch = {A, B}         # B 到达,插入
step 3: batch = {A, B}         # A 生成了 <eos>,即将离开
step 4: batch = {B, C}         # A 离开,C 到达
step 5: batch = {B, C, D}      # D 到达

每步最多一个新请求的 prefill 开销与现有 decode 请求交错,GPU 利用率持续很高。

对比:request-level scheduling(请求级)以"整个请求"为单位调度,对应静态批处理;vLLM 是 iteration-level,单位是"一个 token 步"。

4.1 多步调度(Multi-Step Scheduling):减少 CPU 开销的"批处理升级"

连续批处理里,每一步(iteration)都要做一次 CPU 端调度 + 一次 GPU kernel 启动 。当 decode 阶段每个 token 的 GPU 计算极短(几微秒到几十微秒)时,"CPU 决定跑什么 → 启动 kernel"的固定开销会占掉一大部分时间------这就是调度墙(scheduling wall)

Multi-step scheduling(--num-scheduler-steps N 让调度器一次决定未来 N 步 :第 1 步跑完,第 2 步直接复用同一个批次和 kernel 启动序列,中间不再回 CPU 做 schedule()

  • 收益 :CPU 调度次数与 kernel 启动次数降到原来的 1/N,GPU 利用率与吞吐明显提升;V1 引擎里它是默认开启的核心吞吐机制之一。
  • 代价 :批次在 N 步内不再变动 ------新请求要等这批 N 步跑完才能插入;N 越大,批次的"新鲜度"越差(吞吐换响应性)。这正是 V1 调度器把 max_num_batched_tokens 做成动态预算的原因之一:既想吃多步的吞吐红利,又想尽量快地吸收新请求。
  • 与正确性的关系 :多步调度是乐观调度 ------它会在一步里给序列"预分配" N 个 token 的输出预算,若某序列在中间步提前触发 stop,这 N 步里的空 slot 就浪费了(不会产出错误的 token,只是少算)。这也是 §7.4"调度不影响正确性"里唯一的例外形态:浪费而非错误

多步调度与 CUDA Graphs(第 08 章 §3)是"一对好搭档":多步把"调度次数"压下来,CUDA Graphs 把"kernel 启动开销"压下来,两者叠加后单 token 的 CPU 开销可以忽略不计。


5. Chunked Prefill(分块预填充)

为什么

decode 是内存密集、prefill 是计算密集。当一个很长的 prompt(比如 10k token)进来时,它的 prefill 计算量大,会占用 GPU 数百毫秒,期间所有正在 decode 的请求都被阻塞,尾延迟暴涨。长 prompt 是 serving 场景最常见的延迟杀手。

怎么做

Chunked prefill :把一个长 prompt 的 prefill 拆成多个 chunk(例如每个 chunk 512 token),在调度循环里与 decode 请求交错执行:跑一小段 prefill,切出去跑几轮 decode,再回来继续 prefill。这样:

  • 长 prompt 不会一次性占满 GPU;
  • decode 请求的延迟不会被单次 prefill 拖垮;
  • 代价:prefill 整体完成时间略有增加(因为被切碎)。

版本差异 :V0 中 chunked prefill 默认关闭 ,用 --enable-chunked-prefill 显式开启;V1 中恒开启 (该 flag 在 V1 已移除),--max-num-batched-tokens 也变为动态预算。教程正文以 V1 行为为主线,V0 只作对照。

例子

复制代码
不启用:prefill(10000 tokens) 一次性占 GPU 500ms,期间所有 decode 冻结
启用后(chunk=512):
  step 1: prefill chunk 0 (512 tok)
  step 2: decode A, B
  step 3: decode A, B
  step 4: prefill chunk 1 (512 tok)
  ...
  prefill 总时长略长,但 decode 每次最多等 1 个 chunk 的时间

6. Prefix Caching(前缀缓存)

为什么

真实场景大量请求共享公共前缀:固定的 system prompt、RAG 注入的相同文档、多轮对话的历史上下文。这些前缀的 KV 每来一个新请求都重算一遍,既浪费算力又占显存。既然前缀相同,生成的 KV 也相同,为什么不算第二次?

怎么做

Prefix caching:把每个 KV block 的哈希(由内容 + 位置计算)记录在**哈希表(hash table)**中。新请求做 prefill 时,逐块查哈希表:

  • 命中 → 直接**共享(引用)**已有的物理块,跳过计算,也跳过新分配;
  • 未命中 → 计算并从该位置重新开始匹配。

块级共享正是 PagedAttention 的块表能轻松支持的。用法:

bash 复制代码
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-8B-Instruct \
    --enable-prefix-caching

在代码里由 PrefixCachingBlockAllocatorvllm/core/block/prefix_caching_block.py)管理。

例子

text 复制代码
请求1: "你是一个中文助手。用户说:帮我总结文档A"  → 前缀 KV 已缓存
请求2: "你是一个中文助手。用户说:帮我总结文档B"
        → "你是一个中文助手。" 的 KV 块直接命中缓存,只重算后半段
      命中部分 prefill 时间 ≈ 0

对 RAG、agent、多轮对话这类场景,命中率可以到 50%+,吞吐提升非常可观。


7. Preemption(抢占):KV 满时怎么办

为什么

并发请求多、显存有限时,KV cache 会耗尽 。此时来了新请求,要么拒绝,要么想办法腾出空间。vLLM 的策略是抢占:暂停某些请求,腾出 KV 块给新请求。

怎么做

vLLM 有两种抢占模式:

  • RECOMPUTE(重计算,默认) :把被抢占请求占用的 KV 块释放 ,等以后有空间了重新 prefill 该请求(从保存的 prompt 从头重算)。代价是重算的算力开销,但不需要交换到 CPU。
  • SWAP(换出) :把被抢占请求的 KV 块拷贝到 CPU 内存,空间够了再拷回 GPU。代价是 PCIe 拷贝带宽,适合"显存小但 CPU 内存充足"的机器。

选择依据:重计算吃 GPU 算力,换出吃 PCIe 带宽;一般显存紧张 + 带宽好 → SWAP,否则 → RECOMPUTE。调度器在 vllm/core/scheduler.py 中决定抢占顺序(一般按最老或优先级最低的请求先被抢占)。

例子

text 复制代码
显存耗尽,新请求 D 到达
调度器选择 RECOMPUTE:把请求 B 的 KV 块释放,D 进入批次
D 结束后,调度器把 B 的 prompt 重新 prefill,B 继续 decode
用户感知:B 的延迟变长(因为被抢占了),但不会出错

8. Speculative Decoding(投机解码)

为什么

decode 是串行的、内存带宽密集的,每步只产出一个 token。能不能一次多猜几个 token,让目标大模型一次验证多个,从而摊薄带宽成本?投机解码就是干这个的。

怎么做

  1. 用一个更小更快的 draft model 自回归地快速生成 K 个候选 token(K 通常 4--8);
  2. 目标大模型对这 K 个 token 并行做一次 forward(只算一遍,像 prefill),得到每位的概率;
  3. 从第 0 位开始逐位比对:draft 预测对了就接受,第一个不一致的位置之后全部拒绝,并用目标模型在该位置的采样结果作为真实 token(称为 bonus token);
  4. 因为并行验证 K 个 token 的算力成本 ≈ 验证 1 个,所以只要 draft 命中率高,解码就加速。

关键性质:无损(lossless)------最终输出分布与不用投机解码完全一致,只是更快。投机在 GPU 空闲(如 batch 小、带宽有余)时效果最好。

8.1 为什么无损:修正拒绝采样(modified rejection sampling)

"输出分布不变"不是拍脑袋的断言,而是由修正拒绝采样 这一数学机制保证的。设目标模型(大)对某个位置的分布为 p,draft 模型(小)的分布为 q,它们都从同一个候选 token 集合里选。验证算法逐位进行:

复制代码
对 draft 提出的候选 token x(其 draft 概率为 q(x)):
  计算目标模型对 x 的概率 p(x)

  接受规则:
    以概率 min(1, p(x) / q(x))  接受 x   ------ 接受后直接作为本步输出
    否则(以概率 1 − min(1, p/q))拒绝:
       拒绝后,按 "p 扣除已接受部分" 的归一化分布
         max(0, p(x') − q(x')) / Σ_z max(0, p(z) − q(z))
        重新采样一个 token x'(称为 bonus token)

为什么这样能保证无损?把"接受 draft token x"的概率算出来:

复制代码
P(输出 = x) = 接受路径:q(x) × min(1, p(x)/q(x))
            = 拒绝路径:在位置 x 被拒后、bonus 恰好采到 x

逐项展开可得 P(输出 = x) = p(x)------也就是说,无论 draft 模型猜得有多差,最终每个 token 的边际分布都和直接采样目标模型完全一致(证明细节见投机解码原论文)。这正是"投机解码只改速度、不改分布"的数学根源:

  • 接受率高 → 每个目标前向产出多个 token → 加速;
  • 接受率趋近 0 → 退化为"每步重采样 bonus token",等价于普通采样,不会更差

工程要点:验证时必须用同一套种子/随机源 做采样,且 draft 与 target 的 logits 都要完整(不能只给 top-1),否则修正采样无法正确计算 p(x)/q(x)。mini-vLLM 的 minivllm/speculative.py 演示了同一套 draft→verify→accept/reject 流程(接受率输出见第 06 章 §6.12)。

例子

text 复制代码
draft 模型猜: "I", "love", "vLLM", "because"   (K=4)
目标模型并行验证这 4 个位置:
  位置0 接受 "I",位置1 接受 "love",位置2 拒绝(目标要的是 "LLM")
→ 实际接受 2 个 token + 1 个 bonus token(位置2的目标token)= 本步产出 3 个 token
   而只做了 1 次大模型 forward → 理论加速 ~3x

相关家族 :vLLM 原生支持多种 draft 方法(--speculative-config),除标准 draft 模型外还有 EAGLE (用特征层做自回归预测,效果更强)和 Medusa(在模型头并联多个解码头)等。SGLang 也有类似支持,但 vLLM 生态最全。


9. 量化(Quantization)

为什么

模型权重是 fp16(2 字节/参数),8B 模型就 16GB,70B 模型 140GB。显存放不下权重,就放不下 KV cache。且 decode 受带宽限制,权重越小,每 token 搬的字节越少,越快。量化就是把权重(以及激活)用更少的 bit 表示。

怎么做

  • Weight-only 量化(只量化权重) :典型如 GPTQAWQ ,把权重降到 4bit(配合 group size,如 128 个权重共享一个 scale),激活保持 fp16。8B 模型权重从 16GB → ~4GB,几乎无损(用校准数据找到让输出误差最小的量化方案)。
  • Weight + Activation 量化(权重和激活都量化) :典型如 FP8 W8A8,权重和激活都是 8bit 浮点。激活量化的好处是计算也能用低精度加速,但需要硬件支持(H100 等)且校准要求更高。

vLLM 用法(GPTQ/AWQ 模型会自动识别;也可显式指定):

bash 复制代码
# 加载已量化的模型,或指定量化方式
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2.5-7B-Instruct-AWQ \
    --quantization awq

注:早期教程常用 TheBloke/* 仓库的量化模型,但该组织已于 2024 年底下架全部仓库,示例统一改用 Qwen 官方量化版或 casperhansen/* 等仍在维护的镜像。vLLM 实际执行 int4 GEMM 依赖 Marlin 系 kernelgptq_marlin / awq_marlin,group size 需为 128 等),只有格式匹配才走快速内核,否则回退通用 kernel 明显变慢------落地坑详见第 09 章 §9.7。

例子

text 复制代码
Llama-2-7B,fp16:14GB 权重
  → AWQ/GPTQ 4bit:~4GB 权重(-70%)
  → 省下的显存给 KV cache,可并发请求数大幅上升
  → decode 每 token 搬运字节减少 → 吞吐提升
FP8 W8A8(如 Qwen2.5 系列官方 FP8 权重):8GB 权重,精度损失很小

取舍:量化越低,显存越省、越快,但精度损失越大。4bit 是目前"省显存 + 保质量"的甜点,FP8 是"几乎无损 + 显存减半"的现代选择。


10. 注意力架构变体:KV cache 大小的"结构性"决定因素

前面算 KV cache 时反复用到 num_kv_heads。这个数字不是模型作者随手定的------它来自注意力头架构的选择,是模型架构层面决定 KV cache 大小的第一因素:

变体 K/V 头数 相对 MHA 的 KV 代表模型
MHA n_head(每头独立 K/V) 1x GPT-2、LLaMA-1
MQA 1(所有 Q 头共享) 1/n_head Falcon、PaLM
GQA g(分组共享,g < n_head 1/g LLaMA-2/3、Mistral、Qwen2
MLA 低秩潜在向量 ~1/10 或更小 DeepSeek-V2/V3
  • GQA 是当前开源模型的主流 :LLaMA-3.1-8B 用 n_head=32, num_kv_heads=8,KV cache 只有 MHA 的 1/4------这就是第 02 章算例里 128 KB/token(而不是 512 KB)的原因。
  • MLA 更进一步:DeepSeek 把 K/V 压缩进低秩潜在向量,推理时再展开,KV cache 相比 MHA 可缩到 1/10 以下,是 128K 超长上下文的现实基础。
  • 对 vLLM 的影响num_kv_heads 直接进 CacheConfig 的显存预算公式;GQA/MLA 还需要对应的 attention 内核(MLA 有专用 mla 后端)。

一句话:GQA/MLA 不是性能噱头,而是"单卡能不能服务长上下文 + 大并发"的物理前提。 完整讲透见第 19 章《模型架构基础》。


11. PagedAttention 与 FlashAttention 的关系

FlashAttention 和 PagedAttention 常被放在一起说,容易混淆。它们的本质区别一句话就能讲清:

  • FlashAttention 解决"快" :用分块(tiling)算法 + 在线 softmax(滚动 m/l),避免把完整 QK^T 写回显存,把注意力做到显存高效、IO 友好
  • PagedAttention 解决"碎":把 KV cache 从"连续大张量"改成"按块分配 + 块表间接寻址",解决内存碎片与浪费。

它们不冲突,而是叠加

复制代码
PagedAttention = FlashAttention 的分块在线 softmax 算法
                + block table 间接寻址(把"连续 KV 行"换成"散落的物理块")

vLLM 里两者是可选的 attention 后端VLLM_ATTENTION_BACKEND=FLASH_ATTN / FLASHINFER / TRITON_ATTN ...),要分清"算法"与"内存布局"两层:

  • prefill 阶段 :KV 是按序列连续排布的 varlen 张量,可走 flash-attn 库FLASH_ATTN 后端)的 varlen 路径,把长 prompt 的注意力做得 IO 高效;
  • decode 阶段 :每个新 token 的 KV 都必须写进分页的 block 布局 (否则下一轮读不到),所以 decode 注意力一律走 PagedAttention 内核(块表间接寻址 + 在线 softmax),不因序列数量而改变;
  • FLASH_ATTN 只是"后端名",不代表 decode 跳过分页------它内部对 decode 仍用分页布局的注意力内核。

也就是说:"快"(FlashAttention 的 tiling/在线 softmax)与"碎"(PagedAttention 的块表寻址)在 vLLM 里始终叠加使用,只是 prefill 偏重前者、decode 偏重后者。

与 mini-vLLM 的对照 :mini 版的 attention.pygather_kv(按块表把散落 KV 抓成连续数组)+ 因果注意力两步复刻了 PagedAttention 的数值语义,只是把"抓取"和"计算"分离了------真实 vLLM 把它们融合进一个 Triton/CUDA kernel(详见第 08 章 §1 与 §9)。


12. 一张图串起所有机制

text 复制代码
请求到达
  │
  ├─ 查 Prefix Cache(哈希表):命中 → 复用 KV 块,跳过计算
  │
  ├─ Prefill(可被 Chunked 切块):算 KV → 写入 PagedAttention 的物理块
  │
  ├─ Continuous Batching 调度:每步动态增删请求;KV 不足 → Preemption(RECOMPUTE/SWAP)
  │
  ├─ Decode:内存带宽密集;配合 Speculative Decoding 一次多产 token
  │
  └─ 权重/KV 都经 Quantization 压缩,显存与带宽双省

所有机制围绕同一个目标:让 GPU 在单位时间内产出更多有效 token

12.1 每个机制对应的 vLLM flag 与 mini-vLLM 文件

把本章七个机制 + 注意力架构变体,一张表对应到 真实 vLLM 的启动参数 / 源码位置mini-vLLM 的可读文件(方便第 06 章逐行对照):

表中未标注 V1 的源码路径均为 V0 经典路径 (vLLM 1.0 起已被 vllm/v1/ 取代,但机制语义不变);标注 V1 的行是 V1 引擎自己的实现。

机制 真实 vLLM flag / 配置 真实 vLLM 关键源码 mini-vLLM 文件
PagedAttention --block-size(默认 16) vllm/attention/ops/paged_attn.pyvllm/worker/model_runner.py(slot 映射) attention.pykv_cache.py
Continuous batching --max-num-seqs--max-num-batched-tokens vllm/core/scheduler.pyschedule() scheduler.py
Multi-step scheduling --num-scheduler-steps(V1 默认开启) vllm/v1/core/sched/scheduler.pyMultiStepOutputProcessor 暂无(见附录 C 练习 8)
Chunked prefill --enable-chunked-prefill(V0);V1 恒开启 vllm/core/scheduler.pyvllm/worker/model_runner.py scheduler.py
Prefix caching --enable-prefix-caching V0:vllm/core/block/prefix_caching_block.py;V1:vllm/v1/core/kv_cache_manager.py kv_cache.pyPrefixCache
Preemption --preemption-mode {recompute,swap} vllm/core/scheduler.py(抢占逻辑) scheduler.py(仅 recompute)
Speculative decoding --speculative-model--speculative-config vllm/spec_decode/(draft/verify 主循环) speculative.py
Quantization --quantization {gptq,awq,fp8,...} vllm/model_executor/layers/quantization/ quantize.py
注意力架构变体(MHA/GQA/MLA) 读取 config.jsonnum_key_value_heads vllm/attention/(GQA 复用 paged_attn、MLA 有专用 mla 后端) model.py(读 n_head/head_dim

阅读指引:第 06 章会带着你逐行读右侧的 mini-vLLM 文件,并在每个文件顶部标注它对应的真实 vLLM 路径(中间一列)。GQA/MLA 这类架构变体决定 num_kv_heads、进而决定 KV cache 大小与所需内核,完整展开见第 19 章《模型架构基础》。


13. 小结

机制 解决的问题 一句话原理
PagedAttention KV 内存碎片/浪费 KV 切 16-token 块 + 块表映射,可共享
Continuous batching 静态批处理 GPU 空闲 每步动态增删请求
Chunked prefill 长 prompt 拖垮 decode prefill 切块与 decode 交错
Prefix caching 公共前缀重复计算 按块哈希复用 KV
Preemption KV 耗尽 RECOMPUTE / SWAP 抢占腾位
Speculative decoding decode 串行慢 小模型猜 + 大模型并行验证,无损
Quantization 显存/带宽不足 权重/激活降到 4bit/8bit

下一章《03-架构设计.md》将进入代码地图,看这些机制如何落到 vllm/engine/llm_engine.pyvllm/core/scheduler.py 和 PagedAttention 内核里。

相关推荐
iNeuOS工业互联网20 分钟前
多模态知识库,打通文本、图像与视频资源的协同实践与应用
人工智能·python·音视频
OPEN-F30 分钟前
C++并发编程:异步任务与线程池实战
开发语言·c++·算法
Geek-Chow37 分钟前
MCP 模型上下文协议:四、概念地图 · 八个概念与五个组件
人工智能·大语言模型·mcp
Hotchip_MEMS38 分钟前
MS2202AB-M15E输出驱动能力:直连MCU GPIO无需三极管
人工智能·笔记·物联网·电脑·制造
向哆哆41 分钟前
PCB电子元器件目标检测数据集:23类别 | 目标检测
人工智能·目标检测·计算机视觉
凉茶钱1 小时前
【数据结构】计数排序
数据结构·算法·排序算法
陈彬深大1 小时前
《AI 渐进编程》之二十九:从面向对象到 Harness——封装 AI 的不确定性
人工智能·软件工程
克里斯蒂亚诺更新1 小时前
机器学习和深度学习的主要区别--特征提取
人工智能·深度学习·机器学习
同元软控2 小时前
MWORKS EI:从数字空间到物理世界,面向具身智能产品的一体化研发环境
人工智能·机器学习