The-OpenROAD-Project / The-OpenROAD-Project/OpenROAD
repair_timing: add `-effort explore | tapeout` to communicate intent w.r.t. repair_timing
@minjukim55 is already working on this.
Since Jul 14, 2026.
- 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_tnsrequire knowing the achievable WNS up front. It is unknowable — it swings hundreds of ps with the placement lottery.-max_iterations/-max_passesare blind counts: you still guess a number, and it cuts arbitrarily.- The adaptive stop already exists —
SetupLegacyBase::terminateProgressexits when the incremental TNS fix-rate over a window drops below a threshold — but it is unreachable early: gated behind a hardcodediteration > 1000with a0.0001threshold. 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.
-effortbounds 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
lowcan'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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.