litserve 0.2.6.dev0__tar.gz → 0.2.6.dev2__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.
- {litserve-0.2.6.dev0/src/litserve.egg-info → litserve-0.2.6.dev2}/PKG-INFO +5 -5
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/__about__.py +1 -1
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/__init__.py +3 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/api.py +4 -65
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/loops.py +370 -18
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/server.py +12 -6
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/base.py +4 -1
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/openai.py +14 -7
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/openai_embedding.py +11 -5
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/utils.py +44 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2/src/litserve.egg-info}/PKG-INFO +5 -5
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/LICENSE +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/MANIFEST.in +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/README.md +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/requirements.txt +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/setup.cfg +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/setup.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/__main__.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/__init__.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/base.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/defaults/__init__.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/defaults/metric_callback.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/connector.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/docker_builder.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/loggers.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/middlewares.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/python_client.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/schema/__init__.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/schema/image.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/__init__.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/__init__.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/openai_embedding_spec_example.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/openai_spec_example.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/simple_example.py +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/SOURCES.txt +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/dependency_links.txt +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/entry_points.txt +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/not-zip-safe +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/requires.txt +0 -0
- {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: litserve
|
|
3
|
-
Version: 0.2.6.
|
|
3
|
+
Version: 0.2.6.dev2
|
|
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,10 @@ 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
|
+
Provides-Extra: perf
|
|
33
|
+
Requires-Dist: jsonargparse; extra == "perf"
|
|
34
|
+
Requires-Dist: tenacity; extra == "perf"
|
|
35
|
+
Requires-Dist: uvloop; extra == "perf"
|
|
32
36
|
Provides-Extra: test
|
|
33
37
|
Requires-Dist: asgi-lifespan; extra == "test"
|
|
34
38
|
Requires-Dist: coverage[toml]>=7.5.3; extra == "test"
|
|
@@ -47,10 +51,6 @@ Requires-Dist: python-multipart; extra == "test"
|
|
|
47
51
|
Requires-Dist: requests; extra == "test"
|
|
48
52
|
Requires-Dist: torch>2.0.0; extra == "test"
|
|
49
53
|
Requires-Dist: transformers; extra == "test"
|
|
50
|
-
Provides-Extra: perf
|
|
51
|
-
Requires-Dist: jsonargparse; extra == "perf"
|
|
52
|
-
Requires-Dist: tenacity; extra == "perf"
|
|
53
|
-
Requires-Dist: uvloop; extra == "perf"
|
|
54
54
|
|
|
55
55
|
<div align='center'>
|
|
56
56
|
|
|
@@ -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.
|
|
14
|
+
__version__ = "0.2.6.dev2"
|
|
15
15
|
__author__ = "Lightning-AI et al."
|
|
16
16
|
__author_email__ = "community@lightning.ai"
|
|
17
17
|
__license__ = "Apache-2.0"
|
|
@@ -18,6 +18,9 @@ from litserve.callbacks import Callback
|
|
|
18
18
|
from litserve.loggers import Logger
|
|
19
19
|
from litserve.server import LitServer, Request, Response
|
|
20
20
|
from litserve.specs import OpenAIEmbeddingSpec, OpenAISpec
|
|
21
|
+
from litserve.utils import configure_logging
|
|
22
|
+
|
|
23
|
+
configure_logging()
|
|
21
24
|
|
|
22
25
|
__all__ = [
|
|
23
26
|
"LitAPI",
|
|
@@ -11,7 +11,6 @@
|
|
|
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
|
-
import inspect
|
|
15
14
|
import json
|
|
16
15
|
import warnings
|
|
17
16
|
from abc import ABC, abstractmethod
|
|
@@ -57,10 +56,9 @@ class LitAPI(ABC):
|
|
|
57
56
|
|
|
58
57
|
return inputs
|
|
59
58
|
|
|
60
|
-
@abstractmethod
|
|
61
59
|
def predict(self, x, **kwargs):
|
|
62
60
|
"""Run the model on the input and return or yield the output."""
|
|
63
|
-
|
|
61
|
+
raise NotImplementedError("predict is not implemented")
|
|
64
62
|
|
|
65
63
|
def _unbatch_no_stream(self, output):
|
|
66
64
|
if isinstance(output, str):
|
|
@@ -113,75 +111,16 @@ class LitAPI(ABC):
|
|
|
113
111
|
def device(self, value):
|
|
114
112
|
self._device = value
|
|
115
113
|
|
|
116
|
-
def
|
|
114
|
+
def pre_setup(self, max_batch_size: int, spec: Optional[LitSpec]):
|
|
115
|
+
self.max_batch_size = max_batch_size
|
|
117
116
|
if self.stream:
|
|
118
117
|
self._default_unbatch = self._unbatch_stream
|
|
119
118
|
else:
|
|
120
119
|
self._default_unbatch = self._unbatch_no_stream
|
|
121
120
|
|
|
122
|
-
# we will sanitize regularly if no spec
|
|
123
|
-
# in case, we have spec then:
|
|
124
|
-
# case 1: spec implements a streaming API
|
|
125
|
-
# Case 2: spec implements a non-streaming API
|
|
126
121
|
if spec:
|
|
127
|
-
# TODO: Implement sanitization
|
|
128
122
|
self._spec = spec
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
original = self.unbatch.__code__ is LitAPI.unbatch.__code__
|
|
132
|
-
if (
|
|
133
|
-
self.stream
|
|
134
|
-
and max_batch_size > 1
|
|
135
|
-
and not all([
|
|
136
|
-
inspect.isgeneratorfunction(self.predict),
|
|
137
|
-
inspect.isgeneratorfunction(self.encode_response),
|
|
138
|
-
(original or inspect.isgeneratorfunction(self.unbatch)),
|
|
139
|
-
])
|
|
140
|
-
):
|
|
141
|
-
raise ValueError(
|
|
142
|
-
"""When `stream=True` with max_batch_size > 1, `lit_api.predict`, `lit_api.encode_response` and
|
|
143
|
-
`lit_api.unbatch` must generate values using `yield`.
|
|
144
|
-
|
|
145
|
-
Example:
|
|
146
|
-
|
|
147
|
-
def predict(self, inputs):
|
|
148
|
-
...
|
|
149
|
-
for i in range(max_token_length):
|
|
150
|
-
yield prediction
|
|
151
|
-
|
|
152
|
-
def encode_response(self, outputs):
|
|
153
|
-
for output in outputs:
|
|
154
|
-
encoded_output = ...
|
|
155
|
-
yield encoded_output
|
|
156
|
-
|
|
157
|
-
def unbatch(self, outputs):
|
|
158
|
-
for output in outputs:
|
|
159
|
-
unbatched_output = ...
|
|
160
|
-
yield unbatched_output
|
|
161
|
-
"""
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
if self.stream and not all([
|
|
165
|
-
inspect.isgeneratorfunction(self.predict),
|
|
166
|
-
inspect.isgeneratorfunction(self.encode_response),
|
|
167
|
-
]):
|
|
168
|
-
raise ValueError(
|
|
169
|
-
"""When `stream=True` both `lit_api.predict` and
|
|
170
|
-
`lit_api.encode_response` must generate values using `yield`.
|
|
171
|
-
|
|
172
|
-
Example:
|
|
173
|
-
|
|
174
|
-
def predict(self, inputs):
|
|
175
|
-
...
|
|
176
|
-
for i in range(max_token_length):
|
|
177
|
-
yield prediction
|
|
178
|
-
|
|
179
|
-
def encode_response(self, outputs):
|
|
180
|
-
for output in outputs:
|
|
181
|
-
encoded_output = ...
|
|
182
|
-
yield encoded_output
|
|
183
|
-
"""
|
|
184
|
-
)
|
|
123
|
+
spec.pre_setup(self)
|
|
185
124
|
|
|
186
125
|
def set_logger_queue(self, queue: Queue):
|
|
187
126
|
"""Set the queue for logging events."""
|
|
@@ -18,8 +18,9 @@ import multiprocessing as mp
|
|
|
18
18
|
import sys
|
|
19
19
|
import time
|
|
20
20
|
from abc import ABC
|
|
21
|
+
from dataclasses import dataclass
|
|
21
22
|
from queue import Empty, Queue
|
|
22
|
-
from typing import Dict, List, Optional, Tuple, Union
|
|
23
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
23
24
|
|
|
24
25
|
from fastapi import HTTPException
|
|
25
26
|
from starlette.formparsers import MultiPartParser
|
|
@@ -37,10 +38,11 @@ try:
|
|
|
37
38
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
38
39
|
|
|
39
40
|
except ImportError:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if sys.platform != "win32":
|
|
42
|
+
print(
|
|
43
|
+
"uvloop is not installed. Falling back to the default asyncio event loop. "
|
|
44
|
+
"Please install uvloop for better performance using `pip install uvloop`."
|
|
45
|
+
)
|
|
44
46
|
|
|
45
47
|
logger = logging.getLogger(__name__)
|
|
46
48
|
|
|
@@ -439,6 +441,9 @@ class _BaseLoop(ABC):
|
|
|
439
441
|
|
|
440
442
|
"""
|
|
441
443
|
|
|
444
|
+
def pre_setup(self, lit_api: LitAPI, spec: Optional[LitSpec]):
|
|
445
|
+
pass
|
|
446
|
+
|
|
442
447
|
def __call__(
|
|
443
448
|
self,
|
|
444
449
|
lit_api: LitAPI,
|
|
@@ -485,7 +490,108 @@ class _BaseLoop(ABC):
|
|
|
485
490
|
raise NotImplementedError
|
|
486
491
|
|
|
487
492
|
|
|
488
|
-
class
|
|
493
|
+
class LitLoop(_BaseLoop):
|
|
494
|
+
def __init__(self):
|
|
495
|
+
self._context = {}
|
|
496
|
+
|
|
497
|
+
def get_batch_requests(self, lit_api: LitAPI, request_queue: Queue, max_batch_size: int, batch_timeout: float):
|
|
498
|
+
batches, timed_out_uids = collate_requests(
|
|
499
|
+
lit_api,
|
|
500
|
+
request_queue,
|
|
501
|
+
max_batch_size,
|
|
502
|
+
batch_timeout,
|
|
503
|
+
)
|
|
504
|
+
return batches, timed_out_uids
|
|
505
|
+
|
|
506
|
+
def get_request(self, request_queue: Queue, timeout: float = 1.0):
|
|
507
|
+
try:
|
|
508
|
+
return request_queue.get(timeout=timeout)
|
|
509
|
+
except Empty:
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
def populate_context(self, lit_spec: LitSpec, request: Any):
|
|
513
|
+
if lit_spec and hasattr(lit_spec, "populate_context"):
|
|
514
|
+
lit_spec.populate_context(self._context, request)
|
|
515
|
+
|
|
516
|
+
def put_response(
|
|
517
|
+
self, response_queues: List[Queue], response_queue_id: int, uid: str, response_data: Any, status: LitAPIStatus
|
|
518
|
+
) -> None:
|
|
519
|
+
response_queues[response_queue_id].put((uid, (response_data, status)))
|
|
520
|
+
|
|
521
|
+
def put_error_response(
|
|
522
|
+
self, response_queues: List[Queue], response_queue_id: int, uid: str, error: Exception
|
|
523
|
+
) -> None:
|
|
524
|
+
response_queues[response_queue_id].put((uid, (error, LitAPIStatus.ERROR)))
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class DefaultLoop(LitLoop):
|
|
528
|
+
def pre_setup(self, lit_api: LitAPI, spec: Optional[LitSpec]):
|
|
529
|
+
# we will sanitize regularly if no spec
|
|
530
|
+
# in case, we have spec then:
|
|
531
|
+
# case 1: spec implements a streaming API
|
|
532
|
+
# Case 2: spec implements a non-streaming API
|
|
533
|
+
if spec:
|
|
534
|
+
# TODO: Implement sanitization
|
|
535
|
+
lit_api._spec = spec
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
original = lit_api.unbatch.__code__ is LitAPI.unbatch.__code__
|
|
539
|
+
if (
|
|
540
|
+
lit_api.stream
|
|
541
|
+
and lit_api.max_batch_size > 1
|
|
542
|
+
and not all([
|
|
543
|
+
inspect.isgeneratorfunction(lit_api.predict),
|
|
544
|
+
inspect.isgeneratorfunction(lit_api.encode_response),
|
|
545
|
+
(original or inspect.isgeneratorfunction(lit_api.unbatch)),
|
|
546
|
+
])
|
|
547
|
+
):
|
|
548
|
+
raise ValueError(
|
|
549
|
+
"""When `stream=True` with max_batch_size > 1, `lit_api.predict`, `lit_api.encode_response` and
|
|
550
|
+
`lit_api.unbatch` must generate values using `yield`.
|
|
551
|
+
|
|
552
|
+
Example:
|
|
553
|
+
|
|
554
|
+
def predict(self, inputs):
|
|
555
|
+
...
|
|
556
|
+
for i in range(max_token_length):
|
|
557
|
+
yield prediction
|
|
558
|
+
|
|
559
|
+
def encode_response(self, outputs):
|
|
560
|
+
for output in outputs:
|
|
561
|
+
encoded_output = ...
|
|
562
|
+
yield encoded_output
|
|
563
|
+
|
|
564
|
+
def unbatch(self, outputs):
|
|
565
|
+
for output in outputs:
|
|
566
|
+
unbatched_output = ...
|
|
567
|
+
yield unbatched_output
|
|
568
|
+
"""
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if lit_api.stream and not all([
|
|
572
|
+
inspect.isgeneratorfunction(lit_api.predict),
|
|
573
|
+
inspect.isgeneratorfunction(lit_api.encode_response),
|
|
574
|
+
]):
|
|
575
|
+
raise ValueError(
|
|
576
|
+
"""When `stream=True` both `lit_api.predict` and
|
|
577
|
+
`lit_api.encode_response` must generate values using `yield`.
|
|
578
|
+
|
|
579
|
+
Example:
|
|
580
|
+
|
|
581
|
+
def predict(self, inputs):
|
|
582
|
+
...
|
|
583
|
+
for i in range(max_token_length):
|
|
584
|
+
yield prediction
|
|
585
|
+
|
|
586
|
+
def encode_response(self, outputs):
|
|
587
|
+
for output in outputs:
|
|
588
|
+
encoded_output = ...
|
|
589
|
+
yield encoded_output
|
|
590
|
+
"""
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class SingleLoop(DefaultLoop):
|
|
489
595
|
def __call__(
|
|
490
596
|
self,
|
|
491
597
|
lit_api: LitAPI,
|
|
@@ -503,7 +609,7 @@ class SingleLoop(_BaseLoop):
|
|
|
503
609
|
run_single_loop(lit_api, lit_spec, request_queue, response_queues, callback_runner)
|
|
504
610
|
|
|
505
611
|
|
|
506
|
-
class BatchedLoop(
|
|
612
|
+
class BatchedLoop(DefaultLoop):
|
|
507
613
|
def __call__(
|
|
508
614
|
self,
|
|
509
615
|
lit_api: LitAPI,
|
|
@@ -529,7 +635,7 @@ class BatchedLoop(_BaseLoop):
|
|
|
529
635
|
)
|
|
530
636
|
|
|
531
637
|
|
|
532
|
-
class StreamingLoop(
|
|
638
|
+
class StreamingLoop(DefaultLoop):
|
|
533
639
|
def __call__(
|
|
534
640
|
self,
|
|
535
641
|
lit_api: LitAPI,
|
|
@@ -547,7 +653,7 @@ class StreamingLoop(_BaseLoop):
|
|
|
547
653
|
run_streaming_loop(lit_api, lit_spec, request_queue, response_queues, callback_runner)
|
|
548
654
|
|
|
549
655
|
|
|
550
|
-
class BatchedStreamingLoop(
|
|
656
|
+
class BatchedStreamingLoop(DefaultLoop):
|
|
551
657
|
def __call__(
|
|
552
658
|
self,
|
|
553
659
|
lit_api: LitAPI,
|
|
@@ -573,6 +679,248 @@ class BatchedStreamingLoop(_BaseLoop):
|
|
|
573
679
|
)
|
|
574
680
|
|
|
575
681
|
|
|
682
|
+
def notify_timed_out_requests(
|
|
683
|
+
response_queues: List[Queue],
|
|
684
|
+
timed_out_uids: List[Tuple[int, str]],
|
|
685
|
+
):
|
|
686
|
+
for response_queue_id, uid in timed_out_uids:
|
|
687
|
+
logger.error(f"Request {uid} was waiting in the queue for too long and has been timed out.")
|
|
688
|
+
response_queues[response_queue_id].put((uid, (HTTPException(504, "Request timed out"), LitAPIStatus.ERROR)))
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@dataclass
|
|
692
|
+
class Output:
|
|
693
|
+
"""Outputs from a single step of the loop."""
|
|
694
|
+
|
|
695
|
+
uid: str
|
|
696
|
+
output: Any
|
|
697
|
+
status: LitAPIStatus
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class ContinuousBatchingLoop(LitLoop):
|
|
701
|
+
def __init__(self, max_sequence_length: int = 2048):
|
|
702
|
+
"""Runs continuous batching loop. This loop handles adding new requests, processing them in batches, and
|
|
703
|
+
managing the state of active sequences.
|
|
704
|
+
|
|
705
|
+
The loop requires the following methods to be implemented in the LitAPI:
|
|
706
|
+
- setup: sets up the model on the device
|
|
707
|
+
- decode_request: decodes the client request into a format that can be processed by the model
|
|
708
|
+
- step: generates a new token for each sequence
|
|
709
|
+
- encode_response: encodes the response into a format that can be sent to the client
|
|
710
|
+
- has_finished: checks if the sequence has finished generating
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
max_sequence_length (int): The maximum sequence length allowed for any active sequence.
|
|
714
|
+
|
|
715
|
+
"""
|
|
716
|
+
super().__init__()
|
|
717
|
+
self.active_sequences: Dict[str, Dict] = {} # uid -> {input, current_length, generated_sequence}
|
|
718
|
+
self.max_sequence_length = max_sequence_length
|
|
719
|
+
self.response_queue_ids: Dict[str, int] = {} # uid -> response_queue_id
|
|
720
|
+
|
|
721
|
+
def pre_setup(self, lit_api: LitAPI, spec: Optional[LitSpec]):
|
|
722
|
+
if not lit_api.stream:
|
|
723
|
+
raise ValueError(
|
|
724
|
+
"Continuous batching loop requires streaming to be enabled. Please set LitServe(..., stream=True)"
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
if not hasattr(lit_api, "step") and not hasattr(lit_api, "predict"):
|
|
728
|
+
raise ValueError("""Using the default step method with Continuous batching loop requires the lit_api to
|
|
729
|
+
have a `predict` method which accepts decoded request inputs and a list of generated_sequence.
|
|
730
|
+
Please implement the has_finished method in the lit_api.
|
|
731
|
+
|
|
732
|
+
class ExampleAPI(LitAPI):
|
|
733
|
+
...
|
|
734
|
+
def predict(self, inputs, generated_sequence):
|
|
735
|
+
# implement predict logic
|
|
736
|
+
# return list of new tokens
|
|
737
|
+
...
|
|
738
|
+
""")
|
|
739
|
+
|
|
740
|
+
if not hasattr(lit_api, "step") and not hasattr(lit_api, "has_finished"):
|
|
741
|
+
raise ValueError("""Using the default step method with Continuous batching loop
|
|
742
|
+
requires the lit_api to have a has_finished method. Please implement the has_finished method in the lit_api.
|
|
743
|
+
|
|
744
|
+
class ExampleAPI(LitAPI):
|
|
745
|
+
...
|
|
746
|
+
def has_finished(self, uid: str, token: str, max_sequence_length: int) -> bool:
|
|
747
|
+
# implement has_finished logic
|
|
748
|
+
return False
|
|
749
|
+
""")
|
|
750
|
+
|
|
751
|
+
def add_request(self, uid: str, request: Any, lit_api: LitAPI, lit_spec: Optional[LitSpec]) -> None:
|
|
752
|
+
"""Add a new sequence to active sequences and perform any action before prediction such as filling the cache."""
|
|
753
|
+
if hasattr(lit_api, "add_request"):
|
|
754
|
+
lit_api.add_request(uid, request)
|
|
755
|
+
decoded_request = lit_api.decode_request(request)
|
|
756
|
+
self.active_sequences[uid] = {"input": decoded_request, "current_length": 0, "generated_sequence": []}
|
|
757
|
+
|
|
758
|
+
def mark_completed(self, uid: str) -> None:
|
|
759
|
+
"""Mark a request as completed and remove it from the tracked state."""
|
|
760
|
+
logger.debug(f"Marking sequence {uid} as completed")
|
|
761
|
+
del self.active_sequences[uid]
|
|
762
|
+
del self.response_queue_ids[uid]
|
|
763
|
+
|
|
764
|
+
def has_capacity(self, lit_api: LitAPI) -> bool:
|
|
765
|
+
"""Check if we can add more sequences based on current batch."""
|
|
766
|
+
capacity = len(self.active_sequences) < lit_api.max_batch_size
|
|
767
|
+
if not capacity:
|
|
768
|
+
logger.info(
|
|
769
|
+
f"No capacity: {len(self.active_sequences)} active sequences, max batch size: {lit_api.max_batch_size}"
|
|
770
|
+
)
|
|
771
|
+
return capacity
|
|
772
|
+
|
|
773
|
+
def step(self, prev_outputs: Optional[List[Output]], lit_api: LitAPI, lit_spec: Optional[LitSpec]) -> List[Output]:
|
|
774
|
+
"""Process one token generation step for all active sequences."""
|
|
775
|
+
if hasattr(lit_api, "step"):
|
|
776
|
+
return lit_api.step(prev_outputs)
|
|
777
|
+
|
|
778
|
+
if not self.active_sequences:
|
|
779
|
+
return []
|
|
780
|
+
|
|
781
|
+
# Batch forward pass for all active sequences
|
|
782
|
+
inputs = [seq["input"] for seq in self.active_sequences.values()]
|
|
783
|
+
generated = [seq["generated_sequence"] for seq in self.active_sequences.values()]
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
# Assume lit_api.predict handles batched token generation
|
|
787
|
+
new_tokens: List[Any] = lit_api.predict(inputs, generated)
|
|
788
|
+
|
|
789
|
+
responses: List[Output] = []
|
|
790
|
+
|
|
791
|
+
# Process each sequence's new token
|
|
792
|
+
for uid, token in zip(self.active_sequences.keys(), new_tokens):
|
|
793
|
+
seq = self.active_sequences[uid]
|
|
794
|
+
seq["generated_sequence"].append(token)
|
|
795
|
+
seq["current_length"] += 1
|
|
796
|
+
|
|
797
|
+
step_output = Output(uid, token, LitAPIStatus.OK)
|
|
798
|
+
responses.append(step_output)
|
|
799
|
+
|
|
800
|
+
# Check completion conditions
|
|
801
|
+
is_finished = lit_api.has_finished(uid, token, self.max_sequence_length)
|
|
802
|
+
|
|
803
|
+
if is_finished:
|
|
804
|
+
# Encode final response for completed sequence
|
|
805
|
+
step_output = Output(uid, "", LitAPIStatus.FINISH_STREAMING)
|
|
806
|
+
responses.append(step_output)
|
|
807
|
+
|
|
808
|
+
return responses
|
|
809
|
+
|
|
810
|
+
except Exception as e:
|
|
811
|
+
logger.exception("Error during batch token generation")
|
|
812
|
+
# On error, terminate all active sequences
|
|
813
|
+
responses = [(uid, (e, LitAPIStatus.ERROR)) for uid in self.active_sequences]
|
|
814
|
+
self.active_sequences.clear()
|
|
815
|
+
return responses
|
|
816
|
+
|
|
817
|
+
def prefill(
|
|
818
|
+
self,
|
|
819
|
+
pending_requests: List[Tuple[str, Any]],
|
|
820
|
+
lit_api: LitAPI,
|
|
821
|
+
lit_spec: Optional[LitSpec],
|
|
822
|
+
request_queue: Queue,
|
|
823
|
+
max_batch_size: int,
|
|
824
|
+
response_queues: List[Queue],
|
|
825
|
+
) -> List[Tuple[str, Any]]:
|
|
826
|
+
"""Fill available capacity with pending and new requests."""
|
|
827
|
+
# First process existing pending requests
|
|
828
|
+
while pending_requests and self.has_capacity(lit_api):
|
|
829
|
+
response_queue_id, uid, input = pending_requests.pop(0)
|
|
830
|
+
self.add_request(uid, input, lit_api, lit_spec)
|
|
831
|
+
self.response_queue_ids[uid] = response_queue_id
|
|
832
|
+
|
|
833
|
+
# Then check for new requests if we still have capacity
|
|
834
|
+
if self.has_capacity(lit_api):
|
|
835
|
+
new_batches, timed_out_uids = self.get_batch_requests(
|
|
836
|
+
lit_api, request_queue, max_batch_size, batch_timeout=0.0001
|
|
837
|
+
)
|
|
838
|
+
notify_timed_out_requests(response_queues, timed_out_uids)
|
|
839
|
+
|
|
840
|
+
if new_batches:
|
|
841
|
+
# Add new requests to pending_requests and try to process them
|
|
842
|
+
for response_queue_id, uid, input in new_batches:
|
|
843
|
+
logger.debug(f"New request: {uid}, {input}")
|
|
844
|
+
if self.has_capacity(lit_api):
|
|
845
|
+
self.add_request(uid, input, lit_api, lit_spec)
|
|
846
|
+
self.response_queue_ids[uid] = response_queue_id
|
|
847
|
+
else:
|
|
848
|
+
pending_requests.append((response_queue_id, uid, input))
|
|
849
|
+
|
|
850
|
+
return pending_requests
|
|
851
|
+
|
|
852
|
+
def run(
|
|
853
|
+
self,
|
|
854
|
+
lit_api: LitAPI,
|
|
855
|
+
lit_spec: Optional[LitSpec],
|
|
856
|
+
device: str,
|
|
857
|
+
worker_id: int,
|
|
858
|
+
request_queue: Queue,
|
|
859
|
+
response_queues: List[Queue],
|
|
860
|
+
max_batch_size: int,
|
|
861
|
+
batch_timeout: float,
|
|
862
|
+
stream: bool,
|
|
863
|
+
workers_setup_status: Dict[int, str],
|
|
864
|
+
callback_runner: CallbackRunner,
|
|
865
|
+
):
|
|
866
|
+
"""Main loop that processes batches of requests."""
|
|
867
|
+
pending_requests = self.prefill(
|
|
868
|
+
[],
|
|
869
|
+
lit_api,
|
|
870
|
+
lit_spec,
|
|
871
|
+
request_queue,
|
|
872
|
+
max_batch_size,
|
|
873
|
+
response_queues,
|
|
874
|
+
)
|
|
875
|
+
try:
|
|
876
|
+
prev_outputs = None
|
|
877
|
+
while pending_requests or self.active_sequences:
|
|
878
|
+
# Process one step for all active sequences
|
|
879
|
+
responses = self.step(prev_outputs, lit_api, lit_spec)
|
|
880
|
+
logger.debug(f"Responses from step(): {responses}")
|
|
881
|
+
if len(responses) == 0:
|
|
882
|
+
raise HTTPException(500, "No responses from step()")
|
|
883
|
+
if responses and not isinstance(responses[0], Output):
|
|
884
|
+
raise HTTPException(500, "Expected StepOutput from step()")
|
|
885
|
+
|
|
886
|
+
prev_outputs = responses
|
|
887
|
+
|
|
888
|
+
# Send responses for all sequences (both streaming and completed)
|
|
889
|
+
for step_output in responses:
|
|
890
|
+
logger.debug(f"Processing response: {step_output}")
|
|
891
|
+
status = step_output.status
|
|
892
|
+
response_data = lit_api.encode_response(step_output.output)
|
|
893
|
+
uid = step_output.uid
|
|
894
|
+
response_queue_id = self.response_queue_ids[uid]
|
|
895
|
+
|
|
896
|
+
response_data = lit_api.format_encoded_response(response_data)
|
|
897
|
+
if status == LitAPIStatus.ERROR:
|
|
898
|
+
self.put_error_response(response_queues, response_queue_id, uid, response_data)
|
|
899
|
+
self.mark_completed(uid)
|
|
900
|
+
elif status == LitAPIStatus.FINISH_STREAMING:
|
|
901
|
+
self.put_response(response_queues, response_queue_id, uid, response_data, status)
|
|
902
|
+
self.mark_completed(uid)
|
|
903
|
+
else:
|
|
904
|
+
self.put_response(response_queues, response_queue_id, uid, response_data, status)
|
|
905
|
+
|
|
906
|
+
# Fill available capacity with both pending and new requests
|
|
907
|
+
pending_requests = self.prefill(
|
|
908
|
+
pending_requests,
|
|
909
|
+
lit_api,
|
|
910
|
+
lit_spec,
|
|
911
|
+
request_queue,
|
|
912
|
+
max_batch_size,
|
|
913
|
+
response_queues,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
except Exception as e:
|
|
917
|
+
logger.exception(f"Error in continuous batching loop: {e}")
|
|
918
|
+
# Handle any errors by sending error responses for all tracked requests
|
|
919
|
+
for uid, response_queue_id in self.response_queue_ids.items():
|
|
920
|
+
self.put_error_response(response_queues, response_queue_id, uid, e)
|
|
921
|
+
self.response_queue_ids.clear()
|
|
922
|
+
|
|
923
|
+
|
|
576
924
|
def inference_worker(
|
|
577
925
|
lit_api: LitAPI,
|
|
578
926
|
lit_spec: Optional[LitSpec],
|
|
@@ -606,15 +954,7 @@ def inference_worker(
|
|
|
606
954
|
logging.info(f"LitServe will use {lit_spec.__class__.__name__} spec")
|
|
607
955
|
|
|
608
956
|
if loop == "auto":
|
|
609
|
-
loop = (
|
|
610
|
-
BatchedStreamingLoop()
|
|
611
|
-
if stream and max_batch_size > 1
|
|
612
|
-
else StreamingLoop()
|
|
613
|
-
if stream
|
|
614
|
-
else BatchedLoop()
|
|
615
|
-
if max_batch_size > 1
|
|
616
|
-
else SingleLoop()
|
|
617
|
-
)
|
|
957
|
+
loop = get_default_loop(stream, max_batch_size)
|
|
618
958
|
|
|
619
959
|
loop(
|
|
620
960
|
lit_api,
|
|
@@ -629,3 +969,15 @@ def inference_worker(
|
|
|
629
969
|
workers_setup_status,
|
|
630
970
|
callback_runner,
|
|
631
971
|
)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def get_default_loop(stream: bool, max_batch_size: int) -> _BaseLoop:
|
|
975
|
+
return (
|
|
976
|
+
BatchedStreamingLoop()
|
|
977
|
+
if stream and max_batch_size > 1
|
|
978
|
+
else StreamingLoop()
|
|
979
|
+
if stream
|
|
980
|
+
else BatchedLoop()
|
|
981
|
+
if max_batch_size > 1
|
|
982
|
+
else SingleLoop()
|
|
983
|
+
)
|
|
@@ -40,7 +40,7 @@ from litserve import LitAPI
|
|
|
40
40
|
from litserve.callbacks.base import Callback, CallbackRunner, EventTypes
|
|
41
41
|
from litserve.connector import _Connector
|
|
42
42
|
from litserve.loggers import Logger, _LoggerConnector
|
|
43
|
-
from litserve.loops import
|
|
43
|
+
from litserve.loops import LitLoop, get_default_loop, inference_worker
|
|
44
44
|
from litserve.middlewares import MaxSizeMiddleware, RequestCountMiddleware
|
|
45
45
|
from litserve.python_client import client_template
|
|
46
46
|
from litserve.specs import OpenAISpec
|
|
@@ -113,7 +113,7 @@ class LitServer:
|
|
|
113
113
|
spec: Optional[LitSpec] = None,
|
|
114
114
|
max_payload_size=None,
|
|
115
115
|
track_requests: bool = False,
|
|
116
|
-
loop: Optional[Union[str,
|
|
116
|
+
loop: Optional[Union[str, LitLoop]] = "auto",
|
|
117
117
|
callbacks: Optional[Union[List[Callback], Callback]] = None,
|
|
118
118
|
middlewares: Optional[list[Union[Callable, tuple[Callable, dict]]]] = None,
|
|
119
119
|
loggers: Optional[Union[Logger, List[Logger]]] = None,
|
|
@@ -154,6 +154,8 @@ class LitServer:
|
|
|
154
154
|
|
|
155
155
|
if isinstance(loop, str) and loop != "auto":
|
|
156
156
|
raise ValueError("loop must be an instance of _BaseLoop or 'auto'")
|
|
157
|
+
if loop == "auto":
|
|
158
|
+
loop = get_default_loop(stream, max_batch_size)
|
|
157
159
|
|
|
158
160
|
if middlewares is None:
|
|
159
161
|
middlewares = []
|
|
@@ -198,7 +200,7 @@ class LitServer:
|
|
|
198
200
|
"but the max_batch_size parameter was not set."
|
|
199
201
|
)
|
|
200
202
|
|
|
201
|
-
self._loop = loop
|
|
203
|
+
self._loop: LitLoop = loop
|
|
202
204
|
self.api_path = api_path
|
|
203
205
|
self.healthcheck_path = healthcheck_path
|
|
204
206
|
self.info_path = info_path
|
|
@@ -206,7 +208,8 @@ class LitServer:
|
|
|
206
208
|
self.timeout = timeout
|
|
207
209
|
lit_api.stream = stream
|
|
208
210
|
lit_api.request_timeout = self.timeout
|
|
209
|
-
lit_api.
|
|
211
|
+
lit_api.pre_setup(max_batch_size, spec=spec)
|
|
212
|
+
self._loop.pre_setup(lit_api, spec=spec)
|
|
210
213
|
self.app = FastAPI(lifespan=self.lifespan)
|
|
211
214
|
self.app.response_queue_id = None
|
|
212
215
|
self.response_queue_id = None
|
|
@@ -427,9 +430,12 @@ class LitServer:
|
|
|
427
430
|
|
|
428
431
|
await event.wait()
|
|
429
432
|
response, status = self.response_buffer.pop(uid)
|
|
430
|
-
|
|
431
|
-
|
|
433
|
+
if status == LitAPIStatus.ERROR and isinstance(response, HTTPException):
|
|
434
|
+
logger.error("Error in request: %s", response)
|
|
432
435
|
raise response
|
|
436
|
+
if status == LitAPIStatus.ERROR:
|
|
437
|
+
logger.error("Error in request: %s", response)
|
|
438
|
+
raise HTTPException(status_code=500)
|
|
433
439
|
self._callback_runner.trigger_event(EventTypes.ON_RESPONSE, litserver=self)
|
|
434
440
|
return response
|
|
435
441
|
|
|
@@ -15,7 +15,7 @@ from abc import abstractmethod
|
|
|
15
15
|
from typing import TYPE_CHECKING, Callable, List
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from litserve import LitServer
|
|
18
|
+
from litserve import LitAPI, LitServer
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class LitSpec:
|
|
@@ -26,6 +26,9 @@ class LitSpec:
|
|
|
26
26
|
|
|
27
27
|
self._server: LitServer = None
|
|
28
28
|
|
|
29
|
+
def pre_setup(self, lit_api: "LitAPI"):
|
|
30
|
+
pass
|
|
31
|
+
|
|
29
32
|
def setup(self, server: "LitServer"):
|
|
30
33
|
self._server = server
|
|
31
34
|
|
|
@@ -30,7 +30,7 @@ from litserve.specs.base import LitSpec
|
|
|
30
30
|
from litserve.utils import LitAPIStatus, azip
|
|
31
31
|
|
|
32
32
|
if typing.TYPE_CHECKING:
|
|
33
|
-
from litserve import LitServer
|
|
33
|
+
from litserve import LitAPI, LitServer
|
|
34
34
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
@@ -262,18 +262,18 @@ class OpenAISpec(LitSpec):
|
|
|
262
262
|
self.add_endpoint("/v1/chat/completions", self.chat_completion, ["POST"])
|
|
263
263
|
self.add_endpoint("/v1/chat/completions", self.options_chat_completions, ["OPTIONS"])
|
|
264
264
|
|
|
265
|
-
def
|
|
265
|
+
def pre_setup(self, lit_api: "LitAPI"):
|
|
266
266
|
from litserve import LitAPI
|
|
267
267
|
|
|
268
|
-
super().setup(server)
|
|
269
|
-
|
|
270
|
-
lit_api = self._server.lit_api
|
|
271
268
|
if not inspect.isgeneratorfunction(lit_api.predict):
|
|
272
269
|
raise ValueError(LITAPI_VALIDATION_MSG.format("predict is not a generator"))
|
|
273
270
|
|
|
274
271
|
is_encode_response_original = lit_api.encode_response.__code__ is LitAPI.encode_response.__code__
|
|
275
272
|
if not is_encode_response_original and not inspect.isgeneratorfunction(lit_api.encode_response):
|
|
276
273
|
raise ValueError(LITAPI_VALIDATION_MSG.format("encode_response is not a generator"))
|
|
274
|
+
|
|
275
|
+
def setup(self, server: "LitServer"):
|
|
276
|
+
super().setup(server)
|
|
277
277
|
print("OpenAI spec setup complete")
|
|
278
278
|
|
|
279
279
|
def populate_context(self, context, request):
|
|
@@ -381,8 +381,11 @@ class OpenAISpec(LitSpec):
|
|
|
381
381
|
usage_infos = []
|
|
382
382
|
# iterate over n choices
|
|
383
383
|
for i, (response, status) in enumerate(streaming_response):
|
|
384
|
-
if status == LitAPIStatus.ERROR:
|
|
384
|
+
if status == LitAPIStatus.ERROR and isinstance(response, HTTPException):
|
|
385
385
|
raise response
|
|
386
|
+
elif status == LitAPIStatus.ERROR:
|
|
387
|
+
logger.error("Error in streaming response: %s", response)
|
|
388
|
+
raise HTTPException(status_code=500)
|
|
386
389
|
encoded_response = json.loads(response)
|
|
387
390
|
logger.debug(encoded_response)
|
|
388
391
|
chat_msg = ChoiceDelta(**encoded_response)
|
|
@@ -425,8 +428,12 @@ class OpenAISpec(LitSpec):
|
|
|
425
428
|
tool_calls = None
|
|
426
429
|
usage = None
|
|
427
430
|
async for response, status in streaming_response:
|
|
428
|
-
if status == LitAPIStatus.ERROR:
|
|
431
|
+
if status == LitAPIStatus.ERROR and isinstance(response, HTTPException):
|
|
429
432
|
raise response
|
|
433
|
+
if status == LitAPIStatus.ERROR:
|
|
434
|
+
logger.error("Error in OpenAI non-streaming response: %s", response)
|
|
435
|
+
raise HTTPException(status_code=500)
|
|
436
|
+
|
|
430
437
|
# data from LitAPI.encode_response
|
|
431
438
|
encoded_response = json.loads(response)
|
|
432
439
|
logger.debug(encoded_response)
|
|
@@ -18,7 +18,8 @@ import time
|
|
|
18
18
|
import uuid
|
|
19
19
|
from typing import List, Literal, Optional, Union
|
|
20
20
|
|
|
21
|
-
from fastapi import Request, Response, status
|
|
21
|
+
from fastapi import HTTPException, Request, Response, status
|
|
22
|
+
from fastapi import status as status_code
|
|
22
23
|
from pydantic import BaseModel
|
|
23
24
|
|
|
24
25
|
from litserve.specs.base import LitSpec
|
|
@@ -28,10 +29,11 @@ logger = logging.getLogger(__name__)
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class EmbeddingRequest(BaseModel):
|
|
31
|
-
input: Union[str, List[str]]
|
|
32
|
+
input: Union[str, List[str], List[int], List[List[int]]]
|
|
32
33
|
model: str
|
|
33
34
|
dimensions: Optional[int] = None
|
|
34
|
-
encoding_format: Literal["float"] = "float"
|
|
35
|
+
encoding_format: Literal["float", "base64"] = "float"
|
|
36
|
+
user: Optional[str] = None
|
|
35
37
|
|
|
36
38
|
def ensure_list(self):
|
|
37
39
|
return self.input if isinstance(self.input, list) else [self.input]
|
|
@@ -147,7 +149,7 @@ class OpenAIEmbeddingSpec(LitSpec):
|
|
|
147
149
|
f"{EMBEDDING_API_EXAMPLE}"
|
|
148
150
|
)
|
|
149
151
|
|
|
150
|
-
async def embeddings(self, request: EmbeddingRequest):
|
|
152
|
+
async def embeddings(self, request: EmbeddingRequest) -> EmbeddingResponse:
|
|
151
153
|
response_queue_id = self.response_queue_id
|
|
152
154
|
logger.debug("Received embedding request: %s", request)
|
|
153
155
|
uid = uuid.uuid4()
|
|
@@ -159,8 +161,12 @@ class OpenAIEmbeddingSpec(LitSpec):
|
|
|
159
161
|
|
|
160
162
|
response, status = self._server.response_buffer.pop(uid)
|
|
161
163
|
|
|
162
|
-
if status == LitAPIStatus.ERROR:
|
|
164
|
+
if status == LitAPIStatus.ERROR and isinstance(response, HTTPException):
|
|
165
|
+
logger.error("Error in embedding request: %s", response)
|
|
163
166
|
raise response
|
|
167
|
+
if status == LitAPIStatus.ERROR:
|
|
168
|
+
logger.error("Error in embedding request: %s", response)
|
|
169
|
+
raise HTTPException(status_code=status_code.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
164
170
|
|
|
165
171
|
logger.debug(response)
|
|
166
172
|
|
|
@@ -15,6 +15,7 @@ import asyncio
|
|
|
15
15
|
import dataclasses
|
|
16
16
|
import logging
|
|
17
17
|
import pickle
|
|
18
|
+
import sys
|
|
18
19
|
from contextlib import contextmanager
|
|
19
20
|
from typing import TYPE_CHECKING, AsyncIterator
|
|
20
21
|
|
|
@@ -87,3 +88,46 @@ class WorkerSetupStatus:
|
|
|
87
88
|
READY: str = "ready"
|
|
88
89
|
ERROR: str = "error"
|
|
89
90
|
FINISHED: str = "finished"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def configure_logging(
|
|
94
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", stream=sys.stdout
|
|
95
|
+
):
|
|
96
|
+
"""Configure logging for the entire library with sensible defaults.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
level (int): Logging level (default: logging.INFO)
|
|
100
|
+
format (str): Log message format string
|
|
101
|
+
stream (file-like): Output stream for logs
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
# Create a library-wide handler
|
|
105
|
+
handler = logging.StreamHandler(stream)
|
|
106
|
+
|
|
107
|
+
# Set formatter with user-configurable format
|
|
108
|
+
formatter = logging.Formatter(format)
|
|
109
|
+
handler.setFormatter(formatter)
|
|
110
|
+
|
|
111
|
+
# Configure root library logger
|
|
112
|
+
library_logger = logging.getLogger("litserve")
|
|
113
|
+
library_logger.setLevel(level)
|
|
114
|
+
library_logger.addHandler(handler)
|
|
115
|
+
|
|
116
|
+
# Prevent propagation to root logger to avoid duplicate logs
|
|
117
|
+
library_logger.propagate = False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def set_log_level(level):
|
|
121
|
+
"""Allow users to set the global logging level for the library."""
|
|
122
|
+
logging.getLogger("litserve").setLevel(level)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def add_log_handler(handler):
|
|
126
|
+
"""Allow users to add custom log handlers.
|
|
127
|
+
|
|
128
|
+
Example usage:
|
|
129
|
+
file_handler = logging.FileHandler('library_logs.log')
|
|
130
|
+
add_log_handler(file_handler)
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
logging.getLogger("litserve").addHandler(handler)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: litserve
|
|
3
|
-
Version: 0.2.6.
|
|
3
|
+
Version: 0.2.6.dev2
|
|
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,10 @@ 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
|
+
Provides-Extra: perf
|
|
33
|
+
Requires-Dist: jsonargparse; extra == "perf"
|
|
34
|
+
Requires-Dist: tenacity; extra == "perf"
|
|
35
|
+
Requires-Dist: uvloop; extra == "perf"
|
|
32
36
|
Provides-Extra: test
|
|
33
37
|
Requires-Dist: asgi-lifespan; extra == "test"
|
|
34
38
|
Requires-Dist: coverage[toml]>=7.5.3; extra == "test"
|
|
@@ -47,10 +51,6 @@ Requires-Dist: python-multipart; extra == "test"
|
|
|
47
51
|
Requires-Dist: requests; extra == "test"
|
|
48
52
|
Requires-Dist: torch>2.0.0; extra == "test"
|
|
49
53
|
Requires-Dist: transformers; extra == "test"
|
|
50
|
-
Provides-Extra: perf
|
|
51
|
-
Requires-Dist: jsonargparse; extra == "perf"
|
|
52
|
-
Requires-Dist: tenacity; extra == "perf"
|
|
53
|
-
Requires-Dist: uvloop; extra == "perf"
|
|
54
54
|
|
|
55
55
|
<div align='center'>
|
|
56
56
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/defaults/metric_callback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/openai_spec_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|