Repository navigation
Add opt_design-style netlist constant propagation and sweep - #1468
Draft
clavin-xlnx wants to merge 20 commits into
Draft
clavin-xlnx wants to merge 20 commits into
clavin-xlnx wants to merge 20 commits into
Conversation
Add netlist-level equivalents of Vivado's opt_design -propconst and -sweep, run directly on an EDIFNetlist: - EDIFTools.propagateConstants() ~ opt_design -propconst - EDIFTools.sweepDeadLogic() ~ opt_design -sweep - EDIFTools.optimizeNetlist() ~ opt_design -propconst -sweep Constant propagation simplifies LUTs (constant, duplicate and unused inputs; buffers removed), folds flip-flops, latches and shift registers that can only hold one value, and absorbs inverters into the LUTs and invertible pins (IS_*_INVERTED) they drive. Remaining inverters are pulled back into the LUT that drives them, as Vivado's post-processing does. Sweep removes logic that reaches no top-level output, preserved (DONT_TOUCH/MARK_DEBUG) object or black box, then empty hierarchy. DONT_TOUCH on a hierarchical instance makes its ports an optimization boundary and protects its contents from sweep. Modified cells get the OPT_MODIFIED property, and EDIFOptStats counts changes the same way as Vivado's Opt 31-389 messages. The analysis runs on EDIFLeafIndex, a new flat, integer-indexed view of leaf connectivity built in parallel per cell definition. EDIFOptEditor then applies the decisions to the hierarchical netlist. Shared cell definitions are copied (<cell>_HD<n>) only where their instances need different edits, and ports are added through the hierarchy where a moved pin needs them. Also add, in edif.compare: - EDIFLeafNetlistComparator, a leaf-level netlist diff (type, properties, pin drivers) - EDIFRandomSimEquivalence, a bit-parallel random-simulation equivalence checker EDIFNetlistOptimizer is a command-line entry point. All three are registered in MainEntrypoint. The rules were referenced from Vivado 2025.1 as a black box. 106 probe netlists in OptProbes, with Vivado's results in test/resources/edif/opt_probes_expected.txt, are checked by TestEDIFTools for a matching result, idempotence and equivalence. On 10 post-synthesis Versal benchmarks (0.5M-3.2M leaf cells), 6 results match Vivado's cell for cell. The others differ only in OPT_MODIFIED tags or, with encrypted IP, at the IP boundary. All are equivalent under random simulation. The optimization runs 7.5-52x faster than Vivado's opt_design. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
…weep Fixes four correctness bugs, one performance issue and a gap in the equivalence checker, all found in code review. Each has a regression test in TestEDIFTools that fails without the fix. EDIFConstPropEngine: - Consolidate connectivity into a single layer. A pin is on exactly one net at a time: the net it was moved to, or else its original net. Nets merged by aliasing form a group. Only movePin/dropPin/setRemoved/ addAlias change connectivity, and they keep a live-load count for each group. Lut.net[] and Lut.moved[] are removed; LUT input nets are always pinNet(srcPin[i]). - Visit pins moved onto every net of an alias group, not just the root, and handle pins moved more than once. Previously a flip-flop reset absorbed into a net that was later aliased dropped out of load tracking, so its driver's net was removed and the pin disconnected. - Make hasLoads() O(1). Sweep no longer rescans high-fanout nets once per removed load: 100k dead loads now take under 1s instead of 36s. - Sweep keeps drivers and bidirectional pins of opaque nets live even when the net has no unidirectional driver, so an IOBUF driving a top-level inout port is no longer removed. - Add checkConsistency(), asserted at the end of run(). - Share one setup path for the getLut() cases. EDIFOptEditor: - Replace the generic Op(a, b, c, nets) with typed edit records (RemoveCell, RebuildLut, SetProperty, MovePin, MergeNets, AddStaticSource, NewPort, ConnectChild, RemoveNet). The delimiter- encoded LUT output pins are gone. - When a buffer is removed, merge its output into the net its input pin is currently on (currentNetName). Previously the output merged into the input's original net, so partial inverter absorption could invert an output. - Generated port/net names are now unique against the existing ports, bus names and nets of the cell and against names already generated for that hierarchical instance. Previously "<inst>_<port>" could reuse an existing net and short unrelated signals. - Create the nets of new child connections before merges are applied, because merges can refer to them. EDIFRandomSimEquivalence: - Cells that are not simulated and exist in both netlists must have the same type, the same properties (except OPT_MODIFIED and IS_*_INVERTED, which the simulation already models) and the same connected inputs. A pin on an undriven net counts as unconnected. Previously a disconnected reset, a changed INIT or a changed cell type went unreported. Validated on the 10 versal_multislr designs. Leaf diffs against Vivado are unchanged, random-simulation equivalence finds 0 mismatches, and Vivado opt_design on the output makes no further changes (as before). Optimizer runtime is unchanged or slightly faster. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Fixes three issues found reviewing eaba956. Each has a regression test in TestEDIFTools that fails without the fix. EDIFOptEditor: - Fix a partial inverter absorption across hierarchy that disconnected a top-level output. To find a net's local copy in a cell, the editor also looked at nets aliased into it during analysis. Those nets are only joined to the target by the merges being generated, so a removed buffer's output could be merged into itself (MergeNets(z, z)). Cleanup then removed the emptied child instance and the output with it. The lookup (ownLocalNet) now only uses segments of the alias group's root net, or nets routed from it through new ports. EDIFConstPropEngine: - Make moving a pin a second time O(1). Each moved pin records its position in its target net's moved-pin list, and removal swaps the last entry into its slot. IntList.removeValue() did a linear search, so two inverter stages partially absorbing into the same large set of loads took quadratic time. With 250k loads moved twice, constant propagation drops from 3.7s to about 0.2s. checkConsistency() also checks the recorded positions. EDIFRandomSimEquivalence: - Compare the properties of cells that are not simulated by their exact string values. Only Verilog numbers (compared by value) and TRUE/FALSE (case-insensitive) are normalized. Upper-casing every value hid changes such as INIT_FILE /tmp/a.mem -> /tmp/A.mem, which name different files on a case-sensitive filesystem. Tests: the hierarchy reproducer (with and without sweep), a linear-time test for double moves in both leaf orders, and a RAM INIT_FILE case change. The double-move and hierarchy cases are also added to the connectivity invariant test. Validated on the 10 versal_multislr designs. Leaf diffs against Vivado are unchanged, random-simulation equivalence finds 0 mismatches, and Vivado opt_design makes no further changes to our output, as before. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
…type When comparing cells that are not simulated, property values that looked like booleans or Verilog numbers were normalized whatever the property meant. A RAM whose INIT_FILE changed from "true" to "TRUE" (or from "8'h1" to "8'h01") was therefore not reported. Those are different relative file names on a case-sensitive filesystem. Values are now normalized according to their declared type. The type comes from the primitive's parameter list (Design.getDefaultCellProperties for the netlist's series), or from the EDIF value type for integer and boolean values: - HEX, BINARY and INT are compared by numeric value. - BOOL accepts TRUE/FALSE, 1/0 and 1'b1/1'b0. - DOUBLE is compared as a number. - Everything else is compared exactly. This includes STRING properties such as INIT_FILE and enumerations, properties of unknown primitives, and all properties when the part cannot be identified. The test now checks that file names resembling booleans or numbers are compared exactly, and that a declared HEX property written in different radixes compares equal while a different value is still reported. Random-simulation equivalence still finds 0 mismatches on the 10 versal_multislr designs, checking the same number of observations as before. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
…itives Fixes two regressions from type-aware property normalization (288b1b2). - Declared parameter types were looked up by cell name alone, so a user-defined black box named like a primitive (e.g. FDRE in the work library) was normalized with the primitive's types and a STRING INIT change from "0" to "1'b0" went unreported. Declared types now apply only to cells of the primitives library. Other cells use their EDIF value types and compare STRING values exactly. - An EDIF PART missing from the part database made PartNameTools.getPart() throw, so the comparison never ran. An unknown part now only disables declared types. Two more checks matched cells by bare name and now also distinguish primitives from user-defined cells. When a cell exists in both netlists, its type is qualified by library unless it is a primitive, so a user FDRE no longer matches the primitive FDRE. Only primitives are treated as removed constant flip-flops or shift registers. Tests: a user-defined FDRE with a changed STRING INIT, a user cell replacing the primitive of the same name, and identical and modified netlists with an unknown part. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Moves the constant propagation / sweep implementation out of the edif package into com.xilinx.rapidwright.edif.optimizer: EDIFLeafIndex, EDIFConstPropEngine, EDIFOptEditor, EDIFOptOptions, EDIFOptStats and EDIFNetlistOptimizer. - EDIFNetlistOptimizer.optimize() now holds the pipeline (index -> analysis -> edits -> cleanup). EDIFTools.propagateConstants(), sweepDeadLogic() and optimizeNetlist() keep their signatures and delegate to it. - The editor no longer uses package-private EDIF methods. Batched net edits use the list returned by EDIFNet.getPortInsts() (as documented for addPortInst(pi, deferSort)), and pins are added to cell instances in sorted order. This also fixes ports added to child instances, whose pin lists were never re-sorted. - EDIFCellInst.removePortInst(EDIFPortInst) is now public, like EDIFNet.removePortInst(). - Tests move to edif/optimizer: the optimizer tests from TestEDIFTools become TestEDIFNetlistOptimizer, along with OptProbes, the probe/debug utilities and opt_probes_expected.txt. TestEDIFTools is back to its previous contents. - The compare package and MainEntrypoint only change imports. No functional change: on the versal_multislr designs the optimized netlists have the same content as before. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Fixes one correctness bug in the optimizer, two in cleanup and performance, and two in the comparison tools. Each has a regression test in TestEDIFNetlistOptimizer that fails without the fix. EDIFConstPropEngine / EDIFOptEditor: - Fix a LUT6_2 output bypass that could change another cell's input polarity. The editor merged the bypassed output into the net its input pin was on at the end of the analysis. When the LUT survived through its other output, inverter absorption could move that pin afterwards, so the bypass connected to the wrong signal (!b became b). Alias creation now goes through one helper, bypass(), which records the target net. The input pin is kept only if its own net was already in the target group at that time. - Make buffer removal linear for buffers sharing an input. Each merge re-evaluated every load of the growing destination group. Now only the loads of the smaller group are re-evaluated (a load that can gain a duplicate input has a pin in both groups), using a per-group pin count that checkConsistency() also checks. 64k buffers sharing an input: 44s -> ~0.2s. - Remove hierarchy emptied by the optimization. Cleanup removed the last nets of a swept child before removing empty instances, and the emptied definition then looked like a black box, so it was kept and protected in later runs. The editor now records the hierarchical definitions (including uniquified copies) and keeps treating them as hierarchical. EDIFRandomSimEquivalence / EDIFLeafNetlistComparator: - Treat a removed shift register as constant only if all its INIT bits are equal. Otherwise it keeps pseudo-random values. Using INIT bit 0 alone accepted a wrong replacement of an SRL16E with INIT=16'h0002. - Reset per-comparison state (counts, examples, diffs) on each call to check() and compare(), and clear the declared-type cache when the device series changes. Shared literal decoding: - Add VerilogLiterals (parseNumber, parseBoolean, isTrue) and use it for the optimizer's isTrue(), the simulator's attrTrue(), the checker's number and boolean normalization and the comparator's DONT_TOUCH / MARK_DEBUG / KEEP values. The simulator now accepts literals such as 1'h1 and YES like the optimizer. Evaluation logic stays independent. Validated on the 10 versal_multislr designs. Leaf diffs against Vivado are unchanged, random-simulation equivalence finds 0 mismatches, and Vivado opt_design makes no further changes to our output, as before. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
…rsing EDIFConstPropEngine: - Reconnect a LUT input pin that simplification uses again after it was dropped. Removing a buffer can merge two input nets of a LUT, so the re-simplified function may represent the merged variable by a pin that an earlier pass dropped as unused. updateDropped() only ever dropped pins, so that pin stayed disconnected. Its driver could then lose its last load and be swept: q = a ^ b ^ c ended with the final LUT's I0 disconnected and the driving LUT removed. updateDropped() now sets each input pin's dropped state exactly, through a new restorePin() in the connectivity layer that keeps the live-load counts consistent. VerilogLiterals: - Parse signed decimal numbers again (-1 == -01, +1 == 1); the previous commit only accepted unsigned digits. - Give sized and signed Verilog literals their Verilog meaning: a sized literal is truncated to its width and a signed literal is two's complement of its width (32 bits if unsized). 8'shff is -1, so it no longer compares equal to 255 or 8'hff in the equivalence checker. Tests: the a ^ b ^ c reproducer (with and without sweep, also in the connectivity invariant cases), signed and sized literal decoding, and the checker's comparison of signed integer properties. Validated on the 10 versal_multislr designs. Leaf diffs against Vivado are unchanged, random-simulation equivalence finds 0 mismatches, and Vivado opt_design makes no further changes to our output, as before. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
…t width Fixes two regressions from signed literal parsing (aef7479). VerilogLiterals: - Unsized literals were all given a width of 32 bits, so a signed literal needing more bits was truncated: 'sh100000000 parsed as 0 instead of 4294967296 (Vivado gives it 36 bits). An unsized literal is now as wide as its digits need, and at least 32 bits. A decimal value gets one more bit, so it stays non-negative. - Add parseWidth(), the explicit or inferred width of a literal, and toWidth(), the bits a number assigns to a parameter of a given width (truncated, or sign-extended if negative). EDIFRandomSimEquivalence: - Compare HEX and BINARY parameters by the bits they assign at the parameter's width. The width comes from the sized default in the primitive's parameter list, else from the literal itself. Comparing signed values directly reported 36'shfffffffff != 36'hfffffffff for the 36-bit RAMB36E5.SRVAL_A. INT parameters are still compared as signed integers. Tests: inferred widths and values of unsized literals, and RAMB36E5 SRVAL_A comparisons. 'sh100000000 vs 36'h000000000 mismatches; 36'shfffffffff == 36'hfffffffff; 8'shff (sign-extended) equals 36'hfffffffff while 8'hff (zero-extended) does not. Random-simulation equivalence still finds 0 mismatches on the 10 versal_multislr designs, checking the same number of observations as before. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Width inference for unsized literals (af663b3) gave a decimal literal the bits its value needs plus a sign bit, so signed decimals stayed positive: 'sd2147483648 parsed as 2147483648 and 'sd4294967295 as 4294967295. Vivado's simulator gives -2147483648 and -1. As a result, RAMB36E5.SRVAL_A 'sd2147483648 (which sign-extends to 36'hf80000000) compared equal to 36'h080000000. As in Vivado, an unsized decimal literal is now always 32 bits wide: larger values are truncated, and signed values are two's complement. Width inference from the number of digits (leading zeros included) now applies only to unsized binary, octal and hex literals. Tests: values and widths of 12 unsized literals as given by xsim 2025.1 ('sd/'d around 2^31, 2^32 and 2^33, 'sh/'sb/'so wider than 32 bits), and the SRVAL_A comparisons against 36'h080000000 (mismatch) and 36'hf80000000 (equal). Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Fixes two optimizer issues and two comparison-tool gaps. Each has a regression test in TestEDIFNetlistOptimizer that fails without the fix. PrimitiveDefaults (new): - Shared table of the primitive property defaults that both the optimizer and the comparison tools evaluate (from the unisim models): INIT of FDRE/FDCE/LDCE (0), FDSE/FDPE/LDPE (1), SRL16E/SRLC32E (0), and IS_*_INVERTED (0). It applies only to cells of the primitives library. - EDIFRandomSimEquivalence assumed INIT=0 when a removed flip-flop omitted it, so replacing an FDSE/FDPE/LDPE held at its default (1) by ground was accepted. The optimizer was correct only through an implicit fallback to the set/reset value. The optimizer (flip-flop and SRL INIT, pin inversions) and the simulator now both read these properties through PrimitiveDefaults. A test checks the table against the declared defaults of the Versal primitives. EDIFOptEditor: - Apply net merges in linear time. Each merge moved all pins of its source net, so along a buffer chain the accumulated loads moved again at every stage (32k stages: 59s). The merges of a cell are now collected into a union-find forest first, and each merged net's pins move once to their final net. The two root-finding routines are replaced by one rootOf() with path compression. - Keep preserved static sources. Cleanup removed a DONT_TOUCH or MARK_DEBUG GND/VCC once its net lost its last load. As Vivado does, the cell now stays (its net is still removed). EDIFLeafNetlistComparator / EDIFRandomSimEquivalence: - Compare the top-level interfaces (port names, directions and widths) and the outputs of both netlists. An extra connected output in the candidate netlist was not reported by either tool. Validated on the 10 versal_multislr designs. Leaf diffs against Vivado are unchanged, random-simulation equivalence finds 0 mismatches, and Vivado opt_design makes no further changes to our output, as before. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
parseBoolean() is called for every pin inversion and flip-flop INIT during constant propagation. Matching the usual values (1'b0, 1'b1, 0, 1, TRUE/true, FALSE/false) before the general Verilog-number parser brings constant propagation back to the first commit's speed (it took about 30% longer since the shared literal decoder was introduced). Signed-off-by: Chris Lavin <chris.lavin@amd.com>
- Sweep marked the drivers of a net live once per live load. For a net with several drivers, bidirectional pins or DONT_TOUCH/MARK_DEBUG, that rescanned the whole net each time, which is quadratic in the fanout of a preserved net (the run did not finish in an hour). Each net is now handled once. - To find a net's local copy in a hierarchical instance, the editor scanned all of the net's segments, which is quadratic for nets that reach many instances. Such nets are now indexed once (hierarchical instance -> segment). Generated port names also restarted their numbered suffix at 1 each time; the next free suffix is now remembered. Apply edits on the large netlist: 130s -> 13s. The optimizer now scales linearly: 157s for 25.5M cells (2.4M cells, takes 18s). Regression tests: 100k-load DONT_TOUCH net (sweep 4.8s -> <1s) and a net reaching 20k instances of a shared cell (apply 7.2s -> <1s). Signed-off-by: Chris Lavin <chris.lavin@amd.com>
The simulation model looked up a net's driver by scanning all pins of the net, once per load (building the observed cone, the evaluation order and the constant-net check). That is quadratic in fanout: on a 25.5M-cell netlist with clock and reset nets of very high fanout, the check was still building its observed cone after 9 hours. The first driver pin of every net is now found in a single pass over the pins. Regression test: a LUT driving 150k LUT+flip-flop loads checks in a few seconds (about 45s before). Signed-off-by: Chris Lavin <chris.lavin@amd.com>
A removed flip-flop is checked by assuming every removed sequential cell holds its constant, then checking its next state. A clock was only taken as never ticking when tied directly to GND/VCC, so on a large netlist the checker reported 2,570 false mismatches from correctly removed stopped ripple counters. Their first stage is clocked by VCC, and each next stage by the previous one, sometimes through an inverter or a clock gate with a constant-0 enable. - A net never changes if it is driven by a static source or by a removed cell assumed constant, or by simulated logic that does not depend on its non-fixed inputs given its fixed ones. A LUT is evaluated with the known values; other cells need all inputs fixed. This is structural, not based on simulated values: a net such as a wide AND of random inputs would look constant. - A flip-flop clocked by such a net sees no edges. - The classification repeats until nothing more is dropped. It used to stop after 20 rounds, possibly leaving unchecked assumptions. It also no longer removes from a list quadratically. - -Drapidwright.equiv.debug prints each removed cell found not constant and why. The large netlist (25.5M cells) is now equivalent: 163M observations, 0 mismatches. Benchmarks are unchanged. Regression test: stopped ripple counters with a tied, inverted or gated clock. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
EDIFOptOptions.setReport(Writer), or -report <file> on the command
line, writes one tab-separated line per decision (action, object, cell
type, explanation), then the number of each action. Off by default.
- Actions: REMOVED_CONSTANT, REMOVED_BUFFER, REMOVED_INVERTER,
REMOVED_UNLOADED, SWEPT, LUT_RESIZED, LUT_CHANGED, PIN_INVERTED,
PIN_MOVED, MARKED, NET_DISCONNECTED, SOURCE_ADDED, DEFINITION_COPIED,
HIERARCHY_REMOVED.
- Explanations:
- why a flip-flop, latch or SRL can only hold one value;
- each constant input of a LUT and the cell driving it, so a chain can
be followed back to its source;
- where a removed buffer or inverter was merged;
- for each LUT input: kept, dropped as constant, as a duplicate, or as
unused, or moved after absorbing an inverter.
- Names are those of the input netlist. Edits to a cell definition are
written as definition:instance.
- The engine keeps the few reasons it cannot rebuild from its final
state (sequential cells, absorbed and pulled-back inverters) only when
a report is requested. The rest is derived when the report is written,
before the netlist is edited.
On every probe, the removed lines, resized LUTs and removed hierarchy
(with nested instances) add up to the cells removed in the statistics.
On a 25.5M-cell netlist the report has 916k lines and adds 5% to
optimization time.
Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical comparison and simulation flaws can falsely report equivalence despite changed connections, cell types, properties, or behavior, while boundary-pin handling can remove driving logic and disconnect preserved instances.
7 open issues
Include bidirectional pins in net driver identities · New Compare qualified cell types and restrict LUT canonicalization · New Compare non-INIT properties before LUT fast path · New Require proof of constant async control before overriding INIT · New Handle vector inversion masks bit-by-bit · New Retain connected boundary pins on preserved hierarchy · New Batch orphan net pin removals to avoid quadratic fanout edits · New
What changed in this PR
Adds Vivado-style constant propagation and dead-logic sweep directly to RapidWright EDIF netlists, with hierarchy-aware editing and tools for validating optimization results.
Changes:
- Adds optimizer entry points, configurable phases, statistics, and optional change reports.
- Introduces shared connectivity indexing, primitive defaults, and literal decoding.
- Adds leaf-level comparison, random-simulation equivalence checking, and probe-analysis utilities.
| File | Description |
|---|---|
test/src/com/xilinx/rapidwright/edif/optimizer/OptProbeCheck.java |
Compares optimized probes with Vivado reference outputs. |
test/src/com/xilinx/rapidwright/edif/optimizer/OptNetlistStats.java |
Prints netlist structure and preservation statistics. |
test/src/com/xilinx/rapidwright/edif/optimizer/OptMarkAnalysis.java |
Analyzes modification tags on surviving loads. |
test/src/com/xilinx/rapidwright/edif/optimizer/OptDebug.java |
Prints optimizer classifications for selected cells. |
test/resources/edif/optimizer/opt_probes_expected.txt |
Stores expected optimization results for probes. |
src/com/xilinx/rapidwright/MainEntrypoint.java |
Registers optimizer and comparison commands. |
src/com/xilinx/rapidwright/edif/optimizer/VerilogLiterals.java |
Decodes numeric and boolean property values. |
src/com/xilinx/rapidwright/edif/optimizer/PrimitiveDefaults.java |
Supplies shared primitive property defaults. |
src/com/xilinx/rapidwright/edif/optimizer/EDIFOptStats.java |
Tracks phase counts and timings. |
src/com/xilinx/rapidwright/edif/optimizer/EDIFOptReport.java |
Reports optimization decisions and their causes. |
src/com/xilinx/rapidwright/edif/optimizer/EDIFOptOptions.java |
Configures optimization phases and reporting. |
src/com/xilinx/rapidwright/edif/optimizer/EDIFOptEditor.java |
Applies hierarchy-aware edits and cleanup. |
src/com/xilinx/rapidwright/edif/optimizer/EDIFNetlistOptimizer.java |
Coordinates optimization and provides the command line. |
src/com/xilinx/rapidwright/edif/optimizer/EDIFLeafIndex.java |
Indexes leaf connectivity and preservation boundaries. |
src/com/xilinx/rapidwright/edif/EDIFTools.java |
Exposes convenience APIs for optimization. |
src/com/xilinx/rapidwright/edif/EDIFCellInst.java |
Makes port-instance removal public. |
src/com/xilinx/rapidwright/edif/compare/EDIFRandomSimEquivalence.java |
Checks equivalence using bit-parallel random simulation. |
src/com/xilinx/rapidwright/edif/compare/EDIFLeafNetlistComparator.java |
Compares leaf types, properties, functions, and connections. |
Review effort: Balanced
…ckers EDIFLeafIndex: - A port of a preserved (DONT_TOUCH) instance connected on only one side (e.g. an input driven outside but unused inside) got no boundary pin, so sweep removed its driver and cleanup disconnected the port. Each connected side now gets its own pin. This matches Vivado 2025.1, which keeps the driver and both connections. EDIFOptEditor: - Disconnect the pins of a removed orphan net through the existing batch, instead of one at a time while the removals of its loads were pending, which was quadratic in fanout (400k loads: 7.4s -> 2.0s). EDIFLeafNetlistComparator: - Include bidirectional pins in a net's driver identity, so swapped I/O buffer to INOUT port connections are reported. - Qualify user-defined cell types by library, and canonicalize LUT functions only for primitives. - Compare the other properties of equivalent LUTs of different sizes. EDIFRandomSimEquivalence: - Assume a removed flip-flop or latch is held by its asynchronous clear/preset only if that control is on a net that never changes, rechecked every round; a value sampled in one round is no proof. - Compare the inversion masks of bus pins (e.g. IS_ALUMODE_INVERTED), which are not simulated; only single-bit pin inversions are ignored. Each fix has a regression test. Benchmark results are unchanged. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Signed-off-by: Chris Lavin <chris.lavin@amd.com>
…ying Each copy of a shared definition searched for a free <cell>_HD<n> name starting from 1, which is quadratic when one definition needs many different copies (20k copies: 12.3s in Apply edits). The next suffix is now kept per definition; existing names are still skipped (20k copies: 0.34s). Found by a GPT Sol review. Regression test: 20k instances of a shared cell, each absorbing a different inverter. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical issues can delete protected hierarchy and falsely report equivalence by ignoring output inversions, mishandling multiple drivers, or accepting non-positive simulation rounds.
7 open issues
Reject or resolve multiple drivers during comparison · New Reject non-positive simulation round counts · New Keep output inversion properties in cut-cell comparison · New Preserve hierarchy containing protected nets · New Propagate preservation into nested hierarchy cleanup · New Use indexed port lookup to avoid quadratic scans · New Count shared-definition removals per retained instance · New
7 resolved since last review
Retain connected boundary pins on preserved hierarchy Handle vector inversion masks bit-by-bit Require proof of constant async control before overriding INIT Compare non-INIT properties before LUT fast path Compare qualified cell types and restrict LUT canonicalization Include bidirectional pins in net driver identities Batch orphan net pin removals to avoid quadratic fanout edits
Review effort: Balanced
EDIFOptEditor: - Keep a hierarchical cell that only contains a DONT_TOUCH/MARK_DEBUG net; cleanup removed it (and the net) as empty. - Leave definitions instantiated, even indirectly, under a preserved instance untouched when removing empty hierarchy. - Count each removed hierarchical instance once per instance of its parent definition, as Vivado does (an empty child of a cell with two instances: Sweep removed 2). EDIFLeafIndex: - Find the inner port instance of a preserved instance's port with the indexed lookup instead of scanning the inner net per port. EDIFRandomSimEquivalence: - Combine the values of all drivers of a net (XOR), so a change in the drivers of a multi-driven net (e.g. tri-state buffers) is seen; drivers used to overwrite each other. - Reject a non-positive number of rounds instead of reporting equivalence without simulating. - Compare output pin inversion properties; only single-bit input pin inversions are simulated, so only those are exempt. Each fix has a regression test. Benchmark results are unchanged. Signed-off-by: Chris Lavin <chris.lavin@amd.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Add opt_design-style netlist constant propagation and sweep
Summary
This adds RapidWright's equivalent of two Vivado
opt_designphases, run directly on anEDIFNetlist:-propconst)-sweep)On a set of post-synthesis Versal benchmarks, the result matches Vivado's at the leaf-cell level:
opt_designagain on the output makes no furtherchanges.
opt_design.Runtime and memory grow linearly with netlist size. This was checked up to a 25.5M leaf-cell
netlist.
Running both optimizations in a single command
Constant propagation and sweep run together by default. They share one connectivity index, and
all netlist edits are applied in a single pass.
Command line:
rapidwright EDIFNetlistOptimizer input.edf output.edf # The same, with both optimizations named explicitly: rapidwright EDIFNetlistOptimizer input.edf output.edf -propconst -sweep-propconstor-sweepalone runs only that optimization.-no_push_invertersand-no_post_processdisable those steps..edf.gz).Java:
EDIFTools.propagateConstants(netlist)andEDIFTools.sweepDeadLogic(netlist)run oneoptimization each.
Report: what changed and why
The optional report explains every change. It is off by default.
counts per action at the end.
cell definition are written as
definition:instance.of a constant input, so a chain can be followed back to its source.
Examples (the names are illustrative):
What it does
The rules follow Vivado's observed behavior, learned from small black-box probe netlists. The full
specification is in the
EDIFConstPropEngineJavadoc.order.
and IS_*_INVERTED are taken into account.
(IS_*_INVERTED), but never when they drive a top-level or preserved port. Post-processing also
pulls inverters back into a driving LUT.
are removed.
contents.
<cell>_HD<n>) only whentheir instances need different edits.
OPT_MODIFIED(PROPCONST,SWEEP,POST_PROCESS_NETLIST), asin Vivado.
simplified again. Running the optimizer a second time can therefore still find a few changes:
about 0.01–0.02% of cells on the largest netlist tested, and none on the benchmarks.
Implementation
New package
com.xilinx.rapidwright.edif.optimizer:EDIFLeafIndexEDIFConstPropEngineEDIFOptEditorEDIFNetlistOptimizeroptimize()) and command lineEDIFOptOptions,EDIFOptStatsEDIFOptReportPrimitiveDefaults,VerilogLiteralsNotes:
per-net live-load counts.
checkConsistency()checks these invariants (asserted in tests).preserved nets in sweep, shared-input buffers, buffer chains, repeated inverter moves, nets
reaching many hierarchical instances, and high fanout in the equivalence checker.
New comparison tools in
com.xilinx.rapidwright.edif.compare:EDIFLeafNetlistComparator: a leaf-level diff of two netlists, comparing cell types,properties, pin drivers, LUT functions and the top-level interface.
EDIFRandomSimEquivalence: a bit-parallel random-simulation equivalence check.behavior-defining properties and connected inputs.
or gated off by logic, e.g. a stopped ripple counter or a clock gate with a constant-0 enable.
-Drapidwright.equiv.debugprints each removed cell found not constant, and why.Both tools, and the optimizer, are registered in
MainEntrypoint.Changes to existing code:
EDIFTools:propagateConstants(),sweepDeadLogic()andoptimizeNetlist()were added.EDIFCellInst.removePortInst(EDIFPortInst): this goes from protected to public, matchingEDIFNet.removePortInst. It is the only change to an existing API.Validation
Ten post-synthesis Versal benchmark designs (0.7M–3.2M leaf cells) were compared against Vivado
2025.1
opt_design -propconst -sweep:it. All diffs on those designs are on the IP boundary.
under random simulation.
TestEDIFNetlistOptimizer.test/resources/edif/optimizer/opt_probes_expected.txt.tests, and regression tests for every issue found in review.
./gradlew testJava --tests 'com.xilinx.rapidwright.edif.optimizer.*'Performance
Against Vivado 2025.1 on the benchmarks:
opt_designsetup (timing analysis and similar).opt_design(s)Opening, optimizing and writing EDIF end to end is 8–50× faster than in Vivado.
Scaling to large netlists
The optimizer itself adds about 42 GB of live heap on top of the parsed netlist: the peak live heap
is 106 GB after reading and 148 GB during optimization.
At this size, reading and writing EDIF dominate the end-to-end time, and the parsed netlist
dominates memory. Two separate open PRs improve those steps. All three variants were run on the same
host (384 cores,
-Xmx350g), with the optimizer unchanged:edif-parser-speedupseriesalone it was 276–1812 s in other runs.
is 24% lower.
connectivity index from the edited netlist.
This PR does not depend on either one.
On this netlist, the optional report adds 5% to optimization time (171 s vs 163 s). It has 916k
lines (376 MB).