litserve 0.2.6.dev2__tar.gz → 0.2.7.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {litserve-0.2.6.dev2/src/litserve.egg-info → litserve-0.2.7.dev0}/PKG-INFO +17 -2
  2. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/requirements.txt +1 -0
  3. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/__about__.py +1 -1
  4. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/api.py +18 -0
  5. litserve-0.2.7.dev0/src/litserve/loops/__init__.py +38 -0
  6. litserve-0.2.7.dev0/src/litserve/loops/base.py +364 -0
  7. litserve-0.2.7.dev0/src/litserve/loops/continuous_batching_loop.py +289 -0
  8. litserve-0.2.7.dev0/src/litserve/loops/loops.py +96 -0
  9. litserve-0.2.7.dev0/src/litserve/loops/simple_loops.py +252 -0
  10. litserve-0.2.7.dev0/src/litserve/loops/streaming_loops.py +234 -0
  11. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/server.py +58 -33
  12. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/specs/base.py +4 -0
  13. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/specs/openai.py +13 -8
  14. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/utils.py +22 -4
  15. litserve-0.2.7.dev0/src/litserve/zmq_queue.py +192 -0
  16. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0/src/litserve.egg-info}/PKG-INFO +17 -2
  17. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve.egg-info/SOURCES.txt +7 -1
  18. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve.egg-info/requires.txt +1 -0
  19. litserve-0.2.6.dev2/src/litserve/loops.py +0 -983
  20. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/LICENSE +0 -0
  21. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/MANIFEST.in +0 -0
  22. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/README.md +0 -0
  23. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/setup.cfg +0 -0
  24. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/setup.py +0 -0
  25. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/__init__.py +0 -0
  26. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/__main__.py +0 -0
  27. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/callbacks/__init__.py +0 -0
  28. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/callbacks/base.py +0 -0
  29. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/callbacks/defaults/__init__.py +0 -0
  30. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/callbacks/defaults/metric_callback.py +0 -0
  31. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/connector.py +0 -0
  32. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/docker_builder.py +0 -0
  33. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/loggers.py +0 -0
  34. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/middlewares.py +0 -0
  35. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/python_client.py +0 -0
  36. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/schema/__init__.py +0 -0
  37. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/schema/image.py +0 -0
  38. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/specs/__init__.py +0 -0
  39. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/specs/openai_embedding.py +0 -0
  40. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/test_examples/__init__.py +0 -0
  41. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/test_examples/openai_embedding_spec_example.py +0 -0
  42. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/test_examples/openai_spec_example.py +0 -0
  43. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve/test_examples/simple_example.py +0 -0
  44. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve.egg-info/dependency_links.txt +0 -0
  45. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve.egg-info/entry_points.txt +0 -0
  46. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve.egg-info/not-zip-safe +0 -0
  47. {litserve-0.2.6.dev2 → litserve-0.2.7.dev0}/src/litserve.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: litserve
3
- Version: 0.2.6.dev2
3
+ Version: 0.2.7.dev0
4
4
  Summary: Lightweight AI server.
5
5
  Home-page: https://github.com/Lightning-AI/litserve
6
6
  Download-URL: https://github.com/Lightning-AI/litserve
@@ -29,6 +29,7 @@ Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
30
  Requires-Dist: fastapi>=0.100
31
31
  Requires-Dist: uvicorn[standard]>=0.29.0
32
+ Requires-Dist: pyzmq>=22.0.0
32
33
  Provides-Extra: perf
33
34
  Requires-Dist: jsonargparse; extra == "perf"
34
35
  Requires-Dist: tenacity; extra == "perf"
@@ -51,6 +52,20 @@ Requires-Dist: python-multipart; extra == "test"
51
52
  Requires-Dist: requests; extra == "test"
52
53
  Requires-Dist: torch>2.0.0; extra == "test"
53
54
  Requires-Dist: transformers; extra == "test"
55
+ Dynamic: author
56
+ Dynamic: author-email
57
+ Dynamic: classifier
58
+ Dynamic: description
59
+ Dynamic: description-content-type
60
+ Dynamic: download-url
61
+ Dynamic: home-page
62
+ Dynamic: keywords
63
+ Dynamic: license
64
+ Dynamic: project-url
65
+ Dynamic: provides-extra
66
+ Dynamic: requires-dist
67
+ Dynamic: requires-python
68
+ Dynamic: summary
54
69
 
55
70
  <div align='center'>
56
71
 
@@ -1,2 +1,3 @@
1
1
  fastapi >=0.100
2
2
  uvicorn[standard] >=0.29.0
3
+ pyzmq >=22.0.0
@@ -11,7 +11,7 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- __version__ = "0.2.6.dev2"
14
+ __version__ = "0.2.7.dev0"
15
15
  __author__ = "Lightning-AI et al."
16
16
  __author_email__ = "community@lightning.ai"
17
17
  __license__ = "Apache-2.0"
@@ -137,3 +137,21 @@ class LitAPI(ABC):
137
137
  )
138
138
  return
139
139
  self._logger_queue.put((key, value))
140
+
141
+ def has_active_requests(self) -> bool:
142
+ raise NotImplementedError("has_active_requests is not implemented")
143
+
144
+ def has_capacity(self) -> bool:
145
+ raise NotImplementedError("has_capacity is not implemented")
146
+
147
+ def health(self) -> bool:
148
+ """Check the additional health status of the API.
149
+
150
+ This method is used in the /health endpoint of the server to determine the health status.
151
+ Users can extend this method to include additional health checks specific to their application.
152
+
153
+ Returns:
154
+ bool: True if the API is healthy, False otherwise.
155
+
156
+ """
157
+ return True
@@ -0,0 +1,38 @@
1
+ # Copyright The Lightning AI team.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import multiprocessing as mp
15
+
16
+ from litserve.loops.base import LitLoop, _BaseLoop
17
+ from litserve.loops.continuous_batching_loop import ContinuousBatchingLoop, Output
18
+ from litserve.loops.loops import (
19
+ get_default_loop,
20
+ inference_worker,
21
+ )
22
+ from litserve.loops.simple_loops import BatchedLoop, SingleLoop
23
+ from litserve.loops.streaming_loops import BatchedStreamingLoop, StreamingLoop
24
+
25
+ mp.allow_connection_pickling()
26
+
27
+ __all__ = [
28
+ "_BaseLoop",
29
+ "LitLoop",
30
+ "get_default_loop",
31
+ "inference_worker",
32
+ "Output",
33
+ "SingleLoop",
34
+ "BatchedLoop",
35
+ "StreamingLoop",
36
+ "BatchedStreamingLoop",
37
+ "ContinuousBatchingLoop",
38
+ ]
@@ -0,0 +1,364 @@
1
+ # Copyright The Lightning AI team.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import asyncio
15
+ import inspect
16
+ import logging
17
+ import pickle
18
+ import signal
19
+ import sys
20
+ import time
21
+ from abc import ABC
22
+ from queue import Empty, Queue
23
+ from typing import Any, Dict, List, Optional, Tuple, Union
24
+
25
+ from starlette.formparsers import MultiPartParser
26
+
27
+ from litserve import LitAPI
28
+ from litserve.callbacks import CallbackRunner
29
+ from litserve.specs.base import LitSpec
30
+ from litserve.utils import LitAPIStatus
31
+ from litserve.zmq_queue import Producer
32
+
33
+ logger = logging.getLogger(__name__)
34
+ # FastAPI writes form files to disk over 1MB by default, which prevents serialization by multiprocessing
35
+ MultiPartParser.max_file_size = sys.maxsize
36
+
37
+
38
+ def _inject_context(context: Union[List[dict], dict], func, *args, **kwargs):
39
+ sig = inspect.signature(func)
40
+ if "context" in sig.parameters:
41
+ return func(*args, **kwargs, context=context)
42
+ return func(*args, **kwargs)
43
+
44
+
45
+ def collate_requests(
46
+ lit_api: LitAPI, request_queue: Queue, max_batch_size: int, batch_timeout: float
47
+ ) -> Tuple[List, List]:
48
+ payloads = []
49
+ timed_out_uids = []
50
+ entered_at = time.monotonic()
51
+ end_time = entered_at + batch_timeout
52
+ apply_timeout = lit_api.request_timeout not in (-1, False)
53
+
54
+ if batch_timeout == 0:
55
+ while len(payloads) < max_batch_size:
56
+ try:
57
+ response_queue_id, uid, timestamp, x_enc = request_queue.get_nowait()
58
+ if apply_timeout and time.monotonic() - timestamp > lit_api.request_timeout:
59
+ timed_out_uids.append((response_queue_id, uid))
60
+ else:
61
+ payloads.append((response_queue_id, uid, x_enc))
62
+ except Empty:
63
+ break
64
+ return payloads, timed_out_uids
65
+
66
+ while time.monotonic() < end_time and len(payloads) < max_batch_size:
67
+ remaining_time = end_time - time.monotonic()
68
+ if remaining_time <= 0:
69
+ break
70
+
71
+ try:
72
+ response_queue_id, uid, timestamp, x_enc = request_queue.get(timeout=min(remaining_time, 0.001))
73
+ if apply_timeout and time.monotonic() - timestamp > lit_api.request_timeout:
74
+ timed_out_uids.append((response_queue_id, uid))
75
+ else:
76
+ payloads.append((response_queue_id, uid, x_enc))
77
+
78
+ except Empty:
79
+ continue
80
+
81
+ return payloads, timed_out_uids
82
+
83
+
84
+ class _BaseLoop(ABC):
85
+ """Loop runs an inference engine that executes a specific set of hooks, implemented in the LitAPI, in a predefined
86
+ order.
87
+
88
+ For a default loop, LitAPI must implement the following hooks:
89
+ - decode_request
90
+ - batch
91
+ - predict
92
+ - unbatch
93
+ - encode_response
94
+
95
+ To implement a custom loop, subclass this class and implement the `run` method. The `run` method should execute the
96
+ hooks in the desired order.
97
+
98
+ `__call__` method is the entry point for the worker process. It calls the `run` method in a loop until the worker is
99
+ terminated.
100
+
101
+ Example:
102
+
103
+ ```python
104
+ class TestLoop(_BaseLoop):
105
+ def run(
106
+ self,
107
+ lit_api: LitAPI,
108
+ lit_spec: Optional[LitSpec],
109
+ device: str,
110
+ worker_id: int,
111
+ request_queue: Queue,
112
+ response_queues: List[Queue],
113
+ max_batch_size: int,
114
+ batch_timeout: float,
115
+ stream: bool,
116
+ workers_setup_status: Dict[int, str],
117
+ callback_runner: CallbackRunner,
118
+ ):
119
+ item = request_queue.get()
120
+ if item is None:
121
+ return
122
+
123
+ response_queue_id, uid, timestamp, x_enc = item
124
+ # Expects LitAPI to implement the load_cache method
125
+ lit_api.load_cache(x_enc)
126
+ x = lit_api.decode_request(x_enc)
127
+ response = lit_api.predict(x)
128
+ response_enc = lit_api.encode_response(response)
129
+ response_queues[response_queue_id].put((uid, (response_enc, LitAPIStatus.OK)))
130
+ ```
131
+
132
+ """
133
+
134
+ def pre_setup(self, lit_api: LitAPI, spec: Optional[LitSpec]):
135
+ pass
136
+
137
+ async def schedule_task(
138
+ self,
139
+ lit_api: LitAPI,
140
+ lit_spec: Optional[LitSpec],
141
+ request_queue: Queue,
142
+ max_batch_size: int,
143
+ batch_timeout: float,
144
+ response_queues: List[Queue],
145
+ ):
146
+ pass
147
+
148
+ def __call__(
149
+ self,
150
+ lit_api: LitAPI,
151
+ lit_spec: Optional[LitSpec],
152
+ device: str,
153
+ worker_id: int,
154
+ request_queue: Queue,
155
+ response_queues: List[Queue],
156
+ max_batch_size: int,
157
+ batch_timeout: float,
158
+ stream: bool,
159
+ workers_setup_status: Dict[int, str],
160
+ callback_runner: CallbackRunner,
161
+ ):
162
+ if asyncio.iscoroutinefunction(self.run):
163
+ event_loop = asyncio.new_event_loop()
164
+
165
+ async def _wrapper():
166
+ logger.info("Running LitLoop in a asyncio event loop")
167
+ future = self.schedule_task(
168
+ lit_api, lit_spec, request_queue, max_batch_size, batch_timeout, response_queues
169
+ )
170
+ _ = event_loop.create_task(future)
171
+ while True:
172
+ try:
173
+ await self.run(
174
+ lit_api,
175
+ lit_spec,
176
+ device,
177
+ worker_id,
178
+ request_queue,
179
+ response_queues,
180
+ max_batch_size,
181
+ batch_timeout,
182
+ stream,
183
+ workers_setup_status,
184
+ callback_runner,
185
+ )
186
+ await asyncio.sleep(0)
187
+ except Exception as e:
188
+ logger.exception("An error occurred in the loop: %s", e)
189
+
190
+ event_loop.run_until_complete(_wrapper())
191
+ else:
192
+ while True:
193
+ self.run(
194
+ lit_api,
195
+ lit_spec,
196
+ device,
197
+ worker_id,
198
+ request_queue,
199
+ response_queues,
200
+ max_batch_size,
201
+ batch_timeout,
202
+ stream,
203
+ workers_setup_status,
204
+ callback_runner,
205
+ )
206
+
207
+ def run(
208
+ self,
209
+ lit_api: LitAPI,
210
+ lit_spec: Optional[LitSpec],
211
+ device: str,
212
+ worker_id: int,
213
+ request_queue: Queue,
214
+ response_queues: List[Queue],
215
+ max_batch_size: int,
216
+ batch_timeout: float,
217
+ stream: bool,
218
+ workers_setup_status: Dict[int, str],
219
+ callback_runner: CallbackRunner,
220
+ ):
221
+ raise NotImplementedError
222
+
223
+
224
+ class LitLoop(_BaseLoop):
225
+ def __init__(self):
226
+ self.producer: Optional[Producer] = None
227
+ self._context = {}
228
+ self._setup_signal_handlers()
229
+
230
+ def get_batch_requests(self, lit_api: LitAPI, request_queue: Queue, max_batch_size: int, batch_timeout: float):
231
+ batches, timed_out_uids = collate_requests(
232
+ lit_api,
233
+ request_queue,
234
+ max_batch_size,
235
+ batch_timeout,
236
+ )
237
+ return batches, timed_out_uids
238
+
239
+ def get_request(self, request_queue: Queue, block: bool = True, timeout: Optional[float] = None):
240
+ try:
241
+ return request_queue.get(block=block, timeout=timeout)
242
+ except Empty:
243
+ return None
244
+
245
+ def populate_context(self, lit_spec: LitSpec, request: Any):
246
+ if lit_spec and hasattr(lit_spec, "populate_context"):
247
+ lit_spec.populate_context(self._context, request)
248
+
249
+ def put_response(
250
+ self, response_queues: List[Queue], response_queue_id: int, uid: str, response_data: Any, status: LitAPIStatus
251
+ ) -> None:
252
+ if self.producer:
253
+ self.producer.put((uid, (response_data, status)), consumer_id=response_queue_id)
254
+ else:
255
+ response_queues[response_queue_id].put((uid, (response_data, status)), block=False)
256
+
257
+ def put_error_response(
258
+ self, response_queues: List[Queue], response_queue_id: int, uid: str, error: Exception
259
+ ) -> None:
260
+ error = pickle.dumps(error)
261
+ self.put_response(response_queues, response_queue_id, uid, error, LitAPIStatus.ERROR)
262
+
263
+ def __del__(self):
264
+ if self.producer:
265
+ self.producer.close()
266
+
267
+ def _setup_signal_handlers(self):
268
+ def cleanup_handler(signum=None, frame=None):
269
+ logging.debug("Worker process received shutdown signal")
270
+ if self.producer:
271
+ self.producer.close()
272
+ sys.exit(0)
273
+
274
+ signal.signal(signal.SIGINT, cleanup_handler)
275
+ signal.signal(signal.SIGTERM, cleanup_handler)
276
+
277
+
278
+ class DefaultLoop(LitLoop):
279
+ def pre_setup(self, lit_api: LitAPI, spec: Optional[LitSpec]):
280
+ # we will sanitize regularly if no spec
281
+ # in case, we have spec then:
282
+ # case 1: spec implements a streaming API
283
+ # Case 2: spec implements a non-streaming API
284
+ if spec:
285
+ # TODO: Implement sanitization
286
+ lit_api._spec = spec
287
+ return
288
+
289
+ original = lit_api.unbatch.__code__ is LitAPI.unbatch.__code__
290
+ if not lit_api.stream and any([
291
+ inspect.isgeneratorfunction(lit_api.predict),
292
+ inspect.isgeneratorfunction(lit_api.encode_response),
293
+ ]):
294
+ raise ValueError(
295
+ """When `stream=False`, `lit_api.predict`, `lit_api.encode_response` must not be
296
+ generator functions.
297
+
298
+ Correct usage:
299
+
300
+ def predict(self, inputs):
301
+ ...
302
+ return {"output": output}
303
+
304
+ Incorrect usage:
305
+
306
+ def predict(self, inputs):
307
+ ...
308
+ for i in range(max_token_length):
309
+ yield prediction
310
+ """
311
+ )
312
+ if (
313
+ lit_api.stream
314
+ and lit_api.max_batch_size > 1
315
+ and not all([
316
+ inspect.isgeneratorfunction(lit_api.predict),
317
+ inspect.isgeneratorfunction(lit_api.encode_response),
318
+ (original or inspect.isgeneratorfunction(lit_api.unbatch)),
319
+ ])
320
+ ):
321
+ raise ValueError(
322
+ """When `stream=True` with max_batch_size > 1, `lit_api.predict`, `lit_api.encode_response` and
323
+ `lit_api.unbatch` must generate values using `yield`.
324
+
325
+ Example:
326
+
327
+ def predict(self, inputs):
328
+ ...
329
+ for i in range(max_token_length):
330
+ yield prediction
331
+
332
+ def encode_response(self, outputs):
333
+ for output in outputs:
334
+ encoded_output = ...
335
+ yield encoded_output
336
+
337
+ def unbatch(self, outputs):
338
+ for output in outputs:
339
+ unbatched_output = ...
340
+ yield unbatched_output
341
+ """
342
+ )
343
+
344
+ if lit_api.stream and not all([
345
+ inspect.isgeneratorfunction(lit_api.predict),
346
+ inspect.isgeneratorfunction(lit_api.encode_response),
347
+ ]):
348
+ raise ValueError(
349
+ """When `stream=True` both `lit_api.predict` and
350
+ `lit_api.encode_response` must generate values using `yield`.
351
+
352
+ Example:
353
+
354
+ def predict(self, inputs):
355
+ ...
356
+ for i in range(max_token_length):
357
+ yield prediction
358
+
359
+ def encode_response(self, outputs):
360
+ for output in outputs:
361
+ encoded_output = ...
362
+ yield encoded_output
363
+ """
364
+ )