Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added graph.pdf
Binary file not shown.
43 changes: 43 additions & 0 deletions tools/area-profiler/README.md
Original file line number Diff line number Diff line change
@@ -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 <IL_FILE> <STAT_FILE> [-o OUTPUT]
```

- `-o` optional output JSON (default stdout)

**`aprof-plot`** – visualize JSON summary

```bash
aprof plot <INPUT_JSON> <MODE> [-o OUTPUT]
```

- `MODE` one of `bar`, `treemap`
- `-o` optional output HTML (default depends on mode)
3 changes: 3 additions & 0 deletions tools/area-profiler/area_profiler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""WIP."""

__version__ = "0.1.0"
204 changes: 204 additions & 0 deletions tools/area-profiler/area_profiler/parse.py
Original file line number Diff line number Diff line change
@@ -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()
90 changes: 90 additions & 0 deletions tools/area-profiler/area_profiler/plot.py
Original file line number Diff line number Diff line change
@@ -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()
15 changes: 15 additions & 0 deletions tools/area-profiler/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading