Skip to content

perf(nlp-bb): wire LNS improvers (RINS + local branching) into _solve_nlp_bb#321

Merged
jkitchin merged 2 commits into
mainfrom
perf/lns-improvers-in-nlp-bb
Jun 24, 2026
Merged

perf(nlp-bb): wire LNS improvers (RINS + local branching) into _solve_nlp_bb#321
jkitchin merged 2 commits into
mainfrom
perf/lns-improvers-in-nlp-bb

Conversation

@jkitchin

Copy link
Copy Markdown
Owner

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_bb path, 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_branching 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); the dual bound is never touched.
  • _lns_enabled recursion guard threaded through both _solve_nlp_bb dispatch sites — local_branching's sub-solve sets it False, so the layer can never nest.

Measured result (15s budget, vs corpus baseline)

instance base gap new gap
clay0204m 43% 12%
syn30m 16% 12%
rsyn0810m 19% 18%
others neutral

All sound (no incumbent beats the optimum). clay0205m still finds no incumbent — a separate finder gap, out of scope here.

Gates (all green)

  • smoke 209 passed
  • adversarial soundness suite 10/10
  • perf gate: 0 incorrect, 0 regressions (bound-neutral — nvs12/nvs22/gear4 node counts unchanged)
  • test_nlp_bb_lns_layer.py: layer fires, terminates with bounded local_branching calls (catches a broken recursion guard), never injects a super-optimal incumbent.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lq9NRuyCAWVxWoFv6AFuAt

jkitchin and others added 2 commits June 24, 2026 08:01
… 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 jkitchin merged commit e1ae380 into main Jun 24, 2026
6 checks passed
@jkitchin jkitchin deleted the perf/lns-improvers-in-nlp-bb branch June 24, 2026 15:16
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>
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