Skip to content

Use the thread-safe aliases in the kZeroObjSingletonContinuousCol undo case - #3306

Merged
jajhall merged 1 commit into
ERGO-Code:latestfrom
oaasis-ai:fix/upstream-thread-safe-undo-zeroobj-singleton
Sep 21, 2026
Merged

jajhall merged 1 commit into
ERGO-Code:latestfrom
oaasis-ai:fix/upstream-thread-safe-undo-zeroobj-singleton

Conversation

@MathieuDutSik

@MathieuDutSik MathieuDutSik commented Sep 20, 2026

Copy link
Copy Markdown

Description

Fixes a crash in the parallel MIP search: kZeroObjSingletonContinuousCol is the one HighsPostsolveStack::undo case that does not use the thread-safe aliases.

undo aliases its three mutable members to thread-local copies when thread_safe is set (HighsPostsolveStack.h:1069-1071):

HighsDataStack& reductionValues_ = thread_safe ? reductionValuesCopy : reductionValues;
std::vector<Nonzero>& colValues_  = thread_safe ? colValuesCopy      : colValues;
std::vector<Nonzero>& rowValues_  = thread_safe ? rowValuesCopy      : rowValues;

Fifteen of the sixteen switch cases go through those aliases. kZeroObjSingletonContinuousCol pops from the members instead, so with thread_safe = true it reads the member stack at a position nobody reset — undoPrimal calls resetPosition() on the member stack only in the serial path — and mutates state shared with the other workers.

HighsDataStack::pop(std::vector<T>&) reads the element count from the stack at the current position and resizes to it, so a stale position yields an arbitrary count: std::length_error when it exceeds max_size(), std::bad_alloc when it does not, and an out-of-bounds read when position underflows past zero. When it does not throw, the singleton comes back as 0 and the ranged row it was removed from is left violated.

HighsMipWorker::transformNewIntegerFeasibleSolution is the only caller passing thread_safe = true, which is why this appears only in a parallel MIP search. The throw lands on a HighsTaskExecutor worker with no handler above it, so std::terminate runs and the process dies; Highs::optimizeModelTryCatch only guards the main-thread chain, which is why the same corruption sometimes surfaces as a returned Error instead.

The case was added in 9cce7235d9 ("Remove continuous singletons from double-sided rows", 2026-04-10), seven months after 848ced1785 ("Add thread safe undoPrimal option", 2025-09-11) introduced thread_safe, so the new case body was written against the pre-thread_safe shape.

The fix is three lines, using the aliases already in scope.

Verification

  • The full unit suite is green.
  • clang-format --dry-run -Werror is clean on the changed file.

Separately, on our fork (upstream latest at 939eba361d plus some local MIP-option commits, so not a clean latest build), a 2,982-row / 3,450-column / 540-integer dispatch model that aborted with std::length_error 3/3 at the shipped parallel=on, threads=0 default now solves to optimality 5/5, and a larger model that aborted with std::bad_alloc via RINSsolveSubMiptrySolution solves 3/3. The patched objectives match the threads=1 and parallel=off runs, so this restores correct postsolve rather than only suppressing the throw.

Checklist

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

The analysis and patch were AI-assisted; I have reviewed and verified them. Leaving that box unchecked deliberately rather than misreporting it — the source change is the three-line alias swap, so please take it directly if that is the easier path for you.

Related issue

Closes #3305

Secondary and out of scope here: HighsTaskExecutor::run_worker / HighsSplitDeque::runStolenTask let any std::exception escape a task to std::terminate, which is what turns this bug into a process death rather than an error return.

…o case

HighsPostsolveStack::undo aliases reductionValues/colValues/rowValues to
thread-local copies when thread_safe is set. The
kZeroObjSingletonContinuousCol case pops from the members instead, so with
thread_safe = true it reads the member stack at a position no one reset and
mutates state shared with the other workers.

HighsDataStack::pop(std::vector<T>&) reads the element count from the stack
at that position and resizes to it, so a stale position yields an arbitrary
count: std::length_error when it exceeds max_size(), std::bad_alloc when it
does not, and an out-of-bounds read when position underflows.

HighsMipWorker::transformNewIntegerFeasibleSolution is the only caller that
passes thread_safe = true, which is why this only shows up in a parallel MIP
search, and it throws on a task-executor worker thread with no handler above
it, so the process terminates.
@jajhall

jajhall commented Sep 20, 2026

Copy link
Copy Markdown
Member

Thanks, even to me this looks OK and acceptable. Pattern-matching (at least) tells me that case ReductionType::kZeroObjSingletonContinuousCol should use reductionValues_ and rowValues_ rather than reductionValues and rowValues.

I see that thread_safe is only set true for undoPrimal in HighsMipWorker. I guess this is to avoid the overhead of allocating reductionValuesCopy in every undo operation where reductionValues are used. Otherwise it would look simpler to always use thread_safe.

@Opt-Mucca

Copy link
Copy Markdown
Collaborator

@jajhall I should've spotted this when we merged kZeroObjSingletonContinuousCol, although a missing _ is truly nasty....... This slipped through because we made those changes before merging parallel MIP (and then revived them a few weeks ago).

@MathieuDutSik great spot! That bug currently completely corrupts some parallel MIP runs. Can you please remove the new test? I believe it is a bit overkill for this specific bit of the code and the parallel interaction. I'd then happily merge the change!

@MathieuDutSik
MathieuDutSik force-pushed the fix/upstream-thread-safe-undo-zeroobj-singleton branch from fa9e648 to 5ee233d Compare September 21, 2026 10:16
@MathieuDutSik

Copy link
Copy Markdown
Author

@jajhall I should've spotted this when we merged kZeroObjSingletonContinuousCol, although a missing _ is truly nasty....... This slipped through because we made those changes before merging parallel MIP (and then revived them a few weeks ago).

@MathieuDutSik great spot! That bug currently completely corrupts some parallel MIP runs. Can you please remove the new test? I believe it is a bit overkill for this specific bit of the code and the parallel interaction. I'd then happily merge the change!

Done.

@jajhall

jajhall commented Sep 21, 2026

Copy link
Copy Markdown
Member

Spurious CI failures

@jajhall
jajhall merged commit d8f11c0 into ERGO-Code:latest Sep 21, 2026
373 of 380 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parallel MIP: kZeroObjSingletonContinuousCol undo bypasses the thread-safe aliases, aborting the process

3 participants