A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 6, 13:15 UTC
[kv_offload+HMA][0/N]: Support block-level preemption handling (#34805)
Signed-off-by: Or Ozeri <oro@il.ibm.com> Co-authored-by: Nicolò Lucchesi <nlucches@redhat.com>
Or Ozeri committed
Mar 18, 2026 at 08:49 UTC
fcf0687b27b78c3b214504f5e9525f3f66a2d04a
7 files changed
+27
-25
tests/v1/kv_connector/unit/test_multi_connector.py
+6
-4
@@ -231,10 +231,11 @@ def test_multi_example_connector_consistency():
231
]
232
# First three events are from initialization (register_kv_caches,
233
# set_host_xfer_buffer_ops, get_handshake_metadata), then generate() events.
234
- assert events["storage1-WORKER"][:7] == [
234
+ assert events["storage1-WORKER"][:8] == [
235
"register_kv_caches",
236
"set_host_xfer_buffer_ops",
237
"get_handshake_metadata",
238
+ "handle_preemptions",
239
"bind_connector_metadata",
240
"start_load_kv",
241
"wait_for_layer_load",
@@ -246,10 +247,11 @@ def test_multi_example_connector_consistency():
247
"update_state_after_alloc num_blocks=[0] 0",
248
"build_connector_meta",
249
]
249
- assert events["storage2-WORKER"][:7] == [
250
+ assert events["storage2-WORKER"][:8] == [
251
"register_kv_caches",
252
"set_host_xfer_buffer_ops",
253
"get_handshake_metadata",
254
+ "handle_preemptions",
255
"bind_connector_metadata",
256
"start_load_kv",
257
"wait_for_layer_load",
@@ -399,8 +401,8 @@ def test_multi_connector_handle_preemptions_integration():
401
# testing the delegation behavior of MultiConnector here.
402
# The connector attribute contains the KV connector.
403
assert scheduler.connector is not None, "Scheduler should have a connector"
402
- preempted_req_ids = {"req-1", "req-2", "req-3"}
403
- scheduler.connector.handle_preemptions(preempted_req_ids)
404
+ connector_md = scheduler.connector.build_connector_meta(scheduler.schedule())
405
+ scheduler.connector.handle_preemptions(connector_md)
406
407
# Verify both connectors received the handle_preemptions call
408
events = get_connector_events()
tests/v1/kv_connector/unit/test_offloading_connector.py
+1
-4
@@ -363,10 +363,7 @@ class RequestRunner:
363
assert kv_connector_metadata is not None
364
assert isinstance(kv_connector_metadata, OffloadingConnectorMetadata)
365
366
- if scheduler_output.preempted_req_ids:
367
- self.worker_connector.handle_preemptions(
368
- scheduler_output.preempted_req_ids
369
- )
366
+ self.worker_connector.handle_preemptions(kv_connector_metadata)
367
368
self.worker_connector.bind_connector_metadata(kv_connector_metadata)
369
self.worker_connector.start_load_kv(self._dummy_ctx)
vllm/distributed/kv_transfer/kv_connector/v1/base.py
+4
-4
@@ -25,8 +25,8 @@ The class provides the following primitives:
25
26
Worker-side: runs in each worker, loads/saves KV cache to/from
27
the Connector based on the metadata.
28
- handle_preemptions() - called if there are preempted requests,
29
- before their blocks are overwritten
28
+ handle_preemptions() - called for handling preempted requests
29
+ or request evicted blocks before they are overwritten
30
31
start_load_kv() - starts loading all KVs (maybe async)
32
wait_for_layer_load() - blocks until layer i load is done
@@ -288,9 +288,9 @@ class KVConnectorBase_V1(ABC):
288
"""
289
return
290
291
- def handle_preemptions(self, preempted_req_ids: set[str]):
291
+ def handle_preemptions(self, kv_connector_metadata: KVConnectorMetadata):
292
"""
293
- Handle preempted requests BEFORE their blocks are overwritten.
293
+ Handle preempted requests or evicted blocks BEFORE they are overwritten.
294
Needed for connectors which use async saves (e.g., OffloadingConnector)
295
"""
296
return
vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py
+4
-3
@@ -315,10 +315,11 @@ class MultiConnector(KVConnectorBase_V1):
315
for c in self._connectors:
316
c.set_host_xfer_buffer_ops(copy_operation)
317
318
- def handle_preemptions(self, preempted_req_ids: set[str]):
318
+ def handle_preemptions(self, kv_connector_metadata: KVConnectorMetadata):
319
"""Handle preempted requests for all sub-connectors."""
320
- for c in self._connectors:
321
- c.handle_preemptions(preempted_req_ids)
320
+ assert isinstance(kv_connector_metadata, MultiKVConnectorMetadata)
321
+ for c, cm in zip(self._connectors, kv_connector_metadata.metadata):
322
+ c.handle_preemptions(cm)
323
324
def get_finished_count(self) -> int | None:
325
# TODO(https://github.com/vllm-project/vllm/issues/33400)
vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py
+7
-4
@@ -111,6 +111,7 @@ class OffloadingConnectorStats(KVConnectorStats):
111
class OffloadingConnectorMetadata(KVConnectorMetadata):
112
reqs_to_load: dict[ReqId, TransferSpec]
113
reqs_to_store: dict[ReqId, TransferSpec]
114
+ reqs_to_flush: set[str] | None = None
115
116
117
class OffloadingConnector(KVConnectorBase_V1):
@@ -146,9 +147,10 @@ class OffloadingConnector(KVConnectorBase_V1):
147
assert self.connector_worker is not None
148
self.connector_worker.register_cross_layers_kv_cache(kv_cache, attn_backend)
149
149
- def handle_preemptions(self, preempted_req_ids: set[str]):
150
+ def handle_preemptions(self, kv_connector_metadata: KVConnectorMetadata):
151
assert self.connector_worker is not None
151
- self.connector_worker.handle_preemptions(preempted_req_ids)
152
+ assert isinstance(kv_connector_metadata, OffloadingConnectorMetadata)
153
+ self.connector_worker.handle_preemptions(kv_connector_metadata)
154
155
def start_load_kv(self, forward_context: "ForwardContext", **kwargs) -> None:
156
assert self.connector_worker is not None
@@ -482,6 +484,7 @@ class OffloadingConnectorScheduler:
484
meta = OffloadingConnectorMetadata(
485
reqs_to_load=self._reqs_to_load,
486
reqs_to_store=self._get_reqs_to_store(scheduler_output),
487
+ reqs_to_flush=scheduler_output.preempted_req_ids,
488
)
489
self._reqs_to_load = {}
490
@@ -619,13 +622,13 @@ class OffloadingConnectorWorker:
622
attn_backends = {cross_layer_name: attn_backend}
623
self._register_handlers(kv_caches, attn_backends)
624
622
- def handle_preemptions(self, preempted_req_ids: set[str]):
625
+ def handle_preemptions(self, kv_connector_metadata: OffloadingConnectorMetadata):
626
for job_id, transfer_spec in self._unsubmitted_store_jobs:
627
success = self.worker.transfer_async(job_id, transfer_spec)
628
assert success
629
self._unsubmitted_store_jobs.clear()
630
628
- for req_id in preempted_req_ids:
631
+ for req_id in kv_connector_metadata.reqs_to_flush or ():
632
job_ids = self._store_jobs.get(req_id)
633
if job_ids:
634
self.worker.wait(job_ids)
vllm/v1/worker/gpu/kv_connector.py
+1
-2
@@ -63,11 +63,10 @@ class ActiveKVConnector(KVConnector):
63
if self._disabled:
64
return
65
66
- if scheduler_output.preempted_req_ids:
67
- self.kv_connector.handle_preemptions(scheduler_output.preempted_req_ids)
66
kv_connector_metadata = scheduler_output.kv_connector_metadata
67
assert kv_connector_metadata is not None
68
self.kv_connector.bind_connector_metadata(kv_connector_metadata)
69
+ self.kv_connector.handle_preemptions(kv_connector_metadata)
70
71
# TODO: sort out KV Connectors' use of forward_context
72
if is_forward_context_available():
vllm/v1/worker/gpu_model_runner.py
+4
-4
@@ -3594,10 +3594,10 @@ class GPUModelRunner(
3594
scheduled_spec_decode_tokens=spec_decode_tokens_copy,
3595
)
3596
3597
- if scheduler_output.preempted_req_ids and has_kv_transfer_group():
3598
- get_kv_transfer_group().handle_preemptions(
3599
- scheduler_output.preempted_req_ids
3600
- )
3597
+ if has_kv_transfer_group():
3598
+ kv_connector_metadata = scheduler_output.kv_connector_metadata
3599
+ assert kv_connector_metadata is not None
3600
+ get_kv_transfer_group().handle_preemptions(kv_connector_metadata)
3601
3602
num_scheduled_tokens = scheduler_output.total_num_scheduled_tokens
3603
with (