diff --git a/docs/user/FlowVariables.md b/docs/user/FlowVariables.md index 8d32a9d6ad..7b895223a4 100644 --- a/docs/user/FlowVariables.md +++ b/docs/user/FlowVariables.md @@ -337,6 +337,7 @@ configuration file. | VIA_IN_PIN_MAX_LAYER| Passed as -via_in_pin_top_layer to pin_access and detailed_route.| | | VIA_IN_PIN_MIN_LAYER| Passed as -via_in_pin_bottom_layer to pin_access and detailed_route.| | | WIRE_LOAD_MODEL| Name of a Liberty wire_load group applied with set_wire_load_model whenever open.tcl reads timing. OpenSTA applies a wire-load model only to nets without parasitics, so it gives wire delay to timing read before placement and leaves placed and routed timing unaffected. An unknown name is an error. Empty, the default, applies none. A platform whose libraries define no wire_load group can supply one in a Liberty file listed in ADDITIONAL_LIBS.| | +| WRITE_ABSTRACT_LEF_ARGS| Arguments to write_abstract_lef when it writes a block's abstract. The default obstructs every layer the block uses over its whole outline. Empty, the abstract obstructs only the shapes the block occupies, so a block whose outline holds a notch, where other macros sit, leaves the notch free for them.| -bloat_occupied_layers| | WRITE_ODB_AND_SDC_EACH_STAGE| Save out .sdc and .odb file after each stage, useful to disable when using a single OpenROAD instance to run all stages of the flow.| 1| | YOSYS_FLAGS| Flags to pass to yosys.| -v 3| ## synth variables @@ -646,6 +647,7 @@ configuration file. ## generate_abstract variables - [ABSTRACT_SOURCE](#ABSTRACT_SOURCE) +- [WRITE_ABSTRACT_LEF_ARGS](#WRITE_ABSTRACT_LEF_ARGS) ## Uncategorized variables diff --git a/flow/designs/asap7/tpu/README.md b/flow/designs/asap7/tpu/README.md new file mode 100644 index 0000000000..c4cd54d9a2 --- /dev/null +++ b/flow/designs/asap7/tpu/README.md @@ -0,0 +1,162 @@ +# tpu: abutted tiles, the parent a macro too + +A systolic array of 8 x 8 processing elements (`src/tpu`), built as 17 +macros and no standard cell at the top: + +- **16 identical tiles** (`tpu_block`) of 2 x 2 processing elements, + carved out of the flat design during synthesis + (`SYNTH_VERILOG_SURGERY`, `surgery.py` running `flow/util/carve.py`): + the design's own cells regrouped, the RTL untouched. +- **the parent as a macro** (`tpu_rest`): everything else of tpu, the + skew and deskew registers, the clock root, the weights in, is one + C-shaped macro whose notch holds the tiles (`flow/util/wrap_parent.py`, + `tpu_io.tcl`). Its abstract obstructs only what it occupies + (`WRITE_ABSTRACT_LEF_ARGS` empty), so the notch stays free. +- **abutment**: every tile meets its neighbours and the parent pin on + pin. Every pin is placed by rule, from the constants in `tpu_io.tcl`, + before anything is built, so the tile and the parent build side by + side; the top level places the macros (`place.tcl`) and is placement + and wires only. + +`make DESIGN_CONFIG=./designs/asap7/tpu/config.mk FLOW_VARIANT=flat` +builds tpu flat from the same RTL, the reference. + +![placement density across the parent and its 16 tiles](floorplan.svg) + +Placement density at global route, each tile's own cells drawn in place: +the tiles are 80 % full; the parent's three sides around them hold its +skew and deskew registers sparsely. + +## Why + +QoR is the best result within a build budget. In a hierarchical flow the +parent places, repairs and routes around its macros' abstracts: it +waits for every macro, and every macro change re-places it. Here the +parent is a macro like the tiles, built against the same pin plan, so +every macro builds side by side and an RTL edit in a tile rebuilds that +tile alone. Measured at global route, with the reg2reg minimum period +stitched across every macro (the study: +https://github.com/The-OpenROAD-Project/bazel-orfs/pull/1396): + +| | tiles in a C | flat | +|---|---|---| +| reg2reg minimum period, mean of three placement seeds | 672.6 ps (92 %) | 729.3 ps | +| a fresh build, critical path | 76 s | 436 s | +| an RTL edit in a processing element | 99 s | 487 s | +| die | 1.39 x | 1 x | + +![best period against the CPU spent searching](period_vs_cpu.svg) + +The carved design's first sensible floorplan beats everything flat +reaches with five times the CPU; tuning either one moves its period by +less than a placement seed does. + +![an RTL edit's round trip](round_trip.svg) + +## The rules of the game + +tpu is mesochronous with its surroundings: the same clock frequency, a +fixed but unknown phase. Its clock reaches each tile through the tiles +before it, so the phase at its registers is not the clock root's; data +crosses its boundary through FIFOs, which are assumed to be outside tpu. +So only reg2reg paths are judged, and the boundary is a `set_max_delay` +budget for repair, with no input or output delay (`constraint.sdc`, +`flow/platforms/asap7/constraints.sdc`). tpu has every pin on its bottom +edge, as a macro abutting its parent would; inside, each macro has its +pins on the sides its neighbours are on. Every macro routes on M2-M8; +M9 brings power from above. + +## How it is clocked + +The clock, reset and load enable enter each row of tiles from the +parent at its west end and run east through the tiles with the +activations, one buffer per tile; rows enter together. Each tile sees +the clock about 77 ps after its west neighbour, so data flowing east is +captured later than it is launched, and data flowing south sees no +skew. Only neighbours' skew matters, as Fisher and Kung showed for +pipelined clocks in systolic arrays: no clock tree is balanced across +the array. The bottom row passes its clock down to the deskew registers +in the parent. + +![how the clock runs through the tiles](clocking.svg) + +![clock arrival at each tile's registers, measured](clock_skew.svg) + +## The worst path at the top level + +The worst slack at the top level is not reg2reg but in2reg: tpu's +`a_valid` and `a_data` into the top-left tile. + +``` + +----+-------------------------------------------+ + | | parent, top side | + | +-------------+-------------+-------- ... | + | | u_b0_0 | u_b0_1 | | + | | PE(0,0) | | | row 0 + | +----> a_in | | | + | ^ +-------------+-------------+-------- ... | + | | | the other 14 tiles | + | | +-------------------------------------------+ + | | | parent, bottom side | + +--^-+-------------------------------------------+ + a_valid, a_data[7:0]: tpu's pins, bottom edge + + 0.0 ps pin a_valid + 100.8 ps u_b0_0/a_in: up the parent's west side, through the mux + a_valid ? a_data : 0; row 0 has no skew register + 670.8 ps PE(0,0)'s multiply-add into its partial-sum register + 300.0 ps the in2reg budget: slack -370.8 ps +``` + +The RTL delays row i of the activations i cycles so partial sums meet +down each column: row 0 gets none, and the minimum latency with it. The +zeros on idle cycles are injected where the rows enter, and the PE +multiplies its west input as it arrives. So from tpu's inputs to its +first partial sum is one multiply-add, half of a reg2reg path whose +other half is a register outside tpu: no in2reg budget under one +multiply-add holds it, wherever the pins are. + +The mesochronous context makes it a reg2reg path. tpu's interface is +valid in (`a_valid`, `w_valid`) and valid out (`c_valid`), with no +back-pressure, and it takes a bubble anywhere. A FIFO at tpu's level, +on tpu's one clock, its pin side on a short branch of the clock tree +and its logic side on the macros' deep one, turns the path into FIFO +register to partial sum, judged with the period; the pins see only the +short branch. + +## Files + +| file | what | +|---|---| +| `config.mk` | tpu: the top level of macros (base), or flat (`FLOW_VARIANT=flat`) | +| `tpu_block/config.mk` | the tile, with the settings a placement and clock-tree search found best | +| `tpu_rest/config.mk` | the parent macro | +| `surgery.py` | the carve and the parent's module, during synthesis | +| `tpu_io.tcl` | the pins of the tile and of the parent, and the parent's notch | +| `place.tcl`, `io_top.tcl`, `pdn_top.tcl`, `tapcell_none.tcl`, `dpl_none.tcl` | the top level of macros only | +| `pdn_macro.tcl` | every macro's power: rails, M5 straps, M8 straps the top level's M9 lands on | +| `pins_bottom.tcl` | flat's pins, all on its bottom edge | +| `*.svg` | the figures above, from the study | + +## Limits + +- **Hold fails across macros.** Timed with every macro's own cells and + wires, 143 endpoints from a tile into the parent fail hold, worst + -417.4 ps, and 75 inside the parent, worst -130.0 ps. The parent's + deskew registers take their clock from the bottom row of tiles through + the parent's own tree, about 250 ps after the tile registers that feed + them, and the parent mixes those clocks with its own; no flow repairs + hold across macros, each sees only its side. The clock taken out of + the macros and balanced at a top level of its own closes hold, at a + 13 % longer period so far: the chain's skew was free time for every + path flowing east (the study has the numbers). The periods above are + setup only; with the clock paths derated 5 % either way and common path + pessimism removed, the carved period is 668.3 ps. + +- **Area**: 1.39 x flat's die. The tiles are 80 % full; the parent's + three sides around them hold its 676 um2 of registers at 5.6 %. +- **The parent's repair does not see inside the tiles**: it times their + pins as ports with budgets. tpu's worst path starts and ends inside one + tile, so this costs nothing here. +- **Power at the top level reaches the parent only**: pdngen takes the + parent's whole outline as occupied and draws nothing over the tiles. diff --git a/flow/designs/asap7/tpu/clock_skew.svg b/flow/designs/asap7/tpu/clock_skew.svg new file mode 100644 index 0000000000..792b1703ae --- /dev/null +++ b/flow/designs/asap7/tpu/clock_skew.svg @@ -0,0 +1,86 @@ + + +tpu: clock arrival at each tile's registers, ps +stitched at global route; the clock enters each row from the west and runs east through the tiles + +148 + +225 + +302 + +379 + +148 + +225 + +302 + +379 + +154 + +231 + +308 + +385 + +150 + +227 + +304 + +381 + ++77 → + ++0 ↓ + ++77 → + ++0 ↓ + ++77 → + ++0 ↓ + ++0 ↓ + ++77 → + ++6 ↓ + ++77 → + ++6 ↓ + ++77 → + ++6 ↓ + ++6 ↓ + ++77 → + +-4 ↓ + ++77 → + +-4 ↓ + ++77 → + +-4 ↓ + +-4 ↓ + ++77 → + ++77 → + ++77 → +step to the east neighbour: the skew activations flowing east see; to the south: weights and sums + diff --git a/flow/designs/asap7/tpu/clocking.svg b/flow/designs/asap7/tpu/clocking.svg new file mode 100644 index 0000000000..c6c1715198 --- /dev/null +++ b/flow/designs/asap7/tpu/clocking.svg @@ -0,0 +1,116 @@ + + + + + + + tpu: macros only, abutted; every pin on the bottom edge + + + parent macro + (C-shaped) + skew registers, + clock root + parent macro: deskew registers, clocked from the tile above them + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + every macro touches its neighbours: no channel, no gap + + parent macro: the top row's weights in, its partial sums tied to zero + + tile macrotile macrotile macro… + + + + + shared edge,parent to tile:pin meets pin + + + clock, reset, load: one buffer per tile + data: activations east, weights and sums south + + + diff --git a/flow/designs/asap7/tpu/config.mk b/flow/designs/asap7/tpu/config.mk index c349601544..bfb49fc3e3 100644 --- a/flow/designs/asap7/tpu/config.mk +++ b/flow/designs/asap7/tpu/config.mk @@ -4,18 +4,41 @@ export DESIGN_NAME = tpu export VERILOG_FILES = $(sort $(wildcard $(DESIGN_HOME)/src/tpu/*.sv)) export SDC_FILE = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/constraint.sdc -export ASAP7_USE_VT = RVT LVT SLVT - -# Synthesis export SYNTH_HDL_FRONTEND = slang export LEC_CHECK = 0 -# Floorplan -export CORE_UTILIZATION = 60 -export CORE_ASPECT_RATIO = 1 -export CORE_MARGIN = 2 - -export TNS_END_PERCENT = 100 +# Every macro, and tpu itself, routes on M2-M8; M9 brings power from +# the level above. tpu's pins are all on its bottom edge: it is a macro +# that abuts its parent. +export MIN_ROUTING_LAYER = M2 +export MAX_ROUTING_LAYER = M8 +export IO_PLACER_H = M2 M4 +export IO_PLACER_V = M3 M5 -# Routing -export MAX_ROUTING_LAYER = M9 +ifeq ($(FLOW_VARIANT),flat) +# The reference: tpu flat, from its RTL, every pin on its bottom edge. +export CORE_UTILIZATION = 60 +export PLACE_DENSITY = 0.65 +export IO_CONSTRAINTS = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/pins_bottom.tcl +export PDN_TCL = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/pdn_macro.tcl +else +# 16 tiles of 2 x 2 processing elements, and the rest of tpu built as a +# C-shaped macro around them (surgery.py carves the netlist): the top +# level is those 17 macros, abutted, and wires. +export BLOCKS = tpu_block tpu_rest +export SYNTH_VERILOG_SURGERY = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/surgery.py \ + $(FLOW_HOME)/util/carve.py $(FLOW_HOME)/util/wrap_parent.py +# the C's die: the tiles and the C cover it +export DIE_AREA = 0 0 103.68 120.96 +export CORE_AREA = 0 0 103.68 120.96 +# no standard cell at the top: its ports are the C's pins +export DONT_BUFFER_PORTS = 1 +export IO_CONSTRAINTS = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/io_top.tcl +export MACRO_PLACEMENT_TCL = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/place.tcl +export PDN_TCL = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/pdn_top.tcl +export TAPCELL_TCL = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/tapcell_none.tcl +# detail_place, cts and grt each legalize: nothing at the top is movable +export PRE_DETAIL_PLACE_TCL = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/dpl_none.tcl +export PRE_CTS_TCL = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/dpl_none.tcl +export PRE_GLOBAL_ROUTE_TCL = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/dpl_none.tcl +endif diff --git a/flow/designs/asap7/tpu/constraint.sdc b/flow/designs/asap7/tpu/constraint.sdc index a447a41f8c..8413c5cb2f 100644 --- a/flow/designs/asap7/tpu/constraint.sdc +++ b/flow/designs/asap7/tpu/constraint.sdc @@ -1,10 +1,12 @@ -set_units -time ps -capacitance fF -resistance kOhm -voltage V -current mA -power pW -current_design tpu - -set clk_period 200 -set io_delay [expr 0.20 * $clk_period] -create_clock -name core_clock -period $clk_period [get_ports clk] -create_clock -name vclk_core -period $clk_period -set_max_fanout 16 [current_design] -set_input_delay $io_delay -clock vclk_core [all_inputs -no_clocks] -set_output_delay $io_delay -clock vclk_core [all_outputs] +# tpu is mesochronous with its surroundings: the same clock, a phase it +# does not know (its clock runs through its tiles), with the FIFOs on its +# boundary outside it. So only register-to-register paths are judged, +# and the boundary is a set_max_delay budget for repair, with no input or +# output delay (the platform's constraints.sdc). +set clk_name core_clock +set clk_port_name clk +set clk_period 450 +set in2reg_max 300 +set reg2out_max 100 +set in2out_max 100 +source $::env(PLATFORM_DIR)/constraints.sdc diff --git a/flow/designs/asap7/tpu/dpl_none.tcl b/flow/designs/asap7/tpu/dpl_none.tcl new file mode 100644 index 0000000000..2b69c3010f --- /dev/null +++ b/flow/designs/asap7/tpu/dpl_none.tcl @@ -0,0 +1,16 @@ +# The top level has no standard cell to legalize, and its macros' boxes +# overlap where the parent's notch holds the tiles: detailed placement would +# count the overlap twice and refuse (DPL-0038). With nothing movable, +# there is nothing to legalize. Sourced before each stage that legalizes. +set movable 0 +foreach inst [[ord::get_db_block] getInsts] { + if { ![[$inst getMaster] isBlock] && ![$inst isFixed] } { + incr movable + } +} +if { $movable == 0 } { + utl::info FLW 1 "No movable standard cell, detailed placement skipped." + proc detailed_placement_helper { args } { } + proc optimize_mirroring { args } { } + proc check_placement { args } { return 0 } +} diff --git a/flow/designs/asap7/tpu/floorplan.svg b/flow/designs/asap7/tpu/floorplan.svg new file mode 100644 index 0000000000..fcac18f05a --- /dev/null +++ b/flow/designs/asap7/tpu/floorplan.svg @@ -0,0 +1,2558 @@ + + +tpu at global route: placement density, the C-shaped parent and its 16 tiles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 % +100 % +cell area per 2.16 µm bin + \ No newline at end of file diff --git a/flow/designs/asap7/tpu/io_top.tcl b/flow/designs/asap7/tpu/io_top.tcl new file mode 100644 index 0000000000..66f3466f86 --- /dev/null +++ b/flow/designs/asap7/tpu/io_top.tcl @@ -0,0 +1,21 @@ +# tpu's top level, macros only: each of its ports sits where the parent +# macro's (u_rest's) pin for it is, on that macro's bottom edge. +set block [ord::get_db_block] +set dbu [[ord::get_db_tech] getDbUnitsPerMicron] +set l [$block findInst u_rest] +foreach bt [$block getBTerms] { + set net [$bt getNet] + if { $net eq "NULL" } { continue } + foreach it [$net getITerms] { + if { [$it getInst] ne $l } { continue } + foreach mpin [[$it getMTerm] getMPins] { + foreach box [$mpin getGeometry] { + set x [expr { ([$box xMin] + [$box xMax]) / 2.0 / $dbu }] + set layer [[$box getTechLayer] getName] + place_pin -pin_name [$bt getName] -layer $layer -location [list $x 0] -force_to_die_boundary + break + } + break + } + } +} diff --git a/flow/designs/asap7/tpu/pdn_macro.tcl b/flow/designs/asap7/tpu/pdn_macro.tcl new file mode 100644 index 0000000000..4a55c12bdb --- /dev/null +++ b/flow/designs/asap7/tpu/pdn_macro.tcl @@ -0,0 +1,16 @@ +# A macro's grid for abutment: rails, M5 straps, and M8 straps as the pins +# the parent's M9 lands on. No ring: nothing at the edge the band abuts. +# The macro routes signals on M2-M8; M9 belongs to the parent's grid. +add_global_connection -net {VDD} -inst_pattern {.*} -pin_pattern {^VDD$} -power +add_global_connection -net {VSS} -inst_pattern {.*} -pin_pattern {^VSS$} -ground +set_voltage_domain -name {CORE} -power {VDD} -ground {VSS} +define_pdn_grid -name {top} -voltage_domains {CORE} -pins {M8} +add_pdn_stripe -grid {top} -layer {M1} -width {0.018} -pitch {0.54} -offset {0} -followpins +add_pdn_stripe -grid {top} -layer {M2} -width {0.018} -pitch {0.54} -offset {0} -followpins +add_pdn_stripe -grid {top} -layer {M5} -width {0.12} -spacing {0.072} -pitch {2.976} \ + -offset {1.5} -extend_to_core_ring +add_pdn_stripe -grid {top} -layer {M8} -width {0.16} -spacing {0.16} -pitch {2.16} \ + -offset {0.5} -extend_to_boundary +add_pdn_connect -grid {top} -layers {M1 M2} +add_pdn_connect -grid {top} -layers {M2 M5} +add_pdn_connect -grid {top} -layers {M5 M8} diff --git a/flow/designs/asap7/tpu/pdn_top.tcl b/flow/designs/asap7/tpu/pdn_top.tcl new file mode 100644 index 0000000000..69f046809f --- /dev/null +++ b/flow/designs/asap7/tpu/pdn_top.tcl @@ -0,0 +1,30 @@ +# The grid of a top level that is macros only. A core grid takes its +# extent from the rows, and this top level has none: the M9 straps are +# each macro's instance grid instead, over the macro, landing on its M8 +# straps; macros that abut have straps that abut. +add_global_connection -net {VDD} -inst_pattern {.*} -pin_pattern {^VDD$} -power +add_global_connection -net {VSS} -inst_pattern {.*} -pin_pattern {^VSS$} -ground +set_voltage_domain -name {CORE} -power {VDD} -ground {VSS} +# The parent macro's (u_rest's) box holds its notch, where the tiles +# are; a grid takes the area it covers first, so the tiles' grid is +# defined before the parent's. +set tiles {} +set others {} +foreach inst [[ord::get_db_block] getInsts] { + set m [$inst getMaster] + if { ![$m isBlock] } { continue } + if { [$inst getName] eq "u_rest" } { + dict set others [$m getName] 1 + } else { + dict set tiles [$m getName] 1 + } +} +# Finding: the tiles' grid draws nothing, defined first or not; the +# generator seems to take the parent's whole box as occupied. The design +# is read at global route, where M9 carries no signal: the parent's grid +# only, the tiles' power left unconnected at the top level. +foreach {name cells} [list ParentGrid [dict keys $others]] { + define_pdn_grid -macro -cells $cells -halo {0 0 0 0} -voltage_domains {CORE} -name $name + add_pdn_stripe -grid $name -layer {M9} -width {0.16} -spacing {0.16} -pitch {2.88} -offset {0.5} + add_pdn_connect -grid $name -layers {M8 M9} +} diff --git a/flow/designs/asap7/tpu/period_vs_cpu.svg b/flow/designs/asap7/tpu/period_vs_cpu.svg new file mode 100644 index 0000000000..b3875b9488 --- /dev/null +++ b/flow/designs/asap7/tpu/period_vs_cpu.svg @@ -0,0 +1,22 @@ + + +tpu 8 x 8: best stitched period against CPU time spent +652 +674 +695 +717 +739 +500 +1,000 +2,000 +5,000 +10,000 +20,000 +50,000 +cumulative CPU time, s (synthesis to global route, log scale) +best period so far, ps + +2 x 2 tiles + +flat + \ No newline at end of file diff --git a/flow/designs/asap7/tpu/pins_bottom.tcl b/flow/designs/asap7/tpu/pins_bottom.tcl new file mode 100644 index 0000000000..a59ede0fa3 --- /dev/null +++ b/flow/designs/asap7/tpu/pins_bottom.tcl @@ -0,0 +1,9 @@ +# Every signal and clock pin on the bottom side: the one side a macro built +# to be abutted shows its parent. +set names {} +foreach bterm [[ord::get_db_block] getBTerms] { + if { [$bterm getSigType] ne "SIGNAL" && [$bterm getSigType] ne "CLOCK" } { continue } + lappend names [$bterm getName] +} +set_io_pin_constraint -region bottom:* -pin_names $names +puts "pins_bottom.tcl: [llength $names] pins on the bottom side" diff --git a/flow/designs/asap7/tpu/place.tcl b/flow/designs/asap7/tpu/place.tcl new file mode 100644 index 0000000000..5ed75695c9 --- /dev/null +++ b/flow/designs/asap7/tpu/place.tcl @@ -0,0 +1,25 @@ +# tpu's top level, macros only: the parent macro u_rest at the die's +# origin, the tiles u_b_ in a grid in its notch, row 0 at the +# notch's top, its west and top edges at 6.336 and 114.48 um +# (tpu_io.tcl). place_macro refuses macros whose bounding boxes overlap +# (MPL-0041), and the parent's box holds its notch: the macros are placed +# through the database directly, fixed, and the router sees their real +# shapes. +set block [ord::get_db_block] +set dbu [[ord::get_db_tech] getDbUnitsPerMicron] +proc at { inst x y } { + $inst setOrigin $x $y + $inst setOrient R0 + $inst setPlacementStatus FIRM +} +at [$block findInst u_rest] 0 0 +set x0 [expr { int(round(6.336 * $dbu)) }] +set y1 [expr { int(round(114.48 * $dbu)) }] +set n 0 +foreach i [$block getInsts] { + if { ![regexp {^u_b(\d+)_(\d+)$} [$i getName] -> r c] } { continue } + set m [$i getMaster] + at $i [expr { $x0 + $c * [$m getWidth] }] [expr { $y1 - ($r + 1) * [$m getHeight] }] + incr n +} +puts "place_notch.tcl: $n tiles in the notch" diff --git a/flow/designs/asap7/tpu/round_trip.svg b/flow/designs/asap7/tpu/round_trip.svg new file mode 100644 index 0000000000..7c487cf852 --- /dev/null +++ b/flow/designs/asap7/tpu/round_trip.svg @@ -0,0 +1,17 @@ + + + tpu: an RTL edit in the processing element, warm cache, to global route + + tiles in a C + flat + + + + + + 99 s: one tile 68 s, the top level 9 s, the carve and the timing read 22 s + + 487 s + Bazel's wall time; what re-ran from the flows' own logs. The tile, built once for its 16 instances, is the only + macro the edit touches; the C-shaped parent stayed cached, or rebuilds beside the tile (24 s) if its netlist reorders. + diff --git a/flow/designs/asap7/tpu/surgery.py b/flow/designs/asap7/tpu/surgery.py new file mode 100644 index 0000000000..8159fa69d7 --- /dev/null +++ b/flow/designs/asap7/tpu/surgery.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +"""SYNTH_VERILOG_SURGERY for asap7/tpu: the array carved into abutted tiles. + + surgery.py CARVE_PY WRAP_PARENT_PY --out-dir DIR -- tpu.sv tpu_array.sv tpu_pe.sv + +CARVE_PY and WRAP_PARENT_PY are flow/util's carve.py and wrap_parent.py, +named in SYNTH_VERILOG_SURGERY after this script so that synthesis +declares every file the surgery reads. + +Elaborates tpu with yosys's read_slang, carves its 8 x 8 array of +processing elements into 16 identical tiles of 2 x 2 (flow/util/carve.py: +the design's own cells regrouped, no RTL edited, no cell added), and moves +the rest of tpu into a module of its own (flow/util/wrap_parent.py), so +that tpu is instances only: + + tpu_block a tile, 2 x 2 processing elements + tpu_rest the rest of tpu: the skew and deskew registers, the clock + root, the weights in; built as a C-shaped macro whose notch + holds the tiles + tpu one tpu_rest and 16 tpu_block, wires only + +The netlist is the copy of tpu.sv; the copies of the other two sources +are empty. Each flow of the design (tpu, tpu_block, tpu_rest) synthesises +its own module from it. + +Each tile holds its processing elements' weight, partial-sum and +activation registers and their input cones. Its ports face its +neighbours: activations in on the west and out on the east, weights and +partial sums in on the north and out on the south. The clock, reset and +load enable run through the tiles with the activations: each row's +westmost tile reads tpu_rest's, every other tile its west neighbour's, +and the bottom row's tiles clock the deskew registers below them. +""" + +import argparse +import importlib.util +import json +import os +import subprocess +import sys +import tempfile + +N = 8 +BLOCK = (2, 2) + + +def pe(i, j, reg): + return "u_array.g_pe_row[%d].g_pe_col[%d].u_pe.%s" % (i, j, reg) + + +# A register between two PEs has a name in each, and yosys keeps +# whichever sorts first: with two-digit rows or columns that is the +# neighbour's. A spec names such a net by both. +def a_east(n, i, j): + """A PE's activation register, also its east neighbour's a_west.""" + if j + 1 < n: + return [pe(i, j, "a_east"), pe(i, j + 1, "a_west")] + return pe(i, j, "a_east") + + +def w(n, i, j): + """A PE's weight register, also the PE below's w_north.""" + if i + 1 < n: + return [pe(i, j, "w"), pe(i, j, "w_south"), pe(i + 1, j, "w_north")] + return [pe(i, j, "w"), pe(i, j, "w_south")] + + +def spec(n, r, c): + def psum(i, j): + # the last row's partial sums carry the deskew's names + if i == n - 1: + return "g_deskew[%d].cin" % j + return [pe(i, j, "psum_south"), pe(i + 1, j, "psum_north")] + + tiles = [] + for bi in range(n // r): + for bj in range(n // c): + rows = range(bi * r, bi * r + r) + cols = range(bj * c, bj * c + c) + last_col = bj * c + c - 1 == n - 1 + seeds = [] + for i in rows: + for j in cols: + seeds += [{"net": w(n, i, j)}, {"net": psum(i, j)}] + # activation registers, those of the block's last column last: + # a block at the array's east edge has none of those + seeds += [ + {"net": a_east(n, i, j)} for i in rows for j in cols if j != cols[-1] + ] + if not last_col: + seeds += [{"net": a_east(n, i, cols[-1])} for i in rows] + ports = [] + for k, i in enumerate(rows): + if bj == 0: + # the skew registers' outputs have no name of their + # own: the inputs of the first column's registers + ports.append( + { + "net": a_east(n, i, 0), + "via": "D", + "port": "a_in", + "port_lsb": 8 * k, + } + ) + else: + ports.append( + { + "net": a_east(n, i, cols[0] - 1), + "port": "a_in", + "port_lsb": 8 * k, + } + ) + if not last_col: + ports.append( + { + "net": a_east(n, i, cols[-1]), + "port": "a_out", + "port_lsb": 8 * k, + } + ) + for k, j in enumerate(cols): + if bi == 0: + ports.append( + { + "net": "w_data", + "lsb": 8 * j, + "width": 8, + "port": "w_in", + "port_lsb": 8 * k, + } + ) + else: + ports.append( + { + "net": w(n, rows[0] - 1, j), + "port": "w_in", + "port_lsb": 8 * k, + } + ) + ports.append( + { + "net": psum(rows[0] - 1, j), + "port": "psum_in", + "port_lsb": 32 * k, + } + ) + ports.append( + {"net": w(n, rows[-1], j), "port": "w_out", "port_lsb": 8 * k} + ) + ports.append( + {"net": psum(rows[-1], j), "port": "psum_out", "port_lsb": 32 * k} + ) + ports += [ + {"net": "clk", "port": "clk"}, + {"net": "rst", "port": "rst"}, + {"net": "w_valid", "port": "w_valid"}, + ] + t = {"instance": "u_b%d_%d" % (bi, bj), "seeds": seeds, "ports": ports} + unread = (["a_out"] if last_col else []) + ( + ["w_out"] if bi == n // r - 1 else [] + ) + if unread: + t["unread"] = unread + if bi == 0: + t["tie"] = ["psum_in"] + # the clock, reset and load enter each block row at its west + # end and run east with the activations, tile to tile + if bj > 0: + t["chain_from"] = "u_b%d_%d" % (bi, bj - 1) + # the bottom row's clock runs on into the deskew registers + # its results go to + if bi == n // r - 1: + t["south_registers"] = [{"net": psum(n - 1, j)} for j in cols] + tiles.append(t) + + # the template: below the top row (its partial-sum adders are real), + # left of the last column where there is one (its activation outputs + # exist); a bottom block exports its weight outputs all the same + def rank(t): + return ("tie" in t, "a_out" in t.get("unread", [])) + + tiles.sort(key=rank) + return { + "module": "tpu_block", + "stops": [{"net": "g_skew[0].din"}], + "tiles": tiles, + "chain": [["clk", "clk_out"], ["rst", "rst_out"], ["w_valid", "w_valid_out"]], + "south": [["clk", "clk_south"]], + } + + +def yosys(script): + subprocess.run([os.environ["YOSYS_EXE"], "-q", "-p", script], check=True) + + +def slang(): + """read_slang is built into yosys from 0.67; before, it is the slang + plugin, found on YOSYS_PLUGIN_PATH as synth_preamble.tcl finds it.""" + probe = subprocess.run( + [os.environ["YOSYS_EXE"], "-p", "help read_slang"], + capture_output=True, + text=True, + check=True, + ) + return "plugin -i slang; " if "No such command" in probe.stdout else "" + + +def load(path): + name = os.path.splitext(os.path.basename(path))[0] + spec_ = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec_) + spec_.loader.exec_module(module) + return module + + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument("carve_py") + parser.add_argument("wrap_parent_py") + parser.add_argument("--out-dir", required=True) + parser.add_argument("files", nargs="+") + args = parser.parse_args(argv) + carve = load(args.carve_py) + wrap_parent = load(args.wrap_parent_py) + names = sorted(os.path.basename(f) for f in args.files) + if names != ["tpu.sv", "tpu_array.sv", "tpu_pe.sv"]: + sys.exit("surgery.py: expected tpu.sv, tpu_array.sv, tpu_pe.sv, got %s" % names) + with tempfile.TemporaryDirectory(dir=args.out_dir) as work: + flat = os.path.join(work, "flat.json") + yosys( + slang() + + "read_slang --top tpu %s; hierarchy -top tpu; proc; flatten; memory; " + "opt -purge; write_json %s" % (" ".join(args.files), flat) + ) + with open(flat) as f: + design = json.load(f) + carve.carve(design, "tpu", spec(N, *BLOCK)) + wrap_parent.wrap(design, "tpu", "tpu_rest") + carved = os.path.join(work, "carved.json") + with open(carved, "w") as f: + json.dump(design, f) + yosys( + "read_json %s; hierarchy -check -top tpu; write_verilog -noattr %s" + % (carved, os.path.join(args.out_dir, "tpu.sv")) + ) + for name in ["tpu_array.sv", "tpu_pe.sv"]: + with open(os.path.join(args.out_dir, name), "w") as f: + f.write("// carved into tpu.sv by surgery.py\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/flow/designs/asap7/tpu/tapcell_none.tcl b/flow/designs/asap7/tpu/tapcell_none.tcl new file mode 100644 index 0000000000..7a11b4ac35 --- /dev/null +++ b/flow/designs/asap7/tpu/tapcell_none.tcl @@ -0,0 +1,3 @@ +# A top level that is macros only: no tap cells, and the rows stay under +# the macros, which placement treats as blocked; cut_rows would remove +# every row, and placement refuses a design without one (DPL-0012). diff --git a/flow/designs/asap7/tpu/tpu_block/config.mk b/flow/designs/asap7/tpu/tpu_block/config.mk new file mode 100644 index 0000000000..331cf60bb0 --- /dev/null +++ b/flow/designs/asap7/tpu/tpu_block/config.mk @@ -0,0 +1,30 @@ +# The tile: 2 x 2 processing elements, carved out of tpu (../surgery.py), +# its pins planned with the C's (../tpu_io.tcl). +export DESIGN_NICKNAME = tpu_tpu_block +export DESIGN_NAME = tpu_block +export PLATFORM = asap7 + +export VERILOG_FILES = $(sort $(wildcard $(DESIGN_HOME)/src/tpu/*.sv)) +export SYNTH_VERILOG_SURGERY = $(DESIGN_HOME)/$(PLATFORM)/tpu/surgery.py \ + $(FLOW_HOME)/util/carve.py $(FLOW_HOME)/util/wrap_parent.py +export SDC_FILE = $(DESIGN_HOME)/$(PLATFORM)/tpu/tpu_block/constraint.sdc +export SYNTH_HDL_FRONTEND = slang +export LEC_CHECK = 0 + +export DIE_AREA = 0 0 24.336 25.92 +export CORE_AREA = 1.008 1.08 23.328 24.84 +export MIN_ROUTING_LAYER = M2 +export MAX_ROUTING_LAYER = M8 +export IO_PLACER_H = M2 M4 +export IO_PLACER_V = M3 M5 +export IO_CONSTRAINTS = $(DESIGN_HOME)/$(PLATFORM)/tpu/tpu_io.tcl +export PDN_TCL = $(DESIGN_HOME)/$(PLATFORM)/tpu/pdn_macro.tcl + +# the settings a search over placement density, padding, clock tree +# clustering and global route layer adjustment found best +export PLACE_DENSITY = 0.634 +export CELL_PAD_IN_SITES_GLOBAL_PLACEMENT = 1 +export CELL_PAD_IN_SITES_DETAIL_PLACEMENT = 0 +export CTS_CLUSTER_SIZE = 184 +export CTS_CLUSTER_DIAMETER = 353 +export ROUTING_LAYER_ADJUSTMENT = 0.137 diff --git a/flow/designs/asap7/tpu/tpu_block/constraint.sdc b/flow/designs/asap7/tpu/tpu_block/constraint.sdc new file mode 100644 index 0000000000..15a4639178 --- /dev/null +++ b/flow/designs/asap7/tpu/tpu_block/constraint.sdc @@ -0,0 +1,9 @@ +# The tile's repair aims at a tighter clock than tpu's: the period the +# tile reaches is what sets tpu's (constraint.sdc in the parent folder). +set clk_name core_clock +set clk_port_name clk +set clk_period 337 +set in2reg_max 300 +set reg2out_max 100 +set in2out_max 100 +source $::env(PLATFORM_DIR)/constraints.sdc diff --git a/flow/designs/asap7/tpu/tpu_io.tcl b/flow/designs/asap7/tpu/tpu_io.tcl new file mode 100644 index 0000000000..ab702e4209 --- /dev/null +++ b/flow/designs/asap7/tpu/tpu_io.tcl @@ -0,0 +1,205 @@ +# The pins of tpu's two macros, the tile (tpu_block) and the parent built +# around the tiles (tpu_rest): IO_CONSTRAINTS of both. +# +# Every pin is placed by rule, from constants here, before anything is +# built, so the tile and the parent agree without either reading the +# other: a tile's pin and its partner on the opposite edge sit at one +# offset, on asap7 tracks every macro placed on the 0.144 x 2.16 um +# lattice shares (M3 and M5 on the north and south edges, M4 on the west +# and east). The parent's pin for a tile pin is the tile's mirrored +# across the edge they share, so the two touch there. + +namespace eval tpu_io { +# the tile, and the parent's notch that holds 4 x 4 of them +variable tile_w 24.336 +variable tile_h 25.92 +variable notch {6.336 10.8 103.68 114.48} +# each side's ports in order, bit 0 first, with their widths; west and +# east are partners slot by slot, and north and south +variable sides { + west {a_in 16 clk 1 rst 1 w_valid 1} + east {a_out 16 clk_out 1 rst_out 1 w_valid_out 1} + north {w_in 16 psum_in 64} + south {w_out 16 psum_out 64 clk_south 1} + } +# asap7's make_tracks: offset, pitch and wire width, um +variable tracks {M3 {0.009 0.036 0.018} M4 {0.012 0.048 0.024} M5 {0.012 0.048 0.024}} +variable length 0.12 +variable corner 1.0 +} + +proc tpu_io::bits { ports } { + set out {} + foreach {name width} $ports { + if { $width == 1 } { + lappend out $name + } else { + for { set i 0 } { $i < $width } { incr i } { + lappend out "$name\[$i\]" + } + } + } + return $out +} + +proc tpu_io::snap { layer v } { + variable tracks + lassign [dict get $tracks $layer] off pitch + return [expr { round(($off + round(($v - $off) / $pitch) * $pitch) * 1000) / 1000.0 }] +} + +# Every tile pin: name -> {side layer x0 y0 x1 y1}, in tile um. +proc tpu_io::plan { } { + variable tile_w + variable tile_h + variable sides + variable tracks + variable length + variable corner + set plan [dict create] + foreach {a b span vertical} [list west east $tile_h 0 north south $tile_w 1] { + set pa [bits [dict get $sides $a]] + set pb [bits [dict get $sides $b]] + set n [expr { max([llength $pa], [llength $pb]) }] + set step [expr { ($span - 2 * $corner) / $n }] + for { set i 0 } { $i < $n } { incr i } { + set layer [expr { $vertical ? [lindex {M3 M5} [expr { $i % 2 }]] : "M4" }] + set at [snap $layer [expr { $corner + $step * ($i + 0.5) }]] + set half [expr { [lindex [dict get $tracks $layer] 2] / 2.0 }] + foreach side [list $a $b] pins [list $pa $pb] { + if { $i >= [llength $pins] } { continue } + if { $vertical } { + set y0 [expr { $side eq "south" ? 0 : $tile_h - $length }] + set rect [list [expr { $at - $half }] $y0 [expr { $at + $half }] [expr { $y0 + $length }]] + } else { + set x0 [expr { $side eq "west" ? 0 : $tile_w - $length }] + set rect [list $x0 [expr { $at - $half }] [expr { $x0 + $length }] [expr { $at + $half }]] + } + dict set plan [lindex $pins $i] [list $side $layer {*}$rect] + } + } + } + return $plan +} + +proc tpu_io::place { name layer x y w h } { + place_pin -pin_name $name -layer $layer -location [list $x $y] -pin_size [list $w $h] +} + +# The tile: every pin where the plan has it. +proc tpu_io::tile { } { + dict for {pin p} [plan] { + lassign $p side layer x0 y0 x1 y1 + place $pin $layer [expr { ($x0 + $x1) / 2.0 }] [expr { ($y0 + $y1) / 2.0 }] \ + [expr { $x1 - $x0 }] [expr { $y1 - $y0 }] + } + puts "tpu_io.tcl: [dict size [plan]] tile pins planned" +} + +# The parent: the whole die, the notch kept for the tiles. +proc tpu_io::parent { } { + variable tile_w + variable tile_h + variable notch + set block [ord::get_db_block] + set tech [ord::get_db_tech] + set dbu [$tech getDbUnitsPerMicron] + lassign [lmap v $notch { expr { int(round($v * $dbu)) } }] nx0 ny0 nx1 ny1 + # nothing placed or routed in the notch + odb::dbBlockage_create $block $nx0 $ny0 $nx1 $ny1 + foreach layer [$tech getLayers] { + if { [$layer getType] eq "ROUTING" } { + odb::dbObstruction_create $block $layer $nx0 $ny0 $nx1 $ny1 + } + } + # no row in the notch or within 0.54 um of it, room for the wires to + # the tiles' pins: rows that cross it are split at its edges + set halo [expr { int(round(0.54 * $dbu)) }] + foreach row [$block getRows] { + set b [$row getBBox] + if { [$b yMax] <= $ny0 - $halo || [$b yMin] >= $ny1 + $halo } { continue } + if { [$b xMax] <= $nx0 - $halo || [$b xMin] >= $nx1 + $halo } { continue } + set site [$row getSite] + set sw [$site getWidth] + lassign [$row getOrigin] ox oy + set name [$row getName] + set orient [$row getOrient] + set end [$b xMax] + odb::dbRow_destroy $row + set n [expr { ($nx0 - $halo - $ox) / $sw }] + if { $n > 0 } { + odb::dbRow_create $block ${name}_w $site $ox $oy $orient HORIZONTAL $n $sw + } + set x1 [expr { $ox + (($nx1 + $halo - $ox + $sw - 1) / $sw) * $sw }] + set n [expr { ($end - $x1) / $sw }] + if { $n > 0 } { + odb::dbRow_create $block ${name}_e $site $x1 $oy $orient HORIZONTAL $n $sw + } + } + # each port for a tile pin, u_b___, the tile's pin + # mirrored across their shared edge: the two touch there, and the + # parent's lies outside the notch, where its router reaches it + set plan [plan] + set own {} + set placed 0 + foreach bt [$block getBTerms] { + set name [$bt getName] + if { ![regexp {^u_b(\d+)_(\d+)__(.+)$} $name -> r c pin] } { + lappend own $name + continue + } + if { ![dict exists $plan $pin] } { error "tpu_io.tcl: $name: the tile has no pin $pin" } + lassign [dict get $plan $pin] side layer x0 y0 x1 y1 + set cx [expr { ($x0 + $x1) / 2.0 }] + set cy [expr { ($y0 + $y1) / 2.0 }] + switch $side { + south { set cy [expr { -$cy }] } + north { set cy [expr { 2 * $tile_h - $cy }] } + west { set cx [expr { -$cx }] } + east { set cx [expr { 2 * $tile_w - $cx }] } + } + place $name $layer [expr { [lindex $notch 0] + $c * $tile_w + $cx }] \ + [expr { [lindex $notch 3] - ($r + 1) * $tile_h + $cy }] \ + [expr { $x1 - $x0 }] [expr { $y1 - $y0 }] + incr placed + } + # tpu's own ports on its bottom edge; the weights at its west end, so + # the global router's box for each reaches round the notch, up the C's + # west side to its top side + set west [lsearch -all -inline -regexp $own {^w_data}] + set own [lsearch -all -inline -not -regexp $own {^w_data}] + set_io_pin_constraint -region bottom:0-[expr { 2 * [lindex $notch 0] }] -pin_names $west + set_io_pin_constraint -region bottom:* -pin_names $own + # a path from port to port is one wire through the parent: the buffer + # synthesis put between the ports goes, and repair leaves the net + # alone, rather than buffer it along a line through the notch + set through 0 + foreach inst [$block getInsts] { + if { ![string match "BUF*" [[$inst getMaster] getName]] } { continue } + set in "" + set out "" + foreach it [$inst getITerms] { + set net [$it getNet] + if { $net eq "NULL" || [$net getSigType] ne "SIGNAL" } { continue } + if { [$it isInputSignal] } { set in $net } + if { [$it isOutputSignal] } { set out $net } + } + if { $in eq "" || $out eq "" } { continue } + if { [llength [$in getITerms]] != 1 || [llength [$in getBTerms]] != 1 } { continue } + if { [llength [$out getITerms]] != 1 || [llength [$out getBTerms]] != 1 } { continue } + set bt [lindex [$out getBTerms] 0] + odb::dbInst_destroy $inst + $bt connect $in + odb::dbNet_destroy $out + $in setDoNotTouch 1 + incr through + } + puts "tpu_io.tcl: $placed tile pins in the notch, [llength $own] on the\ + bottom edge, [llength $west] at its west end, $through feed-throughs kept whole" +} + +switch [[ord::get_db_block] getName] { + tpu_block { tpu_io::tile } + tpu_rest { tpu_io::parent } + default { error "tpu_io.tcl: for tpu_block and tpu_rest, not [[ord::get_db_block] getName]" } +} diff --git a/flow/designs/asap7/tpu/tpu_rest/config.mk b/flow/designs/asap7/tpu/tpu_rest/config.mk new file mode 100644 index 0000000000..3a4adbb56a --- /dev/null +++ b/flow/designs/asap7/tpu/tpu_rest/config.mk @@ -0,0 +1,25 @@ +# The rest of tpu as a macro: the skew and deskew registers, the clock +# root and the weights in, on the whole die with a notch for the 16 +# tiles (../tpu_io.tcl); its abstract obstructs only what it occupies, so +# the notch stays free. +export DESIGN_NICKNAME = tpu_tpu_rest +export DESIGN_NAME = tpu_rest +export PLATFORM = asap7 + +export VERILOG_FILES = $(sort $(wildcard $(DESIGN_HOME)/src/tpu/*.sv)) +export SYNTH_VERILOG_SURGERY = $(DESIGN_HOME)/$(PLATFORM)/tpu/surgery.py \ + $(FLOW_HOME)/util/carve.py $(FLOW_HOME)/util/wrap_parent.py +export SDC_FILE = $(DESIGN_HOME)/$(PLATFORM)/tpu/constraint.sdc +export SYNTH_HDL_FRONTEND = slang +export LEC_CHECK = 0 + +export DIE_AREA = 0 0 103.68 120.96 +export CORE_AREA = 1.008 1.08 102.672 119.88 +export PLACE_DENSITY = 0.6 +export MIN_ROUTING_LAYER = M2 +export MAX_ROUTING_LAYER = M8 +export IO_PLACER_H = M2 M4 +export IO_PLACER_V = M3 M5 +export IO_CONSTRAINTS = $(DESIGN_HOME)/$(PLATFORM)/tpu/tpu_io.tcl +export PDN_TCL = $(DESIGN_HOME)/$(PLATFORM)/tpu/pdn_macro.tcl +export WRITE_ABSTRACT_LEF_ARGS = diff --git a/flow/scripts/generate_abstract.tcl b/flow/scripts/generate_abstract.tcl index 7b6ec30cbd..05fe0c07af 100644 --- a/flow/scripts/generate_abstract.tcl +++ b/flow/scripts/generate_abstract.tcl @@ -34,7 +34,8 @@ if { [env_var_exists_and_non_empty CORNERS] } { } else { log_cmd write_timing_model $::env(RESULTS_DIR)/$::env(DESIGN_NAME)_typ.lib } -log_cmd write_abstract_lef -bloat_occupied_layers $::env(RESULTS_DIR)/$::env(DESIGN_NAME).lef +log_cmd write_abstract_lef {*}$::env(WRITE_ABSTRACT_LEF_ARGS) \ + $::env(RESULTS_DIR)/$::env(DESIGN_NAME).lef if { [env_var_exists_and_non_empty CDL_FILES] } { cdl read_masters $::env(CDL_FILES) diff --git a/flow/scripts/variables.json b/flow/scripts/variables.json index 2810d1ec80..e32b5d024c 100644 --- a/flow/scripts/variables.json +++ b/flow/scripts/variables.json @@ -1561,6 +1561,13 @@ "description": "Name of a Liberty wire_load group applied with set_wire_load_model\nwhenever open.tcl reads timing. OpenSTA applies a wire-load model only\nto nets without parasitics, so it gives wire delay to timing read\nbefore placement and leaves placed and routed timing unaffected. An\nunknown name is an error. Empty, the default, applies none. A platform\nwhose libraries define no wire_load group can supply one in a Liberty\nfile listed in ADDITIONAL_LIBS.\n", "type": "string" }, + "WRITE_ABSTRACT_LEF_ARGS": { + "default": "-bloat_occupied_layers", + "description": "Arguments to write_abstract_lef when it writes a block's abstract.\nThe default obstructs every layer the block uses over its whole\noutline. Empty, the abstract obstructs only the shapes the block\noccupies, so a block whose outline holds a notch, where other macros\nsit, leaves the notch free for them.\n", + "stages": [ + "generate_abstract" + ] + }, "WRITE_ODB_AND_SDC_EACH_STAGE": { "default": 1, "description": "Save out .sdc and .odb file after each stage, useful to disable when using a single OpenROAD instance to run all stages of the flow.\n", diff --git a/flow/scripts/variables.yaml b/flow/scripts/variables.yaml index 4d71623a35..c7e5faa5da 100644 --- a/flow/scripts/variables.yaml +++ b/flow/scripts/variables.yaml @@ -1397,6 +1397,16 @@ ABSTRACT_SOURCE: Which .odb file to use to create abstract stages: - generate_abstract +WRITE_ABSTRACT_LEF_ARGS: + description: | + Arguments to write_abstract_lef when it writes a block's abstract. + The default obstructs every layer the block uses over its whole + outline. Empty, the abstract obstructs only the shapes the block + occupies, so a block whose outline holds a notch, where other macros + sit, leaves the notch free for them. + stages: + - generate_abstract + default: -bloat_occupied_layers PRE_GLOBAL_ROUTE_TCL: description: | Specifies a Tcl script with commands to run before global route. diff --git a/flow/test/test_carve.py b/flow/test/test_carve.py new file mode 100644 index 0000000000..9b836ca651 --- /dev/null +++ b/flow/test/test_carve.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""carve.py on hand-written yosys JSON: two identical tiles and a parent.""" + +import copy +import os +import sys +import unittest + +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "util")) + +import carve # noqa: E402 + + +def dff(d, q, width=1): + return { + "type": "$dff", + "parameters": {"WIDTH": format(width, "032b"), "CLK_POLARITY": "1"}, + "attributes": {}, + "port_directions": {"CLK": "input", "D": "input", "Q": "output"}, + "connections": {"CLK": [2], "D": d, "Q": q}, + } + + +def xor(a, b, y): + return { + "type": "$xor", + "parameters": { + "A_WIDTH": format(len(a), "032b"), + "B_WIDTH": format(len(b), "032b"), + "Y_WIDTH": format(len(y), "032b"), + }, + "attributes": {}, + "port_directions": {"A": "input", "B": "input", "Y": "output"}, + "connections": {"A": a, "B": b, "Y": y}, + } + + +def design(): + """in -> p (parent) ; t0 <= p ^ t1 ; t1 <= p ^ t0 ; out = t0 ^ t1 (parent). + + Tiles a and b each own one register and the xor in front of it; the + parent keeps p and the xor to the output. Bits: clk 2, in 3, out 4, + p 5, t0 6, t1 7, xa 8, xb 9. + """ + return { + "modules": { + "top": { + "attributes": {}, + "ports": { + "clk": {"direction": "input", "bits": [2]}, + "in": {"direction": "input", "bits": [3]}, + "out": {"direction": "output", "bits": [4]}, + }, + "cells": { + "p": dff([3], [5]), + "xa": xor([5], [7], [8]), + "ra": dff([8], [6]), + "xb": xor([5], [6], [9]), + "rb": dff([9], [7]), + "xo": xor([6], [7], [4]), + }, + "netnames": { + "clk": {"hide_name": 0, "bits": [2]}, + "in": {"hide_name": 0, "bits": [3]}, + "out": {"hide_name": 0, "bits": [4]}, + "p": {"hide_name": 0, "bits": [5]}, + "t0": {"hide_name": 0, "bits": [6]}, + "t1": {"hide_name": 0, "bits": [7]}, + }, + } + } + } + + +def spec(): + return { + "module": "T", + "tiles": [ + { + "instance": "a", + "seeds": [{"net": "t0"}], + "ports": [ + {"net": "clk", "port": "clk"}, + {"net": "p", "port": "p"}, + {"net": "t1", "port": "other"}, + {"net": "t0", "port": "q"}, + ], + }, + { + "instance": "b", + "seeds": [{"net": "t1"}], + "ports": [ + {"net": "clk", "port": "clk"}, + {"net": "p", "port": "p"}, + {"net": "t0", "port": "other"}, + {"net": "t1", "port": "q"}, + ], + }, + ], + } + + +class CarveTest(unittest.TestCase): + def test_two_tiles_and_parent(self): + d = design() + census = carve.carve(d, "top", spec()) + top = d["modules"]["top"] + self.assertEqual(sorted(top["cells"]), ["a", "b", "p", "xo"]) + self.assertEqual( + top["cells"]["a"]["connections"], + {"clk": [2], "p": [5], "other": [7], "q": [6]}, + ) + self.assertEqual( + top["cells"]["b"]["connections"], + {"clk": [2], "p": [5], "other": [6], "q": [7]}, + ) + t = d["modules"]["T"] + self.assertEqual(sorted(t["cells"]), ["ra", "xa"]) + self.assertEqual( + {p: v["direction"] for p, v in t["ports"].items()}, + {"clk": "input", "p": "input", "other": "input", "q": "output"}, + ) + self.assertEqual([x["pins_in"] for x in census["tiles"]], [3, 3]) + self.assertEqual(census["parent_registers"], 1) + + def test_multibit_register_split(self): + m = {"cells": {"r": dff([3, 4], [5, 6], width=2)}} + m["cells"]["r"]["type"] = "$sdff" + m["cells"]["r"]["parameters"]["SRST_VALUE"] = "10" + carve.split_flops(m) + self.assertEqual(sorted(m["cells"]), ["r[0]", "r[1]"]) + self.assertEqual(m["cells"]["r[1]"]["connections"]["Q"], [6]) + self.assertEqual(m["cells"]["r[0]"]["parameters"]["SRST_VALUE"], "0") + self.assertEqual(m["cells"]["r[1]"]["parameters"]["SRST_VALUE"], "1") + + def test_unmapped_boundary_refused(self): + s = spec() + s["tiles"][0]["ports"] = s["tiles"][0]["ports"][:-1] + with self.assertRaisesRegex(ValueError, "no port"): + carve.carve(design(), "top", s) + + def test_shared_cell_refused(self): + d = design() + # one xor in front of both registers + top = d["modules"]["top"] + top["cells"]["rb"]["connections"]["D"] = [8] + del top["cells"]["xb"] + with self.assertRaisesRegex(ValueError, "is in a and b"): + carve.carve(d, "top", spec()) + + def test_different_boundary_refused(self): + s = copy.deepcopy(spec()) + s["tiles"][1]["ports"][2]["port"] = "elsewhere" + with self.assertRaisesRegex(ValueError, "different boundary"): + carve.carve(design(), "top", s) + + def test_stop_keeps_shared_logic_in_parent(self): + # without the stop, xa and xb would both read p through one shared + # inverter in front of them, claimed by both tiles + d = design() + top = d["modules"]["top"] + top["cells"]["inv"] = { + "type": "$not", + "parameters": {"A_WIDTH": format(1, "032b"), "Y_WIDTH": format(1, "032b")}, + "attributes": {}, + "port_directions": {"A": "input", "Y": "output"}, + "connections": {"A": [5], "Y": [10]}, + } + top["netnames"]["np"] = {"hide_name": 0, "bits": [10]} + top["cells"]["xa"]["connections"]["A"] = [10] + top["cells"]["xb"]["connections"]["A"] = [10] + s = spec() + for t in s["tiles"]: + t["ports"][1] = {"net": "np", "port": "p"} + with self.assertRaisesRegex(ValueError, "is in a and b"): + carve.carve(copy.deepcopy(d), "top", copy.deepcopy(s)) + s["stops"] = [{"net": "np"}] + carve.carve(d, "top", s) + self.assertIn("inv", top["cells"]) + + def test_split_bitwise(self): + m = {"cells": {"x": xor([3, 4], [5, 6], [7, 8])}} + m["cells"]["m"] = { + "type": "$mux", + "parameters": {"WIDTH": format(2, "032b")}, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "S": "input", + "Y": "output", + }, + "connections": {"A": [3, 4], "B": [5, 6], "S": [9], "Y": [10, 11]}, + } + carve.split_bitwise(m) + self.assertEqual(sorted(m["cells"]), ["m[0]", "m[1]", "x[0]", "x[1]"]) + self.assertEqual( + m["cells"]["x[1]"]["connections"], {"A": [4], "B": [6], "Y": [8]} + ) + self.assertEqual( + m["cells"]["m[1]"]["connections"], {"A": [4], "B": [6], "S": [9], "Y": [11]} + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/flow/test/test_wrap_parent.py b/flow/test/test_wrap_parent.py new file mode 100644 index 0000000000..793fd583c0 --- /dev/null +++ b/flow/test/test_wrap_parent.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""wrap_parent.py on a parent with two tiles: ports per tile pin, ties +inside the parent, nets between tiles left at the top.""" + +import os +import sys +import unittest + +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "util")) + +import wrap_parent # noqa: E402 + +TILE = { + "ports": { + "clk": {"direction": "input", "bits": [2]}, + "a": {"direction": "input", "bits": [3, 4]}, + "z": {"direction": "output", "bits": [5, 6]}, + }, + "cells": {}, + "netnames": {}, +} + + +def design(): + # top: port x (bits 2, 3) feeds a parent cell r driving net 10; tile + # t0 reads clk and net 10 / a constant; t1 reads t0's output alone + return { + "modules": { + "top": { + "ports": { + "clk": {"direction": "input", "bits": [2]}, + "x": {"direction": "input", "bits": [3]}, + "y": {"direction": "output", "bits": [21]}, + }, + "cells": { + "r": { + "type": "$not", + "port_directions": {"A": "input", "Y": "output"}, + "connections": {"A": [3], "Y": [10]}, + }, + "s": { + "type": "$not", + "port_directions": {"A": "input", "Y": "output"}, + "connections": {"A": [13], "Y": [21]}, + }, + "u_b0_0": { + "type": "tile", + "connections": {"clk": [2], "a": [10, "0"], "z": [11, 12]}, + }, + "u_b0_1": { + "type": "tile", + "connections": {"clk": [2], "a": [11, 12], "z": [13, 14]}, + }, + }, + "netnames": { + "clk": {"bits": [2]}, + "x": {"bits": [3]}, + "y": {"bits": [21]}, + }, + }, + "tile": TILE, + } + } + + +class WrapParent(unittest.TestCase): + def setUp(self): + self.d = wrap_parent.wrap(design(), "top", "rest") + self.top = self.d["modules"]["top"] + self.rest = self.d["modules"]["rest"] + + def test_top_is_instances_only(self): + self.assertEqual(sorted(self.top["cells"]), ["u_b0_0", "u_b0_1", "u_rest"]) + types = {c["type"] for c in self.top["cells"].values()} + self.assertEqual(types, {"tile", "rest"}) + + def test_parent_cells_and_ports(self): + self.assertIn("r", self.rest["cells"]) + self.assertIn("s", self.rest["cells"]) + for p in ["clk", "x", "y"]: + self.assertIn(p, self.rest["ports"]) + + def test_a_port_per_tile_pin(self): + ports = self.rest["ports"] + # both tiles' clocks get a port each, so each sits on its tile + self.assertEqual(ports["u_b0_0__clk"]["direction"], "output") + self.assertEqual(ports["u_b0_1__clk"]["direction"], "output") + self.assertEqual(ports["u_b0_0__clk"]["bits"], [2]) + # t1's output goes to the parent's cell s + self.assertEqual(ports["u_b0_1__z"]["direction"], "input") + self.assertEqual(ports["u_b0_1__z"]["bits"], [13]) + + def test_constant_tied_inside(self): + a = self.rest["ports"]["u_b0_0__a"]["bits"] + self.assertEqual(a[0], 10) + ties = [c for c in self.rest["cells"].values() if c["type"] == "$pos"] + self.assertEqual(len(ties), 1) + self.assertEqual(ties[0]["connections"]["A"], ["0"]) + self.assertEqual(ties[0]["connections"]["Y"], [a[1]]) + + def test_tile_to_tile_stays_at_top(self): + t0 = self.top["cells"]["u_b0_0"]["connections"] + t1 = self.top["cells"]["u_b0_1"]["connections"] + self.assertEqual(t0["z"], [11, 12]) + self.assertEqual(t1["a"], [11, 12]) + self.assertNotIn("u_b0_0__z", self.rest["ports"]) + self.assertNotIn("u_b0_1__a", self.rest["ports"]) + + def test_parent_instance_meets_tiles(self): + rest = self.top["cells"]["u_rest"]["connections"] + t0 = self.top["cells"]["u_b0_0"]["connections"] + self.assertEqual(rest["u_b0_0__clk"], t0["clk"]) + self.assertEqual(rest["u_b0_0__a"], t0["a"]) + self.assertNotEqual(t0["clk"], [2]) + + +if __name__ == "__main__": + unittest.main() diff --git a/flow/util/carve.py b/flow/util/carve.py new file mode 100644 index 0000000000..3fb278b321 --- /dev/null +++ b/flow/util/carve.py @@ -0,0 +1,732 @@ +#!/usr/bin/env python3 +"""Carve identical tiles out of a flat module, leave the remainder as the parent. + +The input is yosys's JSON of one flat module (read, `proc`, `flatten`, +`memory`, `opt`). A spec names, for each tile, the registers it owns (its +seeds) and how the nets on its boundary map to the tile's ports. Each +tile takes its seeds and every combinational cell in their input cones, +back to a register it does not own or a port of the module. Every tile +is checked to cut the same boundary, then replaced by an instance of one +module built from the first tile's cells. Nothing else changes: the RTL +is not edited, no cell is added or duplicated, and the remainder of the +module stays the parent, its ports as they were. + +That the other tiles compute what the first one does is not checked +here. The carved design's equivalence to the input, kepler-formal's +combinational check between registers matched by name (lec_names, the +census), proves the carve and the reuse in one go, and a tile with one +port broken must be refuted. + +The spec is JSON: + + {"module": "aes_column", + "tiles": [{"instance": "u_col0", + "seeds": [{"net": "sa00"}, {"net": "text_out", "lsb": 96, "width": 32}], + "ports": [{"net": "sa11", "port": "row1_in"}, + {"net": "text_in", "lsb": 96, "width": 32, "port": "text_in"}]}, + ...]} + +A seed entry {"prefix": "if_stage_i."} takes every register under that +instance path; a tile's "confine" keeps its cones inside the instance, so +its inputs are the instance's own; "auto_ports": true names its ports +after the parent's nets, for a macro that exists once. --spec may be +given several times: one carve after the other, each its own module. + +A seed or port entry takes the bits [lsb, lsb + width) of a net; without +lsb and width, all of them. Port bit i is net bit lsb + i. A seed may be +logic too, such as a slice of a register file's read multiplexer. +A port entry's "port_lsb" puts it at that bit of a wider port, and +"via": "D" names the D inputs of the registers driving the net instead. A tile's +"tie" lists inputs of the first tile that are constant 0 at this one, +the top row of a systolic array: synthesis folded their logic, and the +tile becomes the first tile's module with them tied. A tile's +"unread" lists outputs of the first tile that nothing reads at this one, +the last column of a systolic array: synthesis removed their registers, +the tile is still the first tile's module, and the outputs stay open; the +tile lists the seeds it lacks last. Optional: "stops", entries whose bits end every cone, so logic shared by +all tiles (an address decoder) stays in the parent and reaches each tile +through a port; "split_bitwise": true splits wide gates and multiplexers +per bit, so tiles can own some bits each. +""" + +import argparse +import copy +import json +import re +import sys + +# Registers: data ports scale with WIDTH, the rest are one bit for the cell. +FLOPS = { + "$dff": ("D", "Q"), + "$dffe": ("D", "Q"), + "$sdff": ("D", "Q"), + "$sdffe": ("D", "Q"), + "$sdffce": ("D", "Q"), + "$adff": ("D", "Q"), + "$adffe": ("D", "Q"), +} +# Per-bit values of a flop, sliced with it. +FLOP_VALUE_PARAMS = ("SRST_VALUE", "ARST_VALUE") + + +def param_int(value): + """yosys JSON writes an integer parameter as a binary string, MSB first.""" + if isinstance(value, int): + return value + return int(value, 2) + + +def split_flops(module): + """Every multi-bit register as one cell per bit, so tiles can own bits.""" + cells = module["cells"] + for name in sorted(cells): + cell = cells[name] + if cell["type"] not in FLOPS: + continue + width = param_int(cell["parameters"]["WIDTH"]) + if width == 1: + continue + del cells[name] + for i in range(width): + bit = copy.deepcopy(cell) + bit["parameters"]["WIDTH"] = format(1, "032b") + for p in FLOP_VALUE_PARAMS: + if p in bit["parameters"]: + v = cell["parameters"][p] + # MSB first: bit i is the (width - 1 - i)th character + bit["parameters"][p] = v[len(v) - 1 - i] + for port in FLOPS[cell["type"]]: + bit["connections"][port] = [cell["connections"][port][i]] + cells["%s[%d]" % (name, i)] = bit + + +def net_bits(module, entry): + """The bits an entry of the spec names, in order. + + With "via": "D", the D inputs of the registers whose outputs are the + net's bits: a register's input where elaboration left it no name. + """ + nets = module["netnames"] + # a list names one net by its aliases: yosys keeps one name of a + # net that has several, and which one depends on how they sort + names = entry["net"] if isinstance(entry["net"], list) else [entry["net"]] + found = [n for n in names if n in nets] + if not found: + raise ValueError("no net %s" % " or ".join(names)) + bits = nets[found[0]]["bits"] + if entry.get("via") == "D": + q = {} + for cell in module["cells"].values(): + if cell["type"] in FLOPS: + for i, b in enumerate(cell["connections"]["Q"]): + q[b] = cell["connections"]["D"][i] + missing = [b for b in bits if b not in q] + if missing: + raise ValueError( + "%s: %d bits are no register's output" % (found[0], len(missing)) + ) + bits = [q[b] for b in bits] + lsb = entry.get("lsb", 0) + width = entry.get("width", len(bits) - lsb) + if lsb + width > len(bits): + raise ValueError("%s has %d bits, not %d" % (found[0], len(bits), lsb + width)) + return bits[lsb : lsb + width] + + +class Graph: + """Who drives each bit of a module, and which cells read it.""" + + def __init__(self, module): + self.driver = {} + self.readers = {} + for name, cell in module["cells"].items(): + for port, bits in cell["connections"].items(): + for b in bits: + if isinstance(b, str): + continue + if cell["port_directions"][port] == "output": + self.driver[b] = name + else: + self.readers.setdefault(b, set()).add(name) + self.inputs = set() + self.outputs = set() + for port in module["ports"].values(): + bits = {b for b in port["bits"] if not isinstance(b, str)} + (self.inputs if port["direction"] == "input" else self.outputs).update(bits) + + +def cone(module, graph, seeds, stops=frozenset(), confine=None): + """The seed cells and every combinational cell in their input cones. + + A cone stops at a register, a port of the module and a bit in `stops`; + with `confine`, an instance path, also at any cell outside it: the + tile is that instance's registers and the logic in front of them, its + inputs the instance's own. + """ + cells = module["cells"] + owned = set(seeds) + todo = list(seeds) + while todo: + name = todo.pop() + cell = cells[name] + for port, bits in cell["connections"].items(): + if cell["port_directions"][port] != "input": + continue + for b in bits: + if b in stops: + continue + d = graph.driver.get(b) + if d is None or d in owned or cells[d]["type"] in FLOPS: + continue + # yosys's flatten names a cell $flatten\. + if confine and not ( + d.startswith(confine) or d.startswith("$flatten\\" + confine) + ): + continue + owned.add(d) + todo.append(d) + return owned + + +def seed_cells(module, graph, tile): + """The cells driving the tile's seed bits: its registers, or logic it owns.""" + seeds = set() + for entry in tile["seeds"]: + if "prefix" in entry: + seeds |= prefix_registers(module, entry["prefix"]) + continue + for b in net_bits(module, entry): + d = graph.driver.get(b) + if d is None: + raise ValueError( + "%s: seed bit %s has no driver" % (tile["instance"], b) + ) + seeds.add(d) + return seeds + + +def prefix_registers(module, prefix): + """Every register whose cell or output net is under an instance path.""" + named = set() + for name, net in module["netnames"].items(): + if name.startswith(prefix): + named.update(b for b in net["bits"] if not isinstance(b, str)) + out = set() + for name, cell in module["cells"].items(): + if cell["type"] not in FLOPS: + continue + if ( + name.startswith(prefix) + or name.startswith("$flatten\\" + prefix) + or set(cell["connections"]["Q"]) & named + ): + out.add(name) + if not out: + raise ValueError("no register under %s" % prefix) + return out + + +# Cells whose bit i depends only on the inputs' bit i (and a shared select). +BITWISE = ("$and", "$or", "$xor", "$xnor", "$not", "$mux", "$pmux", "$bmux") + + +def split_bitwise(module): + """Every multi-bit bitwise cell and multiplexer as one cell per bit. + + So that tiles which each own some bits of a wide gate or multiplexer, + such as a register file's slices of one read multiplexer, can own them. + """ + cells = module["cells"] + for name in sorted(cells): + cell = cells[name] + t = cell["type"] + if t not in BITWISE: + continue + c = cell["connections"] + width = len(c["Y"]) + if width == 1: + continue + if t in ("$and", "$or", "$xor", "$xnor") and not ( + len(c["A"]) == len(c["B"]) == width + ): + continue + if t == "$not" and len(c["A"]) != width: + continue + del cells[name] + for i in range(width): + bit = copy.deepcopy(cell) + conn = bit["connections"] + conn["Y"] = [c["Y"][i]] + if t in ("$mux", "$pmux", "$bmux"): + n = len(c["B"]) // width if t == "$pmux" else len(c["A"]) // width + if t == "$mux": + conn["A"], conn["B"] = [c["A"][i]], [c["B"][i]] + bit["parameters"]["WIDTH"] = format(1, "032b") + elif t == "$pmux": + conn["A"] = [c["A"][i]] + conn["B"] = [c["B"][j * width + i] for j in range(n)] + bit["parameters"]["WIDTH"] = format(1, "032b") + else: + conn["A"] = [c["A"][j * width + i] for j in range(n)] + bit["parameters"]["WIDTH"] = format(1, "032b") + else: + conn["A"] = [c["A"][i]] + bit["parameters"]["A_WIDTH"] = format(1, "032b") + if "B" in conn: + conn["B"] = [c["B"][i]] + bit["parameters"]["B_WIDTH"] = format(1, "032b") + bit["parameters"]["Y_WIDTH"] = format(1, "032b") + cells["%s[%d]" % (name, i)] = bit + + +def boundary(module, graph, owned): + """(inputs, outputs): bits the tile reads from outside and drives to it.""" + cells = module["cells"] + ins, outs = set(), set() + for name in owned: + cell = cells[name] + for port, bits in cell["connections"].items(): + for b in bits: + if isinstance(b, str): + continue + if cell["port_directions"][port] == "output": + if b in graph.outputs or graph.readers.get(b, set()) - owned: + outs.add(b) + elif graph.driver.get(b) not in owned: + ins.add(b) + return ins, outs + + +def auto_ports(module, bits): + """{bit: (port, index)}: a port per parent net, named after it, its + bits in the net's order; for a macro that exists once.""" + by_net = {} + for b in sorted(bits): + name = bit_name(module, b) + net, idx = name.rsplit("[", 1) + by_net.setdefault(net, []).append((int(idx[:-1]), b)) + mapping = {} + used = set() + for net, idx_bits in sorted(by_net.items()): + port = re.sub(r"[^A-Za-z0-9_]", "_", net).strip("_") or "p" + while port in used: + port += "_" + used.add(port) + for i, (_, b) in enumerate(sorted(idx_bits)): + mapping[b] = (port, i) + return mapping + + +def port_map(module, tile, bits): + """{bit: (port, index)} for every boundary bit, from the spec's ports.""" + if tile.get("auto_ports"): + return auto_ports(module, bits) + mapping = {} + taken = {} + for entry in tile["ports"]: + base = entry.get("port_lsb", 0) + for i, b in enumerate(net_bits(module, entry), base): + if b in bits and b not in mapping: + if (entry["port"], i) in taken: + raise ValueError( + "%s: %s[%d] maps two boundary bits" + % (tile["instance"], entry["port"], i) + ) + taken[(entry["port"], i)] = b + mapping[b] = (entry["port"], i) + missing = sorted(b for b in bits if b not in mapping) + if missing: + names = names_of(module, missing[:8]) + raise ValueError( + "%s: %d boundary bits have no port, e.g. %s" + % (tile["instance"], len(missing), names) + ) + return mapping + + +def names_of(module, bits): + """A readable name per bit, for messages.""" + found = {} + for name, net in sorted( + module["netnames"].items(), key=lambda kv: kv[1].get("hide_name", 0) + ): + for i, b in enumerate(net["bits"]): + if b in bits and b not in found: + found[b] = "%s[%d]" % (name, i) + return [found.get(b, str(b)) for b in bits] + + +def interface(mapping, ins, outs): + """{port: (direction, width)} the tile shows its parent.""" + ports = {} + for b, (port, i) in mapping.items(): + direction = "output" if b in outs else "input" + d, w = ports.get(port, (direction, 0)) + if d != direction: + raise ValueError("port %s is both input and output" % port) + ports[port] = (direction, max(w, i + 1)) + return ports + + +def build_module(module, owned, mapping, ports): + """The tile's module: its cells as they are, ports by the spec.""" + cells = {n: copy.deepcopy(module["cells"][n]) for n in sorted(owned)} + used = { + b + for c in cells.values() + for bits in c["connections"].values() + for b in bits + if not isinstance(b, str) + } + mod_ports = {} + for port, (direction, width) in sorted(ports.items()): + bits = [None] * width + for b, (p, i) in mapping.items(): + if p == port: + bits[i] = b + if None in bits: + raise ValueError("port %s has a gap at bit %d" % (port, bits.index(None))) + mod_ports[port] = {"direction": direction, "bits": bits} + netnames = {} + for port, p in mod_ports.items(): + netnames[port] = {"hide_name": 0, "bits": p["bits"], "attributes": {}} + inside = used - set(mapping) + for name, net in module["netnames"].items(): + bits = [b for b in net["bits"] if b in inside] + if name in netnames or not bits: + continue + # a net partly inside keeps its name for the bits that are + netnames[name] = { + "hide_name": net.get("hide_name", 0), + "bits": bits, + "attributes": {}, + } + return {"attributes": {}, "ports": mod_ports, "cells": cells, "netnames": netnames} + + +def carve(design, top, spec, gold=None): + """Rewrite `design` in place; return the census. + + `gold`, a dict, receives a copy of the design with its registers split + per bit and nothing carved: the reference for the equivalence test. + """ + module = design["modules"][top] + split_flops(module) + if spec.get("split_bitwise"): + split_bitwise(module) + if gold is not None: + gold.update(copy.deepcopy(design)) + graph = Graph(module) + stops = frozenset(b for e in spec.get("stops", []) for b in net_bits(module, e)) + cones = [] + claimed = {} + for tile in spec["tiles"]: + owned = cone( + module, graph, seed_cells(module, graph, tile), stops, tile.get("confine") + ) + for name in owned: + if name in claimed: + raise ValueError( + "cell %s is in %s and %s" % (name, claimed[name], tile["instance"]) + ) + claimed[name] = tile["instance"] + cones.append((tile, owned)) + tiles = [] + for k, (tile, owned) in enumerate(cones): + ins, outs = boundary(module, graph, owned) + if k == 0 and not tile.get("auto_ports"): + # the module every tile instantiates exports every output its + # spec names, read outside or not: a tile elsewhere in the + # array may need one this tile's neighbours do not read + for entry in tile["ports"]: + for b in net_bits(module, entry): + if graph.driver.get(b) in owned: + outs.add(b) + mapping = port_map(module, tile, ins | outs) + tiles.append((tile, owned, mapping, interface(mapping, ins, outs), ins, outs)) + + first = tiles[0] + for tile, owned, _, ports, _, _ in tiles[1:]: + # outputs nothing reads, whose registers synthesis already removed, + # and inputs that are constants, whose logic synthesis folded + unread = set(tile.get("unread", [])) | set(tile.get("tie", [])) + expected = {p: v for p, v in first[3].items() if p not in unread} + if {p: v for p, v in ports.items() if p not in unread} != expected or set( + ports + ) - set(first[3]): + raise ValueError( + "%s cuts a different boundary than %s" + % (tile["instance"], first[0]["instance"]) + ) + if unread: + continue + if types(module, owned) != types(module, first[1]): + raise ValueError( + "%s has other cells than %s" % (tile["instance"], first[0]["instance"]) + ) + + name = spec["module"] + if name in design["modules"]: + raise ValueError("module %s exists" % name) + design["modules"][name] = build_module(module, first[1], first[2], first[3]) + + next_bit = [ + 1 + + max( + b + for c in module["cells"].values() + for bits in c["connections"].values() + for b in bits + if not isinstance(b, str) + ) + ] + census = { + "module": name, + "tiles": [], + "types": types(module, first[1]), + "lec_names": lec_names(module, design["modules"][name], tiles), + } + for tile, owned, mapping, ports, ins, outs in tiles: + census["tiles"].append( + { + "instance": tile["instance"], + "cells": len(owned), + "registers": sum( + 1 for n in owned if module["cells"][n]["type"] in FLOPS + ), + "pins_in": len(ins), + "pins_out": len(outs), + } + ) + connections = {p: [None] * w for p, (_, w) in first[3].items()} + for b, (p, i) in mapping.items(): + connections[p][i] = b + for p, bits in connections.items(): + for i, b in enumerate(bits): + if b is None: + if first[3][p][0] == "output": + # an unread output: a net of its own, read by nothing + bits[i] = next_bit[0] + next_bit[0] += 1 + elif p in tile.get("tie", []): + # a constant input: the first tile's logic with it + # tied, which the equivalence test checks + bits[i] = "0" + else: + raise ValueError( + "%s: input %s[%d] is not connected" + % (tile["instance"], p, i) + ) + for n in owned: + del module["cells"][n] + module["cells"][tile["instance"]] = { + "hide_name": 0, + "type": name, + "parameters": {}, + "attributes": {}, + "port_directions": {p: d for p, (d, _) in first[3].items()}, + "connections": connections, + } + chain(module, design["modules"][name], spec, next_bit) + census["parent_cells"] = len(module["cells"]) - len(tiles) + census["parent_registers"] = sum( + 1 for c in module["cells"].values() if c["type"] in FLOPS + ) + return census + + +def chain(module, tile_module, spec, next_bit): + """Pass nets every tile reads from one tile to the next. + + For routing by abutment a macro in the middle of an array cannot reach + the parent, so the clock and the other nets every tile reads enter one + tile and leave it again for the next: spec "chain", a list of + [input port, output port]; a tile's "chain_from", the tile its inputs + come from. A tile without one reads the parent's net. "south" names + output ports a tile also passes to the parent's registers named in + its "south_registers" (net names: the registers fed from them, and + those fed from those), so a clock that ran through the tiles reaches + the registers the tiles' results go to. Inside the module an output + is its input, so after flattening every chained net is the net it was. + """ + pairs = [tuple(p) for p in spec.get("chain", []) + spec.get("south", [])] + if not pairs: + return + ports = tile_module["ports"] + for pin, pout in pairs: + if pin not in ports: + raise ValueError("chain: the tile has no port %s" % pin) + ports[pout] = {"direction": "output", "bits": list(ports[pin]["bits"])} + tile_module["netnames"][pout] = { + "hide_name": 0, + "bits": list(ports[pin]["bits"]), + "attributes": {}, + } + cells = module["cells"] + + def fresh(n): + bits = list(range(next_bit[0], next_bit[0] + n)) + next_bit[0] += n + return bits + + for tile in spec["tiles"]: + inst = cells[tile["instance"]] + for pin, pout in pairs: + inst["port_directions"][pout] = "output" + inst["connections"].setdefault(pout, None) + for tile in spec["tiles"]: + src = tile.get("chain_from") + inst = cells[tile["instance"]] + for pin, pout in [tuple(p) for p in spec.get("chain", [])]: + if src: + bits = fresh(len(inst["connections"][pin])) + cells[src]["connections"][pout] = bits + inst["connections"][pin] = bits + for tile in spec["tiles"]: + inst = cells[tile["instance"]] + for pin, pout in pairs: + if inst["connections"][pout] is None: + inst["connections"][pout] = fresh(len(inst["connections"][pin])) + # the parent's registers downstream of the tile's results take + # their clock from the tile's south output + nets = tile.get("south_registers", []) + if not nets: + continue + seen = set() + for e in nets: + seen.update(b for b in net_bits(module, e) if not isinstance(b, str)) + regs = [] + grew = True + while grew: + grew = False + for n, c in cells.items(): + if ( + c["type"] in FLOPS + and n not in regs + and set(c["connections"]["D"]) & seen + ): + regs.append(n) + seen.update(c["connections"]["Q"]) + grew = True + for pin, pout in [tuple(p) for p in spec.get("south", [])]: + for n in regs: + cells[n]["connections"]["CLK"] = list(inst["connections"][pout]) + + +def lec_names(module, tile_module, tiles): + """{flattened tile net bit: the reference's name for it}, for every seed bit. + + Every tile is the first tile's module, so after flattening the carved + design a register of tile c drives a net named after the module's own + net under c's instance. The k-th seed bit of every tile is the same + register of the module; the equivalence test names it after tile c's + own k-th seed bit in the reference. + """ + first = tiles[0][0] + out = {} + # the first tile is its module's own cells: each register's output + # has the same bit in both, named in each + for n in tiles[0][1]: + cell = module["cells"][n] + if cell["type"] in FLOPS: + for b in cell["connections"]["Q"]: + out["%s.%s" % (first["instance"], bit_name(tile_module, b))] = bit_name( + module, b + ) + if len(tiles) == 1: + return out + inner = [ + bit_name(tile_module, b) for e in first["seeds"] for b in net_bits(module, e) + ] + for tile, _, _, _, _, _ in tiles[1:]: + # a tile with unread outputs lists the seeds it lacks last, so the + # seeds it has line up with the first tile's + gold = [bit_name(module, b) for e in tile["seeds"] for b in net_bits(module, e)] + for name, g in zip(inner, gold): + out["%s.%s" % (tile["instance"], name)] = g + return out + + +def bit_name(module, bit): + """`net[i]` for a bit: the first public name, else the first name.""" + best = None + for name, net in sorted(module["netnames"].items()): + if bit in net["bits"]: + cand = (net.get("hide_name", 0), name, net["bits"].index(bit)) + if best is None or cand < best: + best = cand + if best is None: + raise ValueError("bit %s has no name" % bit) + return "%s[%d]" % (best[1], best[2]) + + +def types(module, owned): + """{cell type: count}, to tell tiles apart cheaply.""" + count = {} + for n in owned: + t = module["cells"][n]["type"] + count[t] = count.get(t, 0) + 1 + return count + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument( + "--json", required=True, help="yosys write_json of the flat design" + ) + parser.add_argument("--top", required=True) + parser.add_argument( + "--spec", required=True, action="append", help="carve specs, applied in order" + ) + parser.add_argument("--out", required=True, help="the carved design, yosys JSON") + parser.add_argument("--census", required=True, help="cells and pins per tile, JSON") + parser.add_argument( + "--gold", help="the design uncarved, registers split per bit, yosys JSON" + ) + args = parser.parse_args(argv) + with open(args.json) as f: + design = json.load(f) + gold = {} + censuses = [] + try: + for i, path in enumerate(args.spec): + with open(path) as f: + spec = json.load(f) + censuses.append(carve(design, args.top, spec, gold if i == 0 else None)) + except ValueError as e: + print("carve: %s" % e, file=sys.stderr) + return 1 + # one census: the first spec's, the others' tiles and register names added + census = dict(censuses[0]) + for c in censuses[1:]: + census["tiles"] = census["tiles"] + c["tiles"] + census["lec_names"] = dict(census["lec_names"], **c["lec_names"]) + census["parent_cells"] = c["parent_cells"] + census["parent_registers"] = c["parent_registers"] + census["modules"] = [c["module"] for c in censuses] + with open(args.out, "w") as f: + json.dump(design, f) + with open(args.census, "w") as f: + json.dump(census, f, indent=1, sort_keys=True) + if args.gold: + with open(args.gold, "w") as f: + json.dump(gold, f) + t = census["tiles"][0] + print( + "carve: %d x %s, %d cells, %d registers, %d pins in, %d out; parent %d cells, %d registers" + % ( + len(census["tiles"]), + census["module"], + t["cells"], + t["registers"], + t["pins_in"], + t["pins_out"], + census["parent_cells"], + census["parent_registers"], + ) + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/flow/util/wrap_parent.py b/flow/util/wrap_parent.py new file mode 100644 index 0000000000..79c6fd4c23 --- /dev/null +++ b/flow/util/wrap_parent.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""The parent as a macro: a carved top's own cells moved into a module. + + wrap_parent.py IN.json OUT.json TOP PARENT + +Reads yosys JSON of a carved design (carve.py's output, read back with +`proc`) whose top TOP instantiates the tiles. Writes TOP as instances +only: PARENT, a new module holding every cell of TOP that is not a tile, +and the tiles. TOP's ports stay PARENT's. Every tile pin bit on a net the +parent touches (a parent cell, a port of TOP, a constant) gets a port of +PARENT of its own, `__`, so each sits where its one tile +pin is and the two abut pin on pin; a constant a tile reads is driven +from inside PARENT, since TOP has no cell to tie it. Nets between tiles +alone stay in TOP. +""" + +import json +import sys + + +def is_const(b): + return isinstance(b, str) + + +def wrap(design, top, parent): + mods = design["modules"] + t = mods[top] + tiles = {n: c for n, c in t["cells"].items() if c["type"] in mods} + own = {n: c for n, c in t["cells"].items() if n not in tiles} + + def direction(cell, pin): + return mods[cell["type"]]["ports"][pin]["direction"] + + touched = set() + for p in t["ports"].values(): + touched.update(p["bits"]) + for c in own.values(): + for bits in c["connections"].values(): + touched.update(bits) + + used = [b for p in t["ports"].values() for b in p["bits"] if not is_const(b)] + for c in t["cells"].values(): + used += [ + b for bits in c["connections"].values() for b in bits if not is_const(b) + ] + fresh = [max(used + [1]) + 1] + + def new_bit(): + fresh[0] += 1 + return fresh[0] - 1 + + ports = {n: dict(p) for n, p in t["ports"].items()} + cells = dict(own) + rest_conn = {n: p["bits"] for n, p in t["ports"].items()} + new_tiles = {} + for name in sorted(tiles): + cell = tiles[name] + conn = {} + for pin, bits in cell["connections"].items(): + if not any(is_const(b) or b in touched for b in bits): + conn[pin] = bits + continue + d = direction(cell, pin) + outer = [] + inner = [] + joined = list(bits) + for k, b in enumerate(bits): + if not is_const(b) and b not in touched: + # a bit only tiles share stays in TOP + continue + o = new_bit() + outer.append(o) + joined[k] = o + if is_const(b): + # a constant from inside the parent: a buffer of it, + # which synthesis turns into a tie cell + y = new_bit() + cells["$tie$%s$%s$%d" % (name, pin, len(inner))] = { + "type": "$pos", + "parameters": { + "A_SIGNED": "0", + "A_WIDTH": "1", + "Y_WIDTH": "1", + }, + "port_directions": {"A": "input", "Y": "output"}, + "connections": {"A": [b], "Y": [y]}, + } + inner.append(y) + else: + inner.append(b) + port = "%s__%s" % (name, pin) + ports[port] = { + "direction": "output" if d == "input" else "input", + "bits": inner, + } + rest_conn[port] = outer + conn[pin] = joined + new_tiles[name] = dict(cell, connections=conn) + + netnames = { + n: v + for n, v in t.get("netnames", {}).items() + if all(is_const(b) or b in touched for b in v["bits"]) + } + mods[parent] = { + "attributes": {}, + "ports": ports, + "cells": cells, + "netnames": netnames, + } + inst = { + "type": parent, + "parameters": {}, + "attributes": {}, + "port_directions": {n: p["direction"] for n, p in ports.items()}, + "connections": rest_conn, + } + top_nets = {n: v for n, v in t.get("netnames", {}).items() if n in t["ports"]} + mods[top] = { + "attributes": t.get("attributes", {}), + "ports": t["ports"], + "cells": dict(new_tiles, u_rest=inst), + "netnames": top_nets, + } + return design + + +def main(argv): + src, out, top, parent = argv + with open(src) as f: + design = json.load(f) + with open(out, "w") as f: + json.dump(wrap(design, top, parent), f, indent=1) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))