Files
retroSoC/scripts/apu_p9_evidence.py
T

343 lines
13 KiB
Python

#!/usr/bin/env python3
"""Initialize or assemble fail-closed APU-P9 evidence reports."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from typing import Any
from apu_kws_coeff import validate_layout_manifest
REPORTS = (
"coefficient-layout.json",
"coefficient-lifecycle.json",
"memo-equivalence.json",
"numerical-latency.json",
"memory-synthesis-ab.json",
"physical.json",
)
ROOT = Path(__file__).resolve().parents[1]
REQUIRED_CHECKS = {
REPORTS[1]: (
"lp_admission",
"unsupported_discovery",
"cache_acl",
"dma_backpressure",
"payload_corruption",
"readback_corruption",
"partial_retry",
"lock",
"ownership",
"hard_reset",
"soft_reset",
"resource_reset",
"quiesce_load",
"abort_every_state",
),
REPORTS[2]: (
"valid_v1_v2",
"mutated_v1_v2",
"duplicate_keys",
"collisions",
"full_wrap",
"full_table",
"deeper_call_bypass",
"key_boundaries",
"bitmap_boundaries",
"interrupted_clear_insert",
"verdict_equivalence",
"first_error_equivalence",
"visit_count_equivalence",
),
REPORTS[3]: (
"all_coefficients",
"scalar_table_boundaries",
"variable_backpressure",
"no_double_commit",
"no_stale_response",
"mfcc_bit_exact",
"layers_bit_exact",
"full_1000_windows",
"complete_window_before_successor",
"concurrent_60s_zero_xrun",
),
REPORTS[4]: (
"configuration_matches",
"revision_qualified",
"candidate_synthesis_completed",
"target_inferred_bits_zero",
"apu_reduction_at_least_90_percent",
"candidate_apu_bits_at_most_65536",
"candidate_macro_inventory_76",
"pass_timing_reported",
"post_memory_inventory_reported",
"peak_rss_measured_both",
"candidate_peak_rss_lower",
"synthesis_duration_measured_both",
"candidate_duration_lower",
),
REPORTS[5]: (
"revision_qualified",
"macro_inventory_76",
"logic_macro_area_separate",
"wns_nonnegative",
"tns_zero",
"hold_reported",
"no_unconstrained_paths",
"macro_views_complete",
"netlist_models_complete",
),
}
BASELINE_REVISION = "0b73994772068e5fac00fb1a98f87f88ed121cdf"
def _read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"cannot read JSON evidence {path}: {error}") from error
if not isinstance(value, dict):
raise ValueError(f"JSON evidence is not an object: {path}")
return value
def _write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
def _full_revision(value: object) -> bool:
return isinstance(value, str) and len(value) == 40 and all(
character in "0123456789abcdef" for character in value.lower()
)
def _require_artifacts(report: dict[str, Any], name: str) -> None:
artifacts = report.get("artifacts")
if not isinstance(artifacts, dict) or not artifacts:
raise ValueError(f"{name} passed report lacks named artifacts")
missing = [
key
for key, value in artifacts.items()
if not isinstance(value, str) or not Path(value).is_file()
]
if missing:
raise ValueError(f"{name} passed report has missing artifacts: {missing}")
def _require_passed_tests(report: dict[str, Any], name: str) -> None:
tests = report.get("tests")
if not isinstance(tests, list) or not tests or any(
not isinstance(test, dict) or test.get("status") != "passed" for test in tests
):
raise ValueError(f"{name} passed report lacks passed test records")
def _validate_memory_ab(report: dict[str, Any]) -> None:
baseline = report.get("baseline")
candidate = report.get("candidate")
tool = report.get("tool_identity")
if not isinstance(baseline, dict) or not isinstance(candidate, dict) or not isinstance(tool, dict):
raise ValueError("memory-synthesis-ab.json lacks baseline/candidate/tool identity")
if (
baseline.get("revision") != BASELINE_REVISION
or baseline.get("dirty") is not False
or candidate.get("revision") != report.get("revision")
or candidate.get("dirty") is not False
):
raise ValueError("memory-synthesis-ab.json revisions are not frozen and clean")
memory = candidate.get("memory")
macros = candidate.get("macros")
pass_timing = candidate.get("pass_timing")
post_memory = candidate.get("post_memory")
if (
not isinstance(memory, dict)
or memory.get("target_bits") != 0
or not isinstance(memory.get("apu_bits"), int)
or memory["apu_bits"] > 65536
or not isinstance(macros, dict)
or macros.get("apu_wrappers") != 76
or not isinstance(pass_timing, dict)
or any(pass_timing.get(name) is None for name in ("memory", "opt_dff", "abc", "total_ns"))
or not isinstance(post_memory, dict)
or not isinstance(post_memory.get("cell_types"), dict)
):
raise ValueError("memory-synthesis-ab.json lacks the frozen memory/pass inventory")
yosys_path = Path(str(tool.get("yosys_path", "")))
yosys_sha = tool.get("yosys_sha256")
if not yosys_path.is_file() or not isinstance(yosys_sha, str):
raise ValueError("memory-synthesis-ab.json lacks the Yosys executable identity")
if hashlib.sha256(yosys_path.read_bytes()).hexdigest() != yosys_sha:
raise ValueError("memory-synthesis-ab.json Yosys executable SHA-256 does not match")
if tool.get("read_slang_provider") != "yosys-executable":
raise ValueError("memory-synthesis-ab.json does not identify the Slang provider")
def _validate_numerical(report: dict[str, Any]) -> None:
accuracy = report.get("accuracy")
concurrency = report.get("concurrency")
latency = report.get("latency")
if (
not isinstance(accuracy, dict)
or accuracy.get("windows") != 1000
or not isinstance(accuracy.get("top1_correct"), int)
or accuracy["top1_correct"] < 900
or not isinstance(concurrency, dict)
or concurrency.get("seconds_per_scenario", 0) < 60
or sorted(concurrency.get("rates", [])) != [48000, 96000]
or sorted(concurrency.get("precisions", [])) != [16, 24]
or concurrency.get("xrun_count") != 0
or not isinstance(latency, dict)
or not isinstance(latency.get("maximum_window_cycles"), int)
or latency["maximum_window_cycles"] > 4_800_000
):
raise ValueError("numerical-latency.json lacks the frozen accuracy/latency/concurrency result")
def _validate_physical(report: dict[str, Any]) -> None:
inventory = report.get("macro_inventory")
area = report.get("area")
timing = report.get("timing")
if (
not isinstance(inventory, dict)
or inventory.get("wrappers") != 76
or not isinstance(area, dict)
or any(not isinstance(area.get(name), (int, float)) for name in ("logic_um2", "macro_um2", "total_um2"))
or not isinstance(timing, dict)
or not isinstance(timing.get("wns_ns"), (int, float))
or timing["wns_ns"] < 0
or timing.get("tns_ns") != 0
or timing.get("unconstrained_paths") != 0
or not isinstance(timing.get("hold"), dict)
):
raise ValueError("physical.json lacks the frozen macro/area/timing result")
def _validate_passed(name: str, report: dict[str, Any]) -> None:
if name == REPORTS[0]:
if report.get("profile") != "configs/ci/ihp130.mk":
raise ValueError("coefficient-layout.json does not use configs/ci/ihp130.mk")
layout_source = Path(str(report.get("source", "")))
apuc_source = Path(str(report.get("apuc_source", "")))
if not layout_source.is_file() or not apuc_source.is_file():
raise ValueError("coefficient-layout.json lacks its layout/APUC source artifacts")
validate_layout_manifest(apuc_source.read_bytes(), _read_json(layout_source))
return
if report.get("schema_version") != 1 or report.get("phase") != "APU-P9":
raise ValueError(f"{name} passed report lacks the APU-P9 schema identity")
if report.get("profile") != "configs/ci/ihp130.mk":
raise ValueError(f"{name} passed report does not use configs/ci/ihp130.mk")
if not _full_revision(report.get("revision")) or report.get("dirty") is not False:
raise ValueError(f"{name} passed report is not revision-qualified")
checks = report.get("checks")
required = REQUIRED_CHECKS[name]
if not isinstance(checks, dict) or any(checks.get(check) is not True for check in required):
raise ValueError(f"{name} passed report lacks one or more frozen checks")
if name in (REPORTS[1], REPORTS[2]):
_require_passed_tests(report, name)
if name == REPORTS[2] and (
report.get("baseline_revision") != BASELINE_REVISION
or report.get("candidate_revision") != report.get("revision")
):
raise ValueError("memo-equivalence.json revisions do not match the frozen comparison")
if name == REPORTS[3]:
_validate_numerical(report)
elif name == REPORTS[4]:
_validate_memory_ab(report)
elif name == REPORTS[5]:
_validate_physical(report)
_require_artifacts(report, name)
def _initialize(args: argparse.Namespace) -> None:
layout = _read_json(args.layout)
try:
apuc = args.apuc.read_bytes()
except OSError as error:
raise ValueError(f"cannot read APUC evidence {args.apuc}: {error}") from error
validate_layout_manifest(apuc, layout)
profile_path = args.profile.resolve()
try:
profile = profile_path.relative_to(ROOT).as_posix()
except ValueError:
profile = str(profile_path)
layout["phase"] = "APU-P9"
layout["profile"] = profile
layout["status"] = "passed"
layout["source"] = str(args.layout.resolve())
layout["apuc_source"] = str(args.apuc.resolve())
_write_json(args.output_dir / REPORTS[0], layout)
requirements = {
REPORTS[1]: "APUC admission, corruption, retry, lock, reset, quiesce and abort matrix",
REPORTS[2]: "baseline versus macro memo verdict, first-error and visit-count equivalence",
REPORTS[3]: "bit-exact coefficient, MFCC/layer/window and bounded-cycle qualification",
REPORTS[4]: "same-input baseline/candidate inferred memory, runtime and peak-RSS comparison",
REPORTS[5]: "76-wrapper area, WNS/TNS, unconstrained-path and macro-view accounting",
}
for name, requirement in requirements.items():
destination = args.output_dir / name
if not destination.exists():
_write_json(
destination,
{
"schema_version": 1,
"phase": "APU-P9",
"status": "unrun",
"requirement": requirement,
"reason": "required qualifying run has not been supplied",
},
)
def _assemble(args: argparse.Namespace) -> None:
sources: dict[str, Path] = {}
for item in args.report:
name, separator, raw_path = item.partition("=")
if not separator or name not in REPORTS:
raise ValueError(f"report must be NAME=PATH for one of {REPORTS}: {item}")
sources[name] = Path(raw_path)
missing = [name for name in REPORTS if name not in sources]
if missing:
raise ValueError(f"missing required P9 reports: {missing}")
for name, source in sources.items():
report = _read_json(source)
status = report.get("status")
if status not in ("passed", "unrun", "failed", "timed_out"):
raise ValueError(f"{name} lacks a recognized fail-closed status")
if status == "passed":
_validate_passed(name, report)
elif not isinstance(report.get("reason", report.get("failure")), str):
raise ValueError(f"{name} {status} report lacks a reason/failure")
report["source"] = str(source.resolve())
_write_json(args.output_dir / name, report)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
initialize = subparsers.add_parser("initialize")
initialize.add_argument("--layout", type=Path, required=True)
initialize.add_argument("--apuc", type=Path, required=True)
initialize.add_argument("--profile", type=Path, required=True)
initialize.add_argument("--output-dir", type=Path, required=True)
initialize.set_defaults(handler=_initialize)
assemble = subparsers.add_parser("assemble")
assemble.add_argument("--report", action="append", default=[])
assemble.add_argument("--output-dir", type=Path, required=True)
assemble.set_defaults(handler=_assemble)
args = parser.parse_args()
try:
args.handler(args)
except ValueError as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())