rsz,dpl: add ECO timing-closure command surface#10839
Conversation
OpenROAD's odb already has a full ECO journaling layer (dbDatabase::beginEco/writeEco/readEco/undoEco), but it was only reachable through raw SWIG-mangled names with manual block resolution. There was no clean, documented user-facing command for the incremental closure loop. Add top-level Tcl commands in src/OpenRoad.tcl, alongside read_db/write_db: begin_eco start recording netlist edits on the current block write_eco <file> serialize the open ECO journal to a file read_eco <file> replay (redo) an ECO file onto the current block Each command has sta::define_cmd_args, argument-count checks, a shared ord_eco_block helper raising clean ORD-1060..1062 errors when no design is loaded, and (read_eco) file existence/readability checks (ORD-1063/1064). They wrap the existing odb::dbDatabase_* SWIG entry points: no C++ changes, no change to repair_timing defaults or any existing command. Add an end-to-end round-trip regression test (rsz/eco_round_trip) on the small Nangate45 reg1 design: begin_eco -> repair_timing -setup -> write_eco, then undo_eco (verify netlist restored to original) and read_eco (verify the replayed netlist signature exactly matches the modified one). Registered in both CMakeLists.txt and BUILD. Document the commands in src/README.md. end_eco is intentionally not exposed: repair_timing manages nested journals internally, and writing from the journal stack after endEco hits a pre-existing segfault in dbDatabase::writeEco's stack-write branch. The supported, tested flow writes while the journal is open. See ECO_INVESTIGATION.md and AGENT_REPORT.md. Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
Add a repair_timing_eco command (src/rsz/src/Resizer.tcl) that wraps the
existing repair_timing engine and the begin_eco/write_eco ECO machinery into a
timing-fix closure loop with a concise, accurate audit trail:
- snapshot baseline WNS/TNS and the netlist,
- begin_eco then run repair_timing via full argument passthrough (no
reimplementation, no default changes),
- with the journal still open, diff the netlist to report gates resized (with
before/after sizes), buffers inserted/removed, instances added/removed,
nets modified, and WNS/TNS before -> after,
- optionally write_eco the exact change set via -eco_file.
set_dont_touch is honored by the underlying resizer, so clean logic is not
perturbed. Counts are derived from the real netlist delta (which equals the
journal's structural content), not estimated.
Add src/rsz/test/repair_timing_eco.tcl (+ .ok), dual-registered in CMake and
Bazel, asserting WNS does not regress, that reported counts match an
independently computed netlist delta, and that a set_dont_touch buffer the
unconstrained flow would otherwise remove is left untouched. rsz suite:
240/240. Existing begin_eco/write_eco/read_eco and repair_timing are unchanged
(Resizer.tcl change is purely additive; OpenRoad.tcl untouched).
Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
Add an opt-in repair_hold_eco command that closes hold (min-path)
violations by inserting delay buffers on the violating fast paths,
distinct from setup repair. It is a thin ECO-style orchestration on
top of the existing repair_timing -hold engine and the begin_eco /
write_eco journal machinery -- buffering, journaling and timing are
reused, not reimplemented.
The command:
1. snapshots hold WNS (min), setup WNS (max) and the netlist;
2. opens an ECO journal and runs the real repair_timing -hold engine
(allow_setup_violations OFF by default, so rsz's own guard backs
off any insert that would break setup; set_dont_touch honored);
3. reports buffers inserted and hold/setup WNS before -> after;
4. asserts setup WNS did not regress (the classic hold-fix hazard),
erroring out (RSZ-0245) on a real regression;
5. optionally writes the ECO change set.
Pure Tcl, additive: no C++, no src/sta edit, no .i change. A plain
repair_timing run is byte-identical to the integration baseline binary.
New regression repair_hold_eco (sky130hs) proves hold WNS -0.14 -> 0,
1 buffer inserted, setup not degraded, and a dont_touch cell untouched.
Registered in both CMake and Bazel. New message ids RSZ-0244/0245.
Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
Add a flag-gated, additive incremental-legalization path that snaps only the
cells an ECO touched (resized, inserted, moved) to site and resolves the local
overlaps they create, leaving every other placed cell pinned at its exact
coordinates. This is the placement companion to repair_timing_eco: after the
resizer upsizes/inserts cells it can leave small overlaps and off-site cells,
and a full re-place would destroy the existing placement and timing.
New public API Opendp::ecoLegalizePlacement(eco_insts, max_disp_x, max_disp_y,
verbose) and a setEcoGridCells() helper (src/dpl/src/Opendp.cpp, Opendp.h):
- import the db and snapshot every cell's pre-legalization location,
- paint the grid like the existing -incremental path, but only UNPLACE the
ECO cells plus any cell that physically overlaps an ECO cell's footprint;
every other placed cell keeps its pixels and stays pinned,
- run the existing diamond-search place() bounded by a tight default window
(20 sites x 4 rows; caller-overridable) so untouched regions stay put,
- attach a dpl Journal during place() to get an exact record of which cells
moved, then assert the minimal-perturbation invariant: any cell that is
neither in the legalize set nor recorded by the journal MUST keep its exact
coordinates (DPL-1114 errors loudly otherwise),
- report cells legalized (ECO + overlapping), neighbors displaced, max ECO
displacement, and the count of provably-unmoved cells.
Exposed as the Tcl command improve_eco_legalization -cells {...}
[-max_displacement disp|{dx dy}] [-verbose] (Opendp.tcl) wrapping a new
dpl::eco_legalize_cmd SWIG entry (Opendp.i). No change to detailed_placement,
no src/sta edit; all new code marked // OpenROAD-fork: eco-legalize.
Default/flag-off byte-identity verified: a normal detailed_placement run on
gcd_nangate45 produces a placed DEF whose MD5 (876c50cb4aecc9dbf167ab495158b32e)
exactly matches the committed gcd.defok.
New regression src/dpl/test/eco_legalize.tcl (+ .ok), dual-registered in
CMakeLists.txt and BUILD: legalize a clean gcd placement, emulate an ECO by
upsizing 3 cells to create overlaps, run improve_eco_legalization on only those
cells, and assert (a) check_placement passes (overlaps resolved), (b) every ECO
cell is site-aligned, (c) 546/549 cells are provably unmoved (minimal
perturbation), and (d) the moved set is tiny -- no full re-place. dpl+rsz
suites: 360/360.
Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
Compose the existing ECO closure pieces into one iterate-to-convergence
command. This does NOT reimplement setup repair, hold repair, ECO
legalization or STA -- it orchestrates them:
loop until converged or -max_iters:
1. repair_timing -setup (real setup engine; honors dont_touch)
2. improve_eco_legalization (legalize only the setup-touched cells)
3. incremental STA (OpenSTA lazily re-times the affected cone
on the next slack query -- the WNS read IS
the incremental STA)
4. repair_timing -hold (real hold engine; allow_setup_violations
OFF so it backs off setup-breaking inserts)
5. improve_eco_legalization (legalize the hold-touched cells)
6. check setup+hold WNS: STOP on converged / no-improvement / max_iters
Termination is guaranteed by a -max_iters cap AND a no-improvement detector.
The no-setup-regression invariant (anti-ping-pong guard) is enforced two ways:
a per-iteration check that reverts the whole ECO journal to baseline via
odb::dbDatabase_undoEco and stops if cumulative setup ever drops below the
loop baseline, plus a hard post-condition assertion (RSZ 247). Per-iteration
trajectory (setup/hold WNS+TNS, cells changed, buffers inserted) is reported
so termination and monotonicity are auditable.
All additions are additive Tcl in src/rsz/src/Resizer.tcl (new procs only;
zero existing lines modified) plus new helpers eco_changed_cells /
eco_hold_tns / eco_report_closure. New RSZ message ids 246 (warn) and 247
(error) are unused elsewhere. No C++, no src/sta, no *.i changes.
Default/flag-off byte-identity: the existing repair_timing_eco /
repair_hold_eco / eco_round_trip golden logs are unchanged and their tests
still pass, proving the existing command output is byte-identical.
New regression test rsz/repair_timing_closure (registered in both CMake and
Bazel) on gcd_nangate45_placed at period 0.58 with a 0.2ns hold uncertainty,
which starts with BOTH a setup (-0.082ns) and hold (-0.055ns) violation. It
asserts: both violated before; both WNS >= 0 after (converges in 2 iters);
terminates within max_iters; setup never regresses below baseline on any
trajectory row; the setup trajectory is non-decreasing (no ping-pong); and a
set_dont_touch instance is left untouched.
ctest: 362/362 rsz+dpl tests pass (excluding pre-existing *_NOT_BUILT cpp
placeholders that were not compiled in this build).
Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
There was a problem hiding this comment.
Code Review
This pull request introduces ECO (Engineering Change Order) capture and replay capabilities to OpenROAD, including incremental legalization (improve_eco_legalization) and dedicated timing-closure loops (repair_timing_eco, repair_hold_eco, and repair_timing_closure). The review feedback highlights several critical improvements: allowing fractional micron values for -max_displacement validation, clamping converted displacement values to prevent truncation to zero, ensuring database rollback via undoEco if repair_hold_eco aborts, and skipping write_eco in repair_timing_closure if the loop was reverted.
| if { [llength $max_displacement] == 1 } { | ||
| sta::check_positive_integer "-max_displacement" $max_displacement | ||
| set max_displacement_x $max_displacement | ||
| set max_displacement_y $max_displacement | ||
| } elseif { [llength $max_displacement] == 2 } { | ||
| lassign $max_displacement max_displacement_x max_displacement_y | ||
| sta::check_positive_integer "-max_displacement" $max_displacement_x | ||
| sta::check_positive_integer "-max_displacement" $max_displacement_y |
There was a problem hiding this comment.
The -max_displacement argument is specified in microns (as evidenced by the subsequent conversion using ord::microns_to_dbu). However, the input validation uses sta::check_positive_integer, which restricts the user to integer micron values (e.g., 1 or 2 microns) and rejects perfectly valid fractional micron values (e.g., 0.5 or 1.5 microns).\n\nWe should allow positive floating-point numbers by validating using string is double and checking that the value is greater than zero.
if { [llength $max_displacement] == 1 } {
if { ![string is double -strict $max_displacement] || $max_displacement <= 0 } {
utl::error DPL 1122 "-max_displacement must be a positive number."
}
set max_displacement_x $max_displacement
set max_displacement_y $max_displacement
} elseif { [llength $max_displacement] == 2 } {
lassign $max_displacement max_displacement_x max_displacement_y
if { ![string is double -strict $max_displacement_x] || $max_displacement_x <= 0 || ![string is double -strict $max_displacement_y] || $max_displacement_y <= 0 } {
utl::error DPL 1122 "-max_displacement must be positive numbers."
}
| set max_displacement_x [expr { | ||
| [ord::microns_to_dbu $max_displacement_x] / [$site getWidth] | ||
| }] | ||
| set max_displacement_y [expr { | ||
| [ord::microns_to_dbu $max_displacement_y] / [$site getHeight] | ||
| }] |
There was a problem hiding this comment.
When converting the user-specified -max_displacement from microns to sites/rows, a very small positive value (e.g., 0.01 microns) can truncate to 0 due to integer division. If either max_displacement_x or max_displacement_y becomes 0, the C++ implementation (ecoLegalizePlacement) interprets this as "not specified" and resets both to their default values (20 sites and 4 rows). This completely defeats the user's intent of restricting cell movement to a very tight window.\n\nTo prevent this, we should:\n1. Ensure the converted values are integers by wrapping the expression in int(...).\n2. Clamp the minimum converted value to 1 so that a very small user-specified displacement does not truncate to 0 and trigger the default reset.
set max_displacement_x [expr {
int([ord::microns_to_dbu $max_displacement_x] / [$site getWidth])
}]
set max_displacement_y [expr {
int([ord::microns_to_dbu $max_displacement_y] / [$site getHeight])
}]
if { $max_displacement_x < 1 } { set max_displacement_x 1 }
if { $max_displacement_y < 1 } { set max_displacement_y 1 }
| set setup_eps 1e-12 | ||
| if { $setup_wns_after < [expr { $setup_wns_before - $setup_eps }] } { | ||
| utl::error RSZ 245 \ | ||
| [format "hold repair degraded setup WNS from %.4g to %.4g; aborting." \ | ||
| $setup_wns_before $setup_wns_after] | ||
| } |
There was a problem hiding this comment.
If repair_hold_eco aborts because hold repair degraded setup WNS, the ECO changes are left in the database and the ECO journal remains open. If the user catches the Tcl error to handle it gracefully, the design is left in a degraded state.\n\nWe should call odb::dbDatabase_undoEco $block to roll back the changes before raising the error.
set setup_eps 1e-12
if { $setup_wns_after < [expr { $setup_wns0 - $setup_eps }] } {
odb::dbDatabase_undoEco $block
utl::error RSZ 245 [format "hold repair degraded setup WNS from %.4g to %.4g; aborting." $setup_wns_before $setup_wns_after]
}
| if { $eco_file ne "" } { | ||
| write_eco $eco_file | ||
| utl::report "ECO change set written ([file tail $eco_file])" | ||
| } |
There was a problem hiding this comment.
If the closure loop was aborted and rolled back due to setup regression (stop_reason is setup_regression_reverted), the ECO journal has already been completely undone via odb::dbDatabase_undoEco. Attempting to call write_eco in this state will either fail (since the ECO session was undone) or write an empty/baseline ECO file, which is misleading.\n\nWe should only write the ECO file if the loop did not revert the ECO.
if { $eco_file ne "" && $stop_reason ne "setup_regression_reverted" } {
write_eco $eco_file
utl::report "ECO change set written ([file tail $eco_file])"
}
| Movements Summary | ||
| --------------------------------------- | ||
| Total cells: 192 | ||
| Diamond Move Success: 192 (100.00%) |
There was a problem hiding this comment.
We should modify our default legalizer to negotiation legalizer soon.
gudeh
left a comment
There was a problem hiding this comment.
Just leaving a comment for now. I still want to understand why we need a different DPL behavior for this. I assume it is due to movable instances.
Address review findings on the ECO command surface: - improve_eco_legalization: validate -max_displacement as a positive float (was integer, rejecting fractional microns) and floor the micron->site/row conversion to >=1, so a small request never truncates to 0 (which the C++ treats as unset and resets to defaults). - repair_hold_eco: roll back via dbDatabase_undoEco before erroring on setup-WNS regression, so a caught error leaves the design unmodified rather than degraded with the ECO journal still open. - repair_timing_closure: skip write_eco when the loop reverted to baseline (the journal was already undone), instead of writing an empty/misleading ECO file. Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
6ce1a6c to
ddafe62
Compare
|
Thanks for the review — addressed all four findings in the latest commit (
All 5 ECO tests still pass. @gudeh — agreed on moving to the negotiation legalizer; happy to follow up on that separately since it's broader than this PR. |
Summary
Adds an additive, opt-in ECO (Engineering Change Order) timing-closure command
surface. No existing command's behavior changes: the default
repair_timing/report_checks/ placement paths are unchanged unless one of the new commands isinvoked.
The 5 commits build up the surface in order:
begin_eco/write_eco/read_eco-- ECO capture-replay journal.repair_timing_eco-- setup-repair ECO closure loop with an accurate changesummary.
repair_hold_eco-- dedicated hold-violation repair.improve_eco_legalization-- legalize only ECO-touched cells (dpl).repair_timing_closure-- unified setup+hold convergence-loop driver thatiterates the pieces above to convergence in a single command.
repair_timing_closureguarantees termination (bounded by-max_itersplus ano-improvement detector), no setup regression (each iteration snapshots setup
WNS and rolls back via
undo_ecoif it worsens), and honorsdont_touchthroughout.
Type of Change
Verification
rsz.eco_round_trip,rsz.repair_timing_eco,rsz.repair_hold_eco,rsz.repair_timing_closure,dpl.eco_legalize(5/5).Scope
ECO-only:
src/rsz,src/dpl,src/OpenRoad.tcl, and tests. Nosrc/stachanges.