-
-
Notifications
You must be signed in to change notification settings - Fork 20.7k
[KV Connector] Canonical KV Cache Allocation for HMA Models #37885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Etelis
wants to merge
38
commits into
vllm-project:main
Choose a base branch
from
Etelis:canonical-kv-caches
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 37 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
52688de
Add CanonicalKVCaches data classes for HMA KV cache representation
EtelisIBM e25e020
Add WorkerConnectorInitializationData and initialize_worker_connector
EtelisIBM 01b7897
Add canonical KV cache allocation for HMA models
EtelisIBM 03903f1
Wire up canonical KV cache allocation in gpu_model_runner
EtelisIBM 9697d1c
Add unit tests for canonical KV cache allocation
EtelisIBM d9f7203
Fix mypy error: rename shadowed variable in use_canonical_kv_caches
EtelisIBM f39ae32
Move canonical KV cache dataclasses to connector base
EtelisIBM 68ce39a
Address CR: relax group count check and use per-group spec
EtelisIBM 5b2b3bc
Address CR: prioritize canonical path and scope initialize_worker_con…
EtelisIBM 3f90424
Address CR: merge loops in allocate_canonical_kv_caches
EtelisIBM 432d002
Address CR: always call initialize_worker_connector
EtelisIBM 77655ed
Validate tensor sizes in use_canonical_kv_caches
EtelisIBM 24b90c8
Merge branch 'main' into canonical-kv-caches
Etelis d44b920
Merge branch 'main' into canonical-kv-caches
Etelis 5a209c2
Refactor canonical KV cache allocation into single-pass loop
EtelisIBM 61c2e96
Simplify canonical KV cache allocation using physical buffer
EtelisIBM 94fa930
Move per-layer reshape logic into inner loop
EtelisIBM 2119286
Merge branch 'main' into canonical-kv-caches
Etelis d6fbfbf
Merge branch 'main' into canonical-kv-caches
Etelis ebe6311
Address CR: use single cross-layers int8 tensor for canonical KV caches
EtelisIBM 16e28c2
Merge branch 'main' into canonical-kv-caches
Etelis fa03305
Merge branch 'main' into canonical-kv-caches
Etelis 9adee65
Reuse CanonicalKVCaches from kv_offload/spec; single ref per group
EtelisIBM 4673d60
Merge branch 'main' into canonical-kv-caches
Etelis f869fc6
Address CR: restore canonical dataclasses in base.py and fix group pa…
EtelisIBM 7e6f1e5
Merge branch 'main' into canonical-kv-caches
Etelis a3d8166
Address CR: allow single-group, use config.num_blocks, drop dead try
EtelisIBM 7e6c93d
Merge branch 'main' into canonical-kv-caches
Etelis 18fb58b
Merge branch 'main' into canonical-kv-caches
Etelis afd03b6
Merge branch 'main' into canonical-kv-caches
Etelis 73ab712
Merge remote-tracking branch 'origin/main' into canonical-kv-caches
LucasWilkinson 76cefbd
fixes
LucasWilkinson f567c43
Merge branch 'main' into canonical-kv-caches
Etelis 6220b0b
Merge branch 'main' into canonical-kv-caches
Etelis 99c1c97
Merge branch 'main' into canonical-kv-caches
Etelis 73edcc8
Merge branch 'main' into canonical-kv-caches
Etelis fba712c
Merge branch 'main' into canonical-kv-caches
Etelis 757e0fc
Merge branch 'main' into canonical-kv-caches
Etelis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| """Unit tests for CanonicalKVCaches abstraction.""" | ||
|
|
||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
| import torch | ||
|
|
||
| from vllm.distributed.kv_transfer.kv_connector.v1.base import ( | ||
| CanonicalKVCaches, | ||
| SupportsHMA, | ||
| ) | ||
| from vllm.v1.kv_cache_interface import ( | ||
| FullAttentionSpec, | ||
| KVCacheConfig, | ||
| KVCacheGroupSpec, | ||
| KVCacheTensor, | ||
| MambaSpec, | ||
| SlidingWindowSpec, | ||
| ) | ||
| from vllm.v1.worker.kv_connector_model_runner_mixin import ( | ||
| KVConnectorModelRunnerMixin, | ||
| ) | ||
| from vllm.v1.worker.utils import AttentionGroup | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Mock backends and connectors | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| BLOCK_SIZE = 16 | ||
| NUM_KV_HEADS = 4 | ||
| HEAD_SIZE = 8 | ||
| NUM_BLOCKS = 10 | ||
| DTYPE = torch.float16 | ||
|
|
||
|
|
||
| class MockFlashAttnBackend: | ||
| """Mimics FlashAttention NHD layout.""" | ||
|
|
||
| @staticmethod | ||
| def get_kv_cache_shape( | ||
| num_blocks, block_size, num_kv_heads, head_size, cache_dtype_str="auto" | ||
| ): | ||
| return (2, num_blocks, block_size, num_kv_heads, head_size) | ||
|
|
||
| @staticmethod | ||
| def get_kv_cache_stride_order(include_num_layers_dimension=False): | ||
| if include_num_layers_dimension: | ||
| return (2, 0, 1, 3, 4, 5) | ||
| return (0, 1, 2, 3, 4) | ||
|
|
||
|
|
||
| class MockNoStrideOrderBackend: | ||
| """Backend that does not support stride order.""" | ||
|
|
||
| @staticmethod | ||
| def get_kv_cache_shape( | ||
| num_blocks, block_size, num_kv_heads, head_size, cache_dtype_str="auto" | ||
| ): | ||
| return (2, num_blocks, block_size, num_kv_heads, head_size) | ||
|
|
||
| @staticmethod | ||
| def get_kv_cache_stride_order(include_num_layers_dimension=False): | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| class MockConnector(SupportsHMA): | ||
| prefer_cross_layer_blocks = True | ||
|
|
||
| def request_finished_all_groups(self, request, block_ids): | ||
| return False, None | ||
|
|
||
|
|
||
| class MockConnectorNoHMA: | ||
| prefer_cross_layer_blocks = True | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Helpers | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _make_full_attn_spec(): | ||
| return FullAttentionSpec( | ||
| block_size=BLOCK_SIZE, | ||
| num_kv_heads=NUM_KV_HEADS, | ||
| head_size=HEAD_SIZE, | ||
| dtype=DTYPE, | ||
| ) | ||
|
|
||
|
|
||
| def _make_sw_spec(sliding_window=128): | ||
| return SlidingWindowSpec( | ||
| block_size=BLOCK_SIZE, | ||
| num_kv_heads=NUM_KV_HEADS, | ||
| head_size=HEAD_SIZE, | ||
| dtype=DTYPE, | ||
| sliding_window=sliding_window, | ||
| ) | ||
|
|
||
|
|
||
| def _make_hma_kv_cache_config(): | ||
| """HMA config: 3 groups, group_size=2, 2 KVCacheTensors.""" | ||
| full_spec = _make_full_attn_spec() | ||
| sw_spec = _make_sw_spec() | ||
| page_size = full_spec.page_size_bytes | ||
|
|
||
| groups = [ | ||
| KVCacheGroupSpec(["full.0", "full.1"], full_spec), | ||
| KVCacheGroupSpec(["sw.0", "sw.2"], sw_spec), | ||
| KVCacheGroupSpec(["sw.1", "sw.3"], sw_spec), | ||
| ] | ||
| size = page_size * NUM_BLOCKS | ||
| tensors = [ | ||
| KVCacheTensor(size=size, shared_by=["full.0", "sw.0", "sw.1"]), | ||
| KVCacheTensor(size=size, shared_by=["full.1", "sw.2", "sw.3"]), | ||
| ] | ||
| return KVCacheConfig( | ||
| num_blocks=NUM_BLOCKS, | ||
| kv_cache_tensors=tensors, | ||
| kv_cache_groups=groups, | ||
| ) | ||
|
|
||
|
|
||
| def _make_attn_groups(backend_cls, kv_cache_config): | ||
| attn_groups = [] | ||
| for gid, group in enumerate(kv_cache_config.kv_cache_groups): | ||
| attn_groups.append( | ||
| [ | ||
| AttentionGroup( | ||
| backend=backend_cls, | ||
| layer_names=group.layer_names, | ||
| kv_cache_spec=group.kv_cache_spec, | ||
| kv_cache_group_id=gid, | ||
| ) | ||
| ] | ||
| ) | ||
| return attn_groups | ||
|
|
||
|
|
||
| def _patch_connector(connector): | ||
| return ( | ||
| patch( | ||
| "vllm.v1.worker.kv_connector_model_runner_mixin.has_kv_transfer_group", | ||
| return_value=True, | ||
| ), | ||
| patch( | ||
| "vllm.v1.worker.kv_connector_model_runner_mixin.get_kv_transfer_group", | ||
| return_value=connector, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def _use_canonical(config, attn_groups): | ||
| return KVConnectorModelRunnerMixin.use_canonical_kv_caches( | ||
| config, attn_groups, "auto" | ||
| ) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Tests | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| @pytest.mark.cpu_test | ||
| def test_use_canonical_kv_caches_happy_path(): | ||
| """Should return True for a valid HMA model with compatible connector.""" | ||
| config = _make_hma_kv_cache_config() | ||
| attn_groups = _make_attn_groups(MockFlashAttnBackend, config) | ||
| p1, p2 = _patch_connector(MockConnector()) | ||
| with p1, p2: | ||
| assert _use_canonical(config, attn_groups) is True | ||
|
|
||
|
|
||
| @pytest.mark.cpu_test | ||
| @pytest.mark.parametrize( | ||
| "description,config_fn,backend,connector_fn,patch_no_connector", | ||
| [ | ||
| ( | ||
| "no_connector", | ||
| _make_hma_kv_cache_config, | ||
| MockFlashAttnBackend, | ||
| None, | ||
| True, | ||
| ), | ||
| ( | ||
| "no_hma_support", | ||
| _make_hma_kv_cache_config, | ||
| MockFlashAttnBackend, | ||
| MockConnectorNoHMA, | ||
| False, | ||
| ), | ||
| ( | ||
| "mamba_group", | ||
| lambda: KVCacheConfig( | ||
| num_blocks=NUM_BLOCKS, | ||
| kv_cache_tensors=[], | ||
| kv_cache_groups=[ | ||
| KVCacheGroupSpec(["attn.0"], _make_full_attn_spec()), | ||
| KVCacheGroupSpec( | ||
| ["mamba.0"], | ||
| MambaSpec( | ||
| block_size=BLOCK_SIZE, | ||
| shapes=((16,), (16,)), | ||
| dtypes=(DTYPE,), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| MockFlashAttnBackend, | ||
| MockConnector, | ||
| False, | ||
| ), | ||
| ( | ||
| "no_stride_order", | ||
| _make_hma_kv_cache_config, | ||
| MockNoStrideOrderBackend, | ||
| MockConnector, | ||
| False, | ||
| ), | ||
| ], | ||
| ids=lambda x: x if isinstance(x, str) else "", | ||
| ) | ||
| def test_use_canonical_kv_caches_returns_false( | ||
| description, config_fn, backend, connector_fn, patch_no_connector | ||
| ): | ||
| """Should return False when any precondition is not met.""" | ||
| config = config_fn() | ||
| attn_groups = _make_attn_groups(backend, config) | ||
|
|
||
| if patch_no_connector: | ||
| with patch( | ||
| "vllm.v1.worker.kv_connector_model_runner_mixin.has_kv_transfer_group", | ||
| return_value=False, | ||
| ): | ||
| assert _use_canonical(config, attn_groups) is False | ||
| else: | ||
| p1, p2 = _patch_connector(connector_fn()) | ||
| with p1, p2: | ||
| assert _use_canonical(config, attn_groups) is False | ||
|
|
||
|
|
||
| @pytest.mark.cpu_test | ||
| def test_allocate_canonical_kv_caches(): | ||
| """Allocation should produce correct kv_caches dict and | ||
| CanonicalKVCaches with contiguous per-block data.""" | ||
| config = _make_hma_kv_cache_config() | ||
| attn_groups = _make_attn_groups(MockFlashAttnBackend, config) | ||
|
|
||
| num_groups = len(config.kv_cache_groups) | ||
| kv_caches, canonical = KVConnectorModelRunnerMixin.allocate_canonical_kv_caches( | ||
| config, | ||
| attn_groups, | ||
| "auto", | ||
| torch.device("cpu"), | ||
| [BLOCK_SIZE] * num_groups, | ||
| ) | ||
|
|
||
| assert isinstance(canonical, CanonicalKVCaches) | ||
|
|
||
| # -- kv_caches dict: all 6 layers present with correct shapes | ||
| expected_shape = (2, NUM_BLOCKS, BLOCK_SIZE, NUM_KV_HEADS, HEAD_SIZE) | ||
| assert len(kv_caches) == 6 | ||
| for name in ["full.0", "full.1", "sw.0", "sw.1", "sw.2", "sw.3"]: | ||
| assert kv_caches[name].shape == expected_shape | ||
|
|
||
| # layers sharing a position point to the same memory | ||
| assert kv_caches["full.0"].data_ptr() == kv_caches["sw.0"].data_ptr() | ||
| assert kv_caches["full.1"].data_ptr() == kv_caches["sw.2"].data_ptr() | ||
|
|
||
| # -- single cross-layers tensor: (num_blocks, cross_layer_page_size) int8 | ||
| assert len(canonical.tensors) == 1 | ||
| bt = canonical.tensors[0] | ||
| per_position_page = 2 * BLOCK_SIZE * NUM_KV_HEADS * HEAD_SIZE * DTYPE.itemsize | ||
| group_size = len(config.kv_cache_tensors) | ||
| cross_layer_page = per_position_page * group_size | ||
| assert bt.tensor.shape == (NUM_BLOCKS, cross_layer_page) | ||
| assert bt.tensor.dtype == torch.int8 | ||
| assert bt.page_size_bytes == cross_layer_page | ||
|
|
||
| # each row is contiguous and covers all positions for one block | ||
| assert bt.tensor.is_contiguous() | ||
|
|
||
| # -- group_data_refs: a single data reference per group (3 groups) | ||
| assert len(canonical.group_data_refs) == 3 | ||
| full_page = config.kv_cache_groups[0].kv_cache_spec.page_size_bytes | ||
| for refs in canonical.group_data_refs: | ||
| assert len(refs) == 1 | ||
| assert refs[0].tensor_idx == 0 | ||
| assert refs[0].page_size_bytes == full_page * group_size |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure these dataclasses about tensors belong here with kv_connector interface. They look a lot more related to whats in kv_cache_manager.py.
I'd rather keep this file lean for the actual interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Connectors need a way to know how to access the KV cache tensors.
Currently, connectors have 2 tasks:
KVCacheConfig)Using the canonical KV caches saves connectors these 2 tasks:
With cross-layers layout you cannot use KVCacheConfig as the tensors (single one) do not match
kv_cache_config.kv_cache_tensors.