Prhub

#48531 [Perf][KVConnector][Mooncake] Vectorize prepare_value on the KV load path

原始 PR 作者 GirasoleY 合并时间 2026-07-23 18:12 文件变更 4 提交数 3 评论 3 代码增减 +211 / -21

执行摘要

Mooncake prepare_value 向量化加速 4.4 倍

对于 289K tokens 的请求,prepare_value 需要处理约 2258 个 chunk,每个 chunk 遍历约 100 个 cache 区域,纯 Python 整数运算导致每请求约 35 ms 的 GIL 锁定时间。结合 PR#45971 的 receive-thread 池,四线程并发时 wall time 从 174 ms 膨胀到 208 ms,成为长请求性能瓶颈。

推荐阅读,尤其是对性能敏感的后端优化:展示了如何利用 NumPy 将逐元素循环向量化以获得数量级提升,同时通过精心设计的测试确保正确性。

讨论亮点

Reviewer ivanium 在 worker.py 行内评论 "can we vectorize this too?",建议也对 store 发送路径进行向量化。作者在随后的 commit 810e6339 中添加了按组批处理逻辑。最终 reviewer 没有进一步意见并批准了 PR。

实现拆解

  1. 新增 prepare_values 向量化接口data.py):基于 NumPy 数组同时计算多个 token 区间的地址和大小,返回 list[list[int]],替代逐 chunk 循环。
  2. 改造 prepare_value 为包装函数data.py):使原有单 chunk 接口内部调用 prepare_values,保持向后兼容。
  3. 优化 store 发送路径worker.py):在 _handle_request 中将缺失 chunk 按 group 分组,一次性调用 prepare_values,避免每个 chunk 单独调用的 Python 开销。
  4. 优化 load 路径worker.py):在 _handle_request 中同样将每个 group 的 chunk 收集后调用 prepare_values,减少循环重复。
  5. 新增聚焦测试test_mooncake_store_prepare_values.py):包含与参考实现的一致性测试、单 chunk 回归、空输入和非法对齐检查。
  6. 补充 worker 单元测试test_mooncake_store_worker.py):验证 store 发送路径按组调用 prepare_values 的正确性,并确认不再调用标量函数。
文件 模块 状态 重要度
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/data.py 数据层 modified 7.4
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py Worker 模块 modified 6.31
tests/v1/kv_connector/unit/test_mooncake_store_prepare_values.py 测试 added 7.42
tests/v1/kv_connector/unit/test_mooncake_store_worker.py 测试 modified 5.67

关键符号

ChunkedTokenDatabase.prepare_value ChunkedTokenDatabase.prepare_values StoreSendingThread._handle_request _reference_prepare_value test_prepare_values_matches_reference test_store_sending_thread_prepares_missing_chunks_once_per_group

关键源码片段

vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/data.py core-logic

核心变更,新增 `prepare_values` 向量化方法,重写 `prepare_value`。

def prepare_values(
    self,
    chunks: Sequence[tuple[int, int]],
    block_ids: list[int],
) -> tuple[list[list[int]], list[list[int]], list[int]]:
    """Compute memory addresses and sizes for multiple token ranges.
    Returns (addr_lists, size_lists, chunk_block_ids), one entry per chunk.
    使用 NumPy 向量化计算,避免逐 chunk 循环的开销。
    """
    if not chunks:
        return [], [], []
    base = np.asarray(self.kv_caches_base_addr, dtype=np.int64)
    length = len(self.block_len)
    # 扩展 block_len 以匹配 cache 区域数量,处理循环索引
    blen = np.asarray(
        [self.block_len[i % length] for i in range(base.shape[0])],
        dtype=np.int64,
    )
    n = len(chunks)
    starts = np.fromiter((c[0] for c in chunks), dtype=np.int64, count=n)
    spans = np.fromiter((c[1] for c in chunks), dtype=np.int64, count=n) - starts
    assert not (spans % self.block_size).any() # 所有跨度必须是 block_size 整数倍
    # 每个 chunk 的 block_id 由起始 token 索引映射得到
    bids = np.fromiter(
        (block_ids[i] for i in (starts // self.block_size).tolist()),
        dtype=np.int64, count=n,
    )
    # 广播计算所有地址和大小:(n_chunks, n_regions)
    addrs = base[None, :] + bids[:, None] * blen[None, :]
    sizes = blen[None, :] * (spans // self.block_size)[:, None]
    # 转换为 Python int 嵌套列表供下游 RDMA 绑定使用
    return addrs.tolist(), sizes.tolist(), bids.tolist()
vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/worker.py core-logic

修改 `_handle_request` 方法,在 store 发送和 load 路径中改用向量化批处理。

# 在 store 发送路径中,按 group 收集缺失 chunks
chunks_per_group: list[list[tuple[int, int]]] = [
    [] for _ in self.token_databases
]
for start, end, g_idx in zip(starts, ends, group_indices, strict=True):
    chunks_per_group[g_idx].append((start, end))
for g_idx, chunks in enumerate(chunks_per_group):
    if not chunks:
        continue
    db = self.token_databases[g_idx]
    group_addrs, group_sizes, _ = db.prepare_values(
        chunks, block_ids_per_group[g_idx]
    )
    addrs.extend(group_addrs)
    sizes.extend(group_sizes)
# 之后仍然循环构建 kv 事件,但地址计算已提前完成
tests/v1/kv_connector/unit/test_mooncake_store_prepare_values.py test-coverage

新增文件,全面测试 `prepare_values` 的正确性。

def _reference_prepare_value(
    db: ChunkedTokenDatabase, start: int, end: int, block_ids: list[int]
) -> tuple[list[int], list[int], int]:
    """Compute a token range with the original scalar implementation.
    作为参考实现,逐 region 循环计算。
    """
    addr_list = []
    size_list = []
    block_id = block_ids[start // db.block_size]
    length = len(db.block_len)
    for index, base_addr in enumerate(db.kv_caches_base_addr):
        addr = base_addr + block_id * db.block_len[index % length]
        assert (end - start) % db.block_size == 0
        size = db.block_len[index % length] * cdiv(end - start, db.block_size)
        addr_list.append(addr)
        size_list.append(size)
    return addr_list, size_list, block_id
​
​
@pytest.mark.parametrize("num_regions,num_block_lens", [(96, 96), (96, 2), (1, 1)])
def test_prepare_values_matches_reference(num_regions: int, num_block_lens: int):
    db = _make_db(num_regions, num_block_lens)
    rng = random.Random(0)
    n_blocks = 300
    block_ids = [rng.randrange(0, 1 << 20) for _ in range(n_blocks)]
    chunks = []
    b = 0
    while b < n_blocks - 4:
        span = rng.choice([1, 1, 1, 2, 4])
        chunks.append((b * BLOCK_SIZE, (b + span) * BLOCK_SIZE))
        b += span + rng.choice([0, 1])
    addrs, sizes, bids = db.prepare_values(chunks, block_ids)
    assert len(addrs) == len(sizes) == len(bids) == len(chunks)
    for (start, end), addr, size, bid in zip(chunks, addrs, sizes, bids):
        ref_addr, ref_size, ref_bid = _reference_prepare_value(
            db, start, end, block_ids
        )
        assert addr == ref_addr
        assert size == ref_size
        assert bid == ref_bid
        # Native bindings require Python ints rather than numpy scalars.
        assert all(type(a) is int for a in addr)
        assert type(bid) is int

评论区精华

worker.py 发送路径是否也应向量化 性能

ivanium 在 worker.py 文件评论中提出 "can we vectorize this too?",建议对 store 发送路径中每个缺失 chunk 单独调用 `prepare_value` 的部分也进行向量化。

结论:作者在 commit 810e6339 中添加了按组批处理逻辑,将 chunks 按 group 收集后统一调用 `prepare_values`。reviewer 随后批准了 PR。 · 已解决

风险与影响

引入了 NumPy 依赖(已在文件级导入),但 vLLM 通常已有 numpy。主要风险是向量化结果与原始标量计算是否完全一致——测试通过随机 chunk 和多参数组合验证了正确性。此外,prepare_values 返回 list[list[int]],下游代码需要处理嵌套列表,已检查 worker.py 中的 extend 拼接正确。

影响范围限 Mooncake KV connector 模块。对于使用 KV 传输功能的用户,长文本请求的性能将显著提升(约 4.4 倍)。无功能变更,无 API 破坏。store 发送路径的批处理不改变外部可见行为。

核心路径变更 向量化精度验证 新增 NumPy 导入

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论