From e53f73ae1f81f8080be2a1fa19a337cee30c1be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Tue, 27 Oct 2026 06:11:40 +0100 Subject: [PATCH] synth: a ROM is not counted against SYNTH_MEMORY_MAX_BITS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SYNTH_MEMORY_MAX_BITS is the AUTO_MEMORIES policy's tripwire: a read/write memory larger than it belongs in a macro (AUTO_MEMORIES, fakeram) or a mock, and must not go undetected into flip-flops. mem_dump.py counted ROMs too. A ROM, a $mem_v2 with no write port, holds constants that synthesis maps to logic like any other logic; it is not storage a RAM would replace, and it goes through the normal flow. Admitting a large ROM meant raising the limit, which let read/write memories of that size through as well. coralnpu's FPU divider has one (#4653): its SRT bound table, 128 x 83 = 10,624 bits, which synthesises on its own in about 3 s to about 700 cells on asap7. ROMs are rare, so they get no limit of their own: they are not counted. The table still lists every memory, a ROM marked as one, and variables.yaml says so under SYNTH_MEMORY_MAX_BITS. test_mem_dump.py checks that a read/write memory over the limit stops synthesis and that a ROM of the same size does not. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- docs/user/FlowVariables.md | 2 +- flow/scripts/mem_dump.py | 16 +++++--- flow/scripts/variables.json | 2 +- flow/scripts/variables.yaml | 7 ++++ flow/test/test_mem_dump.py | 77 +++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 flow/test/test_mem_dump.py diff --git a/docs/user/FlowVariables.md b/docs/user/FlowVariables.md index 8d32a9d6ad..e03f5971d5 100644 --- a/docs/user/FlowVariables.md +++ b/docs/user/FlowVariables.md @@ -305,7 +305,7 @@ configuration file. | SYNTH_INSBUF| Insert input buffers on top-level input ports during synthesis. Useful to disable when doing parallel synthesis and concatenating netlists later as we're generating netlists of submodules.| 1| | SYNTH_KEEP_MOCKED_MEMORIES| When `SYNTH_MOCK_LARGE_MEMORIES=1`, setting this to 1, will keep mocked memories (not flattening them). This preserves some of the access logic complexity and avoids optimizations outside of the mocked memory.| 1| | SYNTH_KEEP_MODULES| Mark modules to keep from getting removed in flattening.| | -| SYNTH_MEMORY_MAX_BITS| Maximum number of bits for memory synthesis. Ideally, real RAM or realistic fakeram should be used for RAMs much larger than 1024 bits. To temporarily ignore the RAM concerns and investigate other aspects of the design, consider setting `SYNTH_MOCK_LARGE_MEMORIES=1`, or adjusting `SYNTH_MEMORY_MAX_BITS`.| 4096| +| SYNTH_MEMORY_MAX_BITS| Maximum number of bits for memory synthesis. Ideally, real RAM or realistic fakeram should be used for RAMs much larger than 1024 bits. To temporarily ignore the RAM concerns and investigate other aspects of the design, consider setting `SYNTH_MOCK_LARGE_MEMORIES=1`, or adjusting `SYNTH_MEMORY_MAX_BITS`. ROMs, inferred memories with no write port, are not counted: a ROM holds constants that synthesis maps to logic like any other, and it goes through the normal flow. The limit is there so that a read/write memory, which belongs in a macro (`AUTO_MEMORIES`, fakeram) or a mock, does not go undetected into flip-flops; raising it to admit a large ROM would let read/write memories of that size through too.| 4096| | SYNTH_MINIMUM_KEEP_SIZE| For hierarchical synthesis, we keep modules of larger area than given by this variable and flatten smaller modules. The area unit used is the size of a basic nand2 gate from the platform's standard cell library. The default value is platform specific.| 0| | SYNTH_MOCK_LARGE_MEMORIES| Reduce Yosys inferred memories larger than SYNTH_MEMORY_MAX_BITS to 1 row. Yosys will generally infer memories from behavioral Verilog code, whether the memories are in standalone modules or instantiated within some larger module. fakeram and empty Verilog memories(blackboxes) of memories will not be inferred memories by Yosys and are therefore not affected by this variable. This is useful and convenient to separate the concern of instantiating and placing memories from investigating other issues with a design, though it comes at the expense of the increased accuracy that using realistic fakemem would provide. Memories with a single 1 row will of course have unrealistically good timing and area characteristics, but timing will still correctly terminate in a register. Large port memories, typically register files, will still have the retain a lot of the port logic that can be useful to investigate issues. This can be especially useful during development of designs where the behavioral model comes first and suitable memories are matched up when the design RTL is stable. A typical use case would be Chisel which will generate a behavioral model for a memories with the required clocks, ports, etc. in addition to a computer readable file with the specification of the memories that is used to [automatically](https://chipyard.readthedocs.io/en/stable/Tools/Barstools.html/) match up suitable memory macros later in the flow. During an architectural screening study, a large range of memory configurations can be investigated quickly with this option, without getting bogged down in the concern of how to realize the memories in silicon for emphemral RTL configurations that exist only long enough to run through the ORFS flow to create a table of some characteristics of a design configuration.| 0| | SYNTH_NETLIST_FILES| Skips synthesis and uses the supplied netlist files. If the netlist files contains duplicate modules, which can happen when using hierarchical synthesis on indvidual netlist files and combining here, subsequent modules are silently ignored and only the first module is used.| | diff --git a/flow/scripts/mem_dump.py b/flow/scripts/mem_dump.py index aac74f9e56..cfb2b5d9a3 100644 --- a/flow/scripts/mem_dump.py +++ b/flow/scripts/mem_dump.py @@ -69,15 +69,21 @@ def format_ram_table_from_json(data, max_bits=None): instance_bits = size * width bits = instance_bits * len(instances) - entries.append((size, width, bits, module_name, ", ".join(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 - if max_bits is not None and instance_bits > max_bits: + 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 entry in entries: - table += formatting.format(*entry) + 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) @@ -85,7 +91,7 @@ def format_ram_table_from_json(data, max_bits=None): largest_instance_bits = 0 largest_instance_module = None - for size, width, _, module, _ in entries: + for size, width, _, module, _, _ in entries: instance_bits = size * width if instance_bits > largest_instance_bits: largest_instance_bits = instance_bits diff --git a/flow/scripts/variables.json b/flow/scripts/variables.json index 2810d1ec80..705ca89012 100644 --- a/flow/scripts/variables.json +++ b/flow/scripts/variables.json @@ -1361,7 +1361,7 @@ }, "SYNTH_MEMORY_MAX_BITS": { "default": 4096, - "description": "Maximum number of bits for memory synthesis. Ideally, real RAM or realistic fakeram should be used for RAMs much larger than 1024 bits. To temporarily ignore the RAM concerns and investigate other aspects of the design, consider setting `SYNTH_MOCK_LARGE_MEMORIES=1`, or adjusting `SYNTH_MEMORY_MAX_BITS`.\n", + "description": "Maximum number of bits for memory synthesis. Ideally, real RAM or realistic fakeram should be used for RAMs much larger than 1024 bits. To temporarily ignore the RAM concerns and investigate other aspects of the design, consider setting `SYNTH_MOCK_LARGE_MEMORIES=1`, or adjusting `SYNTH_MEMORY_MAX_BITS`. ROMs, inferred memories with no write port, are not counted: a ROM holds constants that synthesis maps to logic like any other, and it goes through the normal flow. The limit is there so that a read/write memory, which belongs in a macro (`AUTO_MEMORIES`, fakeram) or a mock, does not go undetected into flip-flops; raising it to admit a large ROM would let read/write memories of that size through too.\n", "stages": [ "synth" ] diff --git a/flow/scripts/variables.yaml b/flow/scripts/variables.yaml index 4d71623a35..ad4863abfb 100644 --- a/flow/scripts/variables.yaml +++ b/flow/scripts/variables.yaml @@ -264,6 +264,13 @@ SYNTH_MEMORY_MAX_BITS: To temporarily ignore the RAM concerns and investigate other aspects of the design, consider setting `SYNTH_MOCK_LARGE_MEMORIES=1`, or adjusting `SYNTH_MEMORY_MAX_BITS`. + ROMs, inferred memories with no write port, are not counted: a ROM + holds constants that synthesis maps to logic like any other, and it + goes through the normal flow. The limit is there so that a + read/write memory, which belongs in a macro (`AUTO_MEMORIES`, + fakeram) or a mock, does not go undetected into flip-flops; raising + it to admit a large ROM would let read/write memories of that size + through too. default: 4096 stages: - synth diff --git a/flow/test/test_mem_dump.py b/flow/test/test_mem_dump.py new file mode 100644 index 0000000000..ab37f69ef6 --- /dev/null +++ b/flow/test/test_mem_dump.py @@ -0,0 +1,77 @@ +#!/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()