mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +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.
429 lines
15 KiB
Python
429 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the isolated NPU IHP130 synthesis and 72 MHz STA flow (P1/P3 modules)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
SYNTH_TCL = ROOT / "physical/smoke/syn/yosys/script/synth.tcl"
|
|
STA_TCL = ROOT / "physical/smoke/sta/opensta/npu_p1.tcl"
|
|
STA_SDC = ROOT / "physical/smoke/sta/opensta/npu_p1.sdc"
|
|
RUN_FLOW = ROOT / "scripts/run_flow.py"
|
|
|
|
PDK_ROOT = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref"
|
|
STA_LIBERTY = PDK_ROOT / "sg13g2_stdcell/lib/sg13g2_stdcell_slow_1p08V_125C.lib"
|
|
STA_LINK_LIB = PDK_ROOT / "sg13g2_io/lib/sg13g2_io_slow_1p08V_3p0V_125C.lib"
|
|
|
|
TOPS = (
|
|
"npu_local_sram",
|
|
"npu_patch_packer",
|
|
"npu_mac_array",
|
|
"npu_accumulator",
|
|
"npu_vector",
|
|
"npu_requantizer",
|
|
"npu_dma",
|
|
"npu_job_decoder",
|
|
"npu_scheduler",
|
|
"npu_core",
|
|
"apb4_npu",
|
|
"npu_block_top",
|
|
)
|
|
NPU_RTL = (
|
|
"npu_pkg.sv",
|
|
"npu_local_sram.sv",
|
|
"npu_patch_packer.sv",
|
|
"npu_mac_array.sv",
|
|
"npu_accumulator.sv",
|
|
"npu_vector.sv",
|
|
"npu_requantizer.sv",
|
|
"npu_dma.sv",
|
|
"npu_job_decoder.sv",
|
|
"npu_scheduler.sv",
|
|
"npu_reg.sv",
|
|
"npu_control_cdc.sv",
|
|
"npu_core.sv",
|
|
"apb4_npu.sv",
|
|
)
|
|
# Tops whose only non-logic port is an AXI4 master interface get a generated
|
|
# synthesis wrapper in the build tree (never tracked): the interface is
|
|
# instantiated inside the wrapper so the DUT has no unconnected ports.
|
|
INTERFACE_WRAPPED = {
|
|
"npu_dma": """
|
|
module npu_dma_synth_top (
|
|
input logic clk_hp_i,
|
|
input logic rst_hp_n_i,
|
|
input logic clear_i,
|
|
input logic block_new_i,
|
|
output logic pause_ack_o,
|
|
input logic read_req_valid_i,
|
|
output logic read_req_ready_o,
|
|
input logic [31:0] read_addr_i,
|
|
input logic [31:0] read_bytes_i,
|
|
output logic read_data_valid_o,
|
|
input logic read_data_ready_i,
|
|
output logic [63:0] read_data_o,
|
|
output logic [ 7:0] read_keep_o,
|
|
output logic read_last_o,
|
|
input logic write_req_valid_i,
|
|
output logic write_req_ready_o,
|
|
input logic [31:0] write_addr_i,
|
|
input logic [31:0] write_bytes_i,
|
|
input logic write_data_valid_i,
|
|
output logic write_data_ready_o,
|
|
input logic [63:0] write_data_i,
|
|
input logic [ 7:0] write_keep_i,
|
|
input logic write_last_i,
|
|
output logic write_done_o,
|
|
output logic busy_o,
|
|
output logic read_busy_o,
|
|
output logic write_busy_o,
|
|
output logic [63:0] read_bytes_o,
|
|
output logic [63:0] write_bytes_o,
|
|
output logic [63:0] stall_cycles_o,
|
|
output logic fault_o,
|
|
output logic [ 3:0] fault_code_o,
|
|
output logic [31:0] fault_addr_o,
|
|
output logic [ 1:0] fault_resp_o,
|
|
output logic read_cmd_err_o,
|
|
output logic write_cmd_err_o
|
|
);
|
|
axi4_if #(
|
|
.ADDR_WIDTH(32),
|
|
.DATA_WIDTH(64),
|
|
.ID_WIDTH (3),
|
|
.USER_WIDTH(1)
|
|
) u_axi4 (
|
|
.aclk (clk_hp_i),
|
|
.aresetn(rst_hp_n_i)
|
|
);
|
|
npu_dma u_dut (
|
|
.clk_hp_i (clk_hp_i),
|
|
.rst_hp_n_i (rst_hp_n_i),
|
|
.clear_i (clear_i),
|
|
.block_new_i (block_new_i),
|
|
.pause_ack_o (pause_ack_o),
|
|
.read_req_valid_i (read_req_valid_i),
|
|
.read_req_ready_o (read_req_ready_o),
|
|
.read_addr_i (read_addr_i),
|
|
.read_bytes_i (read_bytes_i),
|
|
.read_data_valid_o (read_data_valid_o),
|
|
.read_data_ready_i (read_data_ready_i),
|
|
.read_data_o (read_data_o),
|
|
.read_keep_o (read_keep_o),
|
|
.read_last_o (read_last_o),
|
|
.write_req_valid_i (write_req_valid_i),
|
|
.write_req_ready_o (write_req_ready_o),
|
|
.write_addr_i (write_addr_i),
|
|
.write_bytes_i (write_bytes_i),
|
|
.write_data_valid_i(write_data_valid_i),
|
|
.write_data_ready_o(write_data_ready_o),
|
|
.write_data_i (write_data_i),
|
|
.write_keep_i (write_keep_i),
|
|
.write_last_i (write_last_i),
|
|
.write_done_o (write_done_o),
|
|
.busy_o (busy_o),
|
|
.read_busy_o (read_busy_o),
|
|
.write_busy_o (write_busy_o),
|
|
.read_bytes_o (read_bytes_o),
|
|
.write_bytes_o (write_bytes_o),
|
|
.stall_cycles_o (stall_cycles_o),
|
|
.fault_o (fault_o),
|
|
.fault_code_o (fault_code_o),
|
|
.fault_addr_o (fault_addr_o),
|
|
.fault_resp_o (fault_resp_o),
|
|
.read_cmd_err_o (read_cmd_err_o),
|
|
.write_cmd_err_o (write_cmd_err_o),
|
|
.axi4 (u_axi4)
|
|
);
|
|
endmodule
|
|
""",
|
|
"apb4_npu": """
|
|
module apb4_npu_synth_top (
|
|
input logic clk_i,
|
|
input logic rst_n_i,
|
|
input logic clk_hp_i,
|
|
input logic rst_hp_n_i,
|
|
input logic [1:0] resource_owner_i,
|
|
input logic resource_owner_lock_i,
|
|
input logic resource_quiesce_i,
|
|
input logic resource_reset_i,
|
|
output logic idle_o,
|
|
output logic block_ack_o,
|
|
output logic irq_o,
|
|
input logic hp_block_new_i,
|
|
output logic hp_pause_ack_o,
|
|
input logic hp_flush_i,
|
|
output logic hp_flush_busy_o,
|
|
output logic hp_idle_o
|
|
);
|
|
apb4_if u_apb4 (.pclk(clk_i), .presetn(rst_n_i));
|
|
axi4_if #(
|
|
.ADDR_WIDTH(32),
|
|
.DATA_WIDTH(64),
|
|
.ID_WIDTH (3),
|
|
.USER_WIDTH(1)
|
|
) u_npu_axi4 (
|
|
.aclk (clk_hp_i),
|
|
.aresetn(rst_hp_n_i)
|
|
);
|
|
apb4_npu u_dut (
|
|
.clk_i (clk_i),
|
|
.rst_n_i (rst_n_i),
|
|
.clk_hp_i (clk_hp_i),
|
|
.rst_hp_n_i (rst_hp_n_i),
|
|
.resource_owner_i (resource_owner_i),
|
|
.resource_owner_lock_i(resource_owner_lock_i),
|
|
.resource_quiesce_i (resource_quiesce_i),
|
|
.resource_reset_i (resource_reset_i),
|
|
.apb4 (u_apb4),
|
|
.idle_o (idle_o),
|
|
.block_ack_o (block_ack_o),
|
|
.irq_o (irq_o),
|
|
.hp_block_new_i (hp_block_new_i),
|
|
.hp_pause_ack_o (hp_pause_ack_o),
|
|
.hp_flush_i (hp_flush_i),
|
|
.hp_flush_busy_o (hp_flush_busy_o),
|
|
.hp_idle_o (hp_idle_o),
|
|
.npu_axi4 (u_npu_axi4)
|
|
);
|
|
endmodule
|
|
""",
|
|
}
|
|
SRAM_MACRO = "RM_IHPSG13_1P_1024x32_c2_bm_bist"
|
|
EXPECTED_SRAM_MACROS = 16
|
|
TARGET_PERIOD_PS = 13889
|
|
|
|
VIOLATED = re.compile(r"VIOLATED")
|
|
LATCH = re.compile(r"\$dlatch|latch", re.IGNORECASE)
|
|
|
|
|
|
def _run_flow(tool: str, log: Path, result: Path, env: dict[str, str], command: list[str]) -> None:
|
|
log.parent.mkdir(parents=True, exist_ok=True)
|
|
args = [sys.executable, str(RUN_FLOW), "--tool", tool, "--log", str(log), "--result", str(result)]
|
|
for name, value in env.items():
|
|
args += ["--env", f"{name}={value}"]
|
|
args += ["--", *command]
|
|
completed = subprocess.run(args, cwd=ROOT)
|
|
if completed.returncode != 0:
|
|
raise RuntimeError(f"{tool} flow failed for {env.get('TOP_DESIGN', env.get('OPENSTA_TOP'))}: {log}")
|
|
|
|
|
|
def _synth_top(top: str) -> str:
|
|
return f"{top}_synth_top" if top in INTERFACE_WRAPPED else top
|
|
|
|
|
|
def _write_filelist(path: Path) -> None:
|
|
lines = [
|
|
"+define+PDK_IHP130",
|
|
"+define+HAVE_SRAM_MACRO",
|
|
"+define+SYNTHESIS",
|
|
"+define+SV_ASSRT_DISABLE",
|
|
f"+incdir+{ROOT / 'rtl/ip/multimedia'}",
|
|
f"+incdir+{ROOT / 'rtl/managed/clusterip/common/rtl'}",
|
|
]
|
|
for name in NPU_RTL:
|
|
source = ROOT / "rtl/ip/multimedia" / name
|
|
if not source.is_file():
|
|
raise RuntimeError(f"missing NPU RTL source: {source}")
|
|
lines.append(str(source))
|
|
lines.append(str(ROOT / "physical/smoke/syn/yosys/npu_block_top.sv"))
|
|
common_rtl = ROOT / "rtl/managed/clusterip/common/rtl"
|
|
lines.append(str(ROOT / "rtl/tech/tc_sram.sv"))
|
|
for extra in (
|
|
"interface/apb4_if.sv",
|
|
"interface/axi4_if.sv",
|
|
"utils/register.sv",
|
|
"utils/xchecker.sv",
|
|
"utils/fifo.sv",
|
|
"clkrst/rst_sync.sv",
|
|
"cdc/cdc_sync.sv",
|
|
"cdc/cdc_rst_ctrlr.sv",
|
|
"cdc/async_reqack.sv",
|
|
):
|
|
source = common_rtl / extra
|
|
if not source.is_file():
|
|
raise RuntimeError(f"missing Common source: {source}")
|
|
lines.append(str(source))
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
for top, wrapper in INTERFACE_WRAPPED.items():
|
|
wrapper_path = path.parent / f"{_synth_top(top)}.sv"
|
|
wrapper_path.write_text(wrapper, encoding="utf-8")
|
|
lines.append(str(wrapper_path))
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def _synth(top: str, build_root: Path, filelist: Path) -> dict[str, object]:
|
|
base = build_root / "syn/yosys-npu" / top
|
|
synth_top = _synth_top(top)
|
|
for sub in ("out", "tmp", "rpt"):
|
|
(base / sub).mkdir(parents=True, exist_ok=True)
|
|
env = {
|
|
"PDK": "IHP130",
|
|
"SOC": "MINI",
|
|
"SYNTH_RECIPE": "balanced",
|
|
"HAVE_SRAM_MACRO": "YES",
|
|
"SRAM_SIZE_KIB": "32",
|
|
"YOSYS_TARGET_PERIOD_PS": str(TARGET_PERIOD_PS),
|
|
"SV_FLIST": str(filelist),
|
|
"TOP_DESIGN": synth_top,
|
|
"PROJ_NAME": synth_top,
|
|
"BUILD": str(base / "out"),
|
|
"WORK": str(base / "tmp"),
|
|
"REPORTS": str(base / "rpt"),
|
|
"NETLIST": str(base / "out" / f"{synth_top}_yosys.v"),
|
|
"CONFIG": str(base / "out" / f"{synth_top}_yosys.config"),
|
|
}
|
|
log = build_root / "syn/yosys-npu" / f"{top}.log"
|
|
_run_flow("yosys", log, base / "result-synth.json", env, ["yosys", "-c", str(SYNTH_TCL)])
|
|
area = json.loads((base / "rpt" / f"{synth_top}_area.json").read_text(encoding="utf-8"))
|
|
design = area["design"]
|
|
macro_count = design.get("num_cells_by_type", {}).get(SRAM_MACRO, 0)
|
|
latch_cells = {
|
|
name: count
|
|
for name, count in design.get("num_cells_by_type", {}).items()
|
|
if "dlatch" in name.lower() or "latch" in name.lower()
|
|
}
|
|
log_text = log.read_text(encoding="utf-8", errors="replace")
|
|
latch_lines = [
|
|
line
|
|
for line in log_text.splitlines()
|
|
if LATCH.search(line) and "PROC_DLATCH pass" not in line
|
|
]
|
|
return {
|
|
"top": top,
|
|
"area_um2": design.get("area"),
|
|
"num_cells": design.get("num_cells"),
|
|
"sram_macro_count": macro_count,
|
|
"latch_cells": latch_cells,
|
|
"latch_log_lines": latch_lines[:10],
|
|
"log": str(log),
|
|
"netlist": env["NETLIST"],
|
|
}
|
|
|
|
|
|
def _sta(top: str, build_root: Path, netlist: Path) -> dict[str, object]:
|
|
base = build_root / "sta/opensta-npu" / top
|
|
base.mkdir(parents=True, exist_ok=True)
|
|
sram_libs = sorted(
|
|
glob.glob(str(PDK_ROOT / "sg13g2_sram/lib/*_slow_1p08V_125C.lib"))
|
|
)
|
|
env = {
|
|
"OPENSTA_NETLIST": str(netlist),
|
|
"OPENSTA_LIBERTY": str(STA_LIBERTY),
|
|
"OPENSTA_LINK_LIBS": str(STA_LINK_LIB),
|
|
"OPENSTA_SRAM_LIBS": " ".join(sram_libs),
|
|
"OPENSTA_SDC": str(STA_SDC),
|
|
"OPENSTA_REPORT": str(base / f"{top}_checks.rpt"),
|
|
"OPENSTA_METRICS": str(base / f"{top}_timing_metrics.rpt"),
|
|
"OPENSTA_TOP": _synth_top(top),
|
|
}
|
|
log = base / f"{top}.log"
|
|
_run_flow("opensta", log, base / "result-sta.json", env, ["sta", "-no_init", "-exit", str(STA_TCL)])
|
|
metrics = {}
|
|
for line in (base / f"{top}_timing_metrics.rpt").read_text(encoding="utf-8").splitlines():
|
|
name, _, value = line.partition("=")
|
|
metrics[name] = float(value)
|
|
checks = (base / f"{top}_checks.rpt").read_text(encoding="utf-8", errors="replace")
|
|
return {
|
|
"top": top,
|
|
"wns_setup_ns": metrics.get("wns_max"),
|
|
"tns_setup_ns": metrics.get("tns_max"),
|
|
"setup_paths": metrics.get("paths_max"),
|
|
"wns_hold_ns": metrics.get("wns_min"),
|
|
"tns_hold_ns": metrics.get("tns_min"),
|
|
"hold_paths": metrics.get("paths_min"),
|
|
"violated_paths": len(VIOLATED.findall(checks)),
|
|
"log": str(log),
|
|
}
|
|
|
|
|
|
def _tool_version(command: list[str]) -> str:
|
|
completed = subprocess.run(command, capture_output=True, text=True)
|
|
return (completed.stdout or completed.stderr).splitlines()[0].strip()
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--build-root", type=Path, default=ROOT / "build/npu-p1-flow")
|
|
parser.add_argument("--top", action="append", choices=TOPS, help="limit to one top module")
|
|
parser.add_argument("--synth", action="store_true")
|
|
parser.add_argument("--sta", action="store_true")
|
|
parser.add_argument("--evidence", type=Path, help="evidence JSON output path")
|
|
args = parser.parse_args()
|
|
if not args.synth and not args.sta:
|
|
args.synth = args.sta = True
|
|
|
|
tops = tuple(args.top) if args.top else TOPS
|
|
build_root = args.build_root.resolve()
|
|
filelist = build_root / "npu_p1.fl"
|
|
_write_filelist(filelist)
|
|
|
|
evidence: dict[str, object] = {
|
|
"phase": "NPU-P1",
|
|
"verification_ids": ["NPU-V017"],
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"git_revision": subprocess.check_output(
|
|
["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True
|
|
).strip(),
|
|
"tools": {
|
|
"yosys": _tool_version(["yosys", "--version"]),
|
|
"opensta": _tool_version(["sta", "-version"]),
|
|
},
|
|
"target_period_ps": TARGET_PERIOD_PS,
|
|
"filelist": str(filelist),
|
|
"tops": {},
|
|
"failures": [],
|
|
}
|
|
failures: list[str] = evidence["failures"]
|
|
for top in tops:
|
|
record: dict[str, object] = {}
|
|
if args.synth:
|
|
record["synth"] = _synth(top, build_root, filelist)
|
|
synth = record["synth"]
|
|
if top == "npu_local_sram" and synth["sram_macro_count"] != EXPECTED_SRAM_MACROS:
|
|
failures.append(
|
|
f"npu_local_sram maps {synth['sram_macro_count']} {SRAM_MACRO}, "
|
|
f"expected {EXPECTED_SRAM_MACROS}"
|
|
)
|
|
if synth["latch_log_lines"] or synth["latch_cells"]:
|
|
failures.append(
|
|
f"{top} synthesis shows latches: {synth['latch_log_lines']} {synth['latch_cells']}"
|
|
)
|
|
netlist = build_root / "syn/yosys-npu" / top / "out" / f"{_synth_top(top)}_yosys.v"
|
|
if args.sta:
|
|
if not netlist.is_file():
|
|
raise RuntimeError(f"missing netlist for {top}: {netlist}; run --synth first")
|
|
record["sta"] = _sta(top, build_root, netlist)
|
|
sta = record["sta"]
|
|
if not (sta["wns_setup_ns"] >= 0.0):
|
|
failures.append(f"{top} setup WNS {sta['wns_setup_ns']} ns < 0")
|
|
if not (sta["tns_setup_ns"] == 0.0):
|
|
failures.append(f"{top} setup TNS {sta['tns_setup_ns']} ns != 0")
|
|
if sta["violated_paths"]:
|
|
failures.append(f"{top} has {sta['violated_paths']} violating paths")
|
|
if not sta["setup_paths"]:
|
|
failures.append(f"{top} has no constrained setup paths (vacuous STA)")
|
|
evidence["tops"][top] = record
|
|
|
|
evidence["verdict"] = "PASS" if not failures else "FAIL"
|
|
payload = json.dumps(evidence, indent=2, sort_keys=True) + "\n"
|
|
evidence_path = args.evidence or (build_root / "evidence-p1.json")
|
|
evidence_path.parent.mkdir(parents=True, exist_ok=True)
|
|
evidence_path.write_text(payload, encoding="utf-8")
|
|
print(payload, end="")
|
|
return 0 if not failures else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|