Skip to content

rsz,dpl: add ECO timing-closure command surface#10839

Open
saurav-fermions wants to merge 6 commits into
The-OpenROAD-Project:masterfrom
Fermions-ASI:feat/eco-closure
Open

rsz,dpl: add ECO timing-closure command surface#10839
saurav-fermions wants to merge 6 commits into
The-OpenROAD-Project:masterfrom
Fermions-ASI:feat/eco-closure

Conversation

@saurav-fermions

Copy link
Copy Markdown
Contributor

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 is
invoked.

The 5 commits build up the surface in order:

  1. begin_eco / write_eco / read_eco -- ECO capture-replay journal.
  2. repair_timing_eco -- setup-repair ECO closure loop with an accurate change
    summary.
  3. repair_hold_eco -- dedicated hold-violation repair.
  4. improve_eco_legalization -- legalize only ECO-touched cells (dpl).
  5. repair_timing_closure -- unified setup+hold convergence-loop driver that
    iterates the pieces above to convergence in a single command.

repair_timing_closure guarantees termination (bounded by -max_iters plus a
no-improvement detector), no setup regression (each iteration snapshots setup
WNS and rolls back via undo_eco if it worsens), and honors dont_touch
throughout.

Type of Change

  • New feature (additive, opt-in)

Verification

  • Local build succeeds.
  • New tests pass: rsz.eco_round_trip, rsz.repair_timing_eco,
    rsz.repair_hold_eco, rsz.repair_timing_closure, dpl.eco_legalize (5/5).
  • Additive: default flows unchanged unless a new command is invoked.
  • clang-format + tclfmt clean.
  • I have signed my commits (DCO).

Scope

ECO-only: src/rsz, src/dpl, src/OpenRoad.tcl, and tests. No src/sta
changes.

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>

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment thread src/dpl/src/Opendp.tcl Outdated
Comment on lines +152 to +159
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

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

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."
      }

Comment thread src/dpl/src/Opendp.tcl
Comment on lines +164 to +169
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]
}]

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

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 }

Comment thread src/rsz/src/Resizer.tcl
Comment on lines +718 to +723
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]
}

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

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]
  }

Comment thread src/rsz/src/Resizer.tcl
Comment on lines +966 to +969
if { $eco_file ne "" } {
write_eco $eco_file
utl::report "ECO change set written ([file tail $eco_file])"
}

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

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

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.

We should modify our default legalizer to negotiation legalizer soon.

@gudeh gudeh 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.

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>
@saurav-fermions

Copy link
Copy Markdown
Contributor Author

Thanks for the review — addressed all four findings in the latest commit (ddafe62):

  1. -max_displacement validation (Opendp.tcl): switched to check_positive_float, so fractional microns (e.g. 0.5) are accepted. Verified check_positive_float 0.5 passes where check_positive_integer 0.5 errored (STA-0574).
  2. micron→site/row truncation: the converted counts are now floored to max(1, …), so a small positive request can't truncate to 0 (which the C++ treats as unset and resets to the defaults).
  3. repair_hold_eco rollback: on a setup-WNS regression it now calls odb::dbDatabase_undoEco (and refreshes parasitics) before raising, so a caught error leaves the design in its pre-repair state rather than degraded with the ECO journal still open.
  4. repair_timing_closure write_eco: skipped when the loop reverted to baseline (the journal was already undone), instead of writing an empty/misleading ECO file.

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.

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.

2 participants