[Bugfix][Core] Drop stale block hashes when a session truncates tokens (#59103)

Signed-off-by: David Cheung <d7cheung@gmail.com>
Co-authored-by: Brian Woods <me@brian.biz>
Co-authored-by: Change72 <changg@nvidia.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Cheung
2026-10-06 02:51:03 +00:00
committed by GitHub
co-authored by Brian Woods Change72 Claude Opus 5
parent b0b23a48d4
commit 2b32d9ac6a
2 changed files with 50 additions and 0 deletions
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock
import numpy as np
import torch
import vllm.v1.core.kv_cache_utils as kv_cache_utils
from vllm.config import DeviceConfig, VllmConfig
from vllm.multimodal.inputs import (
MultiModalFeatureSpec,
@@ -14,6 +15,12 @@ from vllm.multimodal.inputs import (
PlaceholderRange,
)
from vllm.sampling_params import SamplingParams
from vllm.utils.hashing import sha256
from vllm.v1.core.kv_cache_utils import (
get_request_block_hasher,
hash_block_tokens,
init_none_hash,
)
from vllm.v1.core.sched.scheduler import Scheduler
from vllm.v1.engine import FinishReason
from vllm.v1.kv_cache_interface import (
@@ -365,6 +372,48 @@ class TestStreamingScheduler(unittest.TestCase):
num_new_tokens = session.num_tokens - session.num_computed_tokens
assert num_new_tokens == 2
def test_update_request_as_session_drops_stale_block_hashes(self):
"""A discarded sampled token must not stay fingerprinted in
block_hashes (#49377)."""
init_none_hash(sha256)
scheduler = create_scheduler()
hash_block_size = scheduler.hash_block_size
# hash_block_size - 1 prompt tokens, so the discarded sampled token
# completes hash block 0.
prompt = list(range(1, hash_block_size))
discarded_token = 999
session = Request(
request_id="session",
prompt_token_ids=list(prompt),
sampling_params=SamplingParams(max_tokens=16),
pooling_params=None,
block_hasher=get_request_block_hasher(hash_block_size, sha256),
resumable=True,
)
session.append_output_token_ids(discarded_token)
assert len(session.block_hashes) == 1
stale_hash = session.block_hashes[0]
session.num_computed_tokens = len(prompt)
next_token = 42
update = StreamingUpdate.from_request(
DummyRequest(request_id="session", prompt_token_ids=[next_token])
)
scheduler._update_request_as_session(session, update)
assert list(session.all_token_ids) == prompt + [next_token]
assert len(session.block_hashes) == 1
expected = hash_block_tokens(
sha256, kv_cache_utils.NONE_HASH, tuple(prompt + [next_token])
)
assert session.block_hashes[0] == expected, (
"block_hashes[0] does not identify the current token sequence"
)
assert session.block_hashes[0] != stale_hash, (
"block_hashes[0] still fingerprints the discarded sampled token"
)
def test_streaming_e2e_lifecycle(self):
"""Comprehensive integration test covering complete streaming request lifecycle
including scheduler state management and aliasing bug prevention.
+1
View File
@@ -1628,6 +1628,7 @@ class Scheduler(SchedulerInterface):
session.num_prompt_tokens : num_computed_tokens
]
del session._all_token_ids[num_computed_tokens:]
del session.block_hashes[num_computed_tokens // self.hash_block_size :]
session._output_token_ids.clear()
assert session.prompt_token_ids is not None
# Extend prompt with kept output tokens.