Skip to content

Limit nodes in the RENS/RINS depth-first search - #3445

Merged
Opt-Mucca merged 2 commits into
ERGO-Code:latestfrom
DoumarCharles:heur-dfs-node-limit
Oct 20, 2026
Merged

Opt-Mucca merged 2 commits into
ERGO-Code:latestfrom
DoumarCharles:heur-dfs-node-limit

Conversation

@DoumarCharles

Copy link
Copy Markdown

Description

RENS/RINS depth-first search can run until the time limit

Issue

On some MIPs with general-integer columns of wide range, HiGHS spends most of its
time in a single heuristic dive at the root, although the root bound and a good
incumbent are already available. On the attached model the solve takes about 20 s
instead of about 3 s; with a shorter time limit the run ends at the limit instead.

I see it in single-threaded runs (threads = 1, as in WebAssembly builds);
whether a given model stalls depends on the search path, so on the thread count
and the random seed.

Reproduction (attached g101_0188.lp, 2.4 KB, g101_0188.zip a synthetic batching model:
orders split over production lines, line capacities, items made in whole batches;
all columns general integer):

mip_rel_gap = 0
threads = 1
random_seed = 3
status objective nodes time
latest (726863d) Optimal 15055369 45237 21.1–21.4 s
with this PR Optimal 15055369 9573 3.0–3.2 s

(Linux, clang, Release; two runs each.)

Cause. When RENS or RINS cannot fix enough columns to build a sub-MIP, they run
HighsSearch::solveDepthFirst(10) on the heuristic search. That calls dive() with
no node limit (the main branch-and-bound passes one). In the stalled dive the same
general-integer column is chosen again and again: each node tightens its bound by
one unit, the LP stays feasible with the same fractional part, so the dive never
reaches a leaf and the backtrack limit of solveDepthFirst never applies. Only
the time limit or the column's bound range stops it (on the attached model: a
single dive of more than 130,000 nodes, depth growing by one per node).

The earlier mitigation for long dives in the main search (1adcbd0: node limit per
batch with one worker, faster reduced domain-change stacks) is already in latest;
it does not cover this depth-first search inside RENS/RINS.

Change

  • HighsSearch::solveDepthFirst gets an optional node limit (default: unlimited,
    so other callers are unchanged), passed to dive(); the search stops once it is
    reached.
  • RENS and RINS pass a limit of 1,000 nodes. In the cases we looked at, the normal
    depth-first search in these heuristics finishes in tens of nodes.

Stopping the heuristic early only means it may return without a new solution;
the main branch-and-bound continues as before.

Testing

  • ctest: 169/169 passed; unit_tests: all 441 test cases passed (latest 726863d,
    Release, clang, Linux).
  • Synthetic instances from the same generator, single-threaded (200 instances ×
    seeds 0–4, scanned for heuristic dives ≥ 2,000 nodes deep; 9 found). With a
    short time limit, 3 runs that hit it on latest now finish Optimal in 3–5 s
    (2 of them with a better solution), 2 others get faster (3.25 → 0.62 s,
    2.96 → 1.34 s), 3 stay at the limit either way (hard proofs, unrelated to the
    heuristic).
  • MIPLIB 2017 benchmark, no-regression (Windows 11, MSVC 19.51, Release; run on the
    previous latest 8fd6a9a, before the rebase; none of the commits since touch the
    changed files). Selection: the 41 instances latest solves to
    optimality within 60 s (threads 1, seed 0). Comparison at 120 s, threads 1,
    seeds 0/1/2, plus default threads seed 0: identical solved counts (41/40/38
    and 41), no objective differences, and an identical search in every finished
    run (node counts, bounds and LP iterations equal); the node limit never takes
    effect on these instances. Shifted geometric mean of solve time (shift 1 s),
    patch / latest: 0.999 (threads 1), 1.003 (default threads).
  • The reproduction also shows on the same Windows builds (latest stalls in the
    dive; patch Optimal 15055369 in under 5 s).

Checklist

  • I have read the contributing guidelines
  • This PR targets the latest branch
  • Tests are passing
  • Documentation was updated where relevant
  • This PR is not primarily AI-generated (per the AI contributions policy in CONTRIBUTING.md)

When RENS or RINS cannot fix enough columns for a sub-MIP they run
HighsSearch::solveDepthFirst(10), which dives with no node limit. A dive
that tightens one general-integer bound by a single unit per node can then
run until the time limit. solveDepthFirst gets an optional node limit
(default unlimited, so other callers are unchanged); RENS and RINS pass
1000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Opt-Mucca
Opt-Mucca self-requested a review October 19, 2026 15:22
@Opt-Mucca

Copy link
Copy Markdown
Collaborator

@DoumarCharles I've been meaning to look into budgets for this bit of the code for a while......

I think adding a limit of 1000 here is appropriate. Can you remove the comments that you've added? (I don't believe they're necessary).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DoumarCharles

Copy link
Copy Markdown
Author

Can you remove the comments that you've added? (I don't believe they're necessary).

Done - I’ve removed the comments. Happy to adjust anything else, e.g. if you’d prefer the limit as a named option rather than a constant. In practice this makes a real difference for our single-threaded runs as it prevented cases from timing out.

@jajhall

jajhall commented Oct 19, 2026

Copy link
Copy Markdown
Member

In practice this makes a real difference for our single-threaded runs as it prevented cases from timing out.

This is a good outcome. It's interesting that users are able to report increasingly detailed information about why HiGHS fails or is slow on their instances. We can't adopt all fixes, as they may slow HiGHS down generally, but here you've landed on something useful

@codecov

codecov Bot commented Oct 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.11%. Comparing base (d5e8e73) to head (287a629).
⚠️ Report is 4 commits behind head on latest.

Additional details and impacted files
@@           Coverage Diff           @@
##           latest    #3445   +/-   ##
=======================================
  Coverage   75.10%   75.11%           
=======================================
  Files         460      460           
  Lines      112008   112031   +23     
  Branches    17757    17759    +2     
=======================================
+ Hits        84125    84153   +28     
+ Misses      27601    27596    -5     
  Partials      282      282           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Opt-Mucca Opt-Mucca left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DoumarCharles looks good! Thanks for making the changes.

@Opt-Mucca
Opt-Mucca merged commit b128287 into ERGO-Code:latest Oct 20, 2026
380 checks passed
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.

3 participants