File size: 29,160 Bytes
49ad2ef | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 | """Independently check browser traces against the pinned Python CPU ONNX graph.
This consumes historical full-graph bundles, not the shared deployment format.
No output is overwritten. Score-head rejection is an unsupported capability,
never a successful numerical comparison. The current browser path does not
apply calibration; matching it must not be described as calibrated parity.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import re
from collections import Counter
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Any
import numpy as np
from vons.bundle import verify_bundle_manifest
HASH_PATTERN = re.compile(r"[0-9a-f]{64}")
SCHEMA = "vons.browser-parity/v1"
class ParityError(ValueError):
"""A mismatch or missing evidence prevents a successful comparison."""
class InputOverflow(ParityError):
"""The independently tokenized question exceeds the declared budget."""
def file_hash(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def stable_json(value: Any) -> str:
"""Match the harness's JSON.stringify/localeCompare for its fixture domain.
Python's Unicode sorting is not JS localeCompare. Restrict object keys to
lowercase ASCII letters (the complete current fixture domain), and numeric
values to safe integers. Unsupported collation/number formatting fails
closed instead of silently attesting to a differently canonicalized hash.
Unicode *values* are preserved, including the current multilingual states.
"""
if isinstance(value, dict):
if any(not isinstance(key, str) or not re.fullmatch(r"[a-z]+", key) for key in value):
raise ParityError("canonical request keys require lowercase ASCII letters")
return "{" + ",".join(
json.dumps(key) + ":" + stable_json(value[key]) for key in sorted(value)
) + "}"
if isinstance(value, list):
return "[" + ",".join(stable_json(item) for item in value) + "]"
if isinstance(value, bool) or value is None or isinstance(value, str):
return json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
if isinstance(value, (int, float)):
if not math.isfinite(value) or int(value) != value or abs(value) > 2**53 - 1:
raise ParityError("canonical request numbers must be finite safe integers")
return str(int(value))
raise ParityError("request is not JSON-compatible")
def request_hash(request: dict[str, Any]) -> str:
return hashlib.sha256(stable_json(request).encode("utf-8")).hexdigest()
def _required_hash(value: Any, label: str) -> str:
if not isinstance(value, str) or HASH_PATTERN.fullmatch(value) is None:
raise ParityError(f"{label} requires a lowercase SHA-256 digest")
return value
def _object_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ParityError(f"duplicate JSON key: {key}")
result[key] = value
return result
def read_json(path: Path) -> Any:
def reject_constant(value: str) -> None:
raise ParityError(f"non-finite JSON constant: {value}")
return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=_object_pairs,
parse_constant=reject_constant)
def _relative_file(root: Path, relative: Any) -> Path:
if not isinstance(relative, str) or not relative or "\\" in relative or ":" in relative:
raise ParityError("manifest file path must be a relative POSIX path")
value = PurePosixPath(relative)
if value.is_absolute() or PureWindowsPath(relative).drive or ".." in value.parts:
raise ParityError(f"manifest path escapes the bundle: {relative}")
current = root
for part in value.parts:
current = current / part
if current.is_symlink():
raise ParityError(f"manifest path contains a symlink: {relative}")
if not current.is_file() or not current.resolve().is_relative_to(root.resolve()):
raise ParityError(f"manifest file is missing or outside bundle: {relative}")
return current
def validate_artifacts(report: dict[str, Any], manifest_path: Path) -> dict[str, Any]:
if not isinstance(report, dict):
raise ParityError("browser report must be an object")
if report.get("schema") != SCHEMA:
raise ParityError("unsupported browser report schema")
if report.get("requested_provider") not in {"wasm", "webgpu"}:
raise ParityError("requested_provider must identify wasm or webgpu")
expected = _required_hash(report.get("manifest_hash"), "manifest_hash")
if file_hash(manifest_path) != expected:
raise ParityError("browser manifest_hash does not match the supplied manifest bytes")
manifest = read_json(manifest_path)
if manifest.get("schema_version") != "vons.bundle.manifest/v1":
raise ParityError("only historical full-graph bundle manifests are supported")
backend = report.get("backend")
if backend not in {"direct", "diffusion"} or manifest.get("metadata", {}).get("backend") != backend:
raise ParityError("report backend does not match manifest")
files = manifest.get("files")
if not isinstance(files, list) or not files:
raise ParityError("manifest file inventory is empty")
for item in files:
if not isinstance(item, dict):
raise ParityError("manifest file entries must be objects")
path = _relative_file(manifest_path.parent, item.get("path"))
if type(item.get("bytes")) is not int or item["bytes"] < 0:
raise ParityError("manifest bytes must be a nonnegative integer")
if path.stat().st_size != item["bytes"] or file_hash(path) != _required_hash(
item.get("sha256"), f"file {item['path']}"
):
raise ParityError(f"manifest file hash/bytes mismatch: {item['path']}")
# Also inspect actual ONNX external-data references and bundle byte totals.
verification = verify_bundle_manifest(manifest_path)
tokenizers = [item for item in files if item.get("role") == "tokenizer"
and item["path"].endswith("/tokenizer.json")]
tokenizers += [item for item in files if item.get("role") == "tokenizer"
and item["path"] == "tokenizer.json"]
graphs = [item for item in files if item.get("role") == "model_graph"]
if len(tokenizers) != 1 or len(graphs) != 1:
raise ParityError("manifest must select exactly one tokenizer.json and model graph")
if tokenizers[0]["sha256"] != _required_hash(report.get("tokenizer_hash"), "tokenizer_hash"):
raise ParityError("browser tokenizer_hash does not match the pinned tokenizer")
graph = _relative_file(manifest_path.parent, graphs[0]["path"])
if graph.parent != manifest_path.parent:
raise ParityError("historical graph must be beside its manifest for external-data resolution")
return {"manifest": manifest, "verification": verification, "graph": graph,
"tokenizer": _relative_file(manifest_path.parent, tokenizers[0]["path"])}
def validate_request(request: Any) -> list[dict[str, Any]]:
if not isinstance(request, dict):
raise ParityError("request must be an object")
state = request.get("state")
if not isinstance(state, (str, dict)) or isinstance(state, str) and not state.strip():
raise ParityError("state must be a nonempty string or an object")
questions = request.get("questions")
if not isinstance(questions, list) or not 1 <= len(questions) <= 8:
raise ParityError("questions must contain 1..8 items")
ids: set[str] = set()
for question in questions:
if not isinstance(question, dict):
raise ParityError("questions must contain objects")
key = question.get("id")
if not isinstance(key, str) or not key or key in ids:
raise ParityError("question IDs must be unique nonempty strings")
ids.add(key)
kind, options = question.get("type"), question.get("options")
if kind not in {"choice", "boolean", "score"}:
raise ParityError("unsupported question type")
if not isinstance(question.get("prompt"), str) or not question["prompt"].strip():
raise ParityError("question prompt must be a nonempty string")
if not isinstance(options, list) or any(not isinstance(x, str) or not x.strip()
for x in options):
raise ParityError("options must contain nonempty strings")
if len(set(options)) != len(options):
raise ParityError("options must be unique")
if kind == "boolean":
if options not in ([], ["true", "false"]):
raise ParityError("boolean options must be true/false")
elif not 2 <= len(options) <= 32:
raise ParityError("candidate count must be 2..32")
rubric = question.get("rubric", [])
if not isinstance(rubric, list):
raise ParityError("rubric must be a list")
if kind == "score":
if not 2 <= len(rubric) <= 10 or any(not isinstance(x, str) for x in rubric):
raise ParityError("score requires 2..10 string rubric levels")
elif rubric:
raise ParityError("only score may provide rubric levels")
return questions
def candidate_options(question: dict[str, Any]) -> list[str]:
return ["true", "false"] if question["type"] == "boolean" and not question["options"] \
else question["options"]
def build_inputs(tokenizer: Any, state: Any, question: dict[str, Any],
metadata: dict[str, Any], backend: str) -> tuple[dict[str, np.ndarray], dict[str, int]]:
options = candidate_options(question)
sequence = metadata.get("sequence_length", 512)
limit = metadata.get("option_count", 32)
if type(sequence) is not int or not 1 <= sequence <= 512:
raise ParityError("manifest sequence_length must be an integer in 1..512")
if type(limit) is not int or not len(options) <= limit <= 32:
raise ParityError("manifest option_count cannot accommodate the question")
slots = limit if backend == "diffusion" else len(options)
state_text = state if isinstance(state, str) else stable_json(state)
prefix = f"{state_text}\nQuestion: {question['prompt']}\n"
aggregate = tokenizer.encode(prefix + "Candidates:\n" + "\n".join(options))
if len(aggregate.ids) > sequence:
raise InputOverflow(f"aggregate tokens {len(aggregate.ids)} exceed {sequence}")
encoded = [tokenizer.encode(prefix + "Candidate: " + option) for option in options]
if any(len(item.ids) > sequence for item in encoded):
raise InputOverflow(f"candidate tokens exceed {sequence}")
padding = tokenizer.token_to_id("[PAD]")
feed = {"input_ids": np.full((1, slots, sequence), padding or 0, dtype=np.int64),
"attention_mask": np.zeros((1, slots, sequence), dtype=np.int64),
"token_type_ids": np.zeros((1, slots, sequence), dtype=np.int64),
"option_mask": np.zeros((1, slots), dtype=np.bool_)}
for index, item in enumerate(encoded):
size = len(item.ids)
if len(item.attention_mask) != size or len(item.type_ids) != size:
raise ParityError("Python tokenizer returned inconsistent array lengths")
feed["input_ids"][0, index, :size] = item.ids
feed["attention_mask"][0, index, :size] = item.attention_mask
feed["token_type_ids"][0, index, :size] = item.type_ids
feed["option_mask"][0, index] = True
shape = {"live_candidates": len(options), "allocated_candidates": slots,
"live_tokens": sum(len(item.ids) for item in encoded), "sequence_length": sequence}
return feed, shape
def finite_array(value: Any, label: str) -> np.ndarray:
try:
array = np.asarray(value, dtype=np.float64)
except (ValueError, TypeError) as exc:
raise ParityError(f"{label} is not numeric") from exc
if not array.size or not np.isfinite(array).all():
raise ParityError(f"{label} is empty or non-finite")
return array
def assert_close(actual: Any, expected: Any, label: str, atol: float, rtol: float) -> float:
left, right = finite_array(actual, label), finite_array(expected, label)
if left.shape != right.shape or not np.allclose(left, right, atol=atol, rtol=rtol,
equal_nan=False):
raise ParityError(f"{label} differs from Python reference (atol={atol}, rtol={rtol})")
return float(np.max(np.abs(left - right)))
def postprocess(scores: Any, answerability_logit: float, options: list[str],
question_id: str) -> dict[str, Any]:
values = finite_array(scores, "reference scores")
if values.shape != (len(options),):
raise ParityError("reference score count differs from candidates")
logit = float(finite_array([answerability_logit], "reference answerability")[0])
exp = np.exp(values - np.max(values))
probabilities = exp / exp.sum()
answerability = 1 / (1 + math.exp(-logit)) if logit >= 0 else math.exp(logit) / (1 + math.exp(logit))
index = int(np.argmax(probabilities))
confidence = float(probabilities[index] * answerability)
reason = "answerability_below_threshold" if answerability < 0.5 else (
"confidence_below_threshold" if confidence < 0.55 else None
)
return {"question_id": question_id, "choice": None if reason else options[index],
"probabilities": [{"option": option, "probability": float(probabilities[i])}
for i, option in enumerate(options)],
"confidence": confidence, "status": "abstain" if reason else "ok",
"abstain_reason": reason}
def compare_answer(actual: Any, expected: dict[str, Any], atol: float, rtol: float) -> None:
if not isinstance(actual, dict):
raise ParityError("answer is missing")
for field in ("question_id", "choice", "status", "abstain_reason"):
if field not in actual or actual[field] != expected[field]:
raise ParityError(f"answer {field} differs from Python reference")
assert_close([actual.get("confidence")], [expected["confidence"]], "confidence", atol, rtol)
probabilities = actual.get("probabilities")
if not isinstance(probabilities, list) or len(probabilities) != len(expected["probabilities"]):
raise ParityError("answer probability count differs")
for left, right in zip(probabilities, expected["probabilities"], strict=True):
if not isinstance(left, dict) or left.get("option") != right["option"]:
raise ParityError("answer probability options differ")
assert_close([item.get("probability") for item in probabilities],
[item["probability"] for item in expected["probabilities"]],
"answer probabilities", atol, rtol)
class CpuReference:
def __init__(self, graph: Path):
import onnxruntime as ort
options = ort.SessionOptions()
options.intra_op_num_threads = 1
options.inter_op_num_threads = 1
self.session = ort.InferenceSession(str(graph), options, providers=["CPUExecutionProvider"])
self.version = ort.__version__
def run(self, feed: dict[str, np.ndarray], backend: str) -> tuple[np.ndarray, np.ndarray]:
scores, answerability = self.session.run(
["scores" if backend == "diffusion" else "logits", "answerability"], feed
)
return scores, answerability
def verify_trace(trace: dict[str, Any], timing: dict[str, Any], response_answer: Any,
request: dict[str, Any], question: dict[str, Any], tokenizer: Any,
metadata: dict[str, Any], backend: str, noise_values: Any,
reference: Any, atol: float, rtol: float) -> dict[str, Any]:
if trace.get("questionId") != question["id"] or timing.get("questionId") != question["id"]:
raise ParityError("trace/timing question order or ID differs")
feed, shape = build_inputs(tokenizer, request["state"], question, metadata, backend)
for field, expected in shape.items():
if type(timing.get(field)) is not int or timing[field] != expected:
raise ParityError(f"timing shape {field} differs from independent tokenization")
for browser_key, python_key in (("inputIds", "input_ids"), ("attentionMask", "attention_mask"),
("tokenTypeIds", "token_type_ids"), ("optionMask", "option_mask")):
actual = trace.get(browser_key)
if not isinstance(actual, list) or any(type(item) is not int for item in actual):
raise ParityError(f"{browser_key} must be an integer array")
if actual != feed[python_key].astype(np.int64).ravel().tolist():
raise ParityError(f"{browser_key} differs from independent tokenizer/padding")
noise_hash = None
if backend == "diffusion":
expected_shape = (shape["allocated_candidates"],)
noise = finite_array(trace.get("initialNoise"), "initialNoise")
frozen = finite_array(noise_values, "report noise_values")
if noise.shape != expected_shape or frozen.shape != expected_shape:
raise ParityError("initial noise dimensions differ from candidate slots")
# Both sources must represent the same exact stored Float32 values.
if not np.array_equal(noise, noise.astype(np.float32).astype(np.float64)):
raise ParityError("trace initialNoise is not an exact Float32 array")
if not np.array_equal(noise, frozen):
raise ParityError("trace noise differs from report's frozen noise_values")
noise32 = noise.astype("<f4")
noise_hash = hashlib.sha256(noise32.tobytes()).hexdigest()
feed["initial_noise"] = noise32.reshape(1, -1)
elif trace.get("initialNoise") is not None or noise_values is not None:
raise ParityError("Direct must not carry diffusion noise")
raw_scores, raw_answerability = reference.run(feed, backend)
all_scores = finite_array(raw_scores, "CPU raw scores")
raw_answerability = finite_array(raw_answerability, "CPU answerability")
if all_scores.shape != (1, shape["allocated_candidates"]) or raw_answerability.size != 1:
raise ParityError("CPU output tensor shape differs from bundle contract")
scores = all_scores[0, :shape["live_candidates"]]
logit = float(raw_answerability.ravel()[0])
score_delta = assert_close(trace.get("rawScores"), scores, "raw scores", atol, rtol)
logit_delta = assert_close([trace.get("answerabilityLogit")], [logit], "answerability", atol, rtol)
answer = postprocess(scores, logit, candidate_options(question), question["id"])
probabilities = [item["probability"] for item in answer["probabilities"]]
probability_delta = assert_close(trace.get("probabilities"), probabilities,
"trace probabilities", atol, rtol)
compare_answer(trace.get("answer"), answer, atol, rtol)
compare_answer(response_answer, answer, atol, rtol)
return {"question_id": question["id"], "status": "passed", "shape": shape,
"noise_float32_le_sha256": noise_hash,
"raw_scores": scores.tolist(), "answerability_logit": logit,
"answer": answer, "max_absolute_difference": {
"raw_scores": score_delta, "answerability_logit": logit_delta,
"probabilities": probability_delta}, "calibration": None}
def verify_records(report: dict[str, Any], manifest: dict[str, Any], tokenizer: Any,
reference: Any, atol: float = 1e-5, rtol: float = 1e-5) -> dict[str, Any]:
if any(not math.isfinite(x) or x < 0 for x in (atol, rtol)):
raise ParityError("tolerances must be finite and nonnegative")
records = report.get("records")
if not isinstance(records, list) or not records or report.get("fixture_count") != len(records):
raise ParityError("report requires nonempty records and matching fixture_count")
backend, metadata = report["backend"], manifest["metadata"]
results: list[dict[str, Any]] = []
seen: set[str] = set()
for row in records:
if not isinstance(row, dict):
raise ParityError("fixture records must be objects")
result: dict[str, Any] = {"fixture_id": row.get("fixture_id"), "questions": []}
results.append(result)
try:
key = row.get("fixture_id")
if not isinstance(key, str) or not key or key in seen:
raise ParityError("fixture IDs must be unique nonempty strings")
seen.add(key)
request = row.get("request")
fingerprint = _required_hash(row.get("input_hash"), "input_hash")
if request_hash(request) != fingerprint:
raise ParityError("input_hash differs from canonical request")
result["input_hash"] = fingerprint
questions = validate_request(request)
status = row.get("status")
traces, timings = row.get("traces"), row.get("timings")
if not isinstance(traces, list) or not isinstance(timings, list):
raise ParityError("trace and timing arrays are required, including for errors")
if status == "expected_error":
if traces or timings or not isinstance(row.get("error"), str) or not row["error"]:
raise ParityError("expected errors require an error and empty trace/timing arrays")
if any(question["type"] == "score" for question in questions):
if "score questions are not supported" not in row["error"]:
raise ParityError("score rejection does not match the recorded SDK limitation")
result.update(status="unverified", reason="score_head_unsupported",
numerical_comparison=False, contract_error_verified=False)
continue
overflow = None
for question in questions:
try:
build_inputs(tokenizer, request["state"], question, metadata, backend)
except InputOverflow as exc:
overflow = str(exc)
break
if overflow is None or not any(text in row["error"].lower()
for text in ("token", "budget", "overflow")):
raise ParityError("expected overflow could not be independently confirmed")
result.update(status="contract_error_verified", reason=overflow,
numerical_comparison=False, contract_error_verified=True)
continue
if status != "ok":
raise ParityError(f"browser request did not succeed: {row.get('error', status)}")
if any(question["type"] == "score" for question in questions):
raise ParityError("current full-graph reference does not implement score questions")
response = row.get("response")
if not isinstance(response, dict) or response.get("backend") != backend:
raise ParityError("response backend is missing or mismatched")
expected_model_id = metadata.get("model_id", f"vons-{backend}-onnx-web")
if response.get("model_id") != expected_model_id:
raise ParityError("response model_id differs from the supplied manifest")
answers = response.get("answers")
if not isinstance(answers, list) or not len(traces) == len(timings) == len(answers) == len(questions):
raise ParityError("every question requires exactly one trace, timing and answer")
for question, trace, timing, answer in zip(questions, traces, timings, answers, strict=True):
item = {"question_id": question["id"], "status": "failed"}
result["questions"].append(item)
try:
item.update(verify_trace(trace, timing, answer, request, question, tokenizer,
metadata, backend, report.get("noise_values"), reference,
atol, rtol))
except (ValueError, TypeError, KeyError, AttributeError) as exc:
item["reason"] = str(exc)
failed = any(item["status"] != "passed" for item in result["questions"])
result.update(status="failed" if failed else "passed", numerical_comparison=True,
contract_error_verified=False)
except (ValueError, TypeError, KeyError, AttributeError) as exc:
result.update(status="failed", reason=str(exc), numerical_comparison=False,
contract_error_verified=False)
counts = Counter(item["status"] for item in results)
numeric = Counter(item["status"] for row in results for item in row["questions"])
return {"records": results,
"counts": {key: counts[key] for key in
("passed", "failed", "unverified", "contract_error_verified")},
"numerical_questions": {key: numeric[key] for key in ("passed", "failed")},
"all_numerical_comparisons_passed": numeric["passed"] > 0 and not numeric["failed"]
and not counts["failed"],
"complete_contract_verified": not counts["failed"] and not counts["unverified"],
"calibration": None,
"postprocess": {"temperature": 1.0, "abstain_threshold": 0.55,
"answerability_threshold": 0.5},
"limitations": [
"The browser path uses uncalibrated probabilities and default thresholds.",
"Unsupported score questions do not count as numerical or contract passes.",
"Provider execution partition, lifecycle, latency and offline operation are not verified.",
"Canonical request hashes support lowercase ASCII object keys and safe integer numbers; Unicode string values are supported.",
"This reference checks the supplied full graph, not shared-bundle SDK execution or task accuracy.",
]}
def verify_report(report_path: Path, manifest_path: Path, output_path: Path,
atol: float = 1e-5, rtol: float = 1e-5) -> dict[str, Any]:
if output_path.exists():
raise FileExistsError(f"refusing to overwrite evidence: {output_path}")
from tokenizers import Tokenizer
report = read_json(report_path)
artifacts = validate_artifacts(report, manifest_path)
tokenizer = Tokenizer.from_file(str(artifacts["tokenizer"]))
tokenizer.no_truncation()
tokenizer.no_padding()
reference = CpuReference(artifacts["graph"])
results = verify_records(report, artifacts["manifest"], tokenizer, reference, atol, rtol)
output = {"schema": "vons.browser-parity-reference/v1", "backend": report["backend"],
"requested_provider": report.get("requested_provider"),
"provider_execution_verified": False,
"manifest_sha256": file_hash(manifest_path),
"report_sha256": file_hash(report_path), "tokenizer_sha256": report["tokenizer_hash"],
"verifier_sha256": file_hash(Path(__file__)), "manifest_verification": artifacts["verification"],
"reference_runtime": {"provider": "CPUExecutionProvider", "ort_version": reference.version,
"intra_op_threads": 1, "inter_op_threads": 1},
"tolerances": {"atol": atol, "rtol": rtol, "equal_nan": False}, **results}
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("x", encoding="utf-8") as handle:
json.dump(output, handle, indent=2, ensure_ascii=False, allow_nan=False)
handle.write("\n")
return output
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--report", required=True, type=Path)
parser.add_argument("--manifest", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--atol", type=float, default=1e-5)
parser.add_argument("--rtol", type=float, default=1e-5)
args = parser.parse_args()
try:
result = verify_report(args.report, args.manifest, args.output, args.atol, args.rtol)
except (ValueError, OSError, TypeError, KeyError) as exc:
parser.exit(2, f"parity verification rejected: {exc}\n")
print(json.dumps({"output": str(args.output), "counts": result["counts"],
"numerical_questions": result["numerical_questions"]}, allow_nan=False))
return 0 if result["all_numerical_comparisons_passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())
|