Skip to content

Commit 023e6ec

Browse files
andigclaude
andcommitted
feat: CBC-style threaded branch & bound (-threads N)
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>
1 parent a5784fc commit 023e6ec

5 files changed

Lines changed: 451 additions & 135 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ It fails on any failure not listed in `testdata/pulp_known_failures.txt`.
7777
node bound propagation, reduced-cost fixing per incumbent, SOS1/2, one
7878
restart inheriting cuts/fixings/probes; periodic in-tree rounds of the
7979
globally-valid cut families with cold-restarted open nodes.
80+
- **Threads** (`-threads N`, CBC's opportunistic parallel B&B): N workers over
81+
a shared node heap/incumbent/pseudocosts, each with its own problem copy and
82+
LP; warm bases cross threads factorization-free (as CBC's CoinWarmStartBasis)
83+
and the receiver refactorizes. Serial (`N<=1`) runs the same code path
84+
bit-identically. Tree-heavy instances scale near-linearly (8.4x at 8
85+
threads on a 90x30 multi-knapsack); dive-dominated trees gain what CBC
86+
gains (020: 1.3x at 4 threads vs CBC's 1.25x). In-tree cuts stay
87+
serial-only.
8088
- **Branching**: CBC reliability branching (pseudocosts, `numberBeforeTrust`=10,
8189
capped strong-branch probes, `maxStrong`=5, CBC score) + strong-branch fixing.
8290
- **Heuristics** (reduced-sub-problem first, as CBC's mini branch and bound):

cmd/cbc/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
// section: cuts/presolve/threads only affect performance, not the answer.
2323
var valueOnlyFlags = map[string]bool{
2424
"presolve": true, "gomory": true, "knapsack": true, "probing": true,
25-
"cuts": true, "threads": true, "strong": true, "timemode": true,
25+
"cuts": true, "strong": true, "timemode": true,
2626
"printingoptions": true,
2727
}
2828

@@ -56,6 +56,7 @@ func run(args []string) error {
5656
var solutionFile, mipsFile string
5757
maximize := false
5858
lpOnly := false
59+
threads := 0
5960
// GapAbs 1e-5 = CBC's default cutoff increment (CbcCutoffIncrement):
6061
// nodes within 1e-5 of the incumbent are pruned, as real CBC does
6162
limits := mip.Limits{GapRel: 1e-9, GapAbs: 1e-5}
@@ -91,6 +92,10 @@ func run(args []string) error {
9192
if v, err := strconv.ParseFloat(next(), 64); err == nil {
9293
limits.GapAbs = v
9394
}
95+
case "threads":
96+
if v, err := strconv.Atoi(next()); err == nil {
97+
threads = v
98+
}
9499
case "maxnodes":
95100
if v, err := strconv.Atoi(next()); err == nil {
96101
limits.MaxNodes = v
@@ -138,6 +143,7 @@ func run(args []string) error {
138143
} else {
139144
model := mip.New(p)
140145
model.Limits = limits
146+
model.Threads = threads
141147
res = model.Solve()
142148
}
143149

0 commit comments

Comments
 (0)