Files
retroSoC/scripts/run_npu_p6_physical.py
T
maksyuki e6b9a9f43d feat: add NPU-P6 tests and RTL model updates
- 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.
2026-09-22 07:30:54 +08:00

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())