From 136051cc382e265a27fec3d07af55f631adc8bc5 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 29 Jul 2026 14:42:07 +0200 Subject: [PATCH 1/4] Add redesign draft --- highs/mip/HighsMipSolver.cpp | 220 +++++++++++++++++++++++++---------- highs/mip/HighsMipSolver.h | 6 +- highs/mip/HighsMipWorker.h | 1 + 3 files changed, 160 insertions(+), 67 deletions(-) diff --git a/highs/mip/HighsMipSolver.cpp b/highs/mip/HighsMipSolver.cpp index 2044074f486..4d23e6e317c 100644 --- a/highs/mip/HighsMipSolver.cpp +++ b/highs/mip/HighsMipSolver.cpp @@ -608,20 +608,46 @@ void HighsMipSolver::run() { const HighsInt num_search_workers = std::min(num_workers, static_cast(mipdata_->nodequeue.numActiveNodes())); - const HighsInt num_heuristic_workers = - mipdata_->upper_bound < kHighsInf - ? std::max(HighsInt{1}, (num_search_workers + 3) / 4) - : std::max(HighsInt{1}, (num_search_workers + 1) / 2); for (HighsInt i = 0; i < num_search_workers; i++) { assert(!mipdata_->workers[i].search_ptr_->hasNode()); indices.emplace_back(i); - mipdata_->workers[i].setAllowHeuristics(i < num_heuristic_workers); + mipdata_->workers[i].setAllowHeuristics(true); } }; - auto installNodes = [&](std::vector& indices, - bool& limit_reached) -> void { + auto prepareNodes = [&](std::vector& indices) { + const HighsInt numNodesPerWorker = + indices.size() > 1 && + static_cast(mipdata_->nodequeue.numActiveNodes()) >= + 2 * indices.size() + ? 2 + : 1; + for (const HighsInt i : indices) { + for (HighsInt j = 0; j != numNodesPerWorker; j++) { + if ((indices.size() == 1 && numQueueLeaves - lastLbLeave >= 10) || + (indices.size() > 1 && i == 0 && j == 1)) { + mipdata_->workers[i].openNodes.push( + mipdata_->nodequeue.popBestBoundNode()); + lastLbLeave = numQueueLeaves; + } else { + HighsInt bestBoundNodeStackSize = + mipdata_->nodequeue.getBestBoundDomchgStackSize(); + double bestBoundNodeLb = mipdata_->nodequeue.getBestLowerBound(); + HighsNodeQueue::OpenNode nextNode(mipdata_->nodequeue.popBestNode()); + if (nextNode.lower_bound == bestBoundNodeLb && + static_cast(nextNode.domchgstack.size()) == + bestBoundNodeStackSize) + lastLbLeave = numQueueLeaves; + mipdata_->workers[i].openNodes.push(std::move(nextNode)); + } + ++numQueueLeaves; + } + } + }; + + auto installNodesOld = [&](std::vector& indices, + bool& limit_reached) -> void { for (const HighsInt i : indices) { if ((indices.size() == 1 && numQueueLeaves - lastLbLeave >= 10) || (indices.size() > 1 && i == 0)) { @@ -673,7 +699,14 @@ void HighsMipSolver::run() { return false; }; - auto pruneNode = [&](HighsInt i) -> bool { + auto assignEarlyTermination = [&](HighsMipWorker& worker) { + if (worker.getGlobalDomain().infeasible() || + mipdata_->lower_bound > worker.getOptimalityLimit()) { + mipdata_->updateWorkerEarlyTermination(worker); + } + }; + + auto pruneNode = [&](const HighsInt i, bool& infeasible) -> bool { if (!mipdata_->parallelLockActive()) profiling_->start(kMipClockNodePrunedLoop); bool pruned = false; @@ -683,20 +716,17 @@ void HighsMipSolver::run() { pruned = true; ++mipdata_->workers[i].search_ptr_->getLocalNodes(); ++mipdata_->workers[i].search_ptr_->getLocalLeaves(); + if (mipdata_->workers[i].getGlobalDomain().infeasible()) { + assignEarlyTermination(mipdata_->workers[i]); + infeasible = true; + } } if (!mipdata_->parallelLockActive()) profiling_->stop(kMipClockNodePrunedLoop); - return mipdata_->workers[i].getGlobalDomain().infeasible() || pruned; - }; - - auto assignEarlyTermination = [&](HighsMipWorker& worker) { - if (worker.getGlobalDomain().infeasible() || - mipdata_->lower_bound > worker.getOptimalityLimit()) { - mipdata_->updateWorkerEarlyTermination(worker); - } + return pruned; }; - auto separateAndStoreBasis = [&](HighsInt i) -> bool { + auto separateAndStoreBasis = [&](const HighsInt i, bool& infeasible) -> void { HighsMipWorker& worker = mipdata_->workers[i]; if (options_mip_->mip_allow_cut_separation_at_nodes) { if (!mipdata_->parallelLockActive()) @@ -715,7 +745,8 @@ void HighsMipSolver::run() { : mipdata_->nodequeue; worker.search_ptr_->openNodesToQueue(globalqueue); assignEarlyTermination(worker); - return true; + infeasible = true; + return; } if (worker.getLpRelaxation().getStatus() != @@ -734,8 +765,6 @@ void HighsMipSolver::run() { basis = std::make_shared(std::move(b)); worker.getLpRelaxation().setStoredBasis(basis); } - - return false; }; auto backtrackPlunge = [&](HighsInt i) { @@ -759,7 +788,7 @@ void HighsMipSolver::run() { return false; }; - auto runHeuristics = [&](HighsInt i) -> bool { + auto runHeuristics = [&](const HighsInt i, bool& infeasible) -> bool { HighsMipWorker& worker = mipdata_->workers[i]; if (!mipdata_->parallelLockActive()) profiling_->start(kMipClockDiveEvaluateNode); @@ -813,7 +842,8 @@ void HighsMipSolver::run() { if (!mipdata_->parallelLockActive()) profiling_->stop(kMipClockDivePrimalHeuristics); - return worker.getGlobalDomain().infeasible(); + if (worker.getGlobalDomain().infeasible()) infeasible = true; + return false; }; auto dive = [&](HighsInt i, HighsInt nodeLim) -> bool { @@ -836,50 +866,99 @@ void HighsMipSolver::run() { auto processNodes = [&](const std::vector& indices, std::vector& restarts, + std::vector& stallNodes, const bool skip_separation, const HighsInt nodeLim, const HighsInt plungeLimit, double avgiter) { auto processNode = [&](const HighsInt i) { HighsMipWorker& worker = mipdata_->workers[i]; - int64_t nodes_explored = 0; - if (!skip_separation) { - evaluateNode(i); - assignEarlyTermination(worker); - if (pruneNode(i)) return; - if (worker.search_ptr_->checkLimits()) return; - if (!mipdata_->parallelLockActive()) mipdata_->printDisplayLine(); - if (separateAndStoreBasis(i)) return; - } - worker.getConflictPool().performAging(); - HighsInt iterlimit = 10 * std::max(avgiter, mipdata_->avgrootlpiters); - iterlimit = std::max({HighsInt{10000}, iterlimit, - HighsInt((3 * mipdata_->firstrootlpiters) / 2)}); - worker.getLpRelaxation().setIterationLimit(iterlimit); + HighsNodeQueue& nodequeue = mipdata_->parallelLockActive() + ? worker.nodequeue + : mipdata_->nodequeue; + int64_t total_nodes_explored = 0; bool considerHeuristics = true; - while (true) { - if (considerHeuristics && (skip_separation || nodeLim == kHighsIInf) && - worker.getAllowHeuristics() && mipdata_->moreHeuristicsAllowed()) { - if (runHeuristics(i)) break; + bool stop = false; + while (!worker.openNodes.empty()) { + int64_t nodes_explored = 0; + int64_t lastLocalNodeCount = worker.search_ptr_->getLocalNodes(); + worker.search_ptr_->installNode(std::move(worker.openNodes.front())); + worker.openNodes.pop(); + if (worker.search_ptr_->getCurrentEstimate() >= worker.upper_limit) { + stallNodes[i]++; } - considerHeuristics = false; - if (worker.getGlobalDomain().infeasible()) break; - if (dive(i, nodeLim)) break; - if (worker.search_ptr_->checkLimits( - worker.search_ptr_->getLocalNodes())) { - break; + if (!skip_separation) { + evaluateNode(i); + assignEarlyTermination(worker); + if (pruneNode(i, stop)) { + total_nodes_explored++; + if (worker.early_termination) stop = true; + if (stop || total_nodes_explored >= nodeLim) break; + continue; + } + if (stop || worker.search_ptr_->checkLimits()) { + stop = true; + break; + }; + if (!mipdata_->parallelLockActive()) mipdata_->printDisplayLine(); + separateAndStoreBasis(i, stop); + if (stop) break; } - - if (worker.search_ptr_->getLocalNodes() + nodes_explored >= plungeLimit) - break; - if (!mipdata_->parallelLockActive()) { - nodes_explored += worker.search_ptr_->getLocalNodes(); + worker.getConflictPool().performAging(); + HighsInt iterlimit = 10 * std::max(avgiter, mipdata_->avgrootlpiters); + iterlimit = std::max({HighsInt{10000}, iterlimit, + HighsInt((3 * mipdata_->firstrootlpiters) / 2)}); + worker.getLpRelaxation().setIterationLimit(iterlimit); + while (true) { + if (considerHeuristics && + (skip_separation || nodeLim == kHighsIInf) && + worker.getAllowHeuristics() && + mipdata_->moreHeuristicsAllowed()) { + if (runHeuristics(i, stop)) break; + if (stop) break; + } + considerHeuristics = false; + if (worker.getGlobalDomain().infeasible()) { + stop = true; + break; + } + if (dive(i, nodeLim)) break; + if (worker.search_ptr_->checkLimits( + worker.search_ptr_->getLocalNodes())) { + stop = true; + break; + } + + nodes_explored += + worker.search_ptr_->getLocalNodes() - lastLocalNodeCount; + lastLocalNodeCount = worker.search_ptr_->getLocalNodes(); + if (nodes_explored >= plungeLimit) break; + if (backtrackPlunge(i)) break; + if (!mipdata_->parallelLockActive()) { + worker.search_ptr_->flushStatistics(*this); + mipdata_->printDisplayLine(); + lastLocalNodeCount = 0; + } } - if (backtrackPlunge(i)) break; - if (!mipdata_->parallelLockActive()) { - worker.search_ptr_->flushStatistics(*this); - mipdata_->printDisplayLine(); + nodes_explored += + worker.search_ptr_->getLocalNodes() - lastLocalNodeCount; + lastLocalNodeCount = worker.search_ptr_->getLocalNodes(); + total_nodes_explored += nodes_explored; + assignEarlyTermination(worker); + if (worker.early_termination) stop = true; + worker.search_ptr_->openNodesToQueue(nodequeue); + if (total_nodes_explored >= nodeLim || stop) { + break; } } - assignEarlyTermination(worker); + if (worker.search_ptr_->hasNode()) { + worker.search_ptr_->openNodesToQueue(nodequeue); + } + while (!worker.openNodes.empty()) { + HighsNodeQueue::OpenNode node = std::move(worker.openNodes.front()); + worker.openNodes.pop(); + nodequeue.emplaceNode(std::move(node.domchgstack), + std::move(node.branchings), node.lower_bound, + node.estimate, node.depth); + } if (nodeLim == kHighsIInf) { restarts[i] = checkRestart(worker, 1); } @@ -900,6 +979,7 @@ void HighsMipSolver::run() { search_indices.reserve(max_num_workers); std::vector workerRestartVotes(getMaxNumWorkers(), RestartVote::kNoCheck); + std::vector stallNodesPerWorker(getMaxNumWorkers()); bool root_node = true; // Don't separate the root node again HighsInt nodeLim = max_num_workers > 1 ? 1 : kHighsIInf; // for ramp up while (!mipdata_->nodequeue.empty()) { @@ -920,12 +1000,10 @@ void HighsMipSolver::run() { // Assign nodes to workers bool limit_reached = false; if (root_node) { - master_worker.search_ptr_->installNode( - mipdata_->nodequeue.popBestBoundNode()); + master_worker.openNodes.push(mipdata_->nodequeue.popBestBoundNode()); } else { - installNodes(search_indices, limit_reached); + prepareNodes(search_indices); } - if (limit_reached) break; if (nodeLim != kHighsIInf) { if (num_workers >= max_num_workers) { @@ -937,8 +1015,8 @@ void HighsMipSolver::run() { } // Process nodes (separation / heuristics / dives) - processNodes(search_indices, workerRestartVotes, root_node, nodeLim, 100, - mipdata_->getLp().getAvgSolveIters()); + processNodes(search_indices, workerRestartVotes, stallNodesPerWorker, + root_node, nodeLim, 100, mipdata_->getLp().getAvgSolveIters()); root_node = false; @@ -988,7 +1066,7 @@ void HighsMipSolver::run() { infeasible = true; } profiling_->start(kMipClockOpenNodesToQueue0); - worker.search_ptr_->openNodesToQueue(mipdata_->nodequeue); + assert(!worker.search_ptr_->hasNode()); while (worker.nodequeue.numNodes() > 0) { HighsNodeQueue::OpenNode node = std::move(worker.nodequeue.popBestNode()); @@ -1022,6 +1100,22 @@ void HighsMipSolver::run() { break; } + for (const HighsInt i : search_indices) { + if (stallNodesPerWorker[i] == 0) { + numStallNodes = 0; + } else { + numStallNodes += stallNodesPerWorker[i]; + } + stallNodesPerWorker[i] = 0; + if (options_mip_->mip_max_stall_nodes != kHighsIInf && + numStallNodes >= options_mip_->mip_max_stall_nodes) { + limit_reached = true; + modelstatus_ = HighsModelStatus::kSolutionLimit; + break; + } + } + if (limit_reached) break; + assert(!nodesInstalled()); // Sync global information diff --git a/highs/mip/HighsMipSolver.h b/highs/mip/HighsMipSolver.h index 5e4dd6642da..d6bd5ab6246 100644 --- a/highs/mip/HighsMipSolver.h +++ b/highs/mip/HighsMipSolver.h @@ -159,12 +159,10 @@ class HighsMipSolver { void setParallelLock(bool lock) const; void setProfiling(HighsProfiling* profiling); HighsInt getMaxNumWorkers() const { - if (highs::parallel::num_threads() == 1 || - options_mip_->parallel != kHighsOnString || submip) { + if (options_mip_->parallel != kHighsOnString || submip) { return 1; } - return static_cast( - std::ceil(1.7 * highs::parallel::num_threads())); + return highs::parallel::num_threads(); } }; diff --git a/highs/mip/HighsMipWorker.h b/highs/mip/HighsMipWorker.h index 1836e83d95b..5ece6d965b3 100644 --- a/highs/mip/HighsMipWorker.h +++ b/highs/mip/HighsMipWorker.h @@ -71,6 +71,7 @@ class HighsMipWorker { std::unique_ptr search_ptr_; std::unique_ptr sepa_ptr_; HighsNodeQueue nodequeue; + std::queue openNodes; double upper_bound; double upper_limit; From 6595ef8aa3b74d910577d37bfef1615ff87e6dba Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 29 Jul 2026 15:27:36 +0200 Subject: [PATCH 2/4] Add fixes.... --- highs/mip/HighsMipSolver.cpp | 56 ++++++++---------------------------- 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/highs/mip/HighsMipSolver.cpp b/highs/mip/HighsMipSolver.cpp index 4d23e6e317c..f33d86db41b 100644 --- a/highs/mip/HighsMipSolver.cpp +++ b/highs/mip/HighsMipSolver.cpp @@ -461,6 +461,7 @@ void HighsMipSolver::run() { master_worker.search_ptr_->getLocalDomain()); profiling_->start(kMipClockSearch); + HighsInt maxNodesPerWorkerLim = max_num_workers > 1 ? 100 : kHighsIInf; int64_t numStallNodes = 0; int64_t lastLbLeave = 0; int64_t numQueueLeaves = 0; @@ -626,7 +627,7 @@ void HighsMipSolver::run() { for (const HighsInt i : indices) { for (HighsInt j = 0; j != numNodesPerWorker; j++) { if ((indices.size() == 1 && numQueueLeaves - lastLbLeave >= 10) || - (indices.size() > 1 && i == 0 && j == 1)) { + (indices.size() > 1 && i == 0 && j == 0)) { mipdata_->workers[i].openNodes.push( mipdata_->nodequeue.popBestBoundNode()); lastLbLeave = numQueueLeaves; @@ -646,41 +647,6 @@ void HighsMipSolver::run() { } }; - auto installNodesOld = [&](std::vector& indices, - bool& limit_reached) -> void { - for (const HighsInt i : indices) { - if ((indices.size() == 1 && numQueueLeaves - lastLbLeave >= 10) || - (indices.size() > 1 && i == 0)) { - mipdata_->workers[i].search_ptr_->installNode( - mipdata_->nodequeue.popBestBoundNode()); - lastLbLeave = numQueueLeaves; - } else { - HighsInt bestBoundNodeStackSize = - mipdata_->nodequeue.getBestBoundDomchgStackSize(); - double bestBoundNodeLb = mipdata_->nodequeue.getBestLowerBound(); - HighsNodeQueue::OpenNode nextNode(mipdata_->nodequeue.popBestNode()); - if (nextNode.lower_bound == bestBoundNodeLb && - (HighsInt)nextNode.domchgstack.size() == bestBoundNodeStackSize) - lastLbLeave = numQueueLeaves; - mipdata_->workers[i].search_ptr_->installNode(std::move(nextNode)); - } - - ++numQueueLeaves; - - if (mipdata_->workers[i].search_ptr_->getCurrentEstimate() >= - mipdata_->upper_limit) { - ++numStallNodes; - if (options_mip_->mip_max_stall_nodes != kHighsIInf && - numStallNodes >= options_mip_->mip_max_stall_nodes) { - limit_reached = true; - modelstatus_ = HighsModelStatus::kSolutionLimit; - break; - } - } else - numStallNodes = 0; - } - }; - auto evaluateNode = [&](HighsInt i) -> bool { if (!mipdata_->parallelLockActive()) profiling_->start(kMipClockEvaluateNode1); @@ -909,7 +875,7 @@ void HighsMipSolver::run() { worker.getLpRelaxation().setIterationLimit(iterlimit); while (true) { if (considerHeuristics && - (skip_separation || nodeLim == kHighsIInf) && + (skip_separation || nodeLim == maxNodesPerWorkerLim) && worker.getAllowHeuristics() && mipdata_->moreHeuristicsAllowed()) { if (runHeuristics(i, stop)) break; @@ -959,7 +925,7 @@ void HighsMipSolver::run() { std::move(node.branchings), node.lower_bound, node.estimate, node.depth); } - if (nodeLim == kHighsIInf) { + if (nodeLim == maxNodesPerWorkerLim) { restarts[i] = checkRestart(worker, 1); } }; @@ -981,7 +947,7 @@ void HighsMipSolver::run() { RestartVote::kNoCheck); std::vector stallNodesPerWorker(getMaxNumWorkers()); bool root_node = true; // Don't separate the root node again - HighsInt nodeLim = max_num_workers > 1 ? 1 : kHighsIInf; // for ramp up + HighsInt nodeLim = max_num_workers > 1 ? 1 : maxNodesPerWorkerLim; // ramp-up while (!mipdata_->nodequeue.empty()) { // Possibly query existence of an external solution if (!submip) @@ -1005,12 +971,12 @@ void HighsMipSolver::run() { prepareNodes(search_indices); } - if (nodeLim != kHighsIInf) { + if (nodeLim != maxNodesPerWorkerLim) { if (num_workers >= max_num_workers) { nodeLim = std::max(HighsInt{20}, 2 * nodeLim); } - if (nodeLim > 100) { - nodeLim = kHighsIInf; + if (nodeLim >= maxNodesPerWorkerLim) { + nodeLim = maxNodesPerWorkerLim; } } @@ -1108,7 +1074,8 @@ void HighsMipSolver::run() { } stallNodesPerWorker[i] = 0; if (options_mip_->mip_max_stall_nodes != kHighsIInf && - numStallNodes >= options_mip_->mip_max_stall_nodes) { + numStallNodes >= + std::max(HighsInt{1}, options_mip_->mip_max_stall_nodes)) { limit_reached = true; modelstatus_ = HighsModelStatus::kSolutionLimit; break; @@ -1149,7 +1116,8 @@ void HighsMipSolver::run() { mipdata_->nodequeue.numNodes() > num_workers; resetGlobalDomain(spawn_more_workers, mipdata_->hasMultipleWorkers()); - if (nodeLim == kHighsIInf && checkWorkerRestartVotes(workerRestartVotes)) + if (nodeLim == maxNodesPerWorkerLim && + checkWorkerRestartVotes(workerRestartVotes)) goto restart; if (spawn_more_workers) { From 70d3faa0887bd3cee99120e84f004881e081f13c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 31 Jul 2026 12:51:26 +0200 Subject: [PATCH 3/4] Change backtrack plunge. Remove worker.nodequeue --- highs/mip/HighsMipSolver.cpp | 108 ++++++++++++++++++----------------- highs/mip/HighsMipWorker.h | 5 +- highs/mip/HighsSearch.cpp | 86 +++++++++------------------- highs/mip/HighsSearch.h | 11 ++-- 4 files changed, 90 insertions(+), 120 deletions(-) diff --git a/highs/mip/HighsMipSolver.cpp b/highs/mip/HighsMipSolver.cpp index f33d86db41b..b0f3af261e3 100644 --- a/highs/mip/HighsMipSolver.cpp +++ b/highs/mip/HighsMipSolver.cpp @@ -309,7 +309,6 @@ void HighsMipSolver::run() { mipdata_->lps.back().notifyCutPoolsLpCopied(1); mipdata_->workers.back().randgen.initialise(options_mip_->random_seed + mipdata_->workers.size() - 1); - mipdata_->workers.back().nodequeue.setNumCol(numCol()); mipdata_->debugSolution.registerDomain( mipdata_->workers.back().search_ptr_->getLocalDomain()); } @@ -336,8 +335,6 @@ void HighsMipSolver::run() { worker.getLpRelaxation().setMipWorker(worker); worker.resetSearch(); worker.resetSepa(); - worker.nodequeue.clear(); - worker.nodequeue.setNumCol(numCol()); }; auto syncSolutions = [&]() -> void { @@ -451,8 +448,6 @@ void HighsMipSolver::run() { master_worker.resetSearch(); master_worker.resetSepa(); - master_worker.nodequeue.clear(); - master_worker.nodequeue.setNumCol(numCol()); master_worker.upper_bound = mipdata_->upper_bound; master_worker.upper_limit = mipdata_->upper_limit; master_worker.optimality_limit = mipdata_->optimality_limit; @@ -481,14 +476,14 @@ void HighsMipSolver::run() { return false; }; - auto checkRestart = [&](const HighsMipWorker& worker, - HighsInt numWorkerVotes) -> RestartVote { + auto checkRestart = [&](const HighsMipWorker& worker, HighsInt numWorkerVotes, + HighsCDouble currTreeWeight) -> RestartVote { int64_t nNodes = worker.search_ptr_->nnodes + mipdata_->num_nodes; if (!submip && nNodes >= nextCheck && options_mip_->mip_allow_restart) { const HighsInt nTreeRestarts = mipdata_->numRestarts - mipdata_->numRestartsRoot; const HighsCDouble treeWeight = - worker.search_ptr_->treeweight + mipdata_->pruned_treeweight; + currTreeWeight + mipdata_->pruned_treeweight; double currNodeEstim = numNodesLastCheck - mipdata_->num_nodes_before_run + (nNodes - numNodesLastCheck) * double(1.0 - treeWeight) / @@ -582,7 +577,8 @@ void HighsMipSolver::run() { } // Using joint information after workers are synced, query a restart const RestartVote vote = checkRestart( - master_worker, (numRestartVotes + numHugeTreeVotes + 2) / 2); + master_worker, (numRestartVotes + numHugeTreeVotes + 2) / 2, + master_worker.search_ptr_->treeweight); if (vote == RestartVote::kWouldRestart) { performRestart(); return true; @@ -624,23 +620,25 @@ void HighsMipSolver::run() { 2 * indices.size() ? 2 : 1; - for (const HighsInt i : indices) { - for (HighsInt j = 0; j != numNodesPerWorker; j++) { + for (HighsInt j = 0; j != numNodesPerWorker; j++) { + for (const HighsInt i : indices) { if ((indices.size() == 1 && numQueueLeaves - lastLbLeave >= 10) || (indices.size() > 1 && i == 0 && j == 0)) { - mipdata_->workers[i].openNodes.push( - mipdata_->nodequeue.popBestBoundNode()); + mipdata_->workers[i].preparedNodes.push_back( + std::move(mipdata_->nodequeue.popBestBoundNode())); lastLbLeave = numQueueLeaves; } else { HighsInt bestBoundNodeStackSize = mipdata_->nodequeue.getBestBoundDomchgStackSize(); double bestBoundNodeLb = mipdata_->nodequeue.getBestLowerBound(); - HighsNodeQueue::OpenNode nextNode(mipdata_->nodequeue.popBestNode()); + mipdata_->workers[i].preparedNodes.push_back( + std::move(mipdata_->nodequeue.popBestNode())); + HighsNodeQueue::OpenNode& nextNode = + mipdata_->workers[i].preparedNodes.back(); if (nextNode.lower_bound == bestBoundNodeLb && static_cast(nextNode.domchgstack.size()) == bestBoundNodeStackSize) lastLbLeave = numQueueLeaves; - mipdata_->workers[i].openNodes.push(std::move(nextNode)); } ++numQueueLeaves; } @@ -650,12 +648,10 @@ void HighsMipSolver::run() { auto evaluateNode = [&](HighsInt i) -> bool { if (!mipdata_->parallelLockActive()) profiling_->start(kMipClockEvaluateNode1); - if (mipdata_->workers[i].search_ptr_->evaluateNode() == + HighsMipWorker& worker = mipdata_->workers[i]; + if (worker.search_ptr_->evaluateNode() == HighsSearch::NodeResult::kSubOptimal) { - HighsNodeQueue& globalqueue = mipdata_->parallelLockActive() - ? mipdata_->workers[i].nodequeue - : mipdata_->nodequeue; - mipdata_->workers[i].search_ptr_->currentNodeToQueue(globalqueue); + worker.search_ptr_->stashCurrentNode(); if (!mipdata_->parallelLockActive()) profiling_->stop(kMipClockEvaluateNode1); return true; @@ -706,10 +702,7 @@ void HighsMipSolver::run() { if (worker.getGlobalDomain().infeasible()) { worker.search_ptr_->cutoffNode(); - HighsNodeQueue& globalqueue = mipdata_->parallelLockActive() - ? worker.nodequeue - : mipdata_->nodequeue; - worker.search_ptr_->openNodesToQueue(globalqueue); + worker.search_ptr_->stashOpenNodes(); assignEarlyTermination(worker); infeasible = true; return; @@ -736,20 +729,18 @@ void HighsMipSolver::run() { auto backtrackPlunge = [&](HighsInt i) { if (!mipdata_->parallelLockActive()) profiling_->start(kMipClockBacktrackPlunge); - const bool backtrack_plunge = - mipdata_->workers[i].search_ptr_->backtrackPlunge( - mipdata_->parallelLockActive() ? mipdata_->workers[i].nodequeue - : mipdata_->nodequeue); + HighsMipWorker& worker = mipdata_->workers[i]; + const bool backtrack_plunge = worker.search_ptr_->backtrackPlunge(); if (!mipdata_->parallelLockActive()) profiling_->stop(kMipClockBacktrackPlunge); if (!backtrack_plunge) return true; - assert(mipdata_->workers[i].search_ptr_->hasNode()); + assert(worker.search_ptr_->hasNode()); - if (mipdata_->workers[i].getConflictPool().getNumConflicts() > + if (worker.getConflictPool().getNumConflicts() > options_mip_->mip_pool_soft_limit) { - mipdata_->workers[i].getConflictPool().performAging(); + worker.getConflictPool().performAging(); } return false; }; @@ -837,17 +828,16 @@ void HighsMipSolver::run() { const HighsInt plungeLimit, double avgiter) { auto processNode = [&](const HighsInt i) { HighsMipWorker& worker = mipdata_->workers[i]; - HighsNodeQueue& nodequeue = mipdata_->parallelLockActive() - ? worker.nodequeue - : mipdata_->nodequeue; + worker.prepNodeIdx = 0; int64_t total_nodes_explored = 0; bool considerHeuristics = true; bool stop = false; - while (!worker.openNodes.empty()) { + while (worker.prepNodeIdx != worker.preparedNodes.size()) { int64_t nodes_explored = 0; int64_t lastLocalNodeCount = worker.search_ptr_->getLocalNodes(); - worker.search_ptr_->installNode(std::move(worker.openNodes.front())); - worker.openNodes.pop(); + worker.search_ptr_->installNode( + std::move(worker.preparedNodes[worker.prepNodeIdx])); + ++worker.prepNodeIdx; if (worker.search_ptr_->getCurrentEstimate() >= worker.upper_limit) { stallNodes[i]++; } @@ -910,23 +900,33 @@ void HighsMipSolver::run() { total_nodes_explored += nodes_explored; assignEarlyTermination(worker); if (worker.early_termination) stop = true; - worker.search_ptr_->openNodesToQueue(nodequeue); + worker.search_ptr_->stashOpenNodes(); if (total_nodes_explored >= nodeLim || stop) { break; } } if (worker.search_ptr_->hasNode()) { - worker.search_ptr_->openNodesToQueue(nodequeue); + worker.search_ptr_->stashOpenNodes(); } - while (!worker.openNodes.empty()) { - HighsNodeQueue::OpenNode node = std::move(worker.openNodes.front()); - worker.openNodes.pop(); - nodequeue.emplaceNode(std::move(node.domchgstack), - std::move(node.branchings), node.lower_bound, - node.estimate, node.depth); + for (size_t j = worker.prepNodeIdx; j != worker.preparedNodes.size(); + j++) { + worker.processedNodes.emplace_back(std::move(worker.preparedNodes[j]), + worker.search_ptr_->countTreeWeight); } + worker.preparedNodes.clear(); + worker.prepNodeIdx = 0; if (nodeLim == maxNodesPerWorkerLim) { - restarts[i] = checkRestart(worker, 1); + // Need to spoof tree weight + HighsCDouble currTreeWeight = worker.search_ptr_->treeweight; + for (auto& node_treeweight_pair : worker.processedNodes) { + if (node_treeweight_pair.second && + node_treeweight_pair.first.lower_bound > + worker.optimality_limit) { + currTreeWeight += + std::ldexp(1.0, 1 - node_treeweight_pair.first.depth); + } + } + restarts[i] = checkRestart(worker, 1, currTreeWeight); } }; runTask(processNode, tg, true, false, indices); @@ -966,7 +966,8 @@ void HighsMipSolver::run() { // Assign nodes to workers bool limit_reached = false; if (root_node) { - master_worker.openNodes.push(mipdata_->nodequeue.popBestBoundNode()); + master_worker.preparedNodes.push_back( + std::move(mipdata_->nodequeue.popBestBoundNode())); } else { prepareNodes(search_indices); } @@ -1013,7 +1014,7 @@ void HighsMipSolver::run() { for (const HighsInt i : search_indices) { if (i == early_terminated_worker) continue; HighsMipWorker& worker = mipdata_->workers[i]; - worker.nodequeue.clear(); + worker.processedNodes.clear(); worker.search_ptr_->resetStatistics(); worker.resetHeurStats(); worker.resetSepaStats(); @@ -1033,13 +1034,16 @@ void HighsMipSolver::run() { } profiling_->start(kMipClockOpenNodesToQueue0); assert(!worker.search_ptr_->hasNode()); - while (worker.nodequeue.numNodes() > 0) { - HighsNodeQueue::OpenNode node = - std::move(worker.nodequeue.popBestNode()); - mipdata_->nodequeue.emplaceNode( + for (auto& node_treeweight_pair : worker.processedNodes) { + HighsNodeQueue::OpenNode& node = node_treeweight_pair.first; + double tmpTreeWeight = mipdata_->nodequeue.emplaceNode( std::move(node.domchgstack), std::move(node.branchings), node.lower_bound, node.estimate, node.depth); + if (node_treeweight_pair.second) { + worker.search_ptr_->treeweight += tmpTreeWeight; + } } + worker.processedNodes.clear(); profiling_->stop(kMipClockOpenNodesToQueue0); worker.search_ptr_->flushStatistics(*this); syncSepaStats(worker); diff --git a/highs/mip/HighsMipWorker.h b/highs/mip/HighsMipWorker.h index 5ece6d965b3..a600f031161 100644 --- a/highs/mip/HighsMipWorker.h +++ b/highs/mip/HighsMipWorker.h @@ -70,8 +70,9 @@ class HighsMipWorker { public: std::unique_ptr search_ptr_; std::unique_ptr sepa_ptr_; - HighsNodeQueue nodequeue; - std::queue openNodes; + std::vector> processedNodes; + std::vector preparedNodes; + size_t prepNodeIdx; double upper_bound; double upper_limit; diff --git a/highs/mip/HighsSearch.cpp b/highs/mip/HighsSearch.cpp index 2ab00508239..95c54839f57 100644 --- a/highs/mip/HighsSearch.cpp +++ b/highs/mip/HighsSearch.cpp @@ -8,6 +8,7 @@ #include "mip/HighsSearch.h" #include +#include #include "lp_data/HConst.h" #include "mip/HighsCutGeneration.h" @@ -716,7 +717,7 @@ const HighsSearch::NodeData* HighsSearch::getParentNodeData() const { return &nodestack[nodestack.size() - 2]; } -void HighsSearch::currentNodeToQueue(HighsNodeQueue& nodequeue) { +void HighsSearch::stashCurrentNode() { auto oldchangedcols = localdom.getChangedCols().size(); bool prune = nodestack.back().lower_bound > getCutoffBound(); if (!prune) { @@ -730,12 +731,14 @@ void HighsSearch::currentNodeToQueue(HighsNodeQueue& nodequeue) { if (!prune) { std::vector branchPositions; auto domchgStack = localdom.getReducedDomainChangeStack(branchPositions); - double tmpTreeWeight = nodequeue.emplaceNode( - std::move(domchgStack), std::move(branchPositions), - std::max(nodestack.back().lower_bound, - localdom.getObjectiveLowerBound()), - nodestack.back().estimate, getCurrentDepth()); - if (countTreeWeight) treeweight += tmpTreeWeight; + mipworker.processedNodes.emplace_back( + std::piecewise_construct, + std::forward_as_tuple(std::move(domchgStack), + std::move(branchPositions), + std::max(nodestack.back().lower_bound, + localdom.getObjectiveLowerBound()), + nodestack.back().estimate, getCurrentDepth()), + std::forward_as_tuple(countTreeWeight)); } else { mipsolver.mipdata_->debugSolution.nodePruned(localdom); if (countTreeWeight) treeweight += std::ldexp(1.0, 1 - getCurrentDepth()); @@ -743,7 +746,7 @@ void HighsSearch::currentNodeToQueue(HighsNodeQueue& nodequeue) { nodestack.back().opensubtrees = 0; } -void HighsSearch::openNodesToQueue(HighsNodeQueue& nodequeue) { +void HighsSearch::stashOpenNodes() { if (nodestack.empty()) return; // get the basis of the node highest up in the tree @@ -771,12 +774,14 @@ void HighsSearch::openNodesToQueue(HighsNodeQueue& nodequeue) { if (!prune) { std::vector branchPositions; auto domchgStack = localdom.getReducedDomainChangeStack(branchPositions); - double tmpTreeWeight = nodequeue.emplaceNode( - std::move(domchgStack), std::move(branchPositions), - std::max(nodestack.back().lower_bound, - localdom.getObjectiveLowerBound()), - nodestack.back().estimate, getCurrentDepth()); - if (countTreeWeight) treeweight += tmpTreeWeight; + mipworker.processedNodes.emplace_back( + std::piecewise_construct, + std::forward_as_tuple(std::move(domchgStack), + std::move(branchPositions), + std::max(nodestack.back().lower_bound, + localdom.getObjectiveLowerBound()), + nodestack.back().estimate, getCurrentDepth()), + std::forward_as_tuple(countTreeWeight)); } else { mipsolver.mipdata_->debugSolution.nodePruned(localdom); if (countTreeWeight) treeweight += std::ldexp(1.0, 1 - getCurrentDepth()); @@ -1619,7 +1624,7 @@ bool HighsSearch::backtrack(bool recoverBasis) { return true; } -bool HighsSearch::backtrackPlunge(HighsNodeQueue& nodequeue) { +bool HighsSearch::backtrackPlunge() { const std::vector& domchgstack = localdom.getDomainChangeStack(); @@ -1686,21 +1691,14 @@ bool HighsSearch::backtrackPlunge(HighsNodeQueue& nodequeue) { currnode.opensubtrees = 0; bool fallbackbranch = currnode.branchingdecision.boundval == currnode.branching_point; - double nodeScore; if (currnode.branchingdecision.boundtype == HighsBoundType::kLower) { currnode.branchingdecision.boundtype = HighsBoundType::kUpper; currnode.branchingdecision.boundval = std::floor(currnode.branchingdecision.boundval - 0.5); - nodeScore = pseudocost.getScoreDown( - currnode.branchingdecision.column, - fallbackbranch ? 0.5 : currnode.branching_point); } else { currnode.branchingdecision.boundtype = HighsBoundType::kLower; currnode.branchingdecision.boundval = std::ceil(currnode.branchingdecision.boundval + 0.5); - nodeScore = pseudocost.getScoreUp( - currnode.branchingdecision.column, - fallbackbranch ? 0.5 : currnode.branching_point); } if (fallbackbranch) @@ -1737,49 +1735,17 @@ bool HighsSearch::backtrackPlunge(HighsNodeQueue& nodequeue) { nodelb = std::max(nodelb, localdom.getObjectiveLowerBound()); bool nodeToQueue = nodelb > mipworker.getOptimalityLimit(); - // we check if switching to the other branch of an ancestor yields a higher - // additive branch score than staying in this node and if so we postpone the - // node and put it to the queue to backtrack further. - if (!nodeToQueue) { - for (HighsInt i = nodestack.size() - 2; i >= 0; --i) { - if (nodestack[i].opensubtrees == 0) continue; - - bool fallbackbranch = nodestack[i].branchingdecision.boundval == - nodestack[i].branching_point; - double branchpoint = - fallbackbranch ? 0.5 : nodestack[i].branching_point; - double ancestorScoreActive; - double ancestorScoreInactive; - if (nodestack[i].branchingdecision.boundtype == - HighsBoundType::kLower) { - ancestorScoreInactive = pseudocost.getScoreDown( - nodestack[i].branchingdecision.column, branchpoint); - ancestorScoreActive = pseudocost.getScoreUp( - nodestack[i].branchingdecision.column, branchpoint); - } else { - ancestorScoreActive = pseudocost.getScoreDown( - nodestack[i].branchingdecision.column, branchpoint); - ancestorScoreInactive = pseudocost.getScoreUp( - nodestack[i].branchingdecision.column, branchpoint); - } - - // if (!mipsolver.submip) - // printf("nodeScore: %g, ancestorScore: %g\n", nodeScore, - // ancestorScore); - nodeToQueue = ancestorScoreInactive - ancestorScoreActive > - nodeScore + getFeasTol(); - break; - } - } if (nodeToQueue) { // if (!mipsolver.submip) printf("node goes to queue\n"); std::vector branchPositions; auto domchgStack = localdom.getReducedDomainChangeStack(branchPositions); - double tmpTreeWeight = nodequeue.emplaceNode( - std::move(domchgStack), std::move(branchPositions), nodelb, - nodestack.back().estimate, getCurrentDepth() + 1); - if (countTreeWeight) treeweight += tmpTreeWeight; + mipworker.processedNodes.emplace_back( + std::piecewise_construct, + std::forward_as_tuple( + std::move(domchgStack), std::move(branchPositions), nodelb, + nodestack.back().estimate, getCurrentDepth() + 1), + std::forward_as_tuple(countTreeWeight)); localdom.backtrack(); localdom.clearChangedCols(numChangedCols); continue; diff --git a/highs/mip/HighsSearch.h b/highs/mip/HighsSearch.h index 373bfbc4a24..bae39797e88 100644 --- a/highs/mip/HighsSearch.h +++ b/highs/mip/HighsSearch.h @@ -200,9 +200,9 @@ class HighsSearch { HighsInt getCurrentDepth() const { return nodestack.size() + depthoffset; } - void openNodesToQueue(HighsNodeQueue& nodequeue); + void stashOpenNodes(); - void currentNodeToQueue(HighsNodeQueue& nodequeue); + void stashCurrentNode(); void flushStatistics(HighsMipSolver& mipsolver); @@ -226,10 +226,9 @@ class HighsSearch { /// backtrack one level in DFS manner bool backtrack(bool recoverBasis = true); - /// backtrack an unspecified amount of depth level until the next - /// node that seems worthwhile to continue the plunge. Put unpromising nodes - /// to the node queue - bool backtrackPlunge(HighsNodeQueue& nodequeue); + /// backtrack until the next node (DFS manner) that can be worthwhile + /// to continue the plunge. Put unpromising nodes to workers processedNodes + bool backtrackPlunge(); /// for heuristics. Will discard nodes above targetDepth regardless of their /// status From ef56dde6fb8611fd1532b9fddf0dc9db23ccd1e1 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 21 Aug 2026 14:33:07 +0200 Subject: [PATCH 4/4] Add comments from Franz --- highs/mip/HighsMipSolver.cpp | 17 ++++++++----- highs/mip/HighsSearch.cpp | 47 +++++++++++++++--------------------- highs/mip/HighsSearch.h | 2 ++ 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/highs/mip/HighsMipSolver.cpp b/highs/mip/HighsMipSolver.cpp index c52fd8e8f4e..38641281b98 100644 --- a/highs/mip/HighsMipSolver.cpp +++ b/highs/mip/HighsMipSolver.cpp @@ -457,7 +457,10 @@ void HighsMipSolver::run() { master_worker.search_ptr_->getLocalDomain()); profiling_->start(kMipClockSearch); - HighsInt maxNodesPerWorkerLim = max_num_workers > 1 ? 100 : kHighsIInf; + // Maximum node processing budget for each worker in a batch during parallel + constexpr HighsInt kMaxNodesPerWorker = 100; + HighsInt maxNodesPerWorkerLim = + max_num_workers > 1 ? kMaxNodesPerWorker : kHighsIInf; int64_t numStallNodes = 0; int64_t lastLbLeave = 0; int64_t numQueueLeaves = 0; @@ -614,7 +617,7 @@ void HighsMipSolver::run() { } }; - auto prepareNodes = [&](std::vector& indices) { + auto prepareNodes = [&](const std::vector& indices) { const HighsInt numNodesPerWorker = indices.size() > 1 && static_cast(mipdata_->nodequeue.numActiveNodes()) >= @@ -670,6 +673,7 @@ void HighsMipSolver::run() { }; auto pruneNode = [&](const HighsInt i, bool& infeasible) -> bool { + assert(infeasible == false); if (!mipdata_->parallelLockActive()) profiling_->start(kMipClockNodePrunedLoop); bool pruned = false; @@ -827,7 +831,7 @@ void HighsMipSolver::run() { std::vector& stallNodes, const bool skip_separation, const HighsInt nodeLim, const HighsInt plungeLimit, double avgiter) { - auto processNode = [&](const HighsInt i) { + auto processWorkerNodes = [&](const HighsInt i) { HighsMipWorker& worker = mipdata_->workers[i]; worker.prepNodeIdx = 0; int64_t total_nodes_explored = 0; @@ -861,8 +865,9 @@ void HighsMipSolver::run() { } worker.getConflictPool().performAging(); HighsInt iterlimit = 10 * std::max(avgiter, mipdata_->avgrootlpiters); - iterlimit = std::max({HighsInt{10000}, iterlimit, - HighsInt((3 * mipdata_->firstrootlpiters) / 2)}); + iterlimit = std::max( + {HighsInt{10000}, iterlimit, + static_cast((3 * mipdata_->firstrootlpiters) / 2)}); worker.getLpRelaxation().setIterationLimit(iterlimit); while (true) { if (considerHeuristics && @@ -930,7 +935,7 @@ void HighsMipSolver::run() { restarts[i] = checkRestart(worker, 1, currTreeWeight); } }; - runTask(processNode, tg, true, false, indices); + runTask(processWorkerNodes, tg, true, false, indices); }; auto syncSepaStats = [&](HighsMipWorker& worker) { diff --git a/highs/mip/HighsSearch.cpp b/highs/mip/HighsSearch.cpp index fce92cfe825..ff56f31f40b 100644 --- a/highs/mip/HighsSearch.cpp +++ b/highs/mip/HighsSearch.cpp @@ -717,6 +717,17 @@ const HighsSearch::NodeData* HighsSearch::getParentNodeData() const { return &nodestack[nodestack.size() - 2]; } +void HighsSearch::stashNodeToProcessed(double lb, double estimate, + HighsInt depth) { + std::vector branchPositions; + auto domchgStack = localdom.getReducedDomainChangeStack(branchPositions); + mipworker.processedNodes.emplace_back( + std::piecewise_construct, + std::forward_as_tuple(std::move(domchgStack), std::move(branchPositions), + lb, estimate, depth), + std::forward_as_tuple(countTreeWeight)); +} + void HighsSearch::stashCurrentNode() { auto oldchangedcols = localdom.getChangedCols().size(); bool prune = nodestack.back().lower_bound > getCutoffBound(); @@ -729,16 +740,9 @@ void HighsSearch::stashCurrentNode() { pseudocost); } if (!prune) { - std::vector branchPositions; - auto domchgStack = localdom.getReducedDomainChangeStack(branchPositions); - mipworker.processedNodes.emplace_back( - std::piecewise_construct, - std::forward_as_tuple(std::move(domchgStack), - std::move(branchPositions), - std::max(nodestack.back().lower_bound, - localdom.getObjectiveLowerBound()), - nodestack.back().estimate, getCurrentDepth()), - std::forward_as_tuple(countTreeWeight)); + stashNodeToProcessed(std::max(nodestack.back().lower_bound, + localdom.getObjectiveLowerBound()), + nodestack.back().estimate, getCurrentDepth()); } else { mipsolver.mipdata_->debugSolution.nodePruned(localdom); if (countTreeWeight) treeweight += std::ldexp(1.0, 1 - getCurrentDepth()); @@ -772,16 +776,9 @@ void HighsSearch::stashOpenNodes() { mipworker.getGlobalDomain(), pseudocost); } if (!prune) { - std::vector branchPositions; - auto domchgStack = localdom.getReducedDomainChangeStack(branchPositions); - mipworker.processedNodes.emplace_back( - std::piecewise_construct, - std::forward_as_tuple(std::move(domchgStack), - std::move(branchPositions), - std::max(nodestack.back().lower_bound, - localdom.getObjectiveLowerBound()), - nodestack.back().estimate, getCurrentDepth()), - std::forward_as_tuple(countTreeWeight)); + stashNodeToProcessed(std::max(nodestack.back().lower_bound, + localdom.getObjectiveLowerBound()), + nodestack.back().estimate, getCurrentDepth()); } else { mipsolver.mipdata_->debugSolution.nodePruned(localdom); if (countTreeWeight) treeweight += std::ldexp(1.0, 1 - getCurrentDepth()); @@ -1739,14 +1736,8 @@ bool HighsSearch::backtrackPlunge() { if (nodeToQueue) { // if (!mipsolver.submip) printf("node goes to queue\n"); - std::vector branchPositions; - auto domchgStack = localdom.getReducedDomainChangeStack(branchPositions); - mipworker.processedNodes.emplace_back( - std::piecewise_construct, - std::forward_as_tuple( - std::move(domchgStack), std::move(branchPositions), nodelb, - nodestack.back().estimate, getCurrentDepth() + 1), - std::forward_as_tuple(countTreeWeight)); + stashNodeToProcessed(nodelb, nodestack.back().estimate, + getCurrentDepth() + 1); localdom.backtrack(); localdom.clearChangedCols(numChangedCols); continue; diff --git a/highs/mip/HighsSearch.h b/highs/mip/HighsSearch.h index bae39797e88..b927cf3eabd 100644 --- a/highs/mip/HighsSearch.h +++ b/highs/mip/HighsSearch.h @@ -145,6 +145,8 @@ class HighsSearch { bool orbitsValidInChildNode(const HighsDomainChange& branchChg) const; + void stashNodeToProcessed(double lb, double estimate, HighsInt depth); + public: HighsSearch(HighsMipWorker& mipworker, HighsPseudocost& pseudocost);