Skip to content

dpl: Add post-PA placement legalization command - #12053

Open
phsauter wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
phsauter:dpl-repair-pin-access
Open

phsauter wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
phsauter:dpl-repair-pin-access

Conversation

@phsauter

Copy link
Copy Markdown
Contributor

Summary

Adds the command repair_pin_access as an addition to the works started with #11120.
The command is meant to be run after pin_access and uses PA information to slightly move a few cells with bad pin access traits in rounds. It has two distinct goals:

  1. Move cells to increase the number of access points (AP) (sometimes the specific grid offset and orientation a cell has just happens to be bad for the number of APs it can generate there)
  2. Move cells when pin_access thinks they have valid APs but once you look at the escape routing, you can actually see that it is blocked by something else like PDN geometry.

It is implemented as a separate command but could feasible be added to detailed_placement as well (where it would only take effect if PA information is available).
I also thought about adding it to pin_access but decided against it as this command is currently just analytical and it is probably better to keep it that way.

Regarding point 1, there are definitely improvements that could be made here like figuring out which grid offsets and orientations are good or bad for cells and directly using that to move the cell once to a better locations instead of using rounds of trials like I do now.
The downside is that this would cost a quite lengthy up-front computation of all pins in all cells against every grid offset and orientation, which is rather expensive (I have a naively parallelized implementation that does it for a library with ~1000 cell masters in about five hours).
If this is something you would like to go for (the advantage being that the data is available from the first detailed_placement), this data would have to be saved and reloaded from a file for each technology to not make it too expensive. This would actually overall then save runtime as it would pre-solve most of pin-access.

Type of Change

  • New feature

Impact

Adds a new command that can be run between pin_access and detailed_routing to fix some routability of some badly placed cells. I would expect only a small amount of cells to move for a few designs and only for standard cell libraries with hard to access pins.
As one point of data: For a ML accelerator design with more than 500k cells and a commercial standard cell library in a process with coloring, only 85 cells had to be moved to give each pin at least two valid access points.

Verification

  • I have verified that the local build succeeds (./etc/Build.sh).
  • I have run the relevant tests and they pass.
  • My code follows the repository's formatting guidelines.
  • I have included tests to prevent regressions.
  • I have signed my commits (DCO).

Disclosure

The Readme entry is currently written by Codex (GPT-6.1 Sol). The code is majority written by Claude (Opus 5.5 and Sonnet 5.5) with reviews by me and Codex. Tests are implemented by Claude and reviewed by Codex with a final review by me to check that all add value and make sense.

@github-actions

Copy link
Copy Markdown
Contributor

This PR exceeds the recommended size of 1000 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@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 a new repair_pin_access feature to move standard cells to legal sites where their pin-access vias clear fixed supply geometry, integrating Opendp with TritonRoute's PinAccessService. The review feedback highlights several critical areas for improvement: potential double-restore/rollback bugs in PinAccessRepair::commit and PinAccessRepair::recover during error handling, potential null pointer dereferences in TritonRoute::isPinAccessRequired and FlexPA::updateDirtyRecoveryInsts, and a validation error in Opendp.tcl that incorrectly prevents setting -recovery_attempts to 0.

Comment on lines +704 to +732
int commit(const std::vector<Move>& moves, int remaining)
{
try {
for (const Move& move : moves) {
const odb::Point location = dp_.getDplLocation(move.cell);
move.cell->getDbInst()->setLocation(location.x(), location.y());
}
pa->updateDirtyPinAccess();
// AP objects may have been replaced by the native dirty-class refresh.
AccessRound fresh(supply, pa, min_access_points);
const int count = scan(fresh, true).size();
if (count >= remaining) {
restore(moves);
return remaining;
}
return count;
} catch (...) {
try {
restore(moves);
} catch (const std::exception& error) {
dp_.logger_->warn(utl::DPL,
513,
"Placement restored, but pin access refresh after "
"rollback failed: {}",
error.what());
}
throw;
}
}

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 PinAccessRepair::commit, if restore(moves) at line 716 throws an exception, it will be caught by the outer catch (...) block at line 720, which will then call restore(moves) again at line 722. This double-restore can lead to undefined behavior or nested exceptions if the first restore failed or partially succeeded. Introduce a boolean flag to track if the restore has already been executed to prevent calling it twice.

  int commit(const std::vector<Move>& moves, int remaining)
  {
    bool restored = false;
    try {
      for (const Move& move : moves) {
        const odb::Point location = dp_.getDplLocation(move.cell);
        move.cell->getDbInst()->setLocation(location.x(), location.y());
      }
      pa->updateDirtyPinAccess();
      // AP objects may have been replaced by the native dirty-class refresh.
      AccessRound fresh(supply, pa, min_access_points);
      const int count = scan(fresh, true).size();
      if (count >= remaining) {
        restored = true;
        restore(moves);
        return remaining;
      }
      return count;
    } catch (...) {
      if (!restored) {
        try {
          restore(moves);
        } catch (const std::exception& error) {
          dp_.logger_->warn(utl::DPL,
                            513,
                            "Placement restored, but pin access refresh after "
                            "rollback failed: {}",
                            error.what());
        }
      }
      throw;
    }
  }

Comment thread src/dpl/src/PinAccessRepair.cpp Outdated
Comment on lines +503 to +512
restorePlacement({move});
pa->updatePinAccessRecovery();
if (missing() != unresolved) {
dp_.logger_->error(
utl::DPL,
518,
"Pin access recovery could not restore the required-pin access "
"state after a rejected trial.");
}
moves.pop_back();

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 PinAccessRepair::recover, if pa->updatePinAccessRecovery() at line 504 throws an exception, moves.pop_back() at line 512 is never reached. Since move was already added to moves at line 476, the exception will propagate to the outer catch (...) block at line 526, which will call restorePlacement(moves) and restore move again. Since restorePlacement({move}) was already executed at line 503, this results in a redundant double-restore. Popping move from moves before calling restorePlacement({move}) avoids this issue.

Suggested change
restorePlacement({move});
pa->updatePinAccessRecovery();
if (missing() != unresolved) {
dp_.logger_->error(
utl::DPL,
518,
"Pin access recovery could not restore the required-pin access "
"state after a rejected trial.");
}
moves.pop_back();
moves.pop_back();
restorePlacement({move});
pa->updatePinAccessRecovery();
if (missing() != unresolved) {
dp_.logger_->error(
utl::DPL,
518,
"Pin access recovery could not restore the required-pin access "
"state after a rejected trial.");
}

Comment on lines +114 to +118
auto inst = design_->getTopBlock()->findInst(term->getInst());
const auto index = term->getMTerm()->getIndex();
auto inst_term = inst && index < inst->getInstTerms().size()
? inst->getInstTerm(index)
: nullptr;

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 TritonRoute::isPinAccessRequired, design_->getTopBlock() is called without checking if it is null. If the design is empty or not fully initialized, getTopBlock() can return nullptr, leading to a null pointer dereference. Add a null check for getTopBlock() before calling findInst.

  auto top_block = design_->getTopBlock();
  auto inst = top_block ? top_block->findInst(term->getInst()) : nullptr;
  const auto index = term->getMTerm()->getIndex();
  auto inst_term = inst && index < inst->getInstTerms().size()
                       ? inst->getInstTerm(index)
                       : nullptr;

Comment thread src/drt/src/pa/FlexPA.cpp
Comment on lines +283 to +294
for (auto inst : dirty_insts_) {
auto old_class = unique_insts_.getUniqueClass(inst);
auto new_class = unique_insts_.computeUniqueClass(inst);
if (old_class != nullptr) {
affected.insert(old_class);
}
if (old_class != new_class) {
unique_insts_.deleteInst(inst);
unique_insts_.addInst(inst);
}
affected.insert(new_class);
}

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 FlexPA::updateDirtyRecoveryInsts, new_class is computed and inserted into affected without checking if it is null. However, old_class is explicitly checked for null. If new_class is null, inserting it into affected will cause a null pointer dereference when iterating over affected and calling unique_class->resetSkipTerms(). Add a null check for new_class before inserting it.

  for (auto inst : dirty_insts_) {
    auto old_class = unique_insts_.getUniqueClass(inst);
    auto new_class = unique_insts_.computeUniqueClass(inst);
    if (old_class != nullptr) {
      affected.insert(old_class);
    }
    if (old_class != new_class) {
      unique_insts_.deleteInst(inst);
      unique_insts_.addInst(inst);
    }
    if (new_class != nullptr) {
      affected.insert(new_class);
    }
  }

Comment thread src/dpl/src/Opendp.tcl
Comment on lines +118 to +122
set recovery_attempts 0
if { [info exists keys(-recovery_attempts)] } {
set recovery_attempts $keys(-recovery_attempts)
sta::check_positive_integer "-recovery_attempts" $recovery_attempts
}

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 repair_pin_access, -recovery_attempts is validated using sta::check_positive_integer, which rejects 0. However, the documentation states that recovery attempts may be zero, and the C++ implementation explicitly allows 0. Change the validation to allow non-negative integers.

  set recovery_attempts 0
  if { [info exists keys(-recovery_attempts)] } {
    set recovery_attempts $keys(-recovery_attempts)
    if { ![string is integer -strict $recovery_attempts] || $recovery_attempts < 0 } {
      utl::error DPL 349 "-recovery_attempts requires a non-negative integer."
    }
  }

@phsauter
phsauter force-pushed the dpl-repair-pin-access branch from 9ab5011 to e84031d Compare November 2, 2026 17:09
@phsauter
phsauter marked this pull request as ready for review November 2, 2026 19:18
@phsauter
phsauter requested review from a team as code owners November 2, 2026 19:18
@gudeh

gudeh commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

@bnmfw FYI

Add repair_pin_access after native pin access generation. Move cells whose connected pins have insufficient access or blocked escape geometry, refresh affected pin access, and restore placement when recovery fails.

Keep OpenDB access-point lifetimes, special-wire net ownership and the DRT recovery service with the DPL command and its regression coverage.

Signed-off-by: Philippe Sauter <phsauter@iis.ee.ethz.ch>
@phsauter
phsauter force-pushed the dpl-repair-pin-access branch from e84031d to 863f5cf Compare November 5, 2026 18:35
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