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
2 changes: 1 addition & 1 deletion docs/user/FlowVariables.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ configuration file.
| <a name="SYNTH_INSBUF"></a>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|
| <a name="SYNTH_KEEP_MOCKED_MEMORIES"></a>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|
| <a name="SYNTH_KEEP_MODULES"></a>SYNTH_KEEP_MODULES| Mark modules to keep from getting removed in flattening.| |
| <a name="SYNTH_MEMORY_MAX_BITS"></a>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|
| <a name="SYNTH_MEMORY_MAX_BITS"></a>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|
| <a name="SYNTH_MINIMUM_KEEP_SIZE"></a>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|
| <a name="SYNTH_MOCK_LARGE_MEMORIES"></a>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|
| <a name="SYNTH_NETLIST_FILES"></a>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.| |
Expand Down
7 changes: 0 additions & 7 deletions flow/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ==============================================================================


Expand Down
149 changes: 0 additions & 149 deletions flow/scripts/mem_dump.py

This file was deleted.

41 changes: 41 additions & 0 deletions flow/scripts/memories/max_bits.tcl
Original file line number Diff line number Diff line change
@@ -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]
Comment on lines +10 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If select -list returns any unexpected output (such as warnings, info messages, or empty lines), the path may not contain a / character. In such cases, string first will return -1, which causes string range to extract invalid module and instance names, leading to a hard crash in rtlil::get_param. Adding a defensive check to skip paths without a / ensures robustness against unexpected output.

    set index [string first "/" $path]
    if { $index == -1 } {
      continue
    }
    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
}
35 changes: 10 additions & 25 deletions flow/scripts/synth.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions flow/scripts/synth_preamble.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ proc read_checkpoint { file } {
# canonicalization, one `<module> <area>` 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] } {
Expand Down
2 changes: 1 addition & 1 deletion flow/scripts/variables.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions flow/scripts/variables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
101 changes: 101 additions & 0 deletions flow/test/test_memory_max_bits.py
Original file line number Diff line number Diff line change
@@ -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]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the test fails for an unexpected reason (e.g., Yosys syntax error or file not found), the output will not contain the string "over SYNTH_MEMORY_MAX_BITS=2048". Attempting to split and access index 1 will then raise an IndexError: list index out of range, masking the actual failure output. Adding an assertion to verify the presence of the error message before splitting ensures a clear and helpful test failure message.

Suggested change
error = output.split("over SYNTH_MEMORY_MAX_BITS=2048")[1]
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)

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()