Coverage for orchestrant/benchmark/provenance.py: 87%
492 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-18 00:48 +0000
1#!/usr/bin/env python3
2"""Record WHAT produced a measurement, so two reports can be compared later."""
4import glob
5import hashlib
6import json
7import os
8import pathlib
9import platform
10import shutil
11import subprocess # nosec B404 -- runs local tools, argv built here
12import sys
13import sysconfig
14import time
15import urllib.request
16from concurrent.futures import ThreadPoolExecutor
17from datetime import UTC, datetime
19from orchestrant.benchmark.client import redact_argv
21# Re-exported: tools fingerprint determinism.py without fingerprinting this plumbing.
22from orchestrant.benchmark.determinism import ( # noqa: F401
23 PROBE_PROMPT,
24 SPACER_PROMPT,
25 determinism_probe,
26)
27from orchestrant.benchmark.gateway import gateway_block, gateway_notes, lane_behind
30SCHEMA_VERSION = 1
33def _git(*args):
34 try:
35 out = subprocess.run( # nosec B603 B607 -- argv built here, tool from PATH, no shell
36 ["git", *args],
37 capture_output=True,
38 text=True,
39 timeout=10,
40 cwd=os.path.dirname(os.path.abspath(__file__)),
41 )
42 return out.stdout.strip() or None if out.returncode == 0 else None
43 except Exception:
44 return None
47def _server_models(base_url, timeout=5):
48 """Model ids the endpoint advertises, which says nothing about what is LOADED."""
49 try:
50 with urllib.request.urlopen(f"{base_url}/v1/models", timeout=timeout) as r: # nosec B310 -- endpoint URL from config
51 return sorted(m["id"] for m in json.load(r).get("data", []))
52 except Exception:
53 return None
56_GENIEX_VERSION_KEYS = {
57 "geniex cli version": "cli",
58 "qairt runtime version": "qairt",
59 "llamacpp runtime hash": "llama_cpp",
60}
63def parse_geniex_version(text):
64 """`geniex --version` -> {"cli", "qairt", "llama_cpp"}; absent lines stay absent."""
65 out = {}
66 for line in (text or "").splitlines():
67 name, sep, value = line.partition(":")
68 key = _GENIEX_VERSION_KEYS.get(name.strip().lower())
69 if sep and key and value.strip():
70 out[key] = value.strip()
71 return out
74def _installed_geniex():
75 """The GenieX CLI a lane here would run, or None; from WSL2 the Windows side first."""
76 candidates = []
77 local = os.environ.get("LOCALAPPDATA")
78 if local:
79 candidates.append(os.path.join(local, "GenieX CLI", "geniex.exe"))
80 candidates += sorted(
81 glob.glob("/mnt/c/Users/*/AppData/Local/GenieX CLI/geniex.exe")
82 )
83 found = shutil.which("geniex")
84 if found:
85 candidates.append(found)
86 return next((c for c in candidates if os.path.isfile(c)), None)
89def _geniex_version(exe, timeout=30):
90 try:
91 out = subprocess.run( # nosec B603 -- argv built here, no shell
92 [exe, "--version", "--skip-update"],
93 capture_output=True,
94 text=True,
95 timeout=timeout,
96 )
97 except Exception:
98 return None
99 return parse_geniex_version(out.stdout + out.stderr) or None
102def _ollama_version(base_url, timeout=3):
103 try:
104 with urllib.request.urlopen(f"{base_url}/api/version", timeout=timeout) as r: # nosec B310
105 return json.load(r).get("version")
106 except Exception:
107 return None
110def _serves_geniex_root(base_url, timeout=3):
111 """Does the root page look like GenieX's Swagger UI? Too weak to yield `verified`."""
112 try:
113 with urllib.request.urlopen(f"{base_url}/", timeout=timeout) as r: # nosec B310
114 return b"swagger-ui" in r.read(4096)
115 except Exception:
116 return False
119# What served: the model files, the drivers, the host's own view
121# <cache>/<org>/<repo>/geniex.json maps each variant of a model id to its file.
122_GENIEX_CACHE = (".cache", "geniex", "models")
124# Multi-GB GGUFs share their first MiB across quants: hash the head plus sampled windows.
125_HEAD_BYTES = 1 << 20
126_SAMPLES, _SAMPLE_BYTES = 16, 1 << 16
129def _sampled_sha256(f, size):
130 """sha256 over _SAMPLES windows at offsets i * (size - window) // (n - 1)."""
131 h = hashlib.sha256()
132 span = max(size - _SAMPLE_BYTES, 0)
133 for i in range(_SAMPLES):
134 f.seek(span * i // (_SAMPLES - 1))
135 h.update(f.read(_SAMPLE_BYTES))
136 return h.hexdigest()
139def _file_identity(path, manifest_size=None, whole=False):
140 """Name, size, mtime and a sha256 -- whole, or of the first MiB and a sample."""
141 ident = {"name": os.path.basename(path)}
142 try:
143 with open(path, "rb") as f:
144 st = os.fstat(f.fileno())
145 digest = hashlib.sha256(f.read(None if whole else _HEAD_BYTES)).hexdigest()
146 sampled = None if whole else _sampled_sha256(f, st.st_size)
147 except OSError as e:
148 return {**ident, "error": f"{type(e).__name__}: {e}"[:160]}
149 ident["size"] = st.st_size
150 ident["modified_utc"] = datetime.fromtimestamp(st.st_mtime, UTC).isoformat(
151 timespec="seconds"
152 )
153 if whole:
154 ident["sha256"] = digest
155 else:
156 ident.update(
157 head_sha256=digest,
158 head_bytes=_HEAD_BYTES,
159 sampled_sha256=sampled,
160 sampled=f"{_SAMPLES} x {_SAMPLE_BYTES} bytes, evenly to the end",
161 )
162 ident["size_matches_manifest"] = (
163 None if manifest_size is None else st.st_size == manifest_size
164 )
165 return ident
168def _read_json(path):
169 """A JSON object from `path`, or None: a missing or broken file is a gap."""
170 try:
171 with open(path, encoding="utf-8") as f:
172 data = json.load(f)
173 except (OSError, ValueError):
174 return None
175 return data if isinstance(data, dict) else None
178def _geniex_cache_roots(local_only=False):
179 """The model caches to look in: this OS's, and from WSL2 the Windows host's first."""
180 local = str(pathlib.Path.home().joinpath(*_GENIEX_CACHE))
181 if local_only or sys.platform == "win32":
182 return [local]
183 windows = sorted(glob.glob("/mnt/c/Users/*/" + "/".join(_GENIEX_CACHE)))
184 return [*windows, local]
187def _find_manifest(roots, org, repo):
188 """(folder, geniex.json) for org/repo in the first root that has it, or None."""
189 for root in roots:
190 folder = os.path.join(root, org, repo)
191 manifest = _read_json(os.path.join(folder, "geniex.json"))
192 if manifest is not None:
193 return folder, manifest
194 return None
197def _manifest_sizes(manifest):
198 """{file name: the size geniex.json recorded when it was downloaded}."""
199 entries = [
200 *(manifest.get("ModelFile") or {}).values(),
201 manifest.get("MMProjFile") or {},
202 *(manifest.get("ExtraFiles") or []),
203 ]
204 return {
205 e["Name"]: e.get("Size")
206 for e in entries
207 if isinstance(e, dict) and e.get("Name")
208 }
211def _pick_variant(variants, variant):
212 """The ModelFile key a model id names: exactly, ignoring case, or the only one."""
213 if variant is None:
214 return next(iter(variants)) if len(variants) == 1 else None
215 if variant in variants:
216 return variant
217 return next((k for k in variants if k.lower() == variant.lower()), None)
220def _qairt_bundle(folder, sizes):
221 """A QAIRT bundle's genie_config.json (hashed whole: edited in place), ctx-bins and more."""
222 path = os.path.join(folder, "genie_config.json")
223 dialog = (_read_json(path) or {}).get("dialog") or {}
224 engine = dialog.get("engine") or {}
225 bins = ((engine.get("model") or {}).get("binary") or {}).get("ctx-bins") or []
226 ext = (engine.get("backend") or {}).get("extensions")
227 meta = _read_json(os.path.join(folder, "metadata.json")) or {}
228 return {
229 "files": [_file_identity(os.path.join(folder, b), sizes.get(b)) for b in bins],
230 "genie_config": {
231 **_file_identity(path, sizes.get("genie_config.json"), whole=True),
232 "sampler": dialog.get("sampler"),
233 "context_size": (dialog.get("context") or {}).get("size"),
234 },
235 "backend_extensions": _file_identity(
236 os.path.join(folder, ext), sizes.get(ext), whole=True
237 )
238 if ext
239 else None,
240 "bundle_qairt": (meta.get("tool_versions") or {}).get("qairt"),
241 "precision": meta.get("precision"),
242 # No weights named is a gap to say, not an empty list to trust.
243 "error": None if bins else "genie_config.json names no ctx-bins",
244 }
247def _model_files(model, roots):
248 repo_id, _, variant = model.partition(":")
249 org, _, repo = repo_id.partition("/")
250 out = {
251 "model": model,
252 "cache_dir": None,
253 "plugin": None,
254 "variant": None,
255 "files": [],
256 "error": None,
257 }
258 found = _find_manifest(roots, org, repo) if org and repo else None
259 if found is None:
260 return {**out, "error": f"no geniex.json for {repo_id!r} in {roots}"}
261 folder, manifest = found
262 variants = manifest.get("ModelFile") or {}
263 key = _pick_variant(variants, variant or None)
264 out.update(cache_dir=folder, plugin=manifest.get("PluginId"), variant=key)
265 if key is None:
266 why = f"variant {variant!r} is not" if variant else "the id names no variant of"
267 return {**out, "error": f"{why} the manifest's {sorted(variants)}"}
268 sizes = _manifest_sizes(manifest)
269 if out["plugin"] == "qairt":
270 return {**out, **_qairt_bundle(folder, sizes)}
271 names = [variants[key].get("Name"), (manifest.get("MMProjFile") or {}).get("Name")]
272 out["files"] = [
273 _file_identity(os.path.join(folder, n), sizes.get(n)) for n in names if n
274 ]
275 return out
278def geniex_model_files(model, roots=None):
279 """WHICH files a GenieX model id resolves to in the lane's cache; never raises."""
280 if not model:
281 return None
282 try:
283 return _model_files(model, roots or _geniex_cache_roots())
284 except Exception as e: # a manifest in a shape this does not know
285 return {"model": model, "error": f"{type(e).__name__}: {e}"[:200]}
288# Setup classes whose Control\Class keys give driver versions without admin or a subprocess.
289_DRIVER_CLASSES = {
290 "npu": "{f01a9d53-3ff6-48d2-9f97-c8a7004be10c}", # ComputeAccelerator: Hexagon
291 "gpu": "{4d36e968-e325-11ce-bfc1-08002be10318}", # Display: Adreno
292}
293_DRIVER_FIELDS = {
294 "DriverDesc": "name",
295 "DriverVersion": "version",
296 "DriverDate": "date",
297 "ProviderName": "provider",
298 "InfPath": "inf",
299}
302def _driver_row(winreg, parent, sub):
303 """One installed driver's name, version, date, provider and INF, or None."""
304 row = {}
305 try:
306 with winreg.OpenKey(parent, sub) as key:
307 for value, field in _DRIVER_FIELDS.items():
308 try:
309 row[field] = str(winreg.QueryValueEx(key, value)[0])
310 except OSError: # a value this driver's INF does not set
311 row[field] = None
312 except OSError: # a device key this user may not read
313 return None
314 return row
317def _class_drivers(winreg, guid):
318 """Every non-Microsoft driver installed in one device setup class."""
319 base = rf"SYSTEM\CurrentControlSet\Control\Class\{guid}"
320 rows = []
321 with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, base) as cls:
322 for i in range(winreg.QueryInfoKey(cls)[0]):
323 sub = winreg.EnumKey(cls, i)
324 row = _driver_row(winreg, cls, sub) if sub.isdigit() else None
325 # Microsoft's are the remote and basic display adapters, never a lane's.
326 if row and row["version"] and row["provider"] != "Microsoft":
327 rows.append(row)
328 return rows
331def driver_versions():
332 """The NPU and GPU driver versions under a Windows lane, or None with the reason."""
333 out = {"npu": None, "gpu": None, "source": None, "reason": None}
334 if sys.platform != "win32":
335 out["reason"] = (
336 f"not a Windows host ({sys.platform}); from WSL2 the host's drivers "
337 f"are invisible, and a lane-runtime file written on the host "
338 f"({LANE_RUNTIMES_ENV}) carries them"
339 )
340 return out
341 import winreg
343 out["source"] = r"HKLM\SYSTEM\CurrentControlSet\Control\Class"
344 for kind, guid in _DRIVER_CLASSES.items():
345 try:
346 out[kind] = _class_drivers(winreg, guid)
347 except OSError as e:
348 out["reason"] = f"{kind}: {type(e).__name__}: {e}"[:160]
349 return out
352def _geniex_serving(model, roots=None):
353 """What a GenieX runtime adds: the served model's files and the drivers."""
354 return {
355 "model_files": geniex_model_files(model, roots),
356 "drivers": driver_versions(),
357 }
360# WSL2 cannot see the lane process; this names a lane-runtime file written on Windows.
361LANE_RUNTIMES_ENV = "LLM_LANE_RUNTIMES"
363# Lanes restart every round, so an older snapshot keeps its data but loses `verified`.
364SNAPSHOT_MAX_AGE_S = 12 * 3600
367def lane_runtimes(lanes):
368 """{name: runtime_info(url, model)} for {name: (url, model)}; never raises."""
369 out = {}
370 for name, (url, model) in lanes.items():
371 try:
372 out[name] = runtime_info(url, model)
373 except Exception as e:
374 out[name] = {"error": f"{type(e).__name__}: {e}"[:200]}
375 return out
378def write_lane_runtimes(path, lanes):
379 """Snapshot what serves each lane, on the host that can see the lane processes.
381 Run it on Windows once the lanes are up, then export
382 LLM_LANE_RUNTIMES=<the file's /mnt/c path> in WSL2.
383 """
384 runtimes = lane_runtimes(lanes)
385 doc = {
386 "schema_version": SCHEMA_VERSION,
387 "kind": "lane_runtimes",
388 "captured_utc": datetime.now(UTC).isoformat(),
389 "host": platform.node() or None,
390 "lanes": [
391 {"lane": name, "base_url": url, "model": model, "runtime": runtimes[name]}
392 for name, (url, model) in lanes.items()
393 ],
394 }
395 tmp = f"{path}.tmp"
396 with open(tmp, "w", encoding="utf-8") as f:
397 json.dump(doc, f, indent=2)
398 os.replace(tmp, path)
399 return doc
402def _names_a_server(entry):
403 """A snapshot entry the host attributed: {"error": ...} or null is no evidence."""
404 runtime = entry.get("runtime") if isinstance(entry, dict) else None
405 return isinstance(runtime, dict) and bool(runtime.get("server"))
408def _snapshot_entry(entries, base_url):
409 """The entry for `base_url`: exactly, else the only loopback one on its port."""
410 from orchestrant.benchmark.hostload import _port
412 def port(u):
413 try:
414 return _port(u)
415 except ValueError: # a malformed port in a hand-edited file
416 return None
418 url = (base_url or "").rstrip("/")
419 entries = [e for e in entries if _names_a_server(e)]
420 exact = [e for e in entries if (e.get("base_url") or "").rstrip("/") == url]
421 by_port = [e for e in entries if port(url) and port(e.get("base_url")) == port(url)]
422 return exact[0] if exact else (by_port[0] if len(by_port) == 1 else None)
425def _age_s(stamp):
426 try:
427 return round(
428 (datetime.now(UTC) - datetime.fromisoformat(stamp)).total_seconds()
429 )
430 except (TypeError, ValueError):
431 return None
434def _installed_cli_mismatch(runtime):
435 """The installed GenieX's version when it is not the snapshot lane's, else None."""
436 if runtime.get("server") != "geniex":
437 return None
438 installed = _installed_geniex()
439 cli = (_geniex_version(installed) or {}).get("cli") if installed else None
440 return cli if cli and cli != runtime.get("cli") else None
443def load_lane_runtime(base_url, path=None, model=None, max_age_s=SNAPSHOT_MAX_AGE_S):
444 """The lane-runtime file's runtime for `base_url` (`path` or $LLM_LANE_RUNTIMES), or None."""
445 path = path or os.environ.get(LANE_RUNTIMES_ENV)
446 doc = (_read_json(path) if path else None) or {}
447 entry = _snapshot_entry(doc.get("lanes") or [], base_url) or {}
448 runtime = dict(entry.get("runtime") or {})
449 if not runtime:
450 return None
451 captured, age = doc.get("captured_utc"), _age_s(doc.get("captured_utc"))
452 fresh = age is not None and age <= max_age_s
453 mismatch = _installed_cli_mismatch(runtime)
454 runtime["verified"] = bool(runtime.get("verified")) and fresh and not mismatch
455 runtime["source"] = f"lane-runtime file {path}: {runtime.get('source')}"
456 runtime["snapshot"] = {
457 "path": path,
458 "lane": entry.get("lane"),
459 "host": doc.get("host"),
460 "captured_utc": captured,
461 "age_s": age,
462 "stale": not fresh,
463 "installed_cli_mismatch": mismatch,
464 }
465 return _with_model_files(runtime, model)
468def _with_model_files(runtime, model):
469 """`runtime`, its GenieX model files resolved again when another id served."""
470 served = (runtime.get("model_files") or {}).get("model")
471 if model and runtime.get("server") == "geniex" and served != model:
472 runtime["model_files"] = geniex_model_files(model)
473 return runtime
476def _file_keys(model_files):
477 """({name: (size, head, sample)} of the files read, {every name}); a copy moves only mtime."""
478 files = [f for f in model_files.get("files") or [] if isinstance(f, dict)]
479 read = {
480 f.get("name"): (f.get("size"), f.get("head_sha256"), f.get("sampled_sha256"))
481 for f in files
482 if f.get("size") is not None
483 }
484 return read, {f.get("name") for f in files}
487def _changed_files(old, new):
488 """Names listed on one side only, or read on both with another identity."""
489 (was, was_names), (now, now_names) = _file_keys(old), _file_keys(new)
490 moved = {n for n in was.keys() & now.keys() if was[n] != now[n]}
491 return sorted(str(n) for n in (was_names ^ now_names) | moved)
494def _sha_moved(old, new, key):
495 """Both sides hashed the `key` file whole, and the hashes differ."""
496 was = (old.get(key) or {}).get("sha256")
497 now = (new.get(key) or {}).get("sha256")
498 return bool(was and now and was != now)
501def _model_files_of(runtime):
502 return (runtime or {}).get("model_files") or {}
505def model_files_notes(old_rt, new_rt):
506 """Other weights or another bundle config behind the same model id, for compare()."""
507 old, new = _model_files_of(old_rt), _model_files_of(new_rt)
508 if not old.get("files") or old.get("model") != new.get("model"):
509 return []
510 notes = []
511 changed = _changed_files(old, new)
512 if new.get("files") and changed:
513 notes.append(
514 f"MODEL FILES CHANGED behind {old['model']}: {', '.join(changed)} — "
515 f"a re-pull or another file under the same model id"
516 )
517 if _sha_moved(old, new, "genie_config"):
518 notes.append(
519 f"the QAIRT bundle's genie_config.json changed (sampler "
520 f"{old['genie_config'].get('sampler')} → "
521 f"{new['genie_config'].get('sampler')})"
522 )
523 if _sha_moved(old, new, "backend_extensions"):
524 notes.append(
525 "the QAIRT bundle's HTP extensions file changed — it sets the "
526 "NPU's perf profile, which moves speed under the same weights"
527 )
528 return notes
531def main(argv=None):
532 """Write a lane-runtime file: `python -m orchestrant.benchmark.provenance`."""
533 import argparse
535 from orchestrant.benchmark.lanes import resolve_lane
537 ap = argparse.ArgumentParser(
538 description=write_lane_runtimes.__doc__,
539 formatter_class=argparse.RawDescriptionHelpFormatter,
540 )
541 ap.add_argument(
542 "lanes",
543 nargs="+",
544 type=resolve_lane,
545 metavar="BACKEND|name=URL,model=MODEL",
546 help="a backends.json name (geniex-npu) or a full name=URL,model=MODEL spec",
547 )
548 ap.add_argument("--output", required=True, help="the JSON file to write")
549 args = ap.parse_args(argv)
550 doc = write_lane_runtimes(args.output, {n: (u, m) for n, u, m in args.lanes})
551 for e in doc["lanes"]:
552 rt = e["runtime"] or {}
553 print(f" {e['lane']:12s} {runtime_label(rt)} verified={rt.get('verified')}")
554 print(f" Written to {args.output}; in WSL2 export {LANE_RUNTIMES_ENV}=<its path>")
555 return 0
558def _lane_process_runtime(proc, exe, model):
559 """A GenieX lane process seen on this host: the exact binary and its flags."""
560 return {
561 "server": "geniex",
562 **(_geniex_version(exe) or {}),
563 "serve_args": (proc.get("cmdline") or [])[1:],
564 # Different start times are different launches, even with identical flags.
565 "started": proc.get("started"),
566 "verified": True,
567 "source": f"lane process pid {proc['pid']}: {exe} --version",
568 # A process on this OS reads this OS's model cache.
569 **_geniex_serving(model, _geniex_cache_roots(local_only=True)),
570 }
573def runtime_info(base_url, model=None):
574 """WHICH server build produced a measurement, and launched with WHICH flags.
576 Best evidence first: the port's listener (`verified`), a lane-runtime file,
577 then the installed GenieX (`verified: False`).
578 """
579 if not base_url:
580 return None
581 # A gateway alias answers for its lane, as a direct run of that lane would.
582 base_url, model = lane_behind(base_url, model) or (base_url, model)
583 from orchestrant.benchmark.hostload import LaneProcess, _port
585 lane = LaneProcess(base_url)
586 proc = lane.info() if lane.available else None
587 exe = (proc or {}).get("exe") or ""
588 # Either separator: os.path on Linux does not split a Windows path at backslashes.
589 name = os.path.basename(exe.replace("\\", "/")).lower()
590 if proc is not None and name.startswith("geniex"):
591 return _lane_process_runtime(proc, exe, model)
592 snapshot = load_lane_runtime(base_url, model=model)
593 if snapshot:
594 return snapshot
595 version = _ollama_version(base_url)
596 if version:
597 return {
598 "server": "ollama",
599 "version": version,
600 "verified": True,
601 "source": "/api/version",
602 }
603 # Only a lane that ANSWERS may be attributed to the installed binary.
604 installed = None
605 if (
606 _port(base_url)
607 and _server_models(base_url, timeout=2) is not None
608 and _serves_geniex_root(base_url)
609 ):
610 installed = _installed_geniex()
611 if installed:
612 return {
613 "server": "geniex",
614 **(_geniex_version(installed) or {}),
615 "serve_args": None,
616 "verified": False,
617 "source": f"installed binary {installed}; {lane.reason}",
618 **_geniex_serving(model),
619 }
620 return None
623def runtime_label(runtime):
624 """One line naming a runtime, for notes and tables."""
625 if not runtime:
626 return "unknown"
627 if runtime.get("server") == "geniex":
628 return (
629 f"geniex {runtime.get('cli', '?')} (QAIRT {runtime.get('qairt', '?')}, "
630 f"llama.cpp {runtime.get('llama_cpp', '?')})"
631 )
632 return f"{runtime.get('server', '?')} {runtime.get('version', '?')}"
635def _runtime_key(runtime):
636 keys = ("server", "cli", "qairt", "llama_cpp", "version")
637 return tuple((runtime or {}).get(k) for k in keys)
640def _serve_flags(runtime):
641 """The serve arguments minus --host: a port move is not a config change."""
642 args = list((runtime or {}).get("serve_args") or [])
643 if "--host" in args:
644 i = args.index("--host")
645 del args[i : i + 2]
646 return args
649def tool_fingerprint(*paths):
650 """Hash of the benchmark's own source: pass what decides the numbers, not plumbing."""
651 h = hashlib.sha256()
652 here = os.path.dirname(os.path.abspath(__file__))
653 # Sort by basename: callers mix absolute paths and bare names.
654 for name in sorted(paths, key=lambda p: (os.path.basename(p), p)):
655 try:
656 with open(os.path.join(here, name), "rb") as f:
657 # autocrlf makes one commit CRLF on Windows and LF elsewhere.
658 h.update(f.read().replace(b"\r\n", b"\n"))
659 except OSError:
660 return None
661 return h.hexdigest()[:16]
664def busy_lanes(registry_path=None):
665 """Which other endpoints were serving while this ran: results shift with them."""
666 try:
667 from orchestrant.benchmark.openai_api import load_backends
669 backends, _ = load_backends(registry_path)
670 except Exception:
671 return None
672 # Never probe:false (paid) hosts; in parallel, as each refused connect waits out its timeout.
673 urls = {
674 name: entry["base_url"]
675 for name, entry in sorted(backends.items())
676 if entry.get("base_url") and entry.get("probe", True)
677 }
679 def answers(url):
680 try:
681 with urllib.request.urlopen(f"{url.rstrip('/')}/v1/models", timeout=2): # nosec B310 -- endpoint URL from config
682 return True
683 except Exception:
684 return False
686 with ThreadPoolExecutor(max_workers=max(1, len(urls))) as pool:
687 up = dict(zip(urls, pool.map(answers, urls.values()), strict=True))
688 return [name for name in urls if up[name]]
691def energy_proxy():
692 """CPU-seconds consumed, recorded as a PROXY for energy, never as a measurement."""
693 try:
694 import resource
696 me = resource.getrusage(resource.RUSAGE_SELF) # ty: ignore[unresolved-attribute]
697 kids = resource.getrusage(resource.RUSAGE_CHILDREN) # ty: ignore[unresolved-attribute]
698 return {
699 "cpu_seconds_self": round(me.ru_utime + me.ru_stime, 3),
700 "cpu_seconds_children": round(kids.ru_utime + kids.ru_stime, 3),
701 "note": (
702 "CPU time, not joules. This host exposes no power rail "
703 "(no RAPL on aarch64, no battery counter through WSL2), and "
704 "this number says nothing about NPU or GPU draw."
705 ),
706 }
707 except Exception:
708 return None
711# Windows 11's power-mode overlay moves CPU clocks, so a CPU-lane number needs it.
712_POWER_OVERLAYS = {
713 "961cc777-2547-4f9d-8174-7d86181b8a7a": "best power efficiency",
714 "00000000-0000-0000-0000-000000000000": "balanced",
715 "3af9b8d9-7c97-431d-ad78-34a8bfea439f": "better performance",
716 "ded574b5-45a0-4f42-8737-46345c09c238": "best performance",
717}
720def host_power():
721 """AC or battery, and the Windows power mode; None off Windows."""
722 if sys.platform != "win32":
723 return None
724 out = {}
725 try:
726 import ctypes
728 class _Status(ctypes.Structure):
729 _fields_ = [
730 ("ACLineStatus", ctypes.c_byte),
731 ("BatteryFlag", ctypes.c_byte),
732 ("BatteryLifePercent", ctypes.c_byte),
733 ("SystemStatusFlag", ctypes.c_byte),
734 ("BatteryLifeTime", ctypes.c_ulong),
735 ("BatteryFullLifeTime", ctypes.c_ulong),
736 ]
738 status = _Status()
739 if ctypes.windll.kernel32.GetSystemPowerStatus(ctypes.byref(status)):
740 out["on_ac"] = {0: False, 1: True}.get(status.ACLineStatus)
741 out["battery"] = status.BatteryFlag != -128 # 128: no system battery
742 except Exception as e:
743 out["error"] = f"{type(e).__name__}: {e}"[:120]
744 try:
745 import winreg
747 key = r"SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes"
748 with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key) as k:
749 for name in ("ActiveOverlayAcPowerScheme", "ActiveOverlayDcPowerScheme"):
750 try:
751 guid = str(winreg.QueryValueEx(k, name)[0]).lower()
752 except OSError:
753 continue
754 out[name] = _POWER_OVERLAYS.get(guid, guid)
755 except Exception as e:
756 out.setdefault("error", f"{type(e).__name__}: {e}"[:120])
757 return out
760def _what_served(base_url, model):
761 """collect()'s `runtime`, and `gateway`: the lanes behind an alias, and which answered."""
762 if not base_url:
763 return {"runtime": None, "gateway": None}
764 return {
765 "runtime": runtime_info(base_url, model),
766 "gateway": gateway_block(base_url, model, runtime_info),
767 }
770def collect(
771 base_url=None,
772 tool_files=(),
773 extra=None,
774 temperature=None,
775 seed=None,
776 determinism=None,
777 tool_sha256_at_start=None,
778 *,
779 host_load=None,
780 run_started_utc=None,
781 model=None,
782):
783 """Return a provenance block for a report.
785 `tool_sha256_at_start` is the run-start fingerprint: the block is written at
786 the END, so a mid-run source edit must be caught against it.
787 """
788 prov = {
789 "schema_version": SCHEMA_VERSION,
790 "timestamp_utc": datetime.now(UTC).isoformat(),
791 "monotonic_ns": time.monotonic_ns(),
792 "host": platform.node() or None,
793 "os": f"{platform.system()} {platform.release()}"
794 if platform.system()
795 else None,
796 "architecture": platform.machine() or None,
797 "python": platform.python_version(),
798 # An emulated x64 Python on ARM64 still reports ARM64 in platform.machine().
799 "interpreter": sysconfig.get_platform(),
800 "git_sha": _git("rev-parse", "HEAD"),
801 "git_branch": _git("rev-parse", "--abbrev-ref", "HEAD"),
802 # A dirty tree means the recorded SHA does not describe what actually ran.
803 "git_dirty": bool(_git("status", "--porcelain")),
804 # Credentials redacted; compare() skips it, as --output alone always differs.
805 "argv": redact_argv(sys.argv),
806 "base_url": base_url,
807 "server_models": _server_models(base_url) if base_url else None,
808 # Which server build, and the lane's own serve flags when visible.
809 **_what_served(base_url, model),
810 "tool_sha256": tool_fingerprint(*tool_files) if tool_files else None,
811 # A changed file SET moves the hash too; this tells it from a changed grader.
812 "tool_files": sorted(os.path.basename(p) for p in tool_files) or None,
813 "run_started_utc": run_started_utc,
814 # A busy lane slows the one being measured.
815 "live_lanes": busy_lanes(),
816 # Liveness is not load: the start's load is what moves a CPU lane.
817 "host_load": host_load,
818 # Not energy. See energy_proxy() for why this host cannot measure that.
819 "energy_proxy": energy_proxy(),
820 "host_power": host_power(),
821 "temperature": temperature,
822 "seed": seed,
823 "determinism_probe": determinism,
824 }
825 if tool_sha256_at_start:
826 changed = tool_sha256_at_start != prov["tool_sha256"]
827 if changed:
828 prov["tool_sha256_at_start"] = tool_sha256_at_start
829 prov["source_changed_during_run"] = changed
830 if extra:
831 prov.update(extra)
833 required = ("timestamp_utc", "host", "architecture", "git_sha")
834 prov["incomplete"] = [k for k in required if not prov.get(k)]
835 return prov
838def collect_or_error(base_url, tool_files, tool_sha256_at_start=None, **context):
839 """Return collect(), or the error that stopped it, never raising."""
840 try:
841 return collect(
842 base_url, tool_files, tool_sha256_at_start=tool_sha256_at_start, **context
843 )
844 except Exception as e:
845 return {"error": f"{type(e).__name__}: {e}"[:200]}
848def runtime_notes(old_rt, new_rt):
849 """A different server build, or a lane launched with different flags."""
850 if bool(old_rt) != bool(new_rt):
851 return [
852 "runtime recorded on one side only — a runtime change cannot be ruled out"
853 ]
854 if not old_rt:
855 return []
856 notes = []
857 if _runtime_key(old_rt) != _runtime_key(new_rt):
858 notes.append(
859 f"SERVING RUNTIME CHANGED — {runtime_label(old_rt)} → "
860 f"{runtime_label(new_rt)}: a score difference may be the runtime, "
861 f"not the model"
862 )
863 both_seen = (
864 old_rt.get("serve_args") is not None and new_rt.get("serve_args") is not None
865 )
866 if both_seen and _serve_flags(old_rt) != _serve_flags(new_rt):
867 notes.append(
868 f"lane launched with different serve flags: {_serve_flags(old_rt)} vs "
869 f"{_serve_flags(new_rt)}"
870 )
871 return notes + model_files_notes(old_rt, new_rt)
874def _condition_notes(old, new):
875 """A report whose own source moved mid-run, and a changed power condition."""
876 notes: list[str] = [
877 f"{label} run's benchmark source changed WHILE it ran — its first rows "
878 f"came from other code than its tool_sha256 names"
879 for label, prov in (("the old", old), ("the new", new))
880 if prov.get("source_changed_during_run")
881 ]
882 before, after = old.get("host_power"), new.get("host_power")
883 if before and after and before != after:
884 notes.append(
885 f"host power differs: {before} vs {after} — CPU clocks, and so a "
886 f"CPU lane's numbers, move with the Windows power mode"
887 )
888 return notes
891def _source_notes(old, new):
892 """A changed grader, and whether the hash merely covers other files now."""
893 if old.get("tool_sha256") == new.get("tool_sha256"):
894 return []
895 note = (
896 "BENCHMARK SOURCE CHANGED — a score difference may be the grader, not the model"
897 )
898 before, after = old.get("tool_files"), new.get("tool_files")
899 if before is not None and after is not None and before != after:
900 note += f" (the hash covers different files: {before} vs {after})"
901 return [note]
904def _other_cores(prov):
905 """A run's recorded other load, or None: bench_compare reads any JSON given."""
906 load = (prov or {}).get("host_load")
907 cores = load.get("other_cores") if isinstance(load, dict) else None
908 number = isinstance(cores, (int, float)) and not isinstance(cores, bool)
909 return cores if number else None
912def _cores(value):
913 return "unrecorded" if value is None else f"{value:.2f}"
916def _load_notes(old, new):
917 """Two runs started under different background load (hostload's thresholds)."""
918 from orchestrant.benchmark.hostload import BUSY_HOST_CORES, LOAD_DIFF_CORES
920 before, after = _other_cores(old), _other_cores(new)
921 busy = [
922 label
923 for label, cores in (("old", before), ("new", after))
924 if cores is not None and cores > BUSY_HOST_CORES
925 ]
926 if busy:
927 return [
928 f"HOST WAS BUSY when the {' and the '.join(busy)} run started "
929 f"({_cores(before)} vs {_cores(after)} other cores): past one core a "
930 f"CPU lane decodes at about half its quiet rate — its numbers are "
931 f"not evidence about the model"
932 ]
933 if before is None or after is None:
934 return []
935 # Rounded like the readings: 0.80 - 0.50 is 0.30000000000000004 in floats.
936 if round(abs(before - after), 2) > LOAD_DIFF_CORES:
937 return [
938 f"taken under different load — {before:.2f} vs {after:.2f} other "
939 f"cores at the start: a CPU lane's numbers move with it (about "
940 f"-14.5 tok/s per core on GenieX's llama.cpp lane)"
941 ]
942 return []
945def compare(old, new):
946 """Differences between two provenance blocks, worst first."""
947 notes = _source_notes(old, new)
948 notes += _condition_notes(old, new)
949 notes += _load_notes(old, new)
950 notes += runtime_notes(old.get("runtime"), new.get("runtime"))
951 notes += gateway_notes(old, new)
952 # A gateway's /v1/models is its alias list: never a direct lane's (above).
953 front = [bool(p.get("gateway")) for p in (old, new)]
954 if old.get("server_models") != new.get("server_models") and front[0] == front[1]:
955 notes.append(
956 "served models differ between the runs (on GenieX /v1/models is the "
957 "whole local cache, so a pull alone changes it)"
958 )
959 if old.get("architecture") != new.get("architecture") or old.get("host") != new.get(
960 "host"
961 ):
962 notes.append("different host or architecture")
963 if (
964 old.get("live_lanes") is not None
965 and new.get("live_lanes") is not None
966 and old["live_lanes"] != new["live_lanes"]
967 ):
968 notes.append(
969 f"different lanes were live: {old['live_lanes']} vs "
970 f"{new['live_lanes']} — a busy lane slows the one measured"
971 )
972 if new.get("git_dirty") or old.get("git_dirty"):
973 notes.append("at least one run came from a dirty working tree")
974 if old.get("git_sha") != new.get("git_sha"):
975 notes.append(
976 f"repository moved {str(old.get('git_sha'))[:8]} → "
977 f"{str(new.get('git_sha'))[:8]}"
978 )
979 for key in ("temperature", "seed"):
980 if old.get(key) != new.get(key):
981 notes.append(
982 f"{key} differs: {old.get(key)!r} vs {new.get(key)!r} — "
983 f"a flip may be sampling, not the model"
984 )
985 return notes
988def known_deterministic(prov):
989 """True only when a probe in this provenance block saw two draws agree."""
990 probe = (prov or {}).get("determinism_probe") or {}
991 return probe.get("deterministic") is True
994if __name__ == "__main__":
995 raise SystemExit(main())