drt/dst: distributed detailed-routing CI regressions + de-flake (test-only)#10854
drt/dst: distributed detailed-routing CI regressions + de-flake (test-only)#10854saurav-fermions wants to merge 4 commits into
Conversation
Wire OpenROAD's distributed detailed_route into the test suite single-host,
with a built-in correctness oracle (distributed routed DEF == non-distributed
baseline). This turns an experimental, untested path into a regression-protected
one (workstream A of DIST_PNR_SCOPING.md).
The distributed routing engine itself is correct: on gcd_nangate45 the
distributed result is byte-identical to the single-host baseline. The
experimental path's problems were entirely in the test harness orchestration,
not the router. New harness gcd_nangate45_distributed_local.tcl fixes them with
no router source changes:
- dynamic free-port allocation instead of hard-coded 1111/1112/1234
(avoids DST-0009 Address-already-in-use on shared/CI hosts)
- poll for worker/balancer readiness instead of a fixed sleep
- blocking baseline child (no /proc race) and guaranteed child cleanup
Registered as a PASSFAIL test in CMake (verified: drt suite 23/23 green) and in
Bazel (tagged manual/requires-network; the sandbox may forbid sockets).
No changes to src/drt or src/dst C++.
Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
…dation Workstream B: the dst distributed test was disabled as "race-prone". Root cause was hard-coded ports (collide with leftover/concurrent processes on a shared host) plus an unsynchronized load-balancer startup (the balancer binds its acceptor on a detached thread; the test sent a job before it was listening, relying only on sendJob's 250ms connect-retry budget). Fix is test-only: allocate ephemeral ports (bind :0) and poll for listener readiness before sending. Re-enabled in CMake (add_test) and Bazel (cc_binary -> cc_test). The same fixed-port fragility affected the already-CI TestWorker (port 1234) and TestBalancer (5555-5559); both get the same fix. All 3 dst tests now pass reliably (20x sequential + 24x concurrent + 10x suite, zero failures) even with stale processes holding the old ports. Workstream C: validated distributed detailed_route across multiple worker processes on a single host (1/2/4 workers + balancer, local shared dir). The routed DEF is byte-identical to the non-distributed baseline at every worker count (DEF oracle PASS, 0 violations). The detailed-routing phase parallelizes ~1.8x (2 workers) / ~2.2x (4 workers) on aes_nangate45 but saturates on a serial floor (data prep, track assignment, design serialization, DEF I/O); 1 worker is slower than non-distributed. Added a fast 2-worker gcd passfail regression (CMake PASSFAIL_TESTS + Bazel) with the DEF oracle, plus a reusable N-worker harness and a heavier aes timing script. Scope: single-host multi-worker only. True multi-host (separate machines + real NFS) and distributed global placement remain future work. See DIST_MULTIHOST_INVESTIGATION.md and AGENT_REPORT.md. No router/framework C++ changed; non-distributed drt suite unchanged (23/23). Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
…_PORTS
The local single-host distributed-route test shares distributed_balancer.tcl
with the multiworker harness. That balancer was upgraded to read its worker
list from a single comma-separated env var DRT_WORKER_PORTS, but the local
test still set the now-dead DRT_WORKER1_PORT/DRT_WORKER2_PORT and never set
DRT_WORKER_PORTS. The balancer therefore aborted at startup with
[ERROR DRT-9002] DRT_WORKER_PORTS not set, never listened, and
wait_for_port timed out -> [ERROR DRT-9005] Balancer did not come up.
Option A (minimal harness fix): set DRT_WORKER_PORTS="$w1,$w2" before
launching the balancer to match the shared balancer contract, and drop the
two unused DRT_WORKER{1,2}_PORT vars. The test already used the robust
ephemeral-port + readiness-polling pattern; only the env-var contract was
stale. tcl-only change, no rebuild.
Proof: ctest -R distributed -j1 run 3x -> local + multiworker pass every
run (local ~7s vs the prior 34s timeout-failure). ctest -R "^drt" -j1 ->
24/24 pass.
Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
There was a problem hiding this comment.
Code Review
This pull request re-enables and robustifies distributed routing tests by replacing hard-coded ports with dynamic ephemeral port allocation and polling for listener readiness. It also introduces a multi-worker distributed routing test harness. The review feedback suggests allocating multiple ports simultaneously to avoid sequential allocation collisions, ensuring a minimum thread count of 1 on machines with fewer than 4 CPUs, removing duplicate file entries in the Bazel BUILD file, and replacing inefficient external exec sleep calls in Tcl scripts with the native after command.
| proc alloc_free_port {} { | ||
| set s [socket -server {apply {{args {}}}} 0] | ||
| set port [lindex [fconfigure $s -sockname] 2] | ||
| close $s | ||
| return $port | ||
| } |
There was a problem hiding this comment.
In alloc_free_port, the socket is closed immediately after retrieving the port. When allocating multiple ports sequentially, the OS is highly likely to return the exact same port because it was just freed. This can cause port collisions when the workers and balancer are launched. Instead, allocate all required ports simultaneously by keeping the sockets open until all ports are obtained.
proc alloc_free_ports { count } {
set sockets {}
set ports {}
for {set i 0} {$i < $count} {incr i} {
set s [socket -server {apply {{args {}}}} 0]
lappend sockets $s
lappend ports [lindex [fconfigure $s -sockname] 2]
}
foreach s $sockets {
close $s
}
return $ports
}
| uint16_t allocateFreePort(boost::asio::io_context& service) | ||
| { | ||
| using boost::asio::ip::tcp; | ||
| tcp::acceptor probe(service); | ||
| probe.open(tcp::v4()); | ||
| probe.bind(tcp::endpoint(tcp::v4(), 0)); | ||
| const uint16_t port = probe.local_endpoint().port(); | ||
| probe.close(); | ||
| return port; | ||
| } |
There was a problem hiding this comment.
In allocateFreePort, the acceptor is closed immediately after retrieving the port. When allocating multiple ports sequentially, the OS is highly likely to return the exact same port because it was just freed. This can cause port collisions when the workers and balancer are launched. Instead, allocate all required ports simultaneously by keeping the acceptors open until all ports are obtained.
std::vector<uint16_t> allocateFreePorts(boost::asio::io_context& service, size_t count)
{
using boost::asio::ip::tcp;
std::vector<tcp::acceptor> acceptors;
acceptors.reserve(count);
std::vector<uint16_t> ports;
for (size_t i = 0; i < count; ++i) {
acceptors.emplace_back(service);
auto& probe = acceptors.back();
probe.open(tcp::v4());
probe.bind(tcp::endpoint(tcp::v4(), 0));
ports.push_back(probe.local_endpoint().port());
}
return ports;
}| read_lef Nangate45/Nangate45_stdcell.lef | ||
| read_def $DIST_PREROUTE_DEF | ||
| read_guides $DIST_GUIDE | ||
| set_thread_count [expr { [cpu_count] / 4 }] |
There was a problem hiding this comment.
| read_lef Nangate45/Nangate45_stdcell.lef | ||
| read_def gcd_nangate45_preroute.def | ||
| read_guides gcd_nangate45.route_guide | ||
| set_thread_count [expr { [cpu_count] / 4 }] |
There was a problem hiding this comment.
| boost::asio::io_context port_picker; | ||
| const uint16_t worker_port = allocateFreePort(port_picker); | ||
| uint16_t balancer_port = allocateFreePort(port_picker); | ||
| // Ensure the two ports differ even in the unlikely event the OS hands back | ||
| // the same just-freed port twice. | ||
| while (balancer_port == worker_port) { | ||
| balancer_port = allocateFreePort(port_picker); | ||
| } |
There was a problem hiding this comment.
Use the new allocateFreePorts helper to allocate all required ports simultaneously to prevent port collisions and eliminate the need for the while loop retry logic.
| boost::asio::io_context port_picker; | |
| const uint16_t worker_port = allocateFreePort(port_picker); | |
| uint16_t balancer_port = allocateFreePort(port_picker); | |
| // Ensure the two ports differ even in the unlikely event the OS hands back | |
| // the same just-freed port twice. | |
| while (balancer_port == worker_port) { | |
| balancer_port = allocateFreePort(port_picker); | |
| } | |
| boost::asio::io_context port_picker; | |
| auto ports = allocateFreePorts(port_picker, 2); | |
| const uint16_t worker_port = ports[0]; | |
| const uint16_t balancer_port = ports[1]; |
| set worker1_port [alloc_free_port] | ||
| set worker2_port [alloc_free_port] | ||
| set balancer_port [alloc_free_port] |
| asio::io_context port_picker; | ||
| uint16_t balancer_port = allocateFreePort(port_picker); | ||
| uint16_t worker_port_1 = allocateFreePort(port_picker); | ||
| uint16_t worker_port_2 = allocateFreePort(port_picker); | ||
| uint16_t worker_port_3 = allocateFreePort(port_picker); | ||
| uint16_t worker_port_4 = allocateFreePort(port_picker); |
There was a problem hiding this comment.
Use the new allocateFreePorts helper to allocate all required ports simultaneously to prevent port collisions.
| asio::io_context port_picker; | |
| uint16_t balancer_port = allocateFreePort(port_picker); | |
| uint16_t worker_port_1 = allocateFreePort(port_picker); | |
| uint16_t worker_port_2 = allocateFreePort(port_picker); | |
| uint16_t worker_port_3 = allocateFreePort(port_picker); | |
| uint16_t worker_port_4 = allocateFreePort(port_picker); | |
| asio::io_context port_picker; | |
| auto ports = allocateFreePorts(port_picker, 5); | |
| uint16_t balancer_port = ports[0]; | |
| uint16_t worker_port_1 = ports[1]; | |
| uint16_t worker_port_2 = ports[2]; | |
| uint16_t worker_port_3 = ports[3]; | |
| uint16_t worker_port_4 = ports[4]; |
| "gcd_nangate45_distributed.tcl", | ||
| "distributed_worker.tcl", | ||
| "distributed_balancer.tcl", |
There was a problem hiding this comment.
The files distributed_worker.tcl and distributed_balancer.tcl are listed twice in the srcs list of the regression_resources filegroup (once at lines 68 and 70, and again at lines 74 and 75). Remove the duplicate entries to keep the filegroup clean and maintainable.
| "gcd_nangate45_distributed.tcl", | |
| "distributed_worker.tcl", | |
| "distributed_balancer.tcl", | |
| "gcd_nangate45_distributed.tcl", |
| if { [port_is_up $port] } { | ||
| return 1 | ||
| } | ||
| exec sleep 0.1 |
| if { [port_is_up $port] } { | ||
| return 1 | ||
| } | ||
| exec sleep 0.1 |
9d952ee to
ca77389
Compare
…p BUILD srcs - set_thread_count now uses max(1, [cpu_count]/4) so hosts/CI runners with fewer than 4 CPUs still get at least one worker thread (integer division no longer yields 0). - Remove duplicate distributed_worker.tcl / distributed_balancer.tcl entries from the regression_resources filegroup srcs. Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
ca77389 to
5f5bb44
Compare
|
Thanks — this is a draft (test-only; the distributed integration tests need multi-process/network orchestration that my local sandbox can't provide, so they're the
On the port-allocation comments (socket/acceptor closed before the port is used → theoretical reuse race): the poll-for-readiness + retry-on-bind path already tolerates a transient collision, but I take the point that allocating all ports up front is cleaner; I'll fold the |
Summary
Turns OpenROAD's experimental distributed detailed-route path into a regression-protected capability and de-flakes the
dstdistributed test framework. Test-harness / regression only — nosrc/drtorsrc/dstrouter/framework C++ source is changed.gcd_nangate45_distributed_local.tclwith a built-in correctness oracle (distributed routed DEF == non-distributed baseline). Fixes the experimental path's orchestration with no router source changes: dynamic free-port allocation (avoids DST-0009 Address-already-in-use), poll for worker/balancer readiness instead of fixed sleep, blocking baseline child + guaranteed cleanup.dstframework — root-cause fix for the disabled distributed test (hard-coded ports + unsynchronized balancer startup): ephemeral ports (bind :0) + poll for listener readiness; same fix for the CITestWorker/TestBalancer. Re-enabled in CMake and Bazel. Adds single-host multi-worker validation (1/2/4 workers): routed DEF byte-identical to baseline at every worker count.DRT_WORKER_PORTSsingle env var).Testing
dstC++ unit tests pass locally:TestWorker,TestBalancer,TestDistributed.gcd_nangate45_distributed_local,..._multiworker) require multi-process/network orchestration (they spawn a balancer + workers on TCP ports). My local sandbox cannot bring up the balancer process on a network port (DRT-9005), so those two are unverified locally — they are therequires-network/manual-labeled regressions intended to run in the project's CI. Marked draft for that reason.Notes
PASSFAILin CMake andmanual/requires-networkin Bazel.