benchmaker 0.1.0__py3-none-any.whl
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.
- benchmaker/__init__.py +152 -0
- benchmaker/bundle.py +193 -0
- benchmaker/cli.py +382 -0
- benchmaker/collect.py +178 -0
- benchmaker/config.py +448 -0
- benchmaker/env.py +87 -0
- benchmaker/load.py +326 -0
- benchmaker/metrics.py +234 -0
- benchmaker/monitors.py +228 -0
- benchmaker/runner.py +275 -0
- benchmaker/trace.py +217 -0
- benchmaker/types.py +98 -0
- benchmaker/workloads/__init__.py +53 -0
- benchmaker/workloads/agent.py +308 -0
- benchmaker/workloads/base.py +79 -0
- benchmaker/workloads/datasets.py +156 -0
- benchmaker/workloads/eval.py +504 -0
- benchmaker/workloads/hf.py +382 -0
- benchmaker/workloads/http.py +77 -0
- benchmaker/workloads/llm.py +258 -0
- benchmaker/workloads/sandbox.py +470 -0
- benchmaker-0.1.0.dist-info/METADATA +214 -0
- benchmaker-0.1.0.dist-info/RECORD +26 -0
- benchmaker-0.1.0.dist-info/WHEEL +5 -0
- benchmaker-0.1.0.dist-info/entry_points.txt +2 -0
- benchmaker-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Workload-type protocol.
|
|
2
|
+
|
|
3
|
+
A `WorkloadType` knows how to talk to a specific *kind* of HTTP service
|
|
4
|
+
(generic HTTP, OpenAI chat completions, …). It does NOT own the inputs —
|
|
5
|
+
inputs come from a `Workload` (see `benchmaker.workloads.datasets`), which
|
|
6
|
+
yields opaque per-request items.
|
|
7
|
+
|
|
8
|
+
Pairing:
|
|
9
|
+
bench = WorkloadType("how to talk") + Workload("what to send") + LoadModel("when")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
from benchmaker.types import Request, Response, Sample, TicketContext, maybe_await
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkloadType(ABC):
|
|
21
|
+
"""A request protocol/shape.
|
|
22
|
+
|
|
23
|
+
Subclasses turn a workload item (any object yielded by a `Workload`) into a
|
|
24
|
+
`Request`, and the resulting `Response` into a `Sample`. Workload-specific
|
|
25
|
+
metrics (TTFT, tokens/sec, …) are attached via `Sample.extra`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name: str = "workload-type"
|
|
29
|
+
streaming: bool = False # if True, runner reads the response chunk-by-chunk
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
async def make_request(self, item: Any) -> Request:
|
|
33
|
+
"""Build a `Request` from a workload item.
|
|
34
|
+
|
|
35
|
+
`item` may be `None` if the workload yields placeholders (e.g. a fixed-
|
|
36
|
+
request benchmark with no per-request data).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
async def make_sample(self, item: Any, request: Request, response: Response,
|
|
40
|
+
start_ts: float) -> Sample:
|
|
41
|
+
"""Build a Sample. Override to attach workload-type-specific metrics."""
|
|
42
|
+
return Sample(
|
|
43
|
+
start_ts=start_ts,
|
|
44
|
+
latency_s=response.elapsed_s,
|
|
45
|
+
status=response.status,
|
|
46
|
+
ok=response.ok,
|
|
47
|
+
request_ok=response.ok,
|
|
48
|
+
bytes_recv=len(response.body) if response.body else 0,
|
|
49
|
+
bytes_sent=_estimate_request_size(request),
|
|
50
|
+
error=response.error,
|
|
51
|
+
workload=self.name,
|
|
52
|
+
meta=dict(request.meta),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def run_ticket(self, ctx: TicketContext) -> Sample:
|
|
56
|
+
"""Default ticket flow: one make_request → one fire → one make_sample.
|
|
57
|
+
|
|
58
|
+
Override to implement multi-step protocols (e.g. create → exec → delete).
|
|
59
|
+
Use `ctx.fire(req)` to issue requests; it applies pre-hooks and uses
|
|
60
|
+
the shared aiohttp session.
|
|
61
|
+
"""
|
|
62
|
+
req = await self.make_request(ctx.item)
|
|
63
|
+
resp = await ctx.fire(req)
|
|
64
|
+
sample = await self.make_sample(ctx.item, req, resp, ctx.start_mono)
|
|
65
|
+
for hook in ctx.post_hooks:
|
|
66
|
+
sample = await maybe_await(hook(req, resp, sample))
|
|
67
|
+
return sample
|
|
68
|
+
|
|
69
|
+
async def aclose(self) -> None:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _estimate_request_size(req: Request) -> int:
|
|
74
|
+
if req.body is not None:
|
|
75
|
+
return len(req.body)
|
|
76
|
+
if req.json is not None:
|
|
77
|
+
import json as _json
|
|
78
|
+
return len(_json.dumps(req.json).encode("utf-8"))
|
|
79
|
+
return 0
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Workloads = datasets / input sources.
|
|
2
|
+
|
|
3
|
+
A `Workload` is the source of per-request data. It yields opaque items that a
|
|
4
|
+
`WorkloadType` knows how to interpret. Built-ins cover the common shapes;
|
|
5
|
+
custom workloads are a one-method subclass.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import itertools
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import random
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, Iterable, Optional, Union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Workload(ABC):
|
|
20
|
+
"""A source of per-request items (a dataset, replay log, prompt list, …)."""
|
|
21
|
+
|
|
22
|
+
name: str = "workload"
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def next_item(self) -> Any:
|
|
26
|
+
"""Return the next item. Raise `StopAsyncIteration` to halt the bench."""
|
|
27
|
+
|
|
28
|
+
async def aclose(self) -> None:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StaticWorkload(Workload):
|
|
33
|
+
"""Cycle through a fixed list of items forever (or `max_items` times).
|
|
34
|
+
|
|
35
|
+
Use this for trivial benchmarks ("send the same JSON repeatedly") or when
|
|
36
|
+
your dataset fits in memory.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, items: Iterable[Any] = (None,), name: str = "static",
|
|
40
|
+
shuffle: bool = False, seed: Optional[int] = None,
|
|
41
|
+
max_items: Optional[int] = None):
|
|
42
|
+
self.name = name
|
|
43
|
+
items = list(items)
|
|
44
|
+
if not items:
|
|
45
|
+
items = [None]
|
|
46
|
+
if shuffle:
|
|
47
|
+
rng = random.Random(seed)
|
|
48
|
+
rng.shuffle(items)
|
|
49
|
+
self._items = items
|
|
50
|
+
self._cycle = itertools.cycle(items)
|
|
51
|
+
self._max = max_items
|
|
52
|
+
self._n = 0
|
|
53
|
+
|
|
54
|
+
async def next_item(self) -> Any:
|
|
55
|
+
if self._max is not None and self._n >= self._max:
|
|
56
|
+
raise StopAsyncIteration
|
|
57
|
+
self._n += 1
|
|
58
|
+
return next(self._cycle)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class JsonlWorkload(Workload):
|
|
62
|
+
"""Stream items from a JSONL file.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
path: path to .jsonl file.
|
|
66
|
+
field: if set, yield only this top-level field of each line (e.g. "prompt").
|
|
67
|
+
loop: if True (default), restart the file when exhausted.
|
|
68
|
+
max_items: cap on total items yielded across loops.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, path: str, field: Optional[str] = None, loop: bool = True,
|
|
72
|
+
max_items: Optional[int] = None, name: Optional[str] = None):
|
|
73
|
+
if not os.path.exists(path):
|
|
74
|
+
raise FileNotFoundError(path)
|
|
75
|
+
self.name = name or f"jsonl:{os.path.basename(path)}"
|
|
76
|
+
self._path = path
|
|
77
|
+
self._field = field
|
|
78
|
+
self._loop = loop
|
|
79
|
+
self._max = max_items
|
|
80
|
+
self._n = 0
|
|
81
|
+
self._fh = open(path, "r")
|
|
82
|
+
self._lock = asyncio.Lock()
|
|
83
|
+
|
|
84
|
+
async def next_item(self) -> Any:
|
|
85
|
+
if self._max is not None and self._n >= self._max:
|
|
86
|
+
raise StopAsyncIteration
|
|
87
|
+
async with self._lock:
|
|
88
|
+
line = self._fh.readline()
|
|
89
|
+
if not line:
|
|
90
|
+
if not self._loop:
|
|
91
|
+
raise StopAsyncIteration
|
|
92
|
+
self._fh.seek(0)
|
|
93
|
+
line = self._fh.readline()
|
|
94
|
+
if not line:
|
|
95
|
+
raise StopAsyncIteration # empty file
|
|
96
|
+
obj = json.loads(line)
|
|
97
|
+
if self._field is not None:
|
|
98
|
+
obj = obj[self._field]
|
|
99
|
+
self._n += 1
|
|
100
|
+
return obj
|
|
101
|
+
|
|
102
|
+
async def aclose(self) -> None:
|
|
103
|
+
try:
|
|
104
|
+
self._fh.close()
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CallableWorkload(Workload):
|
|
110
|
+
"""Wrap a callable (sync or async) that returns the next item on each call.
|
|
111
|
+
|
|
112
|
+
The callable can be a closure that walks a generator, queries an external
|
|
113
|
+
service, etc. Raise `StopAsyncIteration` from the callable to halt.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, fn: Callable[[], Union[Any, Awaitable[Any]]],
|
|
117
|
+
name: str = "callable"):
|
|
118
|
+
self.name = name
|
|
119
|
+
self._fn = fn
|
|
120
|
+
|
|
121
|
+
async def next_item(self) -> Any:
|
|
122
|
+
result = self._fn()
|
|
123
|
+
if hasattr(result, "__await__"):
|
|
124
|
+
return await result # type: ignore[return-value]
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class IterableWorkload(Workload):
|
|
129
|
+
"""Wrap any (async) iterable as a workload. Halts when iterable is exhausted."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, iterable: Union[Iterable[Any], AsyncIterator[Any]],
|
|
132
|
+
name: str = "iterable", loop: bool = False):
|
|
133
|
+
self.name = name
|
|
134
|
+
self._loop = loop
|
|
135
|
+
if hasattr(iterable, "__aiter__"):
|
|
136
|
+
self._aiter = iterable.__aiter__() # type: ignore[union-attr]
|
|
137
|
+
self._items_cache: Optional[list] = None
|
|
138
|
+
self._is_async = True
|
|
139
|
+
else:
|
|
140
|
+
self._items_cache = list(iterable) # type: ignore[arg-type]
|
|
141
|
+
self._iter = iter(self._items_cache)
|
|
142
|
+
self._is_async = False
|
|
143
|
+
|
|
144
|
+
async def next_item(self) -> Any:
|
|
145
|
+
if self._is_async:
|
|
146
|
+
try:
|
|
147
|
+
return await self._aiter.__anext__()
|
|
148
|
+
except StopAsyncIteration:
|
|
149
|
+
raise
|
|
150
|
+
try:
|
|
151
|
+
return next(self._iter)
|
|
152
|
+
except StopIteration:
|
|
153
|
+
if self._loop and self._items_cache:
|
|
154
|
+
self._iter = iter(self._items_cache)
|
|
155
|
+
return next(self._iter)
|
|
156
|
+
raise StopAsyncIteration
|