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.
@@ -0,0 +1,382 @@
1
+ """HuggingFace `datasets` workload.
2
+
3
+ Yields per-request dicts shaped for downstream LLM workload-types — usually
4
+ `{"prompt": ..., "reference": ..., <extra_fields>: ...}`. Pair with
5
+ `OpenAIChatWorkloadType` (which promotes `prompt` → `messages`) and
6
+ `EvalWorkloadType` + `correctness_hook` (which reads `reference`).
7
+
8
+ The `datasets` library is an optional dependency. Install with:
9
+
10
+ pip install -e .[hf]
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import random
16
+ import re
17
+ from typing import Any, Callable, Iterable, Mapping, Optional, Union
18
+
19
+ from benchmaker.workloads.datasets import Workload
20
+
21
+
22
+ # --------------------------------------------------------------------------- #
23
+ # Reference transforms (postprocessing on the raw reference field)
24
+ # --------------------------------------------------------------------------- #
25
+
26
+
27
+ def _gsm8k_answer(text: str) -> str:
28
+ """GSM8K answers end in `#### <number>`; return that final number."""
29
+ if "####" in text:
30
+ return text.split("####")[-1].strip().replace(",", "")
31
+ m = re.search(r"-?\d+(?:\.\d+)?", text)
32
+ return m.group(0) if m else text.strip()
33
+
34
+
35
+ def _strip(text: str) -> str:
36
+ return text.strip()
37
+
38
+
39
+ def _first_line(text: str) -> str:
40
+ return text.split("\n", 1)[0].strip()
41
+
42
+
43
+ def _lower_strip(text: str) -> str:
44
+ return text.strip().lower()
45
+
46
+
47
+ _TRANSFORMS: dict[str, Callable[[str], str]] = {
48
+ "identity": lambda s: s,
49
+ "strip": _strip,
50
+ "lower_strip": _lower_strip,
51
+ "first_line": _first_line,
52
+ "gsm8k": _gsm8k_answer,
53
+ "gsm8k_answer": _gsm8k_answer,
54
+ }
55
+
56
+
57
+ def get_transform(name: str) -> Callable[[str], str]:
58
+ try:
59
+ return _TRANSFORMS[name]
60
+ except KeyError:
61
+ raise ValueError(
62
+ f"unknown reference_transform {name!r}; known: {sorted(_TRANSFORMS)}"
63
+ )
64
+
65
+
66
+ # --------------------------------------------------------------------------- #
67
+ # Dataset presets (path + subset + field names)
68
+ # --------------------------------------------------------------------------- #
69
+
70
+
71
+ PresetSpec = dict[str, Any]
72
+
73
+ _PRESETS: dict[str, PresetSpec] = {
74
+ "gsm8k": {
75
+ "path": "gsm8k",
76
+ "name": "main",
77
+ "split": "test",
78
+ "prompt_field": "question",
79
+ "reference_field": "answer",
80
+ "reference_transform": "gsm8k_answer",
81
+ },
82
+ "mmlu": {
83
+ # MMLU has many configs; default to "all". Override with `name=...`.
84
+ "path": "cais/mmlu",
85
+ "name": "all",
86
+ "split": "test",
87
+ # MMLU rows: question, choices (list[str]), answer (int idx). We build
88
+ # a multiple-choice prompt via a template and ship the letter answer.
89
+ "prompt_template": (
90
+ "{question}\n"
91
+ "A. {choice_a}\nB. {choice_b}\nC. {choice_c}\nD. {choice_d}\n"
92
+ "Answer with a single letter (A, B, C, or D)."
93
+ ),
94
+ "prompt_template_fields": {
95
+ "question": "question",
96
+ "choice_a": ("choices", 0),
97
+ "choice_b": ("choices", 1),
98
+ "choice_c": ("choices", 2),
99
+ "choice_d": ("choices", 3),
100
+ },
101
+ "reference_field": "answer",
102
+ "reference_transform": "mmlu_letter",
103
+ },
104
+ "humaneval": {
105
+ "path": "openai_humaneval",
106
+ "split": "test",
107
+ "prompt_field": "prompt",
108
+ "reference_field": "canonical_solution",
109
+ "extra_fields": ("task_id", "test", "entry_point"),
110
+ },
111
+ }
112
+
113
+ # Late-bound transforms that need argument bindings.
114
+ _TRANSFORMS["mmlu_letter"] = lambda v: ("ABCD"[int(v)] if isinstance(v, int)
115
+ or (isinstance(v, str) and v.isdigit())
116
+ else str(v).strip().upper()[:1])
117
+
118
+
119
+ def list_presets() -> list[str]:
120
+ return sorted(_PRESETS)
121
+
122
+
123
+ def get_preset(name: str) -> PresetSpec:
124
+ try:
125
+ return dict(_PRESETS[name])
126
+ except KeyError:
127
+ raise ValueError(
128
+ f"unknown HF preset {name!r}; known: {list_presets()}"
129
+ )
130
+
131
+
132
+ # --------------------------------------------------------------------------- #
133
+ # The workload
134
+ # --------------------------------------------------------------------------- #
135
+
136
+
137
+ class HFDatasetWorkload(Workload):
138
+ """Load a HuggingFace dataset and yield `{prompt, reference, ...}` items.
139
+
140
+ Args:
141
+ path: HuggingFace dataset id (`"gsm8k"`, `"cais/mmlu"`, ...) — required
142
+ unless `preset` is given.
143
+ name: dataset config / subset (`"main"` for gsm8k, etc.).
144
+ split: split name (default `"test"`).
145
+ preset: one of `list_presets()`. Fills in `path/name/split/field`
146
+ defaults; explicit kwargs override.
147
+
148
+ prompt_field: source row field whose value becomes `"prompt"`.
149
+ Mutually exclusive with `prompt_template`.
150
+ prompt_template: format string referencing fields from
151
+ `prompt_template_fields`. Use this when the prompt is composed of
152
+ several columns (MMLU, BBH, etc.).
153
+ prompt_template_fields: maps `{template_var: row_field}` (or
154
+ `(row_field, index)` to index into a list field). Required when
155
+ `prompt_template` is set.
156
+
157
+ reference_field: source row field whose value becomes `"reference"`.
158
+ Omit to skip producing a reference (e.g. for non-eval generation).
159
+ reference_transform: callable `(value) -> str` or a registered
160
+ transform name (`"gsm8k_answer"`, `"strip"`, ...).
161
+
162
+ extra_fields: row fields to carry through unchanged on each item
163
+ (handy for `EvalWorkloadType(extra_meta_keys=...)`).
164
+
165
+ max_items: stop after this many items.
166
+ shuffle: permute once at construction (non-streaming only).
167
+ seed: RNG seed for shuffle.
168
+ streaming: use `datasets`'s streaming mode (no shuffle, no len()).
169
+ loop: cycle the dataset when exhausted (non-streaming only).
170
+ cache_dir / token / trust_remote_code: forwarded to `load_dataset`.
171
+ """
172
+
173
+ name: str = "hf"
174
+
175
+ def __init__(
176
+ self,
177
+ path: Optional[str] = None,
178
+ *,
179
+ name: Optional[str] = None,
180
+ split: str = "test",
181
+ preset: Optional[str] = None,
182
+
183
+ prompt_field: Optional[str] = "prompt",
184
+ prompt_template: Optional[str] = None,
185
+ prompt_template_fields: Optional[Mapping[str, Any]] = None,
186
+
187
+ reference_field: Optional[str] = "reference",
188
+ reference_transform: Optional[Union[str, Callable[[Any], str]]] = None,
189
+
190
+ extra_fields: Iterable[str] = (),
191
+
192
+ max_items: Optional[int] = None,
193
+ shuffle: bool = False,
194
+ seed: Optional[int] = None,
195
+ streaming: bool = False,
196
+ loop: bool = False,
197
+
198
+ cache_dir: Optional[str] = None,
199
+ token: Optional[str] = None,
200
+ trust_remote_code: bool = False,
201
+
202
+ workload_name: Optional[str] = None,
203
+ ):
204
+ spec: PresetSpec = {}
205
+ if preset:
206
+ spec = get_preset(preset)
207
+
208
+ path = path or spec.get("path")
209
+ if not path:
210
+ raise ValueError("HFDatasetWorkload requires `path` (or a `preset`)")
211
+
212
+ name = name if name is not None else spec.get("name")
213
+ # `split` is keyword-only with a default ("test"). Only fall back to
214
+ # the preset's value if the caller didn't explicitly override.
215
+ if split == "test" and "split" in spec:
216
+ split = spec["split"]
217
+
218
+ if prompt_template is None and spec.get("prompt_template"):
219
+ prompt_template = spec["prompt_template"]
220
+ prompt_template_fields = (prompt_template_fields
221
+ or spec.get("prompt_template_fields"))
222
+ prompt_field = None
223
+ elif prompt_field == "prompt" and "prompt_field" in spec:
224
+ prompt_field = spec["prompt_field"]
225
+
226
+ if reference_field == "reference" and "reference_field" in spec:
227
+ reference_field = spec["reference_field"]
228
+
229
+ if reference_transform is None and "reference_transform" in spec:
230
+ reference_transform = spec["reference_transform"]
231
+
232
+ if isinstance(reference_transform, str):
233
+ reference_transform = get_transform(reference_transform)
234
+
235
+ if prompt_template and not prompt_template_fields:
236
+ raise ValueError(
237
+ "prompt_template requires prompt_template_fields to map "
238
+ "template vars onto row fields"
239
+ )
240
+ if prompt_template and prompt_field:
241
+ # Caller supplied both; template wins (prompt_field auto-cleared
242
+ # above when preset injected the template).
243
+ prompt_field = None
244
+ if not prompt_template and not prompt_field:
245
+ raise ValueError("Either prompt_field or prompt_template must be set")
246
+
247
+ extra_fields = tuple(extra_fields) or tuple(spec.get("extra_fields") or ())
248
+
249
+ self._path = path
250
+ self._config_name = name
251
+ self._split = split
252
+ self._prompt_field = prompt_field
253
+ self._prompt_template = prompt_template
254
+ self._prompt_template_fields = dict(prompt_template_fields or {})
255
+ self._reference_field = reference_field
256
+ self._reference_transform = reference_transform
257
+ self._extra_fields = tuple(extra_fields)
258
+ self._max = max_items
259
+ self._loop = loop
260
+ self._streaming = streaming
261
+ self._cache_dir = cache_dir
262
+ self._token = token
263
+ self._trust_remote_code = trust_remote_code
264
+
265
+ if workload_name:
266
+ self.name = workload_name
267
+ else:
268
+ tag = path
269
+ if name:
270
+ tag = f"{tag}:{name}"
271
+ self.name = f"hf:{tag}/{split}"
272
+
273
+ self._iter: Any = None
274
+ self._n = 0
275
+ self._shuffle = shuffle
276
+ self._seed = seed
277
+
278
+ # ---- HF wiring ---- #
279
+
280
+ def _load(self) -> Any:
281
+ try:
282
+ from datasets import load_dataset
283
+ except ImportError as e:
284
+ raise ImportError(
285
+ "HFDatasetWorkload requires the `datasets` package. "
286
+ "Install with `pip install datasets` or `pip install -e .[hf]`."
287
+ ) from e
288
+
289
+ kwargs: dict[str, Any] = {"split": self._split}
290
+ if self._config_name is not None:
291
+ kwargs["name"] = self._config_name
292
+ if self._cache_dir is not None:
293
+ kwargs["cache_dir"] = self._cache_dir
294
+ if self._token is not None:
295
+ kwargs["token"] = self._token
296
+ if self._trust_remote_code:
297
+ kwargs["trust_remote_code"] = True
298
+ if self._streaming:
299
+ kwargs["streaming"] = True
300
+ return load_dataset(self._path, **kwargs)
301
+
302
+ def _ensure_iter(self) -> None:
303
+ if self._iter is not None:
304
+ return
305
+ ds = self._load()
306
+ if self._streaming:
307
+ self._iter = iter(ds)
308
+ self._source = None
309
+ else:
310
+ indices = list(range(len(ds)))
311
+ if self._shuffle:
312
+ random.Random(self._seed).shuffle(indices)
313
+ self._source = ds
314
+ self._indices = indices
315
+ self._cursor = 0
316
+ self._iter = self._yield_indexed()
317
+
318
+ def _yield_indexed(self):
319
+ while True:
320
+ if self._cursor >= len(self._indices):
321
+ if not self._loop:
322
+ return
323
+ self._cursor = 0
324
+ idx = self._indices[self._cursor]
325
+ self._cursor += 1
326
+ yield self._source[idx]
327
+
328
+ # ---- item shaping ---- #
329
+
330
+ def _shape(self, row: Mapping[str, Any]) -> dict[str, Any]:
331
+ item: dict[str, Any] = {}
332
+ if self._prompt_template:
333
+ ctx: dict[str, Any] = {}
334
+ for var, src in self._prompt_template_fields.items():
335
+ ctx[var] = _lookup(row, src)
336
+ item["prompt"] = self._prompt_template.format(**ctx)
337
+ elif self._prompt_field:
338
+ item["prompt"] = row[self._prompt_field]
339
+
340
+ if self._reference_field and self._reference_field in row:
341
+ value = row[self._reference_field]
342
+ if self._reference_transform is not None:
343
+ value = self._reference_transform(value)
344
+ item["reference"] = value
345
+
346
+ for f in self._extra_fields:
347
+ if f in row:
348
+ item[f] = row[f]
349
+ return item
350
+
351
+ # ---- Workload protocol ---- #
352
+
353
+ async def next_item(self) -> Any:
354
+ if self._max is not None and self._n >= self._max:
355
+ raise StopAsyncIteration
356
+ self._ensure_iter()
357
+ try:
358
+ row = next(self._iter)
359
+ except StopIteration:
360
+ raise StopAsyncIteration
361
+ self._n += 1
362
+ return self._shape(row)
363
+
364
+ async def aclose(self) -> None:
365
+ self._iter = None
366
+ return None
367
+
368
+
369
+ def _lookup(row: Mapping[str, Any], src: Any) -> Any:
370
+ """Resolve a source spec against a row.
371
+
372
+ `src` is either a field name (`"question"`) or a tuple
373
+ `(field_name, index, ...)` for indexing into list/dict fields.
374
+ """
375
+ if isinstance(src, str):
376
+ return row[src]
377
+ if isinstance(src, tuple) and src:
378
+ value = row[src[0]]
379
+ for key in src[1:]:
380
+ value = value[key]
381
+ return value
382
+ raise TypeError(f"bad template field spec {src!r}")
@@ -0,0 +1,77 @@
1
+ """Generic HTTP workload-type.
2
+
3
+ Interprets workload items flexibly:
4
+
5
+ * `None` → fire the base request unchanged.
6
+ * `bytes` → use as raw body.
7
+ * `str` → use as raw body (utf-8 encoded).
8
+ * `dict` → use as JSON body, UNLESS keys overlap with Request fields
9
+ (`body`/`json`/`params`/`headers`/`url`/`method`/`meta`),
10
+ in which case those fields are applied directly. This
11
+ lets a dataset fully customize the request when needed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Optional
17
+
18
+ from benchmaker.types import Request
19
+ from benchmaker.workloads.base import WorkloadType
20
+
21
+
22
+ _REQUEST_KEYS = {"body", "json", "params", "headers", "url", "method", "meta", "timeout_s"}
23
+
24
+
25
+ class HttpWorkloadType(WorkloadType):
26
+ name = "http"
27
+
28
+ def __init__(
29
+ self,
30
+ url: str = "",
31
+ method: str = "GET",
32
+ headers: Optional[dict[str, str]] = None,
33
+ params: Optional[dict[str, Any]] = None,
34
+ timeout_s: Optional[float] = None,
35
+ name: str = "http",
36
+ ):
37
+ self.name = name
38
+ self._url = url
39
+ self._method = method
40
+ self._headers = headers or {}
41
+ self._params = params or {}
42
+ self._timeout_s = timeout_s
43
+
44
+ async def make_request(self, item: Any) -> Request:
45
+ req = Request(
46
+ method=self._method,
47
+ url=self._url,
48
+ headers=dict(self._headers),
49
+ params=dict(self._params),
50
+ timeout_s=self._timeout_s,
51
+ )
52
+ if item is None:
53
+ return req
54
+ if isinstance(item, (bytes, bytearray)):
55
+ req.body = bytes(item)
56
+ return req
57
+ if isinstance(item, str):
58
+ req.body = item.encode("utf-8")
59
+ return req
60
+ if isinstance(item, dict):
61
+ if _REQUEST_KEYS & item.keys():
62
+ # Treat as a Request override.
63
+ for k, v in item.items():
64
+ if k == "headers":
65
+ req.headers.update(v)
66
+ elif k == "params":
67
+ req.params.update(v)
68
+ elif k == "meta":
69
+ req.meta.update(v)
70
+ elif k in _REQUEST_KEYS:
71
+ setattr(req, k, v)
72
+ # ignore unknown keys silently
73
+ return req
74
+ # Otherwise treat the whole dict as the JSON body.
75
+ req.json = item
76
+ return req
77
+ raise TypeError(f"HttpWorkloadType cannot interpret item of type {type(item).__name__}")