The-OpenROAD-Project / The-OpenROAD-Project/OpenROAD

repair_timing: add `-effort explore | tapeout` to communicate intent w.r.t. repair_timing

Open
#10,900 21 comments 0 reactions 1 assignee View on GitHub

@minjukim55 is already working on this.

Since Jul 14, 2026.

rsz Stale
Dominant language
Verilog
Stars
3.1k
Forks
1k
Avg merge
2d 23h
Merged PRs (30d)
136

Description

Description

repair_timing is built for tape-out: close timing, grind as hard as needed. Design-space exploration is a different job: while iterating RTL or floorplan, we want the achieved WNS at bounded, predictable cost — not closure. There is currently no way to express that intent.

The existing knobs don't:

  • -setup_margin / -repair_tns require knowing the achievable WNS up front. It is unknowable — it swings hundreds of ps with the placement lottery.
  • -max_iterations / -max_passes are blind counts: you still guess a number, and it cuts arbitrarily.
  • The adaptive stop already exists — SetupLegacyBase::terminateProgress exits when the incremental TNS fix-rate over a window drops below a threshold — but it is unreachable early: gated behind a hardcoded iteration > 1000 with a 0.0001 threshold. A non-convergent design must grind >1000 iterations before it can bail.

Concrete case (our design, ASAP7, post-global-route, -setup_margin 0 -repair_tns 0): setup repair enters with 23,616 violating endpoints, grinds ~14 min over 290 iterations while WNS crawls −803 → −603 ps and flattens, ends RSZ-0062. Margin is 0, so this is not margin-too-tight; the design is genuinely uncloseable at this placement. The ~1400 buffers inserted chasing the unreachable target then feed a large hold repair; combined repair_timing is ~3.4 h. For DSE, "≈ −650 ps in a few minutes" would be the right answer. Instead every RTL/floorplan iteration pays the grind.

Suggested Solution

Edit: I think two modes explore and tapeout (default) clearly articulates the intent with running the flow.

Expose the marginal-progress stop as an effort dial: repair_timing -effort low|medium|high. low bails at diminishing returns; high (and unset) is today's behavior, so backward-compatible.

Prototyped: make the hardcoded 1000 / 0.0001 gate tunable, plateau-start lowered so it can fire at the first 100-iteration window. Measured on the grind above (same frozen post-CTS design, repair_timing -setup only):

-effort plateau-start / min-fix-rate wall final setup WNS stop
low 0 / 0.05 316 s −647.8 ps bails ~iter 100
medium 0 / 0.02 594 s −614.2 ps bails ~iter 200
high (default) 1000 / 0.0001 817 s −602.7 ps natural termination iter 290, RSZ-0062

A monotonic runtime↔WNS dial: low returns a WNS within ~45 ps of the futile tail in 39% of the time. Patch below; happy to turn it into a PR and discuss the right interface.

openroad-repair-timing-effort.patch
diff --git a/src/rsz/include/rsz/Resizer.hh b/src/rsz/include/rsz/Resizer.hh
index 35abccdeea..56c57bdf19 100644
--- a/src/rsz/include/rsz/Resizer.hh
+++ b/src/rsz/include/rsz/Resizer.hh
@@ -356,7 +356,9 @@ class Resizer : public sta::dbStaState, public sta::dbNetworkObserver
                    bool skip_buffer_removal,
                    bool skip_last_gasp,
                    bool skip_vt_swap,
-                   bool skip_crit_vt_swap);
+                   bool skip_crit_vt_swap,
+                   int plateau_start_iteration = 1000,
+                   float min_inc_fix_rate = 0.0001f);
   // For testing.
   void repairSetup(const sta::Pin* end_pin);
   // For testing.
diff --git a/src/rsz/src/OptimizerTypes.hh b/src/rsz/src/OptimizerTypes.hh
index 0ca33772d8..a403a9edcf 100644
--- a/src/rsz/src/OptimizerTypes.hh
+++ b/src/rsz/src/OptimizerTypes.hh
@@ -391,6 +391,14 @@ struct OptimizerRunConfig
   int max_iterations{0};
   int max_passes{100};
   int max_repairs_per_pass{1};
+  // DSE effort knob: setup repair's marginal-progress stop
+  // (SetupLegacyBase::terminateProgress) may fire once opto_iteration exceeds
+  // plateau_start_iteration and the incremental TNS fix-rate drops below
+  // min_inc_fix_rate. Defaults reproduce the historical hardcoded 1000/0.0001
+  // gate; lowering plateau_start_iteration lets repair bail out of a
+  // non-convergent grind early.
+  int plateau_start_iteration{1000};
+  float min_inc_fix_rate{0.0001f};
   bool match_cell_footprint{false};
   bool verbose{false};
   bool skip_pin_swap{false};
diff --git a/src/rsz/src/Resizer-py.i b/src/rsz/src/Resizer-py.i
index 7b2c6801d9..b7cd3ae4af 100644
--- a/src/rsz/src/Resizer-py.i
+++ b/src/rsz/src/Resizer-py.i
@@ -36,7 +36,8 @@ using namespace rsz;
 %ignore rsz::Resizer::repairSetup(double, double, int, int, int, bool, bool,
                                   const std::vector<rsz::MoveType>&,
                                   const char*,
-                                  bool, bool, bool, bool, bool, bool, bool, bool);
+                                  bool, bool, bool, bool, bool, bool, bool, bool,
+                                  int, float);
 
 %ignore rsz::Resizer::computeNewDelaysSlews;
 %ignore rsz::Resizer::estimateSlewsAfterBufferRemoval;
diff --git a/src/rsz/src/Resizer.cc b/src/rsz/src/Resizer.cc
index 38d7af4cdb..71366bbcde 100644
--- a/src/rsz/src/Resizer.cc
+++ b/src/rsz/src/Resizer.cc
@@ -4996,7 +4996,9 @@ bool Resizer::repairSetup(double setup_margin,
                           bool skip_buffer_removal,
                           bool skip_last_gasp,
                           bool skip_vt_swap,
-                          bool skip_crit_vt_swap)
+                          bool skip_crit_vt_swap,
+                          int plateau_start_iteration,
+                          float min_inc_fix_rate)
 {
   utl::Timer timer;
   OptimizerRunConfig config;
@@ -5018,6 +5020,8 @@ bool Resizer::repairSetup(double setup_margin,
   config.skip_last_gasp = skip_last_gasp;
   config.skip_vt_swap = skip_vt_swap;
   config.skip_crit_vt_swap = skip_crit_vt_swap;
+  config.plateau_start_iteration = plateau_start_iteration;
+  config.min_inc_fix_rate = min_inc_fix_rate;
 
   rsz::Optimizer optimizer(this);
   optimizer.configure(config);
diff --git a/src/rsz/src/Resizer.i b/src/rsz/src/Resizer.i
index b1c993189c..3c49b0c4d6 100644
--- a/src/rsz/src/Resizer.i
+++ b/src/rsz/src/Resizer.i
@@ -372,7 +372,9 @@ repair_setup(double setup_margin,
              bool skip_buffer_removal,
              bool skip_last_gasp,
              bool skip_vt_swap,
-             bool skip_crit_vt_swap)
+             bool skip_crit_vt_swap,
+             int plateau_start_iteration,
+             double min_inc_fix_rate)
 {
   ensureLinked();
   Resizer *resizer = getResizer();
@@ -383,7 +385,8 @@ repair_setup(double setup_margin,
                        skip_pin_swap, skip_gate_cloning,
                        skip_size_down_fanout,
                        skip_buffering, skip_buffer_removal,
-                       skip_last_gasp, skip_vt_swap, skip_crit_vt_swap);
+                       skip_last_gasp, skip_vt_swap, skip_crit_vt_swap,
+                       plateau_start_iteration, min_inc_fix_rate);
 }
 
 void
diff --git a/src/rsz/src/Resizer.tcl b/src/rsz/src/Resizer.tcl
index 7f81f647f4..b723cd4c42 100644
--- a/src/rsz/src/Resizer.tcl
+++ b/src/rsz/src/Resizer.tcl
@@ -257,6 +257,9 @@ sta::define_cmd_args "repair_timing" {[-setup] [-hold]\
                                         [-max_utilization util] \
                                         [-match_cell_footprint] \
                                         [-max_repairs_per_pass max_repairs_per_pass]\
+                                        [-effort effort]\
+                                        [-plateau_start_iteration n]\
+                                        [-min_inc_fix_rate rate]\
                                         [-verbose]}
 
 proc repair_timing { args } {
@@ -267,7 +270,8 @@ proc repair_timing { args } {
     keys {-setup_margin -hold_margin -slack_margin \
             -libraries -max_utilization -max_buffer_percent -sequence \
             -phases -policy -policies \
-            -recover_power -repair_tns -max_passes -max_iterations -max_repairs_per_pass} \
+            -recover_power -repair_tns -max_passes -max_iterations -max_repairs_per_pass \
+            -effort -plateau_start_iteration -min_inc_fix_rate} \
     flags {-setup -hold -allow_setup_violations -skip_pin_swap -skip_gate_cloning \
              -skip_size_down -skip_buffering -skip_buffer_removal -skip_last_gasp \
              -skip_vt_swap -skip_crit_vt_swap -match_cell_footprint -verbose}
@@ -372,6 +376,39 @@ proc repair_timing { args } {
     set max_repairs_per_pass $keys(-max_repairs_per_pass)
   }
 
+  # DSE "how hard to try" dial for setup repair. Maps to the marginal-progress
+  # stop (plateau_start_iteration / min_inc_fix_rate). Unset reproduces the
+  # historical tape-out behaviour (1000 / 0.0001).
+  set plateau_start_iteration 1000
+  set min_inc_fix_rate 0.0001
+  if { [info exists keys(-effort)] } {
+    switch -- $keys(-effort) {
+      low {
+        set plateau_start_iteration 0
+        set min_inc_fix_rate 0.05
+      }
+      medium {
+        set plateau_start_iteration 0
+        set min_inc_fix_rate 0.02
+      }
+      high {
+        set plateau_start_iteration 1000
+        set min_inc_fix_rate 0.0001
+      }
+      default {
+        error "repair_timing -effort must be low, medium, or high"
+      }
+    }
+  }
+  # Direct overrides of the plateau parameters (for tuning / advanced use);
+  # take precedence over the -effort preset.
+  if { [info exists keys(-plateau_start_iteration)] } {
+    set plateau_start_iteration $keys(-plateau_start_iteration)
+  }
+  if { [info exists keys(-min_inc_fix_rate)] } {
+    set min_inc_fix_rate $keys(-min_inc_fix_rate)
+  }
+
   sta::check_argc_eq0 "repair_timing" $args
   est::check_parasitics
 
@@ -386,7 +423,8 @@ proc repair_timing { args } {
         $max_iterations $max_repairs_per_pass $match_cell_footprint $verbose \
         $sequence $phases \
         $skip_pin_swap $skip_gate_cloning $skip_size_down_fanout $skip_buffering \
-        $skip_buffer_removal $skip_last_gasp $skip_vt_swap $skip_crit_vt_swap]
+        $skip_buffer_removal $skip_last_gasp $skip_vt_swap $skip_crit_vt_swap \
+        $plateau_start_iteration $min_inc_fix_rate]
     }
     if { $hold } {
       set repaired_hold [rsz::repair_hold $setup_margin $hold_margin \
diff --git a/src/rsz/src/policy/SetupLastGaspPolicy.cc b/src/rsz/src/policy/SetupLastGaspPolicy.cc
index d601788921..fe2bab1927 100644
--- a/src/rsz/src/policy/SetupLastGaspPolicy.cc
+++ b/src/rsz/src/policy/SetupLastGaspPolicy.cc
@@ -118,7 +118,7 @@ bool SetupLastGaspPolicy::initializeLastGaspRepair(
   last_gasp_state.initial_tns = setup_context_.initial_tns;
   last_gasp_state.prev_tns = curr_tns;
   last_gasp_state.prev_worst_slack = violating_ends.front().second;
-  last_gasp_state.fix_rate_threshold = inc_fix_rate_threshold_;
+  last_gasp_state.fix_rate_threshold = config_.min_inc_fix_rate;
 
   debugPrint(logger_,
              RSZ,
diff --git a/src/rsz/src/policy/SetupLegacyBase.cc b/src/rsz/src/policy/SetupLegacyBase.cc
index d6d63b90ac..c5a05ce9f1 100644
--- a/src/rsz/src/policy/SetupLegacyBase.cc
+++ b/src/rsz/src/policy/SetupLegacyBase.cc
@@ -865,7 +865,8 @@ bool SetupLegacyBase::terminateProgress(const int iteration,
     float curr_tns = sta_->totalNegativeSlack(max_);
     float inc_fix_rate = (prev_tns - curr_tns) / initial_tns;
     prev_tns = curr_tns;
-    if (iteration > 1000 && inc_fix_rate < fix_rate_threshold) {
+    if (iteration > config_.plateau_start_iteration
+        && inc_fix_rate < fix_rate_threshold) {
       debugPrint(logger_,
                  RSZ,
                  "repair_setup",
diff --git a/src/rsz/src/policy/SetupLegacyPolicy.cc b/src/rsz/src/policy/SetupLegacyPolicy.cc
index 1086e3b779..ca786aaa2f 100644
--- a/src/rsz/src/policy/SetupLegacyPolicy.cc
+++ b/src/rsz/src/policy/SetupLegacyPolicy.cc
@@ -93,7 +93,7 @@ bool SetupLegacyPolicy::initializeMainRepair(MainRepairState& main_state,
   main_state.initial_tns = sta_->totalNegativeSlack(max_);
   main_state.prev_tns = main_state.initial_tns;
   main_state.num_viols = violating_ends.size();
-  main_state.fix_rate_threshold = inc_fix_rate_threshold_;
+  main_state.fix_rate_threshold = config_.min_inc_fix_rate;
 
   printProgress(main_state.opto_iteration, false, main_state.phase_marker);
 
Additional Context

Known weaknesses of the prototype:

  • The heuristic is overfitted to the one design we tested. The low/medium/high → (plateau_start, fix_rate) mapping was tuned on this grind; on another design (or another placement of the same design) it may bail too early, leaving WNS on the table, or too late, still grinding. Needs calibration across designs.
  • -effort bounds cost, not quality: the returned WNS for a given level still swings with the placement.
  • Hold needs its own heuristic. The dial instruments only the setup plateau; hold repair has no effort path. Hold is a different regime — setup grinds non-convergently (TNS-progress plateau is the right bail), hold does converge but is expensive (each buffer genuinely fixes a violation) — so a hold dial wants a different stop criterion: remaining violation count, buffer budget, or per-endpoint diminishing returns.
  • Coarse at the low end: the plateau check fires only at multiples of the 100-iteration window, so low can't bound below ~iter 100 without also exposing the window size.

Whether the same intent should thread through the ORFS flow is a follow-up question.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.