fairbench-ai 0.3.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.
fairbench/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """FairBench - AI model benchmarking suite with SWE-bench integration."""
2
+
3
+ __version__ = "0.3.0"
4
+ __all__ = ["backend", "cli", "config", "data", "engine", "quality", "reporter"]
fairbench/backend.py ADDED
@@ -0,0 +1,211 @@
1
+ """Speed and memory benchmarking for AI models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import time
8
+
9
+ import psutil
10
+
11
+ from fairbench.types import MemoryResult, SpeedResult
12
+
13
+
14
+ def benchmark_speed(
15
+ model_path: str,
16
+ prompt: str = "The quick brown fox jumps over the lazy dog. " * 10,
17
+ n_ctx: int = 4096,
18
+ n_gpu_layers: int = -1,
19
+ n_threads: int = 0,
20
+ runs: int = 3,
21
+ ) -> SpeedResult:
22
+ """Measure TTFT, TPOT, and throughput using llama-cpp-python bindings.
23
+
24
+ Falls back to llama-bench CLI if bindings unavailable.
25
+ """
26
+ try:
27
+ from llama_cpp import Llama as _Llama # type: ignore
28
+ except ImportError:
29
+ return _benchmark_with_llama_bench(model_path, prompt, runs)
30
+
31
+ return _benchmark_with_llama_cpp(
32
+ model_path, prompt, n_ctx, n_gpu_layers, n_threads, runs, _Llama
33
+ )
34
+
35
+
36
+ def _benchmark_with_llama_cpp(
37
+ model_path: str,
38
+ prompt: str,
39
+ n_ctx: int,
40
+ n_gpu_layers: int,
41
+ n_threads: int,
42
+ runs: int,
43
+ Llama: type, # type: ignore[type-arg]
44
+ ) -> SpeedResult:
45
+ """Benchmark using llama-cpp-python directly."""
46
+ ttfts: list[float] = []
47
+ tpots: list[float] = []
48
+ throughputs: list[float] = []
49
+
50
+ for i in range(runs):
51
+ try:
52
+ llm = Llama( # type: ignore[no-untyped-call]
53
+ model_path=model_path,
54
+ n_ctx=n_ctx,
55
+ n_gpu_layers=n_gpu_layers,
56
+ n_threads=n_threads if n_threads > 0 else None,
57
+ verbose=False,
58
+ )
59
+
60
+ start = time.time()
61
+ response = llm.create_completion(
62
+ prompt=prompt,
63
+ max_tokens=512,
64
+ temperature=0.7,
65
+ stream=True,
66
+ )
67
+
68
+ ttft = time.time() - start
69
+ ttfts.append(ttft * 1000) # ms
70
+
71
+ token_count = 0
72
+ last_time = time.time()
73
+ for chunk in response:
74
+ if time.time() - last_time > 0.01:
75
+ elapsed = time.time() - last_time
76
+ tpots.append((token_count / elapsed) if elapsed > 0 else 0)
77
+ token_count = 0
78
+ last_time = time.time()
79
+ token_count += 1
80
+
81
+ total_time = time.time() - start
82
+ throughput = token_count / total_time if total_time > 0 else 0
83
+ throughputs.append(throughput)
84
+
85
+ llm.close()
86
+ except (OSError, RuntimeError) as e:
87
+ print(f" Run {i + 1} failed: {e}")
88
+
89
+ return SpeedResult(
90
+ ttft_avg=sum(ttfts) / len(ttfts) if ttfts else 0.0,
91
+ tpot_avg=sum(tpots) / len(tpots) if tpots else 0.0,
92
+ throughput_avg=sum(throughputs) / len(throughputs) if throughputs else 0.0,
93
+ runs=len(ttfts),
94
+ )
95
+
96
+
97
+ def _benchmark_with_llama_bench(model_path: str, prompt: str, runs: int) -> SpeedResult:
98
+ """Fallback: use llama-bench CLI tool."""
99
+ try:
100
+ cmd = [
101
+ "llama-bench",
102
+ "-m",
103
+ model_path,
104
+ "-n",
105
+ "512",
106
+ "-ngl",
107
+ "0",
108
+ "-p",
109
+ str(len(prompt.split())),
110
+ "-t",
111
+ "0",
112
+ "-r",
113
+ str(runs),
114
+ ]
115
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=300)
116
+ # Parse output - llama-bench format varies by version
117
+ output = result.stdout
118
+ return SpeedResult(runs=runs, metadata={"raw_output": output})
119
+ except (FileNotFoundError, subprocess.TimeoutExpired) as e:
120
+ return SpeedResult(runs=runs, metadata={"error": str(e)})
121
+
122
+
123
+ def benchmark_memory(
124
+ model_path: str,
125
+ prompt: str = "The quick brown fox jumps over the lazy dog. " * 10,
126
+ n_ctx: int = 4096,
127
+ n_gpu_layers: int = -1,
128
+ n_threads: int = 0,
129
+ ) -> MemoryResult:
130
+ """Measure VRAM and RAM peak usage during inference."""
131
+ try:
132
+ from llama_cpp import Llama as _Llama # type: ignore
133
+ except ImportError:
134
+ return MemoryResult(metadata={"reason": "llama-cpp-python not installed"})
135
+
136
+ return _measure_memory_llama_cpp(
137
+ model_path,
138
+ prompt,
139
+ n_ctx,
140
+ n_gpu_layers,
141
+ n_threads,
142
+ _Llama, # type: ignore[arg-type]
143
+ )
144
+
145
+
146
+ def _measure_memory_llama_cpp(
147
+ model_path: str,
148
+ prompt: str,
149
+ n_ctx: int,
150
+ n_gpu_layers: int,
151
+ n_threads: int,
152
+ Llama: type, # type: ignore[type-arg]
153
+ ) -> MemoryResult:
154
+ """Measure memory using llama-cpp-python with peak tracking."""
155
+ vram_samples: list[float] = []
156
+ ram_samples: list[float] = []
157
+
158
+ try:
159
+ # Get initial memory state
160
+ initial_ram = psutil.Process().memory_info().rss
161
+
162
+ llm = Llama( # type: ignore[no-untyped-call]
163
+ model_path=model_path,
164
+ n_ctx=n_ctx,
165
+ n_gpu_layers=n_gpu_layers,
166
+ n_threads=n_threads if n_threads > 0 else None,
167
+ verbose=False,
168
+ )
169
+
170
+ # Measure VRAM via nvidia-smi if available
171
+ if os.path.exists("/usr/bin/nvidia-smi"):
172
+ result = subprocess.run(
173
+ [
174
+ "nvidia-smi",
175
+ "--query-gpu=memory.used",
176
+ "--format=csv,noheader,nounits",
177
+ ],
178
+ check=False,
179
+ capture_output=True,
180
+ text=True,
181
+ )
182
+ if result.returncode == 0:
183
+ vram_samples = [
184
+ float(line.strip())
185
+ for line in result.stdout.strip().split("\n")
186
+ if line.strip()
187
+ ]
188
+
189
+ # Run inference
190
+ llm.create_completion(
191
+ prompt=prompt,
192
+ max_tokens=256,
193
+ temperature=0.7,
194
+ )
195
+
196
+ # Peak RAM
197
+ current_ram = psutil.Process().memory_info().rss
198
+ ram_peak = (current_ram - initial_ram) / (1024 * 1024) # MB
199
+ ram_samples.append(ram_peak)
200
+
201
+ llm.close()
202
+
203
+ return MemoryResult(
204
+ vram_peak_mb=sum(vram_samples) / len(vram_samples) if vram_samples else 0.0,
205
+ ram_peak_mb=ram_peak,
206
+ vram_avg_mb=sum(vram_samples) / len(vram_samples) if vram_samples else 0.0,
207
+ ram_avg_mb=sum(ram_samples) / len(ram_samples) if ram_samples else 0.0,
208
+ metadata={"model_size_gb": os.path.getsize(model_path) / (1024**3)},
209
+ )
210
+ except (OSError, RuntimeError) as e:
211
+ return MemoryResult(metadata={"error": str(e)})
File without changes
File without changes
fairbench/cli.py ADDED
@@ -0,0 +1,377 @@
1
+ """CLI for FairBench."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from fairbench.config import load_config, save_config
8
+ from fairbench.engine import BenchmarkEngine
9
+ from fairbench.swebench import SWEBENCH_SOURCES
10
+ from fairbench.types import BenchmarkCategory
11
+
12
+
13
+ @click.group()
14
+ @click.version_option(version="0.3.0")
15
+ @click.option("--config", "-c", "config_path", help="Path to config file", default=None)
16
+ @click.pass_context
17
+ def cli(ctx, config_path):
18
+ """FairBench - AI model benchmarking suite with SWE-bench integration.
19
+
20
+ FairBench evaluates AI models across multiple dimensions:
21
+ - Quality: MMLU, HellaSwag, GSM8K, HumanEval, IFEval, TruthfulQA, ARC-Challenge, LiveBench
22
+ - Speed: TTFT, TPOT, throughput
23
+ - Memory: VRAM and RAM peak usage
24
+ - SWE-bench: GitHub issue resolution capability
25
+ """
26
+ ctx.ensure_object(dict)
27
+ ctx.obj["config"] = load_config(config_path)
28
+
29
+
30
+ @cli.command()
31
+ @click.argument("model_path")
32
+ @click.option(
33
+ "--category",
34
+ "-k",
35
+ "category",
36
+ type=click.Choice(["quality", "speed", "memory", "swebench", "all"]),
37
+ default="all",
38
+ help="Benchmark category to run",
39
+ )
40
+ @click.option("--tasks", "-t", "tasks", multiple=True, help="Specific quality benchmarks to run")
41
+ @click.option("--n-ctx", "-n", default=None, type=int, help="Context window size")
42
+ @click.option("--n-gpu-layers", "-g", default=None, type=int, help="GPU layers to offload")
43
+ @click.option("--n-threads", "-j", default=None, type=int, help="CPU threads")
44
+ @click.option("--quant", "-q", default=None, help="Quantization type")
45
+ @click.option("--lite/--no-lite", default=None, help="Use lite mode for quality benchmarks")
46
+ @click.option(
47
+ "--swebench-split",
48
+ "-s",
49
+ "swebench_split",
50
+ type=click.Choice(list(SWEBENCH_SOURCES.keys())),
51
+ default=None,
52
+ help="SWE-bench split to use",
53
+ )
54
+ @click.option(
55
+ "--swebench-limit",
56
+ "-l",
57
+ "swebench_limit",
58
+ type=int,
59
+ default=None,
60
+ help="Limit SWE-bench instances",
61
+ )
62
+ @click.option("--publish/--no-publish", default=None, help="Override publish setting")
63
+ @click.pass_context
64
+ def run(
65
+ ctx,
66
+ model_path,
67
+ category,
68
+ tasks,
69
+ n_ctx,
70
+ n_gpu_layers,
71
+ n_threads,
72
+ quant,
73
+ lite,
74
+ swebench_split,
75
+ swebench_limit,
76
+ publish,
77
+ ):
78
+ """Run benchmarks on a model.
79
+
80
+ MODEL_PATH is the path to the GGUF model file.
81
+
82
+ Examples:
83
+ \b
84
+ fairbench run ~/models/qwen3-35b-q4_k_m.gguf
85
+ fairbench run ~/models/model.gguf --category quality --tasks mmlu hellaswag
86
+ fairbench run ~/models/model.gguf --category swebench --swebench-split lite
87
+ fairbench run ~/models/model.gguf --category all --lite
88
+ """
89
+ config = ctx.obj["config"]
90
+
91
+ # Override publish setting if specified
92
+ if publish is not None:
93
+ config.publish = publish
94
+
95
+ engine = BenchmarkEngine(config)
96
+
97
+ # Map category string to enum
98
+ category_map = {
99
+ "quality": BenchmarkCategory.QUALITY,
100
+ "speed": BenchmarkCategory.SPEED,
101
+ "memory": BenchmarkCategory.MEMORY,
102
+ "swebench": BenchmarkCategory.SWE_BENCH,
103
+ "all": None, # Will run all
104
+ }
105
+
106
+ cat = category_map.get(category)
107
+
108
+ # Handle SWE-bench special cases
109
+ if category == "swebench" or (category == "all" and swebench_split):
110
+ lite_mode = "enabled" if lite else "disabled"
111
+ engine.run_benchmark(
112
+ model_path=model_path,
113
+ category=BenchmarkCategory.SWE_BENCH,
114
+ n_ctx=n_ctx,
115
+ n_gpu_layers=n_gpu_layers,
116
+ n_threads=n_threads,
117
+ quantization=quant,
118
+ lite_mode=lite_mode,
119
+ )
120
+ # Override split if specified
121
+ if swebench_split:
122
+ engine.swebench_client.config = config
123
+ engine.swebench_client.config.swebench_split = swebench_split
124
+ return
125
+
126
+ if category == "all":
127
+ # Run all benchmarks
128
+ engine.run_benchmark(
129
+ model_path=model_path,
130
+ category=BenchmarkCategory.QUALITY,
131
+ tasks=list(tasks) if tasks else None,
132
+ n_ctx=n_ctx,
133
+ n_gpu_layers=n_gpu_layers,
134
+ n_threads=n_threads,
135
+ quantization=quant,
136
+ lite_mode="enabled" if lite else "disabled",
137
+ )
138
+ engine.run_benchmark(
139
+ model_path=model_path,
140
+ category=BenchmarkCategory.SPEED,
141
+ n_ctx=n_ctx,
142
+ n_gpu_layers=n_gpu_layers,
143
+ n_threads=n_threads,
144
+ quantization=quant,
145
+ )
146
+ engine.run_benchmark(
147
+ model_path=model_path,
148
+ category=BenchmarkCategory.MEMORY,
149
+ n_ctx=n_ctx,
150
+ n_gpu_layers=n_gpu_layers,
151
+ n_threads=n_threads,
152
+ quantization=quant,
153
+ )
154
+ return
155
+
156
+ if cat is None:
157
+ click.echo("Invalid category")
158
+ return
159
+
160
+ engine.run_benchmark(
161
+ model_path=model_path,
162
+ category=cat,
163
+ tasks=list(tasks) if tasks else None,
164
+ n_ctx=n_ctx,
165
+ n_gpu_layers=n_gpu_layers,
166
+ n_threads=n_threads,
167
+ quantization=quant,
168
+ lite_mode="enabled" if lite else "disabled",
169
+ )
170
+
171
+
172
+ @cli.command("list-benchmarks")
173
+ @click.pass_context
174
+ def list_benchmarks(ctx):
175
+ """List available quality benchmarks."""
176
+ engine = BenchmarkEngine(ctx.obj["config"])
177
+ benchmarks = engine.list_available_benchmarks()
178
+
179
+ click.echo("\nAvailable Quality Benchmarks:")
180
+ click.echo("-" * 50)
181
+ for name, info in benchmarks.items():
182
+ lite_marker = " [lite]" if info.get("lite") else ""
183
+ click.echo(f" {name:20s} {info['name']:25s}{lite_marker}")
184
+ click.echo(f" {info['description']}")
185
+ click.echo()
186
+
187
+
188
+ @cli.command("list-swebench")
189
+ @click.option(
190
+ "--split",
191
+ "-s",
192
+ "split",
193
+ type=click.Choice(list(SWEBENCH_SOURCES.keys())),
194
+ default="full",
195
+ help="SWE-bench split",
196
+ )
197
+ @click.option("--limit", "-l", type=int, default=10, help="Number of instances to show")
198
+ @click.pass_context
199
+ def list_swebench(ctx, split, limit):
200
+ """List available SWE-bench instances."""
201
+ engine = BenchmarkEngine(ctx.obj["config"])
202
+ instances = engine.get_swebench_instances(split=split, limit=limit)
203
+
204
+ click.echo(f"\nSWE-bench '{split}' instances (showing {len(instances)}):")
205
+ click.echo("-" * 70)
206
+ click.echo(f" {'Instance ID':<35s} {'Repo':<25s} {'Language':<10s}")
207
+ click.echo("-" * 70)
208
+ for inst in instances:
209
+ click.echo(
210
+ f" {inst['instance_id']:<35s} {inst['repo']:<25s} {inst['code_language']:<10s}"
211
+ )
212
+ click.echo()
213
+
214
+
215
+ @cli.command("swebench-eval")
216
+ @click.argument("model_path")
217
+ @click.option(
218
+ "--split",
219
+ "-s",
220
+ "split",
221
+ type=click.Choice(list(SWEBENCH_SOURCES.keys())),
222
+ default="full",
223
+ help="SWE-bench split",
224
+ )
225
+ @click.option("--limit", "-l", type=int, default=5, help="Number of instances to evaluate")
226
+ @click.option("--repo-filter", "-r", "repo_filter", help="Filter by repository")
227
+ @click.pass_context
228
+ def swebench_eval(ctx, model_path, split, limit, repo_filter):
229
+ """Run SWE-bench evaluation on a model.
230
+
231
+ MODEL_PATH is the path to the GGUF model file.
232
+ """
233
+ engine = BenchmarkEngine(ctx.obj["config"])
234
+
235
+ click.echo("\nSWE-bench Evaluation")
236
+ click.echo(f"Model: {model_path}")
237
+ click.echo(f"Split: {split}")
238
+ click.echo(f"Limit: {limit}")
239
+ if repo_filter:
240
+ click.echo(f"Repo filter: {repo_filter}")
241
+
242
+ # Fetch instances
243
+ instances = engine.swebench_client.fetch_instances(
244
+ split=split,
245
+ limit=limit,
246
+ repo_filter=repo_filter,
247
+ )
248
+
249
+ if not instances:
250
+ click.echo("No instances available")
251
+ return
252
+
253
+ click.echo(f"\nFound {len(instances)} instances")
254
+
255
+ # Run evaluation
256
+ results = engine.swebench_client.run_evaluation(
257
+ model_path=model_path,
258
+ instances=instances,
259
+ split=split,
260
+ limit=limit,
261
+ )
262
+
263
+ # Show results
264
+ stats = engine.swebench_client.compute_stats(results)
265
+ click.echo(f"\n{'=' * 50}")
266
+ click.echo("Results:")
267
+ click.echo(f" Total: {stats.total_instances}")
268
+ click.echo(f" Resolved: {stats.resolved}")
269
+ click.echo(f" Failed: {stats.failed}")
270
+ click.echo(f" Errors: {stats.errors}")
271
+ click.echo(f" Score: {stats.avg_test_score:.1f}%")
272
+ click.echo(f"{'=' * 50}")
273
+
274
+
275
+ @cli.command("config")
276
+ @click.option("--show", "-s", "show", is_flag=True, help="Show current config")
277
+ @click.option("--set", "-S", "set_kv", multiple=True, help="Set config key=value")
278
+ @click.pass_context
279
+ def config_cmd(ctx, show, set_kv):
280
+ """Manage FairBench configuration."""
281
+ config = ctx.obj["config"]
282
+
283
+ if show:
284
+ click.echo("\nCurrent Configuration:")
285
+ click.echo("-" * 40)
286
+ for key, value in config.__dict__.items():
287
+ click.echo(f" {key}: {value}")
288
+ click.echo()
289
+ return
290
+
291
+ if set_kv:
292
+ for kv in set_kv:
293
+ if "=" not in kv:
294
+ click.echo(f"Invalid key=value: {kv}")
295
+ continue
296
+ key, value = kv.split("=", 1)
297
+ key = key.strip()
298
+ value = value.strip()
299
+
300
+ # Parse value types
301
+ if value.lower() in ("true", "false"):
302
+ value = value.lower() == "true"
303
+ else:
304
+ try:
305
+ value = int(value)
306
+ except ValueError:
307
+ try:
308
+ value = float(value)
309
+ except ValueError:
310
+ pass # Keep as string
311
+
312
+ if hasattr(config, key):
313
+ setattr(config, key, value)
314
+ click.echo(f"Set {key} = {value}")
315
+ else:
316
+ click.echo(f"Unknown config key: {key}")
317
+
318
+ # Save config
319
+ save_config(config)
320
+ click.echo("\nConfig saved")
321
+
322
+
323
+ @cli.command("leaderboard")
324
+ @click.option("--limit", "-l", type=int, default=20, help="Number of results to show")
325
+ @click.pass_context
326
+ def leaderboard(ctx, limit):
327
+ """Show the leaderboard."""
328
+ engine = BenchmarkEngine(ctx.obj["config"])
329
+ results = engine.reporter.get_leaderboard(limit=limit)
330
+
331
+ if not results:
332
+ click.echo("No results available")
333
+ return
334
+
335
+ click.echo("\nLeaderboard:")
336
+ click.echo("-" * 80)
337
+ click.echo(f" {'Model':<30s} {'Quality':<10s} {'Speed':<10s} {'Memory':<10s} {'SWE':<10s}")
338
+ click.echo("-" * 80)
339
+
340
+ for result in results:
341
+ model = result.get("model_name", "unknown")[:30]
342
+ quality = result.get("quality_scores", {})
343
+ avg_quality = sum(quality.values()) / len(quality) if quality else 0
344
+ speed = result.get("speed", {}).get("throughput_tokens_per_sec", 0)
345
+ memory = result.get("memory", {}).get("vram_peak_mb", 0)
346
+ swebench = result.get("swebench", {})
347
+ sweb_score = (
348
+ (swebench.get("resolved", 0) / swebench.get("total", 1) * 100)
349
+ if swebench.get("total", 0) > 0
350
+ else 0
351
+ )
352
+
353
+ click.echo(
354
+ f" {model:<30s} {avg_quality:<10.2f} {speed:<10.1f} {memory:<10.0f} {sweb_score:<10.1f}"
355
+ )
356
+ click.echo()
357
+
358
+
359
+ @cli.command("results")
360
+ @click.option("--category", "-c", help="Filter by category")
361
+ @click.pass_context
362
+ def results_cmd(ctx, category):
363
+ """List local benchmark results."""
364
+ data = BenchmarkEngine(ctx.obj["config"]).data
365
+ results = data.list_results()
366
+
367
+ if not results:
368
+ click.echo("No local results")
369
+ return
370
+
371
+ click.echo(f"\nLocal results ({len(results)}):")
372
+ for r in results:
373
+ click.echo(f" {r}")
374
+
375
+
376
+ if __name__ == "__main__":
377
+ cli()
fairbench/config.py ADDED
@@ -0,0 +1,106 @@
1
+ """Configuration management for FairBench."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+ from fairbench.types import BenchmarkConfig, LiteMode
10
+
11
+ DEFAULT_CONFIG: dict[str, str | int | float | bool] = {
12
+ "api_url": "https://fairbench.vercel.app/api",
13
+ "default_n_ctx": 4096,
14
+ "default_n_gpu_layers": -1,
15
+ "default_quant": "q4_k_m",
16
+ "default_threads": 0,
17
+ "publish": True,
18
+ "results_dir": "/home/jarvispi/.fairbench/results",
19
+ } # type: ignore[dict-item]
20
+
21
+ CONFIG_PATH = Path.home() / ".fairbench" / "config.yaml"
22
+
23
+
24
+ def load_config(path: str | None = None) -> BenchmarkConfig:
25
+ """Load benchmark configuration from YAML file."""
26
+ config_path = Path(path) if path else CONFIG_PATH
27
+ if not config_path.exists():
28
+ return _default_config()
29
+
30
+ with open(config_path, "r") as f:
31
+ data = yaml.safe_load(f) or {}
32
+
33
+ return BenchmarkConfig(
34
+ api_url=data.get("api_url", DEFAULT_CONFIG["api_url"]),
35
+ default_n_ctx=data.get("default_n_ctx", DEFAULT_CONFIG["default_n_ctx"]),
36
+ default_n_gpu_layers=data.get(
37
+ "default_n_gpu_layers", DEFAULT_CONFIG["default_n_gpu_layers"]
38
+ ),
39
+ default_quant=data.get("default_quant", DEFAULT_CONFIG["default_quant"]),
40
+ default_threads=data.get("default_threads", DEFAULT_CONFIG["default_threads"]),
41
+ publish=data.get("publish", DEFAULT_CONFIG["publish"]),
42
+ results_dir=data.get("results_dir", DEFAULT_CONFIG["results_dir"]),
43
+ lite_mode=_parse_lite_mode(data.get("lite_mode")),
44
+ swebench_instances=data.get("swebench_instances", 0),
45
+ swebench_split=data.get("swebench_split", "full"),
46
+ )
47
+
48
+
49
+ def save_config(config: BenchmarkConfig, path: str | None = None) -> None:
50
+ """Save benchmark configuration to YAML file."""
51
+ config_path = Path(path) if path else CONFIG_PATH
52
+ config_path.parent.mkdir(parents=True, exist_ok=True)
53
+
54
+ data = {
55
+ "api_url": config.api_url,
56
+ "default_n_ctx": config.default_n_ctx,
57
+ "default_n_gpu_layers": config.default_n_gpu_layers,
58
+ "default_quant": config.default_quant,
59
+ "default_threads": config.default_threads,
60
+ "publish": config.publish,
61
+ "results_dir": config.results_dir,
62
+ }
63
+
64
+ if config.lite_mode != LiteMode.AUTO:
65
+ data["lite_mode"] = config.lite_mode.value
66
+
67
+ if config.swebench_instances > 0:
68
+ data["swebench_instances"] = config.swebench_instances
69
+
70
+ if config.swebench_split != "full":
71
+ data["swebench_split"] = config.swebench_split
72
+
73
+ with open(config_path, "w") as f:
74
+ yaml.dump(data, f, default_flow_style=False)
75
+
76
+
77
+ def _default_config() -> BenchmarkConfig:
78
+ """Return default configuration."""
79
+ return BenchmarkConfig(
80
+ api_url=str(DEFAULT_CONFIG["api_url"]),
81
+ default_n_ctx=int(DEFAULT_CONFIG["default_n_ctx"]),
82
+ default_n_gpu_layers=int(DEFAULT_CONFIG["default_n_gpu_layers"]),
83
+ default_quant=str(DEFAULT_CONFIG["default_quant"]),
84
+ default_threads=int(DEFAULT_CONFIG["default_threads"]),
85
+ publish=bool(DEFAULT_CONFIG["publish"]),
86
+ results_dir=str(DEFAULT_CONFIG["results_dir"]),
87
+ )
88
+
89
+
90
+ def _parse_lite_mode(value: str | None) -> LiteMode:
91
+ """Parse lite mode from string value."""
92
+ if value is None:
93
+ return LiteMode.AUTO
94
+ mapping = {
95
+ "disabled": LiteMode.DISABLED,
96
+ "enabled": LiteMode.ENABLED,
97
+ "auto": LiteMode.AUTO,
98
+ }
99
+ return mapping.get(value.lower(), LiteMode.AUTO)
100
+
101
+
102
+ def ensure_results_dir(results_dir: str | None = None) -> Path:
103
+ """Ensure results directory exists and return it."""
104
+ dir_path = Path(results_dir) if results_dir else CONFIG_PATH.parent / "results"
105
+ dir_path.mkdir(parents=True, exist_ok=True)
106
+ return dir_path