Skip to content

Add opt_design-style netlist constant propagation and sweep - #1468

Draft
clavin-xlnx wants to merge 20 commits into
masterfrom
opt-constprop-sweep
Draft

clavin-xlnx wants to merge 20 commits into
masterfrom
opt-constprop-sweep

Conversation

@clavin-xlnx

Copy link
Copy Markdown
Member

Add opt_design-style netlist constant propagation and sweep

Summary

This adds RapidWright's equivalent of two Vivado opt_design phases, run directly on an
EDIFNetlist:

  • Constant propagation (-propconst)
  • Dead logic elimination / sweep (-sweep)

On a set of post-synthesis Versal benchmarks, the result matches Vivado's at the leaf-cell level:

  • Same cells: the same surviving leaf cells, cell types, LUT INITs and pin connections.
  • Same function: it is functionally equivalent to the input under random simulation.
  • Nothing left to do: running Vivado opt_design again on the output makes no further
    changes.
  • Speed: it is 8–52× faster than Vivado's 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
  • -propconst or -sweep alone runs only that optimization.
  • -no_push_inverters and -no_post_process disable those steps.
  • The input may be gzipped (.edf.gz).
  • The tool prints Vivado-style per-phase cell counts and timings.

Java:

EDIFNetlist netlist = EDIFTools.readEdifFile(Paths.get("input.edf"));
EDIFOptStats stats = EDIFTools.optimizeNetlist(netlist, new EDIFOptOptions()); // both by default
System.out.print(stats);
netlist.exportEDIF(Paths.get("output.edf"));

EDIFTools.propagateConstants(netlist) and EDIFTools.sweepDeadLogic(netlist) run one
optimization each.

Report: what changed and why

The optional report explains every change. It is off by default.

rapidwright EDIFNetlistOptimizer input.edf output.edf -report changes.tsv
try (Writer w = Files.newBufferedWriter(Paths.get("changes.tsv"))) {
    EDIFTools.optimizeNetlist(netlist, new EDIFOptOptions().setReport(w));
}
  • Format: one tab-separated line per decision (action, object, cell type, explanation), with
    counts per action at the end.
  • Names: leaf cells and nets use their hierarchical names in the input netlist. Edits to a
    cell definition are written as definition:instance.
  • Provenance: an explanation names the cell or net that caused the change, such as the driver
    of a constant input, so a chain can be followed back to its source.
  • Totals: the report accounts for every cell counted as removed in the statistics.

Examples (the names are illustrative):

REMOVED_CONSTANT  u/k        LUT2   O constant 0: constant inputs I1=0 from u/GND/G (GND)
REMOVED_CONSTANT  u/f        FDRE   Q constant 0: holds INIT=0, R never active, CE constant 0
REMOVED_CONSTANT  u/cnt[3]   FDPE   Q constant 1: holds INIT=1, PRE sets the same value, clock C constant (no edges)
REMOVED_BUFFER    u/b        LUT1   buffer; O merged into u/x (from I0)
REMOVED_INVERTER  u/iv       LUT1   inverter absorbed by all its 1 loads; O merged into u/r (from I0)
LUT_RESIZED       u/s        LUT4   LUT4 INIT=16'h4488 -> LUT2 INIT=4'h6: I0->I0, I1 dropped (constant 1 from VCC/P (VCC)), I2 dropped (function does not depend on it), I3->I1
PIN_INVERTED      u/g        FDRE   IS_R_INVERTED=1'b1, absorbed inverter u/iv
SWEPT             u/d        LUT2   no path to a top-level output, preserved cell or net, or black box
NET_DISCONNECTED  u/n        -      all its loads were removed; driver u/q/Q (FDRE) kept
DEFINITION_COPIED sub_HD1    sub    copy of sub for instances that need different edits, e.g. u0
SOURCE_ADDED      sub:GND_1  GND    drives net y, which became constant 0
HIERARCHY_REMOVED top:u2     sub2   no leaf cells left and no signal passing through

What it does

The rules follow Vivado's observed behavior, learned from small black-box probe netlists. The full
specification is in the EDIFConstPropEngine Javadoc.

  • LUTs:
    • Constant inputs, duplicate inputs and inputs the function ignores are removed.
    • A LUT becomes a constant, is removed as a buffer, or shrinks, with its remaining pins kept in
      order.
    • A LUT6_2 is rewritten only when one of its outputs is unused, constant or a buffer.
  • Flip-flops, latches and SRLs:
    • A flip-flop or latch is removed when it can only ever hold one value. CE, the clock, set/reset
      and IS_*_INVERTED are taken into account.
    • An asserted asynchronous clear or preset wins over INIT.
    • An SRL is removed when its data input is a constant equal to all of its INIT bits.
  • Inverters: absorbed into LUTs, LUT6_2, LUT6CY and pins with programmable inversion
    (IS_*_INVERTED), but never when they drive a top-level or preserved port. Post-processing also
    pulls inverters back into a driving LUT.
  • Sweep:
    • Cells that don't reach a top-level output, a preserved cell, net or instance, or a black box
      are removed.
    • A LOOKAHEAD8 fed by a live LUT6CY is kept.
    • Hierarchy left empty is removed.
  • DONT_TOUCH and MARK_DEBUG:
    • On a cell or net, they freeze it.
    • On a hierarchical instance, they make its ports an optimization boundary and protect its
      contents.
    • A preserved GND or VCC is kept even when it loses all its loads, as Vivado does.
    • KEEP and KEEP_HIERARCHY are ignored, as in Vivado.
  • Hierarchy: it is preserved. Shared cell definitions are copied (<cell>_HD<n>) only when
    their instances need different edits.
  • Marking: modified cells get OPT_MODIFIED (PROPCONST, SWEEP, POST_PROCESS_NETLIST), as
    in Vivado.
  • Known difference from a fixpoint: as in Vivado, a LUT that absorbs an inverter is not
    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:

Class Role
EDIFLeafIndex Flat integer index of leaf pins and physical nets across the hierarchy (public, reusable)
EDIFConstPropEngine Analysis only: decides what changes, without touching the netlist
EDIFOptEditor Turns the decisions into per-definition edits, copies shared definitions where needed, applies the edits, cleans up
EDIFNetlistOptimizer Entry point (optimize()) and command line
EDIFOptOptions, EDIFOptStats Options; Vivado-style per-phase statistics and timings
EDIFOptReport The optional per-decision report
PrimitiveDefaults, VerilogLiterals Primitive property defaults and literal decoding, shared by the optimizer and the checkers

Notes:

  • Connectivity in the engine: all connection changes go through a few methods that maintain
    per-net live-load counts. checkConsistency() checks these invariants (asserted in tests).
  • Scaling: every pass scales linearly with netlist size. Regression tests cover high-fanout and
    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.
    • LUT, carry, INV and BUF logic is simulated.
    • Other cells are cut points. Cut cells present in both netlists must have the same type,
      behavior-defining properties and connected inputs.
    • Removed sequential cells must be inductively constant, all checked together.
    • A clock that never changes is recognized: tied off, driven by a removed cell assumed constant,
      or gated off by logic, e.g. a stopped ripple counter or a clock gate with a constant-0 enable.
    • -Drapidwright.equiv.debug prints each removed cell found not constant, and why.

Both tools, and the optimizer, are registered in MainEntrypoint.

Changes to existing code:

  • EDIFTools: propagateConstants(), sweepDeadLogic() and optimizeNetlist() were added.
  • EDIFCellInst.removePortInst(EDIFPortInst): this goes from protected to public, matching
    EDIFNet.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:

Design Leaf diffs vs Vivado Random-sim equivalence Vivado opt_design re-run on our output
D1–D4, D6, D7 0 equivalent 0 changes
D5 7 (OPT_MODIFIED tags only) equivalent same small changes as on Vivado's own output
D8 50 (OPT_MODIFIED tags only) equivalent 0 changes
D9, D10 only pins on the boundary of encrypted IP equivalent same as on Vivado's own output (D9); not run (D10)
  • Encrypted IP: RapidWright sees encrypted IP as black boxes, while Vivado optimizes inside
    it. All diffs on those designs are on the IP boundary.
  • Large netlist: the 25.5M leaf-cell netlist below is also equivalent to its optimized version
    under random simulation.
    • 163M observations × 64 patterns, 0 mismatches.
    • Every removed sequential cell was confirmed constant.
  • Unit tests:
    • 485 tests in TestEDIFNetlistOptimizer.
    • Vivado reference results for about 100 small probe netlists, stored in
      test/resources/edif/optimizer/opt_probes_expected.txt.
    • Idempotence and equivalence checks for every probe, connectivity-invariant checks, scaling
      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:

  • Vivado was given 8 threads.
  • The total excludes Vivado's opt_design setup (timing analysis and similar).
Design Vivado propconst+sweep+post (s) RapidWright analysis (s) Vivado opt_design (s) RapidWright optimize (s) Speedup
D1 18.0 1.2 61.1 5.5 11×
D2 11.0 1.1 43.5 4.8 9×
D3 24.0 1.9 73.6 7.9 9×
D4 49.0 5.2 152.2 15.8 10×
D5 78.0 7.6 208.3 24.4 9×
D6 78.0 3.3 179.5 13.5 13×
D7 48.0 5.4 125.6 16.7 8×
D8 100.0 3.6 351.0 13.6 26×
D9 60.0 2.8 194.5 14.0 14×
D10 96.0 1.0 277.7 5.3 52×

Opening, optimizing and writing EDIF end to end is 8–50× faster than in Vivado.

Scaling to large netlists

Phase Largest benchmark (2.4M cells) 25.5M cells Linear projection
Build index 3.7 s 25 s ~40 s
Constant propagation 3.1 s 27 s ~33 s
Sweep 0.9 s 6 s ~10 s
Apply edits 0.9 s 13 s ~10 s
Cleanup 7.5 s 71 s ~80 s
Optimize total 18 s 157 s ~194 s

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:

Variant Read Optimize Write Total Peak live heap: read / optimize / write Peak process memory
This PR 845 s 165 s 133 s 1143 s 106 / 148 / 134 GB 254 GB
With #1444 (batched PortInst trim) 67 s 164 s 128 s 358 s 105 / 148 / 134 GB 259 GB
With #1466 and its edif-parser-speedup series 21 s 145 s 110 s 277 s 67 / 116 / 126 GB 193 GB

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

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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
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

Comment thread src/com/xilinx/rapidwright/edif/compare/EDIFLeafNetlistComparator.java Outdated
Comment thread src/com/xilinx/rapidwright/edif/compare/EDIFRandomSimEquivalence.java Outdated
Comment thread src/com/xilinx/rapidwright/edif/compare/EDIFRandomSimEquivalence.java Outdated
Comment thread src/com/xilinx/rapidwright/edif/optimizer/EDIFLeafIndex.java Outdated
Comment thread src/com/xilinx/rapidwright/edif/optimizer/EDIFOptEditor.java Outdated
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread src/com/xilinx/rapidwright/edif/compare/EDIFRandomSimEquivalence.java Outdated
Comment thread src/com/xilinx/rapidwright/edif/compare/EDIFRandomSimEquivalence.java Outdated
Comment thread src/com/xilinx/rapidwright/edif/optimizer/EDIFOptEditor.java
Comment thread src/com/xilinx/rapidwright/edif/optimizer/EDIFOptEditor.java
Comment thread src/com/xilinx/rapidwright/edif/optimizer/EDIFLeafIndex.java Outdated
Comment thread src/com/xilinx/rapidwright/edif/optimizer/EDIFOptEditor.java Outdated
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants