perf(nlp-bb): wire LNS improvers (RINS + local branching) into _solve_nlp_bb#321
Merged
Conversation
… not solve/reduction/bound
Entry experiment + reevaluation + reduction spike + routing spike. Corrects an
earlier over-pessimistic read and an over-optimistic magnitude.
Measured chain:
* gear4's continuous bound is genuinely 0 (RLT + all separators + optimal cutoff
still 0) -> bound-lifting is out for any convex relaxation.
* reduction spike: cutoff-OBBT + integer probing/shaving stalls at a 2.46M box
(5.76M -> 2.46M -> fixpoint; probing == OBBT) -> reduction is not the lever.
* per-node measurement: gear4 = 9.28 ms/node (spatial path) vs ex1263 = 0.059
ms/node (Rust MILP). But ex1263 is a PURE MILP (no McCormick relaxation) -
apples-to-oranges; gear4 cannot be reformulated to a pure MILP.
* routing spike: the node-LP SOLVE is already on the Rust simplex (backend=
"simplex" default; rust_time~=0). The real per-node cost is REBUILDING the
McCormick relaxation every node: build_milp_relaxation (6244 calls, 5.3s) +
equilibrate_relaxation_lp (8.3s) + matrix/decompose (~3s) ~= half the wall.
Corrected lever: incremental McCormick relaxation reuse (build structure once,
update only bound-dependent envelope coeffs per node) - the #316 pattern applied
to the node-LP. Bound-neutral, low-risk, gate-validated. Realistic ~2x per-node
win (gear4 ~55s -> ~25-30s), NOT full BARON closure (gear4 fundamentally needs a
per-node McCormick LP).
Errors corrected across this investigation: over-generalizing from the cascade_aux
negative result; conflating "continuous bound is 0" with "unsolvable fast";
mistaking a per-node-cost problem for node-count/bound; and the apples-to-oranges
ex1263 magnitude. Each correction was forced by a measurement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lq9NRuyCAWVxWoFv6AFuAt
… into _solve_nlp_bb Corpus diagnosis (25 instances, 10s budget) showed the broad SCIP/BARON gap is dominated by incumbent QUALITY: ~10 instances return sound-but-suboptimal incumbents (12-66% gaps) on the syn/rsyn/clay families. The cause was not missing heuristics — discopt has the full suite (feasibility_pump, diving, RINS, RENS, local_branching, …) — but the IMPROVEMENT layer (RINS + local branching) lived only in solve_model's loop. The syn/rsyn/clay families take the *separate* _solve_nlp_bb path, which had only the root heuristics (feasibility_pump, fractional_diving) and no improvement layer, so they never got polished past the first root incumbent (measured: on rsyn0810m the improvers fired 0 times). Wire the existing improvers into _solve_nlp_bb's node loop, mirroring the proven solve_model pattern: at non-root nodes with an open gap, run RINS (between incumbent and node relaxation) and local branching (Hamming-ball sub-MIP, escalating k). Sound by construction — every candidate is re-verified integer- and constraint-feasible and injected only on strict improvement (tree.inject_incumbent), so the dual bound is never touched. `_lns_enabled` is threaded through both _solve_nlp_bb dispatch sites as the recursion guard (local_branching's sub-solve sets it False, so the layer never nests). Measured (15s budget, vs the corpus baseline): clay0204m 43% -> 12% gap, syn30m 16% -> 12%, rsyn0810m 19% -> 18%; others neutral; all sound (no incumbent ever beats the optimum). Gates: smoke 209 pass; adversarial suite 10/10; perf gate 0 incorrect / 0 regressions (bound-neutral — nvs12/nvs22/gear4 node counts unchanged). Regression: test_nlp_bb_lns_layer.py asserts the layer fires, terminates with a bounded local_branching call count (a broken recursion guard would hang/explode), and never injects an incumbent better than the true optimum. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lq9NRuyCAWVxWoFv6AFuAt
jkitchin
added a commit
that referenced
this pull request
Jun 24, 2026
…engine internals (#322) Maps how Model.solve() routes a problem, built from the actual code (solver.py / problem_classifier.py), in three layers: 1. Trunk — entry → solver/GDP selectors → factorable & integer-bilinear reformulations → classify_problem (LP/QP/MILP/MIQP/NLP/MINLP) → per-class dispatch, with the convexity gates that keep every fast path sound. 2. Subtree A — the spatial McCormick B&B: bound-mode resolution (lp/none-alphaBB/ nlp), RLT/PSD/cut policy, root FBBT/OBBT, the per-node loop (bound, OBBT, separation, LNS, branching), deadline handling, gap certification. 3. Subtree B — _solve_nlp_bb: node-NLP bound, pounce-batch vs serial, the root primal (RENS/feasibility-pump/diving) and the LNS improvement layer (#321). Includes decision tables (criterion → options) at each branch and approximate line anchors. Cited symbols verified to exist. Claude-Session: https://claude.ai/code/session_01Lq9NRuyCAWVxWoFv6AFuAt Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the deployment gap found by the corpus diagnosis: the broad SCIP/BARON gap is dominated by incumbent quality (~10 instances return sound-but-suboptimal incumbents, 12–66% gaps, on the syn/rsyn/clay families). The cause was not missing heuristics — discopt has the full suite — but the improvement layer (RINS + local branching) lived only in
solve_model's loop. The syn/rsyn/clay families take the separate_solve_nlp_bbpath, which had only root heuristics and no improvement layer, so they never got polished past the first root incumbent (on rsyn0810m the improvers fired 0 times).Change
Wire the existing
rins/local_branchinginto_solve_nlp_bb's node loop, mirroring the provensolve_modelpattern: at non-root nodes with an open gap, run RINS (between incumbent and node relaxation) and local branching (Hamming-ball sub-MIP, escalating k).tree.inject_incumbent); the dual bound is never touched._lns_enabledrecursion guard threaded through both_solve_nlp_bbdispatch sites —local_branching's sub-solve sets itFalse, so the layer can never nest.Measured result (15s budget, vs corpus baseline)
All sound (no incumbent beats the optimum). clay0205m still finds no incumbent — a separate finder gap, out of scope here.
Gates (all green)
test_nlp_bb_lns_layer.py: layer fires, terminates with boundedlocal_branchingcalls (catches a broken recursion guard), never injects a super-optimal incumbent.🤖 Generated with Claude Code
https://claude.ai/code/session_01Lq9NRuyCAWVxWoFv6AFuAt