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 的做法是:先检查引用计数。

  1. 如果引用计数为 0,说明这个块没人用了,可以回收。
  2. 回收时,把物理块 ID 放回空闲列表。
  3. 下次分配时,优先从空闲列表里拿。

这里有个有意思的点:前缀共享。比如两个请求的 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 Manager 工作流程 请求到达 Block Table 查询 分配决策 有空闲块? 从空闲列表取 从内存池新分配 更新 Block Table 和引用计数 分配完成

这张图展示了从请求到达、查询 Block Table、分配决策到最终完成的全过程。你可以看到,分配决策的核心就是判断空闲列表里有没有可用的块。

实际应用中的一些思考

我在实际部署 vLLM 时,遇到过几个和 Block Manager 相关的性能问题:

  • 块大小选择:块太小,分配次数多;块太大,内部碎片多。我一般建议 block_size 设为 16,兼顾两者。
  • 前缀共享的收益:如果业务场景中 prompt 重复率高(比如聊天机器人),前缀共享能省很多显存。我见过一个案例,共享后显存占用降低了 40%。
  • 内存池预热:服务启动时,可以先跑几个假请求,让内存池预热一下。这样真实请求进来时,分配延迟会更低。

总结一下:Block Manager 是 vLLM 动态批处理的基石。它通过 Block Table 管理逻辑到物理的映射,用引用计数支持前缀共享,用内存池减少分配开销。理解了它,你就理解了 vLLM 内存管理的精髓。

好了,今天就聊到这里。下一章我们会深入调度策略,看看 Block Manager 和 Scheduler 是怎么配合的。

专注资料整理