vLLM 底层 PagedAttention(分页注意力)和 Continuous Batching(连续批处理)解释

vLLM的PagedAttention和Continuous Batching

vLLM 底层 PagedAttention(分页注意力)和 Continuous Batching(连续批处理)的源代码解释

vLLM 的两个核心创新------PagedAttentionContinuous Batching ------让它在高并发 LLM 推理中表现突出。下面我用初中生也能看懂的语言 + 真实源代码关键片段(基于 vLLM GitHub 主分支最新实现)一步步解释。

1. PagedAttention(分页注意力)------ 把 KV Cache 像"书页"一样灵活管理

直观比喻

传统 KV Cache 就像给每个同学一张必须连在一起的大书桌 (连续大块内存)。即使只写了 5 行字,也要占满一张能写 100 行的桌子,浪费严重。
PagedAttention 把大书桌拆成很多小书页(每个 Block 大小通常是 16 个 token),小书页可以散落在内存任何地方,只需要一张"地图"(Block Table)记录每页放在哪里。

核心数据结构(Block Table)
  • 每个请求(Sequence)维护一个 Block Table:记录逻辑 token 位置对应到物理 Block 的映射。
  • KV Cache 不再是连续的,而是分散在多个 Block 中。

源代码位置(关键文件):

  • vllm/core/block_manager_v1.pyvllm/v1/core/kv_cache 相关模块(管理 Block 分配)
  • vllm/attention/backends 中的 PagedAttention 实现
  • CUDA Kernel:csrc/attention/attention_kernels.cu 中的 paged_attention_kernel

简化版关键逻辑解释(来自 vLLM 文档和内核):

cuda 复制代码
// 简化后的 paged_attention_kernel(真实在 attention_kernels.cu)
template<...>
__device__ void paged_attention_kernel(...) {
    // 输入参数包括:
    // k_cache: [num_blocks, num_kv_heads, head_size/x, block_size, x]  ← 分块存储
    // v_cache: 类似结构
    // block_tables: 每个序列的 Block 映射表
    
    // 核心:通过 block_table 间接访问 KV
    for each logical position in sequence {
        int block_idx = block_table[logical_pos / BLOCK_SIZE];  // 查地图找哪一页
        int offset_in_block = logical_pos % BLOCK_SIZE;         // 在这一页的第几行
        load K/V from k_cache[block_idx][...][offset_in_block] ...
    }
    // 然后正常计算注意力分数
}

Block Table 示例(数字说明)

假设 BLOCK_SIZE = 16。

  • 请求当前有 50 个 token(需要约 4 个 Block)。

  • Block Table 可能长这样(逻辑 Block → 物理 Block ID):

    复制代码
    逻辑 Block 0 → 物理 Block 23
    逻辑 Block 1 → 物理 Block 45   (不连续!)
    逻辑 Block 2 → 物理 Block 7
    逻辑 Block 3 → 物理 Block 12

当生成第 51 个 token 时:

  • 计算需要的新 Block(如果当前 Block 满了),从全局 Free Block Pool 拿一个空闲 Block(比如 Block 67)。
  • 更新 Block Table:逻辑 Block 4 → 物理 Block 67。
  • 内存碎片几乎为 0(最多浪费 15 个 token)。

内存效率数字

传统方式:内存利用率 ~30-40%。

vLLM PagedAttention:内存利用率可达 90%+,相同 GPU 能服务 2-4 倍以上并发请求。

2. Continuous Batching(连续批处理)------ 像公交车一样动态上下客

直观比喻

传统静态批处理 = 必须等一车人全部到齐才发车,有人早到要等很久,车经常空着。
Continuous Batching(也叫 Iteration-level Batching) = 像城市公交车:有人下车(生成完一个 token),马上让新乘客上车(新请求加入),GPU 几乎不空闲。

核心实现:在 Scheduler 中每一步(iteration)动态构建 batch

源代码位置(关键文件):

  • vllm/v1/core/sched/scheduler.py (V1 引擎调度器)
  • vllm/core/scheduler.py (早期版本)
  • vllm/engine/llm_engine.pyvllm/v1/engine/core.py 中的 step() 循环

关键代码逻辑(高度简化 + 注释)

python 复制代码
# 来自 vllm/v1/core/sched/scheduler.py (简化版核心)
class Scheduler:
    def __init__(self):
        self.waiting = deque()   # 新来的请求队列
        self.running = []        # 正在生成的请求列表
    
    def schedule(self, max_num_batched_tokens: int):
        # 1. 先尽量继续处理正在运行的请求(decode)
        scheduled = []
        token_budget = max_num_batched_tokens
        
        # 优先处理 running 中的请求(每个请求生成 1 个新 token)
        for seq_group in self.running:
            if token_budget > 0:
                scheduled.append(seq_group)
                token_budget -= 1   # 每个 decode 请求通常消耗 1 token
        
        # 2. 如果还有预算,再从 waiting 中加入新请求(prefill)
        while self.waiting and token_budget > 0:
            new_req = self.waiting.popleft()
            # 计算这个新请求 prefill 需要多少 token
            needed = new_req.get_prompt_len()
            if needed <= token_budget:
                scheduled.append(new_req)
                token_budget -= needed
            else:
                # 如果预算不够,可以 chunked prefill(分块预填充)
                break
        
        # 3. 返回本次要一起计算的 batch
        return scheduled

每一步(iteration)流程(真实引擎循环):

  1. 从 running 请求中取出需要生成下一个 token 的序列。
  2. 如果还有预算,从 waiting 队列中加入新 prompt(prefill)。
  3. 把这些请求打包成一个 batch,送给模型做一次 forward pass。
  4. 生成完后:已完成的请求下车(返回结果),未完成的继续留在 running 队列。
  5. 循环重复 ------ GPU 几乎一直有活干。

数字例子(假设 max_num_batched_tokens = 4096):

  • 当前 running 有 60 个请求,每个生成 1 个新 token → 消耗 60 tokens。
  • 还有预算 4036 tokens → 可以加入几个新 prompt(比如一个 2000 token 的 prompt + 几个小 prompt)。
  • 下一步:有人生成完离开 → 马上补新请求进来。
  • 结果:GPU 利用率从传统 ~50% 提升到 90%+,吞吐量提升 2-5 倍(高并发时可达 10 倍以上)。

两者如何配合工作?

  • PagedAttention 负责 内存高效管理(KV Cache 分页存储 + Block Table 映射)。
  • Continuous Batching 负责 动态调度(每一步重新组 batch,让 GPU 不空闲)。
  • 在执行时,注意力内核(PagedAttention Kernel)通过 Block Table 正确读取分散的 KV 数据。

整体效果(真实基准):

  • 相同硬件下,vLLM 的吞吐量通常是 Hugging Face Transformers 或早期 TGI 的 2-5 倍,内存效率提升显著。
  • 高并发场景(几十到几百个请求同时)优势特别明显。

总结(一句话记住)

PagedAttention = 把记忆本(KV Cache)拆成小书页,随便放,浪费几乎为 0。
Continuous Batching = 公交车不等人,有人下就马上让人上,电脑一直满负荷工作。

两者结合,让 vLLM 成为目前最受欢迎的高吞吐 LLM 推理框架之一。

想看更具体的:

  • 某个文件的完整代码片段
  • BlockAllocator 的实现
  • 或者结合前面 RadixAttention 的对比

随时告诉我,我可以继续深入讲解!😊

(以上解释基于 vLLM 官方文档、GitHub 源码结构和论文,实际代码随版本可能有少量调整,建议直接查看 https://github.com/vllm-project/vllm

具体细节

加入现在A decode在产生一个新的token之前,正好进来了一个新prompt B进行prefill,那么接下来A产生的一个新token放在哪里,放在B的后面吗?这里的batch怎么理解,A和B分别放在两行吗?还是说放在另一个的后面(一行)

很好,你问到了 Continuous Batching 最核心、最容易混淆的地方!

我用最直白的话 + 图片 + 数字例子给你讲清楚。

1. 你的场景简化描述

  • 当前时刻:请求 A 正在 decode 阶段(已经生成了一些 token,现在正要生成下一个新 token)。
  • 就在 A 要生成新 token 之前 ,来了一个全新的请求 B (一个完整的 prompt,需要走 prefill)。

问题

  • A 生成的新 token 放在哪里?
  • A 和 B 是放在同一行 (一个序列后面接另一个)?还是两行(并行)?
  • batch 到底是怎么组织的?

2. 正确答案(非常重要)

A 生成的新 token 不会放在 B 的后面!
A 和 B 是完全独立的两个序列,它们是并行(并排)处理的,而不是前后拼接。

Batch 的理解方式

  • Batch 不是"一长行文字" ,而是一张表格(多行多列)。
  • 每一代表一个独立的请求(sequence)。
  • 每一代表模型一次 forward pass(一次计算)要处理的 token 位置。

看下面这张经典图(来自 vLLM 相关讲解):

在这张图里

  • 不同颜色代表不同的请求(A、B、C、D)。
  • 粉色块 = prefill(新 prompt 的一次性处理)
  • 蓝色/绿色块 = decode(每次生成 1 个新 token)
  • 你可以看到:多个请求是并排放在一起的(同一时间步里同时处理 A 的 decode + B 的 prefill)。

3. 具体到你的场景:A + 新来的 B 会怎么处理?

假设当前 GPU 的 batch 容量允许同时处理多个请求。

这一步(iteration)会发生的事

  1. A (老请求):继续 decode,生成它的第 N+1 个新 token。
  2. B (新请求):进行 prefill,一次性处理它的整个 prompt(假设 B 的 prompt 有 200 个 token)。

在模型的一次 forward pass 中:

  • 输入形状 大概是 [batch_size=2, seq_len=当前最大长度](实际用 padding + attention mask 处理)。
  • 第 1 行:请求 A 的序列(已有的 token + 即将生成的新 token 位置)。
  • 第 2 行:请求 B 的序列(它的整个 prompt)。

A 的新 token 不会接在 B 后面,而是:

  • A 的新 token 仍然接在 A 自己 的历史序列后面。
  • B 的 KV Cache 是独立构建的,放在它自己的"行"里。

看这张图能更清楚地看到"并行多行"的概念:

这张图中每个"Slot"(槽位)就是一行(一个请求),不同请求并行存在于同一个 batch 中。

4. 总结(最简单记忆版)

  • Batch = 一张多行表格,不是一长行文字。
  • 每一 = 一个独立的请求(A 一行,B 一行)。
  • A 的新 token 只接在 A 自己的历史后面,不会跑到 B 后面去。
  • A(decode)和 B(prefill)可以同时出现在同一个 batch 里,被模型一次性计算。
  • 这就是 Continuous Batching 的厉害之处:随时有人"下车"(生成完),随时让新乘客(新 prompt)上车,GPU 几乎不空闲。

一句话回答你的问题

A 产生的新 token 放在 A 自己的序列后面 ,B 是独立的一行。A 和 B 在 batch 里是并排的两行,而不是一个接在另一个后面。

想让我再画一个只针对 A+B 这个具体场景的简化图吗?或者我把"attention mask 在这种混合 batch 中怎么工作"也讲一下?

问题:同一个slot里面

上面的图中一个slot里面,比如req0后面跟的是req13,那么req0生成完之后的下一个token放在哪里?放在req13后面吗?我是说同一个slot内部

你的问题问得非常好!

我来用最清楚的方式回答你关于 同一个 slot(槽位) 的疑问。

关键结论(先说答案)

同一个 slot 里面,req0 生成完之后,下一个 token 不会放在 req13 后面!

req0 和 req13 是完全独立的两个请求 ,它们不会拼接成一个长序列

当 req0 生成完(输出结束符 EOS 或达到 max_tokens)之后:

  • req0 直接"下车"(从这个 slot 退出)。
  • 这个 slot 就空出来了。
  • 下一轮 ,调度器会把 req13 (一个全新的请求)放进这个 slot,开始处理 req13 的 prefill 或 decode。

同一个 slot 在不同时间段 只是复用同一个计算位置,而不是把两个请求的 token 接在一起。

用上面的图(Slot 0)来具体解释

看你提到的这张经典图(Slot 0):

  • 紫色部分:Req 0 在运行(正在 decode)。
  • 绿色部分:Req 13 在运行(后来进入)。

时间线是这样的

  1. 前期 (0~100 iteration 左右):

    Slot 0 里只有 Req 0 在 decode,一个 token 一个 token 地生成。

  2. Req 0 生成完(大约第 100 iteration):

    • Req 0 输出结束,立刻退出 这个 slot。
    • Slot 0 此时变为空闲。
  3. 下一轮或几轮之后

    调度器发现 Slot 0 空了,就把等待队列里的 Req 13 放进来。

    Req 13 开始在这个 slot 里运行(先 prefill,再 decode)。

所以 req0 的最后一个 token 和 req13 的第一个 token 完全不相关,它们没有前后关系。

同一个 slot 的本质是什么?

  • Slot(虚拟槽位) 只是 GPU 计算时的一个位置编号(类似座位号)。
  • 它不是固定给某个请求的,而是动态复用的。
  • 当一个请求结束时,这个座位就释放出来,给下一个新请求用。
  • 每个 slot 在任意一个时刻只属于一个请求

就像酒店房间:

  • 今天张三住(Req 0),他退房后。
  • 明天李四住(Req 13)。
  • 但张三的最后一天和李四的第一天不是连续的,他们的"故事"完全独立。

为什么 vLLM 要这样设计?

  • 为了最大化 GPU 利用率:只要有请求结束,就立刻让新请求进来,不让 slot 空着。
  • 如果把 req13 接在 req0 后面,就会破坏因果注意力(causal attention),模型会把两个完全无关的对话混在一起,这是错误的。

总结(一句话记住)

同一个 slot 内部

  • req0 生成完 → req0 退出 slot。
  • 之后 req13 进入同一个 slot。
  • req0 的新 token 只接在 req0 自己 的历史后面。
  • req13 的 token 只接在 req13 自己 的历史后面。
  • 它们不会拼接在一起

后记

2026年4月16日于上海,在grok辅助下完成。

相关推荐
进击的松鼠2 小时前
从对话到动作:用 Function Calling 把 LLM 接到真实 API(含流程拆解)
python·llm·agent
山顶夕景3 小时前
【LLM后训练】看Off-Policy and On-Policy Learning
llm·distillation·蒸馏·posttraining
Pkmer7 小时前
Harness Engineering: 人类掌舵,智能体执行
llm·agent
EdisonZhou8 小时前
MAF快速入门(23)通过C#类定义Skills
llm·agent·.net core
华农DrLai9 小时前
什么是推荐系统中的负反馈?用户的“踩“和“不感兴趣“怎么用?
人工智能·算法·llm·prompt·知识图谱
空巢青年_rui20 小时前
【翻译】现代LLM中注意力变体的可视化指南:从MHA和GQA到MLA、稀疏注意力机制和混合架构
llm·attention·mha·gqa·dsa·mla·swa
OpenBayes贝式计算1 天前
教程上新丨一键部署Gemma 4 31B,最高256K上下文,能力媲美Qwen3.5 397B
google·开源·llm
Code_Artist1 天前
LangChainGo构建RAG应用实况:切分策略、文本向量化、消除幻觉
机器学习·langchain·llm
半夏之沫1 天前
✨向量化✨和Embedding
人工智能·机器学习·llm