[Core] Include the LoRA path in prefix-cache block hashes (#59335)
Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Nick Hill committed
Sep 30, 2026 at 17:37 UTC
c055c1e075ed461cff922aa710d387281f651940
3 files changed
+43
-7
tests/v1/core/test_kv_cache_utils.py
+24
-2
@@ -1022,7 +1022,7 @@ def test_generate_block_hash_extra_keys_lora():
1022
)
1023
1024
extra_keys, _ = generate_block_hash_extra_keys(request, 0, 3, 0)
1025
- assert extra_keys == (("lora", "test_lora_adapter"),)
1025
+ assert extra_keys == (("lora", "test_lora_adapter", "/path/to/lora"),)
1026
1027
request.lora_request = None
1028
extra_keys, _ = generate_block_hash_extra_keys(request, 0, 3, 0)
@@ -1048,6 +1048,28 @@ def test_lora_name_and_cache_salt_block_hashes_do_not_collide(hash_fn):
1048
assert lora_req.block_hashes[0] != salted_req.block_hashes[0]
1049
1050
1051
+@pytest.mark.parametrize("hash_fn", [sha256, sha256_cbor])
1052
+def test_lora_path_change_changes_block_hashes(hash_fn):
1053
+ """Re-pointing a LoRA name at another adapter must not reuse its blocks."""
1054
+
1055
+ def make_lora_request(lora_path: str) -> Request:
1056
+ return Request(
1057
+ request_id=lora_path,
1058
+ prompt_token_ids=[0, 1, 2],
1059
+ sampling_params=SamplingParams(max_tokens=1),
1060
+ pooling_params=None,
1061
+ lora_request=LoRARequest(
1062
+ lora_name="foo", lora_int_id=1, lora_path=lora_path
1063
+ ),
1064
+ block_hasher=get_request_block_hasher(3, hash_fn),
1065
+ )
1066
+
1067
+ first = make_lora_request("/path/to/lora_a")
1068
+ second = make_lora_request("/path/to/lora_b")
1069
+
1070
+ assert first.block_hashes[0] != second.block_hashes[0]
1071
+
1072
+
1073
def test_to_event_extra_keys_keeps_untagged_event_format():
1074
"""KV events keep publishing the extra-key shapes consumers already parse."""
1075
request = make_request(
@@ -1064,7 +1086,7 @@ def test_to_event_extra_keys_keeps_untagged_event_format():
1086
extra_keys, _ = generate_block_hash_extra_keys(request, 0, 5, 0)
1087
1088
assert extra_keys == (
1067
- ("lora", "adapter"),
1089
+ ("lora", "adapter", "/path/to/lora"),
1090
("mm", "hash1", 2),
1091
("cache_salt", "salt"),
1092
)
vllm/entrypoints/openai/models/serving.py
+10
@@ -212,6 +212,16 @@ class OpenAIServingModels:
212
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
213
)
214
215
+ previous = self.lora_requests.get(lora_name)
216
+ if previous is not None and previous.lora_path == lora_path:
217
+ logger.warning(
218
+ "Reloaded LoRA adapter '%s' in place from the same path '%s'. "
219
+ "Prefix-cache blocks computed with its previous weights are "
220
+ "not invalidated; load changed weights from a new path to "
221
+ "avoid reusing them.",
222
+ lora_name,
223
+ lora_path,
224
+ )
225
self.lora_requests[lora_name] = lora_request
226
logger.info(
227
"Loaded new LoRA adapter: name '%s', path '%s'", lora_name, lora_path
vllm/v1/core/kv_cache_utils.py
+9
-5
@@ -565,20 +565,24 @@ def _gen_mm_extra_hash_keys(
565
return extra_keys, curr_mm_idx
566
567
568
-def _gen_lora_extra_hash_keys(request: Request) -> list[tuple[str, str]]:
568
+def _gen_lora_extra_hash_keys(request: Request) -> list[tuple[str, str, str]]:
569
"""Generate extra keys related to LoRA for block hash computation.
570
571
+ The adapter path is included so that re-pointing a LoRA name at a different
572
+ adapter does not reuse KV computed with the previous one.
573
+
574
Args:
575
request: The request object.
576
577
Returns:
575
- Return LoRA name of the request if it is a LoRA request. Return empty
576
- list otherwise.
578
+ Return the LoRA name and path of the request if it is a LoRA request.
579
+ Return empty list otherwise.
580
581
"""
579
- if not request.lora_request:
582
+ lora_request = request.lora_request
583
+ if not lora_request:
584
return []
581
- return [("lora", request.lora_request.lora_name)]
585
+ return [("lora", lora_request.lora_name, lora_request.lora_path)]
586
587
588
def _gen_prompt_embeds_extra_hash_keys(