diff --git a/.gitignore b/.gitignore index 438bbd0..2574abf 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ appendonlydir/ dump.rdb *.log *.jou +dramsim3* output/ result/ @@ -22,3 +23,6 @@ result/ # Temp iii config files generated by bbdev script mode api/iii-config-*.yaml api/data/ + +api/steps/ip/scripts/_smoke_out/ +lc_output.txt diff --git a/api/steps/bebop/bemu/scripts/bemu_analysis.py b/api/steps/bebop/bemu/scripts/bemu_analysis.py index c03da47..a7d91e5 100644 --- a/api/steps/bebop/bemu/scripts/bemu_analysis.py +++ b/api/steps/bebop/bemu/scripts/bemu_analysis.py @@ -19,7 +19,7 @@ 35: "mvin_mmio", } -MATRIX_MNEMONICS = frozenset({"MATRIX", "MATRIX_F32"}) +MATRIX_MNEMONICS = frozenset({"MATRIX", "MATRIX_F32", "SMATMUL_OS"}) _API = Path(__file__).resolve().parents[4] if str(_API) not in sys.path: @@ -104,8 +104,8 @@ def chip_maps(bbdir: str, chip: str) -> tuple[dict[int, str], set[int], int]: return names, matrix, entries -def _mnk(rs2: int) -> tuple[int, int, int]: - return rs2 & 0xFFF, (rs2 >> 12) & 0xFFF, (rs2 >> 24) & 0xFFF +def _mnk(rs1: int, rs2: int) -> tuple[int, int, int]: + return rs2 & 0xFFF, (rs2 >> 12) & 0xFFF, rs1 >> 30 def analysis_dir( @@ -170,8 +170,9 @@ def analysis_dir( funct_cyc[name] += delta prev_clk = clk if funct in matrix: + rs1 = _hex_u(obj.get("rs1"), "rs1", line_no) rs2 = _hex_u(obj.get("rs2"), "rs2", line_no) - mnk_n[_mnk(rs2)] += 1 + mnk_n[_mnk(rs1, rs2)] += 1 elif kind == "mtrace": n_mtrace += 1 rows = obj.get("rows") 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..6dbed01 --- /dev/null +++ b/api/steps/dc/scripts/tapeout.py @@ -0,0 +1,151 @@ +"""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 + bitwrite: bool = False + + +@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 = {} + for row in s["sram"]: + name = row["name"] + if name in sram_table: + raise ValueError(f"duplicate tapeout.sram name {name}") + bitwrite = row["bitwrite"] if "bitwrite" in row else False + if not isinstance(bitwrite, bool): + raise ValueError(f"tapeout.sram {name}: bitwrite must be bool, got {bitwrite!r}") + sram_table[name] = SramGeom(name, row["words"], row["mux"], row["bits"], bitwrite) + + 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..994d82d --- /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, "tapeout" if consumer == "dc" else "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..873ee2c --- /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, "tapeout" if consumer == "dc" else "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..6ed0ca1 --- /dev/null +++ b/api/steps/ip/scripts/generate.py @@ -0,0 +1,284 @@ +from __future__ import annotations + +import json +import re +import sys +from collections import Counter +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 + +_MODULE = re.compile(r"(?m)^module\s+(\w+)\s*\(") +_INST = re.compile(r"(?m)^\s*([A-Za-z_][A-Za-z0-9_$]*)\s+[A-Za-z_][A-Za-z0-9_$]*\s*\(") + + +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 load_mdf(mdf: Path) -> dict[str, tuple[int, int]]: + raw = json.loads(mdf.read_text()) + if not isinstance(raw, list): + raise ValueError(f"mdf must be a list: {mdf}") + index: dict[str, tuple[int, int]] = {} + 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}") + 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 + if depth <= 0 or width <= 0: + raise ValueError(f"mdf entry {name}: depth/width must be positive") + index[name] = (depth, width) + return index + + +def parse_mems(text: str) -> list[tuple[str, int, int]]: + out: list[tuple[str, int, int]] = [] + for i, raw in enumerate(text.splitlines(), 1): + line = raw.strip() + if not line: + continue + parts = line.split() + if len(parts) < 6 or parts[0] != "name" or parts[2] != "depth" or parts[4] != "width": + raise ValueError(f"mems.conf:{i}: expected 'name depth width ...', got {line!r}") + try: + depth = int(parts[3]) + width = int(parts[5]) + except ValueError as exc: + raise ValueError(f"mems.conf:{i}: depth/width not int") from exc + if depth <= 0 or width <= 0: + raise ValueError(f"mems.conf:{i}: depth/width must be positive") + out.append((parts[1], depth, width)) + if not out: + raise ValueError("empty mems.conf") + return out + + +def module_insts(text: str) -> dict[str, Counter[str]]: + hits = list(_MODULE.finditer(text)) + out: dict[str, Counter[str]] = {} + for i, m in enumerate(hits): + name = m.group(1) + if name in out: + raise ValueError(f"duplicate module {name}") + start = m.end() + end = hits[i + 1].start() if i + 1 < len(hits) else len(text) + counts: Counter[str] = Counter() + for inst in _INST.finditer(text[start:end]): + leaf = inst.group(1) + if leaf in ("module", "assign", "wire", "input", "output"): + raise ValueError(f"module {name}: refused instance parse {leaf!r}") + counts[leaf] += 1 + out[name] = counts + return out + + +def check_mapping(*, mems_text: str, macros_v: Path, mdf: dict[str, tuple[int, int]]) -> None: + if not macros_v.is_file(): + raise FileNotFoundError(f"missing macros verilog: {macros_v}") + insts = module_insts(macros_v.read_text()) + lib = ", ".join(f"{n}={d}x{w}" for n, (d, w) in sorted(mdf.items())) + errs: list[str] = [] + for name, depth, width in parse_mems(mems_text): + need = depth * width + if name not in insts: + errs.append(f"{name}: missing module for {depth}x{width} ({need} bits)") + continue + counts = insts[name] + if not counts: + errs.append(f"{name}: empty mapping for {depth}x{width} ({need} bits)") + continue + got = 0 + parts = [] + unknown = False + for leaf, n in sorted(counts.items()): + if leaf not in mdf: + errs.append(f"{name}: unknown leaf {leaf}") + unknown = True + break + d, w = mdf[leaf] + got += n * d * w + parts.append(f"{n}x {leaf} ({d}x{w})") + if unknown: + continue + if got != need: + errs.append( + f"{name}: need {depth}x{width} = {need} bits, " + f"got {' + '.join(parts)} = {got} bits" + ) + if errs: + raise RuntimeError( + "SRAM mapping mismatch:\n " + "\n ".join(errs) + f"\nlibrary: {lib}" + ) + + +def geoms_from_macros( + *, + macros_v: Path, + sram_table: dict[str, SramGeom], + mdf: Path, +) -> list[SramGeom]: + index = load_mdf(mdf) + 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}") + depth, width = index[name] + 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 check_bitwrite_contract(geoms: list[SramGeom], mdf: Path, macros_v: Path, sram_table: dict[str, SramGeom]) -> None: + raw = json.loads(mdf.read_text()) + if not isinstance(raw, list): + raise ValueError(f"mdf must be a list: {mdf}") + by_name: 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 by_name: + raise ValueError(f"duplicate mdf name {name}: {mdf}") + by_name[name] = entry + for g in geoms: + if g.name not in by_name: + raise ValueError(f"sram leaf {g.name} missing from mdf: {mdf}") + ports = by_name[g.name].get("ports") + if not isinstance(ports, list) or len(ports) != 1: + raise ValueError(f"{g.name}: expected exactly 1 mdf port") + p = ports[0] + if not isinstance(p, dict): + raise ValueError(f"{g.name}: mdf port must be object") + mask_name = p.get("mask port name") + if g.bitwrite: + if mask_name != "BWEN": + raise ValueError(f"{g.name}: bitwrite requires mask port BWEN, got {mask_name!r}") + if p.get("mask port polarity") != "active low": + raise ValueError(f"{g.name}: BWEN polarity must be active low") + if p.get("mask granularity") != 1: + raise ValueError(f"{g.name}: BWEN mask granularity must be 1") + elif mask_name is not None: + raise ValueError(f"{g.name}: mask port {mask_name!r} but bitwrite is false") + used = {g.name for g in geoms} + for geom in sram_table.values(): + if geom.bitwrite and geom.name not in used: + raise RuntimeError(f"bitwrite sram {geom.name} not instantiated in {macros_v}") + + +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, + ) + check_mapping( + mems_text=padded, + macros_v=verilog, + mdf=load_mdf(contract.sram_mdf), + ) + geoms = geoms_from_macros( + macros_v=verilog, + sram_table=contract.sram_table, + mdf=contract.sram_mdf, + ) + check_bitwrite_contract(geoms, contract.sram_mdf, verilog, contract.sram_table) + 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/02_vcs_verilog_event.step.py b/api/steps/mill/02_vcs_verilog_event.step.py index 6333bbf..eb6c1fe 100644 --- a/api/steps/mill/02_vcs_verilog_event.step.py +++ b/api/steps/mill/02_vcs_verilog_event.step.py @@ -40,7 +40,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: arch = os.path.join(bbdir, "arch") try: mill_config, build_dir = rtl_out( - bbdir, chip, "verilog", input_data.get("output_dir"), + bbdir, chip, "tapeout", input_data.get("output_dir"), ) os.makedirs(build_dir, exist_ok=True) returncode = ( 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..6b8ed80 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: @@ -90,7 +93,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: try: chip = require_chip(input_data) mill_config, build_dir = rtl_out( - bbdir, chip, "verilog", input_data.get("output_dir"), + bbdir, chip, "tapeout", input_data.get("output_dir"), ) os.makedirs(build_dir, exist_ok=True) ctx.logger.info(f"Using mill config: {mill_config}") @@ -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, "tapeout", datetime.now().strftime("%Y-%m-%d-%H-%M"), "dc", "power" + ) + else: + payload["analysis_dir"] = log_dir( + bbdir, chip, "tapeout", 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/uvm/00_ball_verilog_api.step.py b/api/steps/uvm/00_ball_verilog_api.step.py deleted file mode 100644 index 76e2df8..0000000 --- a/api/steps/uvm/00_ball_verilog_api.step.py +++ /dev/null @@ -1,39 +0,0 @@ -from motia import ApiRequest, ApiResponse, FlowContext, api - -config = { - "name": "uvm-ball-verilog-api", - "description": "generate isolated ball verilog for UVM", - "flows": ["uvm"], - "triggers": [api("POST", "/uvm/verilog")], - "enqueues": ["uvm.verilog"], -} - - -async def handler(request: ApiRequest, ctx: FlowContext) -> ApiResponse: - body = request.body or {} - - if body.get("chip") or ( - isinstance(body.get("config"), str) and body.get("config") and body.get("config") != "None" - ): - return ApiResponse( - status=400, - body={"error": "uvm ball verilog does not take --chip or --config"}, - ) - - balltype = body.get("balltype") - if not isinstance(balltype, str) or not balltype: - return ApiResponse( - status=400, - body={"error": "Missing required parameter: --balltype"}, - ) - - output_dir = body.get("output_dir") - if not isinstance(output_dir, str) or not output_dir: - return ApiResponse( - status=400, - body={"error": "Missing required parameter: --output-dir"}, - ) - - data = {"balltype": balltype, "output_dir": output_dir} - await ctx.enqueue({"topic": "uvm.verilog", "data": {**data, "_trace_id": ctx.trace_id}}) - return ApiResponse(status=202, body={"trace_id": ctx.trace_id}) diff --git a/api/steps/uvm/00_ball_verilog_event.step.py b/api/steps/uvm/00_ball_verilog_event.step.py deleted file mode 100644 index a93c56e..0000000 --- a/api/steps/uvm/00_ball_verilog_event.step.py +++ /dev/null @@ -1,80 +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.path import get_buckyball_path -from utils.stream_run import stream_run_logger_async -from utils.event_common import check_result, get_origin_trace_id - -config = { - "name": "uvm-ball-verilog", - "description": "generate isolated ball verilog for UVM", - "flows": ["uvm"], - "triggers": [queue("uvm.verilog")], - "enqueues": [], -} - - -def _balltype(data: dict) -> str: - balltype = data.get("balltype") - if not isinstance(balltype, str) or not balltype: - raise ValueError("Missing required parameter: --balltype") - return balltype - - -def _output_dir(data: dict) -> str: - output_dir = data.get("output_dir") - if not isinstance(output_dir, str) or not output_dir: - raise ValueError("Missing required parameter: --output-dir") - return output_dir - - -async def handler(input_data: dict, ctx: FlowContext) -> None: - origin_tid = get_origin_trace_id(input_data, ctx) - bbdir = get_buckyball_path() - - try: - balltype = _balltype(input_data) - build_dir = _output_dir(input_data) - except ValueError as error: - ctx.logger.error(str(error)) - _, failure_result = await check_result( - ctx, - 1, - continue_run=False, - extra_fields={ - "task": "validation", - "error": str(error), - "example": 'bbdev uvm --verilog "--balltype ReluBall --output-dir "', - }, - trace_id=origin_tid, - ) - return failure_result - - os.makedirs(build_dir, exist_ok=True) - command = ( - f"mill -i __.test.runMain sims.verify.BallTopMain {balltype} " - "--disable-annotation-unknown --strip-debug-info -O=release " - f"--split-verilog -o={build_dir} " - ) - ctx.logger.info(f"Using build directory: {build_dir}") - result = await stream_run_logger_async( - cmd=command, - logger=ctx.logger, - cwd=f"{bbdir}/arch", - stdout_prefix="uvm ball verilog", - stderr_prefix="uvm ball verilog", - ) - - await check_result( - ctx, - result.returncode, - continue_run=False, - extra_fields={"task": "verilog", "balltype": balltype, "output_dir": build_dir}, - trace_id=origin_tid, - ) diff --git a/api/steps/uvm/01_build_api.step.py b/api/steps/uvm/01_build_api.step.py index 9f9a87e..6217fe5 100644 --- a/api/steps/uvm/01_build_api.step.py +++ b/api/steps/uvm/01_build_api.step.py @@ -10,20 +10,14 @@ def check_args(body: dict) -> str | None: - allowed = {"ball", "config", "core_config", "filelist"} + allowed = {"chip", "ball"} for key in body: if key not in allowed: return f"Unexpected parameter: --{key}" - config = body.get("config") - if not config or config is True: - return "Missing required parameter: --config=" - core_config = body.get("core_config") - if not core_config or core_config is True: - return "Missing required parameter: --core-config=" + if not body.get("chip") or body.get("chip") is True: + return "Missing required parameter: --chip" if body.get("ball") is True: return "Parameter --ball requires a value" - if body.get("filelist") is True: - return "Parameter --filelist requires a path value" return None diff --git a/api/steps/uvm/01_build_event.step.py b/api/steps/uvm/01_build_event.step.py index ac89ab5..0106e0a 100644 --- a/api/steps/uvm/01_build_event.step.py +++ b/api/steps/uvm/01_build_event.step.py @@ -13,7 +13,7 @@ from utils.event_common import check_result, get_origin_trace_id from utils.path import get_buckyball_path -from scripts.uvm_common import run_uvm_build +from scripts.uvm_common import run_chip config = { "name": "uvm-build", @@ -28,11 +28,25 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: origin_tid = get_origin_trace_id(input_data, ctx) bbdir = get_buckyball_path() - result, info = await asyncio.to_thread(run_uvm_build, bbdir, input_data, ctx) + try: + info = await asyncio.to_thread( + run_chip, bbdir, input_data["chip"], input_data.get("ball"), ctx, False + ) + except Exception as e: + ctx.logger.error(str(e)) + await check_result( + ctx, + 1, + continue_run=False, + extra_fields={"error": str(e)}, + trace_id=origin_tid, + ) + return + await check_result( ctx, - result.returncode, + 0, continue_run=False, - extra_fields=info, + extra_fields={"task": "build", **info}, trace_id=origin_tid, ) diff --git a/api/steps/uvm/02_run_api.step.py b/api/steps/uvm/02_run_api.step.py index 1724680..5cf65fd 100644 --- a/api/steps/uvm/02_run_api.step.py +++ b/api/steps/uvm/02_run_api.step.py @@ -10,20 +10,14 @@ def check_args(body: dict) -> str | None: - allowed = {"ball", "filelist", "test", "plusargs"} + allowed = {"chip", "ball"} for key in body: if key not in allowed: return f"Unexpected parameter: --{key}" - ball = body.get("ball") - if not ball or ball is True: - return "Missing required parameter: --ball=" - if body.get("filelist") is True: - return "Parameter --filelist requires a path value" - if body.get("test") is True: - return "Parameter --test requires a value" - plusargs = body.get("plusargs") - if not isinstance(plusargs, str) or not plusargs: - return "Missing required parameter: --plusargs='+BID='" + if not body.get("chip") or body.get("chip") is True: + return "Missing required parameter: --chip" + if body.get("ball") is True: + return "Parameter --ball requires a value" return None diff --git a/api/steps/uvm/02_run_event.step.py b/api/steps/uvm/02_run_event.step.py index efc7ef1..e948a6d 100644 --- a/api/steps/uvm/02_run_event.step.py +++ b/api/steps/uvm/02_run_event.step.py @@ -1,5 +1,5 @@ +import asyncio import os -import shlex import sys from motia import FlowContext, queue @@ -13,8 +13,7 @@ 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 scripts.uvm_common import checked_run_paths, default_test_name +from scripts.uvm_common import run_chip config = { "name": "uvm-run", @@ -28,98 +27,26 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: origin_tid = get_origin_trace_id(input_data, ctx) bbdir = get_buckyball_path() - info = {"task": "run", "ball": input_data.get("ball")} try: - paths = checked_run_paths(bbdir, input_data) - except Exception as e: - ctx.logger.error(str(e)) - await check_result( - ctx, - 1, - continue_run=False, - extra_fields={**info, "error": str(e)}, - trace_id=origin_tid, - ) - return - - info.update({ - "ball": paths["ball"], - "verify_dir": paths["verify_dir"], - "simv": paths["simv"], - "dpi_lib": paths["dpi_lib"], - }) - - if not os.path.isfile(paths["simv"]): - error = f"simv not found: {paths['simv']}. Run `bbdev uvm --build \"--config \"` first." - ctx.logger.error(error) - await check_result( - ctx, - 1, - continue_run=False, - extra_fields={**info, "error": error}, - trace_id=origin_tid, + info = await asyncio.to_thread( + run_chip, bbdir, input_data["chip"], input_data.get("ball"), ctx, True ) - return - - test = input_data.get("test") or default_test_name(paths["ball"]) - if test is True: - ctx.logger.error("Parameter --test requires a value") - await check_result( - ctx, - 1, - continue_run=False, - extra_fields={**info, "error": "invalid_test"}, - trace_id=origin_tid, - ) - return - - plusargs = input_data.get("plusargs") - if not isinstance(plusargs, str) or not plusargs: - ctx.logger.error("Missing required parameter: --plusargs='+BID='") - await check_result( - ctx, - 1, - continue_run=False, - extra_fields={**info, "error": "missing_plusargs"}, - trace_id=origin_tid, - ) - return - try: - plusarg_words = shlex.split(plusargs) - except ValueError as e: + except Exception as e: ctx.logger.error(str(e)) await check_result( ctx, 1, continue_run=False, - extra_fields={**info, "error": str(e)}, + extra_fields={"error": str(e)}, trace_id=origin_tid, ) return - script = ( - f"cd {shlex.quote(paths['verify_dir'])} && " - f"env LD_LIBRARY_PATH=\"$VCS_RUNTIME_LIBRARY_PATH\" {shlex.quote(paths['simv'])} " - f"-sv_lib {shlex.quote(paths['dpi_lib'])} " - f"+UVM_TESTNAME={shlex.quote(test)} " - + " ".join(shlex.quote(word) for word in plusarg_words) - ) - cmd = f"nix develop {shlex.quote(paths['verify_env'])} --command zsh -ic {shlex.quote(script)}" - - ctx.logger.info(f"Running UVM test {test} for ball {paths['ball']}") - run_result = await stream_run_logger_async( - cmd=cmd, - logger=ctx.logger, - cwd=bbdir, - stdout_prefix="uvm run", - stderr_prefix="uvm run", - ) - await check_result( ctx, - run_result.returncode, + 0, continue_run=False, - extra_fields={**info, "test": test}, + extra_fields={"task": "run", **info}, trace_id=origin_tid, ) diff --git a/api/steps/uvm/scripts/uvm_common.py b/api/steps/uvm/scripts/uvm_common.py index 6ac613d..6e6069d 100644 --- a/api/steps/uvm/scripts/uvm_common.py +++ b/api/steps/uvm/scripts/uvm_common.py @@ -1,358 +1,206 @@ import os -import re import shlex import sys import tomllib +from datetime import datetime +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 StreamResult, stream_run_logger - - -def default_test_name(ball: str) -> str: - if ball == "fp2int": - return "fp2int_scale_rows_test" - if ball == "int2fp": - return "int2fp_channel_two_rows_test" - return f"{ball}_ball_test" - - -def config_dir_name(config: str) -> str: - if not isinstance(config, str) or not re.fullmatch(r"[A-Za-z0-9_.-]+", config): - raise ValueError("invalid config name") - return config - - -def core_ball_isa_defines(bbdir: str, input_data: dict, ball: str) -> list[str]: - core_config = input_data.get("core_config") - if not isinstance(core_config, str) or not core_config: - raise ValueError("Missing required parameter: --core-config=") - path = core_config if os.path.isabs(core_config) else os.path.join(bbdir, core_config) - path = os.path.abspath(path) - if not os.path.isfile(path): - raise FileNotFoundError(f"core_config not found: {path}") - with open(path, "rb") as src: - core = tomllib.load(src) - rel_domain = core.get("balldomain") - if not isinstance(rel_domain, str) or not rel_domain: - raise ValueError(f"core_config missing balldomain: {path}") - domain_path = os.path.abspath(os.path.join(os.path.dirname(path), rel_domain)) - if not os.path.isfile(domain_path): - raise FileNotFoundError(f"balldomain not found: {domain_path}") - with open(domain_path, "rb") as src: - domain = tomllib.load(src) - entries = domain.get("ballISA") - if not isinstance(entries, list): - raise ValueError(f"ballISA missing from {domain_path}") - required = { - "fp2int": {"FP2INT": "FP2INT_FUNCT7"}, - "int2fp": { - "INT2FP_TENSOR": "INT2FP_TENSOR_FUNCT7", - "INT2FP_CHANNEL": "INT2FP_CHANNEL_FUNCT7", - }, - "smatmul": { - "SMATMUL_OS": "SMATMUL_OS_FUNCT7", - "SMATMUL_WS": "SMATMUL_WS_FUNCT7", - }, - "matadd": {"MATADD": "MATADD_FUNCT7"}, - "im2col": {"IM2COL": "IM2COL_FUNCT7"}, - "transpose": {"TRANSPOSE": "TRANSPOSE_FUNCT7"}, - }.get(ball) - if required is None: - return [] - values = {} - for entry in entries: - if not isinstance(entry, dict): - raise ValueError(f"invalid ballISA entry in {domain_path}") - mnemonic = entry.get("mnemonic") - funct7 = entry.get("funct7") - if mnemonic in required: - if not isinstance(funct7, int) or funct7 < 0 or funct7 > 127: - raise ValueError(f"invalid funct7 for {mnemonic} in {domain_path}") - if mnemonic in values: - raise ValueError(f"duplicate {mnemonic} in {domain_path}") - values[mnemonic] = funct7 - if set(values) != set(required): - raise ValueError(f"{domain_path} missing required {ball} ISA entries") - defines = [f"+define+{macro}={values[mnemonic]}" for mnemonic, macro in required.items()] - if ball == "smatmul": - mappings = domain.get("ballIdMappings") - if not isinstance(mappings, list): - raise ValueError(f"ballIdMappings missing from {domain_path}") - matching = [entry for entry in mappings - if isinstance(entry, dict) and entry.get("ballName") == "SMatMulBall"] - if len(matching) != 1: - raise ValueError(f"{domain_path} must define exactly one SMatMulBall mapping") - out_bw = matching[0].get("outBW") - if not isinstance(out_bw, int) or out_bw <= 0 or out_bw > 4 or 4 % out_bw: - raise ValueError(f"invalid SMatMulBall outBW in {domain_path}") - defines.append(f"+define+SMATMUL_OUT_BW={out_bw}") - return defines - - -def uvm_paths(bbdir: str, input_data: dict, ball_override: str | None = None) -> dict: - ball = ball_override or input_data.get("ball") - if not isinstance(ball, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", ball): - raise ValueError("invalid ball name") - - config = input_data.get("config") - config_dir = config_dir_name(config) if config else None - ball_dir = os.path.join(bbdir, "examples", "balls", ball) - verify_dir = os.path.join(ball_dir, "verify") - casegen_dir = os.path.join(verify_dir, "casegen") - verify_env = os.path.join(bbdir, "verify") - build_dir = os.path.join(verify_dir, "build") - sim_dir = os.path.join(build_dir, config_dir) if config_dir else os.path.join(build_dir, "current") - simv = os.path.join(sim_dir, "simv") - csrc_dir = os.path.join(sim_dir, "csrc") - core_config = input_data.get("core_config") - core_dir = None - if config_dir: - if not isinstance(core_config, str) or not core_config: - raise ValueError("Missing required parameter: --core-config=") - core_path = core_config if os.path.isabs(core_config) else os.path.join(bbdir, core_config) - core_dir = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(core_path)))) - rtl_dir = os.path.join(bbdir, "arch", "build", core_dir, config_dir) if config_dir else None - - return { - "ball": ball, - "config": config_dir, - "ball_dir": ball_dir, - "verify_dir": verify_dir, - "casegen_dir": casegen_dir, - "verify_env": verify_env, - "build_dir": build_dir, - "sim_dir": sim_dir, - "simv": simv, - "csrc_dir": csrc_dir, - "rtl_dir": rtl_dir, - } - - -def checked_paths(bbdir: str, input_data: dict, ball_override: str | None = None) -> dict: - paths = uvm_paths(bbdir, input_data, ball_override) - for key in ("ball_dir", "verify_dir", "casegen_dir", "verify_env"): - if not os.path.isdir(paths[key]): - raise FileNotFoundError(f"{key} not found: {paths[key]}") - if paths["rtl_dir"] and not os.path.isdir(paths["rtl_dir"]): - raise FileNotFoundError(f"rtl_dir not found: {paths['rtl_dir']}") - - cargo_toml = os.path.join(paths["casegen_dir"], "Cargo.toml") - if not os.path.isfile(cargo_toml): - raise FileNotFoundError(f"Cargo.toml not found: {cargo_toml}") - - filelist = resolve_filelist(paths["verify_dir"], input_data.get("filelist"), paths["ball"]) - crate = read_crate_name(cargo_toml) - dpi_lib = os.path.join(paths["casegen_dir"], "target", "debug", f"lib{crate.replace('-', '_')}") - - paths.update({ - "cargo_toml": cargo_toml, - "filelist": filelist, - "filelist_arg": prepare_filelist(paths, filelist), - "crate": crate, - "dpi_lib": dpi_lib, - }) - return paths - - -def checked_run_paths(bbdir: str, input_data: dict) -> dict: - paths = uvm_paths(bbdir, input_data) - for key in ("ball_dir", "verify_dir", "casegen_dir", "verify_env"): - if not os.path.isdir(paths[key]): - raise FileNotFoundError(f"{key} not found: {paths[key]}") - - cargo_toml = os.path.join(paths["casegen_dir"], "Cargo.toml") - if not os.path.isfile(cargo_toml): - raise FileNotFoundError(f"Cargo.toml not found: {cargo_toml}") - - crate = read_crate_name(cargo_toml) - dpi_lib = os.path.join(paths["casegen_dir"], "target", "debug", f"lib{crate.replace('-', '_')}") - paths.update({ - "cargo_toml": cargo_toml, - "crate": crate, - "dpi_lib": dpi_lib, - }) - return paths - - -def discover_uvm_balls(bbdir: str) -> list[str]: - balls_dir = os.path.join(bbdir, "examples", "balls") - if not os.path.isdir(balls_dir): - return [] - balls = [] - for name in sorted(os.listdir(balls_dir)): - if not re.fullmatch(r"[A-Za-z0-9_-]+", name): - continue - verify_dir = os.path.join(balls_dir, name, "verify") - filelist_dir = os.path.join(verify_dir, "filelists") - casegen_toml = os.path.join(verify_dir, "casegen", "Cargo.toml") - if os.path.isdir(filelist_dir) and os.path.isfile(casegen_toml): - balls.append(name) - return balls - - -def resolve_filelist(verify_dir: str, arg, ball: str) -> str: - if arg is True: - raise ValueError("parameter --filelist requires a path value") - - if arg: - path = arg if os.path.isabs(arg) else os.path.join(verify_dir, arg) - if not os.path.isfile(path): - raise FileNotFoundError(f"filelist not found: {path}") - return os.path.abspath(path) - - default = os.path.join(verify_dir, "filelists", f"{ball}_ball_toy.f") - if os.path.isfile(default): - return default - - filelist_dir = os.path.join(verify_dir, "filelists") - if not os.path.isdir(filelist_dir): - raise FileNotFoundError(f"filelist directory not found: {filelist_dir}") - found = [ - os.path.join(filelist_dir, name) - for name in os.listdir(filelist_dir) - if name.endswith(".f") +config_scripts = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "config", "scripts")) +if config_scripts not in sys.path: + sys.path.insert(0, config_scripts) + +import chip_pb2 +from utils.path import get_buckyball_path, log_dir +from utils.stream_run import stream_run_logger + + +def load_chip(bbdir: str, chip: str): + path = Path(bbdir) / "examples" / "chips" / chip / "configs" / "generated" / "chip.pb" + msg = chip_pb2.Chip() + msg.ParseFromString(path.read_bytes()) + if not msg.name or not msg.cores: + raise ValueError(f"empty chip.pb: {path}") + return msg + + +def ball_domain(chip): + d0 = chip.cores[0].balldomain + key = [(m.ball_id, m.ball_dir, m.in_bw, m.out_bw) for m in d0.mappings] + isa = [(e.mnemonic, e.funct7, e.bid) for e in d0.isa] + for core in chip.cores[1:]: + k = [(m.ball_id, m.ball_dir, m.in_bw, m.out_bw) for m in core.balldomain.mappings] + i = [(e.mnemonic, e.funct7, e.bid) for e in core.balldomain.isa] + if k != key or i != isa: + raise ValueError("chip.pb cores have different balldomains") + return d0 + + +def selected_mappings(domain, ball: str | None): + if ball is None: + return list(domain.mappings) + for m in domain.mappings: + if m.ball_dir == ball: + return [m] + raise ValueError(f"ball {ball!r} not in chip.pb") + + +def vcs_defines(domain, mapping): + defs = [ + f"+define+BB_IN_BW={mapping.in_bw}", + f"+define+BB_OUT_BW={mapping.out_bw}", + f"+define+BB_MMIO_READ_BW={mapping.mmio_read_bw}", + f"+define+BB_MMIO_WRITE_BW={mapping.mmio_write_bw}", ] - if len(found) != 1: - raise RuntimeError(f"expected exactly one filelist under {filelist_dir}, found {len(found)}") - return found[0] - - -def filelist_arg(verify_dir: str, filelist: str) -> str: - rel = os.path.relpath(filelist, verify_dir) - if rel.startswith(".."): - return filelist - return rel - - -def prepare_filelist(paths: dict, filelist: str) -> str: - os.makedirs(paths["sim_dir"], exist_ok=True) - generated = os.path.join(paths["sim_dir"], os.path.basename(filelist)) - rel_rtl_dir = os.path.relpath(paths["rtl_dir"], paths["verify_dir"]) if paths["rtl_dir"] else None - rel_uvm_dir = os.path.relpath(os.path.join(paths["verify_env"], "uvm"), paths["verify_dir"]) - with open(filelist, "r") as src, open(generated, "w") as dst: - for line in src: - original = line - line = line.replace("@UVM@", rel_uvm_dir) - if "@RTL@" in line: - if not rel_rtl_dir: - raise ValueError("filelist uses @RTL@ but --config was not provided") - line = line.replace("@RTL@", rel_rtl_dir) - elif re.search(r"(?:\.\./)+arch/build/[^/\s]+/", original): - raise ValueError("filelist must use @RTL@ instead of hard-coded arch/build/") - if re.search(r"(?:\.\./)+verify/uvm/", original): - raise ValueError("filelist must use @UVM@ instead of hard-coded verify/uvm") - dst.write(line) - return filelist_arg(paths["verify_dir"], generated) - - -def read_crate_name(cargo_toml: str) -> str: - with open(cargo_toml, "rb") as f: - data = tomllib.load(f) - name = data.get("package", {}).get("name") - if not name: - raise ValueError(f"package.name not found in {cargo_toml}") - return name - - -def failed_result(message: str) -> StreamResult: - return StreamResult(returncode=1, stdout="", stderr=message) - - -def run_uvm_build_one(bbdir: str, input_data: dict, ctx, ball: str) -> tuple[StreamResult, dict]: - try: - paths = checked_paths(bbdir, input_data, ball) - isa_defines = core_ball_isa_defines(bbdir, input_data, ball) - except Exception as e: - ctx.logger.error(str(e)) - return failed_result(str(e)), {"task": "build", "error": str(e)} - - info = { - "task": "build", - "ball": paths["ball"], - "config": paths["config"], - "verify_dir": paths["verify_dir"], - "filelist": paths["filelist"], - "resolved_filelist": os.path.join(paths["verify_dir"], paths["filelist_arg"]), - "simv": paths["simv"], - "dpi_lib": paths["dpi_lib"], - "core_config": input_data.get("core_config"), - } - - cargo_cmd = ( - f"nix develop {shlex.quote(paths['verify_env'])} --command " - f"cargo build --manifest-path {shlex.quote(paths['cargo_toml'])}" - ) - ctx.logger.info(f"Building UVM DPI reference for ball {paths['ball']}") - cargo_result = stream_run_logger( - cmd=cargo_cmd, - logger=ctx.logger, - cwd=bbdir, - stdout_prefix="uvm dpi build", - stderr_prefix="uvm dpi build", + for e in domain.isa: + if e.bid == mapping.ball_id: + defs.append(f"+define+{e.mnemonic}_FUNCT7={e.funct7}") + return defs + + +def _filelist(verify_dir: Path, ball_dir: str, uvm_rel: str, rtl_rel: str, sim_dir: Path) -> str: + src = verify_dir / "filelists" / f"{ball_dir}_ball.f" + dst = sim_dir / f"{ball_dir}_ball.f" + sim_dir.mkdir(parents=True, exist_ok=True) + text = src.read_text() + dst.write_text(text.replace("@UVM@", uvm_rel).replace("@RTL@", rtl_rel)) + return str(dst.relative_to(verify_dir)) + + +def build_ball(bbdir: str, chip_name: str, mill_cfg: str, domain, mapping, ctx) -> None: + ball = mapping.ball_dir + verify_dir = Path(bbdir) / "examples" / "balls" / ball / "verify" + casegen = verify_dir / "casegen" / "Cargo.toml" + rtl_dir = Path(bbdir) / "arch" / "build" / chip_name / mill_cfg + sim_dir = verify_dir / "build" / chip_name + uvm_rel = os.path.relpath(Path(bbdir) / "verify" / "uvm", verify_dir) + rtl_rel = os.path.relpath(rtl_dir, verify_dir) + flist = _filelist(verify_dir, ball, uvm_rel, rtl_rel, sim_dir) + cargo = ( + f"nix develop {shlex.quote(str(Path(bbdir) / 'verify'))} --command " + f"cargo build --manifest-path {shlex.quote(str(casegen))}" ) - if cargo_result.returncode != 0: - return cargo_result, info - + r = stream_run_logger(cmd=cargo, logger=ctx.logger, cwd=bbdir, stdout_prefix="uvm dpi", stderr_prefix="uvm dpi") + if r.returncode != 0: + raise RuntimeError(f"cargo build failed for {ball}") + crate = tomllib.loads(casegen.read_text()).get("package", {}).get("name") + if not crate: + raise ValueError(f"package.name missing in {casegen}") + simv = sim_dir / "simv" + csrc = sim_dir / "csrc" + hier = sim_dir / "cm_hier.cfg" + sim_dir.mkdir(parents=True, exist_ok=True) + hier.write_text("+tree tb_top.dut\n") script = ( - f"cd {shlex.quote(paths['verify_dir'])} && " - f"rm -rf {shlex.quote(paths['csrc_dir'])} {shlex.quote(paths['simv'])} {shlex.quote(paths['simv'])}.daidir && " - f"mkdir -p {shlex.quote(paths['sim_dir'])} {shlex.quote(paths['csrc_dir'])} && " + f"cd {shlex.quote(str(verify_dir))} && " + f"rm -rf {shlex.quote(str(csrc))} {shlex.quote(str(simv))} {shlex.quote(str(simv))}.daidir && " + f"mkdir -p {shlex.quote(str(sim_dir))} {shlex.quote(str(csrc))} && " "vcs -full64 -sverilog -timescale=1ns/1ps -debug_access+all " "${=VCS_UVM_ARGS} " - + " ".join(shlex.quote(value) for value in isa_defines) - + " " - f"-Mdir={shlex.quote(paths['csrc_dir'])} " - f"-o {shlex.quote(paths['simv'])} " - f"-f {shlex.quote(paths['filelist_arg'])}" + + " ".join(shlex.quote(d) for d in vcs_defines(domain, mapping)) + + f" -cm line+cond+tgl+assert -cm_hier {shlex.quote(str(hier))} " + f"-Mdir={shlex.quote(str(csrc))} -o {shlex.quote(str(simv))} " + f"-f {shlex.quote(flist)}" ) - vcs_cmd = f"nix develop {shlex.quote(paths['verify_env'])} --command zsh -c {shlex.quote(script)}" - - ctx.logger.info(f"Building UVM simulation for ball {paths['ball']} config {paths['config']}") - vcs_result = stream_run_logger( - cmd=vcs_cmd, - logger=ctx.logger, - cwd=bbdir, - stdout_prefix="uvm vcs build", - stderr_prefix="uvm vcs build", + vcs = f"nix develop {shlex.quote(str(Path(bbdir) / 'verify'))} --command zsh -c {shlex.quote(script)}" + r = stream_run_logger(cmd=vcs, logger=ctx.logger, cwd=bbdir, stdout_prefix="uvm vcs", stderr_prefix="uvm vcs") + if r.returncode != 0: + raise RuntimeError(f"vcs failed for {ball}") + + +def run_ball(bbdir: str, chip_name: str, mill_cfg: str, domain, mapping, ctx, cov_dir: str) -> None: + ball = mapping.ball_dir + verify_dir = Path(bbdir) / "examples" / "balls" / ball / "verify" + simv = verify_dir / "build" / chip_name / "simv" + if not simv.is_file(): + build_ball(bbdir, chip_name, mill_cfg, domain, mapping, ctx) + crate = tomllib.loads((verify_dir / "casegen" / "Cargo.toml").read_text())["package"]["name"] + dpi = verify_dir / "casegen" / "target" / "debug" / f"lib{crate.replace('-', '_')}" + test = f"{ball}_ball_test" + script = ( + f"cd {shlex.quote(str(verify_dir))} && " + f"env LD_LIBRARY_PATH=\"$VCS_RUNTIME_LIBRARY_PATH\" {shlex.quote(str(simv))} " + f"-sv_lib {shlex.quote(str(dpi))} " + f"+UVM_TESTNAME={shlex.quote(test)} +BID={mapping.ball_id} " + f"-cm line+cond+tgl+assert -cm_name {shlex.quote(test)}" ) - if vcs_result.returncode == 0: - current = os.path.join(paths["build_dir"], "current") - os.makedirs(paths["build_dir"], exist_ok=True) - tmp = f"{current}.tmp" - try: - os.unlink(tmp) - except FileNotFoundError: - pass - os.symlink(paths["config"], tmp) - os.replace(tmp, current) - return vcs_result, info - - -def run_uvm_build(bbdir: str, input_data: dict, ctx) -> tuple[StreamResult, dict]: - config = input_data.get("config") - if not config or config is True: - return failed_result("Missing required parameter: --config="), { - "task": "build", - "error": "missing_config", - } - - ball = input_data.get("ball") - balls = [ball] if ball and ball is not True else discover_uvm_balls(bbdir) - if not balls: - return failed_result("No UVM balls found"), {"task": "build", "error": "no_uvm_balls"} - - built = [] - for one_ball in balls: - result, info = run_uvm_build_one(bbdir, input_data, ctx, one_ball) - if result.returncode != 0: - return result, {**info, "built": built} - built.append(one_ball) - - return StreamResult(returncode=0, stdout="", stderr=""), { - "task": "build", - "config": config, - "built": built, - } + cmd = f"nix develop {shlex.quote(str(Path(bbdir) / 'verify'))} --command zsh -ic {shlex.quote(script)}" + r = stream_run_logger(cmd=cmd, logger=ctx.logger, cwd=bbdir, stdout_prefix="uvm run", stderr_prefix="uvm run") + if r.returncode != 0: + raise RuntimeError(f"uvm run failed for {ball} test={test}") + Path(cov_dir).mkdir(parents=True, exist_ok=True) + urg = ( + f"nix develop {shlex.quote(str(Path(bbdir) / 'verify'))} --command " + f"urg -dir {shlex.quote(str(simv))}.vdb -format text -report {shlex.quote(cov_dir)}" + ) + r = stream_run_logger(cmd=urg, logger=ctx.logger, cwd=str(verify_dir), stdout_prefix="uvm urg", stderr_prefix="uvm urg") + if r.returncode != 0: + raise RuntimeError(f"urg failed for {ball}") + + +def dashboard_summary(cov_dir: str) -> dict[str, str]: + path = Path(cov_dir) / "dashboard.txt" + lines = path.read_text().splitlines() + i = 0 + while i < len(lines) and "Total Coverage Summary" not in lines[i]: + i += 1 + if i >= len(lines): + raise RuntimeError(f"Total Coverage Summary missing in {path}") + i += 1 + while i < len(lines) and not lines[i].strip().startswith("SCORE"): + i += 1 + if i + 1 >= len(lines): + raise RuntimeError(f"SCORE row missing in {path}") + keys = lines[i].split() + vals = lines[i + 1].split() + if len(vals) != len(keys): + raise RuntimeError(f"SCORE/value mismatch in {path}: {keys!r} vs {vals!r}") + return dict(zip(keys, vals)) + + +def run_chip(bbdir: str, chip: str, ball: str | None, ctx, do_run: bool) -> dict: + msg = load_chip(bbdir, chip) + domain = ball_domain(msg) + mill_cfg = msg.mill.verilator_config + maps = selected_mappings(domain, ball) + ran = [] + covs = [] + run_root = None + if do_run: + stamp = datetime.now().strftime("%Y-%m-%d-%H-%M") + run_name = "all" if ball is None else maps[0].ball_dir + run_root = log_dir(bbdir, chip, "verilog", stamp, "uvm", run_name) + for m in maps: + if do_run: + cov = ( + os.path.join(run_root, m.ball_dir, "coverage") + if ball is None + else os.path.join(run_root, "coverage") + ) + run_ball(bbdir, chip, mill_cfg, domain, m, ctx, cov) + if not (Path(cov) / "dashboard.txt").is_file(): + raise RuntimeError(f"urg wrote no dashboard.txt under {cov}") + covs.append((m.ball_dir, cov)) + else: + build_ball(bbdir, chip, mill_cfg, domain, m, ctx) + ran.append(m.ball_dir) + info = {"chip": chip, "mill": mill_cfg, "balls": ran} + if do_run and ball is None: + index_dir = Path(run_root) / "coverage" + index_dir.mkdir(parents=True, exist_ok=True) + body = ["ball_dir score line cond toggle assert group result"] + for b, p in covs: + s = dashboard_summary(p) + body.append( + f"{b} {s['SCORE']} {s['LINE']} {s['COND']} {s['TOGGLE']} " + f"{s.get('ASSERT', '--')} {s['GROUP']} pass" + ) + index = index_dir / "index.txt" + index.write_text("\n".join(body) + "\n") + info["index"] = str(index) + info["log"] = run_root + elif do_run: + info["log"] = run_root + return info diff --git a/api/steps/vcs/01_clean_api.step.py b/api/steps/vcs/01_clean_api.step.py index 71adc3f..899228e 100644 --- a/api/steps/vcs/01_clean_api.step.py +++ b/api/steps/vcs/01_clean_api.step.py @@ -25,7 +25,7 @@ async def handler(req: ApiRequest, ctx: FlowContext) -> ApiResponse: "topic": "vcs.clean", "data": { "chip": chip, - "output_dir": rtl_dir(bbdir, chip, "verilog", body.get("output_dir")), + "output_dir": rtl_dir(bbdir, chip, "tapeout", body.get("output_dir")), "_trace_id": ctx.trace_id, }, } diff --git a/api/steps/vcs/01_clean_event.step.py b/api/steps/vcs/01_clean_event.step.py index 7b5b0b3..c7a9c7a 100644 --- a/api/steps/vcs/01_clean_event.step.py +++ b/api/steps/vcs/01_clean_event.step.py @@ -24,7 +24,8 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: origin_tid = get_origin_trace_id(input_data, ctx) bbdir = get_buckyball_path() - build_dir = rtl_dir(bbdir, input_data.get("config", "verilog"), input_data.get("output_dir")) + chip = input_data.get("chip") + build_dir = rtl_dir(bbdir, chip, "tapeout", input_data.get("output_dir")) artifact_dir = os.path.join(build_dir, "vcs") if os.path.isdir(artifact_dir): shutil.rmtree(artifact_dir) diff --git a/api/steps/vcs/03_build_api.step.py b/api/steps/vcs/03_build_api.step.py index c1f5d78..901fba7 100644 --- a/api/steps/vcs/03_build_api.step.py +++ b/api/steps/vcs/03_build_api.step.py @@ -30,7 +30,7 @@ async def handler(req: ApiRequest, ctx: FlowContext) -> ApiResponse: data = { "chip": chip, "jobs": jobs, - "output_dir": rtl_dir(bbdir, chip, "verilog", body.get("output_dir")), + "output_dir": rtl_dir(bbdir, chip, "tapeout", body.get("output_dir")), } await ctx.enqueue({"topic": "vcs.build", "data": {**data, "_trace_id": ctx.trace_id}}) return ApiResponse(status=202, body={"trace_id": ctx.trace_id}) diff --git a/api/steps/vcs/03_build_event.step.py b/api/steps/vcs/03_build_event.step.py index 7c7ca9a..52e2820 100644 --- a/api/steps/vcs/03_build_event.step.py +++ b/api/steps/vcs/03_build_event.step.py @@ -130,7 +130,7 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: return bbdir = get_buckyball_path() - build_dir = rtl_dir(bbdir, chip, "verilog", input_data.get("output_dir")) + build_dir = rtl_dir(bbdir, chip, "tapeout", input_data.get("output_dir")) vsrcs = sorted( path for path in ( glob.glob(f"{build_dir}/**/*.v", recursive=True) diff --git a/api/steps/vcs/04_sim_event.step.py b/api/steps/vcs/04_sim_event.step.py index b19cbef..1ea55cd 100644 --- a/api/steps/vcs/04_sim_event.step.py +++ b/api/steps/vcs/04_sim_event.step.py @@ -55,12 +55,12 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: if binary is None: await check_result(ctx, 1, extra_fields={"task": "sim", "error": f"binary not found: {binary_name}"}, trace_id=origin_tid) return - build_dir = rtl_dir(bbdir, chip, "verilog", input_data.get("output_dir")) + build_dir = rtl_dir(bbdir, chip, "tapeout", 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, + bbdir, chip, "tapeout", timestamp, "vcs", binary_name, input_data.get("output_dir"), )) run_log.mkdir(parents=True) diff --git a/api/steps/vcs/05_run_api.step.py b/api/steps/vcs/05_run_api.step.py index 2b2494a..f90f212 100644 --- a/api/steps/vcs/05_run_api.step.py +++ b/api/steps/vcs/05_run_api.step.py @@ -35,7 +35,7 @@ async def handler(req: ApiRequest, ctx: FlowContext) -> ApiResponse: "binary": binary, "batch": bool(body.get("batch", False)), "jobs": jobs, - "output_dir": rtl_dir(bbdir, chip, "verilog", body.get("output_dir")), + "output_dir": rtl_dir(bbdir, chip, "tapeout", body.get("output_dir")), "from_run_workflow": True, } await ctx.enqueue({"topic": "vcs.verilog", "data": {**data, "_trace_id": ctx.trace_id}}) diff --git a/api/steps/verilator/04_sim_event.step.py b/api/steps/verilator/04_sim_event.step.py index 961cb90..5bac2a8 100644 --- a/api/steps/verilator/04_sim_event.step.py +++ b/api/steps/verilator/04_sim_event.step.py @@ -1,4 +1,6 @@ import os +import shlex +import struct import subprocess import sys from datetime import datetime @@ -25,12 +27,77 @@ } +DRAM_BASE = 0x80000000 +DRAM_SIZE = 0x10000000 + + +def validate_bbsim_elf(path: str) -> tuple[bool, str]: + """Validate the ELF layout accepted by BBSimDRAM before starting Verilator.""" + try: + with open(path, "rb") as binary: + header = binary.read(64) + if len(header) != 64: + return False, "not a complete ELF64 header" + if header[:4] != b"\x7fELF" or header[4] != 2 or header[5] != 1: + return False, "requires a little-endian ELF64 binary" + + (_, _, machine, _, _, phoff, _, _, ehsize, phentsize, phnum, _, _, _) = ( + struct.unpack("<16sHHIQQQIHHHHHH", header) + ) + if machine != 243: + return False, "requires a RISC-V ELF" + if ehsize != 64 or phentsize != 56 or phnum == 0: + return False, "has an unsupported ELF program-header layout" + + file_size = os.fstat(binary.fileno()).st_size + if phoff + phnum * phentsize > file_size: + return False, "program headers extend past the end of the file" + + load_segments = 0 + for index in range(phnum): + binary.seek(phoff + index * phentsize) + program_header = binary.read(phentsize) + if len(program_header) != phentsize: + return False, "cannot read ELF program headers" + p_type, _, p_offset, _, p_paddr, p_filesz, p_memsz, _ = struct.unpack( + " p_memsz or p_offset + p_filesz > file_size: + return False, "has an invalid loadable segment" + if p_paddr < DRAM_BASE or p_paddr + p_memsz > DRAM_BASE + DRAM_SIZE: + return ( + False, + f"load segment {index} at 0x{p_paddr:x} is outside " + f"BBSimDRAM [0x{DRAM_BASE:x}, 0x{DRAM_BASE + DRAM_SIZE:x})", + ) + except (OSError, struct.error): + return False, "cannot read a valid ELF64 program-header table" + + if not load_segments: + return False, "has no loadable ELF segments" + return True, "" + + async def handler(input_data: dict, ctx: FlowContext) -> None: # ================================================================================== # Get simulation parameters # ================================================================================== origin_tid = get_origin_trace_id(input_data, ctx) - chip = require_chip(input_data) + try: + chip = require_chip(input_data) + except ValueError as error: + ctx.logger.error(str(error)) + await check_result( + ctx, + 1, + continue_run=False, + extra_fields={"error": "missing_chip"}, + trace_id=origin_tid, + ) + return bbdir = get_buckyball_path() build_dir = rtl_dir( bbdir, chip, "verilog", input_data.get("output_dir"), @@ -41,19 +108,58 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: binary_name = input_data.get("binary", "") if not binary_name: - raise ValueError("binary required") + await check_result( + ctx, + 1, + continue_run=False, + extra_fields={"error": "binary_required"}, + trace_id=origin_tid, + ) + return coverage = input_data.get("coverage", False) workload_root = workload_tests_root(bbdir, chip) binary_path = search_workload(workload_root, binary_name) ctx.logger.info(f"binary_path: {binary_path}") + if binary_path is None: + await check_result( + ctx, + 1, + continue_run=False, + extra_fields={ + "error": "binary_not_found", + "binary": binary_name, + "search_dir": workload_root, + }, + trace_id=origin_tid, + ) + return + valid_elf, elf_error = validate_bbsim_elf(binary_path) + if not valid_elf: + await check_result( + ctx, + 1, + continue_run=False, + extra_fields={ + "error": "unsupported_elf", + "binary": binary_path, + "detail": ( + "direct Verilator requires a baremetal RISC-V ELF in BBSimDRAM; " + f"{elf_error}. Use bebop-verilator for Linux ELFs" + ), + }, + trace_id=origin_tid, + ) + return topname = "BBSimHarness" run_log = log_dir(bbdir, chip, "verilog", timestamp, "verilator", binary_name, input_data.get("output_dir")) waveform_dir = os.path.join(run_log, "waveform") os.makedirs(run_log, exist_ok=True) - os.makedirs(waveform_dir, exist_ok=True) + no_wave = bool(input_data.get("no-wave", input_data.get("no_wave", False))) + if not no_wave: + os.makedirs(waveform_dir, exist_ok=True) coverage_flag = "" if coverage: @@ -61,8 +167,24 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: coverage_flag = f"+verilator+coverage+file+{coverage_dat_path}" bin_path = f"{build_dir}/obj_dir/V{topname}" + if not os.path.isfile(bin_path): + await check_result( + ctx, + 1, + continue_run=False, + extra_fields={"error": "simulator_not_found", "simulator": bin_path}, + trace_id=origin_tid, + ) + return batch = input_data.get("batch", False) + trace_names = [ + name + for name in ("itrace", "mtrace", "pmctrace", "ctrace", "banktrace") + if input_data.get(name, False) + ] + trace_arg = ",".join(trace_names) if trace_names else "none" + log_path = f"{run_log}/bdb.ndjson" stdout_path = f"{run_log}/stdout.log" meta_path = f"{run_log}/sim_meta.txt" @@ -89,15 +211,30 @@ def _gcc_lib_dir(soname: str) -> str: lib_dirs = [result_lib, _gcc_lib_dir("liblz4.so"), _gcc_lib_dir("libstdc++.so")] ld_lib_path = ":".join(dict.fromkeys(lib_dirs)) ctx.logger.info(f"LD_LIBRARY_PATH prefix: {ld_lib_path}") + sim_args = [ + bin_path, + "+permissive", + f"+elf={binary_path}", + ] + if batch: + sim_args.append("+batch") + if coverage_flag: + sim_args.append(coverage_flag) + if no_wave: + sim_args.append("+no-wave") + else: + sim_args.append(f"+fst={fst_path}") + sim_args.extend([ + f"+log={log_path}", + f"+stdout={stdout_path}", + f"+trace={trace_arg}", + "+permissive-off", + ]) sim_cmd = ( - f"export LD_LIBRARY_PATH=\"{ld_lib_path}:$LD_LIBRARY_PATH\"; " - f"export BDB_SIM_META=\"{meta_path}\"; " - f"{bin_path} +permissive " - f"+elf={binary_path} " - f"{'+batch ' if batch else ''}" - f"{coverage_flag + ' ' if coverage_flag else ''}" - f"+fst={fst_path} +log={log_path} +stdout={stdout_path} +trace=all +permissive-off " - f"{binary_path} 2> >(spike-dasm > {log_dir}/disasm.log)" + f"export LD_LIBRARY_PATH={shlex.quote(ld_lib_path)}:$LD_LIBRARY_PATH; " + f"export BDB_SIM_META={shlex.quote(meta_path)}; " + f"{shlex.join(sim_args)} " + f"2> >(spike-dasm > {shlex.quote(os.path.join(run_log, 'disasm.log'))})" ) script_dir = os.path.dirname(__file__) @@ -123,7 +260,7 @@ def _gcc_lib_dir(soname: str) -> str: "task": "sim", "binary": binary_path, "log_dir": run_log, - "waveform_dir": waveform_dir, + "waveform_dir": None if no_wave else waveform_dir, "timestamp": timestamp, "sim_meta": meta_path, } diff --git a/api/steps/verilator/05_run_api.step.py b/api/steps/verilator/05_run_api.step.py index c04c198..030fc22 100644 --- a/api/steps/verilator/05_run_api.step.py +++ b/api/steps/verilator/05_run_api.step.py @@ -57,6 +57,13 @@ async def handler(request: ApiRequest, ctx: FlowContext) -> ApiResponse: "binary": binary, "jobs": body.get("jobs", "16"), "batch": body.get("batch", False), + "coverage": body.get("coverage", False), + "no-wave": body.get("no-wave", body.get("no_wave", False)), + "itrace": body.get("itrace", False), + "mtrace": body.get("mtrace", False), + "pmctrace": body.get("pmctrace", False), + "ctrace": body.get("ctrace", False), + "banktrace": body.get("banktrace", False), "from_run_workflow": True, } diff --git a/api/steps/verilator/README.md b/api/steps/verilator/README.md index 5ca961b..8369f21 100644 --- a/api/steps/verilator/README.md +++ b/api/steps/verilator/README.md @@ -15,14 +15,21 @@ Hardware simulation workflow based on Verilator in the Buckyball framework, prov - Default value: `16` - **`binary`** [Required] - Test binary file path - Default value: `""` +- **`batch`** - Run without the interactive monitor +- **`no-wave`** - Disable FST waveform generation +- **`itrace`**, **`mtrace`**, **`pmctrace`**, **`ctrace`**, **`banktrace`** - Enable selected BDB traces + +Direct Verilator uses `BBSimHarness` and therefore requires a baremetal RISC-V +ELF linked into the simulated `0x80000000` DRAM window. Linux workload ELFs +must be run with `bebop-verilator` instead. **Example**: ```bash # bbdev wrapper -bbdev verilator --run "jobs 256 --binary ${buckyball}/bb-tests/output/toy/workloads/src/ctest_mvin_mvout_alternate_test-baremetal --batch" +bbdev verilator --run "--chip toy --jobs 16 --binary toy-toy-ctest-mvin_mvout_test-baremetal --batch" # Raw command -curl -X POST http://localhost:5000/verilator/run -H "Content-Type: application/json" -d '{"jobs": 8, "binary": "/home/user/test.elf"}' +curl -X POST http://localhost:5000/verilator/run -H "Content-Type: application/json" -d '{"chip":"toy", "jobs":8, "binary":"toy-toy-ctest-mvin_mvout_test-baremetal", "batch":true}' ``` @@ -77,6 +84,12 @@ curl -X POST http://localhost:5000/verilator/build -d '{"jobs": 16}' **Parameters**: - **`binary`** [Required] - Custom test binary file path +- **`batch`** - Run without the interactive monitor +- **`no-wave`** - Disable FST waveform generation +- **`itrace`**, **`mtrace`**, **`pmctrace`**, **`ctrace`**, **`banktrace`** - Enable selected BDB traces + +The binary may be a workload filename or an existing absolute path. It must be +a baremetal ELF; Linux workload ELFs require `bebop-verilator`. **Example**: ```bash diff --git a/api/steps/verilator/dramsim3.json b/api/steps/verilator/dramsim3.json deleted file mode 100644 index 01aba8e..0000000 --- a/api/steps/verilator/dramsim3.json +++ /dev/null @@ -1 +0,0 @@ -{"0":{"act_energy":43200.0,"act_stb_energy":{"0":3013588.8000000003,"1":1462579.2},"all_bank_idle_cycles":{"0":33121,"1":39365},"average_bandwidth":0.13577000419861668,"average_interarrival":313.7708333333333,"average_power":440.3248756988487,"average_read_latency":17.416666666666668,"channel":0,"epoch_num":0,"hbm_dual_cmds":0,"interarrival_latency":{"1":2,"103":1,"106":1,"107":1,"113":1,"117":1,"12":1,"1216":1,"123":1,"124":1,"127":1,"14":1,"143":1,"15":1,"151":1,"164":1,"172":1,"18":1,"183":1,"2":1,"212":1,"262":1,"266":1,"279":1,"3":3,"301":1,"315":1,"32":14,"32868":1,"350":1,"4":1,"45":6,"46":3,"47":4,"48":2,"49":7,"5":2,"50":3,"51":5,"52":3,"53":1,"54":1,"55":17,"56":1,"57":1,"58":3,"59":3,"60":6,"63":1,"64":2,"65":4,"662":1,"67":3,"68":1,"69":2,"70":2,"74":1,"75":1,"76":1,"79":1,"803":1,"81":1,"82":2,"86":1,"9":1,"91":1,"93":1,"94":1},"interarrival_latency[-0]":0,"interarrival_latency[0-9]":10,"interarrival_latency[10-19]":4,"interarrival_latency[100-]":24,"interarrival_latency[20-29]":0,"interarrival_latency[30-39]":14,"interarrival_latency[40-49]":22,"interarrival_latency[50-59]":38,"interarrival_latency[60-69]":19,"interarrival_latency[70-79]":6,"interarrival_latency[80-89]":4,"interarrival_latency[90-99]":3,"num_act_cmds":10,"num_cycles":45253,"num_ondemand_pres":0,"num_pre_cmds":8,"num_read_cmds":144,"num_read_row_hits":134,"num_reads_done":144,"num_ref_cmds":17,"num_refb_cmds":0,"num_srefe_cmds":0,"num_srefx_cmds":0,"num_write_buf_hits":0,"num_write_cmds":0,"num_write_row_hits":0,"num_writes_done":0,"pre_stb_energy":{"0":6081015.600000001,"1":7227414.000000001},"rank_active_cycles":{"0":12132,"1":5888},"read_energy":304819.2,"read_latency":{"15":125,"16":1,"17":3,"18":2,"19":1,"25":7,"33":1,"61":1,"73":1,"80":1,"89":1},"read_latency[-0]":0,"read_latency[0-19]":132,"read_latency[100-119]":0,"read_latency[120-139]":0,"read_latency[140-159]":0,"read_latency[160-179]":0,"read_latency[180-199]":0,"read_latency[20-39]":8,"read_latency[200-]":0,"read_latency[40-59]":0,"read_latency[60-79]":2,"read_latency[80-99]":2,"ref_energy":1793404.8,"refb_energy":-0.0,"sref_cycles":{"0":0,"1":0},"sref_energy":{"0":0.0,"1":0.0},"total_energy":19926021.6,"write_energy":0.0,"write_latency":null,"write_latency[-0]":0,"write_latency[0-19]":0,"write_latency[100-119]":0,"write_latency[120-139]":0,"write_latency[140-159]":0,"write_latency[160-179]":0,"write_latency[180-199]":0,"write_latency[20-39]":0,"write_latency[200-]":0,"write_latency[40-59]":0,"write_latency[60-79]":0,"write_latency[80-99]":0}} \ No newline at end of file diff --git a/api/steps/verilator/dramsim3.txt b/api/steps/verilator/dramsim3.txt deleted file mode 100644 index a479c6c..0000000 --- a/api/steps/verilator/dramsim3.txt +++ /dev/null @@ -1,78 +0,0 @@ -########################################### -## Statistics of Channel 0 -########################################### -num_srefx_cmds = 0 # Number of SREFX commands -num_srefe_cmds = 0 # Number of SREFE commands -num_refb_cmds = 0 # Number of REFb commands -num_cycles = 45253 # Number of DRAM cycles -epoch_num = 0 # Number of epochs -num_write_buf_hits = 0 # Number of write buffer hits -num_write_cmds = 0 # Number of WRITE/WRITEP commands -num_reads_done = 144 # Number of read requests issued -hbm_dual_cmds = 0 # Number of cycles dual cmds issued -num_ref_cmds = 17 # Number of REF commands -num_read_row_hits = 134 # Number of read row buffer hits -num_read_cmds = 144 # Number of READ/READP commands -num_writes_done = 0 # Number of read requests issued -num_write_row_hits = 0 # Number of write row buffer hits -num_act_cmds = 10 # Number of ACT commands -num_pre_cmds = 8 # Number of PRE commands -num_ondemand_pres = 0 # Number of ondemend PRE commands -sref_cycles.0 = 0 # Cyles of rank in SREF mode rank.0 -sref_cycles.1 = 0 # Cyles of rank in SREF mode rank.1 -rank_active_cycles.0 = 12132 # Cyles of rank active rank.0 -rank_active_cycles.1 = 5888 # Cyles of rank active rank.1 -all_bank_idle_cycles.0 = 33121 # Cyles of all bank idle in rank rank.0 -all_bank_idle_cycles.1 = 39365 # Cyles of all bank idle in rank rank.1 -interarrival_latency[-0] = 0 # Request interarrival latency (cycles) -interarrival_latency[0-9] = 10 # Request interarrival latency (cycles) -interarrival_latency[10-19] = 4 # Request interarrival latency (cycles) -interarrival_latency[20-29] = 0 # Request interarrival latency (cycles) -interarrival_latency[30-39] = 14 # Request interarrival latency (cycles) -interarrival_latency[40-49] = 22 # Request interarrival latency (cycles) -interarrival_latency[50-59] = 38 # Request interarrival latency (cycles) -interarrival_latency[60-69] = 19 # Request interarrival latency (cycles) -interarrival_latency[70-79] = 6 # Request interarrival latency (cycles) -interarrival_latency[80-89] = 4 # Request interarrival latency (cycles) -interarrival_latency[90-99] = 3 # Request interarrival latency (cycles) -interarrival_latency[100-] = 24 # Request interarrival latency (cycles) -write_latency[-0] = 0 # Write cmd latency (cycles) -write_latency[0-19] = 0 # Write cmd latency (cycles) -write_latency[20-39] = 0 # Write cmd latency (cycles) -write_latency[40-59] = 0 # Write cmd latency (cycles) -write_latency[60-79] = 0 # Write cmd latency (cycles) -write_latency[80-99] = 0 # Write cmd latency (cycles) -write_latency[100-119] = 0 # Write cmd latency (cycles) -write_latency[120-139] = 0 # Write cmd latency (cycles) -write_latency[140-159] = 0 # Write cmd latency (cycles) -write_latency[160-179] = 0 # Write cmd latency (cycles) -write_latency[180-199] = 0 # Write cmd latency (cycles) -write_latency[200-] = 0 # Write cmd latency (cycles) -read_latency[-0] = 0 # Read request latency (cycles) -read_latency[0-19] = 132 # Read request latency (cycles) -read_latency[20-39] = 8 # Read request latency (cycles) -read_latency[40-59] = 0 # Read request latency (cycles) -read_latency[60-79] = 2 # Read request latency (cycles) -read_latency[80-99] = 2 # Read request latency (cycles) -read_latency[100-119] = 0 # Read request latency (cycles) -read_latency[120-139] = 0 # Read request latency (cycles) -read_latency[140-159] = 0 # Read request latency (cycles) -read_latency[160-179] = 0 # Read request latency (cycles) -read_latency[180-199] = 0 # Read request latency (cycles) -read_latency[200-] = 0 # Read request latency (cycles) -ref_energy = 1.7934e+06 # Refresh energy -refb_energy = -0 # Refresh-bank energy -write_energy = 0 # Write energy -read_energy = 304819 # Read energy -act_energy = 43200 # Activation energy -sref_energy.0 = 0 # SREF energy rank.0 -sref_energy.1 = 0 # SREF energy rank.1 -pre_stb_energy.0 = 6.08102e+06 # Precharge standby energy rank.0 -pre_stb_energy.1 = 7.22741e+06 # Precharge standby energy rank.1 -act_stb_energy.0 = 3.01359e+06 # Active standby energy rank.0 -act_stb_energy.1 = 1.46258e+06 # Active standby energy rank.1 -average_read_latency = 17.4167 # Average read request latency (cycles) -average_interarrival = 313.771 # Average request interarrival latency (cycles) -total_energy = 1.9926e+07 # Total energy (pJ) -average_power = 440.325 # Average power (mW) -average_bandwidth = 0.13577 # Average bandwidth diff --git a/api/steps/workload/00_clean_event.step.py b/api/steps/workload/00_clean_event.step.py index 29e1d68..a710be5 100644 --- a/api/steps/workload/00_clean_event.step.py +++ b/api/steps/workload/00_clean_event.step.py @@ -10,7 +10,7 @@ from utils.event_common import require_chip from utils.event_common import check_result, get_origin_trace_id -from utils.path import chip_output_root, get_buckyball_path +from utils.path import chip_output_root, get_buckyball_path, workload_build_dir config = { "name": "workload-clean", @@ -50,18 +50,25 @@ async def handler(input_data: dict, ctx: FlowContext) -> None: return bbdir = get_buckyball_path() - path = chip_output_root(bbdir, chip) + output_path = chip_output_root(bbdir, chip) + build_path = workload_build_dir(bbdir, chip) - if os.path.exists(path): - ctx.logger.info("Removing workload output directory", {"path": path}) - shutil.rmtree(path) - else: - ctx.logger.info("Workload output directory already clean", {"path": path}) + for path in (output_path, build_path): + if os.path.exists(path): + ctx.logger.info("Removing workload directory", {"path": path}) + shutil.rmtree(path) + else: + ctx.logger.info("Workload directory already clean", {"path": path}) await check_result( ctx, 0, continue_run=False, - extra_fields={"task": "clean", "chip": chip, "path": path}, + extra_fields={ + "task": "clean", + "chip": chip, + "output_path": output_path, + "build_path": build_path, + }, trace_id=origin_tid, ) 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/tests/test_api_verilator_run.py b/api/tests/test_api_verilator_run.py index dbd5fb3..c126911 100644 --- a/api/tests/test_api_verilator_run.py +++ b/api/tests/test_api_verilator_run.py @@ -1,3 +1,3 @@ from _api_test_helper import run_bbdev_case -run_bbdev_case("bbdev verilator --run '--jobs 16 --binary toy_relu_test-singlecore-baremetal --chip toy --batch'") +run_bbdev_case("bbdev verilator --run '--jobs 16 --binary toy-toy-ctest-mvin_mvout_test-baremetal --chip toy --batch'") diff --git a/api/tests/test_api_verilator_sim.py b/api/tests/test_api_verilator_sim.py index 99addce..6c9ad70 100644 --- a/api/tests/test_api_verilator_sim.py +++ b/api/tests/test_api_verilator_sim.py @@ -1,3 +1,3 @@ from _api_test_helper import run_bbdev_case -run_bbdev_case("bbdev verilator --sim '--binary toy_relu_test-singlecore-baremetal --batch --chip toy'") +run_bbdev_case("bbdev verilator --sim '--binary toy-toy-ctest-mvin_mvout_test-baremetal --batch --chip toy'") diff --git a/api/tests/test_uvm_chip_pb.py b/api/tests/test_uvm_chip_pb.py new file mode 100644 index 0000000..6aa2cb7 --- /dev/null +++ b/api/tests/test_uvm_chip_pb.py @@ -0,0 +1,53 @@ +import sys +from pathlib import Path + +API = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(API / "steps" / "config" / "scripts")) +sys.path.insert(0, str(API / "steps" / "uvm" / "scripts")) + +import chip_pb2 +from uvm_common import ball_domain, selected_mappings, vcs_defines + + +def _chip(): + c = chip_pb2.Chip() + core = c.cores.add() + m = core.balldomain.mappings.add() + m.ball_id = 5 + m.ball_dir = "lut" + m.in_bw = 2 + m.out_bw = 1 + isa = core.balldomain.isa.add() + isa.mnemonic = "LUT" + isa.funct7 = 66 + isa.bid = 5 + return c + + +def test_defines(): + c = _chip() + d = ball_domain(c) + m = selected_mappings(d, "lut")[0] + defs = vcs_defines(d, m) + assert defs == [ + "+define+BB_IN_BW=2", + "+define+BB_OUT_BW=1", + "+define+BB_MMIO_READ_BW=0", + "+define+BB_MMIO_WRITE_BW=0", + "+define+LUT_FUNCT7=66", + ] + + +def test_unknown_ball(): + try: + selected_mappings(ball_domain(_chip()), "transpose") + except ValueError as e: + assert "transpose" in str(e) + return + raise AssertionError("expected ValueError") + + +if __name__ == "__main__": + test_defines() + test_unknown_ball() + print("ok") diff --git a/api/utils/path.py b/api/utils/path.py index e61fc89..60e332e 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,13 @@ 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 == "tapeout": + sim_key = "tapeout" + elif product == "p2e": + sim_key = "p2e" + else: raise ValueError(f"invalid rtl product: {product}") path = ( Path(bbdir) @@ -80,7 +84,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/api/utils/search_workload.py b/api/utils/search_workload.py index 0d4121f..21ec946 100644 --- a/api/utils/search_workload.py +++ b/api/utils/search_workload.py @@ -13,6 +13,9 @@ def search_workload(search_dir: str, filename: str) -> Optional[str]: Returns: Absolute path of the found file, or None if not found """ + if os.path.isfile(filename): + return os.path.abspath(filename) + if not os.path.exists(search_dir): return None diff --git a/bbdev b/bbdev index 933af68..4e0464c 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 ]"', @@ -166,8 +167,16 @@ def cleanup_config(config_path: str): def load_host_environment() -> None: - """No-op placeholder: host EDA env (TARGET_LIBRARY etc.) comes from the user shell (zshrc).""" - return + """Import the host login-shell environment used by the EDA tools.""" + result = subprocess.run( + ["zsh", "-lic", "env -0"], + check=True, + capture_output=True, + ) + for item in result.stdout.split(b"\0"): + if item: + key, value = item.split(b"=", 1) + os.environ[key.decode()] = value.decode() def parse_args(argv: list[str]) -> argparse.Namespace: @@ -205,10 +214,16 @@ def parse_args(argv: list[str]) -> argparse.Namespace: group = cmd_parser.add_mutually_exclusive_group(required=True) for op_name, op_help in operations.items(): if op_help is True: - group.add_argument(f"--{op_name}", action="store_true", help=f"{op_name.capitalize()}.") + group.add_argument( + f"--{op_name}", + dest=op_name, + action="store_true", + help=f"{op_name.capitalize()}.", + ) else: group.add_argument( f"--{op_name}", + dest=op_name, type=str, nargs="?", const="", @@ -555,7 +570,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/common.py b/mcp/common.py index b7ec6ad..d9938bd 100644 --- a/mcp/common.py +++ b/mcp/common.py @@ -433,13 +433,6 @@ def _validate(path: Path) -> Dict[str, Any]: {"ballName": name, "config": cfg_rel, "resolved": str(cfg_path)} ) continue - ball_cfg = _load_toml(cfg_path) - ball = ball_cfg.get("ball") - if not isinstance(ball, dict): - missing_config.append( - {"ballName": name, "config": cfg_rel, "error": "missing [ball]"} - ) - funct7s = [e.get("funct7") for e in isa] mnemonics = [e.get("mnemonic") for e in isa] bids = [e.get("bid") for e in isa] 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, + ), ) ) diff --git a/mcp/tools/uvm_build.py b/mcp/tools/uvm_build.py index d56a14a..0d4792c 100644 --- a/mcp/tools/uvm_build.py +++ b/mcp/tools/uvm_build.py @@ -2,24 +2,18 @@ from __future__ import annotations -from typing import Any, Dict, Optional +from typing import Optional -from common import submit, err, fmt, need, opt +from common import submit, err, fmt, need def register(mcp): @mcp.tool() - def bbdev_uvm_build( - config: str, core_config: str, ball: Optional[str] = None, filelist: Optional[str] = None - ) -> str: + def bbdev_uvm_build(chip: str, ball: Optional[str] = None) -> str: """Build a Ball UVM simulation. POST /uvm/build.""" - if e := need("config", config): + if e := need("chip", chip): return err(e) - if e := need("core_config", core_config): - return err(e) - params: Dict[str, Any] = {"config": config, "core_config": core_config} + params = {"chip": chip} if ball: params["ball"] = ball - if filelist: - params["filelist"] = filelist return fmt(submit("/uvm/build", params)) diff --git a/mcp/tools/uvm_run.py b/mcp/tools/uvm_run.py index 7e763df..93f537c 100644 --- a/mcp/tools/uvm_run.py +++ b/mcp/tools/uvm_run.py @@ -2,25 +2,18 @@ from __future__ import annotations -from typing import Any, Dict, Optional +from typing import Optional -from common import submit, err, fmt, need, opt +from common import submit, err, fmt, need def register(mcp): @mcp.tool() - def bbdev_uvm_run( - ball: str, - filelist: Optional[str] = None, - test: Optional[str] = None, - ) -> str: + def bbdev_uvm_run(chip: str, ball: Optional[str] = None) -> str: """Build and run a Ball UVM simulation. POST /uvm/run.""" - if e := need("ball", ball): + if e := need("chip", chip): return err(e) - params: Dict[str, Any] = {"ball": ball} - if filelist: - params["filelist"] = filelist - if test: - params["test"] = test + params = {"chip": chip} + if ball: + params["ball"] = ball return fmt(submit("/uvm/run", params)) - diff --git a/mcp/tools/verilator_run.py b/mcp/tools/verilator_run.py index 918766e..4aa7592 100644 --- a/mcp/tools/verilator_run.py +++ b/mcp/tools/verilator_run.py @@ -12,8 +12,9 @@ def register(mcp): def bbdev_verilator_run( binary: str, chip: str, - batch: bool = True, + batch: bool = False, coverage: bool = False, + no_wave: bool = False, jobs: Optional[int] = None, itrace: bool = False, mtrace: bool = False, @@ -31,6 +32,7 @@ def bbdev_verilator_run( "chip": chip, "batch": batch, "coverage": coverage, + "no-wave": no_wave, "itrace": itrace, "mtrace": mtrace, "pmctrace": pmctrace, diff --git a/mcp/tools/verilator_sim.py b/mcp/tools/verilator_sim.py index fc40ef7..ad2df68 100644 --- a/mcp/tools/verilator_sim.py +++ b/mcp/tools/verilator_sim.py @@ -12,8 +12,9 @@ def register(mcp): def bbdev_verilator_sim( binary: str, chip: str, - batch: bool = True, + batch: bool = False, coverage: bool = False, + no_wave: bool = False, itrace: bool = False, mtrace: bool = False, pmctrace: bool = False, @@ -28,6 +29,7 @@ def bbdev_verilator_sim( "binary": binary, "chip": chip, "batch": batch, + "no-wave": no_wave, "itrace": itrace, "mtrace": mtrace, "pmctrace": pmctrace,