Prhub

#49612 [KV Connector] Support NIXL heterogeneous P/D block sizes for hybrid models

原始 PR 作者 njhill 合并时间 2026-07-28 21:45 文件变更 7 提交数 11 评论 1 代码增减 +1014 / -111

执行摘要

支持 NIXL 异构 P/D 块大小,消除混合模型限制

修复 Issue #41037:混合模型在异构 P/D 场景下因 block_size_ratio != 1 断言失败。PR body 指出“混合 (mamba) 模型此前整体上断言排除异构块大小,但正是这些模型的 P/D 块大小最容易分化,因为 mamba 填充的 attention 块大小随 TP sharding 变化”。

该 PR 值得精读,特别是 _map_block_ids_for_block_size_ratio 的设计和描述符几何不变量测试框架。关注 head-sharded 拒绝的 future 工作方向。

实现拆解

  1. 描述符 ID 计算调整base_worker.py_compute_desc_ids):当 block_size_ratio 不为 None 时,Attention 组的 num_blocks 按 ratio 缩放 (kernel 粒度),SSM 组保持逻辑块数量不变(状态块不可子分割)。
  2. 块 ID 映射新增base_worker.py 新增 _map_block_ids_for_block_size_ratio):统一 pull 和 push 路径中的块 ID 扩展逻辑。将 Attention 组本地块 ID 乘以 ratio 并裁剪到远程覆盖范围,SSM 组保持 1:1 映射。移除了 pull_worker.pypush_worker.py 中重复的内联代码。
  3. 接收后处理置零base_worker.pypost_process_device_kv_on_receive 及相关路径):当 kernel 页面相等但逻辑块大小不同时,传输只覆盖部分逻辑块。接收后处理将注意力层视图中未传输的尾部子块以及该块之后的区域置零。使用 cached_property (_attention_kv_caches) 快速过滤出注意力层缓存。
  4. 异构 TP 多读分裂适配pull_worker.py_read_blocks_for_req, push_worker.py_xfer_blocks_for_reqbase_worker.py_build_local_splits_from_plan):src_xfer_handles_by_tp_ratio 的 key 从 tp_ratio 扩展为 (tp_ratio, remote_block_size),以支持 block_size_ratio 下不同远程块大小的分裂句柄。_build_local_splits_from_plan 新增 block_size_ratio 参数,并在 head-sharded Attention 与 ratio>1 组合时 assert 拒绝。
  5. 测试覆盖:新增 test_nixl_desc_geometry.py (619 行),通过 _RecordingNixl mock 夹具验证描述符几何不变量;test_nixl_connector_hma.py 增加 3 个测试函数验证异构块大小下的描述符 ID、映射和置零;test_tp_mapping.py 增加两个测试验证异构块大小分裂和 head-sharded 拒绝。
文件 模块 状态 重要度
vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_worker.py 网络传输 modified 8.62
tests/v1/kv_connector/unit/test_nixl_desc_geometry.py 测试夹具 added 8.05
tests/v1/kv_connector/unit/test_nixl_connector_hma.py HMA 测试 modified 7.25
vllm/distributed/kv_transfer/kv_connector/v1/nixl/pull_worker.py Pull Worker modified 6.49
vllm/distributed/kv_transfer/kv_connector/v1/nixl/push_worker.py Push Worker modified 6.34
tests/v1/kv_connector/unit/test_tp_mapping.py TP 映射测试 modified 6.25
tests/v1/kv_connector/unit/test_nixl_connector.py 连接器测试 modified 4.27

关键符号

_map_block_ids_for_block_size_ratio _attention_kv_caches _build_mamba_local _compute_desc_ids _build_local_splits_from_plan _read_blocks _xfer_blocks get_mapped_blocks

关键源码片段

vllm/distributed/kv_transfer/kv_connector/v1/nixl/base_worker.py core-logic

核心实现文件,包含 _map_block_ids_for_block_size_ratio、_attention_kv_caches、_compute_desc_ids 等主要逻辑变更,+207/-64。

# _compute_desc_ids 的 head 版本关键片段:
# - 当 block_size_ratio != None 时,num_blocks 按 ratio 缩放(kernel 粒度)
# - SSM 描述符的 stride 保持逻辑块数量(不进行 ratio 扩展)
def _compute_desc_ids(
    self,
    block_ids: BlockIds,
    dst_num_blocks: int,
    block_size_ratio: float | None,
    physical_blocks_per_logical: int,
) -> np.ndarray:
    num_ssm_regions = 0
    if self._has_mamba:
        assert self._conv_decomp is not None
        ssm_regions_per_layer = len(self._conv_decomp.local_conv_offsets) + 1
        num_ssm_regions = len(self.block_len_per_layer) * ssm_regions_per_layer
​
    num_blocks = dst_num_blocks
    if block_size_ratio is not None:
        num_blocks = int(num_blocks * block_size_ratio) # 扩展 Attention 描述符到远程粒度
    num_fa_descs = self.num_regions * num_blocks
​
    # 纯 Attention 快速路径
    if num_ssm_regions == 0:
        block_arr = np.concatenate(block_ids)[None, :]
        region_ids = np.arange(self.num_regions)[:, None]
        return (region_ids * num_blocks + block_arr).flatten()
​
    # 混合路径:每组按规格类型使用不同 stride
    # FA 描述符 stride = num_blocks(扩展后),SSM 描述符 stride = dst_num_blocks(逻辑块,不扩展)
    logical_blocks = dst_num_blocks // physical_blocks_per_logical
    all_descs: list[np.ndarray] = []
    for i, group in enumerate(block_ids):
        group_arr = np.asarray(group)
        if _is_attention_spec(self._group_spec_types[i]):
            fa_region_ids = np.arange(self.num_regions)[:, None]
            # FA 使用扩展后的 num_blocks 作为 stride
            all_descs.append(fa_region_ids * num_blocks + group_arr)
        else:
            # SSM 使用逻辑块数量作为 stride
            all_descs.append(self.num_regions * num_blocks + group_arr * physical_blocks_per_logical)
    return np.concatenate(all_descs)
tests/v1/kv_connector/unit/test_nixl_desc_geometry.py test-coverage

新增 619 行测试,通过 _RecordingNixl mock 验证混合 MLA+SSM 模型在异构块大小下的描述符几何不变量,确保传入的字节范围不越界。

class _RecordingNixl:
    """最小 NIXL 包装器模拟,记录描述符列表和准备好的传输,
    以便测试可以将 desc id 解析为字节范围。"""
​
    def __init__(self, *args, **kwargs):
        # 记录 descriptor list handle -> descriptor 数组的映射
        self.dlists: dict[int, np.ndarray] = {}
        # 记录传输操作 (op, local_handle, local_ids, remote_handle, remote_ids)
        self.xfers: list[tuple] = []
        self._next_handle = 1
​
    def get_reg_descs(self, caches_data, mem_type):
        return caches_data # 不做额外处理
​
    def register_memory(self, descs, backends=None):
        pass
​
    def deregister_memory(self, descs):
        pass
​
    def get_agent_metadata(self):
        return b"agent-meta"
​
    def get_xfer_descs(self, blocks_data, mem_type):
        return blocks_data
​
    def prep_xfer_dlist(self, agent, descs):
        # 记录 descriptor list,形状为 (n, 3) 的 uint64 数组
        handle = self._next_handle
        self._next_handle += 1
        self.dlists[handle] = np.asarray(descs, dtype=np.uint64).reshape(-1, 3)
        return handle
​
    def make_prepped_xfer(self, op, local_handle, local_ids, remote_handle, remote_ids, notif_msg=None):
        handle = self._next_handle
        self._next_handle += 1
        self.xfers.append(
            (op, local_handle, np.asarray(local_ids), remote_handle, np.asarray(remote_ids))
        )
        return handle
​
    # ... 其他方法均为 pass 或返回模拟值def _make_mla_hybrid_worker(local_block_size, kernel_block_size, num_logical_blocks):
    """构建一个真实的 pull worker,使用混合 MLA + 2xKDA HMA 布局。
    用于验证描述符几何不变量。"""
    # 构建 KVCacheSpec: MLAAttentionSpec + MambaSpec
    mla_spec = MLAAttentionSpec(
        block_size=local_block_size,
        num_kv_heads=1,
        head_size=6,
        dtype=torch.float16,
    )
    unified_page = mla_spec.page_size_bytes
    kda_spec = MambaSpec(
        block_size=local_block_size,
        shapes=((8, 3), (1, 4, 4)),
        dtypes=(torch.float16, torch.float32),
        page_size_padded=unified_page,
        mamba_type=MambaAttentionBackendEnum.GDN_ATTN,
    )
    kv_cache_config = KVCacheConfig(
        num_blocks=num_logical_blocks,
        kv_cache_tensors=[
            KVCacheTensor(size=num_logical_blocks * unified_page, shared_by=[...]) for _ in range(2)
        ],
        kv_cache_groups=[
            KVCacheGroupSpec(["mla.0", "mla.1"], mla_spec),
            KVCacheGroupSpec(["kda_a.0", "kda_a.1"], kda_spec),
            KVCacheGroupSpec(["kda_b.0", "kda_b.1"], kda_spec),
        ],
    )
    # 注入 VllmConfig 并创建 NixlConnectorWorker
    vllm_config = create_vllm_config(block_size=local_block_size)
    # ... 通过 patch 替换 NixlWrapper 为 _RecordingNixl 以记录操作
    return worker

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 核心路径变更_compute_desc_ids_build_local_splits_from_plan 是 NIXL 传输的关键路径,修改后可能影响所有使用 NIXL 连接的模型,尤其是纯 Attention 模型(非混合模型)的块大小 ratio 仍为 1,逻辑不变。
  2. 置零逻辑新加入:接收后处理中新增的尾部置零可能引入性能开销,且对非混合模型可能误置零,但代码仅当 block_size_ratio > 1 时触发。
  3. head-sharded 拒绝:对 head-sharded 读取 + ratio 不匹配直接断言失败,可能影响未来支持这一组合的扩展。
  4. 测试覆盖完整:新测试文件覆盖了 MLA+SSM 混合几何、异构块大小组合及边界情况,降低了回归风险。

对用户:所有使用 NIXL 连接的混合模型(如 Nemotron-Nano 系列)现在可以正常使用异构 TP(如 P_TP=4, D_TP=1),不再断言失败。纯 Attention 模型不受影响。对系统:引入新的置零开销,但仅适用于异构块大小场景;新增 cached_property 优化了缓存过滤。对团队:提供了清晰的实现和测试框架,便于后续扩展。

混合模型路径从断言改为复杂置零逻辑 异构 TP 多读键变更 head-sharded 拒绝但需与 future 工作对齐

关联 Issue

#41037 [Bug] _align_hybrid_block_size produces TP-dependent block sizes, currently unsupported when local and remote kernel block size mismatch

完整报告

参与讨论