diff --git a/graph.pdf b/graph.pdf new file mode 100644 index 0000000000..6918600a9d Binary files /dev/null and b/graph.pdf differ diff --git a/tools/area-profiler/README.md b/tools/area-profiler/README.md new file mode 100644 index 0000000000..fe687981ca --- /dev/null +++ b/tools/area-profiler/README.md @@ -0,0 +1,43 @@ +# 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 + +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 +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"