From 15fb67eb43cec656315916799a8d8885ce0890a2 Mon Sep 17 00:00:00 2001 From: zxy Date: Tue, 9 Jun 2026 12:07:56 +0800 Subject: [PATCH] Add Yosys LEC flow support --- .gitmodules | 3 + chipcompiler/data/parameter.py | 3 + chipcompiler/data/step.py | 4 +- chipcompiler/engine/flow.py | 30 +- chipcompiler/rtl2gds/__init__.py | 8 +- chipcompiler/rtl2gds/builder.py | 23 ++ chipcompiler/tools/yosys/builder.py | 3 + chipcompiler/tools/yosys/runner.py | 9 +- .../tools/yosys/scripts/yosys_synthesis.tcl | 4 + chipcompiler/tools/yosys_lec/__init__.py | 18 ++ chipcompiler/tools/yosys_lec/builder.py | 267 +++++++++++++++++ chipcompiler/tools/yosys_lec/runner.py | 71 +++++ chipcompiler/tools/yosys_lec/subflow.py | 93 ++++++ chipcompiler/tools/yosys_lec/utility.py | 5 + test/icsprout55-pdk | 1 + test/test_tools_yosys_runner.py | 12 +- test/yosys_lec/.gitignore | 1 + test/yosys_lec/test_tools_yosys_lec.py | 278 ++++++++++++++++++ 18 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 chipcompiler/tools/yosys_lec/__init__.py create mode 100644 chipcompiler/tools/yosys_lec/builder.py create mode 100644 chipcompiler/tools/yosys_lec/runner.py create mode 100644 chipcompiler/tools/yosys_lec/subflow.py create mode 100644 chipcompiler/tools/yosys_lec/utility.py create mode 160000 test/icsprout55-pdk create mode 100644 test/yosys_lec/.gitignore create mode 100644 test/yosys_lec/test_tools_yosys_lec.py diff --git a/.gitmodules b/.gitmodules index 0a3f5ffa..0fb630aa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "chipcompiler/thirdparty/ecc-dreamplace"] path = chipcompiler/thirdparty/ecc-dreamplace url = https://github.com/openecos-projects/ecc-dreamplace.git +[submodule "test/icsprout55-pdk"] + path = test/icsprout55-pdk + url = git@github.com:openecos-projects/icsprout55-pdk.git diff --git a/chipcompiler/data/parameter.py b/chipcompiler/data/parameter.py index 854ea20b..9294287a 100644 --- a/chipcompiler/data/parameter.py +++ b/chipcompiler/data/parameter.py @@ -107,6 +107,9 @@ "Top module": "gcd", "Clock": "clk", "Frequency max [MHz]": 100, + "LEC": { + "use_undef": True, + }, } } diff --git a/chipcompiler/data/step.py b/chipcompiler/data/step.py index ee5eb9e0..8428a0a4 100644 --- a/chipcompiler/data/step.py +++ b/chipcompiler/data/step.py @@ -25,6 +25,8 @@ class StepEnum(Enum): FILLER = "filler" GDS = "GDS" SIGNOFF = "Signoff" + LEC = "lec" + POST_ROUTE_LEC = "postRouteLec" STA = "sta" DRC = "drc" RCX = "RCX" @@ -83,4 +85,4 @@ def load_metrics(path : str) -> StepMetrics: def save_metrics(metrics : StepMetrics) -> bool: from chipcompiler.utility import json_write return json_write(file_path=metrics.path, - data=metrics.data) \ No newline at end of file + data=metrics.data) diff --git a/chipcompiler/engine/flow.py b/chipcompiler/engine/flow.py index 6e61cdf5..6f2ae1e6 100644 --- a/chipcompiler/engine/flow.py +++ b/chipcompiler/engine/flow.py @@ -51,7 +51,8 @@ def has_init(self): def init_flow_step(self, step : StepEnum | str, tool : str, - state : str | StateEnum): + state : str | StateEnum, + info: dict | None = None): step_value = step.value if isinstance(step, StepEnum) else step state_value = state.value if isinstance(state, StateEnum) else state return { @@ -60,15 +61,16 @@ def init_flow_step(self, "state" : state_value, # step state "runtime" : "", # step run time "peak memory (mb)" : 0, # step peak memory - "info" : {} # step additional infomation + "info" : info or {} # step additional infomation } def add_step(self, step : StepEnum | str, tool : str, - state : str | StateEnum): + state : str | StateEnum, + info: dict | None = None): steps = self.workspace.flow.data.get("steps", []) - steps.append(self.init_flow_step(step, tool, state)) + steps.append(self.init_flow_step(step, tool, state, info=info)) self.workspace.flow.data = {"steps" : steps} @@ -172,6 +174,11 @@ def check_step_result(self, """ import os success = False + if ( + workspace_step.tool == "yosys_lec" + or workspace_step.name in (StepEnum.LEC.value, StepEnum.POST_ROUTE_LEC.value) + ): + return os.path.exists(workspace_step.output.get("json", "")) match workspace_step.name: case StepEnum.SYNTHESIS.value: if os.path.exists(workspace_step.output.get("verilog", "")): @@ -197,6 +204,8 @@ def create_step_workspaces(self): create all step workspaces """ pre_step = None + synthesis_gate_verilog = "" + synthesis_golden_verilog = "" for step in self.workspace.flow.data.get("steps", []): if pre_step is None: # use the origin def and verilog in workspace for the first step. @@ -209,6 +218,16 @@ def create_step_workspaces(self): input_verilog = pre_step.output.get("verilog", "") input_db = pre_step.output.get("db", "") + if step["tool"] == "yosys_lec": + step_info = step.get("info", {}) or {} + explicit_golden = step_info.get("golden_verilog", "") + if explicit_golden: + input_db = explicit_golden + elif step["name"] == StepEnum.POST_ROUTE_LEC.value: + input_db = synthesis_gate_verilog + elif pre_step is not None and pre_step.name == StepEnum.SYNTHESIS.value: + input_db = synthesis_golden_verilog + from chipcompiler.tools import create_step, run_step # create workspace step eda_step = create_step(workspace=self.workspace, @@ -225,6 +244,9 @@ def create_step_workspaces(self): eda_step.output["spef"] = pre_step.output.get("spef", []) self.workspace_steps.append(eda_step) pre_step = eda_step + if eda_step.name == StepEnum.SYNTHESIS.value: + synthesis_gate_verilog = eda_step.output.get("verilog", "") + synthesis_golden_verilog = eda_step.output.get("golden_verilog", "") else: # error create step, TBD pass diff --git a/chipcompiler/rtl2gds/__init__.py b/chipcompiler/rtl2gds/__init__.py index 8bea8cc6..3861a7b6 100644 --- a/chipcompiler/rtl2gds/__init__.py +++ b/chipcompiler/rtl2gds/__init__.py @@ -1,11 +1,15 @@ from .builder import ( build_rtl2gds_flow, build_harden_flow, - build_rcx_flow + build_rcx_flow, + build_synthesis_lec_flow, + build_post_route_lec_flow ) __all__ = [ 'build_rtl2gds_flow', 'build_harden_flow', - 'build_rcx_flow' + 'build_rcx_flow', + 'build_synthesis_lec_flow', + 'build_post_route_lec_flow' ] diff --git a/chipcompiler/rtl2gds/builder.py b/chipcompiler/rtl2gds/builder.py index 5ce8f0a1..ee09177c 100644 --- a/chipcompiler/rtl2gds/builder.py +++ b/chipcompiler/rtl2gds/builder.py @@ -35,3 +35,26 @@ def build_rcx_flow() -> list: steps.append((StepEnum.STA, "ecc", StateEnum.Unstart)) return steps + +def build_synthesis_lec_flow() -> list: + steps = [] + + steps.append((StepEnum.SYNTHESIS, "yosys", StateEnum.Unstart)) + steps.append((StepEnum.LEC, "yosys_lec", StateEnum.Unstart)) + + return steps + +def build_post_route_lec_flow() -> list: + steps = [] + + steps.append((StepEnum.SYNTHESIS, "yosys", StateEnum.Unstart)) + steps.append((StepEnum.FLOORPLAN, "ecc", StateEnum.Unstart)) + steps.append((StepEnum.NETLIST_OPT, "ecc", StateEnum.Unstart)) + steps.append((StepEnum.PLACEMENT, "dreamplace", StateEnum.Unstart)) + steps.append((StepEnum.CTS, "ecc", StateEnum.Unstart)) + steps.append((StepEnum.LEGALIZATION, "dreamplace", StateEnum.Unstart)) + steps.append((StepEnum.ROUTING, "ecc", StateEnum.Unstart)) + + steps.append((StepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart)) + + return steps diff --git a/chipcompiler/tools/yosys/builder.py b/chipcompiler/tools/yosys/builder.py index 5cdd6b1e..15a42e22 100644 --- a/chipcompiler/tools/yosys/builder.py +++ b/chipcompiler/tools/yosys/builder.py @@ -113,6 +113,7 @@ def generate_global_var_tcl(workspace: Workspace, # Output files set final_netlist_file {_tcl_quote(netlist_file)} +set golden_netlist_file {_tcl_quote(_abspath(step.output.get("golden_verilog", "")))} set timing_cell_stat_rpt {_tcl_quote(timing_cell_stat_rpt)} set timing_cell_count_rpt {_tcl_quote(timing_cell_count_rpt)} set generic_stat_json {_tcl_quote(generic_stat_json)} @@ -180,12 +181,14 @@ def build_step(workspace: Workspace, if output_verilog is None: output_verilog = f"{step.directory}/output/{workspace.design.name}_{step.name}.v" + golden_verilog = f"{step.directory}/output/{workspace.design.name}_{step_name}_golden.v" if output_def is None: output_def = f"{step.directory}/output/{workspace.design.name}_{step.name}.def.gz" step.output = { "dir": f"{step.directory}/output", "def": output_def, "verilog": output_verilog, + "golden_verilog": golden_verilog, "fixed_verilog": f"{step.directory}/output/{workspace.design.name}_{step.name}_fixed.v", "json": f"{step.directory}/output/{workspace.design.name}_{step.name}.json", "report": f"{step.directory}/output/{workspace.design.name}_{step.name}.rpt", diff --git a/chipcompiler/tools/yosys/runner.py b/chipcompiler/tools/yosys/runner.py index 6311c1d4..41d1c0f0 100644 --- a/chipcompiler/tools/yosys/runner.py +++ b/chipcompiler/tools/yosys/runner.py @@ -121,7 +121,11 @@ def run_step(workspace: Workspace, stderr=subprocess.STDOUT, ) - if os.path.exists(step.output["verilog"]): + output_exists = os.path.exists(step.output["verilog"]) + golden_path = step.output.get("golden_verilog", "") + golden_exists = not golden_path or os.path.exists(golden_path) + + if output_exists and golden_exists: sub_flow.update_step(step_name="run yosys", state=StateEnum.Success) fixed_netlist = step.output.get("fixed_verilog", "") @@ -140,7 +144,8 @@ def run_step(workspace: Workspace, sub_flow.update_step(step_name="run yosys", state=StateEnum.Invalid) print( - f"Error: Output netlist not generated at {step.output['verilog']}. " + f"Error: Output netlist not generated at {step.output['verilog']} " + f"or golden netlist not generated at {golden_path}. " f"yosys exit code: {result.returncode}" ) return False diff --git a/chipcompiler/tools/yosys/scripts/yosys_synthesis.tcl b/chipcompiler/tools/yosys/scripts/yosys_synthesis.tcl index a9a178e2..3c27732f 100644 --- a/chipcompiler/tools/yosys/scripts/yosys_synthesis.tcl +++ b/chipcompiler/tools/yosys/scripts/yosys_synthesis.tcl @@ -135,6 +135,10 @@ yosys select -write ${timing_cell_stat_rpt} t:*DFF* yosys tee -q -o ${timing_cell_count_rpt} select -count t:*DFF* yosys tee -q -a ${timing_cell_count_rpt} select -count */t:*_DLATCH*_ */t:*_SR*_ +if {[info exists golden_netlist_file] && $golden_netlist_file ne ""} { + yosys write_verilog -noattr -noexpr -nohex -nodec ${golden_netlist_file} +} + # yosys tee -q -o "${generic_stat_json}" stat -json -tech cmos # yosys tee -q -o "${generic_stat_json}.rpt" stat -tech cmos # ----------------------------------------------------------------------------- diff --git a/chipcompiler/tools/yosys_lec/__init__.py b/chipcompiler/tools/yosys_lec/__init__.py new file mode 100644 index 00000000..db3d8deb --- /dev/null +++ b/chipcompiler/tools/yosys_lec/__init__.py @@ -0,0 +1,18 @@ +from .builder import ( + build_step, + build_step_space, + build_step_config, +) + +from .runner import run_step +from .subflow import YosysLecSubFlow +from .utility import is_eda_exist + +__all__ = [ + "is_eda_exist", + "build_step", + "build_step_space", + "build_step_config", + "run_step", + "YosysLecSubFlow", +] diff --git a/chipcompiler/tools/yosys_lec/builder.py b/chipcompiler/tools/yosys_lec/builder.py new file mode 100644 index 00000000..906737b8 --- /dev/null +++ b/chipcompiler/tools/yosys_lec/builder.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +import os +import stat + +from chipcompiler.data import Workspace, WorkspaceStep + + +def _derive_golden_path(gate_verilog: str) -> str: + if not gate_verilog: + return "" + + directory = os.path.dirname(gate_verilog) + stem, ext = os.path.splitext(os.path.basename(gate_verilog)) + return os.path.join(directory, f"{stem}_golden{ext or '.v'}") + + +def _tcl_quote(value: str) -> str: + value = str(value) + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + + +def _tcl_list(values: list[str]) -> str: + return " ".join(_tcl_quote(value) for value in values if value) + + +def _bool_tcl(value: bool) -> str: + return "true" if value else "false" + + +def build_step( + workspace: Workspace, + step_name: str, + input_def: str, + input_verilog: str, + input_db: str | None = None, + output_def: str | None = None, + output_verilog: str | None = None, + output_gds: str | None = None, +) -> WorkspaceStep: + step = WorkspaceStep() + step.name = step_name + step.tool = "yosys_lec" + step.version = "0.1" + step.directory = f"{workspace.directory}/{step.name}_{step.tool}" + + step.input = { + "gate_verilog": input_verilog, + "golden_verilog": input_db or _derive_golden_path(input_verilog), + "db": input_db, + } + + step.output = { + "dir": f"{step.directory}/output", + "json": f"{step.directory}/output/{workspace.design.name}_{step.name}_result.json", + } + + step.data = { + "dir": f"{step.directory}/data", + "config": f"{step.directory}/data/lec_config.tcl", + } + + step.report = { + "dir": f"{step.directory}/report", + "status": f"{step.directory}/report/run_lec_status.rpt", + "equiv_status": f"{step.directory}/report/equiv_status.rpt", + "failed_rtlil": f"{step.directory}/report/equiv_failed.il", + "failed_verilog": f"{step.directory}/report/equiv_failed.v", + } + + step.log = { + "dir": f"{step.directory}/log", + "file": f"{step.directory}/log/{step.name}.log", + } + + step.script = { + "dir": f"{step.directory}/script", + "main": f"{step.directory}/script/run_lec.tcl", + } + + step.analysis = { + "dir": f"{step.directory}/analysis", + "metrics": f"{step.directory}/analysis/{step.name}_metrics.json", + } + + step.subflow = { + "path": f"{step.directory}/subflow.json", + "steps": [], + } + + step.checklist = { + "path": f"{step.directory}/checklist.json", + "checklist": [], + } + + return step + + +def build_step_space(step: WorkspaceStep) -> None: + os.makedirs(step.directory, exist_ok=True) + os.makedirs(step.output.get("dir", f"{step.directory}/output"), exist_ok=True) + os.makedirs(step.data.get("dir", f"{step.directory}/data"), exist_ok=True) + os.makedirs(step.report.get("dir", f"{step.directory}/report"), exist_ok=True) + os.makedirs(step.log.get("dir", f"{step.directory}/log"), exist_ok=True) + os.makedirs(step.script.get("dir", f"{step.directory}/script"), exist_ok=True) + os.makedirs(step.analysis.get("dir", f"{step.directory}/analysis"), exist_ok=True) + + +def _write_file(path: str, content: str) -> None: + with open(path, "w", encoding="utf-8") as handle: + handle.write(content) + try: + os.chmod(path, os.stat(path).st_mode | stat.S_IWUSR) + except OSError: + pass + + +def _lec_parameters(workspace: Workspace) -> dict: + data = getattr(getattr(workspace, "parameters", None), "data", {}) or {} + return data.get("LEC", {}) if isinstance(data.get("LEC", {}), dict) else {} + + +def build_step_config(workspace: Workspace, step: WorkspaceStep) -> None: + params = _lec_parameters(workspace) + model_files = params.get("model_files", []) + if isinstance(model_files, str): + model_files = [model_files] + + blacklist = str(params.get("blacklist", "") or "") + use_undef = bool(params.get("use_undef", False)) + liberty_files = getattr(workspace.pdk, "libs", []) or [] + + config = f"""# Auto-generated by yosys_lec.builder - DO NOT EDIT MANUALLY +set top_design {_tcl_quote(workspace.design.top_module)} +set golden_file {_tcl_quote(step.input.get("golden_verilog", ""))} +set gate_file {_tcl_quote(step.input.get("gate_verilog", ""))} +set report_dir {_tcl_quote(step.report.get("dir", ""))} +set result_json {_tcl_quote(step.output.get("json", ""))} +set status_file {_tcl_quote(step.report.get("status", ""))} +set equiv_status_file {_tcl_quote(step.report.get("equiv_status", ""))} +set failed_rtlil_file {_tcl_quote(step.report.get("failed_rtlil", ""))} +set failed_verilog_file {_tcl_quote(step.report.get("failed_verilog", ""))} +set liberty_files [list {_tcl_list(liberty_files)}] +set model_files [list {_tcl_list(model_files)}] +set blacklist_file {_tcl_quote(blacklist)} +set use_undef {_bool_tcl(use_undef)} +""" + _write_file(step.data["config"], config) + + script = """proc require_file {label path} { + if {$path eq ""} { + error "$label is empty" + } + if {![file exists $path]} { + error "missing $label: $path" + } + if {![file readable $path]} { + error "$label is not readable: $path" + } +} + +proc read_support_models {} { + global liberty_files model_files + foreach liberty_file $liberty_files { + require_file "liberty file" $liberty_file + yosys read_liberty -ignore_miss_func -ignore_miss_data_latch $liberty_file + } + foreach model_file $model_files { + require_file "LEC model file" $model_file + yosys read_verilog -sv $model_file + } +} + +proc normalize_design {top_design} { + yosys hierarchy -top $top_design + yosys proc + yosys memory + yosys async2sync + yosys flatten + yosys splitnets -ports -format __v + yosys opt_clean -purge +} + +proc build_design {stash_name top_design netlist_file} { + read_support_models + yosys read_verilog -sv $netlist_file + normalize_design $top_design + yosys design -stash $stash_name +} + +proc write_failure_artifacts {reason} { + global status_file equiv_status_file failed_rtlil_file failed_verilog_file + set handle [open $status_file "w"] + puts $handle "Yosys LEC did not prove equivalence." + puts $handle "" + puts $handle "Reason:" + puts $handle $reason + close $handle + + catch {yosys tee -o $equiv_status_file equiv_status} + catch {yosys write_rtlil $failed_rtlil_file} + catch {yosys write_verilog -noattr $failed_verilog_file} +} + +proc run_equivalence {} { + global top_design blacklist_file use_undef equiv_status_file + yosys design -copy-from gold -as gold $top_design + yosys design -copy-from gate -as gate $top_design + if {$blacklist_file ne ""} { + require_file "LEC blacklist" $blacklist_file + yosys equiv_make -blacklist $blacklist_file gold gate equiv + } else { + yosys equiv_make gold gate equiv + } + yosys hierarchy -top equiv + yosys opt_clean -purge + + if {$use_undef} { + yosys equiv_simple -undef + yosys equiv_induct -undef + } else { + yosys equiv_simple + yosys equiv_induct + } + yosys tee -o $equiv_status_file equiv_status + + set handle [open $equiv_status_file "r"] + set status_text [read $handle] + close $handle + if {![regexp {Equivalence successfully proven!} $status_text] + && ![regexp {Found a total of 0 unproven \$equiv cells\.} $status_text]} { + error "equivalence proof has unproven cells; see $equiv_status_file" + } +} + +set script_dir [file dirname [file normalize [info script]]] +source [file normalize [file join $script_dir .. data lec_config.tcl]] +file mkdir $report_dir + +require_file "golden netlist" $golden_file +require_file "gate netlist" $gate_file + +set result [catch { + build_design gold $top_design $golden_file + yosys design -reset + build_design gate $top_design $gate_file + yosys design -reset + run_equivalence +} message] + +if {$result != 0} { + write_failure_artifacts $message + exit 1 +} + +set handle [open $status_file "w"] +puts $handle "Yosys LEC completed with proven equivalence." +puts $handle "Golden: $golden_file" +puts $handle "Gate: $gate_file" +close $handle +""" + _write_file(step.script["main"], script) + + from chipcompiler.tools.yosys_lec.subflow import YosysLecSubFlow + + subflow = YosysLecSubFlow(workspace=workspace, workspace_step=step) + subflow.build_sub_flow() diff --git a/chipcompiler/tools/yosys_lec/runner.py b/chipcompiler/tools/yosys_lec/runner.py new file mode 100644 index 00000000..abbf8e47 --- /dev/null +++ b/chipcompiler/tools/yosys_lec/runner.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +import json +import os +import subprocess + +from chipcompiler.data import StateEnum, Workspace, WorkspaceStep +from chipcompiler.tools.yosys.utility import get_yosys_runtime +from chipcompiler.tools.yosys_lec.subflow import YosysLecSubFlow + + +def _status_is_proven(path: str) -> bool: + if not path or not os.path.exists(path): + return False + with open(path, encoding="utf-8", errors="ignore") as handle: + text = handle.read() + return ( + "Equivalence successfully proven!" in text + or "Found a total of 0 unproven $equiv cells." in text + ) + + +def _write_result(step: WorkspaceStep, proven: bool) -> None: + payload = { + "status": "proven" if proven else "incomplete", + "equiv_status": step.report.get("equiv_status", ""), + "status_report": step.report.get("status", ""), + } + with open(step.output["json"], "w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2) + handle.write("\n") + + +def run_step(workspace: Workspace, step: WorkspaceStep, ecc_module=None) -> bool: + sub_flow = YosysLecSubFlow(workspace=workspace, workspace_step=step) + + yosys_cmd, yosys_env = get_yosys_runtime() + if not yosys_cmd: + sub_flow.update_step(step_name="run lec", state=StateEnum.Invalid) + with open(step.log["file"], "w", encoding="utf-8") as log_file: + log_file.write("Error: yosys is not available.\n") + return False + + for label, path in ( + ("golden netlist", step.input.get("golden_verilog", "")), + ("gate netlist", step.input.get("gate_verilog", "")), + ): + if not path or not os.path.exists(path): + sub_flow.update_step(step_name="run lec", state=StateEnum.Invalid) + with open(step.log["file"], "w", encoding="utf-8") as log_file: + log_file.write(f"Error: missing {label}: {path}\n") + return False + + cmd = yosys_cmd + ["-Q", "-c", os.path.basename(step.script["main"])] + with open(step.log["file"], "w", encoding="utf-8") as log_file: + result = subprocess.run( + cmd, + cwd=step.script["dir"], + env=yosys_env, + stdout=log_file, + stderr=subprocess.STDOUT, + ) + + proven = result.returncode == 0 and _status_is_proven(step.report.get("equiv_status", "")) + if proven: + _write_result(step, proven=True) + sub_flow.update_step(step_name="run lec", state=StateEnum.Success) + sub_flow.update_step(step_name="analysis", state=StateEnum.Success) + return True + + sub_flow.update_step(step_name="run lec", state=StateEnum.Imcomplete) + return False diff --git a/chipcompiler/tools/yosys_lec/subflow.py b/chipcompiler/tools/yosys_lec/subflow.py new file mode 100644 index 00000000..f7063c54 --- /dev/null +++ b/chipcompiler/tools/yosys_lec/subflow.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +import os +import time + +from chipcompiler.data import StateEnum, Workspace, WorkspaceStep + + +class YosysLecSubFlow: + def __init__(self, workspace: Workspace, workspace_step: WorkspaceStep): + self.workspace = workspace + self.workspace_step = workspace_step + self.init_sub_flow() + self.start_time = time.time() + self.start_memory = self.get_peak_memory() + + def init_sub_flow(self): + from chipcompiler.utility import json_read + + data = json_read(self.workspace_step.subflow.get("path", "")) + if len(data) > 0: + self.workspace_step.subflow["steps"] = data.get("steps", []) + else: + self.build_sub_flow() + + def build_sub_flow(self) -> list: + def subflow_template(step_name: str): + return { + "name": step_name, + "state": StateEnum.Unstart.value, + "runtime": "", + "peak memory (mb)": 0, + "info": {}, + } + + self.workspace_step.subflow["steps"] = [ + subflow_template("run lec"), + subflow_template("analysis"), + ] + self.save() + return self.workspace_step.subflow["steps"] + + def save(self) -> bool: + from chipcompiler.utility import json_write + + return json_write( + file_path=self.workspace_step.subflow.get("path", ""), + data=self.workspace_step.subflow, + ) + + def get_runtime(self): + end_time = time.time() + elapsed_time = end_time - self.start_time + runtime = "{}:{}:{}".format( + int(elapsed_time // 3600), + int((elapsed_time % 3600) // 60), + int(elapsed_time % 60), + ) + self.start_time = end_time + return runtime + + def get_peak_memory(self): + peak_memory = 0 + try: + with open(f"/proc/{os.getpid()}/status") as handle: + for line in handle: + if line.startswith("VmRSS:"): + peak_memory = int(line.split()[1]) / 1024 + break + except Exception: + pass + return peak_memory + + def update_step(self, step_name: str, state: str | StateEnum, info: dict | None = None): + state = state.value if isinstance(state, StateEnum) else state + runtime = self.get_runtime() + peak_memory = self.get_peak_memory() - self.start_memory + peak_memory = 0 if peak_memory < 0 else round(peak_memory, 3) + info = info or {} + + for step_dict in self.workspace_step.subflow.get("steps", []): + if step_dict.get("name") == step_name: + step_dict["state"] = state + step_dict["runtime"] = runtime + step_dict["peak memory (mb)"] = peak_memory + step_dict["info"] = info + self.save() + self.workspace.home.update_monitor( + step=self.workspace_step.name, + sub_step=step_name, + memory=str(peak_memory), + runtime=runtime, + ) + break diff --git a/chipcompiler/tools/yosys_lec/utility.py b/chipcompiler/tools/yosys_lec/utility.py new file mode 100644 index 00000000..18476f50 --- /dev/null +++ b/chipcompiler/tools/yosys_lec/utility.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +from chipcompiler.tools.yosys.utility import is_eda_exist + +__all__ = ["is_eda_exist"] diff --git a/test/icsprout55-pdk b/test/icsprout55-pdk new file mode 160000 index 00000000..b8c8b30f --- /dev/null +++ b/test/icsprout55-pdk @@ -0,0 +1 @@ +Subproject commit b8c8b30fd16f68e9ae925b30cfb1fea327849ef5 diff --git a/test/test_tools_yosys_runner.py b/test/test_tools_yosys_runner.py index 6f13d65e..c1d0a045 100644 --- a/test/test_tools_yosys_runner.py +++ b/test/test_tools_yosys_runner.py @@ -14,6 +14,7 @@ def _build_workspace_and_step(tmp_path: Path): rtl_file.write_text("module top; endmodule\n") output_file = tmp_path / "output.v" + golden_file = tmp_path / "output_golden.v" log_file = tmp_path / "yosys.log" workspace = SimpleNamespace( @@ -23,16 +24,16 @@ def _build_workspace_and_step(tmp_path: Path): ) step = SimpleNamespace( input={"verilog": str(rtl_file)}, - output={"verilog": str(output_file)}, + output={"verilog": str(output_file), "golden_verilog": str(golden_file)}, log={"file": str(log_file)}, script={"dir": str(script_dir)}, directory=str(tmp_path) ) - return workspace, step, output_file, log_file + return workspace, step, output_file, golden_file, log_file def test_run_step_uses_local_env_and_runs_synthesis(tmp_path, monkeypatch): - workspace, step, output_file, _ = _build_workspace_and_step(tmp_path) + workspace, step, output_file, golden_file, _ = _build_workspace_and_step(tmp_path) runtime_env = {"PATH": "/opt/yosys/bin", "CUSTOM_ENV": "1"} updates = [] run_calls = [] @@ -68,6 +69,7 @@ def fake_run(cmd, cwd, env, stdout, stderr): }) if cmd == ["yosys", "yosys_synthesis.tcl"]: output_file.write_text("module top(); endmodule\n") + golden_file.write_text("module top(); endmodule\n") return SimpleNamespace(returncode=0) raise AssertionError(f"Unexpected command: {cmd}") @@ -93,7 +95,7 @@ def fake_run(cmd, cwd, env, stdout, stderr): def test_run_step_marks_invalid_when_slang_check_fails(tmp_path, monkeypatch): - workspace, step, _, log_file = _build_workspace_and_step(tmp_path) + workspace, step, _, _, log_file = _build_workspace_and_step(tmp_path) updates = [] run_calls = [] @@ -127,7 +129,7 @@ def fake_run(cmd, cwd, env, stdout, stderr): def test_run_step_marks_invalid_when_yosys_is_missing(tmp_path, monkeypatch): - workspace, step, _, log_file = _build_workspace_and_step(tmp_path) + workspace, step, _, _, log_file = _build_workspace_and_step(tmp_path) updates = [] class FakeSubFlow: diff --git a/test/yosys_lec/.gitignore b/test/yosys_lec/.gitignore new file mode 100644 index 00000000..483fb84a --- /dev/null +++ b/test/yosys_lec/.gitignore @@ -0,0 +1 @@ +/runs/ diff --git a/test/yosys_lec/test_tools_yosys_lec.py b/test/yosys_lec/test_tools_yosys_lec.py new file mode 100644 index 00000000..665f5283 --- /dev/null +++ b/test/yosys_lec/test_tools_yosys_lec.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +from pathlib import Path +from types import SimpleNamespace + +from chipcompiler.utility import json_write +from chipcompiler.data import ( + HomeData, + OriginDesign, + StateEnum, + StepEnum, + Workspace, + WorkspaceStep, + get_design_parameters, + get_pdk, +) +from chipcompiler.engine.flow import EngineFlow + + +REPO_ROOT = Path(__file__).resolve().parents[2] +ICS55_PDK_ROOT = REPO_ROOT / "test" / "icsprout55-pdk" +GCD_RTL = REPO_ROOT / "test" / "fixtures" / "gcd" / "gcd.v" + + +def _workspace(tmp_path: Path) -> Workspace: + parameters = get_design_parameters("ics55", "gcd") + pdk = get_pdk("ics55", pdk_root=str(ICS55_PDK_ROOT)) + + return Workspace( + directory=str(tmp_path / "ws"), + design=OriginDesign( + name=parameters.data["Design"], + top_module=parameters.data["Top module"], + origin_verilog=str(GCD_RTL), + ), + pdk=pdk, + parameters=parameters, + home=HomeData(), + ) + + +def _write_gcd_netlist_pair(gate: Path) -> None: + gate.parent.mkdir(parents=True) + gcd_text = GCD_RTL.read_text() + gate.write_text(gcd_text) + gate.with_name("gcd_Synthesis_golden.v").write_text(gcd_text) + + +def test_yosys_build_step_exposes_rtl_derived_golden_path(tmp_path): + from chipcompiler.tools.yosys import builder + + assert GCD_RTL.is_file() + + workspace = _workspace(tmp_path) + step = builder.build_step( + workspace=workspace, + step_name=StepEnum.SYNTHESIS.value, + input_def="", + input_verilog=str(GCD_RTL), + ) + + assert workspace.pdk.name == "ics55" + assert workspace.design.top_module == "gcd" + assert step.input["verilog"] == str(GCD_RTL) + assert step.output["verilog"].endswith("/gcd_Synthesis.v") + assert step.output["golden_verilog"].endswith("/gcd_Synthesis_golden.v") + + +def test_yosys_global_var_tcl_includes_golden_netlist_path(tmp_path): + from chipcompiler.tools.yosys import builder + + workspace = _workspace(tmp_path) + workspace.design.input_filelist = "" + step = builder.build_step( + workspace=workspace, + step_name=StepEnum.SYNTHESIS.value, + input_def="", + input_verilog=str(GCD_RTL), + ) + + tcl = builder.generate_global_var_tcl(workspace=workspace, step=step) + + assert f"set rtl_file [split {GCD_RTL}]" in tcl + assert "set top_design gcd" in tcl + assert "set golden_netlist_file" in tcl + assert step.output["golden_verilog"] in tcl + + +def test_lec_builder_derives_golden_from_gate_netlist_and_creates_workspace(tmp_path): + from chipcompiler.tools.yosys_lec import builder + + workspace = _workspace(tmp_path) + gate = tmp_path / "Synthesis_yosys" / "output" / "gcd_Synthesis.v" + + step = builder.build_step( + workspace=workspace, + step_name=StepEnum.LEC.value, + input_def="", + input_verilog=str(gate), + ) + builder.build_step_space(step) + + assert step.tool == "yosys_lec" + assert workspace.pdk.name == "ics55" + assert workspace.design.origin_verilog == str(GCD_RTL) + assert step.input["gate_verilog"] == str(gate) + assert step.input["golden_verilog"] == str(gate.with_name("gcd_Synthesis_golden.v")) + assert Path(step.script["main"]).name == "run_lec.tcl" + assert Path(step.output["json"]).name == "gcd_lec_result.json" + assert Path(step.script["dir"]).is_dir() + assert Path(step.report["dir"]).is_dir() + + +def test_lec_builder_accepts_explicit_golden_netlist(tmp_path): + from chipcompiler.tools.yosys_lec import builder + + workspace = _workspace(tmp_path) + golden = tmp_path / "Synthesis_yosys" / "output" / "gcd_Synthesis.v" + gate = tmp_path / "route_ecc" / "output" / "gcd_route.v" + + step = builder.build_step( + workspace=workspace, + step_name=StepEnum.POST_ROUTE_LEC.value, + input_def="", + input_verilog=str(gate), + input_db=str(golden), + ) + + assert step.input["gate_verilog"] == str(gate) + assert step.input["golden_verilog"] == str(golden) + assert Path(step.output["json"]).name == "gcd_postRouteLec_result.json" + + +def test_lec_build_step_config_writes_ics55_gcd_models_and_repo_local_script(tmp_path): + from chipcompiler.tools.yosys_lec import builder + + workspace = _workspace(tmp_path) + gate = tmp_path / "Synthesis_yosys" / "output" / "gcd_Synthesis.v" + _write_gcd_netlist_pair(gate) + + step = builder.build_step( + workspace=workspace, + step_name=StepEnum.LEC.value, + input_def="", + input_verilog=str(gate), + ) + builder.build_step_space(step) + builder.build_step_config(workspace=workspace, step=step) + + config = Path(step.data["config"]).read_text() + script = Path(step.script["main"]).read_text() + + assert str(gate) in config + assert str(gate.with_name("gcd_Synthesis_golden.v")) in config + for lib in workspace.pdk.libs: + assert Path(lib).is_file() + assert lib in config + assert "stdcell.lib" not in config + assert "set use_undef true" in config + assert "splitnets -ports -format __v" in script + assert "equiv_make" in script + assert "/home/zhaoxueyan/code/yosys-flow1" not in script + + +def test_lec_runner_marks_success_from_yosys_status(tmp_path, monkeypatch): + from chipcompiler.tools.yosys_lec import builder, runner + + workspace = _workspace(tmp_path) + gate = tmp_path / "Synthesis_yosys" / "output" / "gcd_Synthesis.v" + _write_gcd_netlist_pair(gate) + + step = builder.build_step( + workspace=workspace, + step_name=StepEnum.LEC.value, + input_def="", + input_verilog=str(gate), + ) + builder.build_step_space(step) + builder.build_step_config(workspace=workspace, step=step) + + updates = [] + + class FakeSubFlow: + def __init__(self, workspace, workspace_step): + pass + + def update_step(self, step_name, state, info=None): + updates.append((step_name, state)) + + def fake_run(cmd, cwd, env, stdout, stderr): + Path(step.report["equiv_status"]).write_text("Equivalence successfully proven!\n") + return SimpleNamespace(returncode=0) + + monkeypatch.setattr(runner, "YosysLecSubFlow", FakeSubFlow) + monkeypatch.setattr(runner, "get_yosys_runtime", lambda: (["yosys"], {"PATH": "/tmp"})) + monkeypatch.setattr(runner.subprocess, "run", fake_run) + + assert runner.run_step(workspace=workspace, step=step) is True + assert ("run lec", StateEnum.Success) in updates + assert Path(step.output["json"]).exists() + + +def test_engine_flow_accepts_lec_result_json(tmp_path): + result_json = tmp_path / "lec_result.json" + result_json.write_text('{"status": "proven"}\n') + step = WorkspaceStep(name=StepEnum.LEC.value, output={"json": str(result_json)}) + + assert EngineFlow(workspace=None).check_step_result(step) is True + + +def test_post_route_lec_flow_appends_lec_after_route(): + from chipcompiler.rtl2gds import build_post_route_lec_flow + + steps = build_post_route_lec_flow() + + assert steps[-2][0] == StepEnum.ROUTING + assert steps[-1] == (StepEnum.POST_ROUTE_LEC, "yosys_lec", StateEnum.Unstart) + + +def test_engine_flow_wires_post_route_lec_against_synthesis_gate(tmp_path, monkeypatch): + import chipcompiler.tools as tools + + workspace = _workspace(tmp_path) + workspace.flow.path = str(tmp_path / "flow.json") + workspace.flow.data = { + "steps": [ + {"name": StepEnum.SYNTHESIS.value, "tool": "yosys", "state": StateEnum.Unstart.value}, + {"name": StepEnum.ROUTING.value, "tool": "ecc", "state": StateEnum.Unstart.value}, + { + "name": StepEnum.POST_ROUTE_LEC.value, + "tool": "yosys_lec", + "state": StateEnum.Unstart.value, + }, + ] + } + json_write(workspace.flow.path, workspace.flow.data) + + def fake_create_step( + workspace, + step, + eda, + input_def, + input_verilog, + input_db=None, + **kwargs, + ): + workspace_step = WorkspaceStep() + workspace_step.name = step + workspace_step.tool = eda + workspace_step.input = { + "def": input_def, + "verilog": input_verilog, + "db": input_db, + } + workspace_step.output = { + "def": str(tmp_path / step / "output" / f"gcd_{step}.def.gz"), + "verilog": str(tmp_path / step / "output" / f"gcd_{step}.v"), + "golden_verilog": str(tmp_path / step / "output" / f"gcd_{step}_golden.v"), + "db": str(tmp_path / step / "output" / f"gcd_{step}_db"), + } + if eda == "yosys_lec": + workspace_step.input = { + "gate_verilog": input_verilog, + "golden_verilog": input_db, + "db": input_db, + } + return workspace_step + + monkeypatch.setattr(tools, "create_step", fake_create_step) + + engine_flow = EngineFlow(workspace=workspace) + engine_flow.create_step_workspaces() + + synth_step, route_step, lec_step = engine_flow.workspace_steps + assert synth_step.name == StepEnum.SYNTHESIS.value + assert route_step.name == StepEnum.ROUTING.value + assert lec_step.name == StepEnum.POST_ROUTE_LEC.value + assert lec_step.input["gate_verilog"] == route_step.output["verilog"] + assert lec_step.input["golden_verilog"] == synth_step.output["verilog"]