Block Manager 详解:Block Table 结构、Block 分配策略、Block 回收与复用、内存池管理
各位同学,今天我们来聊聊 vLLM 里一个非常核心的组件——Block Manager。说白了,它就是整个显存调度的大脑。你想想看,大模型推理时,显存就那么点,请求却一个接一个来,怎么高效地分配、回收、复用这些显存块?这就是 Block Manager 要干的事。
我个人习惯把 Block Manager 看作一个「内存管家」。它手里有几张表,记录着每一块显存谁在用、用在哪、用完了没。今天我们就从它的数据结构开始,一步步拆解它的工作流程。
Block Table 结构:管家的账本
Block Table 是 Block Manager 最核心的数据结构。它本质上是一个哈希表,key 是逻辑块 ID,value 是物理块 ID 和引用计数。嗯,这里要注意,逻辑块和物理块是两码事。
- 逻辑块:请求看到的连续地址空间。比如一个请求需要 8 个 token,那它就有 8 个逻辑块。
- 物理块:显存里实际分配的块。这些块可能是不连续的。
Block Table 的作用就是建立逻辑到物理的映射。我刚开始接触这个设计时,觉得这不就是页表吗?没错,思路确实来自操作系统的虚拟内存管理。
核心数据结构:
struct BlockTable {
// 逻辑块 ID -> 物理块 ID 的映射
unordered_map<int, int> logical_to_physical;
// 物理块 ID -> 引用计数
unordered_map<int, int> physical_ref_count;
// 空闲块列表
vector<int> free_blocks;
};
这里有个关键点:引用计数。一个物理块可能被多个请求共享吗?答案是肯定的。比如 beam search 时,多个 beam 的前缀可能完全一样,那这些前缀的物理块就可以共享。引用计数就是用来跟踪这个的。
Block 分配策略:怎么分才不浪费?
分配策略这块,vLLM 用了两种模式:预分配和按需分配。我个人更倾向于按需分配,因为它更节省显存。
预分配模式:
- 请求进来时,一次性分配最大可能需要的块数。
- 好处是简单,坏处是浪费。比如一个请求可能只生成 10 个 token,但你给它分配了 100 个块的空间。
按需分配模式:
- 请求进来时,只分配当前需要的块。
- 随着生成过程推进,动态增加块。
- 好处是显存利用率高,坏处是分配次数多,有开销。
vLLM 默认用的是按需分配。我在项目中遇到过一个问题:如果分配太频繁,CPU 和 GPU 之间的同步会成为瓶颈。后来我们加了一个小缓存,每次分配时多拿几个块,减少交互次数。效果还不错。
避坑指南:我曾经在分配策略上踩过一个坑。当时为了省显存,把块大小设得很小(比如 8 个 token)。结果分配次数暴增,性能反而下降了。后来我建议块大小至少 16 个 token,最好 32 个。这个值要根据你的模型和显存带宽来调。
Block 回收与复用:用完别扔
当一个请求结束后,它占用的物理块怎么处理?直接释放?那太浪费了。vLLM 的做法是:先检查引用计数。
- 如果引用计数为 0,说明这个块没人用了,可以回收。
- 回收时,把物理块 ID 放回空闲列表。
- 下次分配时,优先从空闲列表里拿。
这里有个有意思的点:前缀共享。比如两个请求的 prompt 都是「今天天气怎么样」,那它们的前缀块就可以共享。第一个请求结束后,这些前缀块的引用计数减 1,但可能还没到 0,因为第二个请求还在用。所以不会立即回收。
你想想看,这种机制天然支持了 prefix caching。我在做性能分析时发现,如果请求的 prompt 重复率很高,显存利用率能提升 30% 以上。
内存池管理:管家的仓库
内存池是 Block Manager 的底层支撑。它负责管理一整块连续的显存,然后切成一个个固定大小的块。
vLLM 的内存池设计有几个要点:
- 块大小固定:每个块的大小是 block_size * token_size。block_size 通常设为 16 或 32。
- 预分配:启动时一次性分配一大块显存,避免运行时频繁申请。
- 碎片管理:因为块大小固定,所以不会产生外部碎片。内部碎片?嗯,如果最后一个块没用满,会有一些浪费,但可以接受。
注意事项:内存池的大小需要根据你的 GPU 显存和模型大小来设置。设太大,其他进程没显存用;设太小,vLLM 频繁 OOM。我一般建议留出 20% 的显存给其他用途。
下面这张图展示了 Block Manager 的整体工作流程:
这张图展示了从请求到达、查询 Block Table、分配决策到最终完成的全过程。你可以看到,分配决策的核心就是判断空闲列表里有没有可用的块。
实际应用中的一些思考
我在实际部署 vLLM 时,遇到过几个和 Block Manager 相关的性能问题:
- 块大小选择:块太小,分配次数多;块太大,内部碎片多。我一般建议 block_size 设为 16,兼顾两者。
- 前缀共享的收益:如果业务场景中 prompt 重复率高(比如聊天机器人),前缀共享能省很多显存。我见过一个案例,共享后显存占用降低了 40%。
- 内存池预热:服务启动时,可以先跑几个假请求,让内存池预热一下。这样真实请求进来时,分配延迟会更低。
总结一下:Block Manager 是 vLLM 动态批处理的基石。它通过 Block Table 管理逻辑到物理的映射,用引用计数支持前缀共享,用内存池减少分配开销。理解了它,你就理解了 vLLM 内存管理的精髓。
好了,今天就聊到这里。下一章我们会深入调度策略,看看 Block Manager 和 Scheduler 是怎么配合的。