From dfc2a68fc76a29b57adcebd693a3a85a1c25af40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Mon, 22 Sep 2025 14:50:51 -0400 Subject: [PATCH 1/8] WIP area profiler heuristic from Yosys --- graph.pdf | Bin 0 -> 17955 bytes tools/area-profiler/README.md | 39 ++++ tools/area-profiler/area_profiler/__init__.py | 3 + tools/area-profiler/area_profiler/parse.py | 204 ++++++++++++++++++ tools/area-profiler/area_profiler/plot.py | 90 ++++++++ tools/area-profiler/pyproject.toml | 15 ++ 6 files changed, 351 insertions(+) create mode 100644 graph.pdf create mode 100644 tools/area-profiler/README.md create mode 100644 tools/area-profiler/area_profiler/__init__.py create mode 100644 tools/area-profiler/area_profiler/parse.py create mode 100644 tools/area-profiler/area_profiler/plot.py create mode 100644 tools/area-profiler/pyproject.toml diff --git a/graph.pdf b/graph.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6918600a9d5baf11abd650c04e1f797a1d27b104 GIT binary patch literal 17955 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 literal 0 HcmV?d00001 diff --git a/tools/area-profiler/README.md b/tools/area-profiler/README.md new file mode 100644 index 0000000000..742ba194e1 --- /dev/null +++ b/tools/area-profiler/README.md @@ -0,0 +1,39 @@ +# Area estimation tool + +This tool estimates and visualizes hardware design areas from Yosys IL and stat files. Yosys IL and stat files can be obtained from a Verilog file via: + +```bash +yosys -p "read_verilog -sv inline.sv; hierarchy -top main; opt; write_rtlil inline.il; tee -o inline.json stat -json" +``` + +## Install + +```bash +uv tool install . +``` + +## Usage + +```bash +aprof-parse -h +aprof-plot -h +``` + +### Commands + +**`aprof-parse`** – convert IL + stat files into JSON summary + +```bash +aprof parse [-o OUTPUT] +``` + +- `-o` optional output JSON (default stdout) + +**`aprof-plot`** – visualize JSON summary + +```bash +aprof plot [-o OUTPUT] +``` + +- `MODE` one of `bar`, `treemap` +- `-o` optional output HTML (default depends on mode) diff --git a/tools/area-profiler/area_profiler/__init__.py b/tools/area-profiler/area_profiler/__init__.py new file mode 100644 index 0000000000..c18cf56661 --- /dev/null +++ b/tools/area-profiler/area_profiler/__init__.py @@ -0,0 +1,3 @@ +"""WIP.""" + +__version__ = "0.1.0" diff --git a/tools/area-profiler/area_profiler/parse.py b/tools/area-profiler/area_profiler/parse.py new file mode 100644 index 0000000000..1432d5d3b0 --- /dev/null +++ b/tools/area-profiler/area_profiler/parse.py @@ -0,0 +1,204 @@ +import pathlib +import sys +import re +import json +from dataclasses import dataclass, asdict, is_dataclass +import argparse + +toplevel: str = "main" + + +# Intermediate representation types +@dataclass +class CellWithParams: + """ + Class representing a cell and its parameters. + """ + + cell_name: str + cell_type: str + cell_params: dict[str, int] + + +""" +Map from modules to cell names to cells with parameters. +""" +type ModuleCellTypes = dict[str, dict[str, CellWithParams]] + +# Output representation types +""" +Map representing resources used by a cell. +""" +type Rsrc = dict[str, int] + + +@dataclass +class CellRsrc: + """ + Class representing a cell and its resources. + """ + + cell_name: str + cell_type: str + cell_width: int | None + generated: bool + rsrc: Rsrc + + +""" +Map between qualified cell names and cell resource values. +""" +type DesignRsrc = dict[str, CellRsrc] + + +def parse_il_file_old(path: str) -> ModuleCellTypes: + module_to_name_to_type: ModuleCellTypes = {} + current_module = None + with open(path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("module"): + current_module = line.split()[1] + module_to_name_to_type[current_module] = {} + elif line.startswith("cell"): + match = re.match(r"cell\s+(\S+)\s+(\S+)", line) + if match: + cell_type, cell_name = match.groups() + module_to_name_to_type[current_module][cell_name] = cell_type + return module_to_name_to_type + + +def parse_il_file(path: str) -> ModuleCellTypes: + module_to_name_to_type: ModuleCellTypes = {} + current_module = None + current_cell = None + with open(path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("module"): + current_module = line.split()[1] + module_to_name_to_type[current_module] = {} + elif line.startswith("cell") and current_module: + current_cell = line.split()[2] + cell_type = line.split()[1] + module_to_name_to_type[current_module][current_cell] = CellWithParams( + current_cell, cell_type, {} + ) + elif line.startswith("parameter") and current_cell: + param_name = line.split()[1] + param_val = line.split()[2] + module_to_name_to_type[current_module][current_cell].cell_params[ + param_name + ] = param_val + elif line.startswith("end") and current_cell: + current_cell = None + elif line.startswith("end") and current_module: + current_module = None + return module_to_name_to_type + + +def flatten_il_rec_helper( + module_to_name_to_type: ModuleCellTypes, module: str, pref: str +): + design_map: DesignRsrc = {} + for cell_name, cell_with_params in module_to_name_to_type[module].items(): + generated_type = cell_with_params.cell_type[0] == "$" + generated_name = cell_name[0] == "$" + if generated_type: + width = max( + { + int(v) + for k, v in cell_with_params.cell_params.items() + if k.endswith("WIDTH") + }, + default=None, + ) + if cell_with_params.cell_type.startswith("$paramod"): + new_width = cell_with_params.cell_type.split("\\")[2] + width = int(new_width.split("'")[1], 2) + design_map[f"{pref}.{cell_name[1:]}"] = CellRsrc( + cell_name[1:], + cell_with_params.cell_type[1:], + width, + generated_name, + {}, + ) + else: + design_map |= flatten_il_rec_helper( + module_to_name_to_type, + cell_with_params.cell_type, + f"{pref}.{cell_name[1:]}", + ) + return design_map + + +def flatten_il(module_to_name_to_type: ModuleCellTypes): + return flatten_il_rec_helper(module_to_name_to_type, "\\main", "main") + + +def parse_stat_file(path: str) -> dict: + with open(path, "r") as f: + return json.load(f) + + +def populate_stats(design_map: DesignRsrc, stat: dict): + for k, v in design_map.items(): + if v.cell_type.startswith("paramod"): + filtered_rsrc = { + k: v + for k, v in stat["modules"][f"${v.cell_type}"].items() + if isinstance(v, int) + } + design_map[k].rsrc.update(filtered_rsrc) + v.cell_type = v.cell_type.split("\\")[1] + + +def main(): + parser = argparse.ArgumentParser( + description="Utility to process Yosys IL and stat files and dump design map as JSON" + ) + parser.add_argument( + "il_file", + type=pathlib.Path, + help="path to the IL file" + ) + parser.add_argument( + "stat_file", + type=pathlib.Path, + help="path to the stat file" + ) + parser.add_argument( + "-o", + "--output", + type=pathlib.Path, + help="output JSON", + ) + args = parser.parse_args() + + name_to_type = parse_il_file(args.il_file) + design_map = flatten_il(name_to_type) + stat = parse_stat_file(args.stat_file) + populate_stats(design_map, stat) + + output_path = args.output + + if output_path: + with open(output_path, "w") as f: + json.dump( + design_map, + f, + indent=2, + default=lambda o: asdict(o) if is_dataclass(o) else str, + ) + else: + print( + json.dumps( + design_map, + indent=2, + default=lambda o: asdict(o) if is_dataclass(o) else str, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/tools/area-profiler/area_profiler/plot.py b/tools/area-profiler/area_profiler/plot.py new file mode 100644 index 0000000000..0a777f1fc0 --- /dev/null +++ b/tools/area-profiler/area_profiler/plot.py @@ -0,0 +1,90 @@ +import argparse +import json +import plotly.express as px +from collections import defaultdict +from pathlib import Path + +AREA_WEIGHTS = { + "and": 1.0, + "or": 1.0, + "not": 0.5, + "eq": 3.0, + "logic_not": 2.0, + "mux": 4.0, + "std_wire": 0.2, + "std_reg": 8.0, +} + +def load_data(path: Path): + with open(path) as f: + return json.load(f) + +def compute_areas(data): + areas = [] + for name, cell in data.items(): + t = cell["cell_type"] + w = cell["cell_width"] + weight = AREA_WEIGHTS.get(t, 1.0) + area = weight * w + areas.append({"cell_name": name, "cell_type": t, "width": w, "area": area}) + return areas + +def make_bar_chart(areas, output): + type_area = defaultdict(float) + for a in areas: + type_area[a["cell_type"]] += a["area"] + summary = [{"cell_type": t, "total_area": area} for t, area in type_area.items()] + + fig = px.bar( + summary, + x="cell_type", + y="total_area", + title="estimated area", + labels={"total_area": "Estimated area"}, + ) + fig.write_html(output) + +def make_treemap(areas, output): + fig = px.treemap( + areas, + path=["cell_type", "cell_name"], + values="area", + title="estimated area treemap", + ) + fig.write_html(output) + +def main(): + parser = argparse.ArgumentParser( + description="Estimate and plot cell areas based on a heuristic" + ) + parser.add_argument( + "input", + type=Path, + help="path to input JSON file", + ) + parser.add_argument( + "mode", + choices=["bar", "treemap"], + help="visualization type", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + help="output HTML file (default: area_by_type.html for bar, area_treemap.html for treemap)", + ) + + args = parser.parse_args() + + data = load_data(args.input) + areas = compute_areas(data) + + if args.mode == "bar": + output = args.output or Path("area_by_type.html") + make_bar_chart(areas, output) + elif args.mode == "treemap": + output = args.output or Path("area_treemap.html") + make_treemap(areas, output) + +if __name__ == "__main__": + main() diff --git a/tools/area-profiler/pyproject.toml b/tools/area-profiler/pyproject.toml new file mode 100644 index 0000000000..eda1951215 --- /dev/null +++ b/tools/area-profiler/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "area-profiler" +authors = [{ name = "The Calyx Authors" }] +classifiers = ["License :: OSI Approved :: MIT License"] +dynamic = ["version", "description"] +dependencies = ["pandas", "plotly"] +readme = "README.md" + +[project.scripts] +aprof-parse = "area_profiler.parse:main" +aprof-plot = "area_profiler.plot:main" From e9c7687f9cfe6fd85f42b16a1e2f744787dad47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Mon, 22 Sep 2025 14:55:26 -0400 Subject: [PATCH 2/8] Better readme with Yosys info --- tools/area-profiler/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/area-profiler/README.md b/tools/area-profiler/README.md index 742ba194e1..fe687981ca 100644 --- a/tools/area-profiler/README.md +++ b/tools/area-profiler/README.md @@ -8,10 +8,14 @@ yosys -p "read_verilog -sv inline.sv; hierarchy -top main; opt; write_rtlil inli ## Install +The tool can be installed with: + ```bash uv tool install . ``` +Additionally, on `havarti`, feel free to use Pedro's installation of the Yosys environment, located in `/scratch/pedro`. The environment can be loaded using the `environment` or `environment.fish` scripts. + ## Usage ```bash From 014eefe292321eba020112ea099667556b6e9d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Sun, 26 Oct 2025 17:44:40 -0400 Subject: [PATCH 3/8] created AreaExtract --- tools/AreaExtract/AreaExtract/__init__.py | 4 + tools/AreaExtract/AreaExtract/bin/__init__.py | 0 tools/AreaExtract/AreaExtract/bin/extract.py | 65 ++++ .../AreaExtract/lib/cdf/__init__.py | 0 tools/AreaExtract/AreaExtract/lib/cdf/cdf.py | 71 +++++ .../AreaExtract/lib/parse/__init__.py | 0 .../AreaExtract/lib/parse/vivado.py | 281 ++++++++++++++++++ .../AreaExtract/lib/parse/yosys.py | 131 ++++++++ tools/AreaExtract/README.md | 0 tools/AreaExtract/pyproject.toml | 14 + 10 files changed, 566 insertions(+) create mode 100644 tools/AreaExtract/AreaExtract/__init__.py create mode 100644 tools/AreaExtract/AreaExtract/bin/__init__.py create mode 100644 tools/AreaExtract/AreaExtract/bin/extract.py create mode 100644 tools/AreaExtract/AreaExtract/lib/cdf/__init__.py create mode 100644 tools/AreaExtract/AreaExtract/lib/cdf/cdf.py create mode 100644 tools/AreaExtract/AreaExtract/lib/parse/__init__.py create mode 100644 tools/AreaExtract/AreaExtract/lib/parse/vivado.py create mode 100644 tools/AreaExtract/AreaExtract/lib/parse/yosys.py create mode 100644 tools/AreaExtract/README.md create mode 100644 tools/AreaExtract/pyproject.toml diff --git a/tools/AreaExtract/AreaExtract/__init__.py b/tools/AreaExtract/AreaExtract/__init__.py new file mode 100644 index 0000000000..2d30abe585 --- /dev/null +++ b/tools/AreaExtract/AreaExtract/__init__.py @@ -0,0 +1,4 @@ +"""AreaExtract is a tool to extract area information from synthesis data +and compile it in a Common Data Format.""" + +__version__ = "0.1.0" diff --git a/tools/AreaExtract/AreaExtract/bin/__init__.py b/tools/AreaExtract/AreaExtract/bin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/AreaExtract/AreaExtract/bin/extract.py b/tools/AreaExtract/AreaExtract/bin/extract.py new file mode 100644 index 0000000000..9daafc982e --- /dev/null +++ b/tools/AreaExtract/AreaExtract/bin/extract.py @@ -0,0 +1,65 @@ +import argparse +import json +from pathlib import Path + +from AreaExtract.lib.parse.vivado import rpt_to_design_with_metadata +from AreaExtract.lib.parse.yosys import il_to_design_with_metadata + + +def main(): + parser = argparse.ArgumentParser( + description=( + "Parse FPGA synthesis reports into a Common Data Format.\n\n" + "Supported origins:\n" + " - Vivado: single hierarchical .rpt file\n" + " - Yosys: .il (intermediate language) and .json (stat) file\n\n" + "Output is a JSON serialization of the Common Data Format." + ), + formatter_class=argparse.RawTextHelpFormatter, + ) + subparsers = parser.add_subparsers(dest="origin", required=True) + vivado = subparsers.add_parser( + "vivado", + help="parse a Vivado utilization .rpt file", + ) + vivado.add_argument( + "rpt", + type=Path, + help="path to Vivado utilization report (.rpt)", + ) + yosys = subparsers.add_parser( + "yosys", + help="parse Yosys IL and stat JSON files", + ) + yosys.add_argument( + "il", + type=Path, + help="path to Yosys IL file (.il)", + ) + yosys.add_argument( + "json", + type=Path, + help="path to Yosys stat file (.json)", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + help="optional output file for JSON (defaults to stdout)", + ) + args = parser.parse_args() + if args.origin == "vivado": + design = rpt_to_design_with_metadata(args.rpt) + elif args.origin == "yosys": + design = il_to_design_with_metadata(args.il, args.json) + else: + parser.error("unknown origin") + json_str = json.dumps(design, default=lambda o: o.__dict__, indent=2) + if args.output: + args.output.write_text(json_str) + else: + print(json_str) + + +if __name__ == "__main__": + main() diff --git a/tools/AreaExtract/AreaExtract/lib/cdf/__init__.py b/tools/AreaExtract/AreaExtract/lib/cdf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/AreaExtract/AreaExtract/lib/cdf/cdf.py b/tools/AreaExtract/AreaExtract/lib/cdf/cdf.py new file mode 100644 index 0000000000..a758570f41 --- /dev/null +++ b/tools/AreaExtract/AreaExtract/lib/cdf/cdf.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass + + +@dataclass +class VivadoRsrc: + """ + Vivado resources for a cell. + """ + + lut: int + llut: int + lutram: int + srl: int + ff: int + ramb36: int + ramb18: int + uram: int + dsp: int + + +type YosysRsrc = dict[str, int] +""" +Yosys resources for a cell. +""" + + +type Rsrc = VivadoRsrc | YosysRsrc +""" +Map representing resources used by a cell. +""" + + +@dataclass +class Cell: + """ + Cell with resources. + """ + + # Unqualified cell name. + name: str + # Cell type. + type: str + # Whether the cell was generated in synthesis. + generated: bool + # Cell resources. + rsrc: Rsrc + +@dataclass +class Metadata: + """ + Design metadata. + """ + + # Origin of the design (Vivado, Yosys). + origin: str + + +type Design = dict[str, Cell] +""" +Design with qualified cell names and associated cells. +""" + + +@dataclass +class DesignWithMetadata: + """ + Design with metadata. + """ + + design: Design + metadata: Metadata diff --git a/tools/AreaExtract/AreaExtract/lib/parse/__init__.py b/tools/AreaExtract/AreaExtract/lib/parse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/AreaExtract/AreaExtract/lib/parse/vivado.py b/tools/AreaExtract/AreaExtract/lib/parse/vivado.py new file mode 100644 index 0000000000..8263fe7ec0 --- /dev/null +++ b/tools/AreaExtract/AreaExtract/lib/parse/vivado.py @@ -0,0 +1,281 @@ +from pathlib import Path +from AreaExtract.lib.cdf.cdf import VivadoRsrc, Cell, Design, Metadata, DesignWithMetadata +import re + + +class RPTParser: + """ + Class implementing parsing functionality of RPT files generated by Xilinx + tools. The core functionality is extracting tables out of these files. + """ + + SKIP_LINE = re.compile(r"^.*(\+-*)*\+$") + + def __init__(self, filepath): + with open(filepath, "r") as data: + self.lines = data.read().split("\n") + + @staticmethod + def _clean_and_strip(elems, preserve_index=None): + """ + Remove all empty elements from the list and strips each string element + while preserving the left indentation of the element at index `preserve_index`. + """ + indexed = filter(lambda ie: ie[1] != "\n" and ie[1] != "", enumerate(elems)) + cleaned = map( + lambda ie: ie[1].rstrip("\n ") + if ie[0] == preserve_index + else ie[1].strip(), + indexed, + ) + return list(cleaned) + + @staticmethod + def _parse_simple_header(line): + assert re.search(r"\s*\|", line), ( + "Simple header line should have | as first non-whitespace character" + ) + return RPTParser._clean_and_strip(line.split("|")) + + @staticmethod + def _parse_multi_header(lines): + """ + Extract header from the form: + +------+--------+--------+----------+-----------+-----------+ + | | Latency | Iteration| Initiation Interval | + | Name | min | max | Latency | achieved | target | + +------+--------+--------+----------+-----------+-----------+ + + into: ["Name", "Latency_min", "Latency_max", "Iteration Latency", ...] + + This will fail to correctly parse this header. See the comment below + for an explanation: + +------+--------+--------+--------+--------+ + | | Latency | Foo | + | Name | min | max | bar | baz | + +------+--------+--------+--------+--------+ + turns into: ["Name", "Latency_min", "Latency_max", + "Latecy_bar", "Latency_baz", "Foo"] + """ + + multi_headers = [] + secondary_hdrs = lines[1].split("|") + + # Use the following heuristic to generate header names: + # - If header starts with a small letter, it is a secondary header. + # - If the last save sequence of headers doesn't already contain this + # header name, add it to the last one. + # - Otherwise add a new sub header class. + for idx, line in enumerate(secondary_hdrs, 1): + clean_line = line.strip() + if len(clean_line) == 0: + continue + elif ( + clean_line[0].islower() + and len(multi_headers) > 0 + and multi_headers[-1][0].islower() + and clean_line not in multi_headers[-1] + ): + multi_headers[-1].append(clean_line) + else: + multi_headers.append([clean_line]) + + # Extract base headers and drop the starting empty lines and ending '\n'. + base_hdrs = lines[0].split("|")[1:-1] + + if len(base_hdrs) != len(multi_headers): + raise Exception( + "Something went wrong while parsing multi header " + + "base len: {}, mult len: {}".format( + len(base_hdrs), len(multi_headers) + ) + ) + + hdrs = [] + for idx in range(0, len(base_hdrs)): + for mult in multi_headers[idx]: + hdrs.append((base_hdrs[idx].strip() + " " + mult).strip()) + + return hdrs + + @staticmethod + def _parse_table(table_lines, multi_header, preserve_indent): + """ + Parses a simple table of the form: + +--------+-------+----------+------------+ + | Clock | Target| Estimated| Uncertainty| + +--------+-------+----------+------------+ + |ap_clk | 7.00| 4.39| 1.89| + |ap_clk | 7.00| 4.39| 1.89| + +--------+-------+----------+------------+ + |ap_clk | 7.00| 4.39| 1.89| + +--------+-------+----------+------------+ + + The might be any number of rows after the headers. The input parameter + is a list of lines of the table starting with the top most header line. + Return a list of dicts, one per row, whose keys come from the header + row. + + """ + + # Extract the headers and set table start + table_start = 0 + if multi_header: + header = RPTParser._parse_multi_header(table_lines[1:3]) + table_start = 3 + else: + header = RPTParser._parse_simple_header(table_lines[1]) + table_start = 2 + + assert len(header) > 0, "No header found" + + rows = [] + for line in table_lines[table_start:]: + if not RPTParser.SKIP_LINE.match(line): + rows.append( + RPTParser._clean_and_strip( + line.split("|"), 1 if preserve_indent else None + ) + ) + + ret = [ + {header[i]: row[i] for i in range(len(header))} + for row in rows + if len(row) == len(header) + ] + return ret + + @staticmethod + def _get_indent_level(instance): + """ + Compute the hierarchy depth of an instance based on its leading spaces. + Assumes 2 spaces per indentation level. + """ + return (len(instance) - len(instance.lstrip(" "))) // 2 + + def get_table(self, reg, off, multi_header=False, preserve_indent=False): + """ + Parse table `off` lines after `reg` matches the files in the current + file. + + The table format is: + +--------+-------+----------+------------+ + | Clock | Target| Estimated| Uncertainty| + +--------+-------+----------+------------+ + |ap_clk | 7.00| 4.39| 1.89| + |ap_clk | 7.00| 4.39| 1.89| + +--------+-------+----------+------------+ + |ap_clk | 7.00| 4.39| 1.89| + +--------+-------+----------+------------+ + """ + start = 0 + end = 0 + for idx, line in enumerate(self.lines, 1): + if reg.search(line): + start = idx + off + + # If start doesn't point to valid header, continue searching + if ( + self.lines[start].strip() == "" + or self.lines[start].strip()[0] != "+" + ): + continue + + end = start + while self.lines[end].strip() != "": + end += 1 + + if end <= start: + return None + + return self._parse_table(self.lines[start:end], multi_header, preserve_indent) + + @classmethod + def build_hierarchy_tree(self, table): + """ + Construct a hierarchical tree from a list of dictionary rows representing + indented instances in a flat table. Each row must contain an 'Instance' key, + where indentation indicates the depth in the hierarchy. + + Returns a nested dictionary tree with 'children' fields populated accordingly. + """ + stack = [] + root = {} + for row in table: + raw_instance = row["Instance"] + name = raw_instance.strip() + level = self._get_indent_level(raw_instance) + row["Instance"] = row["Instance"].strip() + row["children"] = {} + while len(stack) > level: + stack.pop() + if not stack: + root[name] = row + stack.append(row) + else: + parent = stack[-1] + parent["children"][name] = row + stack.append(row) + return root + + +def create_tree(filename): + rpt_file = Path(filename) + parser = RPTParser(rpt_file) + table = parser.get_table( + re.compile(r"^\d+\. Utilization by Hierarchy$"), 2, preserve_indent=True + ) + tree = parser.build_hierarchy_tree(table) + return tree + + +def flatten_tree(tree, prefix=""): + flat = {} + for name, node in tree.items(): + fq_name = f"{prefix}.{name}" if prefix else name + flat[fq_name] = {k: v for k, v in node.items() if k != "children"} + if node.get("children"): + flat.update(flatten_tree(node["children"], fq_name)) + return flat + + +def to_vivado_rsrc(d: dict) -> VivadoRsrc: + mapping = { + "Total LUTs": "lut", + "Logic LUTs": "llut", + "LUTRAMs": "lutram", + "SRLs": "srl", + "FFs": "ff", + "RAMB36": "ramb36", + "RAMB18": "ramb18", + "URAM": "uram", + "DSP Blocks": "dsp", + } + + kwargs = {} + for rpt_key, field_name in mapping.items(): + raw_value = d.get(rpt_key, "0").strip() + kwargs[field_name] = int(raw_value) if raw_value.isdigit() else 0 + + return VivadoRsrc(**kwargs) + + +def to_cell(d: dict) -> Cell: + return Cell( + name=d["Instance"], + type=d["Module"], + generated=False, + rsrc=to_vivado_rsrc(d), + ) + + +def to_design(flat_tree: dict) -> Design: + return {fq_name: to_cell(node) for fq_name, node in flat_tree.items()} + + +def rpt_to_design_with_metadata(filename: str) -> DesignWithMetadata: + tree = create_tree(filename) + flat_tree = flatten_tree(tree) + design = to_design(flat_tree) + metadata = Metadata(origin="Vivado") + return DesignWithMetadata(design=design, metadata=metadata) diff --git a/tools/AreaExtract/AreaExtract/lib/parse/yosys.py b/tools/AreaExtract/AreaExtract/lib/parse/yosys.py new file mode 100644 index 0000000000..3e5f006f34 --- /dev/null +++ b/tools/AreaExtract/AreaExtract/lib/parse/yosys.py @@ -0,0 +1,131 @@ +import re +import json +from dataclasses import dataclass +from AreaExtract.lib.cdf.cdf import YosysRsrc, Cell, Design, Metadata, DesignWithMetadata + +toplevel: str = "main" + +@dataclass +class CellWithParams: + """ + Class representing a cell and its parameters. + """ + + cell_name: str + cell_type: str + cell_params: dict[str, int] + + +""" +Map from modules to cell names to cells with parameters. +""" +type ModuleCellTypes = dict[str, dict[str, CellWithParams]] + + +def parse_il_file_old(path: str) -> ModuleCellTypes: + module_to_name_to_type: ModuleCellTypes = {} + current_module = None + with open(path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("module"): + current_module = line.split()[1] + module_to_name_to_type[current_module] = {} + elif line.startswith("cell"): + match = re.match(r"cell\s+(\S+)\s+(\S+)", line) + if match: + cell_type, cell_name = match.groups() + module_to_name_to_type[current_module][cell_name] = cell_type + return module_to_name_to_type + + +def parse_il_file(path: str) -> ModuleCellTypes: + module_to_name_to_type: ModuleCellTypes = {} + current_module = None + current_cell = None + with open(path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("module"): + current_module = line.split()[1] + module_to_name_to_type[current_module] = {} + elif line.startswith("cell") and current_module: + current_cell = line.split()[2] + cell_type = line.split()[1] + module_to_name_to_type[current_module][current_cell] = CellWithParams( + current_cell, cell_type, {} + ) + elif line.startswith("parameter") and current_cell: + param_name = line.split()[1] + param_val = line.split()[2] + module_to_name_to_type[current_module][current_cell].cell_params[ + param_name + ] = param_val + elif line.startswith("end") and current_cell: + current_cell = None + elif line.startswith("end") and current_module: + current_module = None + return module_to_name_to_type + + +def flatten_il_rec_helper( + module_to_name_to_type: ModuleCellTypes, module: str, pref: str +): + design_map: Design = {} + for cell_name, cell_with_params in module_to_name_to_type[module].items(): + generated_type = cell_with_params.cell_type[0] == "$" + generated_name = cell_name[0] == "$" + if generated_type: + width = max( + { + int(v) + for k, v in cell_with_params.cell_params.items() + if k.endswith("WIDTH") + }, + default=None, + ) + if cell_with_params.cell_type.startswith("$paramod"): + new_width = cell_with_params.cell_type.split("\\")[2] + width = int(new_width.split("'")[1], 2) + design_map[f"{pref}.{cell_name[1:]}"] = Cell( + cell_name[1:], + cell_with_params.cell_type[1:], + generated_name, + {"width": width}, + ) + else: + design_map |= flatten_il_rec_helper( + module_to_name_to_type, + cell_with_params.cell_type, + f"{pref}.{cell_name[1:]}", + ) + return design_map + + +def flatten_il(module_to_name_to_type: ModuleCellTypes): + return flatten_il_rec_helper(module_to_name_to_type, "\\main", "main") + + +def parse_stat_file(path: str) -> dict: + with open(path, "r") as f: + return json.load(f) + + +def populate_stats(design_map: Design, stat: dict): + for k, v in design_map.items(): + if v.type.startswith("paramod"): + filtered_rsrc: YosysRsrc = { + k: v + for k, v in stat["modules"][f"${v.type}"].items() + if isinstance(v, int) + } + design_map[k].rsrc.update(filtered_rsrc) + v.type = v.type.split("\\")[1] + + +def il_to_design_with_metadata(il_path: str, stat_path: str) -> DesignWithMetadata: + modules = parse_il_file(il_path) + design = flatten_il(modules) + stat = parse_stat_file(stat_path) + populate_stats(design, stat) + return DesignWithMetadata(design=design, metadata=Metadata(origin="Yosys")) diff --git a/tools/AreaExtract/README.md b/tools/AreaExtract/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/AreaExtract/pyproject.toml b/tools/AreaExtract/pyproject.toml new file mode 100644 index 0000000000..40391457fc --- /dev/null +++ b/tools/AreaExtract/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "AreaExtract" +authors = [{ name = "The Calyx Authors" }] +classifiers = ["License :: OSI Approved :: MIT License"] +dynamic = ["version", "description"] +dependencies = [] +readme = "README.md" + +[project.scripts] +aext = "AreaExtract.bin.extract:main" From bba6794af3a5f84fe352de8ae80089f9415a5d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Sun, 26 Oct 2025 18:53:03 -0400 Subject: [PATCH 4/8] README and rhai script --- fud2/scripts/synth-verilog-to-report.rhai | 2 +- tools/AreaExtract/README.md | 79 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/fud2/scripts/synth-verilog-to-report.rhai b/fud2/scripts/synth-verilog-to-report.rhai index 38415f9878..e9565611bc 100644 --- a/fud2/scripts/synth-verilog-to-report.rhai +++ b/fud2/scripts/synth-verilog-to-report.rhai @@ -36,7 +36,7 @@ fn synth_setup(e) { // Python scripts for parsing reports for visualization and extracting JSON summary e.rule("parse-rpt", "synthrep viz -t flamegraph -f $in > $out"); e.rule("extract-util-json", "synthrep summary -m utilization > $out"); - e.rule("extract-hierarchy-json", "synthrep summary -m hierarchy > $out"); + e.rule("extract-hierarchy-json", "aext vivado out/hierarchical_utilization_placed.rpt > $out"); } fn flamegraph_setup(e) { diff --git a/tools/AreaExtract/README.md b/tools/AreaExtract/README.md index e69de29bb2..f5906f7689 100644 --- a/tools/AreaExtract/README.md +++ b/tools/AreaExtract/README.md @@ -0,0 +1,79 @@ +# AreaExtract + +AreaExtract is a tool that replaces previous technology-specific frontends for +the Calyx profiler, Petal. It offers a combined frontend for several sources of +area data for accelerator designs, and outputs processed data in a common data +format that is parseable by Petal. Currently, the following technologies are +supported: + +- Vivado, as hierarchical area synthesis reports +- Yosys, as both IL and statistics files + +## Usage + +``` +$ aext -h +usage: aext [-h] [-o OUTPUT] {vivado,yosys} ... + +Parse FPGA synthesis reports into a Common Data Format. + +Supported origins: + - Vivado: single hierarchical .rpt file + - Yosys: .il (intermediate language) and .json (stat) file + +Output is a JSON serialization of the Common Data Format. + +positional arguments: + {vivado,yosys} + vivado parse a Vivado utilization .rpt file + yosys parse Yosys IL and stat JSON files + +options: + -h, --help show this help message and exit + -o OUTPUT, --output OUTPUT + optional output file for JSON (defaults to stdout) +``` + +## Obtaining area data + +This section provides instructions to obtain area data for designs from +supported technologies, to use as input for AreaExtract. + +### Vivado + +The simplest way to obtain a hierarchical area RPT file is to use Fud2 to run +synthesis on a Calyx design: + +``` +fud2 .futil --to area-report > .rpt +``` + +Alternatively, it is possible to use Fud2 to obtain a synthesis-ready Verilog +file, and then use Vivado directly to conduct synthesis. The relevant TCL +command for Vivado is: + +``` +report_utilization -hierarchical -file .rpt +``` + +### Yosys + +Using the OSS-CAD suite, IL and statistics files can be obtained as follows: + +``` +yosys -p "read_verilog -sv .sv; hierarchy -top main; opt; write_rtlil .il; tee -o .json stat -json" +``` + +It is also possible to pass Liberty files to [the `stat` command](https://yosyshq.readthedocs.io/projects/yosys/en/0.47/cmd/stat.html) +through the flag `-liberty `. + +## Future work + +This tool is not yet a full replacement of its technology-specific predecessors, +`synthrep` for Vivado and `aprof` for Yosys, as it is not able to produce area-only +visualizations, which is a desirable feature. In addition, some of `synthrep`'s +functionality is unrelated to area, and is not in scope for AreaExtract. Another +area that is being explored is the addition of other technologies, especially +OpenROAD as it targets ASICs instead of FPGAs. While Yosys also offers ASIC +capabilities, it is primarily oriented towards FPGAs; Vivado exclusively targets +AMD FPGAs. From 7bd52bb9b1701525646757272a9fc0e3394098c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Sun, 26 Oct 2025 18:56:07 -0400 Subject: [PATCH 5/8] Updated profiler to parse CDF --- tools/profiler/profiler/classes/tracedata.py | 24 ++++++++++--------- tools/profiler/profiler/construct_trace.py | 10 ++++---- tools/profiler/profiler/main.py | 16 ++++--------- .../profiler/visuals/utilization_plots.py | 15 ++++++------ 4 files changed, 31 insertions(+), 34 deletions(-) diff --git a/tools/profiler/profiler/classes/tracedata.py b/tools/profiler/profiler/classes/tracedata.py index 708c91d7b8..2d0fb9acd9 100644 --- a/tools/profiler/profiler/classes/tracedata.py +++ b/tools/profiler/profiler/classes/tracedata.py @@ -145,26 +145,28 @@ def add_sourceloc_info(self, adl_map: AdlMap): @dataclass -class Utilization: +class Area: """ - Hierarchical utilization wrapper. + Hierarchical area wrapper. """ map: dict[str, dict[str, str]] accessed: set[str] + origin: str def __init__(self, json_dict): - self.map = json_dict + self.map = json_dict.get("design", {}) self.accessed = set() + self.origin = json_dict.get("metadata", {}).get("origin", "") def get_module(self, name: str) -> dict[str, str]: """ - Get the utilization map for a module. `name` is a fully qualified name + Get the area map for a module. `name` is a fully qualified name of a module on a stack. """ if name in self.map: self.accessed.add(name) - return self.map.get(name, {}) + return self.map.get(name, {}).get("rsrc", {}) def get_unaccessed(self): """ @@ -174,14 +176,14 @@ def get_unaccessed(self): return module_set.difference(self.accessed) -class UtilizationCycleTrace(CycleTrace): +class AreaCycleTrace(CycleTrace): """ List of stacks that are active in a single cycle, containing utilization information (both aggregated and per primitive). """ # Reference to the global utilization map from all primitives to their utilization - global_utilization: Utilization + global_utilization: Area # Aggregated utilization of all the primitives in this cycle # Ex. {'Total LUTs': 21, 'Logic LUTs': 5, 'LUTRAMs': 16, 'SRLs': 0, 'FFs': 38, 'RAMB36': 0, 'RAMB18': 0, 'URAM': 0, 'DSP Blocks': 0} utilization: dict @@ -194,7 +196,7 @@ class UtilizationCycleTrace(CycleTrace): def __init__( self, - utilization: Utilization, + utilization: Area, control_metadata: ControlMetadata, stacks_this_cycle: list[list[StackElement]] | None = None, ): @@ -230,7 +232,7 @@ def add_stack(self, stack, main_shortname="main"): # NOT include control primitives! for p in self.primitives_active: util = { - k: int(v) if v.isdigit() else v + k: int(v) if type(v) is str and v.isdigit() else v for k, v in self.global_utilization.get_module(p).items() } self.utilization_per_primitive[p] = util @@ -412,7 +414,7 @@ def create_trace_with_control_groups( control_groups_trace: dict[int, set[str]], cell_metadata: CellMetadata, control_metadata: ControlMetadata, - utilization: Utilization | None = None, + utilization: Area | None = None, ): """ Populates the field trace_with_control_groups by combining control group information (from control_groups_trace) with self.trace. @@ -424,7 +426,7 @@ def create_trace_with_control_groups( new_cycletrace = ( CycleTrace() if utilization is None - else UtilizationCycleTrace(utilization, control_metadata) + else AreaCycleTrace(utilization, control_metadata) ) # fully qualified control group --> path descriptor active_control_group_to_desc: dict[str, str] = ( diff --git a/tools/profiler/profiler/construct_trace.py b/tools/profiler/profiler/construct_trace.py index 5388861259..6f8bb63e78 100644 --- a/tools/profiler/profiler/construct_trace.py +++ b/tools/profiler/profiler/construct_trace.py @@ -4,8 +4,8 @@ from profiler.classes.control_metadata import ControlMetadata from profiler.classes.tracedata import ( CycleTrace, - Utilization, - UtilizationCycleTrace, + Area, + AreaCycleTrace, TraceData, StackElement, StackElementType, @@ -188,7 +188,7 @@ def postprocess( self, shared_cells_map: dict[str, dict[str, str]], control_metadata: ControlMetadata | None = None, - utilization: Utilization | None = None, + utilization: Area | None = None, ): """ Postprocess data mapping timestamps to events (signal changes) @@ -548,7 +548,7 @@ def create_utilization_cycle_trace( info_this_cycle: dict[str, str | dict[str, str]], shared_cell_map: dict[str, dict[str, str]], include_primitives: bool, - utilization: Utilization, + utilization: Area, ): """ Creates a UtilizationCycleTrace object for stack elements in this cycle, computing the dependencies between them. @@ -556,7 +556,7 @@ def create_utilization_cycle_trace( cycle_trace = create_cycle_trace( cell_info, info_this_cycle, shared_cell_map, include_primitives ) - return UtilizationCycleTrace(utilization, control_metadata, cycle_trace.stacks) + return AreaCycleTrace(utilization, control_metadata, cycle_trace.stacks) def add_control_enables( diff --git a/tools/profiler/profiler/main.py b/tools/profiler/profiler/main.py index 97092a0623..6691eee826 100644 --- a/tools/profiler/profiler/main.py +++ b/tools/profiler/profiler/main.py @@ -12,7 +12,7 @@ from profiler.classes.cell_metadata import CellMetadata from profiler.classes.control_metadata import ControlMetadata -from profiler.classes.tracedata import TraceData, ControlRegUpdateType, Utilization +from profiler.classes.tracedata import TraceData, ControlRegUpdateType, Area def setup_metadata(args): @@ -56,7 +56,7 @@ def process_vcd( control_metadata: ControlMetadata, tracedata: TraceData, vcd_filename: str, - utilization: Utilization | None = None, + utilization: Area | None = None, ): """ Wrapper function to process the VCD file to produce a trace. @@ -208,20 +208,14 @@ def main(): enable_thread_metadata, ) = setup_metadata(args) - utilization: Utilization | None = None + utilization: Area | None = None utilization_variable: str | None = None if args.utilization_report_json is not None: print("Utilization report mode enabled.") with open(args.utilization_report_json) as f: - utilization = Utilization(json.load(f)) - varmap = { - "ff": "FFs", - "lut": "Total LUTs", - "llut": "Logic LUTs", - "lutram": "LUTRAMs", - } - utilization_variable = varmap[args.utilization_variable] + utilization = Area(json.load(f)) + utilization_variable = args.utilization_variable control_reg_updates_per_cycle: dict[int, ControlRegUpdateType] = process_vcd( cell_metadata, diff --git a/tools/profiler/profiler/visuals/utilization_plots.py b/tools/profiler/profiler/visuals/utilization_plots.py index b90b282696..af20cd380c 100644 --- a/tools/profiler/profiler/visuals/utilization_plots.py +++ b/tools/profiler/profiler/visuals/utilization_plots.py @@ -1,7 +1,7 @@ from collections import Counter import pandas as pd import plotly.express as px -from profiler.classes.tracedata import UtilizationCycleTrace +from profiler.classes.tracedata import PTrace class Plotter: @@ -9,14 +9,14 @@ class Plotter: Wrapper around related utilization plotting functions. """ - def __init__(self, data: dict[int, UtilizationCycleTrace]): - self.data = data + def __init__(self, data: PTrace): + self.data = data.trace def plot_utilization_per_cycle(self, var: str, out_dir: str): """Plot the value of `var` per cycle.""" records = [ {"cycle": cycle_id, "value": obj.utilization.get(var, 0)} - for cycle_id, obj in self.data.items() + for cycle_id, obj in enumerate(self.data) ] df = pd.DataFrame(records) fig = px.bar( @@ -27,7 +27,7 @@ def plot_utilization_per_cycle(self, var: str, out_dir: str): def plot_cycles_per_primitive(self, var: str, out_dir: str): """Plot the number of cycles each primitive has a nonzero value for `var`.""" counter = Counter() - for obj in self.data.values(): + for obj in self.data: for prim, varmap in obj.utilization_per_primitive.items(): if var in varmap and varmap[var] != 0: counter[prim] += 1 @@ -48,7 +48,7 @@ def plot_heatmap(self, var: str, out_dir: str): usage_sum = Counter() active_cycles = Counter() - for cycle, trace in self.data.items(): + for cycle, trace in enumerate(self.data): for prim, vars_dict in trace.utilization_per_primitive.items(): if var in vars_dict: value = int(vars_dict[var]) @@ -67,6 +67,7 @@ def plot_heatmap(self, var: str, out_dir: str): sorted_primitives = sorted(ratios, key=ratios.get, reverse=True) df = pd.DataFrame(aggregated) + print(df) heatmap_data = df.pivot(index="primitive", columns="cycle", values="value") all_cycles = range(df["cycle"].min(), df["cycle"].max() + 1) heatmap_data = heatmap_data.reindex(columns=all_cycles) @@ -86,7 +87,7 @@ def plot_ratio(self, var: str, out_dir: str): usage_sum = Counter() active_cycles = Counter() - for obj in self.data.values(): + for obj in self.data: for prim, varmap in obj.utilization_per_primitive.items(): if var in varmap: usage_sum[prim] = int(varmap[var]) From a9274a6eaac09ec8e6a5d1384b4c912f2ec89eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Sun, 26 Oct 2025 19:05:44 -0400 Subject: [PATCH 6/8] Insta tests for fud2 --- .../snapshots/tests__test@plan_area-report-to-flamegraph.snap | 2 +- .../tests__test@plan_synth-verilog-to-area-report.snap | 2 +- .../tests__test@plan_synth-verilog-to-hierarchy-json.snap | 2 +- .../tests__test@plan_synth-verilog-to-timing-report.snap | 2 +- .../snapshots/tests__test@plan_synth-verilog-to-util-json.snap | 2 +- .../tests__test@plan_synth-verilog-to-util-report.snap | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fud2/tests/snapshots/tests__test@plan_area-report-to-flamegraph.snap b/fud2/tests/snapshots/tests__test@plan_area-report-to-flamegraph.snap index 72627fdbf8..ae7de95e6a 100644 --- a/fud2/tests/snapshots/tests__test@plan_area-report-to-flamegraph.snap +++ b/fud2/tests/snapshots/tests__test@plan_area-report-to-flamegraph.snap @@ -29,7 +29,7 @@ rule parse-rpt rule extract-util-json command = synthrep summary -m utilization > $out rule extract-hierarchy-json - command = synthrep summary -m hierarchy > $out + command = aext vivado out/hierarchical_utilization_placed.rpt > $out flamegraph-script = /test/calyx/non-existent.script create-visuals-script = $calyx-base/tools/profiler/create-visuals.sh diff --git a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-area-report.snap b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-area-report.snap index 00c5003169..3091b44cf9 100644 --- a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-area-report.snap +++ b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-area-report.snap @@ -29,7 +29,7 @@ rule parse-rpt rule extract-util-json command = synthrep summary -m utilization > $out rule extract-hierarchy-json - command = synthrep summary -m hierarchy > $out + command = aext vivado out/hierarchical_utilization_placed.rpt > $out build main.sv: copy /input.ext build device.xdc: copy $device_xdc diff --git a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-hierarchy-json.snap b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-hierarchy-json.snap index e9f403cfd6..83f9fd2dc0 100644 --- a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-hierarchy-json.snap +++ b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-hierarchy-json.snap @@ -29,7 +29,7 @@ rule parse-rpt rule extract-util-json command = synthrep summary -m utilization > $out rule extract-hierarchy-json - command = synthrep summary -m hierarchy > $out + command = aext vivado out/hierarchical_utilization_placed.rpt > $out build main.sv: copy /input.ext build device.xdc: copy $device_xdc diff --git a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-timing-report.snap b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-timing-report.snap index e58662582b..59249e4612 100644 --- a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-timing-report.snap +++ b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-timing-report.snap @@ -29,7 +29,7 @@ rule parse-rpt rule extract-util-json command = synthrep summary -m utilization > $out rule extract-hierarchy-json - command = synthrep summary -m hierarchy > $out + command = aext vivado out/hierarchical_utilization_placed.rpt > $out build main.sv: copy /input.ext build device.xdc: copy $device_xdc diff --git a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-json.snap b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-json.snap index 76956aa4fa..2db6b8f9dd 100644 --- a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-json.snap +++ b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-json.snap @@ -29,7 +29,7 @@ rule parse-rpt rule extract-util-json command = synthrep summary -m utilization > $out rule extract-hierarchy-json - command = synthrep summary -m hierarchy > $out + command = aext vivado out/hierarchical_utilization_placed.rpt > $out build main.sv: copy /input.ext build device.xdc: copy $device_xdc diff --git a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-report.snap b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-report.snap index 04a57831dd..655cf4daa2 100644 --- a/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-report.snap +++ b/fud2/tests/snapshots/tests__test@plan_synth-verilog-to-util-report.snap @@ -29,7 +29,7 @@ rule parse-rpt rule extract-util-json command = synthrep summary -m utilization > $out rule extract-hierarchy-json - command = synthrep summary -m hierarchy > $out + command = aext vivado out/hierarchical_utilization_placed.rpt > $out build main.sv: copy /input.ext build device.xdc: copy $device_xdc From e477cf3dc1e1000dee3b61371777d77eb6af1399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Sun, 26 Oct 2025 19:08:28 -0400 Subject: [PATCH 7/8] Formatted code --- tools/AreaExtract/AreaExtract/lib/cdf/cdf.py | 3 ++- tools/AreaExtract/AreaExtract/lib/parse/vivado.py | 8 +++++++- tools/AreaExtract/AreaExtract/lib/parse/yosys.py | 9 ++++++++- tools/area-profiler/area_profiler/parse.py | 12 ++---------- tools/area-profiler/area_profiler/plot.py | 6 ++++++ 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/tools/AreaExtract/AreaExtract/lib/cdf/cdf.py b/tools/AreaExtract/AreaExtract/lib/cdf/cdf.py index a758570f41..f9b1c0c4dd 100644 --- a/tools/AreaExtract/AreaExtract/lib/cdf/cdf.py +++ b/tools/AreaExtract/AreaExtract/lib/cdf/cdf.py @@ -35,7 +35,7 @@ class Cell: """ Cell with resources. """ - + # Unqualified cell name. name: str # Cell type. @@ -45,6 +45,7 @@ class Cell: # Cell resources. rsrc: Rsrc + @dataclass class Metadata: """ diff --git a/tools/AreaExtract/AreaExtract/lib/parse/vivado.py b/tools/AreaExtract/AreaExtract/lib/parse/vivado.py index 8263fe7ec0..ef2884ef2c 100644 --- a/tools/AreaExtract/AreaExtract/lib/parse/vivado.py +++ b/tools/AreaExtract/AreaExtract/lib/parse/vivado.py @@ -1,5 +1,11 @@ from pathlib import Path -from AreaExtract.lib.cdf.cdf import VivadoRsrc, Cell, Design, Metadata, DesignWithMetadata +from AreaExtract.lib.cdf.cdf import ( + VivadoRsrc, + Cell, + Design, + Metadata, + DesignWithMetadata, +) import re diff --git a/tools/AreaExtract/AreaExtract/lib/parse/yosys.py b/tools/AreaExtract/AreaExtract/lib/parse/yosys.py index 3e5f006f34..219281770a 100644 --- a/tools/AreaExtract/AreaExtract/lib/parse/yosys.py +++ b/tools/AreaExtract/AreaExtract/lib/parse/yosys.py @@ -1,10 +1,17 @@ import re import json from dataclasses import dataclass -from AreaExtract.lib.cdf.cdf import YosysRsrc, Cell, Design, Metadata, DesignWithMetadata +from AreaExtract.lib.cdf.cdf import ( + YosysRsrc, + Cell, + Design, + Metadata, + DesignWithMetadata, +) toplevel: str = "main" + @dataclass class CellWithParams: """ diff --git a/tools/area-profiler/area_profiler/parse.py b/tools/area-profiler/area_profiler/parse.py index 1432d5d3b0..ffa899d0eb 100644 --- a/tools/area-profiler/area_profiler/parse.py +++ b/tools/area-profiler/area_profiler/parse.py @@ -157,16 +157,8 @@ def main(): parser = argparse.ArgumentParser( description="Utility to process Yosys IL and stat files and dump design map as JSON" ) - parser.add_argument( - "il_file", - type=pathlib.Path, - help="path to the IL file" - ) - parser.add_argument( - "stat_file", - type=pathlib.Path, - help="path to the stat file" - ) + parser.add_argument("il_file", type=pathlib.Path, help="path to the IL file") + parser.add_argument("stat_file", type=pathlib.Path, help="path to the stat file") parser.add_argument( "-o", "--output", diff --git a/tools/area-profiler/area_profiler/plot.py b/tools/area-profiler/area_profiler/plot.py index 0a777f1fc0..34d2c2df30 100644 --- a/tools/area-profiler/area_profiler/plot.py +++ b/tools/area-profiler/area_profiler/plot.py @@ -15,10 +15,12 @@ "std_reg": 8.0, } + def load_data(path: Path): with open(path) as f: return json.load(f) + def compute_areas(data): areas = [] for name, cell in data.items(): @@ -29,6 +31,7 @@ def compute_areas(data): areas.append({"cell_name": name, "cell_type": t, "width": w, "area": area}) return areas + def make_bar_chart(areas, output): type_area = defaultdict(float) for a in areas: @@ -44,6 +47,7 @@ def make_bar_chart(areas, output): ) fig.write_html(output) + def make_treemap(areas, output): fig = px.treemap( areas, @@ -53,6 +57,7 @@ def make_treemap(areas, output): ) fig.write_html(output) + def main(): parser = argparse.ArgumentParser( description="Estimate and plot cell areas based on a heuristic" @@ -86,5 +91,6 @@ def main(): output = args.output or Path("area_treemap.html") make_treemap(areas, output) + if __name__ == "__main__": main() From 23603c17b6633843cc4a46a5ad0850226d90a29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pontes=20Garc=C3=ADa?= Date: Sun, 26 Oct 2025 19:11:03 -0400 Subject: [PATCH 8/8] Formatted code --- tools/area-profiler/area_profiler/parse.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/area-profiler/area_profiler/parse.py b/tools/area-profiler/area_profiler/parse.py index ffa899d0eb..379b348ea0 100644 --- a/tools/area-profiler/area_profiler/parse.py +++ b/tools/area-profiler/area_profiler/parse.py @@ -1,5 +1,4 @@ import pathlib -import sys import re import json from dataclasses import dataclass, asdict, is_dataclass