Skip to content

dft: add scan chain wirelength optimizer (scan_opt)#10666

Open
mwsoli wants to merge 16 commits into
The-OpenROAD-Project:masterfrom
mwsoli:dft/scan-chain-optimizer
Open

dft: add scan chain wirelength optimizer (scan_opt)#10666
mwsoli wants to merge 16 commits into
The-OpenROAD-Project:masterfrom
mwsoli:dft/scan-chain-optimizer

Conversation

@mwsoli

@mwsoli mwsoli commented Jun 16, 2026

Copy link
Copy Markdown

Summary

Adds a post-placement scan chain wirelength optimizer to the DFT flow (scan_opt Tcl command).
The optimizer runs after execute_dft_plan and reorders cells within each scan chain to reduce total wirelength, then re-stitches the chains directly in odb. Algorithm: greedy nearest-neighbor seed → 2-Opt (asymmetric, with reversal-cost correction) → 3-Opt (direction-preserving subtour swap). For designs with multiple chains on the same clock domain, a k-means spatial pre-clustering stage reassigns cells between chains before per-chain local search.

Type of

  • New feature
  • Refactoring

Impact

scan_opt is a new optional post-placement step. Designs that don't call it are unaffected. Designs that do call it get reduced scan chain wirelength; chain functionality (scan-in/scan-out connectivity) is preserved.

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

@mwsoli mwsoli requested review from a team as code owners June 16, 2026 01:45
@mwsoli mwsoli requested a review from maliberty June 16, 2026 01:45

@github-actions github-actions 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.

Welcome to OpenROAD! Thanks for opening your first PR.
Before we review:

Please ensure:

  • CI passes
  • Code is properly formatted
  • Tests are included where applicable
    A maintainer will review shortly!

@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 implements scan chain wirelength optimization (scan_opt) and spatial pre-clustering using K-Means, along with reporting metrics for scan chains. Key changes include the addition of a new optimizer library, integration with the Dft class, updates to the odb database classes to support resetting and clearing scan lists, and comprehensive unit tests. The review feedback focuses on performance improvements in the 2-Opt and 3-Opt local search algorithms by avoiding redundant database lookups and vector rebuilds. It also addresses critical bugs, such as a potential segmentation fault from a null scan clock, duplicate net creation failures from non-unique net names, and an incorrect coordinate sum comparison for finding the starting cell.

Comment thread src/dft/src/optimizer/Opt.cpp
Comment thread src/dft/src/optimizer/Opt.cpp Outdated
Comment thread src/dft/src/optimizer/Opt.cpp
Comment on lines +342 to +356
size_t start_index = 0;
int64_t lowest_dist = std::numeric_limits<int64_t>::max();
// Get points in a form ready to insert into index
using Point = bg::model::point<int, 2, bg::cs::cartesian>;
std::vector<std::pair<Point, size_t>> transformed;

for (size_t i = 0; i < cells.size(); i++) {
// Find the lower leftmost cell by looking for the cell with the lowest
// manhattan distance to the origin
auto origin = cells[i]->getOrigin();
const int64_t dist = origin.x() + origin.y();
if (dist < lowest_dist) {
start_index = i;
lowest_dist = dist;
}

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

Finding the bottom-leftmost cell using origin.x() + origin.y() can be incorrect if coordinates are negative or if there are multiple cells with the same sum but different distributions. A lexicographical comparison (minimizing X first, then Y) is more robust and standard.

Suggested change
size_t start_index = 0;
int64_t lowest_dist = std::numeric_limits<int64_t>::max();
// Get points in a form ready to insert into index
using Point = bg::model::point<int, 2, bg::cs::cartesian>;
std::vector<std::pair<Point, size_t>> transformed;
for (size_t i = 0; i < cells.size(); i++) {
// Find the lower leftmost cell by looking for the cell with the lowest
// manhattan distance to the origin
auto origin = cells[i]->getOrigin();
const int64_t dist = origin.x() + origin.y();
if (dist < lowest_dist) {
start_index = i;
lowest_dist = dist;
}
size_t start_index = 0;
int lowest_x = std::numeric_limits<int>::max();
int lowest_y = std::numeric_limits<int>::max();
// Get points in a form ready to insert into index
using Point = bg::model::point<int, 2, bg::cs::cartesian>;
std::vector<std::pair<Point, size_t>> transformed;
for (size_t i = 0; i < cells.size(); i++) {
// Find the lower leftmost cell lexicographically (min X, then min Y)
auto origin = cells[i]->getOrigin();
if (origin.x() < lowest_x || (origin.x() == lowest_x && origin.y() < lowest_y)) {
start_index = i;
lowest_x = origin.x();
lowest_y = origin.y();
}

Comment thread src/dft/src/Dft.cpp
Comment on lines +111 to +116
if (net == nullptr) {
net = odb::dbNet::create(block, so_iterm->getName().c_str());
if (net == nullptr) {
logger->error(
utl::DFT, 15, "Failed to create net for scan_opt restitching.");
}

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

Creating a net using so_iterm->getName().c_str() will fail if a net with that name already exists in the block. Since scan-out pins are almost always named the same (e.g., "Q" or "SO"), this will fail on the second cell that needs a net created. Constructing a unique net name using the instance name and the pin name prevents this failure.

Suggested change
if (net == nullptr) {
net = odb::dbNet::create(block, so_iterm->getName().c_str());
if (net == nullptr) {
logger->error(
utl::DFT, 15, "Failed to create net for scan_opt restitching.");
}
if (net == nullptr) {
std::string net_name = std::string(so_iterm->getInst()->getName()) + "_" + so_iterm->getName();
net = odb::dbNet::create(block, net_name.c_str());
if (net == nullptr) {
logger->error(
utl::DFT, 15, "Failed to create net for scan_opt restitching.");
}

Comment thread src/dft/src/Dft.cpp
Comment on lines +407 to +411
odb::dbScanInst* si = firstScanInst(chain);
if (si == nullptr) {
continue;
}
DomainKey key{std::string(si->getScanClock()), si->getClockEdge()};

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 si->getScanClock() returns nullptr (which can happen if the scan clock is not specified or not set), constructing std::string directly from it will cause a segmentation fault. Adding a null check prevents this crash.

Suggested change
odb::dbScanInst* si = firstScanInst(chain);
if (si == nullptr) {
continue;
}
DomainKey key{std::string(si->getScanClock()), si->getClockEdge()};
odb::dbScanInst* si = firstScanInst(chain);
if (si == nullptr) {
continue;
}
const char* clock_name = si->getScanClock();
DomainKey key{clock_name ? std::string(clock_name) : "", si->getClockEdge()};

Comment thread src/dft/CMakeLists.txt
Comment on lines 76 to 80
add_subdirectory(src/architect)
add_subdirectory(src/optimizer)
add_subdirectory(src/cells)
add_subdirectory(src/clock_domain)
add_subdirectory(src/config)

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

The subdirectories are not sorted alphabetically, which violates the # Keep sorted comment.

add_subdirectory(src/architect)
add_subdirectory(src/cells)
add_subdirectory(src/clock_domain)
add_subdirectory(src/config)
add_subdirectory(src/optimizer)

mwsoli and others added 2 commits June 16, 2026 05:15
Signed-off-by: mwsoli <89610174+mwsoli@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: mwsoli <89610174+mwsoli@users.noreply.github.com>
@stefanottili

stefanottili commented Jun 16, 2026

Copy link
Copy Markdown

Watch out !

Unless you have dedicated SO->SI connections, or you're using test gating, just looking at output->SI wire length is the wrong optimization criteria.

I've seen various optimization strategies, including hold time aware ones.
The reported wire length savings never matched the detailed routing reports with/without optimization.
Reason is that if you use Q (or QN)->SI connections, there are "other inputs" connected to this net too.
And if any of these "other inputs" are close to the SI, then the SI connection doesn't have to be "optimized", since it's connection is short and thus "nearly free".

Even worse, I've seen scan chain optimizations leading to routing congestion issues, since SI inputs were unconnected during placement based optimization.

Please correlate scan-chain optimization wire length savings with with/without optimization wire length and be careful with the optimization cost function.

mwsoli and others added 2 commits June 16, 2026 06:56
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: mwsoli <89610174+mwsoli@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Signed-off-by: mwsoli <89610174+mwsoli@users.noreply.github.com>
@mwsoli

mwsoli commented Jun 16, 2026

Copy link
Copy Markdown
Author

Watch out !

Unless you have dedicated SO->SI connections, or you're using test gating, just looking at output->SI wire length is the wrong optimization criteria.

I've seen various optimization strategies, including hold time aware ones. The reported wire length savings never matched the detailed routing reports with/without optimization. Reason is that if you use Q (or QN)->SI connections, there are "other inputs" connected to this net too. And if any of these "other inputs" are close to the SI, then the SI connection doesn't have to be "optimized", since it's connection is short and thus "nearly free".

Even worse, I've seen scan chain optimizations leading to routing congestion issues, since SI inputs were unconnected during placement based optimization.

Please correlate scan-chain optimization wire length savings with with/without optimization wire length and be careful with the optimization cost function.

Thanks for the feedback.
I'm aware that not taking existing routing into consideration is a limitation, and I think this discussed in this paper if I'm correct.

I ran a benchmark on the current implementation, and it indeed shows conflicting results in low scan cell count designs, however the scan chain wirelength decreases with the increase in design scan cell count. These are some results from the benchmark.

Screenshot From 2026-06-16 07-28-18

To calculate the wirelength saving, I sum up the the length of each net connected to a Q pin of each scan cell in the design before and after optimization.
Additionally, I'm planning to add routing awareness as the paper suggests after this merge is completed.

@stefanottili

Copy link
Copy Markdown

To calculate the wirelength saving, I sum up the the length of each net connected to a Q pin of each scan cell in the design before and after optimization.

Unless you only have so->si, this is exactly what’s wrong.
If there is any other input pin in the vicinity of si, then there is 0 gain.

I’m not saying don’t optimize, especially with misr‘s and lot of short chains per clk domain, there is potential for shorter wires. But please take the „other“ input pins into account.

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