|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Patch vLLM's MoRIIOConnector to transfer heterogeneous KV caches per-layer. |
| 3 | +
|
| 4 | +Why |
| 5 | +--- |
| 6 | +MiniMax-M3 (MiniMaxM3SparseForCausalLM) is a hybrid sparse-attention model: |
| 7 | +
|
| 8 | + * main attention layers register a ``FullAttentionSpec`` KV cache: |
| 9 | + rank-5 ``[2, num_blocks, block_size, num_kv_heads, head_dim]``, **fp8**, K+V |
| 10 | + * the lightning indexer (sparse layers) registers a separate |
| 11 | + ``MLAAttentionSpec`` index cache (``MiniMaxM3IndexerCache``): |
| 12 | + rank-3 ``[num_blocks, block_size, head_dim]``, **bf16**, key-only |
| 13 | +
|
| 14 | +The upstream MoRIIOConnector assumes a *single uniform* KV layout: it derives |
| 15 | +``self.kv_cache_shape`` / ``block_len`` / ``element_size`` from the **first** |
| 16 | +cache, and ``_read_blocks`` computes the transfer offsets **once** from |
| 17 | +``first_layer`` and reuses them for **every** layer (see the in-code TODO |
| 18 | +"block_len needs to be per-layer for ... hybrid attn"). For M3 this transfers |
| 19 | +the bf16 key-only rank-3 index cache using the fp8 K+V rank-5 main-cache sizing, |
| 20 | +corrupting the indexer state on the decode worker. The sparse layers then select |
| 21 | +the wrong KV blocks and the model emits incoherent tokens (gsm8k ~= 0). |
| 22 | +
|
| 23 | +This is the vLLM analogue of the already-shipped SGLang MoRI DSA fix in |
| 24 | +``patches/mori_conn.py`` (see patches/README.md). |
| 25 | +
|
| 26 | +Fix |
| 27 | +--- |
| 28 | +Compute transfer geometry **per layer** from each layer's own tensor |
| 29 | +(``shape`` / ``stride`` / ``element_size`` / rank), instead of from the first |
| 30 | +cache. For homogeneous models every layer's geometry equals the first cache's, |
| 31 | +so behaviour is unchanged; only hybrid models (M3) are affected. |
| 32 | +
|
| 33 | +Two minimal, targeted edits (READ path, which the M3 recipe uses with |
| 34 | +``read_mode: true``): |
| 35 | +
|
| 36 | + 1. ``_compute_block_transfer_offsets`` -> use ``self.kv_caches[layer_name]``'s |
| 37 | + own shape (rank/dims) instead of the global ``self.kv_cache_shape``. |
| 38 | + 2. ``_read_blocks`` -> call ``_compute_block_transfer_offsets`` inside the |
| 39 | + per-layer loop instead of once for ``first_layer``. |
| 40 | +
|
| 41 | +Idempotent: re-running detects the ``PATCHED heterogeneous-kv`` marker and exits. |
| 42 | +""" |
| 43 | +import os |
| 44 | +import sys |
| 45 | + |
| 46 | + |
| 47 | +def _default_target() -> str: |
| 48 | + try: |
| 49 | + import vllm |
| 50 | + except Exception: |
| 51 | + return "" |
| 52 | + return os.path.join( |
| 53 | + os.path.dirname(vllm.__file__), |
| 54 | + "distributed/kv_transfer/kv_connector/v1/moriio/moriio_connector.py", |
| 55 | + ) |
| 56 | + |
| 57 | + |
| 58 | +OLD1 = ''' assert self.kv_cache_shape is not None, "KV caches shape not initialized" |
| 59 | + is_mla = len(self.kv_cache_shape) == 3 |
| 60 | + stride = self.kv_caches[layer_name].stride() |
| 61 | + sz = self.kv_caches[layer_name].element_size() |
| 62 | + if is_mla: |
| 63 | + blknum, blksize, hs = self.kv_cache_shape |
| 64 | + hn = 1 |
| 65 | + block_stride = stride[0] |
| 66 | + else: |
| 67 | + _, blknum, blksize, hn, hs = self.kv_cache_shape''' |
| 68 | + |
| 69 | +NEW1 = ''' # [PATCHED heterogeneous-kv] Use this layer's own shape so caches with a |
| 70 | + # different rank/dtype (MiniMax-M3: bf16 key-only rank-3 index cache vs |
| 71 | + # fp8 K+V rank-5 main cache) are sized per-layer, not from the first cache. |
| 72 | + layer_shape = tuple(self.kv_caches[layer_name].shape) |
| 73 | + assert layer_shape, "KV caches shape not initialized" |
| 74 | + is_mla = len(layer_shape) == 3 |
| 75 | + stride = self.kv_caches[layer_name].stride() |
| 76 | + sz = self.kv_caches[layer_name].element_size() |
| 77 | + if is_mla: |
| 78 | + blknum, blksize, hs = layer_shape |
| 79 | + hn = 1 |
| 80 | + block_stride = stride[0] |
| 81 | + else: |
| 82 | + _, blknum, blksize, hn, hs = layer_shape''' |
| 83 | + |
| 84 | +OLD2 = ''' first_layer = list(self.layer_name_to_local_kv_cache_metadata.keys())[0] |
| 85 | + offs = self._compute_block_transfer_offsets( |
| 86 | + first_layer, local_block_ids, remote_block_ids, remote_moriio_meta |
| 87 | + ) |
| 88 | +
|
| 89 | + for layer_name in self.layer_name_to_local_kv_cache_metadata: |
| 90 | + sess_idx = list(self.layer_name_to_local_kv_cache_metadata.keys()).index( |
| 91 | + layer_name |
| 92 | + ) |
| 93 | + # TODO : apply multi-session batch-read when moriio support it |
| 94 | + transfer_status = self.moriio_wrapper.read_remote_data( |
| 95 | + offs[2], offs[0], offs[1], sessions[sess_idx] |
| 96 | + )''' |
| 97 | + |
| 98 | +NEW2 = ''' for layer_name in self.layer_name_to_local_kv_cache_metadata: |
| 99 | + sess_idx = list(self.layer_name_to_local_kv_cache_metadata.keys()).index( |
| 100 | + layer_name |
| 101 | + ) |
| 102 | + # [PATCHED heterogeneous-kv] Per-layer offsets so the bf16 key-only |
| 103 | + # MiniMax-M3 index cache is transferred with its own geometry instead |
| 104 | + # of the first (main fp8 K+V) layer's. |
| 105 | + offs = self._compute_block_transfer_offsets( |
| 106 | + layer_name, local_block_ids, remote_block_ids, remote_moriio_meta |
| 107 | + ) |
| 108 | + # TODO : apply multi-session batch-read when moriio support it |
| 109 | + transfer_status = self.moriio_wrapper.read_remote_data( |
| 110 | + offs[2], offs[0], offs[1], sessions[sess_idx] |
| 111 | + )''' |
| 112 | + |
| 113 | + |
| 114 | +def main() -> int: |
| 115 | + target = sys.argv[1] if len(sys.argv) > 1 else _default_target() |
| 116 | + if not target or not os.path.isfile(target): |
| 117 | + print(f"[PATCH] moriio_connector.py not found ({target!r}); skipping") |
| 118 | + return 0 |
| 119 | + src = open(target).read() |
| 120 | + if "PATCHED heterogeneous-kv" in src: |
| 121 | + print("[PATCH] moriio heterogeneous-kv already applied") |
| 122 | + return 0 |
| 123 | + if OLD1 not in src: |
| 124 | + print("[PATCH] WARN: _compute_block_transfer_offsets pattern not found; " |
| 125 | + "connector version changed — skipping (no-op)") |
| 126 | + return 0 |
| 127 | + if OLD2 not in src: |
| 128 | + print("[PATCH] WARN: _read_blocks pattern not found; " |
| 129 | + "connector version changed — skipping (no-op)") |
| 130 | + return 0 |
| 131 | + src = src.replace(OLD1, NEW1, 1).replace(OLD2, NEW2, 1) |
| 132 | + # Validate it still compiles before writing. |
| 133 | + try: |
| 134 | + compile(src, target, "exec") |
| 135 | + except SyntaxError as e: |
| 136 | + print(f"[PATCH] ERROR: patched source fails to compile: {e}") |
| 137 | + return 1 |
| 138 | + open(target, "w").write(src) |
| 139 | + print("[PATCH] Applied: moriio heterogeneous-kv per-layer transfer " |
| 140 | + "(MiniMax-M3 sparse index cache)") |
| 141 | + return 0 |
| 142 | + |
| 143 | + |
| 144 | +if __name__ == "__main__": |
| 145 | + sys.exit(main()) |
0 commit comments