From 3fff266e5a27adb21a2c58d8f52600e4d69bba39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Sat, 23 Nov 2024 15:16:47 +0100 Subject: [PATCH] sweep: add sweep macro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make sweep in megaboom reusable Signed-off-by: Øyvind Harboe --- .github/scripts/smoketests.sh | 2 +- BUILD | 107 ++++++---------- sweep-wns.tcl | 12 ++ sweep.bzl | 141 +++++++++++++++++++++ wns_report.py | 230 ++++++++++++++++++++++++++++++++++ wns_report_test.py | 36 ++++++ write_binary.bzl | 18 +++ 7 files changed, 479 insertions(+), 67 deletions(-) create mode 100644 sweep-wns.tcl create mode 100644 sweep.bzl create mode 100755 wns_report.py create mode 100755 wns_report_test.py create mode 100644 write_binary.bzl diff --git a/.github/scripts/smoketests.sh b/.github/scripts/smoketests.sh index be5596c..2a5e0da 100755 --- a/.github/scripts/smoketests.sh +++ b/.github/scripts/smoketests.sh @@ -8,7 +8,7 @@ bazel query //:* | grep -q -v lb_32x128_3_place echo This target should exist bazel query //:* | grep -q -v lb_32x128_4_synth -bazel build check_mock_area cell_count lb_32x128_shared_synth_floorplan wns_report //sram:sdq_17x64_mock-naja_floorplan_deps //sram:mock-naja +bazel build check_mock_area cell_count lb_32x128_shared_synth_floorplan lb_32x128_wns_report //sram:sdq_17x64_mock-naja_floorplan_deps //sram:mock-naja grep naja bazel-bin/sram/mock-naja.v grep -q naja bazel-bin/sram/results/asap7/sdq_17x64/mock-naja/1_synth.v && false || true (bazel build //sram:sdq_17x64_naja-error_floorplan 2>&1 || true) | grep "syntax error" diff --git a/BUILD b/BUILD index 080f2e0..57d2ab1 100644 --- a/BUILD +++ b/BUILD @@ -1,5 +1,6 @@ load("//:eqy.bzl", "eqy_test") load("//:openroad.bzl", "get_stage_args", "orfs_floorplan", "orfs_flow", "orfs_run") +load("//:sweep.bzl", "sweep") exports_files(["mock_area.tcl"]) @@ -146,60 +147,6 @@ orfs_flow( verilog_files = LB_VERILOG_FILES, ) -SWEEP = { - "1": { - "variables": { - "PLACE_DENSITY": "0.65", - }, - "previous_stage": {"floorplan": "lb_32x128_synth"}, - }, - "2": { - "variables": { - "PLACE_DENSITY": "0.70", - }, - "previous_stage": {"place": "lb_32x128_floorplan"}, - }, - "3": { - "variables": { - "PLACE_DENSITY": "0.75", - }, - "previous_stage": {"cts": "lb_32x128_place"}, - }, - "4": { - "variables": { - "PLACE_DENSITY": "0.80", - }, - }, -} - -# buildifier: disable=duplicated-name -[ - orfs_flow( - name = "lb_32x128", - abstract_stage = "cts", - arguments = LB_ARGS | SWEEP[variant]["variables"], - # Share synthesis across all variants, the sweep - # differs from floorplan and onwards - previous_stage = SWEEP[variant].get("previous_stage", {}), - stage_sources = LB_STAGE_SOURCES, - variant = variant, - verilog_files = LB_VERILOG_FILES, - ) - for variant in SWEEP -] - -[orfs_run( - name = "lb_32x128_" + variant + "_report", - src = ":lb_32x128_" + ("" if variant == "base" else variant + "_cts"), - outs = [ - "lb_32x128_" + variant + ".yaml", - ], - arguments = { - "OUTFILE": "$(location :lb_32x128_" + variant + ".yaml)", - }, - script = ":report-wns.tcl", -) for variant in SWEEP] - orfs_run( name = "cell_count", src = ":lb_32x128_floorplan", @@ -210,18 +157,6 @@ orfs_run( script = ":cell_count.tcl", ) -genrule( - name = "wns_report", - srcs = ["wns-report.py"] + - [":lb_32x128_" + variant + ".yaml" for variant in SWEEP], - outs = ["lb_32x128_wns_report.md"], - cmd = ( - "$(location :wns-report.py) > $@ " + - " ".join(["$(location :lb_32x128_" + variant + ".yaml)" for variant in SWEEP]) - ), - visibility = ["//visibility:public"], -) - orfs_flow( name = "L1MetadataArray", abstract_stage = "cts", @@ -356,3 +291,43 @@ orfs_flow( "test/rtl/regfile_128x65.sv", ], ) + +# buildifier: disable=duplicated-name +sweep( + name = "lb_32x128", + stage = "cts", + stage_sources = { + "synth": [":constraints-sram"], + "floorplan": [":io-sram"], + "place": [":io-sram"], + }, + sweep = { + "1": { + "variables": { + "PLACE_DENSITY": "0.65", + }, + "previous_stage": {"floorplan": "lb_32x128_synth"}, + }, + "2": { + "variables": { + "PLACE_DENSITY": "0.70", + }, + "previous_stage": {"place": "lb_32x128_floorplan"}, + }, + "3": { + "variables": { + "PLACE_DENSITY": "0.75", + }, + "previous_stage": {"cts": "lb_32x128_place"}, + }, + "4": { + "variables": { + "PLACE_DENSITY": "0.80", + }, + }, + }, + variables = LB_ARGS, + verilog_files = ["test/mock/lb_32x128.sv"], +) + +exports_files(["sweep-wns.tcl"]) diff --git a/sweep-wns.tcl b/sweep-wns.tcl new file mode 100644 index 0000000..f896420 --- /dev/null +++ b/sweep-wns.tcl @@ -0,0 +1,12 @@ +# Test this on some simple design in ORFS: +# make floorplan +# ODB_FILE=results/nangate45/gcd/base/2_floorplan.odb make run RUN_SCRIPT=~/megaboom/report-wns.tcl +source $::env(SCRIPTS_DIR)/open.tcl + +set paths [find_timing_paths -path_group reg2reg -sort_by_slack -group_count 1] +set path [lindex $paths 0] +set slack [get_property $path slack] + +puts "slack: $slack" +report_tns +report_cell_usage diff --git a/sweep.bzl b/sweep.bzl new file mode 100644 index 0000000..f4dcaea --- /dev/null +++ b/sweep.bzl @@ -0,0 +1,141 @@ +"""Sweep OpenROAD stages""" + +load("@bazel-orfs//:openroad.bzl", "orfs_flow", "orfs_run", "set") +load(":write_binary.bzl", "write_binary") + +all_stages = [ + "floorplan", + "place", + "cts", + "grt", + "route", + "final", +] + +def sweep( + name, + variables, + sweep, + verilog_files, + stage_sources, + other_variants = {}, + stage = "floorplan", + macros = []): + """Run a sweep of OpenROAD stages + + Args: + name: Verilog module name + variables: dictionary of the base variables for the flow + sweep: The dictionary describing the variables to sweep + other_variants: Dictionary with other variants to generate, but not as part of the sweep + stage: The stage to do the sweep on + macros: name of modules to use as macros + verilog_files: The Verilog files to build + stage_sources: dictionary with list of sources to use for the stage + """ + sweep_json = { + "name": name, + "base": variables, + "sweep": sweep, + "stage": stage, + "stages": all_stages[0:all_stages.index(stage) + 1], + } + write_binary( + name = name + "_sweep.json", + data = str(sweep_json), + ) + + all_variants = sweep | other_variants + + for variant in all_variants: + orfs_flow( + name = name, + arguments = variables | all_variants[variant].get("variables", {}), + macros = [ + m + for m in macros + if m not in all_variants[variant].get("dissolve", []) + ] + all_variants[variant].get("macros", []), + previous_stage = all_variants[variant].get("previous_stage", {}), + renamed_inputs = all_variants[variant].get("renamed_inputs", {}), + stage_arguments = all_variants[variant].get("stage_arguments", {}), + stage_sources = { + stage: set(stage_sources.get(stage, []) + all_variants[variant].get("stage_sources", {}).get(stage, [])) + for stage in set(stage_sources.keys() + all_variants[variant].get("stage_sources", {}).keys()) + }, + variant = variant, + verilog_files = verilog_files, + ) + + native.filegroup( + name = name + "_" + variant + "_odb", + srcs = [":" + name + "_" + ("" if variant == "base" else variant + "_") + sweep_json["stage"]], + output_group = ("5_1_grt" if sweep_json["stage"] == "grt" else str(sweep_json["stages"].index(sweep_json["stage"]) + 2) + "_" + sweep_json["stage"]) + + ".odb", + visibility = [":__subpackages__"], + ) + + orfs_run( + name = name + "_" + variant + "_report", + src = ":" + name + "_" + ("" if variant == "base" else variant + "_") + sweep_json["stage"], + outs = [ + name + "_" + variant + ".txt", + ], + arguments = { + "ODB_FILE": "$(location :" + name + "_" + variant + "_odb)", + }, + data = [":" + name + "_" + variant + "_odb"], + extra_args = "> $WORK_HOME/" + name + "_" + variant + ".txt", + script = Label(":sweep-wns.tcl"), + ) + + native.filegroup( + name = name + "_" + variant + "_logs", + srcs = [":" + name + "_" + ("" if variant == "base" else variant + "_") + stage for stage in sweep_json["stages"]], + output_group = "logs", + visibility = [":__subpackages__"], + ) + + # This can be built in parallel, but grt needs to be build in serial, or + # we will run out of memory + native.filegroup( + name = name + "_sweep_parallel", + srcs = [name + "_" + ("" if variant == "base" else variant + "_") + "cts" for variant in sweep], + visibility = ["//visibility:public"], + ) + + native.genrule( + name = name + "_wns_report", + srcs = [ + "wns_report.py", + name + "_sweep.json", + ] + [":" + name + "_" + variant + ".txt" for variant in sweep] + + [":" + name + "_" + variant + "_logs" for variant in sweep], + outs = [name + "_wns_report.md"], + cmd = ( + "$(location :wns_report.py) > $@" + + " $(location :" + name + "_sweep.json)" + ), + visibility = ["//visibility:public"], + ) + + native.filegroup( + name = name + "_repair_logs", + srcs = [ + ":" + name + "_" + ("" if variant == "base" else variant + "_") + stage + for stage in sweep_json["stages"] + for variant in sweep + ], + output_group = "logs", + ) + + native.genrule( + name = name + "_plot_repair", + srcs = [ + "plot-retiming.py", + name + "_repair_logs", + ], + outs = [name + "_retiming.pdf"], + cmd = "$(location plot-retiming.py) $(location " + name + "_retiming.pdf) $(locations " + name + "_repair_logs)", + visibility = ["//visibility:public"], + ) diff --git a/wns_report.py b/wns_report.py new file mode 100755 index 0000000..26f8d76 --- /dev/null +++ b/wns_report.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +import os +import json +import re +import pathlib +import sys + +try: + from tabulate import tabulate +except ImportError: + + def tabulate(table_data, headers, tablefmt): + # Simple mock implementation of tabulate, used in CI only + output = [] + output.append(" | ".join(headers)) + output.append("-" * len(output[0])) + for row in table_data: + output.append(" | ".join(map(str, row))) + return "\n".join(output) + + +def transpose_table(table_data): + return list(map(list, zip(*table_data))) + + +# slack: 0.039060 +# Clock core_clock +# 0.00 source latency ctrl.state.out[0]$_DFF_P_/CK ^ +# 0.00 target latency ctrl.state.out[0]$_DFF_P_/CK ^ +# 0.00 CRPR +# -------------- +# 0.00 setup skew + + +# tns 0.00 +# Cell type report: Count Area +# Tap cell 48 12.77 +# Buffer 14 19.95 +# Inverter 85 51.34 +# Sequential cell 35 158.27 +# Multi-Input combinational cell 369 420.55 +# Total 551 662.87 +def parse_stats(report): + """Create a dictionary with the values above""" + stats = {} + report_start = False + for line in report.split("\n"): + if "slack" in line: + stats["slack"] = float(line.split()[1]) + if "tns" in line: + stats["tns"] = float(line.split()[1]) + if "setup skew" in line: + stats["skew"] = line.split()[0] + # First line is "Cell type report", last line is "Total", + # fish out the values in between + if "Cell type report" in line: + report_start = True + continue + if report_start: + # fish out using regex first number column and use the label as key + # and the first number as the value. + # + # use regex, because split() would get confused by space + # in the label + # use regex, but don't get confused by one or more spaces in the label + # Sequentialcell 35 158.27 + # Tap cell 48 12.77 + # Multi-Input combinational cell 369 420.55 + # some labels have spaces, some don't + m = re.match(r"\s*(\D+)(\d+)\s+(\d+\.\d+)", line) + if m: + stats[m.group(1).strip()] = int(m.group(2)) + if "Total" in line: + report_start = False + continue + + return stats + + +# Extract Elapsed Time line from log file +# Elapsed time: 0:04.26[h:]min:sec. CPU time: user 4.08 sys 0.17 (99%). \ +# Peak memory: 671508KB. +def print_log_dir_times(f): + first = True + totalElapsed = 0 + total_max_memory = 0 + + if not os.path.exists(f): + return "N/A" + + with open(f) as logfile: + found = False + for line in logfile: + elapsedTime = None + peak_memory = None + + if "Elapsed time" in line: + found = True + # Extract the portion that has the time + timePor = line.strip().replace("Elapsed time: ", "") + # Remove the units from the time portion + timePor = timePor.split("[h:]", 1)[0] + # Remove any fraction of a second + timePor = timePor.split(".", 1)[0] + # Calculate elapsed time that has this format 'h:m:s' + timeList = timePor.split(":") + if len(timeList) == 2: + # Only minutes and seconds are present + elapsedTime = int(timeList[0]) * 60 + int(timeList[1]) + elif len(timeList) == 3: + # Hours, minutes, and seconds are present + elapsedTime = ( + int(timeList[0]) * 3600 + + int(timeList[1]) * 60 + + int(timeList[2]) + ) + else: + print("Elapsed time not understood in", str(line), file=sys.stderr) + # Find Peak Memory + peak_memory = int( + int(line.split("Peak memory: ")[1].split("KB")[0]) / 1024 + ) + + if not found: + print("No elapsed time found in", str(f), file=sys.stderr) + + # Print the name of the step and the corresponding elapsed time + if elapsedTime is not None and peak_memory is not None: + if first: + first = False + totalElapsed += elapsedTime + total_max_memory = max(total_max_memory, int(peak_memory)) + + return totalElapsed + + +def main(): + if len(sys.argv) != 2: + print("Usage: python script.py ") + sys.exit(1) + + sweep_file = sys.argv[1] + with open(sweep_file, "r") as file: + sweep_json = json.load(file) + sweep = sweep_json["sweep"] + + log_dir = os.path.dirname(sorted(pathlib.Path(".").glob("**/*.log"))[0]) + logs = sorted(map(os.path.basename, pathlib.Path(log_dir).glob("*.log"))) + logs_dir = os.path.join(log_dir, "..") + + variables = sorted( + set(k for v in sweep.values() for k in v.get("variables", {}).keys()) + ) + + def read_file(variant): + with open( + os.path.join( + os.path.dirname(sweep_file), sweep_json["name"] + "_" + variant + ".txt" + ), + "r", + ) as file: + return file.read() + + stats = {variant: parse_stats(read_file(variant)) for variant in sweep} + names = sorted({name for stat in stats.values() for name in stat.keys()}) + variable_names = sorted( + set(k for v in sweep.values() for k in v.get("variables", {}).keys()) + ) + + table_data = None + + def previous_stage(previous_stage): + if len(previous_stage) == 0: + return "" + stage, previous = list(previous_stage.items())[0] + return f"{stage}: {previous}" + + for variant in sweep: + if table_data is None: + table_data = [ + ["Variant", "Description"] + + names + + variable_names + + ["dissolve", "previous_stage"] + + logs + ] + variables = sweep[variant].get("variables", {}) + table_data.append( + ( + [variant, sweep[variant].get("description", "")] + + [stats[variant][name] for name in names] + + [ + ( + variables.get(variable, "") + if sweep_json["base"].get(variable, "") + != variables.get(variable, "") + else "" + ) + for variable in variable_names + ] + + [ + " ".join(sweep[variant].get("dissolve", [])), + previous_stage(sweep[variant].get("previous_stage", {})), + ] + + [ + print_log_dir_times(os.path.join(logs_dir, variant, log)) + for log in logs + ] + ) + ) + + print("Stage: " + sweep_json["stage"]) + table_data = transpose_table(table_data) + table = tabulate(table_data[1:], table_data[0], tablefmt="github") + print(table) + + print() + print("Base configuration variables") + base_keys = sorted(sweep_json["base"].keys()) + print( + tabulate( + [[key, sweep_json["base"][key]] for key in base_keys], + ["Variable", "Value"], + tablefmt="github", + ) + ) + + +if __name__ == "__main__": + main() diff --git a/wns_report_test.py b/wns_report_test.py new file mode 100755 index 0000000..b1762b2 --- /dev/null +++ b/wns_report_test.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import wns_report + + +def test_parse_stats(): + report = """ + slack: 0.039060 + Clock core_clock + 0.00 source latency ctrl.state.out[0]$_DFF_P_/CK ^ + 0.00 target latency ctrl.state.out[0]$_DFF_P_/CK ^ + 0.00 CRPR + -------------- + 0.00 setup skew + + tns 0.00 + Cell type report: Count Area + Tap cell 48 12.77 + Buffer 14 19.95 + Inverter 85 51.34 + Sequential cell 35 158.27 + Multi-Input combinational cell 369 420.55 + Total 551 662.87 + """ + expected_stats = { + "slack": 0.03906, + "skew": "0.00", + "Tap cell": 48, + "Inverter": 85, + "Buffer": 14, + "Sequential cell": 35, + "Multi-Input combinational cell": 369, + "Total": 551, + "tns": 0.00, + } + + assert wns_report.parse_stats(report) == expected_stats diff --git a/write_binary.bzl b/write_binary.bzl new file mode 100644 index 0000000..82024fd --- /dev/null +++ b/write_binary.bzl @@ -0,0 +1,18 @@ +""" +This module contains a rule for writing binary files. +""" + +def _write_binary_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write( + output = out, + content = ctx.attr.data, + ) + return [DefaultInfo(files = depset([out]))] + +write_binary = rule( + implementation = _write_binary_impl, + attrs = { + "data": attr.string(), + }, +)