MiniPaged-Qwen:把 Paged Attention 讲清楚
大模型推理里,大家一提优化,最容易先想到的是 FlashAttention、Tensor Core、Kernel Fusion。但在实际的 LLM Serving 场景里,另一个同样关键的问题往往更“系统”一些:
历史 KV 到底放在哪里?不同请求长度不一样时,显存怎么管?Decode 时又怎么在不连续的物理存储上完成 Attention?
Paged Attention 解决的就是这一类问题。
很多时候我们会把 Paged KV Cache 和 Paged Attention 混在一起说。严格一点看:
- Paged KV Cache 解决的是:KV 怎么分块、怎么映射、怎么分配和释放;
- Paged Attention 解决的是:在 KV 已经按 Block 分页存储之后,Decode Attention 怎么正确、有效地读取它们并完成计算。
前者更偏内存管理,后者更偏注意力计算与 Kernel 设计。
本文按一个比较“从直觉到实现”的顺序来整理:
- Paged Attention 是什么;
- 它用于什么场景、解决什么问题;
- 按原论文思路写出 Paged Attention 的计算过程;
- 用一张图把 静态显存组织 和 动态计算过程 放在一起说明;
- 最后回到 MiniPaged-Qwen,说明我们现在是怎么实现的,与官方方案有哪些差异,以及后续能往哪里改进。
1. Paged Attention 是什么
Paged Attention 出自 vLLM 论文 Efficient Memory Management for Large Language Model Serving with PagedAttention。它的核心思想并不是“重新定义 Attention 数学公式”,而是:
把 Decode 阶段 Attention 所访问的 KV Cache 分成固定大小的 Block,通过 Block Table 把逻辑连续的历史 Token 映射到物理上不连续的存储块,再在 Attention 计算时按这个映射逐块访问。
如果只看标准单步 Decode Attention,对于当前 Query:
$$ q \in \mathbb{R}^{d} $$
历史 Key 和 Value 为:
$$ K \in \mathbb{R}^{L\times d},\qquad V \in \mathbb{R}^{L\times d} $$
则输出为:
$$ o
\sum_{j=0}^{L-1} \frac{ \exp\left( \frac{qk_j^T}{\sqrt d} \right) }{ \sum_{t=0}^{L-1} \exp\left( \frac{qk_t^T}{\sqrt d} \right) } v_j $$
Paged Attention 并不改变这个数学结果。它做的事情是:
- 逻辑上,历史 Token 仍然按 $0,1,2,\dots,L-1$ 排列;
- 物理上,这些 Token 的 KV 可以位于不同的 Physical Block;
- 计算时,先通过
block_table找到某个逻辑位置属于哪个 Physical Block,再从分页后的 KV Cache 中把对应的 K/V 读出来。
所以它本质上是:
“分页式 KV 存储 + 面向分页存储的 Attention 访问与计算方式”。
2. 它用于什么场景
Paged Attention 主要用于:
2.1 LLM Serving / 在线推理服务
离线单条推理时,连续 KV Cache 也能用;但在线服务的情况不同:
- 多个请求同时并发;
- 每个请求的长度动态变化;
- 有的请求很快结束,有的请求会持续生成很长;
- 需要 Continuous Batching、Beam Search、Parallel Sampling 等更复杂的调度。
在这种场景里,KV Cache 的内存管理本身会成为吞吐瓶颈。
2.2 长上下文 Decode 阶段
Prefill 里更像“矩阵 Attention”;而 Decode 阶段更像:
- 每一步只有一个新 Query;
- 但要访问越来越长的历史 KV。
因此随着上下文变长,Decode 中:
- 历史 KV 的显存占用 持续上升;
- 访问历史 KV 的方式 变得越来越重要;
- 不同请求之间的内存碎片 会逐步限制并发数。
Paged Attention 正是在这种“长生命周期、动态长度、多并发请求”的环境里价值最大。
3. 它解决了什么问题
原论文强调的核心问题可以概括为三类。
3.1 连续 KV Cache 带来的显存浪费
如果给每个请求都预留一块连续的大 Buffer:
$$ [H_{kv}, L_{max}, d] $$
那就必须按最大可能长度预留空间。真实生成长度远小于 $L_{max}$ 时,大量显存会被浪费。
3.2 动态扩容连续 Buffer 的搬移成本
如果不提前预留最大长度,而是用“空间不足再扩容”的方式,那么当当前 Buffer 放不下新 Token 时,需要:
申请更大的连续区域
↓
拷贝旧 KV
↓
释放旧区域这在高并发 LLM 服务里代价很高。
3.3 多请求结束时间不同导致的外部碎片
多个请求的 KV Buffer 如果都要求物理连续,就会像传统堆分配那样产生外部碎片:中间零散的空闲区域不能轻易给一个更大的新请求复用。
Paged Attention 的解决思路是:
取消“每个请求的 KV 必须连续存储”这一约束。
只要逻辑上知道历史第 $pos$ 个 Token 落在哪个 Block、哪个 Offset,Attention 就仍然能算对。
4. 先区分:Paged KV Cache 与 Paged Attention
这两个概念容易混。
4.1 Paged KV Cache
它回答:
新生成的 KV 应该写到哪里?历史 KV 目前分布在哪些物理 Block 里?
核心数据结构包括:
block_sizeblock_tablefree_blocksslot_mappingkey_cache / value_cache
4.2 Paged Attention
它回答:
在 Decode 的时候,当前 Query 如何通过
block_table找到所有历史 K/V,并按正确顺序完成 Attention?
也就是说:
- Paged KV Cache 决定“怎么存”;
- Paged Attention 决定“怎么读、怎么算”。
5. 原论文视角下的 Paged Attention 计算过程
下面参考原论文思路,把单步 Decode 的 Paged Attention 写清楚。
假设当前只生成 1 个 Query,对应某个 Sequence、某个 Query Head。
设:
- 历史长度为 $L$;
- Block Size 为 $B_s$;
- Head Dimension 为 $d$。
则逻辑上可把历史 K/V 按 Block 划分为:
$$ K
[K^{(0)}, K^{(1)}, \dots, K^{(T-1)}] $$
$$ V
[V^{(0)}, V^{(1)}, \dots, V^{(T-1)}] $$
其中:
$$ K^{(t)}, V^{(t)} \in \mathbb{R}^{B_s \times d} $$
最后一个 Block 可能没有填满;Block 总数为:
$$ T
\left\lceil \frac{L}{B_s} \right\rceil $$
对第 $t$ 个逻辑 Block,有:
$$ pb_t
block_table[t] $$
这表示逻辑块 $t$ 实际对应的 Physical Block 编号是 $pb_t$。
5.1 第一步:逐 Block 读取历史 Key
对于当前 Query:
$$ q \in \mathbb{R}^{d} $$
第 $t$ 个 Block 的 Score 向量为:
$$ s^{(t)}
\frac{ q (K^{(t)})^T }{ \sqrt d } \in \mathbb{R}^{B_s} $$
但注意这里的 $K^{(t)}$ 并不是连续内存上的“第 $t$ 块”,而是通过:
$$ pb_t
block_table[t] $$
从:
$$ key_cache[pb_t, kvh, :, :] $$
取出的。
5.2 第二步:按全部历史 Token 做 Softmax
Attention 的概率并不是“每个 Block 自己做一次 Softmax”,而是要在全历史维度上归一化:
$$ p_j
\frac{ \exp(s_j) }{ \sum_{u=0}^{L-1} \exp(s_u) } $$
Paged Attention 常见实现不会先把全部 Score 收集完再统一 Softmax,而是使用 Online Softmax 或 分块归约。
设当前累积状态为:
- 运行最大值 $m$;
- 归一化分母 $l$;
- 未归一化输出累积 $\tilde o$。
遍历到一个新 Block 的 Score 向量 $s^{(t)}$ 后,先取该 Block 最大值:
$$ m_t = \max s^{(t)} $$
更新全局最大值:
$$ m’ = \max(m, m_t) $$
旧分母重标定:
$$ l'
l, e^{m-m’} + \sum_j e^{s^{(t)}_j - m’} $$
旧输出重标定并累积:
$$ \tilde o'
\tilde o, e^{m-m’} + \sum_j e^{s^{(t)}_j-m’} v^{(t)}_j $$
然后更新:
$$ m\leftarrow m’, \qquad l\leftarrow l’, \qquad \tilde o\leftarrow \tilde o' $$
全部 Block 遍历结束后:
$$ o = \frac{\tilde o}{l} $$
这就得到了与标准 Attention 完全等价的结果。
5.3 第三步:Paged Attention 与标准 Attention 的关系
从数学上看,它仍然是:
$$ o
\mathrm{Softmax} \left( \frac{qK^T}{\sqrt d} \right)V $$
Paged Attention 做的只是把:
K和V的物理存储方式变成分页的;K和V的读取方式变成通过block_table间接寻址;Softmax的实现方式变成更适合流式 Block 遍历的 Online / Partitioned 版本。
所以它不是改变模型,而是改变 Runtime。
6. 一张图看清:静态显存分布与动态计算过程
下面这张图把 Paged Attention 的两个层面放在了一起:左侧是静态显存布局,右侧是 Decode 时动态计算流程。

这张图可以分成两半理解。
6.1 左侧:静态显存分布
左边表示 Paged KV Cache 的物理存储。
物理 Block Pool
显存中的 KV Cache 不是按请求连续存,而是先切成固定大小的 Physical Block:
physical block 0
physical block 1
physical block 2
...每个 Block 内部有固定数量的 Slot,例如:
$$ block_size = 4 $$
那么一个 Block 就能容纳连续 4 个 Token 的 KV。
Block Table
对于某个 Sequence,其逻辑历史位置被分为:
logical block 0
logical block 1
logical block 2
...通过:
block_table[logical_block] = physical_block映射到物理块。
因此逻辑连续、物理离散。
Free List
未使用的 Physical Block 由:
free_list统一管理。新请求增长到新的 Logical Block 时,从 Free List 分配一个 Physical Block;请求结束时,再把对应 Block 归还。
6.2 右侧:动态计算过程
右边表示单步 Decode 时,Paged Attention 如何访问历史 KV。
输入包括:
- 当前 Query;
- 当前 Sequence 的
block_table; - 当前有效长度
seq_len。
然后执行:
1)遍历逻辑历史位置
对:
$$ pos=0,1,2,\dots,L-1 $$
逐个处理。
2)先从 pos 计算逻辑块与偏移
$$ logical_block
\left\lfloor \frac{pos}{block_size} \right\rfloor $$
$$ off
pos \bmod block_size $$
3)再通过 Block Table 查到物理块
$$ pb
block_table[logical_block] $$
4)从分页后的 KV Cache 中读取 K/V
$$ k_{pos}
key_cache[pb,kvh,off,:] $$
$$ v_{pos}
value_cache[pb,kvh,off,:] $$
5)更新 Attention 计算状态
也就是:
- 点积得到 Score;
- 更新 Online Softmax;
- 累积输出。
这一步并不会因为分页而改变数学结果,只是地址访问方式变了。
7. 进一步理解:Paged Attention 到底改变了什么
如果用一句话总结,我会说:
Paged Attention 把“Attention 计算”与“KV 的物理连续布局”解耦了。
标准连续 KV 的读取是:
base + posPaged Attention 的读取是:
pos
↓
logical_block
↓
block_table
↓
physical_block
↓
offset
↓
KV address它引入了一个额外的“地址翻译层”,用来换取更灵活的显存管理能力。
8. MiniPaged-Qwen 中我们是如何实现的
下面回到项目本身。
8.1 CPU 侧:Block Manager 管理元数据
MiniPaged-Qwen 先实现了一个最小可验证的 CPU 侧 Block Manager。
核心数据结构:
@dataclass
class SequenceState:
sequence_id: int
sequence_length: int = 0
block_table: List[int] = field(default_factory=list)以及:
class KVBlockManager:
num_physical_blocks
block_size
free_blocks
sequences它负责:
create_sequence()append_token()release_sequence()get_block_tables_tensor()get_seq_lens_tensor()invariants_ok()
其中 append_token() 会:
- 根据
sequence_length算出当前 Token 所在的logical_block; - 如果跨越 Block 边界,则申请新的 Physical Block;
- 返回这个新 Token 对应的线性
slot_mapping。
这部分是 Paged KV Cache 的元数据平面。
8.2 GPU 侧:Paged KV Cache 保存真正的 K/V
MiniPaged-Qwen 中每层的 Cache Tensor 布局是:
$$ [num_blocks, num_kv_heads, block_size, head_dim] $$
也就是:
key_cache.shape = [num_blocks, kvh, block_size, dim]
value_cache.shape = [num_blocks, kvh, block_size, dim]这意味着第 pb 个 Physical Block、第 kvh 个 KV Head、第 off 个 Offset 的向量就是:
$$ key_cache[pb,kvh,off,:] $$
$$ value_cache[pb,kvh,off,:] $$
Cache Write Kernel 根据 slot_mapping 把新 Token 的 K/V 写入这里。
8.3 Decode 侧:Paged Attention Kernel
项目中的 Decode Attention 实现分为:
- CPU Reference;
- CUDA Kernel。
CUDA Kernel 的设计是:
一个 CUDA Block 负责一个
(sequence, query_head)输出。
其输入包括:
querykey_cachevalue_cacheblock_tablesseq_lens
计算过程大致是:
- 当前 Block 对应某个
(b, qh); - 根据 GQA 关系得到对应
kvh; - 遍历历史位置
pos = 0...seq_len[b]-1; - 使用:
$$ logical_block = pos // block_size $$
$$ pb = block_tables[b, logical_block] $$
$$ off = pos % block_size $$
从分页 Cache 中读取:
$$ K[pb,kvh,off,:],\quad V[pb,kvh,off,:] $$
- 通过 Online Softmax 累积输出。
也就是说,MiniPaged-Qwen 当前的 Paged Attention 已经打通了:
Qwen3 Query
↓
block table lookup
↓
paged KV gather
↓
online softmax
↓
attention output这个闭环。
9. 我们的实现与官方方案有哪些差异
这一部分很关键。因为“能跑通的学习实现”和“vLLM 官方生产级实现”之间一定有差距。
9.1 相同点
MiniPaged-Qwen 与官方 Paged Attention 在核心思想上是一致的:
一致点 1:分页式 KV 存储
都把 KV Cache 切成固定大小的 Block,由 block_table 完成逻辑到物理的映射。
一致点 2:Decode 时通过 Block Table 间接访问 K/V
都不是假设历史 KV 连续,而是先做地址转换,再读取分页后的 Cache。
一致点 3:Attention 数学结果不变
两者都保持:
$$ o
\mathrm{Softmax} \left( \frac{qK^T}{\sqrt d} \right)V $$
的语义不变。
一致点 4:都需要处理 GQA / 多 Head 映射
对于 Qwen3 这种:
$$ H_q \neq H_{kv} $$
的模型,Attention Kernel 都要考虑:
$$ kvh = \left\lfloor qh / (H_q/H_{kv}) \right\rfloor $$
9.2 当前 MiniPaged-Qwen 的主要差异
差异 1:我们更像“教学实现”
当前实现的重点是:
- 结构清晰;
- 数值正确;
- 能解释全流程;
- 方便与 Hugging Face 对齐验证。
因此在 Kernel 设计上更直白,尤其是 Decode Attention 里,采用:
- 一个 CUDA Block 对一个
(sequence, query_head); - 按历史 Token 顺序遍历;
- 用 Online Softmax 做流式归一化。
这非常适合理解,但不一定是吞吐最优方案。
差异 2:官方实现更强调高吞吐和大批量服务
官方 vLLM Paged Attention 的重点是 Serving Throughput,因此它更关注:
- 多请求大批量并发;
- 更复杂的线程块布局;
- Warp/Thread Group 的向量化加载;
- 更细粒度的并行归约;
- 更复杂的调度与内存共享。
而我们的实现目前更像:
面向单机学习与验证的 Mini Runtime。
差异 3:我们还没有实现官方的 KV 共享能力
原论文里一个非常重要的能力是:
- Shared Prefix;
- Parallel Sampling / Beam Search 的 Block 共享;
- Copy-on-Write;
- Reference Counting。
也就是说,多条请求如果前缀相同,可以让它们的 Block Table 指向同一批 Physical Block,而不是复制一份 KV。
MiniPaged-Qwen 当前实现里:
- 每个 Sequence 拥有自己的 Block Table;
- 但尚未实现 Block 级共享与引用计数;
- 也没有 Copy-on-Write。
所以它已经实现了分页,但还没有实现“共享分页”。
差异 4:官方实现的 Memory Manager 更完整
官方系统要面对:
- Continuous Batching;
- 请求抢占与恢复;
- Beam Search 分叉;
- Prefix Reuse;
- 分布式服务。
因此除了 Paged Attention Kernel,还包括完整的 Serving Runtime。
MiniPaged-Qwen 目前实现的是其中最核心的一条主线:
分页式 KV 管理
+
Decode Paged Attention10. 我们现在这个版本有哪些改进空间
如果从“下一步优化什么”来考虑,我会把方向分成三层。
10.1 内存管理层
1)Block 共享
支持:
- 相同 Prompt 前缀复用;
- Beam Search 分支共享;
- 引用计数;
- Copy-on-Write。
2)更丰富的元数据导出
例如:
- 更适合 Batch 化的 Block Table Tensor;
- 更直接的 GPU Metadata 格式;
- 更低开销的 Sequence 生命周期管理。
10.2 Attention Kernel 层
1)更高效的加载与并行布局
当前的 Kernel 是“正确优先”的实现,之后可以继续优化:
- 更好的 Memory Coalescing;
- 更高效的 Head-Dim 并行;
- 更好的 Shared Memory / Register 使用;
- 更细致的 Warp Reduction。
2)Block 级并行或 Partitioned Reduction
目前我们按历史位置顺序扫描,后续可以向官方更高吞吐的分块并行思路靠近,例如:
- 多个线程组并行处理不同 KV 分片;
- 局部归约后做全局合并;
- 更适合长上下文与大批量的执行模式。
10.3 Runtime 层
1)Continuous Batching
Paged Attention 真正的价值往往要放在:
动态请求进入 / 退出的运行时系统里看。
2)与 Prefill / Decode 全链路更紧密耦合
也就是把:
Prefill
↓
KV Cache Write
↓
Multi-layer Decode
↓
Token Generation做成真正的完整 Runtime,而不只是独立模块。
3)Benchmark 与 Profiling
后续值得系统测量:
- TTFT
- TPOT
- 吞吐
- 峰值显存
- 不同 Block Size 的影响
- 不同上下文长度下的性能变化
11. 一句话总结 Paged Attention
如果让我用一句更偏工程的话来概括:
Paged Attention = 让 Attention 计算不再依赖“KV 在物理显存中连续存放”,从而把 LLM Decode 的内存管理问题转化为“固定大小 Block + Block Table + 分页式访问”的问题。
它的价值不在于改变 Transformer 的数学本质,而在于:
- 让显存利用率更高;
- 让请求生命周期管理更灵活;
- 让高并发 LLM Serving 更容易做大批量调度;
- 为 Prefix Sharing、Beam Search Sharing 等更高级优化提供基础。
12. 总结:MiniPaged-Qwen 当前已经做到什么程度
最后把项目当前状态收束一下。
MiniPaged-Qwen 已经完成了一个面向学习和验证的最小 Paged Attention 系统原型:
已实现
- Qwen3 的 Decode Attention 路径;
- Paged KV Cache;
- CPU Block Manager;
slot_mapping写入;block_tables + seq_lens驱动读取;- GQA-aware Paged Attention;
- CPU 参考实现与 CUDA 实现;
- 与 Hugging Face Attention 路径的数值对齐验证。
当前定位
它更适合被理解为:
一个“把 Paged KV Cache 与 Paged Attention 核心思想走通”的 Mini Runtime。
与官方相比
它已经抓住了最核心的设计点,但仍然缺少:
- Block Sharing / Reference Counting;
- Copy-on-Write;
- 更高吞吐的 Kernel 布局;
- 更完整的 Serving Runtime 与调度系统。
不过这恰好也是它的优点:结构清楚,适合逐层分析和继续扩展。
参考资料
- Woosuk Kwon, et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180.
- vLLM Documentation. Paged Attention.
- MiniPaged-Qwen source code:
block_manager.py,paged_attention.py,integrate_qwen3_paged_attention.py.