dft: add scan chain wirelength optimizer (scan_opt)#10666
Conversation
Signed-off-by: MohamedWHassan <s-mohamedwalled@zewailcity.edu.eg>
Signed-off-by: MohamedWHassan <s-mohamedwalled@zewailcity.edu.eg>
Signed-off-by: MohamedWHassan <s-mohamedwalled@zewailcity.edu.eg>
Signed-off-by: MohamedWHassan <s-mohamedwalled@zewailcity.edu.eg>
Signed-off-by: MohamedWHassan <s-mohamedwalled@zewailcity.edu.eg>
There was a problem hiding this comment.
Welcome to OpenROAD! Thanks for opening your first PR.
Before we review:
- Contribution Guide: https://openroad.readthedocs.io/en/latest/contrib/contributing.html
- Build Instructions: https://openroad.readthedocs.io/en/latest/contrib/BuildWithCMake.html
Please ensure:
- CI passes
- Code is properly formatted
- Tests are included where applicable
A maintainer will review shortly!
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | |
| } |
| 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."); | ||
| } |
There was a problem hiding this comment.
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.
| 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."); | |
| } |
| odb::dbScanInst* si = firstScanInst(chain); | ||
| if (si == nullptr) { | ||
| continue; | ||
| } | ||
| DomainKey key{std::string(si->getScanClock()), si->getClockEdge()}; |
There was a problem hiding this comment.
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.
| 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()}; |
| add_subdirectory(src/architect) | ||
| add_subdirectory(src/optimizer) | ||
| add_subdirectory(src/cells) | ||
| add_subdirectory(src/clock_domain) | ||
| add_subdirectory(src/config) |
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>
|
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. 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. |
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>
Thanks for the feedback. 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.
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. 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. |

Summary
Adds a post-placement scan chain wirelength optimizer to the DFT flow (
scan_optTcl command).The optimizer runs after
execute_dft_planand 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
Impact
scan_optis 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
./etc/Build.sh).