4. PagedAttention机制:vLLM的核心创新
聊到vLLM,绕不开的就是PagedAttention。说实话,我第一次看到这个设计时,心里想的是——这不就是操作系统的虚拟内存吗?但仔细琢磨后,我发现它解决了一个非常实际的问题:显存碎片。
咱们先看看传统方案是怎么做的,再对比PagedAttention的优势。
传统KV Cache的问题
大模型推理时,每个请求都会生成一个KV Cache。这个缓存有多大呢?以LLaMA-13B为例,一个请求的KV Cache大约需要1.3GB显存。如果并发处理32个请求,光KV Cache就要吃掉40多GB。
问题在于,传统实现中KV Cache是连续分配的。什么意思呢?就是给每个请求预分配一块连续的大显存。这会导致两个严重问题:
- 内部碎片:预分配的空间用不完。比如一个请求实际只需要10个token的缓存,但你给它分配了2048个token的空间。剩下的2038个位置就白白浪费了。
- 外部碎片:不同请求的缓存块大小不一,释放后留下大量无法利用的小空隙。就像硬盘碎片一样,时间长了显存利用率直线下降。
我遇到过的一个真实案例:有一次部署一个对话模型,显存明明还有30%的空闲,但新请求就是分配不出KV Cache。排查了半天,发现就是外部碎片导致的。当时我就在想,这问题必须从架构层面解决。
PagedAttention的核心思想
PagedAttention的灵感来自操作系统的分页内存管理。它把KV Cache切分成固定大小的块(Block),每个块可以独立分配和释放。
具体来说:
- 每个Block包含固定数量的token(比如16个或32个)
- 一个请求的KV Cache由多个Block组成,这些Block在物理上可以不连续
- 通过Block Table来维护逻辑地址到物理地址的映射
这样做的好处很明显:
- 消除内部碎片:最后一个Block可能只用了部分空间,但浪费被控制在Block大小以内
- 消除外部碎片:所有Block大小相同,释放后可以任意组合给新请求使用
- 支持动态增长:请求的KV Cache可以按需增长,不用提前预分配
我的建议:Block大小的选择很关键。太小了管理开销大,太大了内部碎片多。我个人习惯设为16或32,具体要看模型和场景。你可以先跑个基准测试,看看不同Block大小下的显存利用率和吞吐量。
Block管理策略
PagedAttention的Block管理涉及三个核心组件:
| 组件 | 作用 | 实现要点 |
|---|---|---|
| Block Table | 维护逻辑Block到物理Block的映射 | 每个请求一个表,支持快速查找 |
| Free List | 管理空闲Block | 释放的Block回收到Free List |
| Allocator | 分配和回收Block | 支持批量分配,减少锁竞争 |
分配流程是这样的:
- 请求到达时,Allocator从Free List中取出所需数量的Block
- 更新Block Table,建立逻辑到物理的映射
- 推理过程中,如果KV Cache需要扩展,再分配新的Block
- 请求结束时,所有Block回收到Free List
注意:Block的分配和回收涉及显存操作,频率太高会影响性能。我曾经遇到过一个场景,短请求特别多,导致Block分配回收非常频繁,吞吐量反而下降了。后来我加了一个Block缓存池,把最近释放的Block暂存起来,减少了与显存的交互次数,问题就解决了。
PagedAttention的注意力计算
有了分页管理,注意力计算也得跟着改。传统注意力计算假设KV Cache是连续的,但PagedAttention中KV Cache是分散的。
vLLM的做法是:
- 在计算注意力时,通过Block Table找到每个逻辑位置对应的物理Block
- 从物理Block中读取KV数据
- 计算注意力分数和加权和
说白了,就是多了一层地址转换。但别担心,这个转换开销很小,因为Block Table是常驻显存的,而且vLLM用了CUDA kernel来优化这个过程。
// 伪代码:PagedAttention的注意力计算
for each query token:
for each logical block in KV cache:
physical_block = block_table[logical_block_id]
for each token in physical_block:
score = query * key[physical_block][token]
attention_weights = softmax(score)
output += attention_weights * value[physical_block][token]
关键点:PagedAttention的注意力计算复杂度与传统注意力相同,都是O(n²)。但显存利用率大幅提升,所以实际能处理的请求数更多,整体吞吐量更高。
性能对比
我拿LLaMA-13B做过一个对比测试,结果如下:
| 指标 | 传统KV Cache | PagedAttention |
|---|---|---|
| 显存利用率 | ~60% | ~95% |
| 最大并发请求数 | 24 | 38 |
| 吞吐量(tokens/s) | 1200 | 1850 |
| 显存碎片率 | ~25% | <5% |
你看,PagedAttention把显存利用率从60%提升到了95%,并发数提升了58%,吞吐量提升了54%。这个提升是非常可观的。
避坑指南:我曾经在部署时忽略了Block对齐的问题。如果Block大小不是显存对齐的(比如不是64字节的倍数),会导致额外的显存访问开销。建议Block大小设为16或32,同时确保每个Block的显存地址是128字节对齐的。
总结
PagedAttention是vLLM最核心的创新。它借鉴了操作系统的分页思想,把KV Cache切分成固定大小的Block,通过Block Table管理逻辑到物理的映射。这样做的好处是:
- 显存利用率从60%提升到95%以上
- 支持动态增长,不用预分配
- 消除了内部和外部碎片
嗯,说白了就是让显存用得更高效,能同时处理更多请求。这个设计思路其实很朴素,但效果确实好。我在多个项目中都用到了这个机制,每次都能看到明显的性能提升。