main
py 141 lines 4.36 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Contains helpers related to asynchronous code.
4
5 This is similar in concept to the `asyncio` module.
6 """
7
8 import asyncio
9 import contextlib
10 from asyncio import FIRST_COMPLETED, AbstractEventLoop, Future, Task
11 from collections.abc import AsyncGenerator, Awaitable, Callable
12 from concurrent.futures import Executor, ThreadPoolExecutor
13 from functools import partial
14 from typing import TYPE_CHECKING, TypeVar
15
16 from typing_extensions import ParamSpec
17
18 P = ParamSpec("P")
19 T = TypeVar("T")
20
21
22 def cancel_task_threadsafe(task: Task):
23 if task and not task.done():
24 run_in_loop(task.get_loop(), task.cancel)
25
26
27 def make_async(
28 func: Callable[P, T],
29 executor: Executor | None = None,
30 ) -> Callable[P, Awaitable[T]]:
31 """Take a blocking function, and run it on in an executor thread.
32
33 This function prevents the blocking function from blocking the
34 asyncio event loop.
35 The code in this function needs to be thread safe.
36 """
37
38 def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Future[T]:
39 loop = asyncio.get_event_loop()
40 p_func = partial(func, *args, **kwargs)
41 return loop.run_in_executor(executor=executor, func=p_func)
42
43 return _async_wrapper
44
45
46 def make_async_with_semaphore(
47 func: Callable[P, T],
48 executor: ThreadPoolExecutor,
49 ) -> Callable[P, Awaitable[T]]:
50 """Take a blocking function, and run it on in an executor thread.
51
52 This function prevents the blocking function from blocking the
53 asyncio event loop.
54 The code in this function needs to be thread safe.
55
56 The function is wrapped in a semaphore to limit the number of
57 concurrent executions making it easier to cancel tasks before they start.
58 """
59 semaphore = asyncio.Semaphore(executor._max_workers)
60
61 async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
62 loop = asyncio.get_event_loop()
63 p_func = partial(func, *args, **kwargs)
64 async with semaphore:
65 return await loop.run_in_executor(executor, p_func)
66
67 return _async_wrapper
68
69
70 def run_in_loop(loop: AbstractEventLoop, function: Callable, *args):
71 if in_loop(loop):
72 function(*args)
73 elif not loop.is_closed():
74 loop.call_soon_threadsafe(function, *args)
75
76
77 def in_loop(event_loop: AbstractEventLoop) -> bool:
78 try:
79 return asyncio.get_running_loop() == event_loop
80 except RuntimeError:
81 return False
82
83
84 # A hack to pass mypy
85 if TYPE_CHECKING:
86
87 def anext(it: AsyncGenerator[T, None]):
88 return it.__anext__()
89
90
91 async def merge_async_iterators(
92 *iterators: AsyncGenerator[T, None],
93 ) -> AsyncGenerator[tuple[int, T], None]:
94 """Merge multiple asynchronous iterators into a single iterator.
95
96 This method handle the case where some iterators finish before others.
97 When it yields, it yields a tuple (i, item) where i is the index of the
98 iterator that yields the item.
99 """
100 if len(iterators) == 1:
101 # Fast-path single iterator case.
102 iterator: AsyncGenerator[T, None] | None = iterators[0]
103 try:
104 async for item in iterator: # type: ignore[union-attr]
105 yield 0, item
106 iterator = None
107 finally:
108 if iterator is not None:
109 with contextlib.suppress(BaseException):
110 await iterator.aclose()
111 return
112
113 loop = asyncio.get_running_loop()
114
115 awaits = {loop.create_task(anext(it)): (i, it) for i, it in enumerate(iterators)}
116 try:
117 while awaits:
118 done, _ = await asyncio.wait(awaits.keys(), return_when=FIRST_COMPLETED)
119 for d in done:
120 pair = awaits.pop(d)
121 try:
122 item = await d
123 i, it = pair
124 awaits[loop.create_task(anext(it))] = pair
125 yield i, item
126 except StopAsyncIteration:
127 pass
128 finally:
129 # Cancel any remaining iterators
130 for f, (_, it) in awaits.items():
131 with contextlib.suppress(BaseException):
132 f.cancel()
133 await it.aclose()
134
135
136 async def collect_from_async_generator(iterator: AsyncGenerator[T, None]) -> list[T]:
137 """Collect all items from an async generator into a list."""
138 items = []
139 async for item in iterator:
140 items.append(item)
141 return items