@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 27df5199d9
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 15, 17:15 UTC

Support SHA256 as hash function in prefix caching (#15297)

Signed-off-by: Marko Rosenmueller <5467316+dr75@users.noreply.github.com>

marko committed Mar 26, 2025 at 19:11 UTC 27df5199d99627e1eb101071c2155f888181bd64
12 files changed +214 -71
docs/source/design/v1/prefix_caching.md
+4 -3
@@ -15,12 +15,13 @@ Block 3: |<------------------ prefix -------------------->| |<--- block tokens -
15 In the example above, the KV cache in the first block can be uniquely identified with the token “A gentle breeze stirred”. The third block can be uniquely identified with the tokens in the block “laughed in the distance”, along with the prefix tokens “A gentle breeze stirred the leaves as children”. Therefore, we can build the block hash of `hash(tuple[components])`, where components are:
16
17 * Parent hash value: The hash value of the parent hash block.
18 -* Block tokens: A tuple of tokens in this block. The reason to include the exact tokens is to reduce potential hash value collision.
18 +* Block tokens: A tuple of tokens in this block. The reason to include the exact tokens is to reduce potential hash value collision.
19 * Extra hashes: Other values required to make this block unique, such as LoRA IDs and multi-modality input hashes (see the example below).
20
21 -Note 1: We only cache full blocks.
21 +> **Note 1:** We only cache full blocks.
22
23 -Note 2: The above hash key structure is not 100% collision free. Theoretically it’s still possible for the different prefix tokens to have the same hash value, but this should be nearly impossible to happen. Of course, contributions are welcome if you have an awesome idea to eliminate collusion entirely.
23 +> **Note 2:** The above hash key structure is not 100% collision free. Theoretically it’s still possible for the different prefix tokens to have the same hash value. To avoid any hash collisions **in a multi-tenant setup, we advise to use SHA256** as hash function instead of the default builtin hash.
24 +SHA256 is supported since vLLM v0.8.3 and must be enabled with a command line argument. It comes with a performance impact of about 100-200ns per token (~6ms for 50k tokens of context).
25
26 **A hashing example with multi-modality inputs**
27 In this example, we illustrate how prefix caching works with multi-modality inputs (e.g., images). Assuming we have a request with the following messages:
tests/test_utils.py
+22 -1
@@ -2,6 +2,8 @@
2 # ruff: noqa
3
4 import asyncio
5 +import hashlib
6 +import pickle
7 import socket
8 from collections.abc import AsyncIterator
9 from unittest.mock import patch
@@ -14,7 +16,8 @@ from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
16 from vllm.utils import (FlexibleArgumentParser, MemorySnapshot,
17 PlaceholderModule, StoreBoolean, bind_kv_cache,
18 deprecate_kwargs, get_open_port, memory_profiling,
17 - merge_async_iterators, supports_kw, swap_dict_values)
19 + merge_async_iterators, sha256, supports_kw,
20 + swap_dict_values)
21
22 from .utils import create_new_process_for_each_test, error_on_warning
23
@@ -476,3 +479,21 @@ def test_swap_dict_values(obj, key1, key2):
479 assert obj[key1] == original_obj[key2]
480 else:
481 assert key1 not in obj
482 +
483 +@pytest.mark.parametrize("input", [(), ("abc", ), (None, ),
484 + (None, bool, [1, 2, 3])])
485 +@pytest.mark.parametrize("output", [0, 1, 2])
486 +def test_sha256(input: tuple, output: int):
487 + hash = sha256(input)
488 + assert hash is not None
489 + assert isinstance(hash, int)
490 + assert hash != 0
491 +
492 + bytes = pickle.dumps(input, protocol=pickle.HIGHEST_PROTOCOL)
493 + assert hash == int.from_bytes(hashlib.sha256(bytes).digest(), byteorder="big")
494 +
495 + # hashing again, returns the same value
496 + assert hash == sha256(input)
497 +
498 + # hashing different input, returns different value
499 + assert hash != sha256(input + (1, ))
tests/v1/core/test_kv_cache_utils.py
+29 -13
@@ -5,8 +5,12 @@ import torch
5
6 from vllm.multimodal.inputs import MultiModalKwargs
7 from vllm.sampling_params import SamplingParams
8 -from vllm.v1.core.kv_cache_utils import (BlockHashType, FreeKVCacheBlockQueue,
9 - KVCacheBlock, PrefixCachingMetrics,
8 +from vllm.utils import sha256
9 +# disable yapf here as it formats differently than isort such that both fail
10 +# yapf: disable
11 +from vllm.v1.core.kv_cache_utils import (NONE_HASH, BlockHashType,
12 + FreeKVCacheBlockQueue, KVCacheBlock,
13 + PrefixCachingMetrics,
14 generate_block_hash_extra_keys,
15 hash_block_tokens,
16 hash_request_tokens,
@@ -16,6 +20,8 @@ from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheConfig,
20 from vllm.v1.metrics.stats import PrefixCacheStats
21 from vllm.v1.request import Request
22
23 +# yapf: enable
24 +
25
26 def make_request(request_id,
27 prompt_token_ids,
@@ -40,6 +46,12 @@ def make_request(request_id,
46 )
47
48
49 +def test_none_hash():
50 + assert NONE_HASH is not None
51 + assert isinstance(NONE_HASH, int)
52 + assert NONE_HASH != 0
53 +
54 +
55 def test_kv_cache_block():
56 # Test KVCacheBlock initialization
57 block = KVCacheBlock(block_id=0)
@@ -190,21 +202,23 @@ def test_generate_block_hash_extra_keys_no_mm_inputs():
202 assert next_mm_idx == 0
203
204
193 -def test_hash_block_tokens():
205 +@pytest.mark.parametrize("hash_fn", [sha256, hash])
206 +def test_hash_block_tokens(hash_fn):
207 parent_block_hash = 123
208 curr_block_token_ids = (1, 2, 3)
209 extra_keys = ("key1", "key2")
210
198 - block_hash = hash_block_tokens(parent_block_hash, curr_block_token_ids,
199 - extra_keys)
211 + block_hash = hash_block_tokens(hash_fn, parent_block_hash,
212 + curr_block_token_ids, extra_keys)
213 assert isinstance(block_hash, BlockHashType)
201 - assert block_hash.hash_value == hash(
214 + assert block_hash.hash_value == hash_fn(
215 (parent_block_hash, curr_block_token_ids, extra_keys))
216 assert block_hash.token_ids == curr_block_token_ids
217 assert block_hash.extra_keys == extra_keys
218
219
207 -def test_hash_request_tokens():
220 +@pytest.mark.parametrize("hash_fn", [sha256, hash])
221 +def test_hash_request_tokens(hash_fn):
222 request = make_request(
223 request_id=0,
224 prompt_token_ids=[_ for _ in range(6)],
@@ -219,7 +233,7 @@ def test_hash_request_tokens():
233 )
234
235 block_size = 3
222 - block_hashes = hash_request_tokens(block_size, request)
236 + block_hashes = hash_request_tokens(hash_fn, block_size, request)
237
238 assert len(block_hashes) == 2
239 assert isinstance(block_hashes[0], BlockHashType)
@@ -234,7 +248,8 @@ def test_hash_request_tokens():
248 assert block_hashes[1].extra_keys == ("hash2", )
249
250
237 -def test_hash_tokens_different_mm_input():
251 +@pytest.mark.parametrize("hash_fn", [sha256, hash])
252 +def test_hash_tokens_different_mm_input(hash_fn):
253 request1 = make_request(
254 request_id=0,
255 prompt_token_ids=[_ for _ in range(6)],
@@ -260,13 +275,14 @@ def test_hash_tokens_different_mm_input():
275 mm_hashes=["hash3", "hash2"],
276 )
277 block_size = 3
263 - block_hashes1 = hash_request_tokens(block_size, request1)
264 - block_hashes2 = hash_request_tokens(block_size, request2)
278 + block_hashes1 = hash_request_tokens(hash_fn, block_size, request1)
279 + block_hashes2 = hash_request_tokens(hash_fn, block_size, request2)
280 assert block_hashes1[0] != block_hashes2[0]
281 assert block_hashes1[1] != block_hashes2[1]
282
283
269 -def test_hash_request_tokens_no_mm_inputs():
284 +@pytest.mark.parametrize("hash_fn", [sha256, hash])
285 +def test_hash_request_tokens_no_mm_inputs(hash_fn):
286 request = make_request(
287 request_id=0,
288 prompt_token_ids=[_ for _ in range(6)],
@@ -275,7 +291,7 @@ def test_hash_request_tokens_no_mm_inputs():
291 )
292
293 block_size = 3
278 - block_hashes = hash_request_tokens(block_size, request)
294 + block_hashes = hash_request_tokens(hash_fn, block_size, request)
295
296 assert len(block_hashes) == 2
297 assert block_hashes[0].token_ids == (0, 1, 2)
tests/v1/core/test_prefix_caching.py
+17 -5
@@ -7,7 +7,7 @@ import pytest
7
8 from vllm.multimodal.inputs import MultiModalKwargs, PlaceholderRange
9 from vllm.sampling_params import SamplingParams
10 -from vllm.utils import cdiv
10 +from vllm.utils import cdiv, sha256
11 from vllm.v1.core.block_pool import BlockPool
12 from vllm.v1.core.kv_cache_manager import KVCacheManager, Request
13 from vllm.v1.core.kv_cache_utils import (BlockHashType, KVCacheBlock,
@@ -39,16 +39,21 @@ def make_request(request_id,
39 )
40
41
42 -def test_prefill():
42 +@pytest.mark.parametrize("hash_algo", ["sha256", "hash"])
43 +def test_prefill(hash_algo):
44 manager = KVCacheManager(
45 block_size=16,
46 num_gpu_blocks=10,
47 max_model_len=8192,
48 sliding_window=None,
49 enable_caching=True,
50 + caching_hash_algo=hash_algo,
51 num_preallocate_tokens=16,
52 )
53
54 + # choose the hash function according to the parameter
55 + hash_fn = sha256 if hash_algo == "sha256" else hash
56 +
57 # Complete 3 blocks (48 tokens)
58 common_token_ids = [i for i in range(3) for _ in range(16)]
59
@@ -68,7 +73,8 @@ def test_prefill():
73 parent_block_hash = None
74 for block_id in (0, 1, 2):
75 block_tokens = tuple(all_token_ids[block_id * 16:(block_id + 1) * 16])
71 - block_hash = hash_block_tokens(parent_block_hash, block_tokens)
76 + block_hash = hash_block_tokens(hash_fn, parent_block_hash,
77 + block_tokens)
78 assert manager.block_pool.blocks[block_id].block_hash == block_hash
79 assert manager.block_pool.blocks[block_id].ref_cnt == 1
80 parent_block_hash = block_hash.hash_value
@@ -163,6 +169,8 @@ def test_prefill_plp():
169 enable_caching=True,
170 num_preallocate_tokens=16,
171 )
172 + # the default hash function is hash
173 + hash_fn = hash
174
175 # Complete 3 blocks (48 tokens)
176 common_token_ids = [i for i in range(3) for _ in range(16)]
@@ -185,7 +193,8 @@ def test_prefill_plp():
193 parent_block_hash = None
194 for block_id in (0, 1, 2):
195 block_tokens = tuple(all_token_ids[block_id * 16:(block_id + 1) * 16])
188 - block_hash = hash_block_tokens(parent_block_hash, block_tokens)
196 + block_hash = hash_block_tokens(hash_fn, parent_block_hash,
197 + block_tokens)
198 assert manager.block_pool.blocks[block_id].block_hash == block_hash
199 assert manager.block_pool.blocks[block_id].ref_cnt == 1
200 parent_block_hash = block_hash.hash_value
@@ -522,7 +531,8 @@ def test_preallocate_blocks(num_preallocate_tokens: int, block_size: int):
531 assert len(blocks) == 1 + num_preallocated_blocks
532
533
525 -def test_cache_blocks():
534 +@pytest.mark.parametrize("hash_fn", [sha256, hash])
535 +def test_cache_blocks(hash_fn):
536 """
537 This is a unit test that tests the correctness of the _cache_full_blocks
538 function of KVCacheManager.
@@ -550,6 +560,7 @@ def test_cache_blocks():
560 num_cached_blocks=0,
561 num_full_blocks=2,
562 block_size=block_size,
563 + hash_fn=hash_fn,
564 )
565
566 assert len(block_pool.cached_block_hash_to_block) == 2
@@ -564,6 +575,7 @@ def test_cache_blocks():
575 num_cached_blocks=2,
576 num_full_blocks=3,
577 block_size=block_size,
578 + hash_fn=hash_fn,
579 )
580 assert len(block_pool.cached_block_hash_to_block) == 3
581 assert blocks[0].block_hash is not None
tests/v1/engine/test_engine_args.py
+20
@@ -1,5 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2
3 +from argparse import ArgumentError
4 +
5 import pytest
6
7 from vllm import envs
@@ -32,6 +34,24 @@ def test_prefix_caching_from_cli():
34 vllm_config = EngineArgs.from_cli_args(args=args).create_engine_config()
35 assert vllm_config.cache_config.enable_prefix_caching
36
37 + # default hash algorithm is "builtin"
38 + assert vllm_config.cache_config.prefix_caching_hash_algo == "builtin"
39 +
40 + # set hash algorithm to sha256
41 + args = parser.parse_args(["--prefix-caching-hash-algo", "sha256"])
42 + vllm_config = EngineArgs.from_cli_args(args=args).create_engine_config()
43 + assert vllm_config.cache_config.prefix_caching_hash_algo == "sha256"
44 +
45 + # set hash algorithm to builtin
46 + args = parser.parse_args(["--prefix-caching-hash-algo", "builtin"])
47 + vllm_config = EngineArgs.from_cli_args(args=args).create_engine_config()
48 + assert vllm_config.cache_config.prefix_caching_hash_algo == "builtin"
49 +
50 + # an invalid hash algorithm raises an error
51 + parser.exit_on_error = False
52 + with pytest.raises(ArgumentError):
53 + args = parser.parse_args(["--prefix-caching-hash-algo", "invalid"])
54 +
55
56 def test_defaults_with_usage_context():
57 engine_args = EngineArgs(model="facebook/opt-125m")
vllm/config.py
+9
@@ -1124,6 +1124,7 @@ class CacheConfig:
1124 num_gpu_blocks_override: Optional[int] = None,
1125 sliding_window: Optional[int] = None,
1126 enable_prefix_caching: bool = False,
1127 + prefix_caching_hash_algo: str = "builtin",
1128 cpu_offload_gb: float = 0,
1129 calculate_kv_scales: Optional[bool] = None,
1130 ) -> None:
@@ -1135,6 +1136,7 @@ class CacheConfig:
1136 self.is_attention_free = is_attention_free
1137 self.sliding_window = sliding_window
1138 self.enable_prefix_caching = enable_prefix_caching
1139 + self.prefix_caching_hash_algo = prefix_caching_hash_algo
1140 self.cpu_offload_gb = cpu_offload_gb
1141 self.calculate_kv_scales = calculate_kv_scales
1142 self._verify_args()
@@ -1185,6 +1187,13 @@ class CacheConfig:
1187 "Prefix caching is not supported with sliding window. "
1188 "Run with --disable-sliding-window to use prefix caching.")
1189
1190 + if self.enable_prefix_caching and self.prefix_caching_hash_algo not in (
1191 + "builtin", "sha256"):
1192 + raise ValueError(
1193 + "Unknown prefix caching hash algorithm: "
1194 + f"{self.prefix_caching_hash_algo}. Must be either "
1195 + "'builtin' or 'sha256'.")
1196 +
1197 def verify_with_parallel_config(
1198 self,
1199 parallel_config: "ParallelConfig",
vllm/engine/arg_utils.py
+32 -6
@@ -118,6 +118,7 @@ class EngineArgs:
118 max_parallel_loading_workers: Optional[int] = None
119 block_size: Optional[int] = None
120 enable_prefix_caching: Optional[bool] = None
121 + prefix_caching_hash_algo: str = "builtin"
122 disable_sliding_window: bool = False
123 disable_cascade_attn: bool = False
124 use_v2_block_manager: bool = True
@@ -475,6 +476,16 @@ class EngineArgs:
476 help="Enables automatic prefix caching. "
477 "Use ``--no-enable-prefix-caching`` to disable explicitly.",
478 )
479 + parser.add_argument(
480 + "--prefix-caching-hash-algo",
481 + type=str,
482 + choices=["builtin", "sha256"],
483 + default=EngineArgs.prefix_caching_hash_algo,
484 + help="Set the hash algorithm for prefix caching. "
485 + "Options are 'builtin' (Python's built-in hash) or 'sha256' "
486 + "(collision resistant but with certain overheads). Defaults "
487 + "to 'builtin'.",
488 + )
489 parser.add_argument('--disable-sliding-window',
490 action='store_true',
491 help='Disables sliding window, '
@@ -1329,6 +1340,7 @@ class EngineArgs:
1340 num_gpu_blocks_override=self.num_gpu_blocks_override,
1341 sliding_window=model_config.get_sliding_window(),
1342 enable_prefix_caching=self.enable_prefix_caching,
1343 + prefix_caching_hash_algo=self.prefix_caching_hash_algo,
1344 cpu_offload_gb=self.cpu_offload_gb,
1345 calculate_kv_scales=self.calculate_kv_scales,
1346 )
@@ -1737,12 +1749,22 @@ class EngineArgs:
1749 msg = "Chunked prefill is not supported for pooling models"
1750 raise ValueError(msg)
1751
1740 - # Disable prefix caching for multimodal models for VLLM_V0.
1741 - if (model_config.is_multimodal_model and self.enable_prefix_caching):
1742 - logger.warning(
1743 - "--enable-prefix-caching is not supported for multimodal "
1744 - "models in V0 and has been disabled.")
1745 - self.enable_prefix_caching = False
1752 + # if using prefix caching, we must set a hash algo
1753 + if self.enable_prefix_caching:
1754 + # Disable prefix caching for multimodal models for VLLM_V0.
1755 + if model_config.is_multimodal_model:
1756 + logger.warning(
1757 + "--enable-prefix-caching is not supported for multimodal "
1758 + "models in V0 and has been disabled.")
1759 + self.enable_prefix_caching = False
1760 +
1761 + # VLLM_V0 only supports builtin hash algo for prefix caching.
1762 + if self.prefix_caching_hash_algo is None:
1763 + self.prefix_caching_hash_algo = "builtin"
1764 + elif self.prefix_caching_hash_algo == "sha256":
1765 + raise ValueError(
1766 + "sha256 is not supported for prefix caching in V0 engine. "
1767 + "Please use 'builtin'.")
1768
1769 # Set max_num_seqs to 256 for VLLM_V0.
1770 if self.max_num_seqs is None:
@@ -1758,6 +1780,10 @@ class EngineArgs:
1780 if self.enable_prefix_caching is None:
1781 self.enable_prefix_caching = True
1782
1783 + # if using prefix caching, we must set a hash algo
1784 + if self.enable_prefix_caching and self.prefix_caching_hash_algo is None:
1785 + self.prefix_caching_hash_algo = "builtin"
1786 +
1787 # V1 should use the new scheduler by default.
1788 # Swap it only if this arg is set to the original V0 default
1789 if self.scheduler_cls == EngineArgs.scheduler_cls:
vllm/utils.py
+20
@@ -10,6 +10,7 @@ import datetime
10 import enum
11 import gc
12 import getpass
13 +import hashlib
14 import importlib
15 import importlib.metadata
16 import importlib.util
@@ -17,6 +18,7 @@ import inspect
18 import ipaddress
19 import multiprocessing
20 import os
21 +import pickle
22 import re
23 import signal
24 import socket
@@ -2442,3 +2444,21 @@ def cprofile(save_file: Optional[str] = None, enabled: bool = True):
2444 return wrapper
2445
2446 return decorator
2447 +
2448 +
2449 +def sha256(input) -> int:
2450 + """Hash any picklable Python object using SHA-256.
2451 +
2452 + The input is serialized using pickle before hashing, which allows
2453 + arbitrary Python objects to be used. Note that this function does
2454 + not use a hash seed—if you need one, prepend it explicitly to the input.
2455 +
2456 + Args:
2457 + input: Any picklable Python object.
2458 +
2459 + Returns:
2460 + An integer representing the SHA-256 hash of the serialized input.
2461 + """
2462 + input_bytes = pickle.dumps(input, protocol=pickle.HIGHEST_PROTOCOL)
2463 + return int.from_bytes(hashlib.sha256(input_bytes).digest(),
2464 + byteorder="big")
vllm/v1/core/block_pool.py
+11 -9
@@ -1,7 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 from collections import defaultdict
3 from collections.abc import Iterable
4 -from typing import Optional
4 +from typing import Callable, Optional
5
6 from vllm.logger import init_logger
7 from vllm.v1.core.kv_cache_utils import (BlockHashType, FreeKVCacheBlockQueue,
@@ -15,10 +15,10 @@ logger = init_logger(__name__)
15
16 class BlockPool:
17 """BlockPool that manages KVCacheBlocks.
18 - It provides methods to allocate, free and cache the kv cache blocks. The
19 - free_block_queue stores the free blocks in eviction order to enable
20 - allocation, free, and cache eviction. The cached_block_hash_to_block
21 - maps between block hash and cached block to support finding cached blocks
18 + It provides methods to allocate, free and cache the kv cache blocks. The
19 + free_block_queue stores the free blocks in eviction order to enable
20 + allocation, free, and cache eviction. The cached_block_hash_to_block
21 + maps between block hash and cached block to support finding cached blocks
22 by their block hash.
23
24 Args:
@@ -75,11 +75,12 @@ class BlockPool:
75 num_cached_blocks: int,
76 num_full_blocks: int,
77 block_size: int,
78 + hash_fn: Callable,
79 ) -> None:
80 """Cache a list of full blocks for prefix caching.
81 This function takes a list of blocks that will have their block hash
82 metadata to be updated and cached. Given a request, it computes the
82 - block hashes for the blocks starting from `num_cached_blocks` to
83 + block hashes for the blocks starting from `num_cached_blocks` to
84 `num_full_blocks`, updating the metadata for each block
85 and caching them in the `cached_block_hash_to_block`.
86
@@ -87,12 +88,13 @@ class BlockPool:
88 request: The request to cache the blocks.
89 blocks: All blocks in the request.
90 block_hashes: Block hashes of the blocks in the request. Note that
90 - this list may be shorter than the blocks list. In this case the
91 + this list may be shorter than the blocks list. In this case the
92 missed block hash will be computed in this function.
93 num_cached_blocks: The number of blocks that are already cached.
93 - num_full_blocks: The number of blocks that are full and should
94 + num_full_blocks: The number of blocks that are full and should
95 be cached after this function.
96 block_size: Number of tokens in each block.
97 + hash_fn: The hash function to use for block hashes.
98 """
99 if num_cached_blocks == num_full_blocks:
100 return
@@ -138,7 +140,7 @@ class BlockPool:
140 request, start_token_idx, end_token_idx, -1)
141
142 # Compute the hash of the current block.
141 - block_hash = hash_block_tokens(prev_block_hash_value,
143 + block_hash = hash_block_tokens(hash_fn, prev_block_hash_value,
144 block_tokens, extra_keys)
145 block_hashes.append(block_hash)
146
vllm/v1/core/kv_cache_manager.py
+6 -2
@@ -5,7 +5,7 @@ from collections.abc import Iterable
5 from typing import Optional
6
7 from vllm.logger import init_logger
8 -from vllm.utils import cdiv
8 +from vllm.utils import cdiv, sha256
9 from vllm.v1.core.block_pool import BlockPool
10 from vllm.v1.core.kv_cache_utils import (BlockHashType, KVCacheBlock,
11 hash_request_tokens)
@@ -24,6 +24,7 @@ class KVCacheManager:
24 max_model_len: int,
25 sliding_window: Optional[int] = None,
26 enable_caching: bool = True,
27 + caching_hash_algo: str = "builtin",
28 num_preallocate_tokens: int = 64,
29 log_stats: bool = False,
30 ) -> None:
@@ -33,6 +34,7 @@ class KVCacheManager:
34 self.max_num_blocks_per_req = cdiv(max_model_len, block_size)
35 self.sliding_window = sliding_window
36 self.enable_caching = enable_caching
37 + self.caching_hash_fn = sha256 if caching_hash_algo == "sha256" else hash
38 # FIXME: make prefix cache stats conditional on log_stats
39 self.log_stats = log_stats
40 # NOTE(woosuk): To avoid frequent block allocation, we preallocate some
@@ -109,7 +111,8 @@ class KVCacheManager:
111 # if the scheduler has tried to schedule the request before.
112 block_hashes = self.req_to_block_hashes[request.request_id]
113 if not block_hashes:
112 - block_hashes = hash_request_tokens(self.block_size, request)
114 + block_hashes = hash_request_tokens(self.caching_hash_fn,
115 + self.block_size, request)
116 self.req_to_block_hashes[request.request_id] = block_hashes
117
118 self.prefix_cache_stats.requests += 1
@@ -247,6 +250,7 @@ class KVCacheManager:
250 num_cached_blocks=num_cached_blocks,
251 num_full_blocks=num_full_blocks_after_append,
252 block_size=self.block_size,
253 + hash_fn=self.caching_hash_fn,
254 )
255
256 self.num_cached_block[
vllm/v1/core/kv_cache_utils.py
+43 -32
@@ -1,12 +1,14 @@
1 # SPDX-License-Identifier: Apache-2.0
2 """KV-Cache Utilities."""
3 +import os
4 from collections import deque
5 from collections.abc import Sequence
6 from dataclasses import dataclass
6 -from typing import Any, NamedTuple, Optional
7 +from typing import Any, Callable, NamedTuple, Optional
8
9 from vllm.config import VllmConfig
10 from vllm.logger import init_logger
11 +from vllm.utils import sha256
12 from vllm.v1.kv_cache_interface import (KVCacheConfig, KVCacheGroupSpec,
13 KVCacheSpec, KVCacheTensor)
14 from vllm.v1.metrics.stats import PrefixCacheStats
@@ -18,9 +20,8 @@ logger = init_logger(__name__)
20 class BlockHashType(NamedTuple):
21 """Hash value of a block (int), the token IDs in the block, and extra keys.
22 We keep a tuple of token IDs and extra keys to reduce the likelihood of
21 - hash collisions when the hash value is the same. But please note that
22 - hash collisions can still theoretically occur, albeit with an extremely
23 - low probability.
23 + hash collisions when the hash value is the same. By using SHA256 however,
24 + hash collisions are practically impossible.
25 """
26 # Hash value of the block in an integer.
27 hash_value: int
@@ -30,6 +31,20 @@ class BlockHashType(NamedTuple):
31 extra_keys: Optional[Any] = None
32
33
34 +# The hash seed for the first block of the prefix block sequence.
35 +#
36 +# Even if the hash function is the builtin hash(), we use sha256 to generate
37 +# the initial hash to simplify the code. This is not performance critical
38 +# as it is done one per process.
39 +#
40 +# We use a random value to avoid hash collisions or PYTHONHASHSEED environment
41 +# variable if set such that processes can share the seed if needed.
42 +# This aligns with the behavior of Python's hash() function, which also uses
43 +# a random seed if PYTHONHASHSEED is not set.
44 +NONE_HASH = int.from_bytes(os.urandom(32), byteorder="big") if os.getenv(
45 + 'PYTHONHASHSEED') is not None else sha256(os.getenv('PYTHONHASHSEED'))
46 +
47 +
48 class PrefixCachingMetrics:
49 """Metrics for prefix caching with a hit rate of the most recent N requests.
50
@@ -148,7 +163,7 @@ class FreeKVCacheBlockQueue:
163 builtin deque to support removing a block in the middle of the queue
164 in O(1) time. To close the performance gap to the builtin deque which is
165 implemented in C++, this class does not allocate any Python objects when
151 - manipulating the linked list. Instead, this class manipulates the
166 + manipulating the linked list. Instead, this class manipulates the
167 prev_free_block and next_free_block attributes of the given blocks.
168
169 The queue is ordered by block ID in the beginning. When a block is allocated
@@ -178,7 +193,7 @@ class FreeKVCacheBlockQueue:
193
194 def popleft(self) -> KVCacheBlock:
195 """Pop the first free block and reduce num_free_blocks by 1.
181 -
196 +
197 Returns:
198 The first free block.
199 """
@@ -191,7 +206,7 @@ class FreeKVCacheBlockQueue:
206
207 def remove(self, block: KVCacheBlock) -> None:
208 """Remove a block in the free list and reduce num_free_blocks by 1.
194 -
209 +
210 Args:
211 block: The block to remove.
212 """
@@ -235,7 +250,7 @@ class FreeKVCacheBlockQueue:
250
251 def get_all_free_blocks(self) -> list[KVCacheBlock]:
252 """Get all free blocks in the free list. Mainly used for testing.
238 -
253 +
254 Returns:
255 A list of free blocks.
256 """
@@ -251,10 +266,10 @@ def need_extra_keys(request: Request) -> bool:
266 """Check whether the blocks allocated to this request need extra hash keys.
267
268 Args:
254 - request (Request): The request.
269 + request (Request): The request.
270
271 Returns:
257 - bool: Whether blocks allocated to this request need extra hash keys.
272 + bool: Whether blocks allocated to this request need extra hash keys.
273 """
274
275 # Multimodal requests need to include the MM hash.
@@ -269,13 +284,13 @@ def _gen_mm_extra_hash_keys(request: Request, start_token_idx: int,
284 computation. For multi-modal inputs, the extra keys are
285 (mm_hash, start_offset) that indicate a mm input contained in the
286 block and its starting offset in the block tokens.
272 -
287 +
288 Args:
289 request: The request object.
290 start_token_idx: The start token index of the block.
291 end_token_idx: The end token index of the block.
292 start_mm_idx: The start multi-modal index of the block.
278 -
293 +
294 Returns:
295 A tuple of extra keys and the next multi-modal index.
296 """
@@ -333,10 +348,10 @@ def _gen_mm_extra_hash_keys(request: Request, start_token_idx: int,
348
349 def _gen_lora_extra_hash_keys(request: Request) -> list[int]:
350 """Generate extra keys related to LoRA for block hash computation.
336 -
351 +
352 Args:
353 request: The request object.
339 -
354 +
355 Returns:
356 Return LoRA id of the request if it is a LoRA request. Return empty
357 list otherwise.
@@ -351,13 +366,13 @@ def generate_block_hash_extra_keys(
366 start_mm_idx: int) -> tuple[Optional[tuple[Any, ...]], int]:
367 """Generate extra keys for the block hash. The extra keys can come from
368 the multi-modal inputs and request specific metadata (e.g., LoRA ID).
354 -
369 +
370 Args:
371 request: The request object.
372 start_token_idx: The start token index of the block.
373 end_token_idx: The end token index of the block.
374 start_mm_idx: The start multi-modal index of the block.
360 -
375 +
376 Returns:
377 A tuple of extra keys and the next multi-modal index.
378 """
@@ -375,6 +390,7 @@ def generate_block_hash_extra_keys(
390
391
392 def hash_block_tokens(
393 + hash_function: Callable,
394 parent_block_hash: Optional[int],
395 curr_block_token_ids: Sequence[int],
396 extra_keys: Optional[tuple[Any, ...]] = None) -> BlockHashType:
@@ -395,21 +411,16 @@ def hash_block_tokens(
411 The entire tuple is used as the hash key of the block.
412 """
413 if not parent_block_hash:
398 - # Note that we use 'None' as a string here instead of None because
399 - # as of Python 3.12, hash(None) returns a constant predictable value.
400 - # This could possibly make it easier to find and exploit hash
401 - # collisions. 'None' as a string will be hashed differently per process,
402 - # but consistently within the same process. This is the same as the
403 - # behavior of None prior to Python 3.12.
404 - parent_block_hash = hash('None')
414 + parent_block_hash = NONE_HASH
415
416 curr_block_token_ids_tuple = tuple(curr_block_token_ids)
417 return BlockHashType(
408 - hash((parent_block_hash, curr_block_token_ids_tuple, extra_keys)),
418 + hash_function(
419 + (parent_block_hash, curr_block_token_ids_tuple, extra_keys)),
420 curr_block_token_ids_tuple, extra_keys)
421
422
412 -def hash_request_tokens(block_size: int,
423 +def hash_request_tokens(hash_function: Any, block_size: int,
424 request: Request) -> list[BlockHashType]:
425 """Computes hash values of a chain of blocks given a sequence of
426 token IDs. The hash value is used for prefix caching.
@@ -441,7 +452,7 @@ def hash_request_tokens(block_size: int,
452 req_extra_keys, curr_mm_idx = generate_block_hash_extra_keys(
453 request, start, end, curr_mm_idx)
454
444 - block_hash = hash_block_tokens(parent_block_hash_value,
455 + block_hash = hash_block_tokens(hash_function, parent_block_hash_value,
456 block_token_ids, req_extra_keys)
457 ret.append(block_hash)
458 parent_block_hash_value = block_hash.hash_value
@@ -452,7 +463,7 @@ def check_enough_kv_cache_memory(vllm_config: VllmConfig,
463 kv_cache_spec: dict[str, KVCacheSpec],
464 available_memory: int):
465 """
455 - Checks whether `available_memory` is enough for the KV cache to hold at
466 + Checks whether `available_memory` is enough for the KV cache to hold at
467 least one request with the model's max_model_len.
468
469 Args:
@@ -489,15 +500,15 @@ def create_kv_cache_group_specs(
500 grouped_layer_names: list[list[str]]) -> list[KVCacheGroupSpec]:
501 """
502 Create KVCacheGroupSpec object for each kv cache group layer.
492 - The layers in the same group should share the same
503 + The layers in the same group should share the same
504 KVCacheSpec.
505
506 Args:
507 kv_cache_spec:
508 A mapping from each layer name to its corresponding KVCacheSpec.
509 grouped_layer_names:
499 - A list of kv cache groups, where each element is a list of layer
500 - names that belong to the same group and should share the same
510 + A list of kv cache groups, where each element is a list of layer
511 + names that belong to the same group and should share the same
512 KVCacheSpec.
513 Returns:
514 A list of KVCacheGroupSpec objects, one for each group.
@@ -614,11 +625,11 @@ def get_kv_cache_config(vllm_config: VllmConfig,
625
626 def unify_kv_cache_configs(kv_cache_configs: list[KVCacheConfig]):
627 """
617 - Make the KV cache configurations for each worker consistent, so that all
628 + Make the KV cache configurations for each worker consistent, so that all
629 workers can be controlled by the same KVCacheManager.
630 This function verifies that the layer group of each worker are the same,
631 and changes the num_blocks of each worker to the smallest among all workers.
621 -
632 +
633 Args:
634 kv_cache_configs: The KV cache configurations for each worker. Will be
635 in-place modified to make them consistent.
vllm/v1/core/sched/scheduler.py
+1
@@ -61,6 +61,7 @@ class Scheduler(SchedulerInterface):
61 max_model_len=self.max_model_len,
62 sliding_window=self.cache_config.sliding_window,
63 enable_caching=self.cache_config.enable_prefix_caching,
64 + caching_hash_algo=self.cache_config.prefix_caching_hash_algo,
65 log_stats=self.log_stats)
66 self.block_size = self.cache_config.block_size
67