From 29c5ab48f0a82972337f4b17ee90695ff1f8f825 Mon Sep 17 00:00:00 2001 From: Matt Guthaus Date: Tue, 14 Nov 2017 13:24:14 -0800 Subject: [PATCH] Add spice pruning for speed-up. Fix spice search bugs. Add time in stages to openram output. --- compiler/bitcell.py | 2 +- compiler/characterizer/delay.py | 3 +- compiler/characterizer/lib.py | 20 +++- compiler/characterizer/trim_spice.py | 117 +++++++++++++++++++++++ compiler/debug.py | 9 +- compiler/globals.py | 40 ++++---- compiler/nand_2.py | 2 +- compiler/nor_2.py | 2 +- compiler/openram.py | 67 ++++++++----- compiler/options.py | 6 +- compiler/pinv.py | 2 +- compiler/sense_amp.py | 2 +- compiler/sram.py | 6 +- compiler/tests/23_lib_sram_prune_test.py | 62 ++++++++++++ compiler/tests/23_lib_sram_test.py | 2 + compiler/tri_gate.py | 2 +- compiler/write_driver.py | 2 +- 17 files changed, 280 insertions(+), 66 deletions(-) create mode 100644 compiler/characterizer/trim_spice.py create mode 100644 compiler/tests/23_lib_sram_prune_test.py diff --git a/compiler/bitcell.py b/compiler/bitcell.py index a79af868..90269692 100644 --- a/compiler/bitcell.py +++ b/compiler/bitcell.py @@ -17,7 +17,7 @@ class bitcell(design.design): def __init__(self): design.design.__init__(self, "cell_6t") - debug.info(2, "Create bitcell object") + debug.info(2, "Create bitcell") self.width = bitcell.width self.height = bitcell.height diff --git a/compiler/characterizer/delay.py b/compiler/characterizer/delay.py index 2d39a08c..f519809e 100644 --- a/compiler/characterizer/delay.py +++ b/compiler/characterizer/delay.py @@ -22,7 +22,7 @@ class delay(): self.word_size = sram.word_size self.addr_size = sram.addr_size self.sram_sp_file = spfile - + self.vdd = tech.spice["supply_voltage"] self.gnd = tech.spice["gnd_voltage"] @@ -326,6 +326,7 @@ class delay(): self.probe_address = probe_address self.probe_data = probe_data + def analyze(self,probe_address, probe_data, slews, loads): """main function to calculate the min period for a low_to_high transistion and a high_to_low transistion returns a dictionary diff --git a/compiler/characterizer/lib.py b/compiler/characterizer/lib.py index 448fd4a0..7cece91a 100644 --- a/compiler/characterizer/lib.py +++ b/compiler/characterizer/lib.py @@ -10,6 +10,7 @@ import delay import charutils as ch import tech import numpy as np +from trim_spice import trim_spice OPTS = globals.get_opts() @@ -18,13 +19,25 @@ class lib: def __init__(self, libname, sram, spfile, use_model=OPTS.analytical_delay): self.sram = sram - self.spfile = spfile + self.sp_file = spfile self.use_model = use_model self.name = sram.name self.num_words = sram.num_words self.word_size = sram.word_size self.addr_size = sram.addr_size + # Set up to trim the netlist here if that is enabled + if OPTS.trim_netlist: + self.sim_sp_file = "{}reduced.sp".format(OPTS.openram_temp) + self.trimsp=trim_spice(self.sp_file, self.sim_sp_file) + self.trimsp.set_configuration(self.sram.num_banks, + self.sram.num_rows, + self.sram.num_cols, + self.sram.word_size) + else: + # Else, use the non-reduced netlist file for simulation + self.sim_sp_file = self.sp_file + # These are the parameters to determine the table sizes #self.load_scales = np.array([0.1, 0.25, 0.5, 1, 2, 4, 8]) self.load_scales = np.array([0.25, 1, 8]) @@ -378,12 +391,15 @@ class lib: try: self.d except AttributeError: - self.d = delay.delay(self.sram, self.spfile) + self.d = delay.delay(self.sram, self.sim_sp_file) if self.use_model: self.delay = self.d.analytical_model(self.sram,self.slews,self.loads) else: probe_address = "1" * self.addr_size probe_data = self.word_size - 1 + # We must trim based on a specific address and data bit + if OPTS.trim_netlist: + self.trimsp.trim(probe_address,probe_data) self.delay = self.d.analyze(probe_address, probe_data, self.slews, self.loads) def compute_setup_hold(self): diff --git a/compiler/characterizer/trim_spice.py b/compiler/characterizer/trim_spice.py new file mode 100644 index 00000000..a5025d60 --- /dev/null +++ b/compiler/characterizer/trim_spice.py @@ -0,0 +1,117 @@ +import debug +from math import log + +class trim_spice(): + """ + A utility to trim redundant parts of an SRAM spice netlist. + Input is an SRAM spice file. Output is an equivalent netlist + that works for a single address and range of data bits. + """ + + def __init__(self, spfile, reduced_spfile): + self.sp_file = spfile + self.reduced_spfile = reduced_spfile + + debug.info(1,"Trimming non-critical cells to speed-up characterization: {}.".format(reduced_spfile)) + + # Load the file into a buffer for performance + sp = open(self.sp_file, "r") + self.spice = sp.readlines() + for i in range(len(self.spice)): + self.spice[i] = self.spice[i].rstrip(" \n") + + + self.sp_buffer = self.spice + + def set_configuration(self, banks, rows, columns, word_size): + """ Set the configuration of SRAM sizes that we are simulating. + Need the: number of banks, number of rows in each bank, number of + columns in each bank, and data word size.""" + self.num_banks = banks + self.num_rows = rows + self.num_columns = columns + self.word_size = word_size + + self.words_per_row = self.num_columns / self.word_size + self.row_addr_size = int(log(self.num_rows, 2)) + self.col_addr_size = int(log(self.words_per_row, 2)) + self.bank_addr_size = self.col_addr_size + self.row_addr_size + self.addr_size = self.bank_addr_size + int(log(self.num_banks, 2)) + + + def trim(self, address, data_bit): + """ Reduce the spice netlist but KEEP the given bits at the + address (and things that will add capacitive load!)""" + + # Always start fresh if we do multiple reductions + self.sp_buffer = self.spice + + # Find the row and column indices for the removals + # Convert address froms tring to int + address = int(address,2) + array_row = address >> self.col_addr_size + # Which word in the array (0 if only one word) + if self.col_addr_size>0: + lower_mask = int(self.col_addr_size-1) + lower_address = address & lower_mask + else: + lower_address=0 + # Which bit in the array + array_bit = lower_address*self.word_size + data_bit + + # 1. Keep cells in the bitcell array based on WL and BL + wl_name = "wl[{}]".format(array_row) + bl_name = "bl[{}]".format(array_bit) + self.remove_insts("bitcell_array",[wl_name,bl_name]) + + # 2. Keep sense amps basd on BL + self.remove_insts("sense_amp_array",[bl_name]) + + # 3. Keep column muxes basd on BL + self.remove_insts("column_mux_array",[bl_name]) + + # 4. Keep write driver based on DATA + data_name = "data[{}]".format(data_bit) + self.remove_insts("write_driver_array",[data_name]) + + # 5. Keep wordline driver based on WL + # Need to keep the gater too + #self.remove_insts("wordline_driver",wl_name) + + # 6. Keep precharges based on BL + self.remove_insts("precharge_array",[bl_name]) + + # Everything else isn't worth removing. :) + + # Finally, write out the buffer as the new reduced file + sp = open(self.reduced_spfile, "w") + sp.write("\n".join(self.sp_buffer)) + + + def remove_insts(self, subckt_name, keep_inst_list): + """This will remove all of the instances in the list from the named + subckt that DO NOT contain a term in the list. It just does a + match of the line with a term so you can search for a single + net connection, the instance name, anything.. + """ + start_name = ".SUBCKT {}".format(subckt_name) + end_name = ".ENDS {}".format(subckt_name) + + in_subckt=False + new_buffer=[] + for line in self.sp_buffer: + if start_name in line: + new_buffer.append(line) + in_subckt=True + elif end_name in line: + new_buffer.append(line) + in_subckt=False + elif in_subckt: + for k in keep_inst_list: + if k in line: + new_buffer.append(line) + break + else: + new_buffer.append(line) + + self.sp_buffer = new_buffer diff --git a/compiler/debug.py b/compiler/debug.py index 5ab46d0b..f36a8df2 100644 --- a/compiler/debug.py +++ b/compiler/debug.py @@ -34,6 +34,9 @@ def info(lev, str): if (OPTS.debug_level >= lev): frm = inspect.stack()[1] mod = inspect.getmodule(frm[0]) - print("[{0}]: {1}".format(frm[0].f_code.co_name,str)) - # This sometimes gets a NoneType mod... - # print "[" , mod.__name__ , "]: ", str + #classname = frm.f_globals['__name__'] + if mod.__name__ == None: + class_name="" + else: + class_name=mod.__name__ + print("[{0}/{1}]: {2}".format(class_name,frm[0].f_code.co_name,str)) diff --git a/compiler/globals.py b/compiler/globals.py index 4ad650f8..04150d05 100644 --- a/compiler/globals.py +++ b/compiler/globals.py @@ -54,8 +54,8 @@ def parse_args(): help="Technology name"), optparse.make_option("-s", "--spiceversion", dest="spice_version", help="Spice simulator name"), - optparse.make_option("-r", "--reduce_netlist", action="store_true", dest="remove_noncritical", - help="Remove noncritical memory cells during characterization"), + optparse.make_option("-r", "--remove_netlist_trimming", action="store_false", dest="trim_netlist", + help="Disable removal of noncritical memory cells during characterization"), optparse.make_option("-a", "--analytical", action="store_true", dest="analytical_delay", help="Use analytical models to calculate delays (default)"), optparse.make_option("-c", "--characterize", action="store_false", dest="analytical_delay", @@ -163,7 +163,7 @@ def set_calibre(): # everything. if not OPTS.check_lvsdrc: # over-ride the check LVS/DRC option - debug.info(0,"Over-riding LVS/DRC. Not performing inline LVS/DRC.") + debug.info(0,"Over-riding LVS/DRC. Not performing LVS/DRC.") else: # see if calibre is in the path (extend to other tools later) for path in os.environ["PATH"].split(os.pathsep): @@ -175,7 +175,7 @@ def set_calibre(): break else: # otherwise, give warning and procede - debug.warning("Calibre not found. Not performing inline LVS/DRC.") + debug.warning("Calibre not found. Not performing LVS/DRC.") OPTS.check_lvsdrc = False def end_openram(): @@ -234,13 +234,12 @@ def setup_paths(): -def find_spice(spice_exe): +def find_spice(check_exe): # Check if the preferred spice option exists in the path for path in os.environ["PATH"].split(os.pathsep): - spice_exe = os.path.join(path, OPTS.spice_version) + spice_exe = os.path.join(path, check_exe) # if it is found, then break and use first version if is_exe(spice_exe): - debug.info(1, "Using spice: " + spice_exe) OPTS.spice_exe = spice_exe return True return False @@ -253,21 +252,18 @@ def set_spice(): debug.info(1,"Using analytical delay models (no characterization)") return else: - debug.info(1,"Performing simulation-based characterization (may be slow!)") - - OPTS.spice_exe = "" - - spice_preferences = ["xa", "hspice", "ngspice", "ngspice.exe"] - - if OPTS.spice_version != "": - if not find_spice(OPTS.spice_version): - debug.error("{0} not found. Unable to perform characterization.".format(OPTS.spice_version),1) - else: - for spice_exe in spice_preferences: - if find_spice(spice_exe): - break - else: - debug.warning("Unable to find spice {0}, trying another.".format(spice_exe)) + spice_preferences = ["xa", "hspice", "ngspice", "ngspice.exe"] + if OPTS.spice_version != "": + if not find_spice(OPTS.spice_version): + debug.error("{0} not found. Unable to perform characterization.".format(OPTS.spice_version),1) + else: + for spice_name in spice_preferences: + if find_spice(spice_name): + OPTS.spice_version=spice_name + debug.info(1, "Using spice: " + OPTS.spice_exe) + break + else: + debug.info(1, "Could not find {}, trying next spice simulator. ".format(spice_name)) # set the input dir for spice files if using ngspice if OPTS.spice_version == "ngspice": diff --git a/compiler/nand_2.py b/compiler/nand_2.py index 07b764d7..a2590907 100644 --- a/compiler/nand_2.py +++ b/compiler/nand_2.py @@ -36,7 +36,7 @@ class nand_2(design.design): self.add_pins() self.create_layout() - self.DRC_LVS() + #self.DRC_LVS() def add_pins(self): """ Add pins """ diff --git a/compiler/nor_2.py b/compiler/nor_2.py index 82812e9c..a1c84637 100644 --- a/compiler/nor_2.py +++ b/compiler/nor_2.py @@ -34,7 +34,7 @@ class nor_2(design.design): self.add_pins() self.create_layout() - self.DRC_LVS() + #self.DRC_LVS() def add_pins(self): self.add_pin_list(["A", "B", "Z", "vdd", "gnd"]) diff --git a/compiler/openram.py b/compiler/openram.py index 23ece9c0..d2b103fc 100755 --- a/compiler/openram.py +++ b/compiler/openram.py @@ -26,6 +26,14 @@ global OPTS (OPTS, args) = globals.parse_args() +def print_time(name, now_time, last_time=None): + if last_time: + time = round((now_time-last_time).total_seconds(),1) + else: + time = now_time + print("** {0}: {1} seconds".format(name,time)) + return now_time + # These depend on arguments, so don't load them until now. import debug @@ -59,7 +67,7 @@ if (OPTS.output_name == ""): num_banks, OPTS.tech_name) -debug.info(1, "Output files are " + OPTS.output_name + ".(sp|gds|v|lib|lef)") +print("Output files are " + OPTS.output_name + ".(sp|gds|v|lib|lef)") print("Technology: {0}".format(OPTS.tech_name)) print("Word size: {0}\nWords: {1}\nBanks: {2}".format(word_size,num_words,num_banks)) @@ -68,20 +76,25 @@ print("Word size: {0}\nWords: {1}\nBanks: {2}".format(word_size,num_words,num_ba if OPTS.analytical_delay: print("Using analytical delay models (no characterization)") else: - print("Performing simulation-based characterization (may be slow!)") + print("Performing simulation-based characterization with {}".format(OPTS.spice_version)) + +if OPTS.trim_netlist: + print("Trimming netlist to speed up characterization (sacrificing some accuracy).") # only start importing modules after we have the config file import calibre import sram -print("Start: {0}".format(datetime.datetime.now())) +start_time = datetime.datetime.now() +last_time = start_time +print_time("Start",datetime.datetime.now()) # import SRAM test generation s = sram.sram(word_size=word_size, num_words=num_words, num_banks=num_banks, name=OPTS.output_name) - +last_time=print_time("SRAM creation", datetime.datetime.now(), last_time) # Measure design area # Not working? #cell_size = s.gds.measureSize(s.name) @@ -92,36 +105,40 @@ s = sram.sram(word_size=word_size, spname = OPTS.output_path + s.name + ".sp" print("SP: Writing to {0}".format(spname)) s.sp_write(spname) +last_time=print_time("Spice writing", datetime.datetime.now(), last_time) -gdsname = OPTS.output_path + s.name + ".gds" -print("GDS: Writing to {0}".format(gdsname)) -s.gds_write(gdsname) - -# Run Characterizer on the design +# Output the extracted design sram_file = spname if OPTS.use_pex: sram_file = OPTS.output_path + "temp_pex.sp" calibre.run_pex(s.name, gdsname, spname, output=sram_file) - -# geenrate verilog -import verilog -vname = OPTS.output_path + s.name + ".v" -print("Verilog: Writing to {0}".format(vname)) -verilog.verilog(vname,s) - -# generate LEF -import lef -lefname = OPTS.output_path + s.name + ".lef" -print("LEF: Writing to {0}".format(lefname)) -lef.lef(gdsname,lefname,s) - -# generate lib +# Characterize the design import lib libname = OPTS.output_path + s.name + ".lib" print("LIB: Writing to {0}".format(libname)) lib.lib(libname,s,sram_file) +last_time=print_time("Characterization", datetime.datetime.now(), last_time) + +# Write the layout +gdsname = OPTS.output_path + s.name + ".gds" +print("GDS: Writing to {0}".format(gdsname)) +s.gds_write(gdsname) +last_time=print_time("GDS", datetime.datetime.now(), last_time) + +# Create a LEF physical model +import lef +lefname = OPTS.output_path + s.name + ".lef" +print("LEF: Writing to {0}".format(lefname)) +lef.lef(gdsname,lefname,s) +last_time=print_time("LEF writing", datetime.datetime.now(), last_time) + +# Write a verilog model +import verilog +vname = OPTS.output_path + s.name + ".v" +print("Verilog: Writing to {0}".format(vname)) +verilog.verilog(vname,s) +last_time=print_time("Verilog writing", datetime.datetime.now(), last_time) globals.end_openram() - -print("End: {0}".format(datetime.datetime.now())) +print_time("End",datetime.datetime.now(), start_time) diff --git a/compiler/options.py b/compiler/options.py index da7cf6df..e1e723e5 100644 --- a/compiler/options.py +++ b/compiler/options.py @@ -18,8 +18,8 @@ class options(optparse.Values): debug_level = 0 # This determines whether LVS and DRC is checked for each submodule. check_lvsdrc = True - # Variable to select the variant of spice (hspice or ngspice right now) - spice_version = "hspice" + # Variable to select the variant of spice + spice_version = "" # Should we print out the banner at startup print_banner = True # The Calibre executable being used which is derived from the user PATH. @@ -29,7 +29,7 @@ class options(optparse.Values): # Run with extracted parasitics use_pex = False # Remove noncritical memory cells for characterization speed-up - remove_noncritical = False + trim_netlist = True # Define the output file paths output_path = "" # Define the output file base name diff --git a/compiler/pinv.py b/compiler/pinv.py index 076042c5..674961ea 100644 --- a/compiler/pinv.py +++ b/compiler/pinv.py @@ -32,7 +32,7 @@ class pinv(design.design): self.add_pins() self.create_layout() - self.DRC_LVS() + #self.DRC_LVS() def add_pins(self): """Adds pins for spice netlist processing""" diff --git a/compiler/sense_amp.py b/compiler/sense_amp.py index 5155f05e..fb6ddb6f 100644 --- a/compiler/sense_amp.py +++ b/compiler/sense_amp.py @@ -17,7 +17,7 @@ class sense_amp(design.design): def __init__(self, name): design.design.__init__(self, name) - debug.info(2, "Create Sense Amp object") + debug.info(2, "Create sense_amp") self.width = sense_amp.width self.height = sense_amp.height diff --git a/compiler/sram.py b/compiler/sram.py index 6fe33811..3b236c1f 100644 --- a/compiler/sram.py +++ b/compiler/sram.py @@ -1,4 +1,3 @@ -import math import sys from tech import drc, spice import debug @@ -11,6 +10,7 @@ import getpass from vector import vector from globals import OPTS + class sram(design.design): """ Dynamically generated SRAM by connecting banks to control logic. The @@ -88,7 +88,7 @@ class sram(design.design): # Compute the area of the bitcells and estimate a square bank (excluding auxiliary circuitry) self.bank_area = self.bitcell.width*self.bitcell.height*self.num_bits_per_bank - self.bank_side_length = math.sqrt(self.bank_area) + self.bank_side_length = sqrt(self.bank_area) # Estimate the words per row given the height of the bitcell and the square side length self.tentative_num_cols = int(self.bank_side_length/self.bitcell.width) @@ -106,7 +106,7 @@ class sram(design.design): self.row_addr_size = int(log(self.num_rows, 2)) self.col_addr_size = int(log(self.words_per_row, 2)) self.bank_addr_size = self.col_addr_size + self.row_addr_size - self.addr_size = self.bank_addr_size + int(math.log(self.num_banks, 2)) + self.addr_size = self.bank_addr_size + int(log(self.num_banks, 2)) def estimate_words_per_row(self,tentative_num_cols, word_size): diff --git a/compiler/tests/23_lib_sram_prune_test.py b/compiler/tests/23_lib_sram_prune_test.py new file mode 100644 index 00000000..ab7d9cf1 --- /dev/null +++ b/compiler/tests/23_lib_sram_prune_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python2.7 +""" +Check the .lib file for an SRAM with pruning +""" + +import unittest +from testutils import header,isapproxdiff +import sys,os +sys.path.append(os.path.join(sys.path[0],"..")) +import globals +import debug +import calibre + +OPTS = globals.get_opts() + + +class lib_test(unittest.TestCase): + + def runTest(self): + OPTS.analytical_delay = False + globals.init_openram("config_20_{0}".format(OPTS.tech_name)) + # we will manually run lvs/drc + OPTS.check_lvsdrc = False + + import sram + import lib + + debug.info(1, "Testing timing for sample 2 bit, 16 words SRAM with 1 bank") + s = sram.sram(word_size=2, + num_words=OPTS.config.num_words, + num_banks=OPTS.config.num_banks, + name="sram_2_16_1_{0}".format(OPTS.tech_name)) + OPTS.check_lvsdrc = True + + tempspice = OPTS.openram_temp + "temp.sp" + s.sp_write(tempspice) + + filename = s.name + ".lib" + libname = OPTS.openram_temp + filename + lib.lib(libname=libname,sram=s,spfile=tempspice,use_model=False) + + # let's diff the result with a golden model + golden = "{0}/golden/{1}".format(os.path.dirname(os.path.realpath(__file__)),filename) + # From an experiment, a 15% difference between between pruned and not was found. + self.assertEqual(isapproxdiff(libname,golden,0.15),True) + + os.system("rm {0}".format(libname)) + OPTS.analytical_delay = True + globals.end_openram() + +# instantiate a copdsay of the class to actually run the test +if __name__ == "__main__": + (OPTS, args) = globals.parse_args() + del sys.argv[1:] + header(__file__, OPTS.tech_name) + unittest.main() + + + + + + diff --git a/compiler/tests/23_lib_sram_test.py b/compiler/tests/23_lib_sram_test.py index 7c1e0209..15bcc1ba 100644 --- a/compiler/tests/23_lib_sram_test.py +++ b/compiler/tests/23_lib_sram_test.py @@ -18,6 +18,7 @@ class lib_test(unittest.TestCase): def runTest(self): OPTS.analytical_delay = False + OPTS.trim_netlist = False globals.init_openram("config_20_{0}".format(OPTS.tech_name)) # we will manually run lvs/drc OPTS.check_lvsdrc = False @@ -46,6 +47,7 @@ class lib_test(unittest.TestCase): os.system("rm {0}".format(libname)) OPTS.analytical_delay = True + OPTS.trim_netlist = True globals.end_openram() # instantiate a copdsay of the class to actually run the test diff --git a/compiler/tri_gate.py b/compiler/tri_gate.py index e6debc4a..7351c575 100644 --- a/compiler/tri_gate.py +++ b/compiler/tri_gate.py @@ -21,7 +21,7 @@ class tri_gate(design.design): name = "tri{0}".format(tri_gate.unique_id) tri_gate.unique_id += 1 design.design.__init__(self, name) - debug.info(2, "Create tri_gate object") + debug.info(2, "Create tri_gate") self.width = tri_gate.width self.height = tri_gate.height diff --git a/compiler/write_driver.py b/compiler/write_driver.py index 1df03a50..57ba39c3 100644 --- a/compiler/write_driver.py +++ b/compiler/write_driver.py @@ -17,7 +17,7 @@ class write_driver(design.design): def __init__(self, name): design.design.__init__(self, name) - debug.info(2, "Create write_driver object") + debug.info(2, "Create write_driver") self.width = write_driver.width self.height = write_driver.height