Skip to content

Commit 361f7c6

Browse files
parfeniukinkDmytro Parfeniuk
andauthored
✨ Executor & Scheduler packages preparation (#11)
* 👔 Executor & Scheduler Improvements * 👔 Executor is refactored to track canceled tasks on the level of the scheduler instead of tracking that on the Task level itself * ✅ Report generation with an executor is tested * 🔥 Some redundancy code is removed * 🧵 `Scheduler.run` uses `Scheduler._event_loop` to manage tasks * 💚 CI process is prepared for all Python versions >= 3.8 * 🔨 `guidellm` build script is added * 👷 CI includes the package publishing workflow ➕ `pytest-cov` is installed. 75% threshold is set. https://pypi.org/project/pytest-cov/ * 🔧 The `pytest.ini_options` `pyproject.toml` file's section is updated with coverage adopts * 👔 TextGenerationError.error -> TextGenerationError.message The error message is user instead of an error instance in dataclass --------- Co-authored-by: Dmytro Parfeniuk <[email protected]>
1 parent 2f06dc3 commit 361f7c6

31 files changed

+945
-333
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 22 deletions
This file was deleted.

.github/workflows/code-quality.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Code Quality Check
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
code-quality-check:
10+
name: Code quality check
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install -e '.[dev]'
26+
27+
- name: Run tests
28+
run: python -m pytest tests/unit
29+
30+
- name: Run linter
31+
run: python -m ruff check src tests
32+
33+
- name: Check formatting
34+
run: |
35+
python -m black --check src tests
36+
python -m isort --check src tests
37+
38+
- name: Check types
39+
run: python -m mypy --check-untyped-defs src tests

.github/workflows/publish.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Publish Python distribution to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- v*
7+
8+
jobs:
9+
build:
10+
name: Build distribution
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.8"
20+
21+
- name: Install pypa/build nad pypa/twine
22+
run: >-
23+
python3 -m pip install build twine --user
24+
25+
- name: Build a binary wheel
26+
run: python3 -m build
27+
28+
publish-to-pypi:
29+
name: Publish Python distribution to PyPI
30+
needs:
31+
- build
32+
if: startsWith(github.ref, 'refs/tags/v')
33+
runs-on: ubuntu-latest
34+
steps:
35+
- name: 🚀📦 Publish to PyPI
36+
env:
37+
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
38+
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
39+
40+
run: python -m twine upload dist/*

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ install.dev:
88
python -m pip install -e .[dev]
99

1010

11+
1112
.PHONY: build
1213
build:
13-
python setup.py sdist bdist_wheel
14+
python -m build
1415

1516
.PHONY: style
1617
style:

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["setuptools", "wheel"]
2+
requires = ["setuptools >= 61.0", "wheel", "build"]
33
build-backend = "setuptools.build_meta"
44

55

@@ -46,8 +46,9 @@ dev = [
4646
"black~=24.4.2",
4747
"isort~=5.13.2",
4848
"mypy~=1.10.1",
49-
"pytest~=8.2.2",
49+
"pytest-cov~=5.0.0",
5050
"pytest-mock~=3.14.0",
51+
"pytest~=8.2.2",
5152
"ruff~=0.5.2",
5253
"tox~=4.16.0",
5354
"types-requests~=2.32.0",
@@ -77,7 +78,7 @@ profile = "black"
7778

7879

7980
[tool.mypy]
80-
files = "src/guidellm"
81+
files = ["src/guidellm", "tests"]
8182
python_version = '3.8'
8283
warn_redundant_casts = true
8384
warn_unused_ignores = true
@@ -101,8 +102,7 @@ lint.select = ["E", "F", "W"]
101102

102103

103104
[tool.pytest.ini_options]
104-
addopts = '-s -vvv --cache-clear'
105-
asyncio_mode = 'auto'
105+
addopts = '-s -vvv --cache-clear --cov-report=term-missing --cov --cov-fail-under=75'
106106
markers = [
107107
"smoke: quick tests to check basic functionality",
108108
"sanity: detailed tests to ensure major functions work correctly",

src/guidellm/backend/openai.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools
22
import os
3-
from typing import Any, Dict, Iterator, List, Optional
3+
from typing import Any, Dict, Generator, List, Optional
44

55
from loguru import logger
66
from openai import OpenAI, Stream
@@ -72,7 +72,7 @@ def __init__(
7272

7373
def make_request(
7474
self, request: TextGenerationRequest
75-
) -> Iterator[GenerativeResponse]:
75+
) -> Generator[GenerativeResponse, None, None]:
7676
"""
7777
Make a request to the OpenAI backend.
7878

src/guidellm/core/result.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,19 @@ class TextGenerationResult(Serializable):
4444
output_token_count: int = Field(
4545
default=0, description="The number of tokens in the output."
4646
)
47-
last_time: float = Field(default=None, description="The last time recorded.")
47+
last_time: Optional[float] = Field(
48+
default=None, description="The last time recorded."
49+
)
4850
first_token_set: bool = Field(
4951
default=False, description="Whether the first token time is set."
5052
)
51-
start_time: float = Field(
53+
start_time: Optional[float] = Field(
5254
default=None, description="The start time of the text generation."
5355
)
54-
end_time: float = Field(
56+
end_time: Optional[float] = Field(
5557
default=None, description="The end time of the text generation."
5658
)
57-
first_token_time: float = Field(
59+
first_token_time: Optional[float] = Field(
5860
default=None, description="The time taken to decode the first token."
5961
)
6062
decode_times: Distribution = Field(
@@ -86,6 +88,9 @@ def output_token(self, token: str):
8688
"""
8789
current_counter = time()
8890

91+
if not self.last_time:
92+
raise ValueError("Last time is not specified to get the output token.")
93+
8994
if not self.first_token_set:
9095
self.first_token_time = current_counter - self.last_time
9196
self.first_token_set = True
@@ -157,13 +162,12 @@ class TextGenerationError(Serializable):
157162
request: TextGenerationRequest = Field(
158163
description="The text generation request that resulted in an error."
159164
)
160-
error: str = Field(
165+
message: str = Field(
161166
description="The error message that occurred during text generation."
162167
)
163168

164-
def __init__(self, request: TextGenerationRequest, error: Exception):
165-
super().__init__(request=request, error=str(error))
166-
logger.error("Text generation error occurred: {}", error)
169+
def model_post_init(self, _: Any):
170+
logger.error(f"Text generation error occurred: {self.message}")
167171

168172

169173
class RequestConcurrencyMeasurement(Serializable):
@@ -185,7 +189,7 @@ class TextGenerationBenchmark(Serializable):
185189
"""
186190

187191
mode: str = Field(description="The generation mode, either 'async' or 'sync'.")
188-
rate: float = Field(
192+
rate: Optional[float] = Field(
189193
default=None, description="The requested rate of requests per second."
190194
)
191195
results: List[TextGenerationResult] = Field(
@@ -238,6 +242,9 @@ def completed_request_rate(self) -> float:
238242
if not self.results:
239243
return 0.0
240244
else:
245+
if not self.results[0].start_time or not self.results[-1].end_time:
246+
raise ValueError("Start time and End time are not defined")
247+
241248
return self.request_count / (
242249
self.results[-1].end_time - self.results[0].start_time
243250
)
@@ -264,7 +271,6 @@ def overloaded(self) -> bool:
264271
# overall this means that a relatively flat or decreasing throughput curve
265272
# over time in addition to a growing processing queue is a sign of overload
266273

267-
# TODO
268274
return False
269275

270276
def request_started(self):
@@ -311,7 +317,7 @@ def request_completed(
311317
)
312318
)
313319
logger.warning(
314-
"Text generation request resulted in error: {}", result.error
320+
f"Text generation request resulted in error: {result.message}"
315321
)
316322
else:
317323
self.results.append(result)

src/guidellm/executor/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from .executor import Executor
22
from .profile_generator import (
33
Profile,
4-
ProfileGenerationModes,
4+
ProfileGenerationMode,
55
ProfileGenerator,
66
SingleProfileGenerator,
77
SweepProfileGenerator,
88
)
99

1010
__all__ = [
1111
"Executor",
12-
"ProfileGenerationModes",
12+
"ProfileGenerationMode",
1313
"Profile",
1414
"ProfileGenerator",
1515
"SingleProfileGenerator",

src/guidellm/executor/executor.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,51 @@
1-
from typing import Any, Dict, Optional, Union
1+
from typing import Any, Dict, Optional
22

33
from guidellm.backend import Backend
4-
from guidellm.core import TextGenerationBenchmarkReport
5-
from guidellm.executor.profile_generator import ProfileGenerationModes, ProfileGenerator
4+
from guidellm.core import TextGenerationBenchmark, TextGenerationBenchmarkReport
65
from guidellm.request import RequestGenerator
7-
from guidellm.scheduler.scheduler import Scheduler
6+
from guidellm.scheduler import Scheduler
7+
8+
from .profile_generator import ProfileGenerationMode, ProfileGenerator
89

910
__all__ = ["Executor"]
1011

1112

1213
class Executor:
14+
"""
15+
The main purpose of the `class Executor` is to dispatch running tasks according
16+
to the Profile Generation mode
17+
"""
18+
1319
def __init__(
1420
self,
15-
request_generator: RequestGenerator,
1621
backend: Backend,
17-
profile_mode: Union[str, ProfileGenerationModes] = "single",
22+
request_generator: RequestGenerator,
23+
profile_mode: ProfileGenerationMode = ProfileGenerationMode.SINGLE,
1824
profile_args: Optional[Dict[str, Any]] = None,
1925
max_requests: Optional[int] = None,
2026
max_duration: Optional[float] = None,
2127
):
2228
self.request_generator = request_generator
2329
self.backend = backend
24-
self.profile = ProfileGenerator.create_generator(
30+
self.profile_generator: ProfileGenerator = ProfileGenerator.create(
2531
profile_mode, **(profile_args or {})
2632
)
27-
self.max_requests = max_requests
28-
self.max_duration = max_duration
33+
self.max_requests: Optional[int] = max_requests
34+
self.max_duration: Optional[float] = max_duration
35+
self._scheduler: Optional[Scheduler] = None
36+
37+
@property
38+
def scheduler(self) -> Scheduler:
39+
if self._scheduler is None:
40+
raise ValueError("The scheduler is not set. Did you run the execution?")
41+
else:
42+
return self._scheduler
2943

3044
def run(self) -> TextGenerationBenchmarkReport:
3145
report = TextGenerationBenchmarkReport()
3246

3347
while True:
34-
profile = self.profile.next_profile(report)
35-
36-
if profile is None:
48+
if not (profile := self.profile_generator.next(report)):
3749
break
3850

3951
scheduler = Scheduler(
@@ -45,7 +57,7 @@ def run(self) -> TextGenerationBenchmarkReport:
4557
max_duration=self.max_duration,
4658
)
4759

48-
benchmark = scheduler.run()
60+
benchmark: TextGenerationBenchmark = scheduler.run()
4961
report.add_benchmark(benchmark)
5062

5163
return report

0 commit comments

Comments
 (0)