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.
Files changed (40) hide show
  1. {litserve-0.2.6.dev0/src/litserve.egg-info → litserve-0.2.6.dev2}/PKG-INFO +5 -5
  2. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/__about__.py +1 -1
  3. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/__init__.py +3 -0
  4. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/api.py +4 -65
  5. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/loops.py +370 -18
  6. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/server.py +12 -6
  7. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/base.py +4 -1
  8. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/openai.py +14 -7
  9. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/openai_embedding.py +11 -5
  10. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/utils.py +44 -0
  11. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2/src/litserve.egg-info}/PKG-INFO +5 -5
  12. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/LICENSE +0 -0
  13. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/MANIFEST.in +0 -0
  14. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/README.md +0 -0
  15. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/requirements.txt +0 -0
  16. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/setup.cfg +0 -0
  17. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/setup.py +0 -0
  18. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/__main__.py +0 -0
  19. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/__init__.py +0 -0
  20. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/base.py +0 -0
  21. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/defaults/__init__.py +0 -0
  22. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/callbacks/defaults/metric_callback.py +0 -0
  23. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/connector.py +0 -0
  24. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/docker_builder.py +0 -0
  25. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/loggers.py +0 -0
  26. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/middlewares.py +0 -0
  27. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/python_client.py +0 -0
  28. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/schema/__init__.py +0 -0
  29. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/schema/image.py +0 -0
  30. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/specs/__init__.py +0 -0
  31. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/__init__.py +0 -0
  32. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/openai_embedding_spec_example.py +0 -0
  33. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/openai_spec_example.py +0 -0
  34. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve/test_examples/simple_example.py +0 -0
  35. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/SOURCES.txt +0 -0
  36. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/dependency_links.txt +0 -0
  37. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/entry_points.txt +0 -0
  38. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/not-zip-safe +0 -0
  39. {litserve-0.2.6.dev0 → litserve-0.2.6.dev2}/src/litserve.egg-info/requires.txt +0 -0
  40. {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.dev0
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.dev0"
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
- pass
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 _sanitize(self, max_batch_size: int, spec: Optional[LitSpec]):
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
- return
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
- print(
41
- "uvloop is not installed. Falling back to the default asyncio event loop. "
42
- "Please install uvloop for better performance using `pip install uvloop`."
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 SingleLoop(_BaseLoop):
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(_BaseLoop):
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(_BaseLoop):
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(_BaseLoop):
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 _BaseLoop, inference_worker
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, _BaseLoop]] = "auto",
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._sanitize(max_batch_size, spec=spec)
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
- if status == LitAPIStatus.ERROR:
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 setup(self, server: "LitServer"):
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.dev0
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