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 2, 06:00 UTC
[EC Connector] Non blocking EC Connector lookup (#41627)
Signed-off-by: omerpaz95 <omerpaz95@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
omerpaz95 committed
Jun 2, 2026 at 11:48 UTC
d247a9dc13a58fef5ce64dc3a8f4be051577bda5
3 files changed
+154
tests/v1/core/test_scheduler.py
+125
@@ -4349,3 +4349,128 @@ def test_eagle3_mm_encoder_cache_with_shift():
4349
f"shifted_end={scheduled_end_with_shift}) overlapping MM at "
4350
f"{start_pos}. The fix must schedule encoder inputs."
4351
)
4352
+
4353
+
4354
+@pytest.mark.parametrize("use_kv_connector", [False, True])
4355
+def test_ec_connector_ensure_cache_available_defers_request(use_kv_connector):
4356
+ """Test that ensure_cache_available() returning False defers the request.
4357
+
4358
+ When the EC connector signals a prefetch is in progress (returns False),
4359
+ the scheduler should:
4360
+ 1. Not schedule the request (no KV cache or encoder cache allocated)
4361
+ 2. Still schedule other requests behind the deferred one
4362
+ 3. Schedule the deferred request on the next step when ensure_cache_available
4363
+ returns True and has_cache_item returns True
4364
+ """
4365
+ scheduler = create_scheduler(
4366
+ model="llava-hf/llava-1.5-7b-hf",
4367
+ enable_prefix_caching=True,
4368
+ use_kv_connector=use_kv_connector,
4369
+ use_ec_connector=True,
4370
+ ec_role="ec_consumer",
4371
+ )
4372
+
4373
+ NUM_TOKENS = 200
4374
+ NUM_ENCODER_TOKENS = 100
4375
+
4376
+ request_deferred = create_requests(
4377
+ num_requests=1,
4378
+ num_tokens=NUM_TOKENS,
4379
+ mm_positions=[[PlaceholderRange(offset=0, length=NUM_ENCODER_TOKENS)]],
4380
+ req_ids=["deferred"],
4381
+ )[0]
4382
+
4383
+ request_behind = create_requests(
4384
+ num_requests=1,
4385
+ num_tokens=20,
4386
+ req_ids=["behind"],
4387
+ )[0]
4388
+
4389
+ # --- Step 1: ensure_cache_available returns False → request deferred ---
4390
+ scheduler.ec_connector.ensure_cache_available = Mock(return_value=False)
4391
+
4392
+ scheduler.add_request(request_deferred)
4393
+ scheduler.add_request(request_behind)
4394
+ output = scheduler.schedule()
4395
+
4396
+ # ensure_cache_available must have been called with (request, num_computed_tokens=0)
4397
+ # for a brand-new request that has no cached tokens yet.
4398
+ scheduler.ec_connector.ensure_cache_available.assert_called_once_with(
4399
+ request_deferred, 0
4400
+ )
4401
+ # Deferred request must NOT be scheduled
4402
+ assert request_deferred.request_id not in output.num_scheduled_tokens
4403
+ _assert_right_encoder_cache_allocated(scheduler, expected_total_allocated=0)
4404
+ # No KV blocks allocated for the deferred request
4405
+ for mgr in scheduler.kv_cache_manager.coordinator.single_type_managers:
4406
+ assert request_deferred.request_id not in mgr.req_to_blocks
4407
+
4408
+ # The text-only request behind the deferred one MUST still be scheduled
4409
+ assert request_behind.request_id in output.num_scheduled_tokens
4410
+ assert output.num_scheduled_tokens[request_behind.request_id] == 20
4411
+
4412
+ # --- Step 2: prefetch done, cache exists → request scheduled ---
4413
+ # has_cache_item is called inside _try_schedule_encoder_inputs (not during
4414
+ # deferral), so it is only relevant here in step 2.
4415
+ scheduler.ec_connector.ensure_cache_available = Mock(return_value=True)
4416
+ scheduler.ec_connector.has_cache_item = Mock(return_value=True)
4417
+
4418
+ output = scheduler.schedule()
4419
+
4420
+ # Now the deferred request should be scheduled
4421
+ assert request_deferred.request_id in output.num_scheduled_tokens
4422
+ assert output.num_scheduled_tokens[request_deferred.request_id] == NUM_TOKENS
4423
+ _assert_right_encoder_cache_allocated(scheduler, requests=[request_deferred])
4424
+ # EC connector metadata should carry the deferred request's MM data
4425
+ _assert_right_ec_connector_metadata(
4426
+ output, mm_features_list=request_deferred.mm_features
4427
+ )
4428
+ # No local encoder compute — all loaded externally
4429
+ _assert_right_encoder_inputs(output, expected_total_reqs=0)
4430
+
4431
+
4432
+def test_ec_connector_pending_prefetch_only_checks_future_mm_features():
4433
+ """Test that future mm feature filtering only yields features beyond
4434
+ the computed token frontier.
4435
+
4436
+ Features already within num_computed_tokens (past/boundary) must be
4437
+ filtered out; only features that extend beyond the frontier (future) should
4438
+ be yielded so that connector implementations know which items to prefetch.
4439
+
4440
+ Filter cases:
4441
+ "past": end = 0 + 16 = 16 < 32 → filtered OUT
4442
+ "boundary": end = 16 + 16 = 32 == 32 → filtered OUT (condition is >, not >=)
4443
+ "future": end = 48 + 32 = 80 > 32 → yielded
4444
+ """
4445
+ BLOCK_SIZE = 16
4446
+ NUM_COMPUTED_TOKENS = BLOCK_SIZE * 2 # 32
4447
+ NUM_TOKENS = BLOCK_SIZE * 8 # 128
4448
+
4449
+ HASH_PAST = "hash_past"
4450
+ HASH_BOUNDARY = "hash_boundary"
4451
+ HASH_FUTURE = "hash_future"
4452
+
4453
+ request = create_requests(
4454
+ num_requests=1,
4455
+ num_tokens=NUM_TOKENS,
4456
+ mm_hashes_list=[[HASH_PAST, HASH_BOUNDARY, HASH_FUTURE]],
4457
+ mm_positions=[
4458
+ [
4459
+ PlaceholderRange(offset=0, length=BLOCK_SIZE), # end=16 (past)
4460
+ PlaceholderRange(offset=16, length=BLOCK_SIZE), # end=32 (boundary)
4461
+ PlaceholderRange(offset=48, length=BLOCK_SIZE * 2), # end=80 (future)
4462
+ ]
4463
+ ],
4464
+ block_size=BLOCK_SIZE,
4465
+ )[0]
4466
+
4467
+ future_hashes = [
4468
+ f.identifier
4469
+ for f in request.mm_features
4470
+ if f.mm_position.offset + f.mm_position.length > NUM_COMPUTED_TOKENS
4471
+ ]
4472
+
4473
+ assert future_hashes == [HASH_FUTURE], (
4474
+ f"Expected only {HASH_FUTURE!r} from future mm feature filtering, "
4475
+ f"got {future_hashes!r}. Past/boundary features must be filtered out."
4476
+ )
vllm/distributed/ec_transfer/ec_connector/base.py
+17
@@ -211,6 +211,23 @@ class ECConnectorBase(ABC):
211
"""
212
pass
213
214
+ def ensure_cache_available(
215
+ self, request: "Request", num_computed_tokens: int
216
+ ) -> bool:
217
+ """
218
+ Ensure encoder cache items are available for the given request.
219
+ May initiate asynchronous transfers for items not yet local.
220
+
221
+ Args:
222
+ request: the request whose multimodal features to check.
223
+ num_computed_tokens: tokens already covered by cached KV blocks.
224
+
225
+ Returns:
226
+ True if all items are ready or no transfer is needed.
227
+ False if any items are still in transit (request should be deferred).
228
+ """
229
+ return True
230
+
231
@abstractmethod
232
def update_state_after_alloc(self, request: "Request", index: int):
233
"""
vllm/v1/core/sched/scheduler.py
+12
@@ -629,6 +629,18 @@ class Scheduler(SchedulerInterface):
629
)
630
assert num_computed_tokens <= request.num_tokens
631
632
+ # Skip request with pending mm encoding prefetches
633
+ if (
634
+ self.ec_connector is not None
635
+ and request.mm_features
636
+ and not self.ec_connector.ensure_cache_available(
637
+ request, num_computed_tokens
638
+ )
639
+ ):
640
+ request_queue.pop_request()
641
+ step_skipped_waiting.prepend_request(request)
642
+ continue
643
+
644
# Track first scheduled prefill, not post-preemption repeat prefills
645
if request.prefill_stats is not None:
646
assert num_computed_tokens <= request.num_prompt_tokens