165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
"""Quality suite — run dataset tasks against each system, score, report.
|
|
|
|
Each task module exposes the same surface:
|
|
load() -> list[{id, problem, answer, source}]
|
|
make_messages(problem) -> list[dict]
|
|
extract_answer(text) -> str | None
|
|
score(pred, gold) -> bool
|
|
|
|
Concurrency is fixed at 1 per system for quality runs. Mixing concurrent
|
|
requests with quality scoring is fine (deterministic temperature=0) but the
|
|
extra moving parts aren't worth it for the first iteration.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import random
|
|
import statistics
|
|
import time
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from .client import chat_stream
|
|
from .config import BenchConfig, SystemEndpoint
|
|
from .tasks import aime, gsm8k
|
|
|
|
TASKS = {
|
|
"aime2025": (aime, "quality_max_tokens_aime"),
|
|
"gsm8k": (gsm8k, "quality_max_tokens_gsm8k"),
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class QualityRow:
|
|
system: str
|
|
task: str
|
|
n_total: int
|
|
n_correct: int
|
|
n_errors: int
|
|
n_length: int
|
|
accuracy: float
|
|
mean_completion_tokens: float
|
|
mean_ttft_ms: float
|
|
mean_tpot_ms: float
|
|
wall_s: float
|
|
|
|
|
|
@dataclass
|
|
class QualityCase:
|
|
system: str
|
|
task: str
|
|
problem_id: str
|
|
gold: str
|
|
pred: str | None
|
|
correct: bool
|
|
completion_tokens: int
|
|
ttft_ms: float
|
|
tpot_ms: float
|
|
e2e_s: float
|
|
error: str | None
|
|
finish_reason: str | None
|
|
response_preview: str
|
|
response_text: str
|
|
|
|
|
|
async def _run_one_task(
|
|
ep: SystemEndpoint, task_name: str, task_mod, max_tokens: int, cfg: BenchConfig,
|
|
) -> tuple[QualityRow, list[QualityCase]]:
|
|
problems = task_mod.load()
|
|
if cfg.quality_limit is not None:
|
|
if cfg.quality_seed is not None and cfg.quality_limit < len(problems):
|
|
problems = random.Random(cfg.quality_seed).sample(problems, cfg.quality_limit)
|
|
else:
|
|
problems = problems[: cfg.quality_limit]
|
|
print(f"[quality] {ep.name} / {task_name}: {len(problems)} problems "
|
|
f"(max_tokens={max_tokens})")
|
|
|
|
cases: list[QualityCase] = []
|
|
t_wall = time.perf_counter()
|
|
async with httpx.AsyncClient(timeout=cfg.request_timeout_s) as client:
|
|
for prob in problems:
|
|
messages = task_mod.make_messages(prob["problem"])
|
|
extra_body = dict(ep.extra_body or {})
|
|
extra_body.update({
|
|
"top_k": cfg.quality_top_k,
|
|
"top_p": cfg.quality_top_p,
|
|
"presence_penalty": cfg.quality_presence_penalty,
|
|
"repetition_penalty": cfg.quality_repetition_penalty,
|
|
})
|
|
r = await chat_stream(
|
|
client, ep.base_url, ep.model_id, messages,
|
|
max_tokens=max_tokens,
|
|
temperature=cfg.quality_temperature,
|
|
api_key=ep.api_key,
|
|
timeout=cfg.request_timeout_s,
|
|
extra_body=extra_body,
|
|
)
|
|
pred = task_mod.extract_answer(r.text) if r.error is None else None
|
|
correct = task_mod.score(pred, prob["answer"]) if r.error is None else False
|
|
cases.append(QualityCase(
|
|
system=ep.name, task=task_name,
|
|
problem_id=prob["id"], gold=prob["answer"], pred=pred,
|
|
correct=correct, completion_tokens=r.completion_tokens,
|
|
ttft_ms=r.ttft_s * 1000 if r.ttft_s > 0 else -1.0,
|
|
tpot_ms=r.tpot_s * 1000 if r.tpot_s > 0 else -1.0,
|
|
e2e_s=r.e2e_s, error=r.error, finish_reason=r.finish_reason,
|
|
response_preview=(r.text or "")[:240].replace("\n", " "),
|
|
response_text=r.text or "",
|
|
))
|
|
mark = "✓" if correct else ("E" if r.error else "✗")
|
|
print(f" [{mark}] {prob['id']:>4s} gold={prob['answer']:>6s} "
|
|
f"pred={str(pred):>6s} tok={r.completion_tokens:5d} "
|
|
f"{r.e2e_s:6.1f}s")
|
|
wall = time.perf_counter() - t_wall
|
|
|
|
ok = [c for c in cases if c.error is None]
|
|
correct = sum(1 for c in cases if c.correct)
|
|
errors = sum(1 for c in cases if c.error)
|
|
row = QualityRow(
|
|
system=ep.name,
|
|
task=task_name,
|
|
n_total=len(cases),
|
|
n_correct=correct,
|
|
n_errors=errors,
|
|
n_length=sum(1 for c in cases if c.finish_reason == "length"),
|
|
# Transport/runtime failures are incorrect attempts, not exclusions.
|
|
accuracy=correct / max(len(cases), 1),
|
|
mean_completion_tokens=statistics.mean(c.completion_tokens for c in ok) if ok else 0.0,
|
|
mean_ttft_ms=statistics.mean(c.ttft_ms for c in ok if c.ttft_ms > 0) if ok else -1.0,
|
|
mean_tpot_ms=statistics.mean(c.tpot_ms for c in ok if c.tpot_ms > 0) if ok else -1.0,
|
|
wall_s=wall,
|
|
)
|
|
return row, cases
|
|
|
|
|
|
def run_quality(
|
|
endpoints: list[SystemEndpoint], cfg: BenchConfig, tasks: list[str],
|
|
) -> tuple[list[QualityRow], list[QualityCase]]:
|
|
all_rows: list[QualityRow] = []
|
|
all_cases: list[QualityCase] = []
|
|
for ep in endpoints:
|
|
print(f"[quality] === {ep.name} ===")
|
|
for task_name in tasks:
|
|
if task_name not in TASKS:
|
|
raise ValueError(f"unknown task: {task_name}")
|
|
task_mod, max_tok_attr = TASKS[task_name]
|
|
row, cases = asyncio.run(_run_one_task(
|
|
ep, task_name, task_mod, getattr(cfg, max_tok_attr), cfg,
|
|
))
|
|
all_rows.append(row)
|
|
all_cases.extend(cases)
|
|
print(f" -> {row.task}: {row.n_correct}/{row.n_total} = "
|
|
f"{row.accuracy * 100:.1f}% ({row.wall_s:.1f}s wall)")
|
|
return all_rows, all_cases
|
|
|
|
|
|
def rows_to_dicts(rows: list[QualityRow]) -> list[dict[str, Any]]:
|
|
return [asdict(r) for r in rows]
|
|
|
|
|
|
def cases_to_dicts(cases: list[QualityCase]) -> list[dict[str, Any]]:
|
|
return [asdict(c) for c in cases]
|