Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b7052cb
fix: Reset resumed streaming session's block table
albertoperdomo2 Oct 9, 2026
c3b0b51
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 9, 2026
34d9136
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 9, 2026
e016a28
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 11, 2026
9d6e1b8
chore: Rename loop variable to be more accurate
albertoperdomo2 Oct 14, 2026
3aae52b
fix: Reset SimpleCPUOffload block table for resumed streaming sessions
albertoperdomo2 Oct 14, 2026
1d584b9
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 14, 2026
3b799b8
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 15, 2026
38f13b4
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 16, 2026
6ada3af
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 16, 2026
da25bb5
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 17, 2026
e26bc84
Merge branch 'main' into fix/offload-streaming-session-block-ids
albertoperdomo2 Oct 18, 2026
dcde27b
Merge branch 'main' into fix/offload-streaming-session-block-ids
mergify[bot] Oct 21, 2026
7122c97
fix: Preserve num_store_blocks on streaming continuation
albertoperdomo2 Oct 21, 2026
7d21aa4
Merge branch 'main' into fix/offload-streaming-session-block-ids
mergify[bot] Oct 22, 2026
fe973a7
Merge branch 'main' into fix/offload-streaming-session-block-ids
vllm-agent Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,24 @@ def take_raw_events():
assert events[0].token_ids == token_ids


@pytest.mark.parametrize("async_scheduling", [True, False])
def test_streaming_session_stores_new_chunks(request_runner, async_scheduling: bool):
"""A streaming session resumed for its next input chunk arrives with its
full block table, so its new chunks must be stored from their own blocks."""
runner = request_runner(
block_size=4, num_gpu_blocks=100, async_scheduling=async_scheduling
)
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
generate_store_output(keys)
)

runner.new_request(token_ids=list(range(1, 9)), req_id="s", resumable=True)
runner.run(decoded_tokens=[100, EOS_TOKEN_ID], expected_stored=(0, 1))

runner.new_request(token_ids=list(range(200, 212)), req_id="s", resumable=True)
runner.run(decoded_tokens=[300, EOS_TOKEN_ID], expected_stored=(2, 3, 4))


@pytest.mark.parametrize("async_scheduling", [True, False])
def test_offloading_connector(request_runner, async_scheduling: bool):
block_size = 4
Expand Down
9 changes: 7 additions & 2 deletions tests/v1/kv_connector/unit/offloading_connector/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,12 @@ def new_request(
token_ids: list[int],
kv_transfer_params: dict | None = None,
skip_reading_prefix_cache: bool = False,
req_id: str | None = None,
resumable: bool = False,
):
self.req_id += 1
if req_id is None:
self.req_id += 1
req_id = str(self.req_id)

sampling_params = SamplingParams(
max_tokens=1000,
Expand All @@ -383,11 +387,12 @@ def new_request(
sampling_params.update_from_generation_config({}, EOS_TOKEN_ID)

req = Request(
request_id=str(self.req_id),
request_id=req_id,
prompt_token_ids=token_ids,
sampling_params=sampling_params,
pooling_params=None,
block_hasher=self._block_hasher,
resumable=resumable,
)
if kv_transfer_params is not None:
req.kv_transfer_params = kv_transfer_params
Expand Down
37 changes: 37 additions & 0 deletions tests/v1/simple_kv_offload/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1686,6 +1686,43 @@ def test_eager_store_resets_block_cursor_after_preemption(
assert resumed_meta.store_gpu_blocks == [resumed_blocks.blocks[0][2].block_id]


def test_eager_store_streaming_session_stores_new_chunks() -> None:
"""A resumed streaming session is rescheduled as a new request carrying
its full block table without being preempted. The accumulated block IDs
must be replaced, not extended, so the new chunk's blocks get stored."""
fix = make_scheduler(num_cpu_blocks=8, num_gpu_blocks=8, lazy=False)
sched = fix.scheduler
req = make_request(num_blocks=4)

gpu_blocks = fix.gpu_block_pool.get_new_blocks(2)
fix.gpu_block_pool.cache_full_blocks(req, gpu_blocks, 0, 2, BLOCK_SIZE, 0)
req.num_computed_tokens = 2 * BLOCK_SIZE
first_turn = KVCacheBlocks(blocks=(gpu_blocks,))
sched.update_state_after_alloc(req, first_turn, num_external_tokens=0)
first_meta = sched.build_connector_meta(
make_scheduler_output(
{req.request_id: 2 * BLOCK_SIZE},
new_reqs={req.request_id: first_turn.get_block_ids()},
)
)
assert len(first_meta.store_gpu_blocks) == 2
simulate_store_completion(sched, first_meta.store_event)

# The paused session keeps its blocks and grows by a new chunk.
gpu_blocks = gpu_blocks + fix.gpu_block_pool.get_new_blocks(2)
fix.gpu_block_pool.cache_full_blocks(req, gpu_blocks, 2, 4, BLOCK_SIZE, 0)
req.num_computed_tokens = 4 * BLOCK_SIZE
second_turn = KVCacheBlocks(blocks=(gpu_blocks,))
sched.update_state_after_alloc(req, second_turn, num_external_tokens=0)
second_meta = sched.build_connector_meta(
make_scheduler_output(
{req.request_id: 2 * BLOCK_SIZE},
new_reqs={req.request_id: second_turn.get_block_ids()},
)
)
assert second_meta.store_gpu_blocks == [b.block_id for b in gpu_blocks[2:]]


# ---------------------------------------------------------------------------
# Test 10: Partial GPU prefix hit + CPU load + new compute blocks
# ---------------------------------------------------------------------------
Expand Down
6 changes: 4 additions & 2 deletions vllm/distributed/kv_transfer/kv_connector/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,14 @@ def yield_req_data(
scheduler_output,
) -> Iterator[tuple[str, tuple[list[int], ...] | None, bool]]:
"""Yields:
(req_id, new_block_id_groups, preempted)
(req_id, block_id_groups, is_full_table)
``is_full_table`` is True when ``block_id_groups`` is the request's whole
block table (new and resumed requests) rather than this step's additions.
"""
# new requests
for req_data in scheduler_output.scheduled_new_reqs:
yield req_data.req_id, req_data.block_ids, False
yield req_data.req_id, req_data.block_ids, True

# cached requests
cached_reqs = scheduler_output.scheduled_cached_reqs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1476,23 +1476,23 @@ def _update_req_states(self, scheduler_output: SchedulerOutput) -> None:
# Used to detect sliding window blocks that got re-allocated.
new_block_ids_end: dict[str, tuple[int, ...]] = {}

for req_id, new_block_id_groups, preempted in yield_req_data(scheduler_output):
for req_id, block_id_groups, is_full_table in yield_req_data(scheduler_output):
req_status = self._req_status[req_id]
req_status.update_offload_keys()

if preempted:
if is_full_table:
for group_state in req_status.group_states:
group_state.block_ids.clear()

if new_block_id_groups:
if block_id_groups:
if self._sliding_window_groups:
new_block_ids_end[req_id] = tuple(
len(req_status.group_states[grp_idx].block_ids)
for grp_idx in self._sliding_window_groups
)
req_status.update_block_id_groups(new_block_id_groups)
req_status.update_block_id_groups(block_id_groups)
for group_config in self.config.kv_group_configs:
new_blocks = new_block_id_groups[group_config.group_idx]
new_blocks = block_id_groups[group_config.group_idx]
for bid in new_blocks:
if bid != 0:
self._current_batch_allocated_block_ids.add(bid)
Expand Down
13 changes: 8 additions & 5 deletions vllm/v1/simple_kv_offload/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,16 +779,19 @@ def _prepare_eager_store_specs(
state.block_ids = tuple([] for _ in range(num_groups))
state.num_stored_blocks = [0] * num_groups

for req_id, new_block_id_groups, _ in yield_req_data(scheduler_output):
for req_id, block_id_groups, is_full_table in yield_req_data(scheduler_output):
state = self._reqs_to_store.get(req_id)
if state is None or state.finished:
continue

if new_block_id_groups:
for g in range(min(num_groups, len(new_block_id_groups))):
if new_block_id_groups[g] is not None:
if is_full_table:
state.block_ids = tuple([] for _ in range(num_groups))

if block_id_groups:
for g in range(min(num_groups, len(block_id_groups))):
if block_id_groups[g] is not None:
# Accumulate new block IDs.
state.block_ids[g].extend(new_block_id_groups[g])
state.block_ids[g].extend(block_id_groups[g])

num_new_tokens = scheduler_output.num_scheduled_tokens.get(req_id, 0)
if num_new_tokens == 0:
Expand Down
Loading