Skip to content

rsz: add inverter-pair insertion to the Rebuffer DP#10597

Open
minjukim55 wants to merge 2 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:secure-rsz-inv-pair-mvp-pr
Open

rsz: add inverter-pair insertion to the Rebuffer DP#10597
minjukim55 wants to merge 2 commits into
The-OpenROAD-Project:masterfrom
The-OpenROAD-Project-staging:secure-rsz-inv-pair-mvp-pr

Conversation

@minjukim55
Copy link
Copy Markdown
Contributor

Summary

Adds optional inverter-pair insertion to the van Ginneken Rebuffer DP, gated behind a new enable_inverter_pair command (off by default). When enabled, the DP may use inverter pairs as repeaters alongside buffers while preserving logical equivalence; with the flag off every inverter path is bypassed and the resizer is byte-identical to baseline.

This PR is a deliberately minimal MVP — just enough machinery to make inverter insertion correct and gated. The quality / robustness levers (cell curation, frontier separation, conservative selection, re-absorption) are intentionally left out and listed under Future work below; they will be added and tuned in follow-ups once the core mechanism is reviewed.

How it works

Legend: 🟧 new code  ·  🟦 modified existing function  ·  ⬜ unchanged.

Inverter pairs preserve polarity, so the whole feature is built around one cached quantity: parity, the inverter count between a node and its loads, mod 2 (0 = same polarity as the driver). The DP carries options of both parities through the tree and only commits even-parity trees at the root.

  1. Enable + collect. enable_inverter_pair sets a flag (inverter_pair_enabled_); on the first Rebuffer::init afterwards, findInverters collects the library's inverter cells into inverter_cells_.
  2. Candidate generation. Wherever the DP wraps an option with a buffer (insertBufferOptions), it now also wraps it with each inverter (insertInverterCandidates). The new option's slack is opt.slack - inverter_delay, its parity flips, and its slack transition is flipped (flipRiseFallBoth, rise↔fall) because the inverter inverts the edge the upstream stage sees.
  3. Per-parity frontier. prunePerParityFrontier keeps the p0 (even) and p1 (odd) Pareto frontiers separate, so a p0 option cannot Pareto-shadow a p1 one — a p1 option may pair into a better p0 further upstream and must survive.
  4. Same-parity junction merge. At a junction the DP merges only same-parity option pairs; merging across parity would drive the two branches of a shared node with opposite polarity and break logical equivalence.
  5. Root gate. isRootParityAccepted accepts only even-parity (p0) trees at the net driver, so any inverter must be paired before reaching the driver.
  6. Area recovery. recoverArea is parity-aware (same-parity junctions, parity-bucketed pruning); its exemplar-reproduction path can reproduce inverter nodes by checking the inverter's capacitance limit directly, since inverters are not in the buffer-size index.
  7. Export. exportBufferTree materializes the chosen buffers and inverters.

Everything above is gated on invPairActive() (the flag). When off, parity is always 0, no inverter candidates are generated, and every step collapses to the existing buffer-only behavior — hence the byte-identical OFF path.

Call stack

flowchart TD
  TCL["enable_inverter_pair (Resizer.tcl)"] --> SET["Resizer::setInverterPairEnabled"]
  SET --> FLAG(["inverter_pair_enabled_"])
  INIT["Rebuffer::init"] -->|"flag on"| FI["Resizer::findInverters"]
  FI --> CELLS(["inverter_cells_"])

  FRS["findResizeSlacks (placement)"] --> FR["Rebuffer::fullyRebuffer"]
  MOVE["repair_setup BufferMove"] --> RBP["Rebuffer::rebufferPin"]
  FR --> BFT["Rebuffer::bufferForTiming"]
  RBP --> BFT
  FR --> RA["Rebuffer::recoverArea"]
  RBP --> RA
  FR --> EXP["Rebuffer::exportBufferTree"]
  RBP --> EXP

  BFT --> IBO["insertBufferOptions"]
  BFT --> IIO["insertInverterOptions"]
  IIO --> IIC["insertInverterCandidates"]
  IIC --> FLIP["flipRiseFallBoth: rise <-> fall"]
  IIO --> PPF["prunePerParityFrontier: p0 / p1"]
  BFT --> JM["junction: same-parity merge only"]
  BFT --> ROOT["isRootParityAccepted: parity == 0 only"]
  RA --> IBO
  CELLS -.->|"flag on"| IIC

  classDef new fill:#fff3e0,stroke:#e65100,color:#000,stroke-width:2px
  classDef mod fill:#e1f5fe,stroke:#0277bd,color:#000
  classDef ext fill:#f5f5f5,stroke:#9e9e9e,color:#000
  class TCL,SET,FLAG,FI,CELLS,IIO,IIC,FLIP,PPF,ROOT new
  class INIT,FR,RBP,BFT,RA,EXP,IBO,JM mod
  class FRS,MOVE ext
Loading

Parity data model

parity is set once, bottom-up, in the BufferedNet constructors; the buffer constructor is the only place it flips.

flowchart LR
  L["load: parity = 0"] --> W["wire / via: copy ref parity"]
  W --> J["junction: copy left-child parity<br/>(merge is same-parity only)"]
  J --> B["buffer ctor: parity = isInverter ? ref^1 : ref<br/>(only flip point)"]
  B --> R{{"root: accept parity == 0 only"}}

  classDef base fill:#e8f5e9,stroke:#2e7d32,color:#000
  classDef flip fill:#fff3e0,stroke:#e65100,color:#000,stroke-width:2px
  classDef gate fill:#e3f2fd,stroke:#1565c0,color:#000
  class L,W base
  class J,B flip
  class R gate
Loading

Same-parity junction merge

flowchart LR
  LP0["left p0"] & RP0["right p0"] --> M0["merge -> p0"]
  LP1["left p1"] & RP1["right p1"] --> M1["merge -> p1"]
  M0 & M1 --> OUT["junction options"]

  classDef p0 fill:#e8f5e9,stroke:#2e7d32,color:#000
  classDef p1 fill:#fff3e0,stroke:#e65100,color:#000,stroke-width:2px
  class LP0,RP0,M0 p0
  class LP1,RP1,M1 p1
Loading

Future work (intentionally not in this MVP)

These quality / robustness pieces were dropped to keep the MVP small and reviewable, and are planned follow-ups:

  • Inverter-cell curation. findInverters currently collects all link-cell inverters. Restore the footprint/VT/site-aware 5-bucket × 2 selection (mirroring findBuffers) plus the input-cap symmetry guard that drops low-drive inverters which would otherwise skew the frontier inverter-heavy.
  • has_inverter frontier separation. Frontiers are split by parity only. Restore the parity-0 split into buffer-only vs inverter-bearing buckets (and never re-prune the buffer-only frontier), so a cheaper-looking inverter cannot Pareto-shadow a buffer option that is actually better after STA correction.
  • Conservative root selection. A single best option is chosen today. Restore separate best-buffer / best-inverter tracking and pick the inverter only on a strict slack gain (ties favor the buffer).
  • Inverter re-absorption on re-import. Inserted inverters are treated as fixed loads on later passes. Tag rebuffer-inserted inverters (e.g. an ODB property) so a subsequent rebuffer pass can strip and re-optimize them, the way it already does for buffers.
  • Estimate-vs-STA re-ranking. The DP prunes on an estimated slack, but the buffer-vs-inverter choice is only trustworthy after the STA correction. Re-ranking near-tie candidates by real STA would remove the need for the conservative bucket guard above (this is the root cause that motivates it).

Type

Enhancement (experimental, off by default).

Impact

  • rsz (resizer / Rebuffer DP) only.
  • No impact when the flag is off: every inverter path is gated on enable_inverter_pair, which defaults off. The OFF path is byte-identical to master.

Verification

  • New inv_pair1 regression test (CMake + Bazel registered): with the flag on, asserts inverters are inserted by both the fully_rebuffer (placement) and repair_timing (repair) entry points.
  • Existing rebuffer1 test passes unchanged -> OFF path byte-identical to baseline.
  • clang-format, clang-tidy, and tclint clean.

Related Issues

N/A

Adds optional inverter-pair candidate insertion to the van Ginneken
Rebuffer DP, gated behind `enable_inverter_pair` (off by default). With
the flag off the resizer is byte-identical to baseline.

When enabled, each BufferedNet tracks inverter parity; the DP generates
inverter-wrapped candidates next to buffer candidates, merges only
same-parity options at junctions, and accepts only even-parity
(polarity-preserving) trees at the root, so logical equivalence holds.

Adds the inv_pair1 regression test, which verifies inverters are
inserted by both the fully_rebuffer (placement) and repair_timing
(repair) entry points when the flag is enabled.

Signed-off-by: Minju Kim <mkim@precisioninno.com>
@minjukim55 minjukim55 self-assigned this Jun 5, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for inverter-pair insertion during rebuffering to optimize timing and area. It adds parity tracking to BufferedNet to ensure correct signal polarity, updates Rebuffer to generate and prune inverter candidates, and adds a new test case to verify the functionality. The review feedback highlights a potential null pointer dereference in Rebuffer::init() when retrieving inverter ports, and suggests a performance optimization to cache inverter ports in BufferSize to avoid redundant heap allocations during recursive tree traversal.

Comment thread src/rsz/src/Rebuffer.cc
Comment on lines +1758 to +1767
for (sta::LibertyCell* inv : resizer_->inverterCells()) {
sta::LibertyPort *in, *out;
inv->bufferPorts(in, out);
inverter_sizes_.push_back(BufferSize{
.cell = inv,
.intrinsic_delay = FixedDelay(out->intrinsicDelay(sta_), resizer_),
.margined_max_cap = 0.0f,
.driver_resistance = out->driveResistance(),
});
}
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.

high

In Rebuffer::init(), inv->bufferPorts(in, out) is called to retrieve the input and output ports of the inverter. However, there is no check to ensure that in and out are not nullptr. If a library cell is malformed or if bufferPorts fails to find the ports, out will be nullptr, and dereferencing it on the subsequent lines (e.g., out->intrinsicDelay(sta_)) will cause a segmentation fault.

Adding a defensive check to ensure both ports are valid before proceeding is highly recommended.

    for (sta::LibertyCell* inv : resizer_->inverterCells()) {
      sta::LibertyPort *in, *out;
      inv->bufferPorts(in, out);
      if (in == nullptr || out == nullptr) {
        continue;
      }
      inverter_sizes_.push_back(BufferSize{
          .cell = inv,
          .intrinsic_delay = FixedDelay(out->intrinsicDelay(sta_), resizer_),
          .margined_max_cap = 0.0f,
          .driver_resistance = out->driveResistance(),
      });
    }

Comment thread src/rsz/src/Rebuffer.cc Outdated
Comment on lines +1029 to +1055
std::vector<std::pair<sta::LibertyPort*, sta::LibertyPort*>> inv_ports;
inv_ports.reserve(resizer_->inverterCells().size());
for (sta::LibertyCell* inv_cell : resizer_->inverterCells()) {
sta::LibertyPort *in, *out;
inv_cell->bufferPorts(in, out);
inv_ports.emplace_back(in, out);
}

for (const BnetPtr& opt : opts) {
const sta::RiseFallBoth* flipped_rf
= flipRiseFallBoth(opt->slackTransition());
if (flipped_rf) {
bool flipped_safe = true;
for (auto rf1 : flipped_rf->range()) {
if (!arrival_path_available[rf1->index()]) {
flipped_safe = false;
break;
}
}
if (!flipped_safe) {
continue;
}
}
for (size_t i = 0; i < resizer_->inverterCells().size(); i++) {
sta::LibertyCell* inv_cell = resizer_->inverterCells()[i];
sta::LibertyPort* in = inv_ports[i].first;
sta::LibertyPort* out = inv_ports[i].second;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Rebuffer::insertInverterCandidates, a local std::vector of inverter ports is allocated and populated by calling bufferPorts on every inverter cell. Since insertInverterCandidates is called frequently during the recursive dynamic programming traversal of the tree, this leads to redundant heap allocations and port lookups on every call.

To optimize performance, we can add in and out port pointers directly to the BufferSize struct, populate them once in Rebuffer::init(), and then reuse them here. This completely avoids heap allocations and redundant bufferPorts calls in the inner loop.

Suggested change
std::vector<std::pair<sta::LibertyPort*, sta::LibertyPort*>> inv_ports;
inv_ports.reserve(resizer_->inverterCells().size());
for (sta::LibertyCell* inv_cell : resizer_->inverterCells()) {
sta::LibertyPort *in, *out;
inv_cell->bufferPorts(in, out);
inv_ports.emplace_back(in, out);
}
for (const BnetPtr& opt : opts) {
const sta::RiseFallBoth* flipped_rf
= flipRiseFallBoth(opt->slackTransition());
if (flipped_rf) {
bool flipped_safe = true;
for (auto rf1 : flipped_rf->range()) {
if (!arrival_path_available[rf1->index()]) {
flipped_safe = false;
break;
}
}
if (!flipped_safe) {
continue;
}
}
for (size_t i = 0; i < resizer_->inverterCells().size(); i++) {
sta::LibertyCell* inv_cell = resizer_->inverterCells()[i];
sta::LibertyPort* in = inv_ports[i].first;
sta::LibertyPort* out = inv_ports[i].second;
for (const BnetPtr& opt : opts) {
const sta::RiseFallBoth* flipped_rf
= flipRiseFallBoth(opt->slackTransition());
if (flipped_rf) {
bool flipped_safe = true;
for (auto rf1 : flipped_rf->range()) {
if (!arrival_path_available[rf1->index()]) {
flipped_safe = false;
break;
}
}
if (!flipped_safe) {
continue;
}
}
for (const BufferSize& inv_size : inverter_sizes_) {
sta::LibertyCell* inv_cell = inv_size.cell;
sta::LibertyPort* in = inv_size.in;
sta::LibertyPort* out = inv_size.out;

@jhkim-pii
Copy link
Copy Markdown
Contributor

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ef777d633

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/rsz/test/inv_pair1.tcl Outdated
set total_inv [inv_count]

puts "rsz path (repair_timing) inserted inverters: [expr { $rsz_inv > 0 ? {yes} : {no} }]"
puts "gpl path (fully_rebuffer) inserted inverters: [expr { $total_inv > 0 ? {yes} : {no} }]"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the GPL check compare against the pre-GPL count

This test is intended to verify that rsz::fully_rebuffer inserts inverters, but total_inv > 0 also counts the inverters already inserted by the preceding repair_timing path. If the GPL path regresses and inserts none, the test still prints yes whenever repair_timing inserted any inverter, so this coverage would not catch the second entry point breaking; compare total_inv against rsz_inv or record the count immediately before the GPL loop.

Useful? React with 👍 / 👎.

…uards)

Follow-up to the experimental inverter-pair Rebuffer DP, addressing review
feedback. Inverter-pair stays off by default; the flag-off path remains
byte-identical to baseline (covered by rebuffer1).

- recoverArea: merge the per-parity option buckets by capacitance instead of
  concatenating them, restoring the global cap-ascending order that
  insertBufferOptions relies on (matches the timing-DP merge path).
- insertInverterCandidates: screen each inverter candidate with
  bufferSizeCanDriveLoad, mirroring the buffer path.
- Cache input/output ports in BufferSize, populated once in init() with a
  null-port guard for inverters; iterate inverter_sizes_ directly instead of
  rebuilding a per-call port vector, and reuse the cached ports in
  insertBufferOptions.
- insertBufferOptions area-recovery exemplar fallback: guard the flipped
  transition's arrival path before creating an inverter-exemplar option,
  preventing a null dereference at the next DP level.
- inv_pair1 test: use the public enable_inverter_pair command (with a -disable
  round-trip) and assert a genuine inverter-insertion delta on the rsz path;
  the placement path is exercised for clean execution.

Signed-off-by: Minju Kim <mkim@precisioninno.com>
@minjukim55 minjukim55 marked this pull request as ready for review June 5, 2026 06:52
@minjukim55 minjukim55 requested a review from a team as a code owner June 5, 2026 06:52
Comment thread src/rsz/src/Resizer.tcl
@@ -44,6 +44,19 @@ proc reset_dont_use { args } {
rsz::reset_dont_use
}

sta::define_cmd_args "enable_inverter_pair" {[-disable]} ;# checker off
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If this is a public command it needs to be added to the README. Is this a seperate command because it would otherwise have to be a flag on many commands?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants