diff --git a/.gitignore b/.gitignore index 438bbd0..d6f53a2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ result/ # Temp iii config files generated by bbdev script mode api/iii-config-*.yaml api/data/ + +api/steps/ip/scripts/_smoke_out/ diff --git a/api/steps/bebop/p2e/03_runworkload_event.step.py b/api/steps/bebop/p2e/03_runworkload_event.step.py index bd94fad..639a683 100644 --- a/api/steps/bebop/p2e/03_runworkload_event.step.py +++ b/api/steps/bebop/p2e/03_runworkload_event.step.py @@ -19,7 +19,7 @@ sys.path.insert(0, utils_path) from utils.event_common import require_chip -from utils.path import bebop_cargo_env, chip_output_root, get_buckyball_path, rtl_dir, log_dir +from utils.path import bebop_cargo_env, chip_output_root, get_buckyball_path, log_dir, rtl_dir from utils.stream_run import stream_run_logger_async from utils.event_common import check_result, get_origin_trace_id diff --git a/api/steps/compiler/scripts/build.py b/api/steps/compiler/scripts/build.py index bba660b..74b7c15 100644 --- a/api/steps/compiler/scripts/build.py +++ b/api/steps/compiler/scripts/build.py @@ -1,5 +1,6 @@ from __future__ import annotations +import fcntl import os import shlex import shutil @@ -7,13 +8,6 @@ from pathlib import Path -def _repo(raw: str | Path) -> Path: - root = Path(raw).resolve() - if not root.is_dir(): - raise ValueError(f"repo does not exist: {root}") - return root - - def _run( cmd: list[str], *, @@ -48,7 +42,7 @@ def _run( def compiler_python(repo: str | Path) -> str: - root = _repo(repo) + root = Path(repo).resolve() candidates = [root / "result" / "bin" / "python3"] path_python = shutil.which("python3") if path_python: @@ -68,7 +62,7 @@ def compiler_python(repo: str | Path) -> str: def build_llvm( repo: str | Path, *, logger: object | None = None, task_scope: str | None = None ) -> Path: - root = _repo(repo) + root = Path(repo).resolve() buddy = root / "compiler" / "thirdparty" / "buddy-mlir" llvm_src = buddy / "llvm" / "llvm" llvm_build = buddy / "llvm" / "build" @@ -95,23 +89,26 @@ def build_llvm( f"-DPython3_EXECUTABLE={python}", f"-DPython_EXECUTABLE={python}", ] - if not (llvm_build / "build.ninja").is_file(): + llvm_build.mkdir(parents=True, exist_ok=True) + with open(llvm_build / ".bbdev.lock", "a+b") as lockf: + fcntl.flock(lockf.fileno(), fcntl.LOCK_EX) + if not (llvm_build / "build.ninja").is_file(): + _run( + cmake, + repo=root, + cwd=buddy, + logger=logger, + task_scope=task_scope, + output_prefix="compiler llvm configure", + ) _run( - cmake, + ["ninja", "-C", str(llvm_build), "-j", str(os.cpu_count() or 1)], repo=root, cwd=buddy, logger=logger, task_scope=task_scope, - output_prefix="compiler llvm configure", + output_prefix="compiler llvm build", ) - _run( - ["ninja", "-C", str(llvm_build), "-j", str(os.cpu_count() or 1)], - repo=root, - cwd=buddy, - logger=logger, - task_scope=task_scope, - output_prefix="compiler llvm build", - ) mlir_cmake = llvm_build / "lib" / "cmake" / "mlir" if not mlir_cmake.is_dir(): raise RuntimeError(f"LLVM/MLIR build failed: missing {mlir_cmake}") @@ -119,7 +116,7 @@ def build_llvm( def compiler_build_dir(repo: str | Path, chip: str) -> Path: - return _repo(repo) / "compiler" / "thirdparty" / "buddy-mlir" / "build" / chip + return Path(repo).resolve() / "compiler" / "thirdparty" / "buddy-mlir" / "build" / chip def build_compiler( @@ -129,7 +126,7 @@ def build_compiler( logger: object | None = None, task_scope: str | None = None, ) -> Path: - root = _repo(repo) + root = Path(repo).resolve() if not chip: raise ValueError("chip is required") chip_pb = root / "examples" / "chips" / chip / "configs" / "generated" / "chip.pb" diff --git a/api/steps/dc/03_area_event.step.py b/api/steps/dc/01_area_event.step.py similarity index 76% rename from api/steps/dc/03_area_event.step.py rename to api/steps/dc/01_area_event.step.py index 5edecfe..2705f4a 100644 --- a/api/steps/dc/03_area_event.step.py +++ b/api/steps/dc/01_area_event.step.py @@ -1,3 +1,4 @@ +import json import os import shlex import shutil @@ -12,14 +13,14 @@ scripts_path = os.path.join(os.path.dirname(__file__), "scripts") if scripts_path not in sys.path: sys.path.insert(0, scripts_path) -step_path = os.path.dirname(__file__) -if step_path not in sys.path: - sys.path.insert(0, step_path) +scripts_path = os.path.join(os.path.dirname(__file__), "scripts") +if scripts_path not in sys.path: + sys.path.insert(0, scripts_path) from utils.event_common import check_result, get_origin_trace_id from utils.path import get_buckyball_path from utils.stream_run import stream_run_logger_async -from tapeout import get_tapeout_contract, technology_settings, write_run_tcl +from tapeout import clock_period_ns, get_tapeout_contract, write_run_tcl config = { "name": "dc-area", @@ -83,7 +84,27 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: os.makedirs(output_dir, exist_ok=True) os.makedirs(report_dir, exist_ok=True) try: - tech = technology_settings() + gman = input_data.get("generate_manifest") + if not isinstance(gman, str) or not gman: + rman = input_data.get("replace_manifest") + if isinstance(rman, str) and os.path.isfile(rman): + with open(rman, encoding="utf-8") as handle: + gman = json.load(handle).get("generate_manifest") + if not isinstance(gman, str) or not os.path.isfile(gman): + raise ValueError("missing generate_manifest with link_dbs") + with open(gman, encoding="utf-8") as handle: + link_dbs = json.load(handle).get("link_dbs") + if not isinstance(link_dbs, list) or not link_dbs: + raise ValueError(f"generate_manifest.link_dbs empty: {gman}") + for path in link_dbs: + if not isinstance(path, str) or not os.path.isfile(path): + raise ValueError(f"sram db missing: {path}") + tech = { + "target_library": contract.target_library, + "synthetic_library": contract.synthetic_library, + "link_library": list(contract.link_library) + list(link_dbs), + "max_cores": contract.max_cores, + } run_config = write_run_tcl( os.path.join(analysis_dir, "run.tcl"), { @@ -91,12 +112,11 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: "source_list": source_list_path, "output_dir": output_dir, "report_dir": report_dir, - "clock_port": contract.clock_port, - "clock_period_ns": contract.clock_period_ns, + "sdc": str(contract.constraints_sdc), **tech, }, ) - except (OSError, ValueError) as exc: + except (OSError, ValueError, json.JSONDecodeError) as exc: await check_result(ctx, 1, continue_run=False, extra_fields={"task": "dc", "error": str(exc)}, trace_id=origin_tid) return script_path = contract.dc_script @@ -104,9 +124,9 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: ctx.logger.info(f"Running chip-owned DC synthesis for {contract.chip}, top {top_module}: {script_path}") result = await stream_run_logger_async( cmd=( - f"dc_shell -f {shlex.quote(str(script_path))} " + f"set -o pipefail; dc_shell -f {shlex.quote(str(script_path))} " f"-x {shlex.quote('set RUN_CONFIG ' + '{' + str(run_config) + '}')} " - f"> {shlex.quote(dc_log)} 2>&1" + f"2>&1 | tee {shlex.quote(dc_log)}" ), logger=ctx.logger, cwd=os.path.dirname(script_path), @@ -123,7 +143,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: "run_config": str(run_config), "chip": contract.chip, "tapeout_dir": str(contract.root), - "sram_manifest": (input_data.get("sram_collateral") or {}).get("sram_manifest"), + "replace_manifest": input_data.get("replace_manifest"), } if result.returncode == 0 and input_data.get("from_regression_area_power"): @@ -153,7 +173,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: trace_id=origin_tid, ) return - freq = 1000.0 / contract.clock_period_ns + freq = 1000.0 / clock_period_ns(contract) merge_metrics(get_buckyball_path(), area=area, freq=freq) extra_fields["area"] = area extra_fields["freq"] = freq diff --git a/api/steps/dc/05_sim_event.step.py b/api/steps/dc/03_sim_event.step.py similarity index 92% rename from api/steps/dc/05_sim_event.step.py rename to api/steps/dc/03_sim_event.step.py index ac1b8ef..8631131 100644 --- a/api/steps/dc/05_sim_event.step.py +++ b/api/steps/dc/03_sim_event.step.py @@ -8,9 +8,9 @@ utils_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) if utils_path not in sys.path: sys.path.insert(0, utils_path) -step_path = os.path.dirname(__file__) -if step_path not in sys.path: - sys.path.insert(0, step_path) +scripts_path = os.path.join(os.path.dirname(__file__), "scripts") +if scripts_path not in sys.path: + sys.path.insert(0, scripts_path) from utils.event_common import check_result, get_origin_trace_id from utils.path import get_buckyball_path, workload_build_dir, workload_tests_root @@ -23,7 +23,7 @@ "description": "rerun chip-owned simulation and produce activity for PTPX", "flows": ["dc"], "triggers": [queue("dc.sim")], - "enqueues": ["dc.power"], + "enqueues": ["pt.run"], } @@ -62,7 +62,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: if not os.path.isfile(explicit_activity): await check_result(ctx, 1, continue_run=False, extra_fields={"task": "sim", "error": f"activity file does not exist: {explicit_activity}"}, trace_id=origin_tid) return - await ctx.enqueue({"topic": "dc.power", "data": {**input_data, "activity": explicit_activity, "format": activity_format, "_trace_id": origin_tid}}) + await ctx.enqueue({"topic": "pt.run", "data": {**input_data, "activity": explicit_activity, "format": activity_format, "_trace_id": origin_tid}}) return if shutil.which("bash") is None: @@ -157,4 +157,4 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: trace_id=origin_tid, ) if result.returncode == 0: - await ctx.enqueue({"topic": "dc.power", "data": {**input_data, "activity": activity_path, "format": activity_format, "strip_path": contract.power_strip_path, "_trace_id": origin_tid}}) + await ctx.enqueue({"topic": "pt.run", "data": {**input_data, "activity": activity_path, "format": activity_format, "strip_path": contract.power_strip_path, "_trace_id": origin_tid}}) diff --git a/api/steps/dc/scripts/dc.tcl b/api/steps/dc/scripts/dc.tcl deleted file mode 100755 index b525d38..0000000 --- a/api/steps/dc/scripts/dc.tcl +++ /dev/null @@ -1,56 +0,0 @@ -# Shared Design Compiler implementation. Chip tapeout/dc.tcl files are thin -# ownership wrappers; all RUN_* values are supplied by bbdev. -if {![info exists RUN_CONFIG] || $RUN_CONFIG eq ""} { - error "bbdev must pass -x {set RUN_CONFIG }" -} -source [file normalize $RUN_CONFIG] - -proc bbdev_read_filelist {path} { - set fh [open $path r] - set files [list] - while {[gets $fh line] >= 0} { - set line [string trim $line] - if {$line eq "" || [string match "#*" $line]} { continue } - lappend files [file normalize $line] - } - close $fh - if {[llength $files] == 0} { error "empty DC source list: $path" } - return $files -} - -set top $RUN_TOP -set output_dir [file normalize $RUN_OUTPUT_DIR] -set report_dir [file normalize $RUN_REPORT_DIR] -file mkdir $output_dir $report_dir [file join $report_dir work] -set target_library [list $RUN_TARGET_LIBRARY] -set synthetic_library $RUN_SYNTHETIC_LIBRARY -set link_library [concat [list *] $target_library $synthetic_library $RUN_LINK_LIBRARY] -set_host_options -max_cores $RUN_MAX_CORES -define_design_lib WORK -path [file join $report_dir work] -set search_path [list .] -set_app_var verilogout_no_tri true -set_app_var verilogout_equation false -analyze -format sverilog -define {SYNTHESIS DC_SYN} [bbdev_read_filelist $RUN_SOURCE_LIST] -elaborate $top -current_design $top -link -set bb_clock [get_ports -quiet $RUN_CLOCK_PORT] -if {[sizeof_collection $bb_clock] != 1} { error "clock port '$RUN_CLOCK_PORT' was not found exactly once" } -create_clock -name bb_clock -period $RUN_CLOCK_PERIOD_NS $bb_clock -set_clock_uncertainty [expr {$RUN_CLOCK_PERIOD_NS * 0.30}] [get_clocks bb_clock] -set_clock_transition [expr {$RUN_CLOCK_PERIOD_NS * 0.10}] [get_clocks bb_clock] -set_input_delay [expr {$RUN_CLOCK_PERIOD_NS * 0.70}] -clock bb_clock [remove_from_collection [all_inputs] $bb_clock] -set_output_delay [expr {$RUN_CLOCK_PERIOD_NS * 0.70}] -clock bb_clock [all_outputs] -set_load 2.0 [all_outputs] -compile_ultra -area_high_effort_script -no_autoungroup -no_boundary_optimization -set_fix_multiple_port_nets -all -buffer_constants -change_names -hierarchy -rules verilog -write -format ddc -hierarchy -output [file join $output_dir ${top}.ddc] -write -format verilog -hierarchy -output [file join $output_dir ${top}.v] -write_sdc [file join $output_dir ${top}.sdc] -report_constraint -all_violators > [file join $report_dir constraint.rpt] -report_timing -delay max -max_paths 50 > [file join $report_dir timing_max.rpt] -report_timing -delay min -max_paths 50 > [file join $report_dir timing_min.rpt] -report_area -hierarchy > [file join $report_dir area.rpt] -report_reference > [file join $report_dir reference.rpt] -exit diff --git a/api/steps/dc/scripts/tapeout.py b/api/steps/dc/scripts/tapeout.py new file mode 100644 index 0000000..8bf1a4f --- /dev/null +++ b/api/steps/dc/scripts/tapeout.py @@ -0,0 +1,145 @@ +"""Resolve chip-owned tapeout flow contracts.""" + +from __future__ import annotations + +import os +import re +import shlex +import tomllib +from dataclasses import dataclass +from pathlib import Path + + +def _tcl_word(value: str) -> str: + return "{" + str(value).replace("\\", "\\\\").replace("}", "\\}") + "}" + + +def _tcl_list(values: list[str]) -> str: + return "[list " + " ".join(_tcl_word(value) for value in values) + "]" + + +@dataclass(frozen=True) +class SramGeom: + name: str + words: int + mux: int + bits: int + + +@dataclass(frozen=True) +class TapeoutContract: + chip: str + root: Path + dc_script: Path + constraints_sdc: Path + power_script: Path + power_sim_script: Path + sram_mdf: Path + top_module: str + target_library: str + synthetic_library: list[str] + link_library: list[str] + max_cores: int + power_format: str + power_start_ns: str | None + power_end_ns: str | None + power_workload: str | None + power_strip_path: str + sram_process: str + sram_corner: str + lc_shell: Path + sram_table: dict[str, SramGeom] + + +def _p(root: Path, value: str) -> Path: + p = Path(value) + return p if p.is_absolute() else root / p + + +def resolve_power_window( + contract: TapeoutContract, start_ns: object | None, end_ns: object | None +) -> tuple[str | None, str | None]: + start = str(start_ns) if start_ns is not None and str(start_ns) != "" else contract.power_start_ns + end = str(end_ns) if end_ns is not None and str(end_ns) != "" else contract.power_end_ns + if (start is None) != (end is None): + raise ValueError("power start_ns and end_ns must be supplied together") + if start is not None: + try: + if float(start) < 0 or float(start) >= float(end): + raise ValueError + except ValueError as exc: + raise ValueError("power start_ns must be smaller than end_ns") from exc + return start, end + + +def clock_period_ns(contract: TapeoutContract) -> float: + text = contract.constraints_sdc.read_text(encoding="utf-8") + match = re.search(r"create_clock\b[^;\n]*-period\s+([0-9]+(?:\.[0-9]+)?)", text) + if match is None: + raise ValueError(f"no create_clock -period in {contract.constraints_sdc}") + period = float(match.group(1)) + if period <= 0: + raise ValueError(f"invalid create_clock -period in {contract.constraints_sdc}") + return period + + +def get_tapeout_contract(bbdir: str | os.PathLike[str], chip: str, top: str | None = None) -> TapeoutContract: + root = Path(bbdir) / "examples" / "chips" / chip / "tapeout" + with (root / "config.toml").open("rb") as f: + s = tomllib.load(f)["tapeout"] + + sram_table = { + row["name"]: SramGeom(row["name"], row["words"], row["mux"], row["bits"]) + for row in s["sram"] + } + + pw = s.get("power_workload") + return TapeoutContract( + chip=chip, + root=root, + dc_script=_p(root, s["dc_script"]), + constraints_sdc=_p(root, s["constraints_sdc"]), + power_script=_p(root, s["power_script"]), + power_sim_script=_p(root, s["power_sim_script"]), + sram_mdf=_p(root, s["sram_mdf"]), + top_module=top or s["top"], + target_library=str(_p(root, s["target_library"])), + synthetic_library=[str(_p(root, p)) for p in s["synthetic_library"]], + link_library=[str(_p(root, p)) for p in s.get("link_library", [])], + max_cores=s["max_cores"], + power_format=s["power_format"], + power_start_ns=s.get("power_start_ns"), + power_end_ns=s.get("power_end_ns"), + power_workload=None if pw in (None, "") else str(pw), + power_strip_path=s.get("power_strip_path", ""), + sram_process=s["sram_process"], + sram_corner=s["sram_corner"], + lc_shell=_p(root, s["lc_shell"]), + sram_table=sram_table, + ) + + +def write_run_tcl(path: str | os.PathLike[str], values: dict[str, object]) -> Path: + output = Path(path) + output.parent.mkdir(parents=True, exist_ok=True) + lines = [] + for key, value in values.items(): + name = "RUN_" + key.upper() + if isinstance(value, list): + lines.append(f"set {name} {_tcl_list([str(item) for item in value])}") + elif value is None: + lines.append(f"set {name} {{}}") + elif isinstance(value, (int, float)): + lines.append(f"set {name} {value}") + else: + lines.append(f"set {name} {_tcl_word(str(value))}") + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + return output + + +def write_run_env(path: str | os.PathLike[str], values: dict[str, object]) -> Path: + output = Path(path) + output.parent.mkdir(parents=True, exist_ok=True) + lines = [f"{key}={shlex.quote(str(value))}" for key, value in values.items() if value is not None] + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + return output diff --git a/api/steps/dc/tapeout.py b/api/steps/dc/tapeout.py deleted file mode 100644 index fd829a8..0000000 --- a/api/steps/dc/tapeout.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Resolve chip-owned tapeout flow contracts.""" - -from __future__ import annotations - -import os -import shlex -import tomllib -from dataclasses import dataclass -from pathlib import Path - -def _tcl_word(value: str) -> str: - return "{" + str(value).replace("\\", "\\\\").replace("}", "\\}") + "}" - - -def _tcl_list(values: list[str]) -> str: - return "[list " + " ".join(_tcl_word(value) for value in values) + "]" - - -@dataclass(frozen=True) -class TapeoutContract: - chip: str - root: Path - dc_script: Path - power_script: Path - power_sim_script: Path - top_module: str - clock_port: str - clock_period_ns: float - power_format: str - power_start_ns: str | None - power_end_ns: str | None - power_workload: str | None - power_strip_path: str - - -def resolve_power_window( - contract: TapeoutContract, start_ns: object | None, end_ns: object | None -) -> tuple[str | None, str | None]: - """Merge optional CLI window bounds with a chip default and validate them.""" - start = str(start_ns) if start_ns is not None and str(start_ns) != "" else contract.power_start_ns - end = str(end_ns) if end_ns is not None and str(end_ns) != "" else contract.power_end_ns - if (start is None) != (end is None): - raise ValueError("power start_ns and end_ns must be supplied together") - if start is not None: - try: - if float(start) < 0 or float(start) >= float(end): - raise ValueError - except ValueError as exc: - raise ValueError("power start_ns must be smaller than end_ns") from exc - return start, end - - -def _read_config(root: Path) -> dict: - path = root / "config.toml" - if not path.is_file(): - return {} - with path.open("rb") as handle: - data = tomllib.load(handle) - section = data.get("tapeout", data) - if not isinstance(section, dict): - raise ValueError(f"{path} must contain a [tapeout] table") - return section - - -def get_tapeout_contract(bbdir: str | os.PathLike[str], chip: str, top: str | None = None) -> TapeoutContract: - """Resolve the tapeout scripts owned by the chip.""" - if not isinstance(chip, str) or not chip: - raise ValueError("Missing required parameter: --chip") - root = Path(bbdir) / "examples" / "chips" / chip / "tapeout" - if not root.is_dir(): - raise ValueError(f"chip {chip} has no tapeout directory: {root}") - - settings = _read_config(root) - top_module = top or settings.get("top", "DigitalTop") - clock_port = str(settings.get("clock_port", "")).strip() - if not clock_port: - raise ValueError(f"missing tapeout.clock_port in {root / 'config.toml'}") - try: - clock_period_ns = float(settings.get("clock_period_ns")) - except (TypeError, ValueError) as exc: - raise ValueError(f"invalid tapeout.clock_period_ns in {root / 'config.toml'}") from exc - if clock_period_ns <= 0: - raise ValueError(f"tapeout.clock_period_ns must be positive in {root / 'config.toml'}") - - power_format = str(settings.get("power_format", "fsdb")).lower() - if power_format not in {"saif", "vcd", "fsdb"}: - raise ValueError(f"unsupported tapeout.power_format in {root / 'config.toml'}: {power_format}") - - def optional_string(name: str) -> str | None: - value = settings.get(name) - return None if value is None or str(value).strip() == "" else str(value) - - power_start_ns = optional_string("power_start_ns") - power_end_ns = optional_string("power_end_ns") - if (power_start_ns is None) != (power_end_ns is None): - raise ValueError(f"tapeout power_start_ns and power_end_ns must be set together in {root / 'config.toml'}") - if power_start_ns is not None: - try: - if float(power_start_ns) < 0 or float(power_start_ns) >= float(power_end_ns): - raise ValueError - except ValueError as exc: - raise ValueError(f"invalid tapeout power window in {root / 'config.toml'}") from exc - - scripts = { - "dc_script": root / str(settings.get("dc_script", "dc.tcl")), - "power_script": root / str(settings.get("power_script", "power.tcl")), - "power_sim_script": root / str(settings.get("power_sim_script", "power_sim.sh")), - } - missing = [str(path) for path in scripts.values() if not path.is_file()] - if missing: - raise ValueError(f"chip {chip} tapeout contract is missing: {', '.join(missing)}") - - return TapeoutContract( - chip=chip, - root=root, - dc_script=scripts["dc_script"], - power_script=scripts["power_script"], - power_sim_script=scripts["power_sim_script"], - top_module=str(top_module), - clock_port=clock_port, - clock_period_ns=clock_period_ns, - power_format=power_format, - power_start_ns=power_start_ns, - power_end_ns=power_end_ns, - power_workload=optional_string("power_workload"), - power_strip_path=str(settings.get("power_strip_path", "")), - ) - - -def technology_settings() -> dict[str, object]: - """Read the small technology contract exported by the host setup.""" - target = os.environ.get("TARGET_LIBRARY", "").strip() - synthetic = [item for item in os.environ.get("SYNTHETIC_LIBRARY", "").split(os.pathsep) if item] - link = [item for item in os.environ.get("LINK_LIBRARY", "").split(os.pathsep) if item] - if not target: - raise ValueError("missing TARGET_LIBRARY; export it in zshrc") - if any(ch.isspace() for ch in target): - raise ValueError( - "TARGET_LIBRARY contains whitespace/newlines; " - f"fix the zshrc export (got {target!r})" - ) - if not Path(target).is_file(): - raise ValueError(f"TARGET_LIBRARY does not exist: {target}") - missing = [item for item in synthetic + link if not Path(item).is_file()] - if missing: - raise ValueError("technology library does not exist: " + ", ".join(missing)) - if not synthetic: - raise ValueError("missing SYNTHETIC_LIBRARY; export it in zshrc") - return { - "target_library": target, - "synthetic_library": synthetic, - "link_library": link, - "max_cores": 8, - } - - -def write_run_tcl(path: str | os.PathLike[str], values: dict[str, object]) -> Path: - """Write a Tcl variable file consumed by chip-owned DC/PT scripts.""" - output = Path(path) - output.parent.mkdir(parents=True, exist_ok=True) - lines = [] - for key, value in values.items(): - name = "RUN_" + key.upper() - if isinstance(value, list): - lines.append(f"set {name} {_tcl_list([str(item) for item in value])}") - elif value is None: - lines.append(f"set {name} {{}}") - elif isinstance(value, (int, float)): - lines.append(f"set {name} {value}") - else: - lines.append(f"set {name} {_tcl_word(str(value))}") - output.write_text("\n".join(lines) + "\n", encoding="utf-8") - return output - - -def write_run_env(path: str | os.PathLike[str], values: dict[str, object]) -> Path: - """Write shell assignments for a chip-owned power simulation wrapper.""" - output = Path(path) - output.parent.mkdir(parents=True, exist_ok=True) - lines = [f"{key}={shlex.quote(str(value))}" for key, value in values.items() if value is not None] - output.write_text("\n".join(lines) + "\n", encoding="utf-8") - return output diff --git a/api/steps/ip-replace/01_run_api.step.py b/api/steps/ip-replace/01_run_api.step.py deleted file mode 100644 index 03afddc..0000000 --- a/api/steps/ip-replace/01_run_api.step.py +++ /dev/null @@ -1,33 +0,0 @@ -from motia import ApiRequest, ApiResponse, FlowContext, api - - -def req_arg(body: dict, name: str): - return body.get(name) or body.get(name.replace("_", "-")) - - -config = { - "name": "ip-replace-api", - "description": "prepare top-scoped synthesis RTL and SRAM metadata", - "flows": ["ip-replace"], - "triggers": [api("POST", "/ip/replace/run")], - "enqueues": ["ip-replace.run"], -} - - -async def handler(req: ApiRequest, ctx: FlowContext) -> ApiResponse: - body = req.body or {} - source_list = req_arg(body, "source_list") - if not isinstance(source_list, str) or not source_list: - return ApiResponse(status=400, body={"error": "source_list is required"}) - output_dir = req_arg(body, "output_dir") - if not isinstance(output_dir, str) or not output_dir: - return ApiResponse(status=400, body={"error": "output_dir is required"}) - - data = { - "source_list": source_list, - "ip_replace_output_dir": output_dir, - "top": req_arg(body, "top"), - "consumer": req_arg(body, "consumer") or "generic", - } - await ctx.enqueue({"topic": "ip-replace.run", "data": {**data, "_trace_id": ctx.trace_id}}) - return ApiResponse(status=202, body={"trace_id": ctx.trace_id}) diff --git a/api/steps/ip-replace/01_run_event.step.py b/api/steps/ip-replace/01_run_event.step.py deleted file mode 100644 index 09aa4c5..0000000 --- a/api/steps/ip-replace/01_run_event.step.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -import sys - -from motia import FlowContext, queue - -utils_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -if utils_path not in sys.path: - sys.path.insert(0, utils_path) - -from utils.event_common import check_result, get_origin_trace_id - -scripts_path = os.path.join(os.path.dirname(__file__), "scripts") -if scripts_path not in sys.path: - sys.path.insert(0, scripts_path) - -from sram_replace import prepare_sram_collateral - - -config = { - "name": "ip-replace", - "description": "prepare top-scoped synthesis RTL and SRAM metadata", - "flows": ["ip-replace", "dc", "yosys"], - "triggers": [queue("ip-replace.run")], - "enqueues": ["dc.area", "yosys.synth"], -} - - -def default_source_list(input_data: dict) -> str | None: - source_list = input_data.get("source_list") - if isinstance(source_list, str) and source_list: - return source_list - build_dir = input_data.get("output_dir") - consumer = input_data.get("consumer") - if not isinstance(build_dir, str): - return None - if consumer == "dc": - return os.path.join(build_dir, "dc_sources.list") - if consumer == "yosys": - return os.path.join(build_dir, "yosys_sources.list") - return None - - -async def handler(input_data: dict, ctx: FlowContext) -> None: - origin_tid = get_origin_trace_id(input_data, ctx) - source_list_path = default_source_list(input_data) - if not source_list_path or not os.path.isfile(source_list_path): - _, failure_result = await check_result( - ctx, - 1, - continue_run=False, - extra_fields={"task": "ip-replace", "error": "missing source list"}, - trace_id=origin_tid, - ) - return failure_result - - with open(source_list_path) as handle: - sources = [line.strip() for line in handle if line.strip()] - if not sources: - _, failure_result = await check_result( - ctx, - 1, - continue_run=False, - extra_fields={"task": "ip-replace", "error": "empty source list"}, - trace_id=origin_tid, - ) - return failure_result - - consumer = input_data.get("consumer", "generic") - top_module = input_data.get("top") or "DigitalTop" - output_dir = input_data.get("ip_replace_output_dir") - if not isinstance(output_dir, str) or not output_dir: - output_dir = os.path.join(os.path.dirname(source_list_path), "ip-replace") - os.makedirs(output_dir, exist_ok=True) - - try: - collateral = prepare_sram_collateral(sources, output_dir, top_module) - except Exception as exc: - _, failure_result = await check_result( - ctx, - 1, - continue_run=False, - extra_fields={"task": "ip-replace", "error": str(exc)}, - trace_id=origin_tid, - ) - return failure_result - - replaced_source_list = os.path.join(output_dir, f"{consumer}_sources.list") - with open(replaced_source_list, "w") as handle: - for path in collateral["source_paths"]: - handle.write(f"{path}\n") - - collateral["source_list"] = replaced_source_list - extra = { - "task": "ip-replace", - "consumer": consumer, - "source_list": replaced_source_list, - "sram_manifest": collateral["sram_manifest"], - "sram_memory_count": collateral["sram_memory_count"], - "top_module": collateral["top_module"], - } - if input_data.get("mem_conf"): - extra["mem_conf"] = input_data["mem_conf"] - ctx.logger.info( - f"Prepared {collateral['sram_memory_count']} technology-neutral SRAM modules " - f"for top {collateral['top_module']}" - ) - - next_topic = input_data.get("next_topic") - if isinstance(next_topic, str) and next_topic: - await check_result(ctx, 0, continue_run=True, extra_fields=extra, trace_id=origin_tid) - await ctx.enqueue( - { - "topic": next_topic, - "data": { - **input_data, - "source_list": replaced_source_list, - "sram_collateral": collateral, - "_trace_id": origin_tid, - }, - } - ) - return - - await check_result(ctx, 0, continue_run=False, extra_fields=extra, trace_id=origin_tid) diff --git a/api/steps/ip-replace/scripts/sram_replace.py b/api/steps/ip-replace/scripts/sram_replace.py deleted file mode 100755 index 61c3e9f..0000000 --- a/api/steps/ip-replace/scripts/sram_replace.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Prepare technology-neutral SRAM collateral for synthesis. - -The elaborator has already split sequential memories into separate SystemVerilog -modules. This module preserves those implementations, selects the RTL reachable -from a synthesis top, and records memory interfaces in a manifest. A PDK-owned -flow can consume that manifest to choose macros and replace modules later. -""" - -from __future__ import annotations - -import argparse -import json -import math -import re -from dataclasses import dataclass -from pathlib import Path - - -MODULE_NAME_RE = re.compile(r"^\s*module\s+([A-Za-z_][A-Za-z0-9_$]*)\b", re.M) -MODULE_HEADER_RE = re.compile( - r"\bmodule\s+([A-Za-z_][A-Za-z0-9_$]*)\s*(?:#\s*\(.*?\)\s*)?\((.*?)\)\s*;", - re.S, -) -MEMORY_RE = re.compile( - r"\breg\s+(?:\[\s*(\d+)\s*:\s*(\d+)\s*\]\s+)?Memory\s*\[\s*(\d+)\s*:\s*(\d+)\s*\]", - re.S, -) -RANGE_RE = re.compile(r"\[\s*(\d+)\s*:\s*(\d+)\s*\]\s*{name}\b") -INSTANCE_RE = re.compile( - r"^\s*([A-Za-z_][A-Za-z0-9_$]*)\s*(?:#\s*\(.*?\)\s*)?([A-Za-z_][A-Za-z0-9_$]*)\s*\(", - re.M | re.S, -) - - -@dataclass(frozen=True) -class Memory: - name: str - source: Path - depth: int - width: int - port_type: str - address_width: int | None - write_mask_width: int | None - - -def width_from_range(text: str, name: str) -> int | None: - match = re.search(RANGE_RE.pattern.format(name=re.escape(name)), text) - if match is None: - return None - return abs(int(match.group(1)) - int(match.group(2))) + 1 - - -def ceil_log2(value: int) -> int: - return max(1, math.ceil(math.log2(value))) - - -def module_sources(source_paths: list[Path]) -> dict[str, Path]: - sources: dict[str, Path] = {} - for source in source_paths: - match = MODULE_NAME_RE.search(source.read_text(errors="replace")) - if match is not None: - sources.setdefault(match.group(1), source) - return sources - - -def reachable_modules(source_paths: list[Path], top_module: str) -> set[str]: - sources = module_sources(source_paths) - if top_module not in sources: - raise ValueError(f"synthesis top is not defined in source list: {top_module}") - - module_text = { - module: path.read_text(errors="replace") for module, path in sources.items() - } - reachable = {top_module} - pending = [top_module] - while pending: - module = pending.pop() - for child, _instance in INSTANCE_RE.findall(module_text[module]): - if child in sources and child not in reachable: - reachable.add(child) - pending.append(child) - return reachable - - -def classify_ports(header: str) -> str: - names = set(re.findall(r"\b((?:RW|R|W)\d+)_[A-Za-z0-9_]+\b", header)) - rw_ports = {name for name in names if name.startswith("RW")} - if rw_ports: - return f"{len(rw_ports)}RW" - reads = {name for name in names if name.startswith("R")} - writes = {name for name in names if name.startswith("W")} - return f"{len(reads)}R{len(writes)}W" - - -def discover_memories(source_paths: list[Path]) -> list[Memory]: - memories: list[Memory] = [] - for source in source_paths: - text = source.read_text(errors="replace") - module_match = MODULE_HEADER_RE.search(text) - memory_match = MEMORY_RE.search(text) - if module_match is None or memory_match is None: - continue - name, header = module_match.groups() - data_msb = int(memory_match.group(1) or 0) - data_lsb = int(memory_match.group(2) or 0) - depth_a = int(memory_match.group(3)) - depth_b = int(memory_match.group(4)) - depth = abs(depth_a - depth_b) + 1 - memories.append( - Memory( - name=name, - source=source, - depth=depth, - width=abs(data_msb - data_lsb) + 1, - port_type=classify_ports(header), - address_width=width_from_range(header, "RW0_addr") or ceil_log2(depth), - write_mask_width=( - width_from_range(header, "RW0_wmask") - or (1 if "RW0_wmask" in header else None) - ), - ) - ) - return memories - - -def prepare_sram_collateral( - source_paths: list[str], output_dir: str, top_module: str -) -> dict[str, object]: - """Emit top-scoped source and memory manifests without choosing a PDK macro.""" - sources = [Path(path).resolve() for path in source_paths] - reachable = reachable_modules(sources, top_module) - source_modules = module_sources(sources) - selected_paths = {source_modules[module] for module in reachable} - selected = [path for path in sources if path in selected_paths] - memories = discover_memories(selected) - - output = Path(output_dir) - output.mkdir(parents=True, exist_ok=True) - manifest_path = output / "sram_manifest.json" - manifest = { - "schema_version": 1, - "top_module": top_module, - "memories": [ - { - "module": memory.name, - "source": str(memory.source), - "depth": memory.depth, - "width": memory.width, - "port_type": memory.port_type, - "address_width": memory.address_width, - "write_mask_width": memory.write_mask_width, - } - for memory in memories - ], - } - manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") - return { - "source_paths": [str(path) for path in selected], - "sram_manifest": str(manifest_path), - "sram_memory_count": len(memories), - "top_module": top_module, - } - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--source-list", type=Path, help="one Verilog source path per line") - parser.add_argument("--source-dir", type=Path, help="directory to scan when --source-list is omitted") - parser.add_argument("--output-dir", type=Path, required=True) - parser.add_argument("--top", required=True) - args = parser.parse_args() - - if args.source_list: - sources = [line.strip() for line in args.source_list.read_text().splitlines() if line.strip()] - elif args.source_dir: - sources = [str(path) for path in sorted(args.source_dir.glob("*.sv"))] - else: - parser.error("one of --source-list or --source-dir is required") - - result = prepare_sram_collateral(sources, str(args.output_dir), args.top) - print(json.dumps(result, indent=2, sort_keys=True)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/api/steps/ip/01_generate_api.step.py b/api/steps/ip/01_generate_api.step.py new file mode 100644 index 0000000..d123727 --- /dev/null +++ b/api/steps/ip/01_generate_api.step.py @@ -0,0 +1,44 @@ +from motia import ApiRequest, ApiResponse, FlowContext, api + +from utils.event_common import require_chip + + +def req_arg(body: dict, name: str): + return body.get(name) or body.get(name.replace("_", "-")) + + +config = { + "name": "ip-generate-api", + "description": "generate SRAM macros from elaborator mems.conf via MacroCompiler", + "flows": ["ip", "dc", "yosys"], + "triggers": [api("POST", "/ip/generate")], + "enqueues": ["ip.generate"], +} + + +async def handler(req: ApiRequest, ctx: FlowContext) -> ApiResponse: + body = req.body or {} + try: + chip = require_chip(body) + except ValueError as e: + return ApiResponse(status=400, body={"error": str(e)}) + + data = { + "chip": chip, + "consumer": req_arg(body, "consumer") or "dc", + "top": req_arg(body, "top") or "DigitalTop", + } + if data["consumer"] not in ("dc", "yosys"): + return ApiResponse(status=400, body={"error": "consumer must be dc or yosys"}) + output_dir = req_arg(body, "output_dir") + if output_dir is not None: + if not isinstance(output_dir, str) or not output_dir: + return ApiResponse(status=400, body={"error": "output_dir must be a non-empty string"}) + data["output_dir"] = output_dir + next_topic = req_arg(body, "next_topic") + if next_topic is not None: + if not isinstance(next_topic, str) or not next_topic: + return ApiResponse(status=400, body={"error": "next_topic must be a non-empty string"}) + data["next_topic"] = next_topic + await ctx.enqueue({"topic": "ip.generate", "data": {**data, "_trace_id": ctx.trace_id}}) + return ApiResponse(status=202, body={"trace_id": ctx.trace_id}) diff --git a/api/steps/ip/01_generate_event.step.py b/api/steps/ip/01_generate_event.step.py new file mode 100644 index 0000000..f44e43a --- /dev/null +++ b/api/steps/ip/01_generate_event.step.py @@ -0,0 +1,72 @@ +import os +import sys +from pathlib import Path + +from motia import FlowContext, queue + +utils_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if utils_path not in sys.path: + sys.path.insert(0, utils_path) + +scripts_path = os.path.join(os.path.dirname(__file__), "scripts") +if scripts_path not in sys.path: + sys.path.insert(0, scripts_path) + +from utils.event_common import check_result, get_origin_trace_id, require_chip +from utils.path import get_buckyball_path, rtl_dir +from generate import generate_sram + + +config = { + "name": "ip-generate", + "description": "generate SRAM macros from elaborator mems.conf via MacroCompiler", + "flows": ["ip", "dc", "yosys"], + "triggers": [queue("ip.generate")], + "enqueues": ["ip.replace"], +} + + +async def handler(input_data: dict, ctx: FlowContext) -> None: + origin_tid = get_origin_trace_id(input_data, ctx) + try: + chip = require_chip(input_data) + consumer = input_data.get("consumer") or "dc" + if consumer not in ("dc", "yosys"): + raise ValueError("consumer must be dc or yosys") + top = input_data.get("top") or "DigitalTop" + if not isinstance(top, str) or not top: + raise ValueError("top must be a non-empty string") + bbdir = Path(get_buckyball_path()).resolve() + build_dir = Path(rtl_dir(bbdir, chip, "verilog", input_data.get("output_dir"))).resolve() + man = generate_sram(bbdir=bbdir, chip=chip, build_dir=build_dir) + except Exception as exc: + _, failure_result = await check_result( + ctx, + 1, + continue_run=False, + extra_fields={"task": "ip.generate", "error": str(exc)}, + trace_id=origin_tid, + ) + return failure_result + + extra = { + "task": "ip.generate", + "chip": man["chip"], + "generate_manifest": man["generate_manifest"], + "sram_macros_v": man["sram_macros_v"], + "mem_count": man["mem_count"], + "build_dir": str(build_dir), + } + await check_result(ctx, 0, continue_run=True, extra_fields=extra, trace_id=origin_tid) + await ctx.enqueue( + { + "topic": "ip.replace", + "data": { + **input_data, + "chip": chip, + "consumer": consumer, + "top": top, + "_trace_id": origin_tid, + }, + } + ) diff --git a/api/steps/ip/02_replace_api.step.py b/api/steps/ip/02_replace_api.step.py new file mode 100644 index 0000000..8e31186 --- /dev/null +++ b/api/steps/ip/02_replace_api.step.py @@ -0,0 +1,44 @@ +from motia import ApiRequest, ApiResponse, FlowContext, api + +from utils.event_common import require_chip + + +def req_arg(body: dict, name: str): + return body.get(name) or body.get(name.replace("_", "-")) + + +config = { + "name": "ip-replace-api", + "description": "assemble top-scoped synthesis RTL plus generated SRAM macros", + "flows": ["ip", "dc", "yosys"], + "triggers": [api("POST", "/ip/replace")], + "enqueues": ["ip.replace"], +} + + +async def handler(req: ApiRequest, ctx: FlowContext) -> ApiResponse: + body = req.body or {} + try: + chip = require_chip(body) + except ValueError as e: + return ApiResponse(status=400, body={"error": str(e)}) + + data = { + "chip": chip, + "consumer": req_arg(body, "consumer") or "dc", + "top": req_arg(body, "top") or "DigitalTop", + } + if data["consumer"] not in ("dc", "yosys"): + return ApiResponse(status=400, body={"error": "consumer must be dc or yosys"}) + output_dir = req_arg(body, "output_dir") + if output_dir is not None: + if not isinstance(output_dir, str) or not output_dir: + return ApiResponse(status=400, body={"error": "output_dir must be a non-empty string"}) + data["output_dir"] = output_dir + next_topic = req_arg(body, "next_topic") + if next_topic is not None: + if not isinstance(next_topic, str) or not next_topic: + return ApiResponse(status=400, body={"error": "next_topic must be a non-empty string"}) + data["next_topic"] = next_topic + await ctx.enqueue({"topic": "ip.replace", "data": {**data, "_trace_id": ctx.trace_id}}) + return ApiResponse(status=202, body={"trace_id": ctx.trace_id}) diff --git a/api/steps/ip/02_replace_event.step.py b/api/steps/ip/02_replace_event.step.py new file mode 100644 index 0000000..10d5234 --- /dev/null +++ b/api/steps/ip/02_replace_event.step.py @@ -0,0 +1,116 @@ +import os +import sys +from pathlib import Path + +from motia import FlowContext, queue + +utils_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if utils_path not in sys.path: + sys.path.insert(0, utils_path) + +scripts_path = os.path.join(os.path.dirname(__file__), "scripts") +if scripts_path not in sys.path: + sys.path.insert(0, scripts_path) + +from utils.event_common import check_result, get_origin_trace_id, require_chip +from utils.path import get_buckyball_path, rtl_dir +from replace import replace_sources + +_SOURCE_LIST = {"dc": "dc_sources.list", "yosys": "yosys_sources.list"} + + +config = { + "name": "ip-replace", + "description": "assemble top-scoped synthesis RTL plus generated SRAM macros", + "flows": ["ip", "dc", "yosys"], + "triggers": [queue("ip.replace")], + "enqueues": ["dc.area", "yosys.synth"], +} + + +async def handler(input_data: dict, ctx: FlowContext) -> None: + origin_tid = get_origin_trace_id(input_data, ctx) + try: + chip = require_chip(input_data) + consumer = input_data.get("consumer") or "dc" + if consumer not in _SOURCE_LIST: + raise ValueError("consumer must be dc or yosys") + top_module = input_data.get("top") or "DigitalTop" + if not isinstance(top_module, str) or not top_module: + raise ValueError("top must be a non-empty string") + + bbdir = Path(get_buckyball_path()).resolve() + build_dir = Path(rtl_dir(bbdir, chip, "verilog", input_data.get("output_dir"))).resolve() + source_list_path = build_dir / _SOURCE_LIST[consumer] + if not source_list_path.is_file(): + raise FileNotFoundError(f"missing source list: {source_list_path}") + sources = [line.strip() for line in source_list_path.read_text().splitlines() if line.strip()] + if not sources: + raise ValueError(f"empty source list: {source_list_path}") + + gen_dir = build_dir / "ip-generate" + sram_macros_v = gen_dir / "sram_macros.v" + generate_manifest = gen_dir / "generate_manifest.json" + if not generate_manifest.is_file(): + raise FileNotFoundError(f"missing generate_manifest: {generate_manifest}") + + result = replace_sources( + source_paths=sources, + top_module=top_module, + sram_macros_v=sram_macros_v, + output_dir=build_dir / "ip-replace", + consumer=consumer, + generate_manifest=generate_manifest, + ) + except Exception as exc: + _, failure_result = await check_result( + ctx, + 1, + continue_run=False, + extra_fields={"task": "ip.replace", "error": str(exc)}, + trace_id=origin_tid, + ) + return failure_result + + extra = { + "task": "ip.replace", + "consumer": consumer, + "source_list": result["source_list"], + "replace_manifest": result["replace_manifest"], + "source_count": result["source_count"], + "top_module": top_module, + "build_dir": str(build_dir), + } + if generate_manifest.is_file(): + extra["generate_manifest"] = str(generate_manifest) + + next_topic = input_data.get("next_topic") + if next_topic is not None: + if not isinstance(next_topic, str) or not next_topic: + _, failure_result = await check_result( + ctx, + 1, + continue_run=False, + extra_fields={"task": "ip.replace", "error": "next_topic must be a non-empty string"}, + trace_id=origin_tid, + ) + return failure_result + await check_result(ctx, 0, continue_run=True, extra_fields=extra, trace_id=origin_tid) + await ctx.enqueue( + { + "topic": next_topic, + "data": { + **input_data, + "chip": chip, + "consumer": consumer, + "top": top_module, + "source_list": result["source_list"], + "replace_manifest": result["replace_manifest"], + "generate_manifest": str(generate_manifest), + "_trace_id": origin_tid, + }, + } + ) + return + + await check_result(ctx, 0, continue_run=False, extra_fields=extra, trace_id=origin_tid) diff --git a/api/steps/ip/scripts/generate.py b/api/steps/ip/scripts/generate.py new file mode 100644 index 0000000..f883eb7 --- /dev/null +++ b/api/steps/ip/scripts/generate.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import json +import sys +from pathlib import Path + +_DC = Path(__file__).resolve().parents[2] / "dc" / "scripts" +if str(_DC) not in sys.path: + sys.path.insert(0, str(_DC)) + +from tapeout import SramGeom, get_tapeout_contract +from macro_compiler import run_macro_compiler +from sram_compiler import generate_sram_dbs, leaf_names_from_macros + + +def pad_mems_conf(text: str) -> str: + lines = [] + for raw in text.splitlines(): + if not raw.strip(): + continue + lines.append(raw.rstrip() + " ") + if not lines: + raise ValueError("empty mems.conf") + return "\n".join(lines) + "\n" + + +def geoms_from_macros( + *, + macros_v: Path, + sram_table: dict[str, SramGeom], + mdf: Path, +) -> list[SramGeom]: + raw = json.loads(mdf.read_text()) + if not isinstance(raw, list): + raise ValueError(f"mdf must be a list: {mdf}") + index: dict[str, dict] = {} + for entry in raw: + if not isinstance(entry, dict): + raise ValueError(f"mdf entry must be object: {mdf}") + name = entry.get("name") + if not isinstance(name, str) or not name: + raise ValueError(f"mdf entry missing name: {mdf}") + if name in index: + raise ValueError(f"duplicate mdf name {name}: {mdf}") + index[name] = entry + + names = leaf_names_from_macros(macros_v, set(sram_table) | set(index)) + geoms: list[SramGeom] = [] + for name in names: + if name not in sram_table: + raise ValueError(f"sram leaf {name} missing from tapeout.sram") + if name not in index: + raise ValueError(f"sram leaf {name} missing from mdf: {mdf}") + entry = index[name] + try: + depth = int(entry["depth"]) + width = int(entry["width"]) + except (KeyError, TypeError, ValueError) as exc: + raise ValueError(f"mdf entry {name} missing depth/width") from exc + geom = sram_table[name] + if depth != geom.words: + raise ValueError( + f"sram leaf {name}: mdf depth {depth} != tapeout.sram words {geom.words}" + ) + if width != geom.bits: + raise ValueError( + f"sram leaf {name}: mdf width {width} != tapeout.sram bits {geom.bits}" + ) + geoms.append(geom) + return geoms + + +def _leaf_paths(cache_dir: Path, name: str, corner: str) -> dict[str, str]: + leaf = cache_dir / name + return { + "v": str((leaf / f"{name}.v").resolve()), + "lib": str((leaf / f"{name}_{corner}.lib").resolve()), + "db": str((leaf / f"{name}_{corner}.db").resolve()), + } + + +def generate_sram( + *, + bbdir: str | Path, + chip: str, + build_dir: str | Path, + out_dir: str | Path | None = None, +) -> dict: + bbdir = Path(bbdir).resolve() + build_dir = Path(build_dir).resolve() + src = build_dir / "mems.conf" + if not src.is_file(): + raise FileNotFoundError(f"missing elaborator mems.conf: {src}") + padded = pad_mems_conf(src.read_text()) + contract = get_tapeout_contract(bbdir, chip) + if out_dir is None: + dest = build_dir / "ip-generate" + else: + if isinstance(out_dir, str) and not out_dir.strip(): + raise ValueError("empty out_dir") + dest = Path(out_dir).resolve() + dest.mkdir(parents=True, exist_ok=True) + mems_out = dest / "mems.conf" + verilog = dest / "sram_macros.v" + firrtl = dest / "sram_macros.fir" + mems_out.write_text(padded) + arch_dir = bbdir / "arch" + run_macro_compiler( + mems_conf=mems_out, + mdf=contract.sram_mdf, + verilog=verilog, + firrtl=firrtl, + arch_dir=arch_dir, + ) + geoms = geoms_from_macros( + macros_v=verilog, + sram_table=contract.sram_table, + mdf=contract.sram_mdf, + ) + ip_db = build_dir.parent / f"{build_dir.name}-ip-db" + db_paths, corner_tag = generate_sram_dbs( + geoms=geoms, + process=contract.sram_process, + corner=contract.sram_corner, + cache_dir=ip_db, + lc_shell=contract.lc_shell, + ) + names = [g.name for g in geoms] + man = { + "chip": chip, + "sram_mdf": str(contract.sram_mdf), + "mems_conf": str(mems_out), + "sram_macros_v": str(verilog), + "sram_macros_fir": str(firrtl), + "sram_corner": corner_tag, + "ip_db": str(ip_db), + "leaves": names, + "leaf_paths": { + g.name: _leaf_paths(ip_db, g.name, corner_tag) + for g in geoms + }, + "link_dbs": [str(p) for p in db_paths], + "leaf_count": len(names), + "mem_count": sum(1 for line in padded.splitlines() if line.strip()), + } + manifest_path = dest / "generate_manifest.json" + manifest_path.write_text(json.dumps(man, indent=2) + "\n") + return {**man, "generate_manifest": str(manifest_path)} diff --git a/api/steps/ip/scripts/macro_compiler.py b/api/steps/ip/scripts/macro_compiler.py new file mode 100644 index 0000000..d6c4b63 --- /dev/null +++ b/api/steps/ip/scripts/macro_compiler.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import os +import shlex +import sys +from pathlib import Path + +utils_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +if utils_path not in sys.path: + sys.path.insert(0, utils_path) + +from utils.stream_run import stream_run + + +def run_macro_compiler( + *, mems_conf: Path, mdf: Path, verilog: Path, firrtl: Path, arch_dir: Path +) -> None: + verilog.parent.mkdir(parents=True, exist_ok=True) + cmd = shlex.join( + [ + "mill", + "-i", + "tapeout.runMain", + "tapeout.macros.MacroCompiler", + "-n", + str(mems_conf), + "-v", + str(verilog), + "-f", + str(firrtl), + "--library", + str(mdf), + "--mode", + "strict", + ] + ) + r = stream_run(cmd, cwd=str(arch_dir)) + if r.returncode != 0: + raise RuntimeError(f"MacroCompiler failed:\n{r.stdout}\n{r.stderr}") + if not verilog.is_file() or verilog.stat().st_size == 0: + raise RuntimeError(f"MacroCompiler produced empty verilog: {verilog}") diff --git a/api/steps/ip/scripts/replace.py b/api/steps/ip/scripts/replace.py new file mode 100644 index 0000000..4440051 --- /dev/null +++ b/api/steps/ip/scripts/replace.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import json +import re +from pathlib import Path + +MODULE_NAME_RE = re.compile( + r"^\s*(?:\(\*.*?\*\)\s*)*module\s+([A-Za-z_][A-Za-z0-9_$]*)\b", + re.M, +) +INSTANCE_RE = re.compile( + r"^\s*([A-Za-z_][A-Za-z0-9_$]*)\s*(?:#\s*\(.*?\)\s*)?([A-Za-z_][A-Za-z0-9_$]*)\s*\(", + re.M | re.S, +) +COMMENT_RE = re.compile(r"/\*.*?\*/|//.*?$", re.M | re.S) +MODULE_TOKEN_RE = re.compile(r"\bmodule\b") + + +def replace_sources( + *, + source_paths: list[str], + top_module: str, + sram_macros_v: str | Path, + output_dir: str | Path, + consumer: str, + generate_manifest: str | Path | None = None, +) -> dict: + paths = [Path(path).resolve() for path in source_paths] + macros = Path(sram_macros_v).resolve() + + modules: dict[str, Path] = {} + texts: dict[str, str] = {} + for path in paths: + text = path.read_text() + match = MODULE_NAME_RE.search(text) + if match is None: + stripped = COMMENT_RE.sub("", text) + if MODULE_TOKEN_RE.search(stripped): + raise RuntimeError(f"no module declaration: {path}") + continue + name = match.group(1) + if name in modules: + raise RuntimeError(f"duplicate module {name}: {modules[name]} vs {path}") + modules[name] = path + texts[name] = text + + if top_module not in modules: + raise ValueError(f"synthesis top is not defined in source list: {top_module}") + if not macros.is_file(): + raise FileNotFoundError(f"missing sram_macros.v: {macros}") + + reachable = {top_module} + pending = [top_module] + while pending: + cur = pending.pop() + for child, _inst in INSTANCE_RE.findall(texts[cur]): + if child in modules and child not in reachable: + reachable.add(child) + pending.append(child) + + selected_paths = {modules[name] for name in reachable} + selected = [path for path in paths if path in selected_paths] + if macros not in selected: + selected.append(macros) + + out = Path(output_dir) + out.mkdir(parents=True, exist_ok=True) + source_list_path = out / f"{consumer}_sources.list" + source_list_path.write_text("\n".join(str(path) for path in selected) + "\n") + + manifest = { + "top_module": top_module, + "consumer": consumer, + "source_count": len(selected), + "sram_macros_v": str(macros), + } + if generate_manifest is not None: + gman = Path(generate_manifest).resolve() + if not gman.is_file(): + raise FileNotFoundError(f"missing generate_manifest: {gman}") + manifest["generate_manifest"] = str(gman) + manifest_path = out / "replace_manifest.json" + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") + + return { + "source_list": str(source_list_path), + "replace_manifest": str(manifest_path), + "source_count": len(selected), + } diff --git a/api/steps/ip/scripts/sram_compiler.py b/api/steps/ip/scripts/sram_compiler.py new file mode 100644 index 0000000..b5adda9 --- /dev/null +++ b/api/steps/ip/scripts/sram_compiler.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import re +import sys +from pathlib import Path + +_HERE = Path(__file__).resolve().parent +_API = _HERE.parents[2] +_IP = _HERE.parents[4] / "thirdparty" / "soc-framework" / "ip" +for _p in (str(_API), str(_IP)): + if _p not in sys.path: + sys.path.insert(0, _p) + +from smic180.compiler import generate_smic180_sram_dbs + + +def leaf_names_from_macros(macros_v: Path, known: set[str]) -> list[str]: + if not macros_v.is_file(): + raise FileNotFoundError(f"missing macros verilog: {macros_v}") + text = macros_v.read_text() + ordered: list[str] = [] + for name in sorted(known): + pat = re.compile(rf"(?m)^\s*{re.escape(name)}\s+[A-Za-z_][A-Za-z0-9_$]*\s*\(") + if pat.search(text): + ordered.append(name) + if not ordered: + raise RuntimeError(f"no known sram leaf instances in {macros_v}") + return ordered + + +def generate_sram_dbs( + *, + geoms: list, + process: str, + corner: str, + cache_dir: Path, + lc_shell: Path, +) -> tuple[list[Path], str]: + if process == "smic180": + return generate_smic180_sram_dbs(geoms, corner, cache_dir, lc_shell) + elif process == "tsmc28": + raise ValueError("tsmc28 sram is not supported yet") + raise ValueError(f"unknown sram process {process!r}") diff --git a/api/steps/mill/03_yosys_verilog_event.step.py b/api/steps/mill/03_yosys_verilog_event.step.py index 4af13ec..eff2a57 100644 --- a/api/steps/mill/03_yosys_verilog_event.step.py +++ b/api/steps/mill/03_yosys_verilog_event.step.py @@ -32,7 +32,7 @@ "description": "generate verilog for yosys flow", "flows": ["yosys"], "triggers": [queue("yosys.run"), queue("yosys.verilog")], - "enqueues": ["ip-replace.run"], + "enqueues": ["ip.generate"], } @@ -215,7 +215,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: yosys_cfg = load_yosys_config() top_module = input_data.get("top") or yosys_cfg.get("top") or "DigitalTop" yosys_log_dir = input_data.get("log_dir") or log_dir( - bbdir, chip, "synth", datetime.now().strftime("%Y%m%d-%H%M%S-%f"), + bbdir, chip, "synth", datetime.now().strftime("%Y-%m-%d-%H-%M"), "yosys", top_module, input_data.get("output_dir"), ) ctx.logger.info(f"Yosys log dir: {yosys_log_dir}") @@ -240,26 +240,28 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: ) return failure_result + need_ip = bool(input_data.get("from_run_workflow")) await check_result( ctx, 0, - continue_run=True, + continue_run=need_ip, extra_fields={"task": "verilog", "source_list": source_list_path, "mem_conf": mem_conf}, trace_id=origin_tid, ) + if not need_ip: + return await ctx.enqueue( { - "topic": "ip-replace.run", + "topic": "ip.generate", "data": { **input_data, - "source_list": source_list_path, - "ip_replace_output_dir": yosys_log_dir, + "chip": chip, "consumer": "yosys", "top": top_module, - "mem_conf": mem_conf, - "next_topic": "yosys.synth" if input_data.get("from_run_workflow") else None, - "task": "run" if input_data.get("from_run_workflow") else "verilog", + "log_dir": yosys_log_dir, + "next_topic": "yosys.synth", + "task": "run", "_trace_id": origin_tid, }, } diff --git a/api/steps/mill/04_dc_verilog_event.step.py b/api/steps/mill/04_dc_verilog_event.step.py index 5d1ddd8..80c45fd 100644 --- a/api/steps/mill/04_dc_verilog_event.step.py +++ b/api/steps/mill/04_dc_verilog_event.step.py @@ -1,8 +1,8 @@ import os -from datetime import datetime import sys import glob import re +from datetime import datetime from motia import FlowContext, queue @@ -31,7 +31,7 @@ "description": "generate RTL and memory metadata for downstream DC/tapeout flow", "flows": ["dc"], "triggers": [queue("dc.verilog")], - "enqueues": ["ip-replace.run"], + "enqueues": ["ip.generate"], } @@ -63,9 +63,12 @@ def prepare_dc_verilog(build_dir: str): ) stub_dir = os.path.join(build_dir, "dc_stubs") os.makedirs(stub_dir, exist_ok=True) + skip_dirs = {os.path.join(build_dir, name) for name in ("ip-generate", "ip-replace", "dc_stubs")} kept = [] stubbed_dpi = [] for path in vsrcs: + if any(path == d or path.startswith(d + os.sep) for d in skip_dirs): + continue if is_dpi_source(path): stub_path = os.path.join(stub_dir, f"stub_{os.path.basename(path)}") with open(stub_path, "w") as f: @@ -146,10 +149,13 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: f"{'...' if len(stubbed_dpi) > 10 else ''}" ) + need_ip = bool( + input_data.get("from_area_workflow") or input_data.get("from_power_workflow") + ) await check_result( ctx, 0, - continue_run=True, + continue_run=need_ip, extra_fields={ "task": "verilog", "source_list": source_list_path, @@ -158,21 +164,25 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: }, trace_id=origin_tid, ) + if not need_ip: + return payload = { **input_data, - "source_list": source_list_path, - "ip_replace_output_dir": os.path.join(build_dir, "ip-replace"), + "chip": chip, "consumer": "dc", - "mem_conf": mem_conf, "top": top_module, - "next_topic": "dc.area" if input_data.get("from_area_workflow") or input_data.get("from_power_workflow") else None, + "next_topic": "dc.area", "_trace_id": origin_tid, } if input_data.get("from_power_workflow"): - payload["analysis_dir"] = log_dir(bbdir, chip, "synth", datetime.now().strftime("%Y%m%d-%H%M%S-%f"), "dc", "power") - elif input_data.get("from_area_workflow"): - payload["analysis_dir"] = log_dir(bbdir, chip, "synth", datetime.now().strftime("%Y%m%d-%H%M%S-%f"), "dc", "area") - await ctx.enqueue({"topic": "ip-replace.run", "data": payload}) + payload["analysis_dir"] = log_dir( + bbdir, chip, "synth", datetime.now().strftime("%Y-%m-%d-%H-%M"), "dc", "power" + ) + else: + payload["analysis_dir"] = log_dir( + bbdir, chip, "synth", datetime.now().strftime("%Y-%m-%d-%H-%M"), "dc", "area" + ) + await ctx.enqueue({"topic": "ip.generate", "data": payload}) return diff --git a/api/steps/dc/06_ptpx_event.step.py b/api/steps/pt/01_ptpx_event.step.py similarity index 79% rename from api/steps/dc/06_ptpx_event.step.py rename to api/steps/pt/01_ptpx_event.step.py index b3c1334..d952f62 100644 --- a/api/steps/dc/06_ptpx_event.step.py +++ b/api/steps/pt/01_ptpx_event.step.py @@ -11,21 +11,21 @@ scripts_path = os.path.join(os.path.dirname(__file__), "scripts") if scripts_path not in sys.path: sys.path.insert(0, scripts_path) -step_path = os.path.dirname(__file__) -if step_path not in sys.path: - sys.path.insert(0, step_path) +dc_scripts_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "dc", "scripts")) +if dc_scripts_path not in sys.path: + sys.path.insert(0, dc_scripts_path) from utils.event_common import check_result, get_origin_trace_id from utils.path import get_buckyball_path -from power import read_dynamic_power from utils.stream_run import stream_run_logger_async -from tapeout import get_tapeout_contract, resolve_power_window, technology_settings, write_run_tcl +from power import read_dynamic_power +from tapeout import get_tapeout_contract, resolve_power_window, write_run_tcl config = { - "name": "dc-ptpx", - "description": "run PrimeTime PX power analysis on DC output", - "flows": ["dc"], - "triggers": [queue("dc.power")], + "name": "pt-ptpx", + "description": "run PrimeTime PX power analysis", + "flows": ["pt", "dc"], + "triggers": [queue("pt.run")], "enqueues": [], } @@ -43,11 +43,11 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: elif not isinstance(activity_format, str) or activity_format not in {"saif", "vcd", "fsdb"}: error = "missing or invalid --format; expected saif, vcd, or fsdb" elif shutil.which("pt_shell") is None: - error = "pt_shell is not on PATH; source the DC host environment before running bbdev dc --power" + error = "pt_shell is not on PATH" else: error = None if error: - await check_result(ctx, 1, continue_run=False, extra_fields={"task": "power", "error": error}, trace_id=origin_tid) + await check_result(ctx, 1, continue_run=False, extra_fields={"task": "pt", "error": error}, trace_id=origin_tid) return try: @@ -59,7 +59,12 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: input_data.get("start_ns", input_data.get("start-ns")), input_data.get("end_ns", input_data.get("end-ns")), ) - tech = technology_settings() + tech = { + "target_library": contract.target_library, + "synthetic_library": contract.synthetic_library, + "link_library": contract.link_library, + "max_cores": contract.max_cores, + } run_config = write_run_tcl( os.path.join(analysis_dir, "power-run.tcl"), { @@ -77,24 +82,24 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: ) script = contract.power_script except (OSError, ValueError) as exc: - await check_result(ctx, 1, continue_run=False, extra_fields={"task": "power", "error": str(exc)}, trace_id=origin_tid) + await check_result(ctx, 1, continue_run=False, extra_fields={"task": "pt", "error": str(exc)}, trace_id=origin_tid) return result = await stream_run_logger_async( cmd=( - f"pt_shell -f {shlex.quote(str(script))} " + f"set -o pipefail; pt_shell -f {shlex.quote(str(script))} " f"-x {shlex.quote('set RUN_CONFIG ' + '{' + str(run_config) + '}')} " - f"> {shlex.quote(os.path.join(analysis_dir, 'pt_shell.log'))} 2>&1" + f"2>&1 | tee {shlex.quote(os.path.join(analysis_dir, 'pt_shell.log'))}" ), logger=ctx.logger, cwd=os.path.dirname(script), - stdout_prefix="ptpx", - stderr_prefix="ptpx", + stdout_prefix="pt", + stderr_prefix="pt", ) report_dir = os.path.join(analysis_dir, "power-reports") dynamic_power = read_dynamic_power(os.path.join(report_dir, "power_total.rpt")) extra_fields = { - "task": "power", + "task": "pt", "top_module": top_module, "activity": activity_path, "format": activity_format, diff --git a/api/steps/dc/power.py b/api/steps/pt/scripts/power.py similarity index 100% rename from api/steps/dc/power.py rename to api/steps/pt/scripts/power.py diff --git a/api/steps/vcs/04_sim_event.step.py b/api/steps/vcs/04_sim_event.step.py index b19cbef..82977b9 100644 --- a/api/steps/vcs/04_sim_event.step.py +++ b/api/steps/vcs/04_sim_event.step.py @@ -58,7 +58,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: build_dir = rtl_dir(bbdir, chip, "verilog", input_data.get("output_dir")) artifact_dir = Path(build_dir) / "vcs" simv = artifact_dir / "simv" - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f") + timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M") run_log = Path(log_dir( bbdir, chip, "verilog", timestamp, "vcs", binary_name, input_data.get("output_dir"), diff --git a/api/steps/yosys/03_synth_api.step.py b/api/steps/yosys/03_synth_api.step.py index 7e393a9..2994e98 100644 --- a/api/steps/yosys/03_synth_api.step.py +++ b/api/steps/yosys/03_synth_api.step.py @@ -4,7 +4,6 @@ from motia import ApiRequest, ApiResponse, FlowContext, api from utils.event_common import require_chip -from utils.path import rtl_dir, get_buckyball_path scripts_path = os.path.join(os.path.dirname(__file__), "scripts") if scripts_path not in sys.path: @@ -17,39 +16,36 @@ "description": "run yosys synthesis for area estimation", "flows": ["yosys"], "triggers": [api("POST", "/yosys/synth")], - "enqueues": ["ip-replace.run"], + "enqueues": ["ip.generate"], } async def handler(req: ApiRequest, ctx: FlowContext) -> ApiResponse: - bbdir = get_buckyball_path() body = req.body or {} try: chip = require_chip(body) - rtl = rtl_dir(bbdir, chip, "synth", req_arg(body, "output_dir")) except ValueError as e: return ApiResponse(status=400, body={"error": str(e)}) data = { "chip": chip, - "output_dir": rtl, + "consumer": "yosys", "top": req_arg(body, "top") or "DigitalTop", - "vcd": req_arg(body, "vcd"), + "next_topic": "yosys.synth", } + output_dir = req_arg(body, "output_dir") + if output_dir: + data["output_dir"] = output_dir + vcd = req_arg(body, "vcd") + if vcd: + data["vcd"] = vcd log_dir = req_arg(body, "log_dir") if log_dir: data["log_dir"] = log_dir await ctx.enqueue( { - "topic": "ip-replace.run", - "data": { - **data, - "source_list": os.path.join(rtl, "yosys_sources.list"), - "ip_replace_output_dir": log_dir or rtl, - "consumer": "yosys", - "next_topic": "yosys.synth", - "_trace_id": ctx.trace_id, - }, + "topic": "ip.generate", + "data": {**data, "_trace_id": ctx.trace_id}, } ) return ApiResponse(status=202, body={"trace_id": ctx.trace_id}) diff --git a/api/steps/yosys/03_synth_event.step.py b/api/steps/yosys/03_synth_event.step.py index 2aee9db..3c297bb 100644 --- a/api/steps/yosys/03_synth_event.step.py +++ b/api/steps/yosys/03_synth_event.step.py @@ -81,13 +81,13 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: if isinstance(liberty, str): liberty = os.path.expandvars(os.path.expanduser(liberty)) - source_list_path = input_data.get("source_list") or os.path.join(build_dir, "yosys_sources.list") - if not os.path.exists(source_list_path): - success_result, failure_result = await check_result( + source_list_path = input_data.get("source_list") + if not isinstance(source_list_path, str) or not os.path.isfile(source_list_path): + _, failure_result = await check_result( ctx, 1, continue_run=False, - extra_fields={"task": "synth", "error": "missing yosys_sources.list, run yosys verilog first"}, + extra_fields={"task": "synth", "error": "missing prepared yosys source list"}, trace_id=origin_tid, ) return failure_result @@ -96,24 +96,22 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: vsrcs = [line.strip() for line in f.readlines() if line.strip()] if not vsrcs: - success_result, failure_result = await check_result( + _, failure_result = await check_result( ctx, 1, continue_run=False, - extra_fields={"task": "synth", "error": "empty yosys_sources.list"}, + extra_fields={"task": "synth", "error": "empty source list"}, trace_id=origin_tid, ) return failure_result - stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f") + stamp = datetime.now().strftime("%Y-%m-%d-%H-%M") yosys_output_dir = input_data.get("log_dir") or log_dir( bbdir, chip, "synth", stamp, "yosys", top_module, input_data.get("output_dir"), ) os.makedirs(yosys_output_dir, exist_ok=True) ctx.logger.info(f"Yosys log dir: {yosys_output_dir}") - sram_collateral = input_data.get("sram_collateral") or {} - read_commands = "\n".join([f"read_verilog -sv {src}" for src in vsrcs]) yosys_script = f"{yosys_output_dir}/synth_area.ys" with open(yosys_script, "w") as f: @@ -150,8 +148,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: extra = { "task": "synth", "output_dir": yosys_output_dir, - "sram_manifest": sram_collateral.get("sram_manifest"), - "sram_memory_count": sram_collateral.get("sram_memory_count", 0), + "replace_manifest": input_data.get("replace_manifest"), } netlist_file = f"{yosys_output_dir}/synth_netlist.v" timing_report_file = f"{yosys_output_dir}/timing_report.txt" diff --git a/api/steps/yosys/scripts/yosys_log.py b/api/steps/yosys/scripts/yosys_log.py index e58811b..a79c3de 100644 --- a/api/steps/yosys/scripts/yosys_log.py +++ b/api/steps/yosys/scripts/yosys_log.py @@ -3,10 +3,10 @@ def req_arg(body: dict, name: str): - return body.get(name) or body.get(name.replace("_", "-")) + return body.get(name) or body.get(name.replace("_", "-")) def make_yosys_log_dir(bbdir: str, trace_id: str) -> str: - stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f") - suffix = trace_id[:8] if trace_id else "no-trace" - return os.path.join(bbdir, "bbdev", "api", "steps", "yosys", "log", f"{stamp}-{suffix}") + stamp = datetime.now().strftime("%Y-%m-%d-%H-%M") + suffix = trace_id[:8] if trace_id else "no-trace" + return os.path.join(bbdir, "bbdev", "api", "steps", "yosys", "log", f"{stamp}-{suffix}") diff --git a/api/tests/test_api_dc_verilog.py b/api/tests/test_api_dc_verilog.py index 41aa489..a6247c2 100644 --- a/api/tests/test_api_dc_verilog.py +++ b/api/tests/test_api_dc_verilog.py @@ -1,16 +1,3 @@ -import json -from pathlib import Path - from _api_test_helper import run_bbdev_case run_bbdev_case("bbdev dc --verilog '--chip toy'") - -root = Path(__file__).resolve().parents[3] -output_dir = root / "arch" / "build" / "toy" / "sims.verilator.BuckyballToyVerilatorConfig" -manifest = json.loads((output_dir / "ip-replace" / "sram_manifest.json").read_text()) -source_list = (output_dir / "ip-replace" / "dc_sources.list").read_text().splitlines() - -assert manifest["top_module"] == "DigitalTop" -assert manifest["memories"] -assert any(path.endswith("/DigitalTop.sv") for path in source_list) -assert not any(path.endswith("/BBSimHarness.sv") for path in source_list) diff --git a/api/utils/path.py b/api/utils/path.py index e61fc89..35d74b7 100644 --- a/api/utils/path.py +++ b/api/utils/path.py @@ -4,8 +4,6 @@ import subprocess from pathlib import Path -_PRODUCT = {"verilog": "verilator", "synth": "verilator", "p2e": "p2e"} - def get_buckyball_path(): current_dir = os.path.dirname(__file__) @@ -68,7 +66,11 @@ def chip_arch_root(bbdir, chip): def sim_name(bbdir, chip, product, *, rushb=False): - if product not in _PRODUCT: + if product == "verilog" or product == "synth": + sim_key = "verilator" + elif product == "p2e": + sim_key = "p2e" + else: raise ValueError(f"invalid rtl product: {product}") path = ( Path(bbdir) @@ -80,7 +82,7 @@ def sim_name(bbdir, chip, product, *, rushb=False): / "config" / "config.json" ) - name = json.loads(path.read_text(encoding="utf-8"))["sims"][_PRODUCT[product]] + name = json.loads(path.read_text(encoding="utf-8"))["sims"][sim_key] if not rushb: return name if product != "verilog" or not name.endswith("VerilatorConfig"): diff --git a/bbdev b/bbdev index 933af68..a4e7ba6 100755 --- a/bbdev +++ b/bbdev @@ -86,8 +86,9 @@ WORKFLOW_COMMANDS = { "area": 'Generate RTL, run DC synthesis, and report area under log////area. Args: "--chip [--top ]".', "power": 'Generate RTL, run chip-owned power simulation, then run PrimeTime PX under log////power. Args: "--chip [--top ] [--workload ] [--start-ns ] [--end-ns ] [--activity --format ]".', }, - "ip-replace": { - "run": 'Replace behavioral IP RTL. Args: "--source-list --output-dir [--top ] [--consumer ]"', + "ip": { + "generate": 'Generate SRAM macros from elaborator mems.conf under arch/build//. Args: "--chip [--consumer ] [--top ] [--output-dir ] [--next-topic ]"', + "replace": 'Assemble top-scoped synthesis RTL plus SRAM macros under arch/build//. Args: "--chip [--consumer ] [--top ] [--output-dir ] [--next-topic ]"', }, "yosys": { "run": 'Run yosys flow. Args: "[--top ] [--chip ] [--output-dir ] [--log-dir ] [--vcd ]"', @@ -555,7 +556,8 @@ def _submit_and_poll(port: int, api_path: str, cmd_info: dict): print(f"\nTask completed on http://{HOST}:{port}") if task_result and not task_result.get("success", False): - print("Error: Task failed") + err = task_result.get("error") or task_result + print(f"Error: Task failed: {err}") return 1 return 0 diff --git a/mcp/tools/__init__.py b/mcp/tools/__init__.py index 3656d07..bb31b3f 100644 --- a/mcp/tools/__init__.py +++ b/mcp/tools/__init__.py @@ -33,6 +33,7 @@ from . import dc_verilog from . import dc_area from . import dc_power +from . import ip_generate from . import ip_replace from . import firesim_enumeratefpgas from . import firesim_buildbitstream @@ -79,6 +80,7 @@ dc_verilog, dc_area, dc_power, + ip_generate, ip_replace, firesim_enumeratefpgas, firesim_buildbitstream, diff --git a/mcp/tools/ip_generate.py b/mcp/tools/ip_generate.py new file mode 100644 index 0000000..be8bd54 --- /dev/null +++ b/mcp/tools/ip_generate.py @@ -0,0 +1,33 @@ +"""MCP tool: bbdev_ip_generate.""" + +from __future__ import annotations + +from typing import Optional + +from common import err, fmt, need, opt, submit + + +def register(mcp): + @mcp.tool() + def bbdev_ip_generate( + chip: str, + consumer: Optional[str] = None, + top: Optional[str] = None, + output_dir: Optional[str] = None, + next_topic: Optional[str] = None, + ) -> str: + """Generate SRAM macros from mems.conf. POST /ip/generate.""" + if e := need("chip", chip): + return err(e) + return fmt( + submit( + "/ip/generate", + opt( + {"chip": chip}, + consumer=consumer, + top=top, + output_dir=output_dir, + next_topic=next_topic, + ), + ) + ) diff --git a/mcp/tools/ip_replace.py b/mcp/tools/ip_replace.py index ecec7da..dccafa3 100644 --- a/mcp/tools/ip_replace.py +++ b/mcp/tools/ip_replace.py @@ -1,26 +1,33 @@ -"""MCP tool: bbdev_ip_replace_run.""" +"""MCP tool: bbdev_ip_replace.""" from __future__ import annotations from typing import Optional -from common import err, fmt, opt, submit +from common import err, fmt, need, opt, submit def register(mcp): @mcp.tool() - def bbdev_ip_replace_run( - source_list: str, - output_dir: str, - top: Optional[str] = None, + def bbdev_ip_replace( + chip: str, consumer: Optional[str] = None, + top: Optional[str] = None, + output_dir: Optional[str] = None, + next_topic: Optional[str] = None, ) -> str: - """Replace behavioral IP RTL. POST /ip/replace/run.""" - if not source_list or not output_dir: - return err("source_list and output_dir are required") + """Assemble synthesis source list plus SRAM macros. POST /ip/replace.""" + if e := need("chip", chip): + return err(e) return fmt( submit( - "/ip/replace/run", - opt({}, source_list=source_list, output_dir=output_dir, top=top, consumer=consumer), + "/ip/replace", + opt( + {"chip": chip}, + consumer=consumer, + top=top, + output_dir=output_dir, + next_topic=next_topic, + ), ) )