Decide the bisection on the coarsest graph: greedy graph-growing for a locality-aware initial cut, then FM to (capped) convergence. The graph is tiny (≈[`COARSEST_TARGET`] vertices), so this is where we spend on quality — the *global* cut is decided here and only locally refined while uncoarsening. We restart from several diverse seeds (see [`INITIAL_RESTARTS`]) and keep the lowest-cut partition,
(lv: Level<'_>, wmin: u64, wmax: u64)
| 1163 | vnets[v as usize].push(i as u32); |
| 1164 | } |
| 1165 | } |
| 1166 | CoarseLevel { vw, bw, nets, vnets, match_map: super_id.to_vec() } |
| 1167 | } |
| 1168 | |
| 1169 | /// Decide the bisection on the coarsest graph: greedy graph-growing for a |
| 1170 | /// locality-aware initial cut, then FM to (capped) convergence. The graph is |
| 1171 | /// tiny (≈[`COARSEST_TARGET`] vertices), so this is where we spend on quality — |
| 1172 | /// the *global* cut is decided here and only locally refined while uncoarsening. |
| 1173 | /// We restart from several diverse seeds (see [`INITIAL_RESTARTS`]) and keep the |
| 1174 | /// lowest-cut partition, which makes graph-growing robust to "leaking" across a |
| 1175 | /// thin bridge between two clusters. |
| 1176 | fn initial_partition(lv: Level<'_>, wmin: u64, wmax: u64) -> Vec<u8> { |
| 1177 | let n = lv.num_vertices(); |
| 1178 | if n == 0 { |
| 1179 | return Vec::new(); |
| 1180 | } |
| 1181 | if n == 1 { |
| 1182 | return vec![0]; |
| 1183 | } |
| 1184 | let total_bw: u64 = lv.bw.iter().sum(); |
| 1185 | // Diverse deterministic seeds: the heaviest vertex plus points spread evenly |
| 1186 | // across the id range (one tends to start far from any inter-cluster bridge). |
| 1187 | let heaviest = (0..n) |
| 1188 | .max_by(|&a, &b| lv.vw[a].cmp(&lv.vw[b]).then(Reverse(a).cmp(&Reverse(b)))) |
| 1189 | .unwrap(); |
| 1190 | let mut seeds = vec![heaviest]; |
| 1191 | for i in 0..INITIAL_RESTARTS { |
| 1192 | let s = (i * n) / INITIAL_RESTARTS; |
| 1193 | if !seeds.contains(&s) { |
| 1194 | seeds.push(s); |
| 1195 | } |
| 1196 | } |
| 1197 | // Select the lowest-cut candidate, but *only* among non-degenerate splits |
| 1198 | // (both sides non-empty) and preferring those within the balance window. A |
| 1199 | // graph-growing run seeded at a light vertex can sweep an entire sub onto one |
| 1200 | // side (cut 0) when a single atomic block holds more than half the balance |
| 1201 | // weight; such a split is both unbalanced and degenerate, and accepting it |
| 1202 | // would leave a shard empty downstream. Key: (unbalanced?, cut), minimized. |
| 1203 | let mut best: Option<((u8, u128), Vec<u8>)> = None; |
| 1204 | for &seed in &seeds { |
| 1205 | let mut side = greedy_grow(lv, total_bw, seed); |
| 1206 | fm_refine(lv, &mut side, wmin, wmax, MAX_FM_PASSES); |
| 1207 | let s0 = (0..n).filter(|&v| side[v] == 0).count(); |
| 1208 | if s0 == 0 || s0 == n { |
| 1209 | continue; // degenerate (one side empty) — never select |
| 1210 | } |
| 1211 | let side0_bw: u64 = |
| 1212 | (0..n).filter(|&v| side[v] == 0).map(|v| lv.bw[v]).sum(); |
| 1213 | let unbalanced = u8::from(side0_bw < wmin || side0_bw > wmax); |
| 1214 | let (_, cut) = NetState::new(lv, &side); |
| 1215 | let key = (unbalanced, cut); |
| 1216 | if best.as_ref().is_none_or(|(bk, _)| key < *bk) { |
no test coverage detected