Recursively split `sub` into `k` shards (contiguous ids `base..base+k`), writing shard ids into `shard_of` (indexed by global block id). The `k` leaf budget is allocated between the two sides ~proportional to heartbeat mass, but clamped so each side receives at least 1 leaf and at most its own vertex count. That guarantees **every shard is non-empty** whenever the subtree holds at least `k` verti
( sub: &SubHyper, k: usize, base: u32, epsilon: f64, shard_of: &[AtomicU32], prog: &PartitionProgress, )
| 526 | } |
| 527 | } |
| 528 | |
| 529 | /// Recursively split `sub` into `k` shards (contiguous ids `base..base+k`), |
| 530 | /// writing shard ids into `shard_of` (indexed by global block id). |
| 531 | /// |
| 532 | /// The `k` leaf budget is allocated between the two sides ~proportional to |
| 533 | /// heartbeat mass, but clamped so each side receives at least 1 leaf and at most |
| 534 | /// its own vertex count. That guarantees **every shard is non-empty** whenever |
| 535 | /// the subtree holds at least `k` vertices (at the top, `#blocks ≥ 2ⁿ`), so the |
| 536 | /// full parallelism is realized; the heartbeat-proportional split also keeps |
| 537 | /// per-shard work as even as the atomic blocks allow. The resulting bisection |
| 538 | /// tree is a (possibly unbalanced) binary tree — still a valid aggregation tree. |
| 539 | fn rec_bisect( |
| 540 | sub: &SubHyper, |
| 541 | k: usize, |
| 542 | base: u32, |
| 543 | epsilon: f64, |
| 544 | shard_of: &[AtomicU32], |
| 545 | prog: &PartitionProgress, |
| 546 | ) -> AggNode { |
| 547 | if k <= 1 || sub.num_vertices() <= 1 { |
| 548 | // A leaf (or a subtree with ≤1 vertex): everything here is one shard. |
| 549 | for &g in &sub.global { |
| 550 | shard_of[g as usize].store(base, Ordering::Relaxed); |
| 551 | } |
| 552 | prog.leaf(); |
| 553 | return AggNode::Leaf(base); |
| 554 | } |
| 555 | prog.bisecting(sub.num_vertices(), k); |
| 556 | let side = bisect(sub, epsilon); |
| 557 | let keep0: Vec<bool> = side.iter().map(|&s| s == 0).collect(); |
| 558 | let keep1: Vec<bool> = side.iter().map(|&s| s == 1).collect(); |
| 559 | let left = sub.induce(&keep0); |
| 560 | let right = sub.induce(&keep1); |
| 561 | let nl = left.num_vertices(); |
| 562 | let nr = right.num_vertices(); |
| 563 | if nl == 0 || nr == 0 { |
| 564 | // Degenerate split: `bisect` guarantees both sides non-empty for |sub| ≥ 2 |
| 565 | // (its initial partition rejects empty-sided candidates), so this is a |
| 566 | // defensive backstop only — keep the subtree together as one shard. |
| 567 | for &g in &sub.global { |
| 568 | shard_of[g as usize].store(base, Ordering::Relaxed); |
| 569 | } |
| 570 | prog.leaf(); |
| 571 | return AggNode::Leaf(base); |
| 572 | } |
| 573 | // Allocate leaves ~proportional to heartbeat mass, clamped to [1, side size]. |
| 574 | // `lo`/`hi` are feasible (lo ≤ hi) because nl + nr ≥ k with nl, nr ≥ 1. |
| 575 | let wl: u128 = left.vw.iter().map(|&w| u128::from(w)).sum(); |
| 576 | let wr: u128 = right.vw.iter().map(|&w| u128::from(w)).sum(); |
| 577 | let wsum = (wl + wr).max(1); |
| 578 | let ideal = ((k as u128 * wl + wsum / 2) / wsum) as usize; |
| 579 | let lo = 1.max(k.saturating_sub(nr)); |
| 580 | let hi = (k - 1).min(nl); |
| 581 | let k_left = ideal.clamp(lo, hi); |
| 582 | let rbase = base + k_left as u32; |
| 583 | // The two halves are independent (disjoint blocks) — recurse them in parallel |
| 584 | // when the work is large enough to amortize the join. The result is identical |
| 585 | // to serial execution (each subtree is deterministic). Each call returns its |
no test coverage detected