Prhub

#53721 [ROCm][Connector] SWA+HMA-support in MoRI-IO connector (Gemma4)

原始 PR 作者 simondanielsson 合并时间 2026-09-14 20:52 文件变更 4 提交数 33 评论 20 代码增减 +408 / -46

执行摘要

MoRI-IO 连接器支持 SWA+HMA,PD 吞吐数倍提升

PR body 明确写出目的:『Run PD with sliding window with hybrid memory allocator enabled (Gemma4)』,即在此之前 MoRI-IO 连接器在 Gemma4 这类含滑动窗口层的混合模型上无法启用 HMA,PD 分离部署只能退回 --disable-hybrid-kv-cache-manager,KV 容量受限。作者同时给出简化约定:『READ mode only. Other hybrids (mamba etc.) need to disable HMA, as before』,并主动声明破坏性变更:WRITE 模式的混合模型需要显式关闭 HMA,否则抛 NotImplementedError。PR body 还附上了 GSM8K 对比、512 并发 bench serve、KV 利用率(HMA 开约 300 并发打满 vs 关闭约 200 并发打满)以及 DeepSeek-V3 / MiniMax-M2.5 的回归验证,说明动机既有功能补齐也有可量化的吞吐收益。

值得精读,尤其适合关注 KV 连接器、PD 分离与 HMA 的工程师。重点看三处设计决策:一是通过 SupportsHMA + request_finished_all_groups 把『每组一个块列表』的契约从类型层(BlockIds)到协议层打通,并与 NIXL 保持一致的命名;二是『能裁剪就裁剪、不能裁剪就 fail closed』的策略——SW 组只传窗口尾部、其他 hybrid 直接抛异常,避免静默精度损失;三是顺手修掉的 local/remote block id 混用 bug,这个话题值得作为案例研究:一个看似只关乎容量的特性改造,如何暴露并修复既有的数据正确性问题。阅读时建议对照 test_moriio_connector.py 中 test_get_exchange_clipped_blocks_clips_only_sw_group 与 test_metadata_hma_block_ids_preserved_per_group,并留意 PR 未覆盖的滚动升级兼容性与多 full-attention 组场景。

讨论亮点

1)作者自曝并自证的正确性回归(moriio_connector.py:663):simondanielsson 在 update_state_after_alloc 处写道 > "This was a bug: local_block_ids = remote_block_ids[...] where we need these local ids to be truly the local ones",随后用数据补强:Before this change(1-2p 回归) flexible-extract 0.7369 / strict-match 0.7278;After removing this local->remote block id override(commit f8d28306) 0.7422 / 0.7362;Compare to main(TP1 无 PD) 0.7468 / 0.7392,确认回归消除。这是本 PR 最有价值的讨论,说明 HMA 改造顺带暴露了旧代码里用远端块 ID 冒充本地块 ID 的语义错位。
2)代码组织建议被采纳:dllehr-amd 针对新增的 HMA 检查块留下 > "can we move this under line 409's check?",作者在 commit ea18bb01ff73a7733a444ba1f87178812939b720(chore: move check for HMA+WRITE mode into the same if statement)中把 WRITE 模式检查合入 _is_hma_required 分支,减少一层独立判断。
3)fork PR 缺少自动 review:claude[bot] 说明 > "This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.",实际上主要依赖作者自审 + tjtanaa 的 APPROVED,review_comments 仅 3 条。
4)CI 排队与重试:AndreasKaratzas 取消 AMD 构建 > "I cancelled the build here. Let's retrigger it later when the queues are decongested first",作者在 5 个 commit 上反复 /ci run、/ci retry、/amd-ci retry,说明该 PR 的合入主要卡在 AMD/上游 CI 资源而非代码争议。

实现拆解

  1. 声明 HMA 能力并打通调度器入参:vllm/distributed/kv_transfer/kv_connector/v1/moriio/moriio_connector.py 中 MoRIIOConnector 由 KVConnectorBase_V1 单继承改为 (KVConnectorBase_V1, SupportsHMA);__init__ 把新入参 kv_cache_config: KVCacheConfig 透传给 MoRIIOConnectorScheduler(vllm_config, engine_id, kv_cache_config)。外层连接器新增 request_finished_all_groups(request, block_ids: BlockIds),原 request_finished 改为把单组 block_ids 包成 (block_ids,) 后委托同一 scheduler 方法,命名与协议对齐 NIXL 连接器(commit 3a3ca20)。
  2. 引入 BlockIds 类型统一多组语义:moriio_common.py 中 ReqMeta.local_block_ids / remote_block_ids 与 MoRIIOConnectorMetadata.add_new_req 的入参从 list[int] 改为 BlockIds(一个 KV cache 组一个列表),连接器内部的 _reqs_need_recv / _reqs_need_save / _reqs_need_pending_save 也同步改成 tuple[Request, BlockIds],使多组结构在类型层面显式化。
  3. Scheduler 初始化做 HMA 判定与块预算:MoRIIOConnectorScheduler.__init__ 计算 _is_hma_required(未关闭 hybrid KV cache manager 且存在非 FullAttentionSpec 的组);命中时先对不支持的 hybrid 类型(只允许 FullAttentionSpec + SlidingWindowSpec)fail closed 抛 NotImplementedError,再对 WRITE 模式抛 NotImplementedError(PR body 声明的 breaking change);随后计算 blocks_per_sw = cdiv(sliding_window, block_size) + 1(+1 保守兜底窗口与块边界不对齐),并定位第一个 full-attention 组,记录 _full_attn_group_idx / _full_attn_block_size 作为 chunked prefill 的 token 计数基准,避免用被裁剪的 SW 组块数推断序列长度。
  4. 新增裁剪逻辑并接入两条交换路径:get_exchange_clipped_blocks(block_ids) 在非 HMA 时原样返回([list(blocks) for blocks in block_ids]),HMA 时对 blocks_per_sw[i] > 0 的组只保留窗口尾部块、full-attention 组全量保留。update_state_after_alloc 的 save 路径改为整体保存 blocks.get_block_ids();recv 路径用裁剪后的真实本地块替换旧实现中『local_block_ids = remote_block_ids[-len(local_block_ids):]』的覆盖写法,并把长度断言从全局比较改为逐组比较(前缀缓存部分命中时本地块可少于远端块,但不会更多)。
  5. 修复 local/remote block id 混用回归:作者在 review 中自曝该 bug,修复集中在 commit f8d28306,GSM8K strict-match 由 0.7278(1-2p 回归)恢复到 0.7362,接近 main 的 0.7392。
  6. 测试与验证配套:tests/v1/kv_connector/unit/test_moriio_connector.py 新增 _make_hybrid_kv_cache_config(full attention + SlidingWindowSpec 各一组)、_read_scheduler 夹具,以及 test_hma_blocks_per_sw_two_groups、test_is_hma_required(三种参数化)、test_non_sliding_window_hybrid_is_rejected、test_token_count_basis_uses_full_attention_group、test_get_exchange_clipped_blocks_clips_only_sw_group、test_metadata_hma_block_ids_preserved_per_group、test_single_group_path_unchanged、test_hybrid_write_mode_rejected、test_worker_layer_to_group_routing 等用例,且把既有断言改为 req_meta.local_block_ids[0] 以适配新结构;tests/v1/kv_connector/unit/test_moriio_routing.py 的 mock producer 补上 _is_hma_required=False、blocks_per_sw=[0] 并把 get_exchange_clipped_blocks 绑定到 stand-in 对象,避免 SimpleNamespace 绕过新逻辑导致测试失真。
文件 模块 状态 重要度
vllm/distributed/kv_transfer/kv_connector/v1/moriio/moriio_connector.py KV 连接器 modified 8.78
tests/v1/kv_connector/unit/test_moriio_connector.py 连接器测试 modified 7.21
vllm/distributed/kv_transfer/kv_connector/v1/moriio/moriio_common.py 元数据类型 modified 5.0
tests/v1/kv_connector/unit/test_moriio_routing.py 路由测试 modified 3.76

关键符号

MoRIIOConnector.__init__ MoRIIOConnector.request_finished MoRIIOConnector.request_finished_all_groups MoRIIOConnectorScheduler.__init__ MoRIIOConnectorScheduler.get_exchange_clipped_blocks MoRIIOConnectorScheduler.update_state_after_alloc MoRIIOConnectorMetadata.add_new_req

关键源码片段

tests/v1/kv_connector/unit/test_moriio_connector.py test-coverage

新增 Gemma-like 的 hybrid KV cache 夹具与 10 个左右用例,覆盖 HMA 判定、blocks_per_sw 计算、SW 组裁剪、非 SW hybrid 拒绝、WRITE+HMA 拒绝、多组块 ID 保留等关键分支,并把既有断言改为 local_block_ids[0] 以适配新的多组结构。

测试用最小化的 Gemma-like 配置把『一个 full-attention 组 + 一个 sliding-window 组』固定下来,并直接断言裁剪结果:

def _make_hybrid_kv_cache_config() -> KVCacheConfig:
    """一个 full-attention 组 + 一个 sliding-window 组(模拟 Gemma)。"""
    full_spec = FullAttentionSpec(
        block_size=16, num_kv_heads=4, head_size=64, dtype=torch.float16
    )
    sw_spec = SlidingWindowSpec(
        block_size=16,
        num_kv_heads=4,
        head_size=64,
        dtype=torch.float16,
        sliding_window=32, # window=32、block=16 → 期望交换 3 个块
    )
    num_blocks = 2
    page = full_spec.page_size_bytes
    return KVCacheConfig(
        num_blocks=num_blocks,
        kv_cache_tensors=[
            KVCacheTensor(
                size=2 * num_blocks * page,
                layers=["full0", "sw0"],
                layer_stride=num_blocks * page,
                block_stride=page,
            )
        ],
        kv_cache_groups=[
            KVCacheGroupSpec(layer_names=["full0"], kv_cache_spec=full_spec),
            KVCacheGroupSpec(layer_names=["sw0"], kv_cache_spec=sw_spec),
        ],
    )
​
​
def _read_scheduler(
    kv_cache_config: KVCacheConfig, disable_hma: bool = False
) -> MoRIIOConnectorScheduler:
    vllm_config = create_vllm_config(role="kv_producer", read_mode=True)
    # disable_hma 对应命令行 --disable-hybrid-kv-cache-manager 的配置项,
    # 用于验证关闭 HMA 时 _is_hma_required 与裁剪逻辑都退化为 no-op。
    vllm_config.scheduler_config.disable_hybrid_kv_cache_manager = disable_hma
    with set_current_vllm_config(vllm_config):
        connector = MoRIIOConnector(
            vllm_config, KVConnectorRole.SCHEDULER, kv_cache_config
        )
        assert connector.connector_scheduler is not None
        return connector.connector_scheduler
​
​
def test_get_exchange_clipped_blocks_clips_only_sw_group():
    """裁剪只作用于 sliding-window 组,full attention 组必须原样保留。"""
    scheduler = _read_scheduler(_make_hybrid_kv_cache_config())
    full = [10, 11, 12, 13, 14]
    sw = [20, 21, 22, 23, 24]
    clipped = scheduler.get_exchange_clipped_blocks([full, sw])
    assert clipped[0] == full # full attention 组:5 个块全部保留
    assert clipped[1] == [22, 23, 24] # SW 组:cdiv(32, 16) + 1 = 3,只留尾部

评论区精华

update_state_after_alloc 用 remote_block_ids 覆盖 local_block_ids 的正确性回归 正确性

作者在 moriio_connector.py:663 自评:> "This was a bug: `local_block_ids = remote_block_ids[...]` where we need these local ids to be truly the local ones",并用三组 GSM8K 数据自证:修改前(1-2p 回归)flexible-extract 0.7369 / strict-match 0.7278,去掉覆盖后 0.7422 / 0.7362,对照 main(TP1 无 PD)0.7468 / 0.7392。

结论:确认为真实 bug,已在 commit f8d28306 中改为使用 blocks.get_block_ids() 经 get_exchange_clipped_blocks 裁剪后的真实本地块,并把断言改为逐组比较本地块数不超过远端块数。 · 已解决

HMA + WRITE 模式检查的代码位置 设计

dllehr-amd 针对新增的 _is_hma_required 检查块提出 > "can we move this under line 409's check?",即把 WRITE 模式拒绝逻辑并入同一个 HMA 判断分支,而不是并列的独立 if。

结论:作者采纳,见 commit ea18bb01ff73a7733a444ba1f87178812939b720(chore: move check for HMA+WRITE mode into the same if statement)。 · 已解决

fork PR 缺少自动化 review 覆盖 测试

claude[bot] 提示 > "This pull request is from a fork — automated review is disabled. A repository maintainer can comment `@claude review` to run a one-time review.",最终 review_comments 仅 3 条,主要意见来自作者自审与 dllehr-amd 的单条建议,tjtanaa 给出 APPROVED。

结论:未强制要求额外人工深审,代码正确性主要依赖作者自测(GSM8K、512 并发 bench、DeepSeek-V3 / MiniMax-M2.5 回归)。 · 已解决

CI 排队拥塞导致的构建取消与重跑 other

AndreasKaratzas 主动取消了一次 AMD 构建:> "I cancelled the build here. Let's retrigger it later when the queues are decongested first";作者随后连续触发 /ci run、/ci retry、/amd-ci retry,跨越 5 个 commit hash(d035b463、8cea273c、540880b4、7e88f3b6、b6275a5b)。

结论:合入主要阻塞在 CI 资源而非代码分歧,最终以 b6275a5bce8e 为 head 通过并 merge。 · 已解决

风险与影响

1)破坏性行为变更(高):MoRIIOConnectorScheduler.__init__ 对 Hybrid 模型 + WRITE 模式直接抛 NotImplementedError,错误信息要求用户改用 READ 模式或传 --disable-hybrid-kv-cache-manager。已按此方式部署的存量脚本在升级后会启动失败,属于显式 breaking change,需要在 release note / 文档中标注。
2)fail closed 范围扩大(中):只放行 FullAttentionSpec + SlidingWindowSpec,ChunkedLocalAttentionSpec、mamba 等一律拒绝启动。虽然没有静默传错块的风险(test_non_sliding_window_hybrid_is_rejected 已覆盖),但把此前『只是不启用 HMA 也能跑』的场景变成硬失败,影响面比标题(Gemma4)所暗示的更大。
3)块预算保守带来的额外传输(低-中):blocks_per_sw = cdiv(window, block_size) + 1 的 +1 是为窗口与块边界不对齐留的余量,窗口远大于 block_size 时会多传一个块;这是作者有意为之的保守设计(代码注释写明),但对 RDMA 传输量有轻微影响,未被 bench 单独量化。
4)块 ID 语义修复的覆盖边界(中):update_state_after_alloc 中改为逐组 assert len(local) <= len(remote),若两端组数或块数不满足预期会直接断言失败而非降级处理;同时 remote_block_ids 现在按组传递,prefill 与 decode 滚动升级、版本不一致时协议可能不兼容。
5)验证证据偏手工(中):全部性能与精度数据来自作者在 MI300 上的手工运行,仓库内没有新增 e2e/CI 用例(MoRI-IO 依赖专有 mori.io 库,CI 无法覆盖),单测大量使用 mock/stand-in,无法覆盖真实 HMA 数据面。
6)full-attention 基准组的选择(低):_full_attn_group_idx 取第一个 blocks_per_sw == 0 的组,若模型存在多个 block_size 不同的 full-attention 组,token 计数基准未必唯一,当前仅在 Gemma-like 两组件下被测。

对用户:Gemma4 等 SW 混合模型在 ROCm + MoRI-IO 的 PD 分离部署下可以打开 HMA,PR body 实测 512 并发时 QPS 由 0.27 提升到 1.10、Mean TPOT 由 860.62ms 降到 240.80ms,KV 打满时的并发请求数由约 200 升至约 300,精度与 main 持平(GSM8K flexible-extract 0.7422 vs 0.7468)。但使用 Hybrid 模型 + WRITE 模式的用户升级后会立即失败,必须改配置。
对系统:影响集中在 kv_connector/v1/moriio 路径与调度器的块交换语义,不改动 attention 内核或其他连接器;BlockIds 类型的引入让多组块列表成为连接器内部的数据契约,后续 worker 侧 _write_blocks_for_req、层到组映射等逻辑都会围绕它展开,是 MoRI-IO 向 HMA 演进的结构性一步。
对团队:补齐了 MoRI-IO 相对 NIXL 在 HMA 能力上的缺口(函数命名也对齐 NIXL),维护者后续需要同步 mamba 等其他 hybrid 的处置策略;验证仍依赖 AMD 硬件与手工脚本,短期内该路径的回归风险由作者与 AMD 侧承担。

核心路径变更 WRITE 模式破坏性变更 fail-closed 拒绝其他 hybrid 端到端验证依赖手工 AMD 机器 缺少滚动升级兼容性说明

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论