[Bugfix][KV Cache] Fix incremental multimodal block hashing (#51694)

Signed-off-by: Jellow <49915976+CZT0@users.noreply.github.com> Signed-off-by: Jellow <dvdx@foxmail.com>

Jellow committed Sep 25, 2026 at 06:38 UTC e6c07ea5763bada5da789ed437851ee1526fd5a8
2 files changed +41 -6
tests/v1/core/test_kv_cache_utils.py
+31
@@ -1018,6 +1018,37 @@ def test_request_block_hasher(hash_fn):
1018 assert block_hashes[1] == hash_fn((block_hashes[0], (3, 4, 5), (("hash2", 0),)))
1019
1020
1021 +@pytest.mark.parametrize("hash_fn", [sha256, sha256_cbor])
1022 +def test_request_block_hasher_incremental_append_with_multiple_mm_features(hash_fn):
1023 + mm_positions = [
1024 + PlaceholderRange(offset=4, length=2),
1025 + PlaceholderRange(offset=6, length=1),
1026 + ]
1027 + incremental = make_request(
1028 + request_id="incremental",
1029 + prompt_token_ids=list(range(7)),
1030 + block_size=4,
1031 + hash_fn=hash_fn,
1032 + mm_positions=mm_positions,
1033 + mm_hashes=["A", "B"],
1034 + )
1035 + incremental.append_output_token_ids(7)
1036 + fresh = make_request(
1037 + request_id="fresh",
1038 + prompt_token_ids=list(range(8)),
1039 + block_size=4,
1040 + hash_fn=hash_fn,
1041 + mm_positions=mm_positions,
1042 + mm_hashes=["A", "B"],
1043 + )
1044 +
1045 + expected_second_hash = hash_fn(
1046 + (incremental.block_hashes[0], (4, 5, 6, 7), (("A", 0), ("B", 2)))
1047 + )
1048 + assert incremental.block_hashes[1] == expected_second_hash
1049 + assert incremental.block_hashes == fresh.block_hashes
1050 +
1051 +
1052 @pytest.mark.parametrize("hash_fn", [sha256, sha256_cbor])
1053 def test_hash_tokens_different_mm_input(hash_fn):
1054 request1 = make_request(
vllm/v1/core/kv_cache_utils.py
+10 -6
@@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple, NewType, TypeAlias, cast, ove
15 from vllm import envs
16 from vllm.config import VllmConfig
17 from vllm.logger import init_logger
18 +from vllm.multimodal.utils import get_mm_features_in_window
19 from vllm.utils.hashing import xxhash, xxhash_cbor
20 from vllm.utils.math_utils import cdiv, round_up
21 from vllm.utils.mem_utils import format_gib
@@ -845,12 +846,15 @@ def get_request_block_hasher(
846 return []
847
848 curr_mm_idx = 0
848 - if start_token_idx > 0:
849 - # Set curr_mm_idx = -1 to indicate the last mm input.
850 - # Note that since we reach to this branch only when the block is
851 - # completed with generated tokens, we only need to consider the
852 - # last mm input.
853 - curr_mm_idx = -1
849 + mm_features = request.mm_features
850 + if start_token_idx > 0 and mm_features:
851 + last_mm_pos = mm_features[-1].mm_position
852 + if last_mm_pos.offset + last_mm_pos.length > start_token_idx:
853 + curr_mm_idx, _ = get_mm_features_in_window(
854 + mm_features,
855 + start_token_idx,
856 + start_token_idx + hash_block_size,
857 + )
858
859 prev_block_hash_value = (
860 request.block_hashes[-1] if request.block_hashes else None