From ed35378233b08fe75e72b8e4e5046f2d599c6677 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 6 Oct 2026 12:59:11 -0700 Subject: [PATCH 1/2] fix(router): fix nondeterminism in autorouter --- substrate/src/layout/routing/auto/abs.rs | 53 ++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/substrate/src/layout/routing/auto/abs.rs b/substrate/src/layout/routing/auto/abs.rs index 24f4166..ef8ebad 100644 --- a/substrate/src/layout/routing/auto/abs.rs +++ b/substrate/src/layout/routing/auto/abs.rs @@ -3,7 +3,7 @@ //! These APIs deal with abstract routing notions (tracks, layers, etc.) //! rather than raw layout (rectangles, GDS layers, etc.). -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use grid::Grid; use itertools::Itertools; @@ -334,7 +334,10 @@ struct AbstractNetInfo { curr_net: Net, curr_group: ConnectionGroup, assigned_nets: HashSet, - conn_groups: HashMap>, + /// Ordered so that `pos_in_group` lists positions in the same order on every run: + /// the router's search takes them as jump successors, and with a hash set the + /// path it picks among equal-length routes varied from process to process. + conn_groups: HashMap>, } impl AbstractNetInfo { @@ -363,7 +366,7 @@ impl AbstractNetInfo { self.curr_group.0 += 1; } } - self.conn_groups.insert(self.curr_group, HashSet::new()); + self.conn_groups.insert(self.curr_group, BTreeSet::new()); self.curr_group } @@ -831,4 +834,48 @@ mod tests { ) .expect("failed to route"); } + + /// A route that joins an existing route of its own net may start from any point of + /// that route, and here five of them are equally close to the destination. The + /// choice must not depend on hash iteration order, which differs between router + /// instances and between processes. + #[test] + fn test_jump_successor_choice_is_deterministic() { + let route_once = || { + let mut router = GreedyAbstractRouter::new( + vec![ + AbstractLayerConfig { + grid_space: 1, + dir: Dir::Horiz, + }, + AbstractLayerConfig { + grid_space: 1, + dir: Dir::Vert, + }, + ], + 20, + 20, + ); + let net = router.get_unused_net(); + router + .route_with_net( + Pos::new(Layer(0), 0, 10).into(), + Pos::new(Layer(0), 19, 10).into(), + net, + ) + .expect("failed to route the trunk"); + // Reachable in the same number of steps from (3..=7, 10) on the trunk. + let dst = PosSpanBuilder::with_layer(Layer(0)) + .with(Dir::Horiz, 3, 7) + .with(Dir::Vert, 15, 15) + .build(); + router + .route_with_net(Pos::new(Layer(0), 0, 10).into(), dst, net) + .expect("failed to route the branch") + }; + let first = route_once(); + for _ in 0..32 { + assert_eq!(route_once(), first); + } + } } From 20a1e0b944eb5629c14c44436467731fba22eff1 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 6 Oct 2026 16:33:52 -0700 Subject: [PATCH 2/2] Use IndexSet instead of BTreeSet --- Cargo.toml | 2 ++ substrate/Cargo.toml | 2 ++ substrate/src/layout/routing/auto/abs.rs | 20 +++++++++++++------- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f727ae3..4b34b0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,8 @@ num-derive = "0.5" num-traits = "0.2" paste = "1" pathfinding = "4" +indexmap = "2" +rustc-hash = "2" proc-macro-crate = "3" proc-macro2 = "1" psfparser = { git = "https://github.com/ucb-substrate/substrate2", branch = "main" } diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index df5f3ca..a8df334 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -27,6 +27,8 @@ regex = { workspace = true } grid = { workspace = true } state = { workspace = true } pathfinding = { workspace = true } +indexmap = { workspace = true } +rustc-hash = { workspace = true } itertools = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } diff --git a/substrate/src/layout/routing/auto/abs.rs b/substrate/src/layout/routing/auto/abs.rs index ef8ebad..5e51e9c 100644 --- a/substrate/src/layout/routing/auto/abs.rs +++ b/substrate/src/layout/routing/auto/abs.rs @@ -3,10 +3,12 @@ //! These APIs deal with abstract routing notions (tracks, layers, etc.) //! rather than raw layout (rectangles, GDS layers, etc.). -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; use grid::Grid; +use indexmap::IndexSet; use itertools::Itertools; +use rustc_hash::FxBuildHasher; use subgeom::Dir; use super::error::*; @@ -334,10 +336,11 @@ struct AbstractNetInfo { curr_net: Net, curr_group: ConnectionGroup, assigned_nets: HashSet, - /// Ordered so that `pos_in_group` lists positions in the same order on every run: - /// the router's search takes them as jump successors, and with a hash set the - /// path it picks among equal-length routes varied from process to process. - conn_groups: HashMap>, + /// Insertion-ordered so that `pos_in_group` lists positions in the same order on + /// every run and platform: the router's search takes them as jump successors, and + /// with a randomly seeded hash set the path it picked among equal-length routes + /// varied from process to process. + conn_groups: HashMap>, } impl AbstractNetInfo { @@ -366,13 +369,16 @@ impl AbstractNetInfo { self.curr_group.0 += 1; } } - self.conn_groups.insert(self.curr_group, BTreeSet::new()); + self.conn_groups + .insert(self.curr_group, IndexSet::default()); self.curr_group } fn delete_from_group(&mut self, pos: Pos, conn_group: ConnectionGroup) { if let Some(v) = self.conn_groups.get_mut(&conn_group) { - v.remove(&pos); + // Moves the last position into the gap: O(1), and the resulting order is + // still a function of the operation sequence alone. + v.swap_remove(&pos); } }