Files
wehub-resource-sync c3749daf48
Tests / test-linux (3.13) (push) Failing after 0s
Tests / test-linux (3.11) (push) Failing after 1s
Tests / lint (push) Failing after 0s
Tests / test-linux (3.9) (push) Failing after 1s
Docker / build (push) Failing after 1s
Docker / build-gpu (push) Failing after 2s
Tests / test-windows (push) Has been cancelled
Tests / test-macos (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:03 +08:00

373 lines
12 KiB
Python

"""Metrics: timing extraction, VRAM measurement, embedding similarity, percentiles."""
from __future__ import annotations
import json
import re
import statistics
import subprocess
import threading
import time
from dataclasses import dataclass, field
from typing import Optional
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
# ── Thinking-token stripping ─────────────────────────────────────────────
#
# Some models (Qwen 3 reasoning variants, DeepSeek-R1, QwQ) emit
# <think>...</think> blocks before their actual answer, or use Ollama's
# `thinking` / `reasoning` response fields. The benchmark scores against
# the final answer only, so we strip these.
#
# This is a local copy of the same logic that lives in
# `mempalace.local_model.strip_thinking_tokens`. We duplicate it here so
# the harness can land on develop independently of the model-router PR.
# When that PR merges, replace this with: from mempalace.local_model
# import strip_thinking_tokens
_THINK_BLOCK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
def strip_thinking_tokens(text: str, raw: dict) -> str:
"""Remove <think>...</think> blocks from text. Falls back to the raw
response's `thinking` or `reasoning` fields if the main text is empty
after stripping."""
cleaned = _THINK_BLOCK_RE.sub("", text or "").strip()
if cleaned:
return cleaned
msg = raw.get("message") if isinstance(raw, dict) else None
if isinstance(msg, dict):
for field_name in ("content", "thinking", "reasoning"):
v = msg.get(field_name)
if isinstance(v, str) and v.strip():
return _THINK_BLOCK_RE.sub("", v).strip()
for field_name in ("thinking", "reasoning"):
v = raw.get(field_name) if isinstance(raw, dict) else None
if isinstance(v, str) and v.strip():
return _THINK_BLOCK_RE.sub("", v).strip()
return cleaned
# ── Timing extraction from Ollama response ───────────────────────────────
@dataclass
class TimingSample:
"""Per-request timing extracted from Ollama's response payload."""
e2e_ms: float
ttft_ms: float
tps: float
eval_tokens: int
prompt_tokens: int
def extract_timing(raw: dict, wall_clock_seconds: float) -> TimingSample:
"""Pull timing numbers out of an Ollama /api/chat response.
Ollama returns durations in nanoseconds. Wall-clock is the Python-side
measurement of the full request; we use it as the e2e source of truth
since `total_duration` excludes some HTTP overhead.
TTFT is approximated as load + prompt_eval, which is the time before
the first output token would have streamed. Close enough for
benchmarking without implementing streaming.
"""
e2e_ms = wall_clock_seconds * 1000.0
load_ns = raw.get("load_duration", 0) or 0
prompt_eval_ns = raw.get("prompt_eval_duration", 0) or 0
eval_ns = raw.get("eval_duration", 0) or 0
eval_count = raw.get("eval_count", 0) or 0
prompt_count = raw.get("prompt_eval_count", 0) or 0
ttft_ms = (load_ns + prompt_eval_ns) / 1e6
tps = (eval_count / (eval_ns / 1e9)) if eval_ns > 0 else 0.0
return TimingSample(
e2e_ms=e2e_ms,
ttft_ms=ttft_ms,
tps=tps,
eval_tokens=eval_count,
prompt_tokens=prompt_count,
)
# ── Percentile aggregation ───────────────────────────────────────────────
@dataclass
class TimingAggregate:
e2e_p50_ms: float
e2e_p95_ms: float
ttft_p50_ms: float
ttft_p95_ms: float
tps_p50: float
tps_p95: float
n: int
def aggregate_timings(samples: list[TimingSample]) -> TimingAggregate:
if not samples:
return TimingAggregate(0, 0, 0, 0, 0, 0, 0)
e2e = sorted(s.e2e_ms for s in samples)
ttft = sorted(s.ttft_ms for s in samples)
tps = sorted(s.tps for s in samples)
return TimingAggregate(
e2e_p50_ms=_p(e2e, 50),
e2e_p95_ms=_p(e2e, 95),
ttft_p50_ms=_p(ttft, 50),
ttft_p95_ms=_p(ttft, 95),
tps_p50=_p(tps, 50),
tps_p95=_p(tps, 95),
n=len(samples),
)
def _p(sorted_values: list[float], pct: int) -> float:
if not sorted_values:
return 0.0
if len(sorted_values) == 1:
return sorted_values[0]
# Nearest-rank: matches what most production tools (statsd, Datadog) report
rank = max(0, min(len(sorted_values) - 1, int(round((pct / 100.0) * (len(sorted_values) - 1)))))
return sorted_values[rank]
# ── VRAM measurement ─────────────────────────────────────────────────────
def vram_resident_mb(
model_tag: str, endpoint: str = "http://localhost:11434", timeout: int = 5
) -> Optional[int]:
"""Resident VRAM for a loaded model, read from Ollama's /api/ps endpoint.
Returns None if the model isn't loaded or the endpoint is unreachable.
Uses the HTTP API rather than `ollama ps` since the CLI's --format flag
is missing in older Ollama versions (verified against 0.23.2).
"""
try:
with urlopen(f"{endpoint}/api/ps", timeout=timeout) as resp:
data = json.loads(resp.read())
except (URLError, HTTPError, OSError, json.JSONDecodeError):
return None
for model in data.get("models", []) or []:
if not isinstance(model, dict):
continue
name = model.get("name") or model.get("model") or ""
if name == model_tag:
size = model.get("size_vram") or model.get("size") or 0
return int(size / (1024 * 1024)) if size else None
return None
class VRAMPoller:
"""Poll nvidia-smi for peak GPU memory during a code block.
Usage:
poller = VRAMPoller()
poller.start()
# ... run inference ...
peak_mb = poller.stop()
Returns None if nvidia-smi is unavailable. Multi-GPU: tracks GPU 0 only.
Single-GPU is the assumed deployment for this benchmark.
"""
def __init__(self, interval_s: float = 0.5):
# 500ms balances peak-capture coverage against jitter from nvidia-smi
# subprocess spawns. Inference VRAM is mostly steady-state during a
# request, so missing the absolute peak by a few-percent margin is
# acceptable and worth the reduced overhead on the run itself.
self.interval_s = interval_s
self._stop = threading.Event()
self._thread: Optional[threading.Thread] = None
self._peak_mb: int = 0
self._available = self._check_nvidia_smi()
@staticmethod
def _check_nvidia_smi() -> bool:
try:
result = subprocess.run(
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
capture_output=True,
text=True,
timeout=5,
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def _poll(self):
while not self._stop.is_set():
try:
result = subprocess.run(
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
capture_output=True,
text=True,
timeout=2,
)
if result.returncode == 0:
first_line = result.stdout.strip().splitlines()[0]
mb = int(first_line.strip())
if mb > self._peak_mb:
self._peak_mb = mb
except (subprocess.TimeoutExpired, ValueError, IndexError):
pass
self._stop.wait(self.interval_s)
def start(self):
if not self._available:
return
self._stop.clear()
self._peak_mb = 0
self._thread = threading.Thread(target=self._poll, daemon=True)
self._thread.start()
def stop(self) -> Optional[int]:
if not self._available or self._thread is None:
return None
self._stop.set()
self._thread.join(timeout=2)
return self._peak_mb if self._peak_mb > 0 else None
# ── Embedding similarity (open-set scoring) ──────────────────────────────
def embed_text(
text: str,
model: str = "nomic-embed-text",
endpoint: str = "http://localhost:11434",
timeout: int = 30,
) -> Optional[list[float]]:
"""Get an embedding from Ollama's /api/embeddings. Returns None on failure."""
body = json.dumps({"model": model, "prompt": text}).encode("utf-8")
req = Request(
f"{endpoint}/api/embeddings",
data=body,
headers={"Content-Type": "application/json"},
)
try:
with urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read())
except (URLError, HTTPError, OSError, json.JSONDecodeError):
return None
return data.get("embedding")
def cosine_similarity(a: list[float], b: list[float]) -> float:
if not a or not b or len(a) != len(b):
return 0.0
dot = sum(x * y for x, y in zip(a, b))
norm_a = sum(x * x for x in a) ** 0.5
norm_b = sum(x * x for x in b) ** 0.5
if norm_a == 0 or norm_b == 0:
return 0.0
return dot / (norm_a * norm_b)
def label_similarity(
predicted: str,
target: str,
embed_model: str = "nomic-embed-text",
endpoint: str = "http://localhost:11434",
) -> float:
"""Cosine similarity between embeddings of two label strings.
Used for open-set scoring where the model invents a label and we need
to compare against a hand-chosen preferred label without requiring
exact-match wording.
"""
if predicted.strip().lower() == target.strip().lower():
return 1.0
emb_p = embed_text(predicted, model=embed_model, endpoint=endpoint)
emb_t = embed_text(target, model=embed_model, endpoint=endpoint)
if emb_p is None or emb_t is None:
return 0.0
return cosine_similarity(emb_p, emb_t)
# ── Hardware reporting ──────────────────────────────────────────────────
@dataclass
class HostInfo:
cpu: str = ""
cores: int = 0
ram_gb: float = 0.0
gpu: str = ""
gpu_vram_mb: int = 0
ollama_version: str = ""
os: str = ""
hostname: str = ""
def gather_host_info() -> HostInfo:
"""Best-effort introspection of the test machine. Failures degrade
silently rather than aborting the benchmark."""
info = HostInfo()
info.hostname = (_run(["hostname"]) or "").strip()
cpuinfo = _read_file("/proc/cpuinfo") or ""
for line in cpuinfo.splitlines():
if line.startswith("model name"):
info.cpu = line.split(":", 1)[1].strip()
break
nproc = _run(["nproc"])
if nproc:
try:
info.cores = int(nproc.strip())
except ValueError:
pass
meminfo = _read_file("/proc/meminfo") or ""
for line in meminfo.splitlines():
if line.startswith("MemTotal:"):
kb = int(line.split()[1])
info.ram_gb = round(kb / (1024 * 1024), 1)
break
nvidia = _run([
"nvidia-smi",
"--query-gpu=name,memory.total",
"--format=csv,noheader,nounits",
])
if nvidia:
first = nvidia.strip().splitlines()[0]
if "," in first:
name, vram = first.split(",", 1)
info.gpu = name.strip()
try:
info.gpu_vram_mb = int(vram.strip())
except ValueError:
pass
info.ollama_version = (_run(["ollama", "--version"]) or "").strip()
info.os = (_read_file("/etc/os-release") or "").splitlines()[0] if _read_file("/etc/os-release") else ""
if info.os.startswith("PRETTY_NAME="):
info.os = info.os.split("=", 1)[1].strip('"')
return info
def _run(cmd: list[str]) -> Optional[str]:
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
if result.returncode == 0:
return result.stdout
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
return None
def _read_file(path: str) -> Optional[str]:
try:
with open(path, "r") as f:
return f.read()
except OSError:
return None