Skip to content

Commit 810bc32

Browse files
[Frontend][Performance] Resolve async media across modalities concurrently (vllm-project#54537)
Signed-off-by: waizuichougou <2082431897@qq.com>
1 parent 9debcd5 commit 810bc32

2 files changed

Lines changed: 78 additions & 13 deletions

File tree

tests/entrypoints/unit_tests/test_chat_utils.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2933,10 +2933,62 @@ def test_postprocess_messages_null_arguments_string():
29332933
assert tool_calls[0]["function"]["arguments"] == {}
29342934

29352935

2936+
@pytest.mark.asyncio
2937+
async def test_resolve_items_runs_modalities_concurrently_and_preserves_order():
2938+
"""Media fetches overlap while modality and item order are preserved."""
2939+
2940+
active_fetches = 0
2941+
max_active_fetches = 0
2942+
2943+
async def _fetch(name: str, delay: float):
2944+
nonlocal active_fetches, max_active_fetches
2945+
active_fetches += 1
2946+
max_active_fetches = max(max_active_fetches, active_fetches)
2947+
try:
2948+
await asyncio.sleep(delay)
2949+
return name, f"{name}-uuid"
2950+
finally:
2951+
active_fetches -= 1
2952+
2953+
tracker = AsyncMultiModalItemTracker(MagicMock())
2954+
tracker._model_config.is_multimodal_model = True
2955+
tracker.__dict__["mm_processor"] = MagicMock()
2956+
tracker._items_by_modality["video"] = [
2957+
lambda: _fetch("video-0", 0.02),
2958+
lambda: _fetch("video-1", 0.01),
2959+
]
2960+
tracker._items_by_modality["image"] = [
2961+
lambda: _fetch("image-0", 0.02),
2962+
lambda: _fetch("image-1", 0.01),
2963+
]
2964+
tracker._items_by_modality["audio"] = [
2965+
lambda: _fetch("audio-0", 0.02),
2966+
lambda: _fetch("audio-1", 0.01),
2967+
]
2968+
2969+
mm_data, mm_uuids = await tracker.resolve_items()
2970+
2971+
assert mm_data is not None
2972+
assert mm_uuids is not None
2973+
assert max_active_fetches == 6
2974+
assert list(mm_data) == ["image", "audio", "video"]
2975+
assert list(mm_uuids) == ["image", "audio", "video"]
2976+
assert mm_data == {
2977+
"image": ["image-0", "image-1"],
2978+
"audio": ["audio-0", "audio-1"],
2979+
"video": ["video-0", "video-1"],
2980+
}
2981+
assert mm_uuids == {
2982+
"image": ["image-0-uuid", "image-1-uuid"],
2983+
"audio": ["audio-0-uuid", "audio-1-uuid"],
2984+
"video": ["video-0-uuid", "video-1-uuid"],
2985+
}
2986+
2987+
29362988
@pytest.mark.asyncio
29372989
async def test_resolve_items_does_not_leak_tasks_on_partial_failure():
29382990
"""Regression test: one failing media fetch must not abandon the other
2939-
still-in-flight fetches in the same modality batch.
2991+
still-in-flight fetches across modality batches.
29402992
29412993
Before the fix, `resolve_items` gathered per-modality fetches with plain
29422994
`asyncio.gather`, so the first exception propagated immediately while
@@ -2957,6 +3009,8 @@ async def _fetch(should_fail: bool, delay: float):
29573009
lambda: _fetch(False, 0.2),
29583010
lambda: _fetch(False, 0.2),
29593011
]
3012+
tracker._items_by_modality["audio"] = [lambda: _fetch(False, 0.2)]
3013+
tracker._items_by_modality["video"] = [lambda: _fetch(False, 0.2)]
29603014

29613015
tasks_before = asyncio.all_tasks()
29623016
with pytest.raises(ValueError, match="simulated fetch failure"):

vllm/entrypoints/chat_utils.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -871,19 +871,30 @@ async def resolve_items(
871871
if not self._items_by_modality:
872872
return None, None
873873

874+
# Fetch all modalities together. Each tracked item is already an
875+
# independent awaitable, and the async connector offloads blocking
876+
# decode work, so waiting for one modality before starting the next
877+
# needlessly adds their latency.
878+
# Keep the original group and item order when rebuilding the result.
879+
item_groups = list(self._items_by_modality.items())
880+
items = [item for _, group in item_groups for item in group]
881+
results = await asyncio.gather(
882+
*(item() for item in items), return_exceptions=True
883+
)
884+
for result in results:
885+
if isinstance(result, BaseException):
886+
# Gathering with return_exceptions=True lets every task finish
887+
# (or itself fail) before we raise, instead of abandoning
888+
# still-in-flight fetches (real network/thread-pool work) the
889+
# moment the first one fails.
890+
raise result
891+
874892
resolved_items_by_modality: dict[str, list[Any]] = {}
875-
for modality, items in self._items_by_modality.items():
876-
results = await asyncio.gather(
877-
*(item() for item in items), return_exceptions=True
878-
)
879-
for result in results:
880-
if isinstance(result, BaseException):
881-
# Gathering with return_exceptions=True lets every task in
882-
# this modality finish (or itself fail) before we raise,
883-
# instead of abandoning still-in-flight fetches (real
884-
# network/thread-pool work) the moment the first one fails.
885-
raise result
886-
resolved_items_by_modality[modality] = results
893+
result_idx = 0
894+
for modality, group in item_groups:
895+
next_result_idx = result_idx + len(group)
896+
resolved_items_by_modality[modality] = results[result_idx:next_result_idx]
897+
result_idx = next_result_idx
887898

888899
mm_processor = (
889900
self.mm_processor if self._model_config.is_multimodal_model else None

0 commit comments

Comments
 (0)