Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -1600,6 +1600,57 @@ def test_abort_queued_request_does_not_build_store_job(
assert queued_req_id not in runner.connector_scheduler._req_status


@pytest.mark.parametrize(
"finish_status", [RequestStatus.FINISHED_ERROR, RequestStatus.FINISHED_ABORTED]
)
def test_finish_mid_prefill_stores_only_computed_blocks(
request_runner, finish_status: RequestStatus
):
"""A request that finishes mid-prefill must not store a block it never
computed under that block's full hash."""
block_size = 4
runner = request_runner(
block_size=block_size, num_gpu_blocks=16, async_scheduling=False
)
runner.manager.prepare_store.side_effect = lambda keys, req_context: (
generate_store_output(keys)
)
# Chunked prefill: 6 of the 16 prompt tokens per step.
runner.scheduler.max_num_scheduled_tokens = 6
runner.scheduler.scheduler_config.max_num_batched_tokens = 6
runner.new_request(token_ids=list(range(100, 116)))
req_id = str(runner.req_id)

scheduler_output = runner.scheduler.schedule()
assert scheduler_output.num_scheduled_tokens[req_id] == 6
runner._update_gpu_blocks()
req = runner.scheduler.requests[req_id]
model_runner_output = create_model_runner_output(
reqs=[req], token_id=0, use_eos=False
)
# A partial prefill samples no token.
model_runner_output.sampled_token_ids = [[]]
runner.scheduler.update_from_output(scheduler_output, model_runner_output)
assert req.num_computed_tokens == 6

# The first step stored the one full block. The second block has only
# tokens 4-5 computed when the request finishes.
runner.scheduler.finish_requests(req_id, finish_status)
metadata = runner.scheduler.schedule().kv_connector_metadata
assert isinstance(metadata, OffloadingConnectorMetadata)
stored_blocks = sorted(
int(block_id)
for job in metadata.store_jobs.values()
if job.req_id == req_id
for block_id in job.src_spec.block_ids
)
assert not stored_blocks, (
f"{finish_status.name}: stored {len(stored_blocks)} blocks {stored_blocks} "
f"but only {6 // block_size} full block(s) were computed "
f"(num_computed_tokens=6, prompt=16)"
)


def test_store_jobs_wait_for_missing_offload_key(request_runner):
"""Allocated chunks must wait until their block hash becomes available."""
block_size = 4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1881,7 +1881,12 @@ def _build_store_jobs(
# Clamp to the GPU prefix cache's commit point. The final sampled
# token's slot is never committed (under spec decode it holds a
# rejected draft's KV), so a block ending there must not be stored.
num_tokens_after_batch = max(req.num_prompt_tokens, req.num_tokens - 1)
# A request that finished before its prefill completed (e.g. an
# error) has computed only num_computed_tokens of its prompt.
num_tokens_after_batch = min(
req.num_computed_tokens,
max(req.num_prompt_tokens, req.num_tokens - 1),
)
else:
num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id]
num_tokens_after_batch = req.num_computed_tokens + num_scheduled_tokens
Expand Down