[kv_offload] Implement reset_cache() for the offloading connector (#41956)

Signed-off-by: Martin Hickey <martin.hickey@ie.ibm.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Or Ozeri <or@ozery.com>
This commit is contained in:
Martin Hickey
2026-05-14 16:00:10 +03:00
committed by GitHub
co-authored by gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Or Ozeri
parent 6548560496
commit 5bd8c71e79
8 changed files with 153 additions and 0 deletions
@@ -919,3 +919,80 @@ def test_complete_store_called_per_job(request_runner, async_scheduling: bool):
# Finish: no store pending -> no further call.
runner.run(decoded_tokens=[EOS_TOKEN_ID])
assert runner.manager.complete_store.call_count == 0
@pytest.mark.parametrize("async_scheduling", [True, False])
def test_reset_cache(request_runner, async_scheduling: bool):
"""reset_cache flushes in-flight loads, calls manager.reset_cache(), resets
next_stored_block_idx for active requests and clears job tracking."""
block_size = 4
block_size_factor = 3
offloaded_block_size = block_size * block_size_factor
num_gpu_blocks = 100
runner = request_runner(
block_size=block_size,
num_gpu_blocks=num_gpu_blocks,
async_scheduling=async_scheduling,
block_size_factor=block_size_factor,
)
# Store 1 offloaded block (3 GPU blocks) to CPU.
runner.new_request(token_ids=[0] * offloaded_block_size)
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
generate_store_output(keys)
)
runner.run(decoded_tokens=[EOS_TOKEN_ID], expected_stored=(0, 1, 2))
# Reset GPU prefix cache then start a request that loads from CPU.
# Leave the load in-flight so that reset_cache must flush it.
runner.scheduler.reset_prefix_cache()
runner.new_request(token_ids=[0] * offloaded_block_size)
runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
generate_store_output([])
)
runner.run(decoded_tokens=[], complete_transfers=False)
# Capture in-flight load job IDs before reset.
load_job_ids = {
jid
for jid, status in runner.connector_scheduler._jobs.items()
if not status.is_store
}
assert load_job_ids, "expected in-flight load jobs before reset"
# Record job counter to verify the reset counter is set correctly.
job_counter_before_reset = runner.connector_scheduler._job_counter
# After update_state_after_alloc, next_stored_block_idx is advanced to
# skip the loaded prefix; reset_cache must bring it back to 0.
for req_status in runner.connector_scheduler._req_status.values():
for group_state in req_status.group_states:
assert group_state.next_stored_block_idx > 0
# Reset the cache
runner.connector_scheduler.reset_cache()
# manager.reset_cache() must be called exactly once.
runner.manager.reset_cache.assert_called_once()
# In-flight load jobs must be queued for flushing to prevent CUDA stream
# races between old loads and new post-reset stores.
assert load_job_ids <= runner.connector_scheduler._current_batch_jobs_to_flush
# All internal job tracking must be cleared.
assert not runner.connector_scheduler._jobs
assert not runner.connector_scheduler._block_id_to_pending_jobs
if runner.connector_scheduler._blocks_being_loaded is not None:
assert not runner.connector_scheduler._blocks_being_loaded
# Job reset counter must equal the job counter so that completions for
# pre-reset jobs arriving from workers are silently discarded.
assert runner.connector_scheduler._stale_job_threshold == job_counter_before_reset
# next_stored_block_idx must be reset to 0 for every active request so
# that post-reset stores restart from block 0.
for req_status in runner.connector_scheduler._req_status.values():
for group_state in req_status.group_states:
assert group_state.next_stored_block_idx == 0