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 9, 20:45 UTC
[EC Connector] Optimize remote cache check in scheduler (#32585)
Signed-off-by: knlnguyen1802 <knlnguyen1802@gmail.com>
knlnguyen1802 committed
Jan 22, 2026 at 11:30 UTC
378385b90cddbe8cbc6e51d4ed59ce83e499530a
5 files changed
+76
-57
tests/v1/core/test_scheduler.py
+24
-16
@@ -2560,15 +2560,14 @@ def test_ec_connector_cache_hit_external_load(use_kv_connector):
2560
mm_positions=mm_positions,
2561
)[0]
2562
2563
- # Mock cache hit - encoder cache exists externally
2564
- scheduler.ec_connector.has_caches = Mock(return_value=[True])
2563
+ # Mock cache hit - encoder cache has_exists externally
2564
+ scheduler.ec_connector.has_cache_item = Mock(return_value=True)
2565
scheduler.ec_connector.update_state_after_alloc = Mock(
2566
wraps=scheduler.ec_connector.update_state_after_alloc
2567
)
2568
2569
scheduler.add_request(request)
2570
output = scheduler.schedule()
2571
-
2571
# Should schedule prompt tokens
2572
scheduled_tokens = output.num_scheduled_tokens[request.request_id]
2573
assert scheduled_tokens == NUM_TOKENS
@@ -2611,7 +2610,7 @@ def test_ec_connector_cache_miss_computes_locally(use_kv_connector):
2610
)[0]
2611
2612
# Mock cache miss - encoder cache doesn't exist externally
2614
- scheduler.ec_connector.has_caches = Mock(return_value=[False])
2613
+ scheduler.ec_connector.has_cache_item = Mock(return_value=False)
2614
2615
scheduler.add_request(request_mm_missed)
2616
output = scheduler.schedule()
@@ -2665,7 +2664,7 @@ def test_ec_connector_with_partial_cache_hit_multi_round(use_kv_connector):
2664
PlaceholderRange(offset=250, length=NUM_ENCODER_TOKENS_1),
2665
]
2666
]
2668
-
2667
+ has_cache_item_result_map_1 = {"hash1_A": False, "hash1_B": True, "hash1_F": True}
2668
# Create request with 4 MM items, with 2 identical items
2669
request1 = create_requests(
2670
num_requests=1,
@@ -2676,7 +2675,9 @@ def test_ec_connector_with_partial_cache_hit_multi_round(use_kv_connector):
2675
)[0]
2676
2677
# Mock partial cache hit: 1st and 3rd missing, 2nd and 4th exist
2679
- scheduler.ec_connector.has_caches = Mock(return_value=[False, True, False, True])
2678
+ scheduler.ec_connector.has_cache_item = Mock(
2679
+ side_effect=lambda hash_val: has_cache_item_result_map_1[hash_val]
2680
+ )
2681
scheduler.ec_connector.update_state_after_alloc = Mock(
2682
wraps=scheduler.ec_connector.update_state_after_alloc
2683
)
@@ -2736,7 +2737,12 @@ def test_ec_connector_with_partial_cache_hit_multi_round(use_kv_connector):
2737
PlaceholderRange(offset=250, length=NUM_ENCODER_TOKENS_2),
2738
]
2739
]
2739
-
2740
+ has_cache_item_result_map_2 = {
2741
+ "hash1_C": True,
2742
+ "hash1_D": False,
2743
+ "hash1_E": False,
2744
+ "hash1_A": True,
2745
+ }
2746
request2 = create_requests(
2747
num_requests=1,
2748
num_tokens=NUM_TOKENS_2,
@@ -2746,7 +2752,9 @@ def test_ec_connector_with_partial_cache_hit_multi_round(use_kv_connector):
2752
)[0]
2753
2754
# Mock partial cache hit: only hash1_A and hash1_C exist in connector
2749
- scheduler.ec_connector.has_caches = Mock(return_value=[True, False, False, True])
2755
+ scheduler.ec_connector.has_cache_item = Mock(
2756
+ side_effect=lambda hash_val: has_cache_item_result_map_2[hash_val]
2757
+ )
2758
2759
scheduler.add_request(request2)
2760
output = scheduler.schedule()
@@ -2821,9 +2829,9 @@ def test_ec_connector_schedule_multiple_requests(cache_exist, use_kv_connector):
2829
2830
if cache_exist == "connector_only":
2831
# Cache exist in ec_connector
2824
- scheduler.ec_connector.has_caches = Mock(return_value=[True])
2832
+ scheduler.ec_connector.has_cache_item = Mock(return_value=True)
2833
elif cache_exist == "no_where":
2826
- scheduler.ec_connector.has_caches = Mock(return_value=[False])
2834
+ scheduler.ec_connector.has_cache_item = Mock(return_value=False)
2835
2836
output = scheduler.schedule()
2837
assert len(output.scheduled_new_reqs) == len(requests)
@@ -2887,7 +2895,7 @@ def test_ec_connector_unable_to_allocate(use_kv_connector):
2895
)
2896
2897
# Mock ec_connector load external cache behavior
2890
- scheduler.ec_connector.has_caches = Mock(return_value=[True])
2898
+ scheduler.ec_connector.has_cache_item = Mock(return_value=True)
2899
scheduler.ec_connector.update_state_after_alloc = Mock(
2900
wraps=scheduler.ec_connector.update_state_after_alloc
2901
)
@@ -2984,7 +2992,7 @@ def test_priority_scheduling_ec_connector_preemption_and_resumption(
2992
)
2993
2994
# Mock cache hit: Both cache exist in connector (at E->PD initially)
2987
- scheduler.ec_connector.has_caches = Mock(return_value=[True])
2995
+ scheduler.ec_connector.has_cache_item = Mock(return_value=True)
2996
scheduler.ec_connector.update_state_after_alloc = Mock(
2997
wraps=scheduler.ec_connector.update_state_after_alloc
2998
)
@@ -3139,9 +3147,9 @@ def test_priority_scheduling_ec_connector_preemption_and_resumption(
3147
3148
if cache_exist == "connector_only":
3149
# Cache exist in ec_connector
3142
- scheduler.ec_connector.has_caches = Mock(return_value=[True])
3150
+ scheduler.ec_connector.has_cache_item = Mock(return_value=True)
3151
elif cache_exist == "no_where":
3144
- scheduler.ec_connector.has_caches = Mock(return_value=[False])
3152
+ scheduler.ec_connector.has_cache_item = Mock(return_value=False)
3153
3154
# 4th Schedule - this should trigger req_low resumption from waiting
3155
output = scheduler.schedule()
@@ -3259,8 +3267,8 @@ def test_ec_connector_allocate_encoder_tokens_with_external_load(use_kv_connecto
3267
)[0]
3268
3269
# Mock cache hit: MM of request1 NOT cached remotely, request2 cached remotely
3262
- scheduler.ec_connector.has_caches = Mock(
3263
- side_effect=lambda req: [True, True, True] if req == request2 else [False]
3270
+ scheduler.ec_connector.has_cache_item = Mock(
3271
+ side_effect=lambda hash_value: hash_value in mm_hashes_list_2[0]
3272
)
3273
scheduler.ec_connector.update_state_after_alloc = Mock(
3274
wraps=scheduler.ec_connector.update_state_after_alloc
tests/v1/ec_connector/unit/test_ec_example_connector.py
+31
-17
@@ -123,15 +123,15 @@ class TestECExampleConnectorBasics:
123
124
125
class TestCacheExistence:
126
- """Test cache existence checking using has_caches() API."""
126
+ """Test cache existence checking using has_cache_item() API."""
127
128
- def test_has_caches_all_exist_3_items(
128
+ def test_has_cache_item_all_exist_3_items(
129
self,
130
mock_vllm_config_producer,
131
mock_vllm_config_consumer,
132
mock_request_with_3_mm,
133
):
134
- """Test has_caches returns True when all 3 caches exist."""
134
+ """Test has_cache_item returns True when all 3 caches exist."""
135
# Test for producer first
136
producer = ECExampleConnector(
137
vllm_config=mock_vllm_config_producer,
@@ -146,8 +146,11 @@ class TestCacheExistence:
146
encoder_cache[mm_hash] = torch.randn(10, 768)
147
producer.save_caches(encoder_cache, mm_hash)
148
149
- # Test using has_caches API
150
- producer_result = producer.has_caches(mock_request_with_3_mm)
149
+ # Test using has_cache_item API
150
+ producer_result = [
151
+ producer.has_cache_item(mm_feature.identifier)
152
+ for mm_feature in mock_request_with_3_mm.mm_features
153
+ ]
154
155
# Assert
156
assert len(producer_result) == 3
@@ -159,14 +162,17 @@ class TestCacheExistence:
162
role=ECConnectorRole.SCHEDULER,
163
)
164
162
- # Test using has_caches API
163
- consumer_result = consumer.has_caches(mock_request_with_3_mm)
165
+ # Test using has_cache_item API
166
+ consumer_result = [
167
+ consumer.has_cache_item(mm_feature.identifier)
168
+ for mm_feature in mock_request_with_3_mm.mm_features
169
+ ]
170
171
# Assert
172
assert len(consumer_result) == 3
173
assert all(consumer_result), f"Expected all True, got {consumer_result}"
174
169
- def test_has_caches_none_exist(
175
+ def test_has_cache_item_none_exist(
176
self, mock_vllm_config_producer, mock_request_with_3_mm
177
):
178
"""Test has_caches returns False when no caches exist."""
@@ -176,13 +182,16 @@ class TestCacheExistence:
182
)
183
184
# Test without creating any files
179
- result = connector.has_caches(mock_request_with_3_mm)
185
+ result = [
186
+ connector.has_cache_item(mm_feature.identifier)
187
+ for mm_feature in mock_request_with_3_mm.mm_features
188
+ ]
189
190
# Assert
191
assert len(result) == 3
192
assert not any(result), f"Expected all False, got {result}"
193
185
- def test_has_caches_partial_exist(
194
+ def test_has_cache_item_partial_exist(
195
self, mock_vllm_config_producer, mock_request_with_3_mm
196
):
197
"""Test has_caches with some caches existing (1 of 3)."""
@@ -197,7 +206,10 @@ class TestCacheExistence:
206
connector.save_caches(encoder_cache, mm_hash_second)
207
208
# Test
200
- result = connector.has_caches(mock_request_with_3_mm)
209
+ result = [
210
+ connector.has_cache_item(mm_feature.identifier)
211
+ for mm_feature in mock_request_with_3_mm.mm_features
212
+ ]
213
214
# Assert
215
assert len(result) == 3
@@ -323,8 +335,11 @@ class TestCacheSaving:
335
encoder_cache[mm_hash] = torch.randn(10, 768)
336
connector.save_caches(encoder_cache, mm_hash)
337
326
- # Verify all files exist using has_caches
327
- result = connector.has_caches(mock_request_with_3_mm)
338
+ # Verify all files exist using has_cache_item
339
+ result = [
340
+ connector.has_cache_item(mm_feature.identifier)
341
+ for mm_feature in mock_request_with_3_mm.mm_features
342
+ ]
343
assert all(result), f"Not all caches were saved: {result}"
344
345
# Verify each file's content
@@ -347,10 +362,9 @@ class TestCacheSaving:
362
# Save should not raise but also not create file
363
connector.save_caches(encoder_cache, mm_hash)
364
350
- # Verify file doesn't exist using has_caches
351
- mock_request = MockRequest("req_consumer", [mm_hash], [10])
352
- result = connector.has_caches(mock_request)
353
- assert not result[0], "Consumer should not save caches"
365
+ # Verify file doesn't exist using has_cache_item
366
+ result = connector.has_cache_item(mm_hash)
367
+ assert not result, "Consumer should not save caches"
368
369
370
class TestCacheLoading:
vllm/distributed/ec_transfer/ec_connector/base.py
+7
-7
@@ -182,19 +182,19 @@ class ECConnectorBase(ABC):
182
# ==============================
183
184
@abstractmethod
185
- def has_caches(
185
+ def has_cache_item(
186
self,
187
- request: "Request",
188
- ) -> list[bool]:
187
+ identifier: str,
188
+ ) -> bool:
189
"""
190
- Check if encoder cache exists for each mm data of requests
190
+ Check if a single encoder cache exists
191
192
Args:
193
- request (Request): the request object.
193
+ identifier (str): the identifier of the media.
194
195
Returns:
196
- A list bool where ith value is True if cache exist for
197
- ith mm_data of requests
196
+ A bool where value is True if cache exist for
197
+ the media
198
"""
199
pass
200
vllm/distributed/ec_transfer/ec_connector/example_connector.py
+7
-10
@@ -117,23 +117,20 @@ class ECExampleConnector(ECConnectorBase):
117
safetensors.torch.save_file(tensors, filename)
118
logger.debug("Save cache successful for mm_hash %s", mm_hash)
119
120
- def has_caches(
120
+ def has_cache_item(
121
self,
122
- request: "Request",
123
- ) -> list[bool]:
122
+ identifier: str,
123
+ ) -> bool:
124
"""
125
- Check if cache exist externally for each mm_data of request
125
+ Check if cache exist externally for the media
126
127
Args:
128
- request (Request): the request object.
128
+ identifier (str): the identifier of the media.
129
130
Returns:
131
- List of bool indicate that ith mm_data exist in cache or not
131
+ Bool indicate that media exists in cache or not
132
"""
133
- result = []
134
- for feature in request.mm_features:
135
- result.append(self._found_match_for_mm_data(feature.identifier))
136
- return result
133
+ return self._found_match_for_mm_data(identifier)
134
135
def update_state_after_alloc(
136
self,
vllm/v1/core/sched/scheduler.py
+7
-7
@@ -947,9 +947,6 @@ class Scheduler(SchedulerInterface):
947
assert len(mm_features) > 0
948
external_load_encoder_input = []
949
950
- # Check remote cache first
951
- if self.ec_connector is not None:
952
- remote_cache_has_item = self.ec_connector.has_caches(request)
950
# NOTE: since scheduler operates on the request level (possibly with
951
# multiple encoder inputs per request), we need to create temporary
952
# trackers for accounting at the encoder input level.
@@ -959,6 +956,7 @@ class Scheduler(SchedulerInterface):
956
start_pos = mm_feature.mm_position.offset
957
num_encoder_tokens = mm_feature.mm_position.length
958
num_encoder_embeds = mm_feature.mm_position.get_num_embeds
959
+ item_identifier = mm_feature.identifier
960
961
# The encoder output is needed if the two ranges overlap:
962
# [num_computed_tokens, num_computed_tokens + num_new_tokens) and
@@ -993,7 +991,7 @@ class Scheduler(SchedulerInterface):
991
if not self.is_encoder_decoder:
992
# We are not using the encoder cache for encoder-decoder models,
993
# yet.
996
- if request.mm_features[i].identifier in mm_hashes_to_schedule:
994
+ if item_identifier in mm_hashes_to_schedule:
995
# The same encoder input has already been scheduled in the
996
# current step.
997
continue
@@ -1051,15 +1049,17 @@ class Scheduler(SchedulerInterface):
1049
if curr_embeds_end - curr_embeds_start == 0:
1050
continue
1051
1054
- if self.ec_connector is not None and remote_cache_has_item[i]:
1055
- mm_hashes_to_schedule.add(request.mm_features[i].identifier)
1052
+ if self.ec_connector is not None and self.ec_connector.has_cache_item(
1053
+ item_identifier
1054
+ ):
1055
+ mm_hashes_to_schedule.add(item_identifier)
1056
external_load_encoder_input.append(i)
1057
num_embeds_to_schedule += num_encoder_embeds
1058
continue
1059
1060
num_embeds_to_schedule += num_encoder_embeds
1061
encoder_compute_budget -= num_encoder_embeds
1062
- mm_hashes_to_schedule.add(request.mm_features[i].identifier)
1062
+ mm_hashes_to_schedule.add(item_identifier)
1063
encoder_inputs_to_schedule.append(i)
1064
1065
return (