From 88c059087f1b94b91cbad7628792eac4ae1688a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 27 Oct 2026 21:22:03 +0100 Subject: [PATCH 1/2] synth: remove obsolete make memory report and clean up code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make memory printed the inferred-memory table of the last synthesis from mem.json. Nothing in ORFS documents or calls it, and its mem_hierarchical.json branch has read a file nothing writes since hierarchical synthesis moved to estimates (d07d6d94e). What is in use is the SYNTH_MEMORY_MAX_BITS check, which stops a doomed synthesis early. That check needed mem.json, a JSON dump of the whole design written on every synthesis, only so that mem_dump.py could find the memories in it. synth.tcl already loops over the same memories in Tcl for SYNTH_MOCK_LARGE_MEMORIES. The check now runs in that loop, synth_memory_max_bits in flow/scripts/memories/max_bits.tcl, and mem.json, mem_dump.py and make memory go. Every synthesis now logs each inferred memory with its dimensions, a ROM marked as one; the log lines are the ones the mock already wrote. A read/write memory over the limit stops synthesis with an error that names each one, its size and the limit. A ROM is not counted. The design is still sorted where json -o mem.json sorted it: a yosys backend sorts the design by name before it writes, and the passes after it depend on that order. write_json /dev/null keeps the sort without the file; dropping it changes results and would be its own change. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/Makefile | 7 -- flow/scripts/mem_dump.py | 155 ----------------------------- flow/scripts/memories/max_bits.tcl | 41 ++++++++ flow/scripts/synth.tcl | 35 ++----- flow/scripts/synth_preamble.tcl | 1 + flow/test/test_mem_dump.py | 77 -------------- flow/test/test_memory_max_bits.py | 101 +++++++++++++++++++ 7 files changed, 153 insertions(+), 264 deletions(-) delete mode 100644 flow/scripts/mem_dump.py create mode 100644 flow/scripts/memories/max_bits.tcl delete mode 100644 flow/test/test_mem_dump.py create mode 100644 flow/test/test_memory_max_bits.py diff --git a/flow/Makefile b/flow/Makefile index 759e13578d..d6e0c91600 100644 --- a/flow/Makefile +++ b/flow/Makefile @@ -246,13 +246,6 @@ synth-report: synth do-synth-report: $(RUN_CMD) --log $(abspath $(LOG_DIR)/1_2_yosys_metrics.log) --tee -- $(OPENROAD_CMD) $(SCRIPTS_DIR)/synth_metrics.tcl -.PHONY: memory -memory: - if [ -f $(RESULTS_DIR)/mem_hierarchical.json ]; then \ - $(PYTHON_EXE) $(SCRIPTS_DIR)/mem_dump.py $(RESULTS_DIR)/mem_hierarchical.json; \ - fi - $(PYTHON_EXE) $(SCRIPTS_DIR)/mem_dump.py $(RESULTS_DIR)/mem.json - # ============================================================================== diff --git a/flow/scripts/mem_dump.py b/flow/scripts/mem_dump.py deleted file mode 100644 index cfb2b5d9a3..0000000000 --- a/flow/scripts/mem_dump.py +++ /dev/null @@ -1,155 +0,0 @@ -import argparse -import json -import sys - - -def find_top_modules(data): - top_module = [] - instantiations = set( - [ - cell["type"] - for minfo2 in data["modules"].values() - for cell in minfo2["cells"].values() - ] - ) - for mname in data["modules"].keys(): - if mname not in instantiations: - top_module.append(mname) - return top_module - - -def find_cells_by_type_in_module( - module_name, data, target_type, current_path, matching_cells -): - for cell_name, cell in data["modules"][module_name]["cells"].items(): - cell_path = ( - f"{current_path}.{module_name}.{cell_name}" - if current_path - else f"{module_name}.{cell_name}" - ) - if cell["type"] == target_type: - matching_cells.append(cell_path) - elif cell["type"] in data["modules"]: - matching_cells.extend( - find_cells_by_type_in_module( - cell["type"], data, target_type, cell_path, [] - ) - ) - return matching_cells - - -def find_cells_by_type(top_modules, data, module_name, current_path=""): - names = [] - for top_module in top_modules: - names.extend( - find_cells_by_type_in_module( - top_module, data, module_name, current_path, [] - ) - ) - return names - - -def format_ram_table_from_json(data, max_bits=None): - top_modules = find_top_modules(data) - formatting = "{:>5} | {:>5} | {:>6} | {:<20} | {:<80}\n" - table = formatting.format("Rows", "Width", "Bits", "Module", "Instances") - table += "-" * len(table) + "\n" - - max_ok = True - entries = [] - - for module_name, module_info in data["modules"].items(): - for cell in module_info["cells"].values(): - if not cell["type"].startswith("$mem"): - continue - parameters = cell["parameters"] - size = int(parameters["SIZE"], 2) - width = int(parameters["WIDTH"], 2) - instances = find_cells_by_type(top_modules, data, module_name) - instance_bits = size * width - bits = instance_bits * len(instances) - - # A ROM has no write port: its contents are constants that - # synthesis maps to logic, so it is not storage a RAM would - # replace and SYNTH_MEMORY_MAX_BITS does not apply to it. - rom = int(parameters["WR_PORTS"], 2) == 0 - - entries.append((size, width, bits, module_name, ", ".join(instances), rom)) - - if max_bits is not None and instance_bits > max_bits and not rom: - max_ok = False - - entries.sort(key=lambda x: x[2], reverse=True) - - for size, width, bits, module, instances, rom in entries: - kind = "ROM " if rom else "" - table += formatting.format(size, width, bits, kind + module, instances) - - # ---- Summary statistics ---- - total_bits = sum(e[2] for e in entries) - - largest_instance_bits = 0 - largest_instance_module = None - - for size, width, _, module, _, _ in entries: - instance_bits = size * width - if instance_bits > largest_instance_bits: - largest_instance_bits = instance_bits - largest_instance_module = module - - summary = { - "memory_count": len(entries), - "largest_instance_bits": largest_instance_bits, - "largest_instance_module": largest_instance_module, - "largest_total_bits": entries[0][2] if entries else 0, - "total_bits": total_bits, - } - - return table, max_ok, summary - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("file") - parser.add_argument("-m", "--max-bits", type=int, default=None) - args = parser.parse_args() - - with open(args.file, "r") as file: - json_data = json.load(file) - - src_files = set() - for module_name, module_info in json_data["modules"].items(): - for cell in list(module_info["cells"].values()) + [module_info]: - if "src" not in cell["attributes"]: - continue - src_file = cell["attributes"]["src"].split(":")[0] - src_files.add(src_file) - - print("Source files actually used in the design:") - print(" " + "\n ".join(src_files)) - - print("Memories found in the design:") - formatted_table, max_ok, summary = format_ram_table_from_json( - json_data, args.max_bits - ) - - print(formatted_table) - - print("Summary:") - print(f"- Total inferred memories: {summary['memory_count']}") - print( - f"- Largest single memory instance: " - f"{summary['largest_instance_bits']} bits " - f"(module {summary['largest_instance_module']})" - ) - print(f"- Total inferred memory bits (all instances): {summary['total_bits']}") - - if args.max_bits is not None: - status = "OK" if max_ok else "FAIL" - print(f"- SYNTH_MEMORY_MAX_BITS: {args.max_bits}") - print(f"- Status: {status}") - - if not max_ok: - sys.exit( - f"Error: Synthesized memory size {args.max_bits} exceeds SYNTH_MEMORY_MAX_BITS" - ) diff --git a/flow/scripts/memories/max_bits.tcl b/flow/scripts/memories/max_bits.tcl new file mode 100644 index 0000000000..ca03099a44 --- /dev/null +++ b/flow/scripts/memories/max_bits.tcl @@ -0,0 +1,41 @@ +# Log every inferred memory with its dimensions. With mock set, shrink each +# memory over max_bits to one row, kept as a module when keep_mocked is set; +# otherwise stop with an error naming every memory over max_bits that has a +# write port. Returns the mocked memories as a list of {module width size}. +proc synth_memory_max_bits { max_bits mock keep_mocked } { + memory_collect + set mocked {} + set over {} + foreach path [split [string trim [tee -q -s result.string select -list t:\$mem_v2]] "\n"] { + set index [string first "/" $path] + set module [string range $path 0 [expr { $index - 1 }]] + set instance [string range $path [expr { $index + 1 }] end] + + set width [rtlil::get_param -uint $module $instance WIDTH] + set size [rtlil::get_param -uint $module $instance SIZE] + set rom [expr { [rtlil::get_param -uint $module $instance WR_PORTS] == 0 }] + set nbits [expr { $width * $size }] + puts "Memory $path has dimensions $size x $width = $nbits[expr { $rom ? ", a ROM" : "" }]" + if { $nbits <= $max_bits } { + continue + } + if { $mock } { + rtlil::set_param -uint $module $instance SIZE 1 + puts "Shrunk memory $path from $size rows to 1" + lappend mocked [list $module $width $size] + if { $keep_mocked } { + select -module $module + setattr -mod -set keep_hierarchy 1 + select -clear + } + } elseif { !$rom } { + lappend over "$path $size x $width = $nbits" + } + } + if { [llength $over] > 0 } { + error "Memories over SYNTH_MEMORY_MAX_BITS=$max_bits, which would go into\ + flip-flops: [join $over {, }]. Put them in macros (AUTO_MEMORIES,\ + fakeram), mock them (SYNTH_MOCK_LARGE_MEMORIES=1) or raise the limit." + } + return $mocked +} diff --git a/flow/scripts/synth.tcl b/flow/scripts/synth.tcl index aa4d3afb6c..857159e141 100644 --- a/flow/scripts/synth.tcl +++ b/flow/scripts/synth.tcl @@ -149,37 +149,22 @@ if { [env_var_equals SYNTH_HDL_FRONTEND slang] } { opt_clean } +# Fail early if this synthesis run is doomed +set mocked [synth_memory_max_bits $::env(SYNTH_MEMORY_MAX_BITS) \ + $::env(SYNTH_MOCK_LARGE_MEMORIES) $::env(SYNTH_KEEP_MOCKED_MEMORIES)] if { $::env(SYNTH_MOCK_LARGE_MEMORIES) } { - memory_collect - set select [tee -q -s result.string select -list t:\$mem_v2] set report_file [open $::env(REPORTS_DIR)/synth_mocked_memories.txt "w"] - foreach path [split [string trim $select] "\n"] { - set index [string first "/" $path] - set module [string range $path 0 [expr { $index - 1 }]] - set instance [string range $path [expr { $index + 1 }] end] - - set width [rtlil::get_param -uint $module $instance WIDTH] - set size [rtlil::get_param -uint $module $instance SIZE] - set nbits [expr $width * $size] - puts "Memory $path has dimensions $size x $width = $nbits" - if { $nbits > $::env(SYNTH_MEMORY_MAX_BITS) } { - rtlil::set_param -uint $module $instance SIZE 1 - puts "Shrunk memory $path from $size rows to 1" - puts -nonewline $report_file "$module:\n width: $width\n size: $size\n" - if { $::env(SYNTH_KEEP_MOCKED_MEMORIES) } { - select -module $module - setattr -mod -set keep_hierarchy 1 - select -clear - } - } + foreach memory $mocked { + lassign $memory module width size + puts -nonewline $report_file "$module:\n width: $width\n size: $size\n" } close $report_file } -json -o $::env(RESULTS_DIR)/mem.json -# Run report and check here so as to fail early if this synthesis run is doomed -exec -- $::env(PYTHON_EXE) $::env(SCRIPTS_DIR)/mem_dump.py \ - --max-bits $::env(SYNTH_MEMORY_MAX_BITS) $::env(RESULTS_DIR)/mem.json +# A yosys backend sorts the design by name before it writes, and the passes +# below depend on that order; json -o mem.json did it here. The sort stays, +# the file goes: dropping the sort changes results and is its own change. +write_json /dev/null if { [env_var_exists_and_non_empty SYNTH_RETIME_MODULES] } { select {*}$::env(SYNTH_RETIME_MODULES) diff --git a/flow/scripts/synth_preamble.tcl b/flow/scripts/synth_preamble.tcl index 268f5cd9da..28cc73e4fc 100644 --- a/flow/scripts/synth_preamble.tcl +++ b/flow/scripts/synth_preamble.tcl @@ -49,6 +49,7 @@ proc read_checkpoint { file } { # canonicalization, one ` ` per line; see # docs/user/AutoMemories.md. source $::env(SCRIPTS_DIR)/memories/check_blackboxed.tcl +source $::env(SCRIPTS_DIR)/memories/max_bits.tcl proc auto_memories_areas { } { if { ![env_var_equals AUTO_MEMORIES 1] } { diff --git a/flow/test/test_mem_dump.py b/flow/test/test_mem_dump.py deleted file mode 100644 index ab37f69ef6..0000000000 --- a/flow/test/test_mem_dump.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -"""SYNTH_MEMORY_MAX_BITS stops synthesis on a read/write memory over the -limit, and lets a ROM of the same size through. - -mem_dump.py reads the memories yosys inferred (mem.json) and exits with -an error when one is over the limit. A ROM, a $mem_v2 with no write -port, holds constants that synthesis maps to logic: it is listed, marked -as a ROM, and not counted. -""" - -import json -import os -import subprocess -import sys -import tempfile -import unittest - -MEM_DUMP = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "..", "scripts", "mem_dump.py" -) - - -def mem_json(wr_ports, size=64, width=128): - def b(n): - return format(n, "032b") - - return { - "modules": { - "top": { - "attributes": {}, - "cells": { - "mem": { - "type": "$mem_v2", - "attributes": {}, - "parameters": { - "SIZE": b(size), - "WIDTH": b(width), - "WR_PORTS": b(wr_ports), - }, - } - }, - } - } - } - - -class TestMemDump(unittest.TestCase): - def run_mem_dump(self, data, max_bits): - with tempfile.TemporaryDirectory() as tmp: - path = os.path.join(tmp, "mem.json") - with open(path, "w") as f: - json.dump(data, f) - return subprocess.run( - [sys.executable, MEM_DUMP, "--max-bits", str(max_bits), path], - capture_output=True, - text=True, - ) - - def test_ram_over_limit_stops(self): - # 64 x 128 = 8192 bits, one write port - result = self.run_mem_dump(mem_json(wr_ports=1), 4096) - self.assertNotEqual(result.returncode, 0, result.stdout) - self.assertIn("exceeds SYNTH_MEMORY_MAX_BITS", result.stderr) - - def test_rom_over_limit_passes(self): - # the same 8192 bits with no write port - result = self.run_mem_dump(mem_json(wr_ports=0), 4096) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("ROM top", result.stdout) - - def test_ram_under_limit_passes(self): - result = self.run_mem_dump(mem_json(wr_ports=1), 8192) - self.assertEqual(result.returncode, 0, result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/flow/test/test_memory_max_bits.py b/flow/test/test_memory_max_bits.py new file mode 100644 index 0000000000..a338aa0e4f --- /dev/null +++ b/flow/test/test_memory_max_bits.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""SYNTH_MEMORY_MAX_BITS stops synthesis on a read/write memory over the +limit, and lets a ROM of the same size through. + +synth_memory_max_bits logs every memory yosys inferred and stops with an +error naming each read/write memory over the limit. A ROM, a $mem_v2 with +no write port, holds constants that synthesis maps to logic: it is logged, +marked as a ROM, and not counted. With SYNTH_MOCK_LARGE_MEMORIES, a memory +over the limit is shrunk to one row instead. + +Skipped when no yosys is available: YOSYS_EXE or yosys on PATH. +""" + +import os +import shutil +import subprocess +import tempfile +import unittest + +MAX_BITS_TCL = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "scripts", + "memories", + "max_bits.tcl", +) + +# A RAM with one write port, 32 x 128 = 4096 bits, and a ROM twice its size, +# 64 x 128 = 8192 bits +DESIGN = """\ +module ram (input clk, input we, input [4:0] addr, input [127:0] d, + output reg [127:0] q); + reg [127:0] mem [0:31]; + always @(posedge clk) begin + if (we) mem[addr] <= d; + q <= mem[addr]; + end +endmodule +module rom (input clk, input [5:0] addr, output reg [127:0] q); + reg [127:0] mem [0:63]; + integer i; + initial for (i = 0; i < 64; i = i + 1) mem[i] = {4{i * 32'h9e3779b9}}; + always @(posedge clk) q <= mem[addr]; +endmodule +module top (input clk, input we, input [5:0] addr, input [127:0] d, + output [127:0] q0, q1); + ram u_ram (.clk, .we, .addr(addr[4:0]), .d, .q(q0)); + rom u_rom (.clk, .addr, .q(q1)); +endmodule +""" + + +class TestMemoryMaxBits(unittest.TestCase): + def run_check(self, max_bits, mock=0): + yosys = os.environ.get("YOSYS_EXE") or shutil.which("yosys") + if not yosys: + self.skipTest("no yosys") + with tempfile.TemporaryDirectory() as tmp: + design = os.path.join(tmp, "top.v") + with open(design, "w") as f: + f.write(DESIGN) + script = os.path.join(tmp, "check.tcl") + with open(script, "w") as f: + f.write( + "yosys -import\n" + f"source {{{MAX_BITS_TCL}}}\n" + f"yosys read_verilog -sv {{{design}}}\n" + "yosys hierarchy -top top\n" + "yosys proc\n" + "yosys memory -nomap\n" + f'puts "mocked: [synth_memory_max_bits {max_bits} {mock} 0]"\n' + ) + result = subprocess.run( + [yosys, "-q", "-c", script], capture_output=True, text=True + ) + return result.returncode, result.stdout + result.stderr + + def test_ram_over_limit_stops(self): + returncode, output = self.run_check(2048) + self.assertNotEqual(returncode, 0, output) + error = output.split("over SYNTH_MEMORY_MAX_BITS=2048")[1] + self.assertRegex(error, r"ram/\S+ 32 x 128 = 4096") + self.assertNotIn("rom/", error) + + def test_rom_over_limit_passes(self): + returncode, output = self.run_check(4096) + self.assertEqual(returncode, 0, output) + self.assertRegex( + output, r"Memory rom/\S+ has dimensions 64 x 128 = 8192, a ROM\n" + ) + self.assertRegex(output, r"Memory ram/\S+ has dimensions 32 x 128 = 4096\n") + + def test_mock_shrinks_instead_of_stopping(self): + returncode, output = self.run_check(2048, mock=1) + self.assertEqual(returncode, 0, output) + self.assertRegex(output, r"Shrunk memory ram/\S+ from 32 rows to 1") + self.assertIn("{ram 128 32}", output) + + +if __name__ == "__main__": + unittest.main() From d7867ca1516746097486b93ec4afcc5a68f0443e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Mon, 2 Nov 2026 18:25:22 +0100 Subject: [PATCH 2/2] synth: test_memory_max_bits checks the error is there before parsing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failure for another reason, a yosys syntax error say, raised an IndexError that hid yosys's output; assertIn shows it. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/test/test_memory_max_bits.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flow/test/test_memory_max_bits.py b/flow/test/test_memory_max_bits.py index a338aa0e4f..6dcdf80df2 100644 --- a/flow/test/test_memory_max_bits.py +++ b/flow/test/test_memory_max_bits.py @@ -78,6 +78,7 @@ def run_check(self, max_bits, mock=0): def test_ram_over_limit_stops(self): returncode, output = self.run_check(2048) self.assertNotEqual(returncode, 0, output) + self.assertIn("over SYNTH_MEMORY_MAX_BITS=2048", output) error = output.split("over SYNTH_MEMORY_MAX_BITS=2048")[1] self.assertRegex(error, r"ram/\S+ 32 x 128 = 4096") self.assertNotIn("rom/", error)