mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-05 06:32:37 +00:00
- Introduced for deterministic corpus framing, CRC checks, and regression evidence for NPU-P6. - Added to validate NPU-P6 reference execution. - Created a new RTL model for gate-level NPU transactions. - Updated to instantiate and streamline the testbench. - Enhanced with capability checks and added NPU-P6 reference contract tests. - Modified to include in the source list and refactor rejection logic. - Updated README.md to document new test scripts and their purposes.
248 lines
9.0 KiB
Python
248 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Run and validate isolated-NPU plus full-PRODUCT IHP130 physical evidence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CONFIG = ROOT / "configs/ci/ihp130.mk"
|
|
SRAM_MACRO = "RM_IHPSG13_1P_1024x32_c2_bm_bist"
|
|
LATCH = re.compile(r"(?:\$dlatch|\blatch inferred)", re.IGNORECASE)
|
|
BLACKBOX = re.compile(r"(?:unresolved|unknown module)", re.IGNORECASE)
|
|
|
|
|
|
def identity(path: Path) -> dict[str, object]:
|
|
data = path.read_bytes()
|
|
return {
|
|
"path": str(path.resolve()),
|
|
"bytes": len(data),
|
|
"sha256": hashlib.sha256(data).hexdigest(),
|
|
}
|
|
|
|
|
|
def load(path: Path) -> dict[str, Any]:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"physical evidence is not an object: {path}")
|
|
return data
|
|
|
|
|
|
def make_command(timestamp: str, *targets: str) -> list[str]:
|
|
return [
|
|
"make",
|
|
f"CONFIG={CONFIG}",
|
|
"SIMU=IVERILOG",
|
|
"SYNTH=YOSYS",
|
|
"STA=OPENSTA",
|
|
"SYNTH_RECIPE=balanced",
|
|
f"BUILD_TIMESTAMP={timestamp}",
|
|
*targets,
|
|
]
|
|
|
|
|
|
def run(command: list[str], *, log: Path) -> None:
|
|
completed = subprocess.run(
|
|
command,
|
|
cwd=ROOT,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
check=False,
|
|
env={**os.environ, "CCACHE_DISABLE": "1"},
|
|
)
|
|
log.parent.mkdir(parents=True, exist_ok=True)
|
|
log.write_text(completed.stdout, encoding="utf-8")
|
|
if completed.returncode != 0:
|
|
raise RuntimeError(f"P6 physical command failed: {log}")
|
|
|
|
|
|
def variant_root(timestamp: str) -> Path:
|
|
completed = subprocess.run(
|
|
make_command(timestamp, "-s", "config"),
|
|
cwd=ROOT,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
check=False,
|
|
)
|
|
if completed.returncode != 0:
|
|
raise RuntimeError(f"failed to resolve P6 physical variant\n{completed.stdout}")
|
|
match = re.search(r"(?m)^VARIANT_ROOT\s+(.+)$", completed.stdout)
|
|
if match is None:
|
|
raise ValueError("P6 physical config omitted VARIANT_ROOT")
|
|
return Path(match.group(1)).resolve()
|
|
|
|
|
|
def timing_metrics(path: Path) -> dict[str, float]:
|
|
values: dict[str, float] = {}
|
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
name, separator, value = line.partition("=")
|
|
if separator and name in {"wns_min", "wns_max", "tns_min", "tns_max"}:
|
|
values[name] = float(value)
|
|
if set(values) != {"wns_min", "wns_max", "tns_min", "tns_max"}:
|
|
raise ValueError("PRODUCT STA did not report all WNS/TNS metrics")
|
|
if values["wns_max"] < 0.0 or values["tns_max"] != 0.0:
|
|
raise ValueError("PRODUCT setup timing did not close")
|
|
if values["wns_min"] < 0.0 or values["tns_min"] != 0.0:
|
|
raise ValueError("PRODUCT hold timing did not close")
|
|
return values
|
|
|
|
|
|
def npu_macro_count(netlist: Path) -> int:
|
|
text = netlist.read_text(encoding="utf-8", errors="replace")
|
|
count = 0
|
|
for match in re.finditer(r"(?ms)^module\s+([^\s(]+).*?^endmodule", text):
|
|
module_name = match.group(1).lower()
|
|
if "npu" in module_name and "tc_sram_1024x32" in module_name:
|
|
count += match.group(0).count(f"{SRAM_MACRO} u_mem")
|
|
return count
|
|
|
|
|
|
def validate_result(path: Path) -> dict[str, Any]:
|
|
result = load(path)
|
|
if result.get("status") != "passed" or result.get("exit_code") != 0:
|
|
raise ValueError(f"required physical flow failed: {path}")
|
|
return result
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--isolated-report", type=Path, required=True)
|
|
parser.add_argument("--output-dir", type=Path, required=True)
|
|
parser.add_argument(
|
|
"--build-timestamp", default=datetime.now().astimezone().strftime("%Y-%m-%d-%H-%M")
|
|
)
|
|
parser.add_argument("--reuse-product", action="store_true", help="reuse existing PRODUCT flow")
|
|
args = parser.parse_args()
|
|
isolated = load(args.isolated_report)
|
|
if (
|
|
isolated.get("verdict") != "PASS"
|
|
or isolated.get("sram_macro_count") != 16
|
|
or isolated.get("verification_ids") != ["NPU-V017", "NPU-V018"]
|
|
):
|
|
raise ValueError("isolated NPU synthesis/STA/netlist evidence is incomplete")
|
|
output = args.output_dir.resolve()
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
variant = variant_root(args.build_timestamp)
|
|
if not args.reuse_product:
|
|
run(
|
|
make_command(
|
|
args.build_timestamp,
|
|
"manifest",
|
|
"synth",
|
|
"sta",
|
|
"check-warnings",
|
|
"metrics",
|
|
"check-metrics",
|
|
),
|
|
log=output / "product-physical.log",
|
|
)
|
|
manifest_path = variant / "meta/manifest.json"
|
|
synth_result_path = variant / "syn/yosys/result-synth.json"
|
|
sta_result_path = variant / "sta/opensta/result-sta.json"
|
|
warning_path = variant / "meta/warnings.json"
|
|
metrics_path = variant / "meta/metrics.json"
|
|
netlist = variant / "syn/yosys/out/retrosoc_asic_yosys.v"
|
|
for required in (
|
|
manifest_path,
|
|
synth_result_path,
|
|
sta_result_path,
|
|
warning_path,
|
|
metrics_path,
|
|
netlist,
|
|
):
|
|
if not required.is_file():
|
|
raise ValueError(f"missing PRODUCT physical artifact: {required}")
|
|
synth_result = validate_result(synth_result_path)
|
|
sta_result = validate_result(sta_result_path)
|
|
manifest = load(manifest_path)
|
|
config = manifest.get("configuration", {})
|
|
expected_config = {
|
|
"MINI_MODE": "PRODUCT",
|
|
"PDK": "IHP130",
|
|
"HAVE_HP": "YES",
|
|
"HAVE_SRAM_MACRO": "YES",
|
|
"EXT_CLK_HZ": "72000000",
|
|
"SYNTH": "YOSYS",
|
|
"STA": "OPENSTA",
|
|
"SYNTH_RECIPE": "balanced",
|
|
}
|
|
if any(config.get(name) != value for name, value in expected_config.items()):
|
|
raise ValueError("PRODUCT physical configuration differs from the P6 contract")
|
|
warnings = load(warning_path)
|
|
if warnings.get("status") != "passed" or warnings.get("failed_tools"):
|
|
raise ValueError("PRODUCT warning review did not pass")
|
|
metrics = load(metrics_path)
|
|
synthesis = metrics.get("synthesis", {})
|
|
if synthesis.get("top_area", 0) <= 0 or synthesis.get("top_cells", 0) <= 0:
|
|
raise ValueError("PRODUCT metrics omit positive area or cell count")
|
|
timing = timing_metrics(variant / "sta/opensta/timing_metrics.rpt")
|
|
synth_log = variant / "syn/yosys/retrosoc_asic.log"
|
|
synth_text = synth_log.read_text(encoding="utf-8", errors="replace")
|
|
suspicious = [
|
|
line
|
|
for line in synth_text.splitlines()
|
|
if (LATCH.search(line) or BLACKBOX.search(line))
|
|
and "PROC_DLATCH pass" not in line
|
|
and "no latch inferred" not in line.lower()
|
|
]
|
|
if suspicious:
|
|
raise ValueError(f"PRODUCT synthesis reported latches or unresolved black boxes: {suspicious[:3]}")
|
|
product_npu_macros = npu_macro_count(netlist)
|
|
if product_npu_macros != 16:
|
|
raise ValueError(f"PRODUCT netlist contains {product_npu_macros} NPU SRAM macros, expected 16")
|
|
report = {
|
|
"schema": 1,
|
|
"phase": "NPU-P6",
|
|
"verification_ids": ["NPU-V017"],
|
|
"git_revision": subprocess.check_output(
|
|
["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True
|
|
).strip(),
|
|
"profile": "configs/ci/ihp130.mk",
|
|
"target_frequency_hz": 72_000_000,
|
|
"product_reused": args.reuse_product,
|
|
"implementation": {
|
|
name: identity(ROOT / name)
|
|
for name in (
|
|
"scripts/run_npu_p6_physical.py",
|
|
"physical/smoke/syn/yosys/script/synth.tcl",
|
|
"physical/smoke/sta/opensta/opensta.tcl",
|
|
"physical/smoke/sta/opensta/generate_sdc.py",
|
|
)
|
|
},
|
|
"isolated": identity(args.isolated_report),
|
|
"product": {
|
|
"variant_root": str(variant),
|
|
"manifest": identity(manifest_path),
|
|
"synthesis_result": {**identity(synth_result_path), "duration_seconds": synth_result.get("duration_seconds")},
|
|
"sta_result": {**identity(sta_result_path), "duration_seconds": sta_result.get("duration_seconds")},
|
|
"warnings": identity(warning_path),
|
|
"metrics": identity(metrics_path),
|
|
"netlist": identity(netlist),
|
|
"area_um2": synthesis["top_area"],
|
|
"cells": synthesis["top_cells"],
|
|
"npu_sram_macros": product_npu_macros,
|
|
"timing": timing,
|
|
},
|
|
"summary": {"required_checks": 2, "passes": 2, "failures": 0, "skipped": 0},
|
|
"verdict": "PASS",
|
|
}
|
|
report_path = output / "qualification-p6-physical.json"
|
|
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
print(f"NPU-P6 physical qualification: PASS ({report_path})")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|