01

V2 设计动机

Block Manager V1 将所有逻辑集中在一个大类中(BlockSpaceManagerV1),随着 prefix caching、sliding window、CoW 等高级特性的加入,代码变得越来越复杂。V2 通过清晰的抽象分层来解决这个问题:

  • Block:单个内存块的抽象,封装 token 存储和 content hash
  • BlockAllocator:块分配器接口,管理块的分配和释放
  • BlockTable:序列级别的块管理,封装 token 到块的映射
  • DeviceAwareBlockAllocator:跨设备(GPU/CPU)的块分配
当前状态
V2 源码注释明确指出这是"partial implementation",sliding window 和 swap 功能尚未完成(can_swap_in 始终返回 AllocStatus.LATERswap_out 直接 raise NotImplementedError)。但其 prefix caching 和 BlockTable 抽象已经完整可用。
02

架构层级总览

graph TD BSM["BlockSpaceManagerV2
scheduler 调用的顶层接口"] BT["BlockTable
每个序列一个,管理 token→block 映射"] CGA["CpuGpuBlockAllocator
跨设备块分配器"] NBA["NaiveBlockAllocator
基础分配策略"] PCA["PrefixCachingBlockAllocator
前缀缓存分配策略"] NB["NaiveBlock
简单块实现"] PCB["PrefixCachingBlock
带 content hash 的块"] BSM -->|"持有 Dict[SeqId, BlockTable]"| BT BSM -->|"持有"| CGA BT -->|"通过 allocator 分配块"| CGA CGA -->|"GPU/CPU 各一个"| NBA CGA -->|"或"| PCA NBA -->|"创建"| NB PCA -->|"创建"| PCB

核心设计理念:BlockTable 负责序列粒度的块管理,BlockAllocator 负责设备粒度的内存管理。BlockSpaceManagerV2 只是将两者组合在一起,提供 Scheduler 需要的接口。

03

Block 接口

Blockblock/interfaces.py L9-89)是所有块的抽象基类:

block/interfaces.py — Block ABC L9-89
class Block(ABC):

    @abstractmethod
    def append_token_ids(self, token_ids: List[int]) -> None:
        pass

    @property
    @abstractmethod
    def block_id(self) -> Optional[int]:
        pass

    @property
    @abstractmethod
    def token_ids(self) -> List[int]:
        pass

    @property
    @abstractmethod
    def num_empty_slots(self) -> int:
        pass

    @property
    @abstractmethod
    def is_full(self) -> bool:
        pass

    @property
    @abstractmethod
    def prev_block(self) -> Optional["Block"]:
        pass

    @property
    @abstractmethod
    def computed(self) -> bool:
        raise NotImplementedError

    @property
    @abstractmethod
    def last_accessed(self) -> float:
        raise NotImplementedError

    @property
    @abstractmethod
    def content_hash(self) -> Optional[int]:
        """Return the content-based hash of the current block, or None if it is
        not yet defined or not supported. For the content-based hash to be
        defined, the current block must be full."""
        return None

关键属性:

属性作用
block_id物理块 ID,对应实际的 GPU/CPU 内存位置
token_ids存储在此块中的 token ID 列表
prev_block前一个块的引用,形成链表结构(用于 prefix hash)
content_hash基于内容的哈希值,只有满块才有定义
computed是否已计算过 KV Cache
last_accessed最近访问时间(用于 LRU 淘汰)
04

BlockTable 抽象

BlockTableblock/block_table.py)为每个序列维护一组有序的 Block,是 V2 的核心设计:

block/block_table.py — BlockTable 核心方法 L7-88
class BlockTable:
    """A class to manage blocks for a specific sequence.

    The BlockTable maps a sequence of tokens to a list of blocks, where each
    block represents a contiguous memory allocation for a portion of the
    sequence.
    """

    def __init__(self, block_size, block_allocator, _blocks=None):
        self._block_size = block_size
        self._allocator = block_allocator
        self._blocks: List[Block] = _blocks or []
        self._num_full_slots = len(self._get_all_token_ids())

    @staticmethod
    def get_num_required_blocks(token_ids, block_size) -> int:
        return cdiv(len(token_ids), block_size)

    def allocate(self, token_ids, device=Device.GPU):
        """首次分配:为整个 token 序列分配所有需要的 block"""
        self._blocks = self._allocate_blocks_for_token_ids(
            prev_block=None, token_ids=token_ids, device=device)
        self._num_full_slots = len(token_ids)

    def append_token_ids(self, token_ids, num_lookahead_slots=0):
        """追加 token:用于 decode 阶段逐步生成"""
        self.ensure_num_empty_slots(len(token_ids) + num_lookahead_slots)
        blocks = self._blocks[self._num_full_slots // self._block_size:]
        token_blocks = self._chunk_token_blocks_for_append(token_ids)
        for block, token_block in zip(blocks, token_blocks):
            block.append_token_ids(token_block)
        self._num_full_slots += len(token_ids)

BlockTable 管理三个核心操作:

  1. allocate:初始化时为完整 prompt 分配块。通过 _allocate_blocks_for_token_ids 将 token 切分为 block_size 大小的 chunk,每个 chunk 分配一个 block
  2. append_token_ids:decode 阶段追加新 token。先通过 ensure_num_empty_slots 确保有足够空间,然后写入
  3. fork:beam search 时复制 block table,新表共享底层 block(CoW 语义)
block/block_table.py — ensure_num_empty_slots L122-149
def ensure_num_empty_slots(self, num_empty_slots: int) -> None:
    if self._num_empty_slots >= num_empty_slots:
        return

    slots_to_allocate = num_empty_slots - self._num_empty_slots
    blocks_to_allocate = cdiv(slots_to_allocate, self._block_size)

    for _ in range(blocks_to_allocate):
        self._blocks.append(
            self._allocator.allocate_mutable(
                prev_block=self._blocks[-1],
                device=Device.GPU))
immutable vs mutable block
  • immutable block:已满的块,token 内容固定,可以计算 content hash。在 prefix caching 中用于匹配已缓存的 KV Cache
  • mutable block:未满的块,还在被追加 token。当它被填满时,自动变为 immutable
05

CpuGpuBlockAllocator

CpuGpuBlockAllocatorblock/cpu_gpu_block_allocator.py)是设备感知的分配器,封装了 GPU 和 CPU 两个独立的 BlockAllocator

cpu_gpu_block_allocator.py — create 工厂方法 L23-91
@staticmethod
def create(allocator_type, num_gpu_blocks, num_cpu_blocks, block_size):
    block_ids = list(range(num_gpu_blocks + num_cpu_blocks))
    gpu_block_ids = block_ids[:num_gpu_blocks]
    cpu_block_ids = block_ids[num_gpu_blocks:]

    if allocator_type == "naive":
        gpu_allocator = NaiveBlockAllocator(
            create_block=NaiveBlock, num_blocks=num_gpu_blocks,
            block_size=block_size, block_ids=gpu_block_ids)
        cpu_allocator = NaiveBlockAllocator(
            create_block=NaiveBlock, num_blocks=num_cpu_blocks,
            block_size=block_size, block_ids=cpu_block_ids)
    elif allocator_type == "prefix_caching":
        gpu_allocator = PrefixCachingBlockAllocator(
            num_blocks=num_gpu_blocks, block_size=block_size,
            block_ids=gpu_block_ids)
        cpu_allocator = PrefixCachingBlockAllocator(
            num_blocks=num_cpu_blocks, block_size=block_size,
            block_ids=cpu_block_ids)

    return CpuGpuBlockAllocator(
        cpu_block_allocator=cpu_allocator,
        gpu_block_allocator=gpu_allocator)

关键设计:

  • Block ID 全局唯一:GPU 和 CPU 的 block ID 是连续分配的(GPU: 0~N-1, CPU: N~N+M-1),通过 _block_ids_to_allocator 映射表可以根据 block ID 找到对应的设备分配器
  • 策略可配置:通过 allocator_type 选择 "naive"(基础分配)或 "prefix_caching"(前缀缓存),GPU 和 CPU 使用相同的策略
  • CoW 在 GPU 侧clear_copy_on_writes() 只在 GPU 分配器上调用
06

NaiveBlock 实现

NaiveBlockblock/naive_block.py)是最基础的 Block 实现,使用简单的 free list 管理块:

NaiveBlockAllocator 维护一个空闲块 ID 池。分配时从池中弹出一个 ID,释放时将 ID 推回池中。CoW 通过引用计数实现:

  • allocate_mutable:从 free list 取一个块,创建新的 NaiveBlock
  • allocate_immutable:与 mutable 相同,但块一开始就是满的
  • fork:通过 _cow_tracker.increase_refcount() 增加引用计数,多个序列共享同一个物理块
  • free:减少引用计数,归零时才真正释放
NaiveBlock 不支持 content hash
NaiveBlock.content_hash 始终返回 None,因此无法用于 prefix caching。如果需要 prefix caching,必须使用 PrefixCachingBlock
07

PrefixCachingBlock

PrefixCachingBlockAllocatorblock/prefix_caching_block.py)在 NaiveBlock 基础上增加了基于内容的块去重能力。

Content Hash 计算

每个满块(is_full=True)会计算一个 content hash:

content_hash = hash((prev_block_hash, tuple(token_ids)))

hash 的输入包括前一个块的 hash当前块的 token ID。这意味着两个块即使 token 内容相同,如果前缀不同,hash 也不同。这正是"前缀缓存"语义所需要的。

Prefix Caching 工作原理

graph TD A["请求到达
token_ids = [1,2,3,4,5,6,7,8]"] --> B["切分为 block_size=4 的块"] B --> C["Block 0: [1,2,3,4]
hash = H0"] B --> D["Block 1: [5,6,7,8]
hash = H1"] C --> E{"H0 在缓存中?"} E -->|"是"| F["复用已有物理块
跳过 prefill"] E -->|"否"| G["分配新物理块
正常 prefill"] D --> H{"H1 在缓存中?"} H -->|"是"| I["复用已有物理块"] H -->|"否"| J["分配新物理块"]

allocate_immutable 的核心逻辑:

  1. 计算 content hash
  2. 查找 _cached_blocks 映射表
  3. 如果找到匹配的已缓存块 → 增加引用计数,直接返回(零分配
  4. 如果没找到 → 分配新块,加入缓存映射表

已计算块的跳过

get_common_computed_block_idsblock_manager_v2.py L234-250)找出所有序列共享的、已计算的前缀块。这些块的 KV Cache 已经在 GPU 上,prefill 时可以直接跳过,大幅减少计算量。

Prefix Caching 的最大价值
当多个请求共享相同的 system prompt(如 ChatGPT 的系统指令),prefix caching 可以让这些共享前缀只计算一次。后续请求直接复用已缓存的 KV Cache 块。对于 4096 token 的 system prompt,可以节省数秒的 TTFT。
08

BlockSpaceManagerV2

BlockSpaceManagerV2block_manager_v2.py L17-278)是 V2 的顶层管理器,提供 Scheduler 所需的标准接口:

block_manager_v2.py — 初始化 L58-90
def __init__(self, block_size, num_gpu_blocks, num_cpu_blocks,
             watermark=0.01, sliding_window=None, enable_caching=False):
    self.block_size = block_size
    self.num_total_gpu_blocks = num_gpu_blocks
    self.num_total_cpu_blocks = num_cpu_blocks
    self.watermark_blocks = int(watermark * num_gpu_blocks)
    self.enable_caching = enable_caching

    self.block_allocator = CpuGpuBlockAllocator.create(
        allocator_type="prefix_caching" if enable_caching else "naive",
        num_gpu_blocks=num_gpu_blocks,
        num_cpu_blocks=num_cpu_blocks,
        block_size=block_size,
    )

    self.block_tables: Dict[SeqId, BlockTable] = {}

关键接口实现:

can_allocate / allocate

can_allocate 使用 watermark 机制(保留 1% 的块防止频繁淘汰),通过 BlockTable.get_num_required_blocks 计算最坏情况下需要的块数。allocate 为第一个序列创建 BlockTable 并分配块,后续序列通过 block_table.fork() 共享。

can_append_slots / append_slots

can_append_slots 使用最坏情况估计:假设每个被触及的块都需要新分配(考虑 CoW)。实际分配在 append_slots 中通过 block_table.append_token_ids 执行。

block_manager_v2.py — can_append_slots L149-176
def can_append_slots(self, seq_group, num_lookahead_slots) -> bool:
    num_touched_blocks = 0
    for seq in seq_group.get_seqs(status=SequenceStatus.RUNNING):
        block_table = self.block_tables[seq.seq_id]
        num_touched_blocks += (
            block_table.get_num_blocks_touched_by_append_slots(
                token_ids=block_table.get_unseen_token_ids(
                    seq.get_token_ids()),
                num_lookahead_slots=num_lookahead_slots,
            ))

    num_free_gpu_blocks = self.block_allocator.get_num_free_blocks(
        Device.GPU)
    return num_touched_blocks <= num_free_gpu_blocks

Lookahead Slots

V2 引入了 lookahead slots 概念——为每个序列预留额外的 KV Cache 空间,供 speculative decoding 使用。这些 slot 在 append_token_ids 时一并分配,但内容未定义。

09

V1 vs V2 对比

维度Block Manager V1Block Manager V2
架构 单体类,所有逻辑集中 分层架构:Block → BlockAllocator → BlockTable → Manager
块抽象 PhysicalTokenBlock + LogicalTokenBlock 统一 Block ABC,immutable/mutable 语义
Prefix Caching 通过 evictor 实现 LRU 淘汰 content hash 驱动,allocate_immutable 自动去重
Swap 完整实现 未实现(NotImplementedError)
Sliding Window 完整实现 未实现(assert)
CoW 引用计数 + copy-on-write 映射 引用计数 + CowTracker
Lookahead Slots 不支持 原生支持(speculative decoding)
配置 block_manager_version="v1" block_manager_version="v2" + enable_prefix_caching=True
选择建议
  • 如果需要 swap 功能(GPU 显存紧张场景),目前只能用 V1
  • 如果需要 prefix caching(大量共享 system prompt 场景),V2 的实现更优雅
  • 如果需要 speculative decoding(lookahead slots),需要 V2
  • V1 是经过生产验证的稳定版本,V2 是面向未来的架构重构