Prhub

#51243 [KV Offload] Emit self-describing events for partial recurrent blocks

原始 PR 作者 chaunceyjiang 合并时间 2026-08-09 16:28 文件变更 5 提交数 3 评论 10 代码增减 +181 / -6

执行摘要

partial 递归块补发自描述 KV offload 事件,Mamba 组保留占位符

PR body 明确要求为 full-attention cache groups 发出包含 hash 对齐元数据的自描述 CPU-offload KV 事件,使外部 KV 事件消费者可以索引 offloaded blocks;Mamba/SSM 组继续保留 legacy 占位符负载(token_ids=[]、block_size=0、无 cache-spec 元数据)。此前 partial recurrent tail 只能以占位符呈现,外部消费者无法定位这些块的 hash 与 token 范围,这是对 KV offload 可观测性的补全。

值得精读 events.py_record_partial_tail 的 hash 对齐计算与 scheduler.py 的接入点,对自描述 KV 事件消费者有直接价值;tiering 混合状态下的元数据回填缺口可作为后续跟进方向。

讨论亮点

Review 中有三处有价值的交锋:

  1. 防护位置选择:作者主动指出 _record_partial_tail 内部没有像 record_store 那样的 sliding-window 检查——"I didn't add the same check as record_store does";orozery 以 suggestion 形式建议把检查放在 record_partial_store / record_partial_lookup 外层,最终采纳,_record_partial_tail 只保留 self_describing_enabled 守卫。

  2. boundary key 复用:orozery 提示 "We already create the boundary key a few lines above. We can save it to a list and re-use here",避免 _lookup 在命中回填时重复构造 key;作者以 boundary_keys 列表落实。

  3. tiering 混合状态边缘情况:Change72 用 Codex 发现——若一组 HIT 而另一组 MISS/RETRY,ready 组的元数据不会被 backfill,其 promotion/removal 事件仍可能带占位符负载;该问题不影响缓存与模型正确性,建议在 lookup 循环内记录每个组 HIT 或作为 follow-up。作者回应 "It feels more natural to only record self-describing metadata when all groups are HIT",非阻塞保留。

实现拆解

实现按 4 步推进:

  1. 事件元数据构造(events.py):在 OffloadingEventTracker 中新增 record_partial_storerecord_partial_lookup 两个入口,共同落到内部实现 _record_partial_tail。核心是依据 boundary_tokens 反推 chunk 起点((boundary_tokens - 1) // tokens_per_chunk 的减一处理避免边界恰在块边缘时选中下一物理块),再换算 first_hash_idx / last_hash_idx,对 req.block_hashesreq.all_token_ids 切片,构造带 parent_block_hashblock_size=tokens_per_hashkv_event_group_spec_OffloadEventMetadatarecord_partial_lookup 仅在 _pending_event_metadata 无记录时回填,避免用 lookup 请求的数据覆盖 store 时保留的原始元数据。

  2. 调度器接入(scheduler.py)_lookup 在某个 boundary 上所有组全部命中(无 MISS、无 HIT_PENDING/RETRY)时,遍历复用 boundary_keys 列表调用 record_partial_lookup_build_partial_tail_store_jobsprepare_store 实际接受的 key 上调用 record_partial_store,保证事件只描述真正落盘的块。同时从 supports_partial_tail 的条件中删除“启用 self-describing 事件则禁用 partial tail”的互斥分支(原先的 not (...) 段),使两者可同时生效。

  3. 测试配套test_events.py 新增 3 个用例——验证事件精确描述 hash 对齐物理块前缀(block_hashes=[hash4..hash6]parent=hash3token_ids=[17,29)block_size=4)、lookup 不覆盖 store 元数据、sliding-window 组仍走占位符(参数化覆盖 store/lookup 两个入口);test_scheduler.py 让 partial tail 调度器默认开启 self-describing(KVEventsConfig(enable_kv_cache_events=True, publisher="null")),并断言 full-attention 组事件带 block_size=4token_ids=[16,28)、3 个 hash,recurrent 组仍为占位符。

  4. 文档更新docs/features/kv_offloading_usage.md 扩展 self_describing_kv_events 说明,补充“partial recurrent tails 会发出从物理块起点到尾部边界的 hash 对齐部分,其余 sliding-window/SSM chunk 保留占位符”。

文件 模块 状态 重要度
vllm/distributed/kv_transfer/kv_connector/v1/offloading/events.py 事件追踪 modified 8.03
tests/v1/kv_connector/unit/offloading_connector/test_events.py 事件测试 modified 6.47
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py 调度器 modified 6.6
tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py 调度测试 modified 5.47
docs/features/kv_offloading_usage.md 功能文档 modified 1.32

关键符号

record_partial_store record_partial_lookup _record_partial_tail _lookup _build_partial_tail_store_jobs

关键源码片段

vllm/distributed/kv_transfer/kv_connector/v1/offloading/events.py core-logic

核心实现文件:新增 record_partial_store、record_partial_lookup、_record_partial_tail 三个方法,完成 partial recurrent tail 的 hash 对齐元数据构造,是自描述事件能力的关键补充。

def record_partial_store(
    self,
    req: Request,
    group_config: "GroupOffloadConfig",
    boundary_tokens: int,
    offload_key: OffloadKey,
) -> None:
    """Snapshot metadata for a newly stored partial recurrent tail."""
    # 与 record_store 保持一致:sliding-window 组不使用自描述事件,
    # 继续走 legacy 占位符负载
    if group_config.sliding_window_size_in_chunks is not None:
        return
    self._record_partial_tail(req, group_config, boundary_tokens, offload_key)
​
​
def record_partial_lookup(
    self,
    req: Request,
    group_config: "GroupOffloadConfig",
    boundary_tokens: int,
    offload_key: OffloadKey,
) -> None:
    """Backfill metadata for a partial recurrent tail lookup hit."""
    if group_config.sliding_window_size_in_chunks is not None:
        return
    # 只回填缺失项:store 已记录的元数据优先,避免 lookup 请求覆盖原始数据
    if offload_key not in self._pending_event_metadata:
        self._record_partial_tail(req, group_config, boundary_tokens, offload_key)
​
​
def _record_partial_tail(
    self,
    req: Request,
    group_config: "GroupOffloadConfig",
    boundary_tokens: int,
    offload_key: OffloadKey,
) -> None:
    """Build metadata for the valid prefix of one physical cache block.    partial recurrent tail 结束在 hash 边界上、但物理块尚未写满,
    事件只描述物理块起点到合法边界之间的 hash 与 token,不描述剩余空洞。
    """
    if not self.self_describing_enabled:
        return
​
    tokens_per_hash = group_config.tokens_per_chunk // group_config.hashes_per_chunk
    # 先减一再取整:当 boundary 恰好落在块边缘时,boundary 自身 token
    # 不应驱动选取下一个物理块,因此用 (boundary_tokens - 1) 定位 chunk 起点
    chunk_start = (
        (boundary_tokens - 1) // group_config.tokens_per_chunk
    ) * group_config.tokens_per_chunk
    first_hash_idx = chunk_start // tokens_per_hash
    last_hash_idx = boundary_tokens // tokens_per_hash
    # 以下断言依赖调用方保证 boundary 为 hash 对齐边界
    assert chunk_start < boundary_tokens
    assert boundary_tokens % tokens_per_hash == 0
    assert last_hash_idx <= len(req.block_hashes)
​
    # 与完整 chunk 不同,partial tail 只取物理块起点到合法边界之间的 hash
    maybe_block_hashes = req.block_hashes[first_hash_idx:last_hash_idx]
    block_hashes = tuple(
        block_hash for block_hash in maybe_block_hashes if block_hash is not None
    )
    assert block_hashes and len(block_hashes) == len(maybe_block_hashes)
    # 父 hash 是有效区间前一个 hash,供消费者重建 hash 链
    parent_block_hash = (
        req.block_hashes[first_hash_idx - 1] if first_hash_idx > 0 else None
    )
    assert first_hash_idx == 0 or parent_block_hash is not None
​
    lora_id = req.lora_request.adapter_id if req.lora_request is not None else None
    lora_name = req.lora_request.name if req.lora_request is not None else None
    self._pending_event_metadata[offload_key] = _OffloadEventMetadata(
        block_hashes=block_hashes,
        parent_block_hash=parent_block_hash,
        token_ids=tuple(req.all_token_ids[chunk_start:boundary_tokens]),
        block_size=tokens_per_hash, # 事件按 hash 粒度描述,块大小即 tokens_per_hash
        lora_id=lora_id,
        lora_name=lora_name,
        extra_keys=None,
        group_idx=group_config.group_idx,
        kv_cache_spec=group_config.kv_event_group_spec,
    )
vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py core-logic

调度器接入点:在 _lookup 命中回填与 _build_partial_tail_store_jobs 落盘路径调用事件记录,并移除 supports_partial_tail 对自描述事件的禁用条件。

def _lookup(self, req_status: RequestOffloadState) -> int | None:
    complete_hit = self._lookup_complete_chunks(req_status)
    req_status.partial_tail_boundary = None
    if complete_hit is None or not self.config.supports_partial_tail:
        return complete_hit
​
    local_tokens = req_status.num_locally_computed_tokens
    complete_boundary = local_tokens + complete_hit
    tokens_per_hash = self.config.tokens_per_hash
    block_end = complete_boundary + self._partial_tail_block_size
    # 只回溯一个物理块跨度内的 hash 边界
    max_boundary = round_down(
        min(req_status.req.num_prompt_tokens - 1, block_end - 1), tokens_per_hash
    )
    if max_boundary <= complete_boundary:
        return complete_hit
​
    pending = False
    # 从最远的合法边界向前搜索,直到找到所有组都命中的边界
    for boundary in range(max_boundary, complete_boundary, -tokens_per_hash):
        boundary_pending = False
        boundary_missed = False
        boundary_keys = []
        for group_config in self.config.kv_group_configs:
            key = self._make_boundary_key(
                req_status.req, group_config.group_idx, boundary
            )
            boundary_keys.append(key)
            result = self.manager.lookup(key, req_status.req_context)
            if result is LookupResult.MISS:
                boundary_missed = True
                break
            if result in (LookupResult.HIT_PENDING, LookupResult.RETRY):
                boundary_pending = True
​
        pending |= boundary_pending
        if not boundary_missed and not boundary_pending:
            # 只有所有组都在该边界命中时才回填自描述元数据,
            # 保证后续 BlockStored/BlockRemoved 事件携带 hash 与 token 信息
            for group_config, key in zip(
                self.config.kv_group_configs, boundary_keys
            ):
                self._events_tracker.record_partial_lookup(
                    req_status.req, group_config, boundary, key
                )
            req_status.partial_tail_boundary = boundary
            return boundary - local_tokens
​
    if pending and complete_hit == 0:
        return None
    return complete_hit

评论区精华

sliding-window 防护应放在哪一层 正确性

作者指出 _record_partial_tail 内没有像 record_store 那样的 sliding-window 检查,orozery 以 suggestion 建议把检查放到 record_partial_store 与 record_partial_lookup 外层。

结论:在外层两个入口添加 sliding_window_size_in_chunks 检查,内部保留 self_describing_enabled 守卫。 · 已解决

lookup 命中时复用已构造的 boundary key 性能

orozery 指出 boundary key 已在上方构造,应保存到列表复用,避免重复调用 _make_boundary_key。

结论:作者用 boundary_keys 列表落实,命名循环内收集、命中后直接遍历。 · 已解决

tiering 混合 HIT/MISS 时 ready 组元数据不回填 设计

Change72 用 Codex 发现:一组 HIT 而另一组 MISS/RETRY 时,ready 组的元数据不会 backfill,promotion/removal 事件仍可能用占位符;不影响缓存与模型正确性。

结论:作者认为只在所有组都 HIT 时记录自描述元数据更自然,作为非阻塞项保留,未在本次修复。 · unresolved

风险与影响

主要风险集中在三点:

  1. 断言脆性_record_partial_tail 内置三条 assert(boundary_tokens % tokens_per_hash == 0last_hash_idx <= len(req.block_hashes)first_hash_idx == 0 or parent_block_hash is not None),依赖调用方保证 hash 边界对齐。当前 supports_partial_tail 约束了 blocks_per_chunk == 1 与单一块大小,未来若放开这些约束(如 EAGLE/DCP 支持、多块大小混合),可能运行期触发断言。

  2. tiering 边缘情况:Change72 指出的混合 HIT/MISS 场景下,ready 组元数据不回填,相关 promotion/removal 事件仍是占位符——不影响缓存与模型正确性,但外部消费者可能暂时看不到 ready 组的自描述事件。

  3. 热路径开销_lookup 是每个请求的关键路径,新增 per-boundary 的 key 收集、zip 遍历与 tracker 写入(dict 写入 + 切片)。均为 CPU 侧小操作,但极端配置下 boundary 循环变长时存在放大效应。

对使用 OffloadingConnector + self-describing KV events 的部署,外部事件消费者现在能索引完整注意力组的 partial tail 块(hash 列表、token 范围、parent hash、block_size);Mamba/SSM 组行为不变,保持占位符,不破坏现有消费者。调度器测试默认配置翻转(启用 self-describing)表明该组合已是第一等支持场景。改动集中在 offloading connector 内部,不影响其他 connector 与核心调度路径。

核心路径变更 边界条件依赖断言 已知边缘情况遗留

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论