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 +4 -0
- fairbench/backend.py +211 -0
- fairbench/benchmarks/__init__.py +0 -0
- fairbench/benchmarks/swebench/__init__.py +0 -0
- fairbench/cli.py +377 -0
- fairbench/config.py +106 -0
- fairbench/data/__init__.py +102 -0
- fairbench/engine.py +270 -0
- fairbench/quality.py +165 -0
- fairbench/registry.py +87 -0
- fairbench/reporter.py +152 -0
- fairbench/scripts/__init__.py +0 -0
- fairbench/swebench.py +512 -0
- fairbench/tests/__init__.py +0 -0
- fairbench/tests/benchmarks/__init__.py +0 -0
- fairbench/tests/benchmarks/swebench/__init__.py +0 -0
- fairbench/tests/test_swebench.py +327 -0
- fairbench/types.py +125 -0
- fairbench_ai-0.3.0.dist-info/METADATA +65 -0
- fairbench_ai-0.3.0.dist-info/RECORD +23 -0
- fairbench_ai-0.3.0.dist-info/WHEEL +5 -0
- fairbench_ai-0.3.0.dist-info/entry_points.txt +2 -0
- fairbench_ai-0.3.0.dist-info/top_level.txt +1 -0
fairbench/__init__.py
ADDED
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
|