From 17feb4dd890acc1658c1a6e9b9c4e3b11ef82efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:04:40 +0200 Subject: [PATCH 1/3] FakeRAM asap7: pick a column mux factor instead of hardcoding 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ASAP7 backend hardcodes `"column_mux_factor": 1`, so a generated array is `rows` bit cells tall and `bits` wide with nothing folding it. A deep, narrow memory comes out as a sliver: asap7/tage_update's 512x17 SRAM is 2.28 x 166.6 um, 73:1, and with 40 such macros macro placement stops with MPL-0065, "The movable cells do not fit in the macro placement area". The platform's own views are not built this way. Height scales as rows/column_mux_factor and width as bits*column_mux_factor, and the shipped LEFs read back through that relation give the factor each was generated with: fakeram7_2048x39 20.33 x 166.60 um -> column_mux_factor 4 fakeram7_256x32 8.36 x 42.00 um -> column_mux_factor 2 fakeram7_256x256 33.25 x 84.00 um -> column_mux_factor 1 The factor is now chosen per memory: the power of two that brings rows/factor closest to bits*factor. That reproduces 256x256 exactly and 256x32 and 2048x39 within one step. Area does not change: column muxing moves bit cells between the two dimensions. The 512x17 SRAM becomes 4.7:1, and tage_update gets through macro placement. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/gen_memories_test.py | 31 ++++++++++++++++++++++ tools/FakeRAM2.0/orfs_asap7/generate.py | 29 +++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index ffd02593d9..57581e788b 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import re import sys import tempfile import unittest @@ -134,6 +135,36 @@ def test_additional_memories_forces_conversion(self): self.assertTrue(tags["idiomatic"]) self.assertIn("forced", tags["reason"]) + def test_deep_memory_is_folded_square(self): + # 512x17 with one bit line per column is 2.3 x 166.6 um, 73:1, a + # sliver macro placement cannot fit; the column mux folds it. The + # fold squares the bit-cell counts, and a bit cell is not square, + # so the bound leaves room for that. + netlist = json.loads(json.dumps(SAMPLE_YOSYS_NETLIST)) + mem = netlist["modules"].pop("\\mem_128x32") + mem["cells"]["\\$mem_0"]["parameters"].update(SIZE=512, WIDTH=17) + netlist["modules"] = {"\\mem_512x17": mem} + d = Path(self.tmp.name) + (d / "netlist.json").write_text(json.dumps(netlist)) + code = gen_memories.main( + [ + "--platform", + "asap7", + "--out-dir", + str(d / "memories"), + "--json", + str(d / "memories.json"), + "--yosys-json", + str(d / "netlist.json"), + ] + ) + self.assertEqual(code, 0) + lef = (d / "memories" / "mem_512x17.lef").read_text() + width, height = map( + float, re.search(r"SIZE\s+(\S+)\s+BY\s+(\S+)", lef).groups() + ) + self.assertLessEqual(max(width, height) / min(width, height), 8.0) + def test_unsupported_platform_fails_clearly(self): code, _d = self.run_generator(platform="nangate45") self.assertEqual(code, 1) diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index be30f8f927..9e2d968f97 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -1,4 +1,5 @@ import json +import math import sys from pathlib import Path @@ -34,6 +35,29 @@ } +def pick_column_mux_factor(rows, bits): + """Fold a tall array into a square one. + + The array is rows/factor cells tall and bits*factor cells wide, so + the factor that squares it is sqrt(rows/bits). Rounded to a power of + two because a column mux selects one of 2^n bit lines, and clamped + at 1 so a wide, shallow memory is left alone. + + Area does not change: this moves cells between the two dimensions + rather than adding or removing any. + """ + if rows <= 0 or bits <= 0: + return 1 + ideal = math.sqrt(float(rows) / float(bits)) + if ideal <= 1.0: + return 1 + lo = 2 ** int(math.floor(math.log(ideal, 2))) + hi = lo * 2 + # Nearest in log space, which is what "closest to square" means when + # the candidates are powers of two. + return lo if (ideal / lo) <= (hi / ideal) else hi + + def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): if platform != "asap7": sys.stderr.write(f"FakeRAM2.0 orfs_asap7: unsupported platform {platform}\n") @@ -51,7 +75,6 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): (m for m in memories if m.get("idiomatic")), key=lambda m: m["name"] ) - process = Process(ASAP7_PROCESS_CONFIG) timing_data = TimingData(ASAP7_TIMING_CONFIG) for m in converted: @@ -59,6 +82,10 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): bits = m.get("bits", 32) rows = m.get("rows", 128) + process_config = dict(ASAP7_PROCESS_CONFIG) + process_config["column_mux_factor"] = pick_column_mux_factor(rows, bits) + process = Process(process_config) + sram_dict = { "name": name, "width": bits, From 4d31c4f2d4bc3a5ec5227ffb1fbffffb9ae1d82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Fri, 9 Oct 2026 10:30:35 +0200 Subject: [PATCH 2/3] FakeRAM asap7: math.log2 for the column mux factor; name a LEF without SIZE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review: math.log2 instead of math.log(x, 2), and math.floor already returns an int. Same factor for every shape: checked over rows 1..70000 and bits 1..300. The test now says so when the generated LEF has no SIZE, instead of failing on None.groups(). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- flow/scripts/memories/gen_memories_test.py | 6 +++--- tools/FakeRAM2.0/orfs_asap7/generate.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index 57581e788b..8c7080bf89 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -160,9 +160,9 @@ def test_deep_memory_is_folded_square(self): ) self.assertEqual(code, 0) lef = (d / "memories" / "mem_512x17.lef").read_text() - width, height = map( - float, re.search(r"SIZE\s+(\S+)\s+BY\s+(\S+)", lef).groups() - ) + size = re.search(r"SIZE\s+(\S+)\s+BY\s+(\S+)", lef) + self.assertIsNotNone(size, "no SIZE in the generated LEF") + width, height = map(float, size.groups()) self.assertLessEqual(max(width, height) / min(width, height), 8.0) def test_unsupported_platform_fails_clearly(self): diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index 9e2d968f97..75c262b25e 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -51,7 +51,7 @@ def pick_column_mux_factor(rows, bits): ideal = math.sqrt(float(rows) / float(bits)) if ideal <= 1.0: return 1 - lo = 2 ** int(math.floor(math.log(ideal, 2))) + lo = 2 ** math.floor(math.log2(ideal)) hi = lo * 2 # Nearest in log space, which is what "closest to square" means when # the candidates are powers of two. From 7f37494dcf211c12fbd77fb5c7f0aec835ee61f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Harboe?= Date: Sun, 11 Oct 2026 08:53:17 +0200 Subject: [PATCH 3/3] FakeRAM2.0: pick the column mux factor only when the config leaves it out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review: the asap7 backend overwrote column_mux_factor unconditionally, so a config could not choose one. column_mux_factor is now optional in the process config. When it is absent, get_macro_dimensions picks the power of two that squares the array, after banking has folded it; when it is set, it is kept. The asap7 config no longer sets it, so asap7 picks one per memory as before. Every config that sets it, including all the test cfgs, is unchanged. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Øyvind Harboe --- tools/FakeRAM2.0/orfs_asap7/generate.py | 30 +----------------- tools/FakeRAM2.0/test/macro_dim_test.py | 33 ++++++++++++++++++++ tools/FakeRAM2.0/test/process_test.py | 8 +++++ tools/FakeRAM2.0/utils/class_process.py | 41 +++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 32 deletions(-) diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index 75c262b25e..66811e1f24 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -1,5 +1,4 @@ import json -import math import sys from pathlib import Path @@ -21,7 +20,6 @@ "manufacturing_grid_nm": 1, "contacted_poly_pitch_nm": 54, "fin_pitch_nm": 27, - "column_mux_factor": 1, "snap_width_nm": 190, "snap_height_nm": 1400, } @@ -35,29 +33,6 @@ } -def pick_column_mux_factor(rows, bits): - """Fold a tall array into a square one. - - The array is rows/factor cells tall and bits*factor cells wide, so - the factor that squares it is sqrt(rows/bits). Rounded to a power of - two because a column mux selects one of 2^n bit lines, and clamped - at 1 so a wide, shallow memory is left alone. - - Area does not change: this moves cells between the two dimensions - rather than adding or removing any. - """ - if rows <= 0 or bits <= 0: - return 1 - ideal = math.sqrt(float(rows) / float(bits)) - if ideal <= 1.0: - return 1 - lo = 2 ** math.floor(math.log2(ideal)) - hi = lo * 2 - # Nearest in log space, which is what "closest to square" means when - # the candidates are powers of two. - return lo if (ideal / lo) <= (hi / ideal) else hi - - def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): if platform != "asap7": sys.stderr.write(f"FakeRAM2.0 orfs_asap7: unsupported platform {platform}\n") @@ -75,6 +50,7 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): (m for m in memories if m.get("idiomatic")), key=lambda m: m["name"] ) + process = Process(ASAP7_PROCESS_CONFIG) timing_data = TimingData(ASAP7_TIMING_CONFIG) for m in converted: @@ -82,10 +58,6 @@ def run_orfs_asap7(platform: str, out_dir: Path, json_path: Path): bits = m.get("bits", 32) rows = m.get("rows", 128) - process_config = dict(ASAP7_PROCESS_CONFIG) - process_config["column_mux_factor"] = pick_column_mux_factor(rows, bits) - process = Process(process_config) - sram_dict = { "name": name, "width": bits, diff --git a/tools/FakeRAM2.0/test/macro_dim_test.py b/tools/FakeRAM2.0/test/macro_dim_test.py index 849fc04516..37820e936a 100755 --- a/tools/FakeRAM2.0/test/macro_dim_test.py +++ b/tools/FakeRAM2.0/test/macro_dim_test.py @@ -109,6 +109,39 @@ def test_macro_dim_bitcell_override(self): self.assertAlmostEqual(width, exp_width, delta=self._delta) self.assertAlmostEqual(height, exp_height, delta=self._delta) + def _dimensions(self, column_mux_factor, width, depth, banks): + """Macro dimensions, column_mux_factor None leaves the key out""" + + process_data = TestUtils.get_base_process_data().copy() + del process_data["column_mux_factor"] + if column_mux_factor is not None: + process_data["column_mux_factor"] = column_mux_factor + return Process(process_data).get_macro_dimensions(width, depth, banks, 0) + + def test_macro_dim_column_mux_picked(self): + """An absent column_mux_factor folds a deep memory square""" + + # sqrt(512 / 17) = 5.5, the nearest power of two is 4 + picked = self._dimensions(None, 17, 512, 1) + self.assertEqual(picked, self._dimensions(4, 17, 512, 1)) + width, height = picked + self.assertLessEqual(max(width, height) / min(width, height), 8.0) + + def test_macro_dim_column_mux_explicit(self): + """An explicit column_mux_factor is kept, even when it is a sliver""" + + width, height = self._dimensions(1, 17, 512, 1) + self.assertGreater(height / width, 50.0) + + def test_macro_dim_column_mux_after_banks(self): + """The factor is picked for the array banking leaves""" + + # 4 banks make 2048x39 a 512x156 array: sqrt(512 / 156) = 1.8, + # so 2, where sqrt(2048 / 39) = 7.2 would have given 8 + self.assertEqual( + self._dimensions(None, 39, 2048, 4), self._dimensions(2, 39, 2048, 4) + ) + if __name__ == "__main__": unittest.main() diff --git a/tools/FakeRAM2.0/test/process_test.py b/tools/FakeRAM2.0/test/process_test.py index e61757e564..779dc36516 100755 --- a/tools/FakeRAM2.0/test/process_test.py +++ b/tools/FakeRAM2.0/test/process_test.py @@ -114,6 +114,14 @@ def test_process_optional_snap(self): + (process.get_pin_width_um() / 2.0), ) + def test_process_optional_column_mux(self): + """Tests that column_mux_factor is left to be picked per memory""" + + process_data = self._base_data.copy() + del process_data["column_mux_factor"] + process = Process(process_data) + self.assertIsNone(process.get_column_mux_factor()) + def test_process_misaligned_pin_mfg_grid_pitch(self): """ Tests detection that pin pitch is not a multiple of the metal track diff --git a/tools/FakeRAM2.0/utils/class_process.py b/tools/FakeRAM2.0/utils/class_process.py index 487a853e6a..58fd503fb3 100644 --- a/tools/FakeRAM2.0/utils/class_process.py +++ b/tools/FakeRAM2.0/utils/class_process.py @@ -8,6 +8,32 @@ ################################################################################ +import math + + +def pick_column_mux_factor(rows, bits): + """Fold a tall array into a square one. + + The array is rows/factor cells tall and bits*factor cells wide, so + the factor that squares it is sqrt(rows/bits). Rounded to a power of + two because a column mux selects one of 2^n bit lines, and clamped + at 1 so a wide, shallow memory is left alone. + + Area does not change: this moves cells between the two dimensions + rather than adding or removing any. + """ + if rows <= 0 or bits <= 0: + return 1 + ideal = math.sqrt(float(rows) / float(bits)) + if ideal <= 1.0: + return 1 + lo = 2 ** math.floor(math.log2(ideal)) + hi = lo * 2 + # Nearest in log space, which is what "closest to square" means when + # the candidates are powers of two. + return lo if (ideal / lo) <= (hi / ideal) else hi + + class Process: def __init__(self, json_data): """Initialize from json_data imported from config file""" @@ -23,9 +49,14 @@ def __init__(self, json_data): self.contacted_poly_pitch_nm = json_data.get("contacted_poly_pitch_nm", None) self.fin_pitch_nm = json_data.get("fin_pitch_nm", None) self.manufacturing_grid_nm = int(json_data["manufacturing_grid_nm"]) - self.column_mux_factor = int(json_data["column_mux_factor"]) # Optional keys + # Absent means get_macro_dimensions picks one per memory + self.column_mux_factor = ( + int(json_data["column_mux_factor"]) + if "column_mux_factor" in json_data + else None + ) self.snap_width_nm = ( int(json_data["snap_width_nm"]) if "snap_width_nm" in json_data else 1 ) @@ -99,7 +130,6 @@ def get_macro_dimensions(self, width_in_bits, depth, num_banks, additional_heigh and process parameters """ - column_mux_factor = self.column_mux_factor bitcell_width, bitcell_height = self.get_bitcell_dimensions() all_bitcell_height = bitcell_height * depth @@ -111,6 +141,11 @@ def get_macro_dimensions(self, width_in_bits, depth, num_banks, additional_heigh elif num_banks != 1: raise Exception("Unsupported number of banks: {}".format(num_banks)) + column_mux_factor = self.column_mux_factor + if column_mux_factor is None: + column_mux_factor = pick_column_mux_factor( + depth / num_banks, width_in_bits * num_banks + ) all_bitcell_height = all_bitcell_height / column_mux_factor all_bitcell_width = all_bitcell_width * column_mux_factor @@ -202,7 +237,7 @@ def get_y_step(self): return self.y_step def get_column_mux_factor(self): - """Returns the column mux factor""" + """Returns the column mux factor, None if picked per memory""" return self.column_mux_factor def set_calc_dimensions(self, val):