Skip to content

feat: CBC-style threaded branch & bound (-threads N)#25

Open
andig wants to merge 1 commit into
mainfrom
feat/threaded-bb
Open

feat: CBC-style threaded branch & bound (-threads N)#25
andig wants to merge 1 commit into
mainfrom
feat/threaded-bb

Conversation

@andig

@andig andig commented Jul 15, 2026

Copy link
Copy Markdown
Member

Adds Model.Threads / CLI -threads N: opportunistic parallel branch & bound modeled on CBC's.

Design

  • N workers, one shared tree: workers pop from a shared best-first heap and plunge depth-first, sharing the incumbent, node counter, dive budget, and pseudocost arrays (mutex-guarded, as CBC shares pseudocosts across threads). Worker 0 is the model itself; extra workers are clones with their own problem copy and LP — reduced-cost fixing mutates column bounds, and a factor's solve scratch belongs to the LP that built it, so nothing LP-adjacent is shared.
  • Warm bases cross threads factorization-free: State.Portable() carries status/basicOf/value only; the receiving worker's warmSolve refactorizes on first use — the same contract as CBC handing CoinWarmStartBasis between threads. Within a plunge the full factorized state is kept as before.
  • Per-worker reduced-cost fixing: when another worker tightens the incumbent, a worker re-fixes on its own LP against the shared root duals before its next node solve.
  • In-tree cuts stay serial-only: local cut rows append to one worker's problem copy and would be dangling row indices everywhere else. Root cuts (before the tree) are unchanged and shared by all clones.
  • Serial unchanged: Threads<=1 runs the identical treeSearch code path with uncontended locks — 018/020/021 keep bit-identical nodes/pivots/objectives vs main (7/31746, 774/182619, 3/62278).

Benchmarks

case t=1 t=2 t=4 t=8
90×30 multi-knapsack (5927 nodes) 1.58s 0.50s 0.36s 0.19s (8.4×)
020 (774 nodes) 10.4s 9.1s 7.8s (1.33×) 8.6s
018 (7 nodes) / 021 (3 nodes) unchanged — no tree to parallelize

Real CBC on 020 shows the same shape: t=1→t=4 is 2.0s→1.6s (1.25×) with nodes inflating 271→832; opportunistic parallel B&B trades node count for wall clock, and dive-dominated trees gain little. Objectives across thread counts differ only within the configured gap (GapAbs 1e-5), as with real CBC.

Race detector clean over the unit tests and a full threaded 020 solve (96s under -race, zero warnings).

🤖 Generated with Claude Code

Opportunistic parallel tree search as CBC's: N workers over a shared
node heap, incumbent and pseudocost arrays, each worker on its own
problem copy + LP (reduced-cost fixing mutates column bounds, and a
factor's solve scratch belongs to the LP that built it). Nodes crossing
the shared heap carry a Portable() basis — status/values only, no
factorization — and the receiving worker refactorizes, the same
contract as CBC handing CoinWarmStartBasis between threads. Each
worker re-applies reduced-cost fixing when another worker tightens the
incumbent. In-tree cut generation stays serial-only (local cut rows
would be dangling row indices in other workers' problems).

Serial (Threads<=1) runs the same treeSearch code path with
uncontended locks: 018/020/021 keep bit-identical nodes, pivots and
objectives vs main (7/31746, 774/182619, 3/62278).

Benchmarks: 90x30 multi-knapsack 1.58s -> 0.19s at 8 threads (8.4x,
same optimum); 020 10.4s -> 7.8s at 4 threads (1.33x) with the same
node-inflation pattern real CBC shows there (271 -> 832 nodes, 1.25x).
Race detector clean over unit tests and a full threaded 020 solve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant