diff --git a/check/TestCheckSolution.cpp b/check/TestCheckSolution.cpp index 1e2f667e11..e1475e58f7 100644 --- a/check/TestCheckSolution.cpp +++ b/check/TestCheckSolution.cpp @@ -87,6 +87,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { HighsSolution optimal_solution = highs.getSolution(); HighsInt scratch_num_nodes = info.mip_node_count; + HighsInt scratch_num_simplex = info.simplex_iteration_count; if (dev_run) printf("Num nodes = %d\n", int(scratch_num_nodes)); std::string solution_file = test_name + model + ".sol"; @@ -113,7 +114,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { highs.run(); if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count)); - REQUIRE(info.mip_node_count != scratch_num_nodes); + const bool different_search = info.mip_node_count != scratch_num_nodes || + info.simplex_iteration_count != scratch_num_simplex; + REQUIRE(different_search); highs.clear(); } @@ -132,7 +135,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { highs.run(); if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count)); - REQUIRE(info.mip_node_count != scratch_num_nodes); + const bool different_search = info.mip_node_count != scratch_num_nodes || + info.simplex_iteration_count != scratch_num_simplex; + REQUIRE(different_search); highs.clear(); } @@ -162,7 +167,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { highs.run(); if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count)); - REQUIRE(info.mip_node_count != scratch_num_nodes); + const bool different_search = info.mip_node_count != scratch_num_nodes || + info.simplex_iteration_count != scratch_num_simplex; + REQUIRE(different_search); highs.clear(); } @@ -185,7 +192,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { highs.run(); if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count)); - REQUIRE(info.mip_node_count != scratch_num_nodes); + const bool different_search = info.mip_node_count != scratch_num_nodes || + info.simplex_iteration_count != scratch_num_simplex; + REQUIRE(different_search); highs.clear(); } @@ -232,7 +241,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { return_status = highs.setSolution(starting_solution); REQUIRE(return_status == HighsStatus::kOk); highs.run(); - REQUIRE(info.mip_node_count != scratch_num_nodes); + const bool different_search = info.mip_node_count != scratch_num_nodes || + info.simplex_iteration_count != scratch_num_simplex; + REQUIRE(different_search); highs.clear(); } @@ -283,7 +294,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { return_status = highs.setSolution(num_entries, index.data(), value.data()); REQUIRE(return_status == HighsStatus::kOk); highs.run(); - REQUIRE(info.mip_node_count != scratch_num_nodes); + const bool different_search = info.mip_node_count != scratch_num_nodes || + info.simplex_iteration_count != scratch_num_simplex; + REQUIRE(different_search); highs.clear(); } assert(other_tests); diff --git a/check/TestMipSolver.cpp b/check/TestMipSolver.cpp index cf0b248c68..32bbb273ff 100644 --- a/check/TestMipSolver.cpp +++ b/check/TestMipSolver.cpp @@ -1404,3 +1404,25 @@ TEST_CASE("issue-2173", "[highs_test_mip_solver]") { const double optimal_objective = -26770.8075489; solve(highs, kHighsOnString, require_model_status, optimal_objective); } + +TEST_CASE("parallel-mip-determinism", "[highs_test_mip_solver]") { + std::string filename = std::string(HIGHS_DIR) + "/check/instances/bell5.mps"; + HighsInt num_runs = 6; + std::vector lp_iters(num_runs); + for (HighsInt i = 0; i < num_runs; i++) { + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.setOptionValue("mip_rel_gap", 0); + highs.setOptionValue("threads", 2); + highs.setOptionValue("mip_search_concurrency", 2); + if (i % 2 == 0) highs.setOptionValue("mip_search_simulate_concurrency", 1); + highs.readModel(filename); + const HighsModelStatus require_model_status = HighsModelStatus::kOptimal; + const double optimal_objective = 8966406.491519; + solve(highs, kHighsOffString, require_model_status, optimal_objective); + lp_iters[i] = highs.getInfo().simplex_iteration_count; + if (i > 0) { + REQUIRE(lp_iters[i] == lp_iters[0]); + } + } +} diff --git a/cmake/sources-python.cmake b/cmake/sources-python.cmake index ac1c94c7e4..4978296f39 100644 --- a/cmake/sources-python.cmake +++ b/cmake/sources-python.cmake @@ -218,6 +218,7 @@ set(highs_sources_python highs/mip/HighsMipAnalysis.cpp highs/mip/HighsMipSolver.cpp highs/mip/HighsMipSolverData.cpp + highs/mip/HighsMipWorker.cpp highs/mip/HighsModkSeparator.cpp highs/mip/HighsNodeQueue.cpp highs/mip/HighsObjectiveFunction.cpp @@ -342,6 +343,7 @@ set(highs_headers_python highs/mip/HighsMipAnalysis.h highs/mip/HighsMipSolver.h highs/mip/HighsMipSolverData.h + highs/mip/HighsMipWorker.h highs/mip/HighsModkSeparator.h highs/mip/HighsNodeQueue.h highs/mip/HighsObjectiveFunction.h diff --git a/cmake/sources.cmake b/cmake/sources.cmake index 1e605de4b7..7afb796276 100644 --- a/cmake/sources.cmake +++ b/cmake/sources.cmake @@ -372,6 +372,7 @@ set(highs_sources mip/HighsMipAnalysis.cpp mip/HighsMipSolver.cpp mip/HighsMipSolverData.cpp + mip/HighsMipWorker.cpp mip/HighsModkSeparator.cpp mip/HighsNodeQueue.cpp mip/HighsObjectiveFunction.cpp @@ -499,6 +500,7 @@ set(highs_headers mip/HighsMipAnalysis.h mip/HighsMipSolver.h mip/HighsMipSolverData.h + mip/HighsMipWorker.h mip/HighsModkSeparator.h mip/HighsNodeQueue.h mip/HighsObjectiveFunction.h diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index 32b9fc5007..8b073b31ff 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -37,6 +37,7 @@ const std::string kHighsChooseString = "choose"; const std::string kHighsOnString = "on"; const HighsInt kHighsMaxStringLength = 512; const HighsInt kSimplexConcurrencyLimit = 8; +const HighsInt kMipSearchConcurrencyLimit = 8; const double kRunningAverageMultiplier = 0.05; const double kExcessivelySmallObjectiveCoefficient = 1e-4; diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 5562deee86..3916cdbfa7 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -504,6 +504,8 @@ struct HighsOptionsStruct { std::string mip_improving_solution_file; bool mip_root_presolve_only; HighsInt mip_lifting_for_probing; + HighsInt mip_search_concurrency; + bool mip_search_simulate_concurrency; bool mip_allow_cut_separation_at_nodes; // Logging callback identifiers @@ -667,6 +669,8 @@ struct HighsOptionsStruct { mip_improving_solution_file(""), mip_root_presolve_only(false), mip_lifting_for_probing(-1), + mip_search_concurrency(0), + mip_search_simulate_concurrency(false), // clang-format off mip_allow_cut_separation_at_nodes(true) {}; // clang-format on @@ -1284,6 +1288,18 @@ class HighsOptions : public HighsOptionsStruct { kHighsInf); records.push_back(record_double); + record_int = new OptionRecordInt( + "mip_search_concurrency", + "Number of workers to create per thread for concurrent MIP search", + advanced, &mip_search_concurrency, 0, 2, kMipSearchConcurrencyLimit); + records.push_back(record_int); + + record_bool = new OptionRecordBool( + "mip_search_simulate_concurrency", + "Simulate MIP search concurrency on a single thread", advanced, + &mip_search_simulate_concurrency, false); + records.push_back(record_bool); + record_int = new OptionRecordInt( "ipm_iteration_limit", "Iteration limit for IPM solver", advanced, &ipm_iteration_limit, 0, kHighsIInf, kHighsIInf); diff --git a/highs/meson.build b/highs/meson.build index 93b4a6f0c8..728c6582c9 100644 --- a/highs/meson.build +++ b/highs/meson.build @@ -235,6 +235,7 @@ _srcs = [ 'mip/HighsMipAnalysis.cpp', 'mip/HighsMipSolver.cpp', 'mip/HighsMipSolverData.cpp', + 'mip/HighsMipWorker.cpp', 'mip/HighsModkSeparator.cpp', 'mip/HighsNodeQueue.cpp', 'mip/HighsObjectiveFunction.cpp', diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index b8fc4b17b3..e4382e64de 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -647,7 +647,7 @@ bool HighsCliqueTable::processNewEdge(HighsDomain& globaldom, CliqueVar v1, void HighsCliqueTable::addClique(const HighsMipSolver& mipsolver, CliqueVar* cliquevars, HighsInt numcliquevars, bool equality, HighsInt origin) { - HighsDomain& globaldom = mipsolver.mipdata_->domain; + HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); mipsolver.mipdata_->debugSolution.checkClique(cliquevars, numcliquevars); const HighsInt maxNumCliqueVars = 100; @@ -841,7 +841,7 @@ void HighsCliqueTable::extractCliques( HighsInt nbin, std::vector& perm, std::vector& clique, double feastol) { HighsImplications& implics = mipsolver.mipdata_->implications; - HighsDomain& globaldom = mipsolver.mipdata_->domain; + HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); perm.resize(inds.size()); std::iota(perm.begin(), perm.end(), 0); @@ -1081,7 +1081,7 @@ void HighsCliqueTable::extractCliquesFromCut(const HighsMipSolver& mipsolver, if (isFull()) return; HighsImplications& implics = mipsolver.mipdata_->implications; - HighsDomain& globaldom = mipsolver.mipdata_->domain; + HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); const double feastol = mipsolver.mipdata_->feastol; @@ -1276,7 +1276,7 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, double rhs; - HighsDomain& globaldom = mipsolver.mipdata_->domain; + HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); for (HighsInt i = 0; i != mipsolver.numRow(); ++i) { HighsInt start = mipsolver.mipdata_->ARstart_[i]; @@ -1380,7 +1380,7 @@ void HighsCliqueTable::extractObjCliques(HighsMipSolver& mipsolver) { HighsInt nbin = mipsolver.mipdata_->objectiveFunction.getNumBinariesInObjective(); if (nbin <= 1) return; - HighsDomain& globaldom = mipsolver.mipdata_->domain; + HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); if (globaldom.getObjectiveLowerBound() == -kHighsInf) return; const double* vals; @@ -1603,7 +1603,9 @@ void HighsCliqueTable::vertexInfeasible(HighsDomain& globaldom, HighsInt col, void HighsCliqueTable::separateCliques(const HighsMipSolver& mipsolver, const std::vector& sol, - HighsCutPool& cutpool, double feastol) { + HighsCutPool& cutpool, double feastol, + HighsRandom& randgen, + int64_t& localNumNeighbourhoodQueries) { BronKerboschData data(sol); data.feastol = feastol; data.maxNeighbourhoodQueries = 1000000 + @@ -1611,7 +1613,7 @@ void HighsCliqueTable::separateCliques(const HighsMipSolver& mipsolver, mipsolver.mipdata_->total_lp_iterations * 1000; if (numNeighbourhoodQueries > data.maxNeighbourhoodQueries) return; data.maxNeighbourhoodQueries -= numNeighbourhoodQueries; - const HighsDomain& globaldom = mipsolver.mipdata_->domain; + const HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); for (HighsInt i : mipsolver.mipdata_->integral_cols) { if (colsubstituted[i] || colDeleted[i]) continue; @@ -1698,9 +1700,9 @@ void HighsCliqueTable::separateCliques(const HighsMipSolver& mipsolver, false, false); } - numNeighbourhoodQueries += data.numNeighbourhoodQueries; + localNumNeighbourhoodQueries += data.numNeighbourhoodQueries; - if (runcliquesubsumption) { + if (runcliquesubsumption && &randgen == &this->randgen) { for (std::vector& clique : data.cliques) { HighsInt nremoved = runCliqueSubsumption(globaldom, clique); @@ -1841,7 +1843,7 @@ void HighsCliqueTable::cleanupFixed(HighsDomain& globaldom) { if (nfixings != oldnfixings) propagateAndCleanup(globaldom); } -HighsInt HighsCliqueTable::getNumImplications(HighsInt col) { +HighsInt HighsCliqueTable::getNumImplications(HighsInt col) const { // first count all cliques as one implication, so that cliques of size two // are accounted for already HighsInt i0 = CliqueVar(col, 0).index(); @@ -1860,7 +1862,7 @@ HighsInt HighsCliqueTable::getNumImplications(HighsInt col) { return numimplics; } -HighsInt HighsCliqueTable::getNumImplications(HighsInt col, bool val) { +HighsInt HighsCliqueTable::getNumImplications(HighsInt col, bool val) const { HighsInt iVal = CliqueVar(col, val).index(); // each size two clique is one implication diff --git a/highs/mip/HighsCliqueTable.h b/highs/mip/HighsCliqueTable.h index 55d46126a0..ca242de832 100644 --- a/highs/mip/HighsCliqueTable.h +++ b/highs/mip/HighsCliqueTable.h @@ -182,6 +182,10 @@ class HighsCliqueTable { HighsInt getNumEntries() const { return numEntries; } + HighsRandom& getRandgen() { return randgen; } + + int64_t& getNumNeighbourhoodQueries() { return numNeighbourhoodQueries; } + HighsInt partitionNeighbourhood(std::vector& neighbourhoodInds, int64_t& numNeighbourhoodqueries, CliqueVar v, CliqueVar* q, HighsInt N) const; @@ -287,7 +291,8 @@ class HighsCliqueTable { void separateCliques(const HighsMipSolver& mipsolver, const std::vector& sol, HighsCutPool& cutpool, - double feastol); + double feastol, HighsRandom& randgen, + int64_t& localNumNeighbourhoodQueries); std::vector> separateCliques( const std::vector& sol, const HighsDomain& globaldom, @@ -300,9 +305,9 @@ class HighsCliqueTable { void addImplications(HighsDomain& domain, HighsInt col, HighsInt val); - HighsInt getNumImplications(HighsInt col); + HighsInt getNumImplications(HighsInt col) const; - HighsInt getNumImplications(HighsInt col, bool val); + HighsInt getNumImplications(HighsInt col, bool val) const; void runCliqueMerging(HighsDomain& globaldomain); diff --git a/highs/mip/HighsConflictPool.cpp b/highs/mip/HighsConflictPool.cpp index 7e1778eec9..031fe342e8 100644 --- a/highs/mip/HighsConflictPool.cpp +++ b/highs/mip/HighsConflictPool.cpp @@ -45,6 +45,7 @@ void HighsConflictPool::addConflictCut( conflictRanges_.emplace_back(start, end); ages_.resize(conflictRanges_.size()); modification_.resize(conflictRanges_.size()); + ageResetWhileLocked_.resize(conflictRanges_.size()); } else { conflictIndex = deletedConflicts_.back(); deletedConflicts_.pop_back(); @@ -52,6 +53,7 @@ void HighsConflictPool::addConflictCut( conflictRanges_[conflictIndex].second = end; } + ageResetWhileLocked_[conflictIndex] = 0; modification_[conflictIndex] += 1; ages_[conflictIndex] = 0; ageDistribution_[ages_[conflictIndex]] += 1; @@ -117,6 +119,7 @@ void HighsConflictPool::addReconvergenceCut( conflictRanges_.emplace_back(start, end); ages_.resize(conflictRanges_.size()); modification_.resize(conflictRanges_.size()); + ageResetWhileLocked_.resize(conflictRanges_.size()); } else { conflictIndex = deletedConflicts_.back(); deletedConflicts_.pop_back(); @@ -124,6 +127,7 @@ void HighsConflictPool::addReconvergenceCut( conflictRanges_[conflictIndex].second = end; } + ageResetWhileLocked_[conflictIndex] = 0; modification_[conflictIndex] += 1; ages_[conflictIndex] = 0; ageDistribution_[ages_[conflictIndex]] += 1; @@ -177,7 +181,8 @@ void HighsConflictPool::removeConflict(HighsInt conflict) { ++modification_[conflict]; } -void HighsConflictPool::performAging() { +void HighsConflictPool::performAging(const bool thread_safe) { + if (age_lock_) return; HighsInt conflictMaxIndex = conflictRanges_.size(); HighsInt agelim = agelim_; HighsInt numActiveConflicts = getNumConflicts(); @@ -188,9 +193,11 @@ void HighsConflictPool::performAging() { for (HighsInt i = 0; i != conflictMaxIndex; ++i) { if (ages_[i] < 0) continue; + if (thread_safe && ageResetWhileLocked_[i] == 1) resetAge(i); ageDistribution_[ages_[i]] -= 1; ages_[i] += 1; + ageResetWhileLocked_[i] = 0; if (ages_[i] > agelim) { ages_[i] = -1; @@ -199,3 +206,77 @@ void HighsConflictPool::performAging() { ageDistribution_[ages_[i]] += 1; } } + +void HighsConflictPool::addConflictFromOtherPool( + const HighsDomainChange* conflictEntries, const HighsInt conflictLen) { + HighsInt conflictIndex; + HighsInt start; + HighsInt end; + std::set>::iterator it; + if (freeSpaces_.empty() || + (it = freeSpaces_.lower_bound( + std::make_pair(conflictLen, HighsInt{-1}))) == freeSpaces_.end()) { + start = conflictEntries_.size(); + end = start + conflictLen; + + conflictEntries_.resize(end); + } else { + std::pair freeslot = *it; + freeSpaces_.erase(it); + + start = freeslot.second; + end = start + conflictLen; + // if the space was not completely occupied, we register the remainder of + // it again in the priority queue + if (freeslot.first > conflictLen) { + freeSpaces_.emplace(freeslot.first - conflictLen, end); + } + } + + // register the range of entries for this conflict with a reused or a new + // index + if (deletedConflicts_.empty()) { + conflictIndex = conflictRanges_.size(); + conflictRanges_.emplace_back(start, end); + ages_.resize(conflictRanges_.size()); + modification_.resize(conflictRanges_.size()); + ageResetWhileLocked_.resize(conflictRanges_.size()); + } else { + conflictIndex = deletedConflicts_.back(); + deletedConflicts_.pop_back(); + conflictRanges_[conflictIndex].first = start; + conflictRanges_[conflictIndex].second = end; + } + + ageResetWhileLocked_[conflictIndex] = 0; + modification_[conflictIndex] += 1; + ages_[conflictIndex] = 0; + ageDistribution_[ages_[conflictIndex]] += 1; + + for (HighsInt i = 0; i != conflictLen; ++i) { + assert(start + i < end); + conflictEntries_[start + i] = conflictEntries[i]; + } + + for (HighsDomain::ConflictPoolPropagation* conflictProp : propagationDomains) + conflictProp->conflictAdded(conflictIndex); +} + +void HighsConflictPool::syncConflictPool(HighsConflictPool& syncpool) { + HighsInt conflictMaxIndex = conflictRanges_.size(); + for (HighsInt i = 0; i != conflictMaxIndex; ++i) { + if (ages_[i] < 0) continue; + HighsInt start = conflictRanges_[i].first; + HighsInt end = conflictRanges_[i].second; + assert(start >= 0 && end >= 0); + syncpool.addConflictFromOtherPool(&conflictEntries_[start], end - start); + removeConflict(i); + } + deletedConflicts_.clear(); + freeSpaces_.clear(); + conflictRanges_.clear(); + conflictEntries_.clear(); + modification_.clear(); + ages_.clear(); + ageResetWhileLocked_.clear(); +} diff --git a/highs/mip/HighsConflictPool.h b/highs/mip/HighsConflictPool.h index 9411824e3d..f3f2950dcd 100644 --- a/highs/mip/HighsConflictPool.h +++ b/highs/mip/HighsConflictPool.h @@ -18,9 +18,11 @@ class HighsConflictPool { private: HighsInt agelim_; HighsInt softlimit_; + bool age_lock_; std::vector ageDistribution_; std::vector ages_; std::vector modification_; + std::vector ageResetWhileLocked_; std::vector conflictEntries_; std::vector> conflictRanges_; @@ -38,9 +40,11 @@ class HighsConflictPool { HighsConflictPool(HighsInt agelim, HighsInt softlimit) : agelim_(agelim), softlimit_(softlimit), + age_lock_(false), ageDistribution_(), ages_(), modification_(), + ageResetWhileLocked_(), conflictEntries_(), conflictRanges_(), freeSpaces_(), @@ -61,10 +65,19 @@ class HighsConflictPool { void removeConflict(HighsInt conflict); - void performAging(); + void performAging(bool thread_safe = false); + + void addConflictFromOtherPool(const HighsDomainChange* conflictEntries, + HighsInt conflictLen); + + void syncConflictPool(HighsConflictPool& syncpool); void resetAge(HighsInt conflict) { if (ages_[conflict] > 0) { + if (age_lock_) { + ageResetWhileLocked_[conflict] = 1; + return; + } ageDistribution_[ages_[conflict]] -= 1; ageDistribution_[0] += 1; ages_[conflict] = 0; @@ -104,6 +117,8 @@ class HighsConflictPool { HighsInt getNumConflicts() const { return conflictRanges_.size() - deletedConflicts_.size(); } + + void setAgeLock(const bool ageLock) { age_lock_ = ageLock; } }; #endif diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index f518b7d800..2eaa661f66 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -8,6 +8,7 @@ #include "mip/HighsCutGeneration.h" #include "../extern/pdqsort/pdqsort.h" +#include "mip/HighsDomain.h" #include "mip/HighsMipSolverData.h" #include "mip/HighsTransformedLp.h" #include "util/HighsIntegers.h" @@ -755,7 +756,7 @@ double HighsCutGeneration::scale(double val) { return std::ldexp(1.0, expshift); } -bool HighsCutGeneration::postprocessCut() { +bool HighsCutGeneration::postprocessCut(const HighsDomain& globaldom) { // right hand sides slightly below zero are likely due to numerical errors and // can cause numerical troubles with scaling, so set them to zero if (rhs < 0 && rhs > -epsilon) rhs = 0; @@ -773,7 +774,6 @@ bool HighsCutGeneration::postprocessCut() { return true; } - HighsDomain& globaldomain = lpRelaxation.getMipSolver().mipdata_->domain; // determine maximal absolute coefficient double maxAbsValue = 0.0; for (HighsInt i = 0; i != rowlen; ++i) @@ -789,13 +789,13 @@ bool HighsCutGeneration::postprocessCut() { if (vals[i] == 0) continue; if (std::abs(vals[i]) <= minCoefficientValue) { if (vals[i] < 0) { - double ub = globaldomain.col_upper_[inds[i]]; + double ub = globaldom.col_upper_[inds[i]]; if (ub == kHighsInf) return false; else rhs -= ub * vals[i]; } else { - double lb = globaldomain.col_lower_[inds[i]]; + double lb = globaldom.col_lower_[inds[i]]; if (lb == -kHighsInf) return false; else @@ -854,12 +854,12 @@ bool HighsCutGeneration::postprocessCut() { // upperbound constraint to make it exactly integral instead and // therefore weaken the right hand side if (delta < 0.0) { - double ub = globaldomain.col_upper_[inds[i]]; + double ub = globaldom.col_upper_[inds[i]]; if (ub == kHighsInf) return false; rhs -= delta * ub; } else { - double lb = globaldomain.col_lower_[inds[i]]; + double lb = globaldom.col_lower_[inds[i]]; if (lb == -kHighsInf) return false; rhs -= delta * lb; @@ -1176,7 +1176,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, rowlen, rhs_); // apply cut postprocessing including scaling and removal of small // coefficients - if (!postprocessCut()) return false; + if (!postprocessCut(transLp.getGlobaldom())) return false; rhs_ = (double)rhs; vals_.resize(rowlen); inds_.resize(rowlen); @@ -1191,8 +1191,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, if (violation <= 10 * feastol) return false; - lpRelaxation.getMipSolver().mipdata_->domain.tightenCoefficients( - inds, vals, rowlen, rhs_); + transLp.getGlobaldom().tightenCoefficients(inds, vals, rowlen, rhs_); // if the cut is violated by a small factor above the feasibility // tolerance, add it to the cutpool @@ -1205,7 +1204,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, return cutindex != -1; } -bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, +bool HighsCutGeneration::generateConflict(const HighsDomain& localdomain, + const HighsDomain& globaldom, std::vector& proofinds, std::vector& proofvals, double& proofrhs) { @@ -1222,27 +1222,26 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, upper.resize(rowlen); solval.resize(rowlen); - HighsDomain& globaldomain = lpRelaxation.getMipSolver().mipdata_->domain; double activity = 0.0; for (HighsInt i = 0; i != rowlen; ++i) { HighsInt col = inds[i]; - upper[i] = globaldomain.col_upper_[col] - globaldomain.col_lower_[col]; + upper[i] = globaldom.col_upper_[col] - globaldom.col_lower_[col]; - solval[i] = vals[i] < 0 ? std::min(globaldomain.col_upper_[col], - localdomain.col_upper_[col]) - : std::max(globaldomain.col_lower_[col], - localdomain.col_lower_[col]); - if (vals[i] < 0 && globaldomain.col_upper_[col] != kHighsInf) { - rhs -= globaldomain.col_upper_[col] * vals[i]; + solval[i] = + vals[i] < 0 + ? std::min(globaldom.col_upper_[col], localdomain.col_upper_[col]) + : std::max(globaldom.col_lower_[col], localdomain.col_lower_[col]); + if (vals[i] < 0 && globaldom.col_upper_[col] != kHighsInf) { + rhs -= globaldom.col_upper_[col] * vals[i]; vals[i] = -vals[i]; complementation[i] = 1; - solval[i] = globaldomain.col_upper_[col] - solval[i]; + solval[i] = globaldom.col_upper_[col] - solval[i]; } else { - rhs -= globaldomain.col_lower_[col] * vals[i]; + rhs -= globaldom.col_lower_[col] * vals[i]; complementation[i] = 0; - solval[i] = solval[i] - globaldomain.col_lower_[col]; + solval[i] = solval[i] - globaldom.col_lower_[col]; } activity += solval[i] * vals[i]; @@ -1270,16 +1269,16 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, if (!complementation.empty()) { for (HighsInt i = 0; i != rowlen; ++i) { if (complementation[i]) { - rhs -= globaldomain.col_upper_[inds[i]] * vals[i]; + rhs -= globaldom.col_upper_[inds[i]] * vals[i]; vals[i] = -vals[i]; } else - rhs += globaldomain.col_lower_[inds[i]] * vals[i]; + rhs += globaldom.col_lower_[inds[i]] * vals[i]; } } // apply cut postprocessing including scaling and removal of small // coefficients - if (!postprocessCut()) return false; + if (!postprocessCut(globaldom)) return false; proofvals.resize(rowlen); proofinds.resize(rowlen); @@ -1287,8 +1286,8 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, bool cutintegral = integralSupport && integralCoefficients; - lpRelaxation.getMipSolver().mipdata_->domain.tightenCoefficients( - proofinds.data(), proofvals.data(), rowlen, proofrhs); + globaldom.tightenCoefficients(proofinds.data(), proofvals.data(), rowlen, + proofrhs); HighsInt cutindex = cutpool.addCut(lpRelaxation.getMipSolver(), proofinds.data(), proofvals.data(), rowlen, @@ -1299,7 +1298,8 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, return cutindex != -1; } -bool HighsCutGeneration::finalizeAndAddCut(std::vector& inds_, +bool HighsCutGeneration::finalizeAndAddCut(const HighsDomain& globaldom, + std::vector& inds_, std::vector& vals_, double& rhs_) { complementation.clear(); @@ -1328,7 +1328,7 @@ bool HighsCutGeneration::finalizeAndAddCut(std::vector& inds_, rowlen, rhs_); // apply cut postprocessing including scaling and removal of small // coefficients - if (!postprocessCut()) return false; + if (!postprocessCut(globaldom)) return false; rhs_ = (double)rhs; vals_.resize(rowlen); inds_.resize(rowlen); @@ -1343,8 +1343,7 @@ bool HighsCutGeneration::finalizeAndAddCut(std::vector& inds_, if (violation <= 10 * feastol) return false; - lpRelaxation.getMipSolver().mipdata_->domain.tightenCoefficients( - inds, vals, rowlen, rhs_); + globaldom.tightenCoefficients(inds, vals, rowlen, rhs_); // if the cut is violated by a small factor above the feasibility // tolerance, add it to the cutpool diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index cfe01b3a8c..034a6e3142 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -67,7 +67,7 @@ class HighsCutGeneration { double scale(double val); - bool postprocessCut(); + bool postprocessCut(const HighsDomain& globaldom); bool preprocessBaseInequality(bool& hasUnboundedInts, bool& hasGeneralInts, bool& hasContinuous); @@ -96,12 +96,15 @@ class HighsCutGeneration { /// generate a conflict from the given proof constraint which cuts of the /// given local domain - bool generateConflict(HighsDomain& localdom, std::vector& proofinds, + bool generateConflict(const HighsDomain& localdom, + const HighsDomain& globaldom, + std::vector& proofinds, std::vector& proofvals, double& proofrhs); /// applies postprocessing to an externally generated cut and adds it to the /// cutpool if it is violated enough - bool finalizeAndAddCut(std::vector& inds, std::vector& vals, + bool finalizeAndAddCut(const HighsDomain& globaldom, + std::vector& inds, std::vector& vals, double& rhs); }; diff --git a/highs/mip/HighsCutPool.cpp b/highs/mip/HighsCutPool.cpp index 067c6bc7c2..52c2a3726e 100644 --- a/highs/mip/HighsCutPool.cpp +++ b/highs/mip/HighsCutPool.cpp @@ -115,9 +115,43 @@ double HighsCutPool::getParallelism(HighsInt row1, HighsInt row2) const { return dotprod * rownormalization_[row1] * rownormalization_[row2]; } -void HighsCutPool::lpCutRemoved(HighsInt cut) { +double HighsCutPool::getParallelism(HighsInt row1, HighsInt row2, + const HighsCutPool& pool2) const { + HighsInt i1 = matrix_.getRowStart(row1); + const HighsInt end1 = matrix_.getRowEnd(row1); + + HighsInt i2 = pool2.matrix_.getRowStart(row2); + const HighsInt end2 = pool2.matrix_.getRowEnd(row2); + + const HighsInt* ARindex1 = matrix_.getARindex(); + const double* ARvalue1 = matrix_.getARvalue(); + const HighsInt* ARindex2 = pool2.matrix_.getARindex(); + const double* ARvalue2 = pool2.matrix_.getARvalue(); + + double dotprod = 0.0; + while (i1 != end1 && i2 != end2) { + HighsInt col1 = ARindex1[i1]; + HighsInt col2 = ARindex2[i2]; + + if (col1 < col2) + ++i1; + else if (col2 < col1) + ++i2; + else { + dotprod += ARvalue1[i1] * ARvalue2[i2]; + ++i1; + ++i2; + } + } + + return dotprod * rownormalization_[row1] * pool2.rownormalization_[row2]; +} + +void HighsCutPool::lpCutRemoved(HighsInt cut, bool thread_safe) { + const HighsInt n = numLps_[cut].fetch_add(-1, std::memory_order_relaxed); + if (thread_safe || n > 1) return; if (matrix_.columnsLinked(cut)) { - propRows.erase(std::make_pair(-1, cut)); + propRows.erase(std::make_pair(ages_[cut], cut)); propRows.emplace(1, cut); } ages_[cut] = 1; @@ -136,6 +170,31 @@ void HighsCutPool::performAging() { } for (HighsInt i = 0; i != cutIndexEnd; ++i) { + // Catch buffered changes (should only occur in parallel case) + if (numLps_[i] > 0 && ages_[i] >= 0) { + // Cut has been added to the LP, but age changes haven't been made + --ageDistribution[ages_[i]]; + if (matrix_.columnsLinked(i)) { + propRows.erase(std::make_pair(ages_[i], i)); + propRows.emplace(-1, i); + } + ages_[i] = -1; + ++numLpCuts; + ageResetWhileLocked_[i].store(0, std::memory_order_relaxed); + } else if (numLps_[i] == 0 && ages_[i] == -1 && rhs_[i] != kHighsInf) { + // Cut was removed from the LP, but age changes haven't been made + if (matrix_.columnsLinked(i)) { + propRows.erase(std::make_pair(ages_[i], i)); + propRows.emplace(1, i); + } + ages_[i] = 1; + --numLpCuts; + ++ageDistribution[1]; + ageResetWhileLocked_[i].store(0, std::memory_order_relaxed); + } else if (ageResetWhileLocked_[i].load(std::memory_order_relaxed) == 1) { + resetAge(i); + } + ageResetWhileLocked_[i].store(0, std::memory_order_relaxed); if (ages_[i] < 0) continue; bool isPropagated = matrix_.columnsLinked(i); @@ -156,6 +215,7 @@ void HighsCutPool::performAging() { matrix_.removeRow(i); ages_[i] = -1; rhs_[i] = kHighsInf; + hasSynced_[i] = false; } else { if (isPropagated) propRows.emplace(ages_[i], i); ageDistribution[ages_[i]] += 1; @@ -165,14 +225,15 @@ void HighsCutPool::performAging() { assert((HighsInt)propRows.size() == numPropRows); } -void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, - HighsCutSet& cutset, double feastol) { +void HighsCutPool::separate(const std::vector& sol, + const HighsDomain& domain, HighsCutSet& cutset, + double feastol, + const std::deque& cutpools, + bool thread_safe) { HighsInt nrows = matrix_.getNumRows(); const HighsInt* ARindex = matrix_.getARindex(); const double* ARvalue = matrix_.getARvalue(); - assert(cutset.empty()); - std::vector> efficacious_cuts; HighsInt agelim = agelim_; @@ -185,6 +246,9 @@ void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, for (HighsInt i = 0; i < nrows; ++i) { // cuts with an age of -1 are already in the LP and are therefore skipped + // TODO MT: Parallel case tries to add cuts already in current LP. + // TODO MT: Inefficient. Not sure what happens if added twice. + // TODO MT: The cut shouldn't have enough violation to be added though. if (ages_[i] < 0) continue; HighsInt start = matrix_.getRowStart(i); @@ -201,10 +265,15 @@ void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, // if the cut is not violated more than feasibility tolerance // we skip it and increase its age, otherwise we reset its age - ageDistribution[ages_[i]] -= 1; bool isPropagated = matrix_.columnsLinked(i); - if (isPropagated) propRows.erase(std::make_pair(ages_[i], i)); + if (!thread_safe) { + ageDistribution[ages_[i]] -= 1; + if (isPropagated) { + propRows.erase(std::make_pair(ages_[i], i)); + } + } if (double(viol) <= feastol) { + if (thread_safe) continue; ++ages_[i]; if (ages_[i] >= agelim) { uint64_t h = compute_cut_hash(&ARindex[start], &ARvalue[start], @@ -221,7 +290,9 @@ void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, matrix_.removeRow(i); ages_[i] = -1; - rhs_[i] = 0; + rhs_[i] = kHighsInf; + ageResetWhileLocked_[i].store(0, std::memory_order_relaxed); + hasSynced_[i] = false; auto range = hashToCutMap.equal_range(h); for (auto it = range.first; it != range.second; ++it) { @@ -262,9 +333,11 @@ void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, } } - ages_[i] = 0; - ++ageDistribution[0]; - if (isPropagated) propRows.emplace(ages_[i], i); + if (!thread_safe) { + ages_[i] = 0; + ++ageDistribution[0]; + if (isPropagated) propRows.emplace(ages_[i], i); + } double score = viol / (numActiveNzs * sqrt(double(rownorm))); efficacious_cuts.emplace_back(score, i); @@ -287,8 +360,13 @@ void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, b.second); }); - bestObservedScore = std::max(efficacious_cuts[0].first, bestObservedScore); - double minScore = minScoreFactor * bestObservedScore; + double bestObservedScoreCopy = bestObservedScore; + double& bestObservedScore_ = + thread_safe ? bestObservedScoreCopy : bestObservedScore; + bestObservedScore_ = std::max(efficacious_cuts[0].first, bestObservedScore); + double minScoreFactorCopy = minScoreFactor; + double& minScoreFactor_ = thread_safe ? minScoreFactorCopy : minScoreFactor; + double minScore = minScoreFactor_ * bestObservedScore; HighsInt numefficacious = std::upper_bound(efficacious_cuts.begin(), efficacious_cuts.end(), @@ -303,38 +381,55 @@ void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, if (numefficacious <= lowerThreshold) { numefficacious = std::max(efficacious_cuts.size() / 2, size_t{1}); - minScoreFactor = + minScoreFactor_ = efficacious_cuts[numefficacious - 1].first / bestObservedScore; } else if (numefficacious > upperThreshold) { - minScoreFactor = efficacious_cuts[upperThreshold].first / bestObservedScore; + minScoreFactor_ = + efficacious_cuts[upperThreshold].first / bestObservedScore; } efficacious_cuts.resize(numefficacious); - HighsInt selectednnz = 0; - - assert(cutset.empty()); + HighsInt orignumcuts = cutset.numCuts(); + HighsInt origselectednnz = cutset.ARindex_.size(); + HighsInt selectednnz = origselectednnz; for (const std::pair& p : efficacious_cuts) { bool discard = false; double maxpar = 0.1; - for (HighsInt k : cutset.cutindices) { - if (getParallelism(k, p.second) > maxpar) { - discard = true; - break; + for (HighsInt i = 0; i != static_cast(cutset.cutindices.size()); + ++i) { + if (cutset.cutpools[i] == index_) { + if (getParallelism(cutset.cutindices[i], p.second) > maxpar) { + discard = true; + break; + } + } else { + // TODO MT: This assumes the cuts in the pool are not changing during, + // TODO MT: this query, i.e., the worker's pool and the global pool. + // TODO MT: Currently safe, but doesn't generalise to all designs. + if (getParallelism(p.second, cutset.cutindices[i], + cutpools[cutset.cutpools[i]]) > maxpar) { + discard = true; + break; + } } } if (discard) continue; - --ageDistribution[ages_[p.second]]; - ++numLpCuts; - if (matrix_.columnsLinked(p.second)) { - propRows.erase(std::make_pair(ages_[p.second], p.second)); - propRows.emplace(-1, p.second); + numLps_[p.second].fetch_add(1, std::memory_order_relaxed); + if (!thread_safe) { + --ageDistribution[ages_[p.second]]; + ++numLpCuts; + if (matrix_.columnsLinked(p.second)) { + propRows.erase(std::make_pair(ages_[p.second], p.second)); + propRows.emplace(-1, p.second); + } + ages_[p.second] = -1; } - ages_[p.second] = -1; cutset.cutindices.push_back(p.second); + cutset.cutpools.push_back(index_); selectednnz += matrix_.getRowEnd(p.second) - matrix_.getRowStart(p.second); } @@ -343,8 +438,8 @@ void HighsCutPool::separate(const std::vector& sol, HighsDomain& domain, assert(int(cutset.ARvalue_.size()) == selectednnz); assert(int(cutset.ARindex_.size()) == selectednnz); - HighsInt offset = 0; - for (HighsInt i = 0; i != cutset.numCuts(); ++i) { + HighsInt offset = origselectednnz; + for (HighsInt i = orignumcuts; i != cutset.numCuts(); ++i) { cutset.ARstart_[i] = offset; HighsInt cut = cutset.cutindices[i]; HighsInt start = matrix_.getRowStart(cut); @@ -369,6 +464,7 @@ void HighsCutPool::separateLpCutsAfterRestart(HighsCutSet& cutset) { HighsInt numcuts = matrix_.getNumRows(); cutset.cutindices.resize(numcuts); + cutset.cutpools.resize(numcuts, index_); std::iota(cutset.cutindices.begin(), cutset.cutindices.end(), 0); cutset.resize(matrix_.nonzeroCapacity()); @@ -382,6 +478,7 @@ void HighsCutPool::separateLpCutsAfterRestart(HighsCutSet& cutset) { propRows.erase(std::make_pair(ages_[i], i)); propRows.emplace(-1, i); } + numLps_[i] = 1; ages_[i] = -1; cutset.ARstart_[i] = offset; HighsInt cut = cutset.cutindices[i]; @@ -431,6 +528,14 @@ HighsInt HighsCutPool::addCut(const HighsMipSolver& mipsolver, HighsInt* Rindex, uint64_t h = compute_cut_hash(Rindex, Rvalue, maxabscoef, Rlen); double normalization = 1.0 / double(sqrt(norm)); + // TODO MT: This global duplicate check assumes the global pool doesn't + // have cuts added or deleted during time when local pools can add a cut. + if (this != &mipsolver.mipdata_->getCutPool()) { + if (mipsolver.mipdata_->getCutPool().isDuplicate(h, normalization, Rindex, + Rvalue, Rlen, rhs)) { + return -1; + } + } if (isDuplicate(h, normalization, Rindex, Rvalue, Rlen, rhs)) return -1; // if (Rlen > 0.15 * matrix_.numCols()) @@ -495,6 +600,9 @@ HighsInt HighsCutPool::addCut(const HighsMipSolver& mipsolver, HighsInt* Rindex, if (rowindex == int(rhs_.size())) { rhs_.resize(rowindex + 1); ages_.resize(rowindex + 1); + numLps_.resize(rowindex + 1); + ageResetWhileLocked_.resize(rowindex + 1); + hasSynced_.resize(rowindex + 1); rownormalization_.resize(rowindex + 1); maxabscoef_.resize(rowindex + 1); rowintegral.resize(rowindex + 1); @@ -505,6 +613,9 @@ HighsInt HighsCutPool::addCut(const HighsMipSolver& mipsolver, HighsInt* Rindex, ages_[rowindex] = std::max(HighsInt{0}, agelim_ - 5); ++ageDistribution[ages_[rowindex]]; rowintegral[rowindex] = integral; + numLps_[rowindex] = 0; + ageResetWhileLocked_[rowindex].store(0, std::memory_order_relaxed); + hasSynced_[rowindex] = false; if (propagate) propRows.emplace(ages_[rowindex], rowindex); assert((HighsInt)propRows.size() == numPropRows); @@ -515,7 +626,7 @@ HighsInt HighsCutPool::addCut(const HighsMipSolver& mipsolver, HighsInt* Rindex, for (HighsDomain::CutpoolPropagation* propagationdomain : propagationDomains) propagationdomain->cutAdded(rowindex, propagate); - if (extractCliques && this == &mipsolver.mipdata_->cutpool) { + if (extractCliques && this == &mipsolver.mipdata_->getCutPool()) { // if this is the global cutpool extract cliques from the cut if (Rlen <= 100) mipsolver.mipdata_->cliquetable.extractCliquesFromCut(mipsolver, Rindex, @@ -524,3 +635,28 @@ HighsInt HighsCutPool::addCut(const HighsMipSolver& mipsolver, HighsInt* Rindex, return rowindex; } + +void HighsCutPool::syncCutPool(const HighsMipSolver& mipsolver, + HighsCutPool& syncpool) { + HighsInt cutIndexEnd = matrix_.getNumRows(); + + for (HighsInt i = 0; i != cutIndexEnd; ++i) { + // Only sync cuts in the LP that are not already synced + if ((numLps_[i] > 0 || + ageResetWhileLocked_[i].load(std::memory_order_relaxed) == 1) && + !hasSynced_[i]) { + HighsInt Rlen; + const HighsInt* Rindex; + const double* Rvalue; + getCut(i, Rlen, Rindex, Rvalue); + // copy cut into something mutable (addCut reorders so can't take const) + std::vector idxs(Rindex, Rindex + Rlen); + std::vector vals(Rvalue, Rvalue + Rlen); + syncpool.addCut(mipsolver, idxs.data(), vals.data(), Rlen, rhs_[i], + rowintegral[i]); + hasSynced_[i] = true; + } + } + + assert((HighsInt)propRows.size() == numPropRows); +} \ No newline at end of file diff --git a/highs/mip/HighsCutPool.h b/highs/mip/HighsCutPool.h index 372aefe213..cae731fe21 100644 --- a/highs/mip/HighsCutPool.h +++ b/highs/mip/HighsCutPool.h @@ -8,6 +8,7 @@ #ifndef HIGHS_CUTPOOL_H_ #define HIGHS_CUTPOOL_H_ +#include #include #include #include @@ -20,6 +21,7 @@ class HighsLpRelaxation; struct HighsCutSet { std::vector cutindices; + std::vector cutpools; std::vector ARstart_; std::vector ARindex_; std::vector ARvalue_; @@ -39,6 +41,7 @@ struct HighsCutSet { void clear() { cutindices.clear(); + cutpools.clear(); upper_.clear(); ARstart_.clear(); ARindex_.clear(); @@ -53,6 +56,10 @@ class HighsCutPool { HighsDynamicRowMatrix matrix_; std::vector rhs_; std::vector ages_; + std::deque> numLps_; + std::deque> + ageResetWhileLocked_; // Was the cut propagated? + std::vector hasSynced_; // Has the cut been globally synced? std::vector rownormalization_; std::vector maxabscoef_; std::vector rowintegral; @@ -72,17 +79,18 @@ class HighsCutPool { std::vector ageDistribution; std::vector> sortBuffer; - bool isDuplicate(size_t hash, double norm, const HighsInt* Rindex, - const double* Rvalue, HighsInt Rlen, double rhs); - public: - HighsCutPool(HighsInt ncols, HighsInt agelim, HighsInt softlimit) + HighsInt index_; + + HighsCutPool(HighsInt ncols, HighsInt agelim, HighsInt softlimit, + HighsInt index) : matrix_(ncols), agelim_(agelim), softlimit_(softlimit), numLpCuts(0), numPropNzs(0), - numPropRows(0) { + numPropRows(0), + index_(index) { ageDistribution.resize(agelim_ + 1); minScoreFactor = 0.9; bestObservedScore = 0.0; @@ -92,8 +100,15 @@ class HighsCutPool { const std::vector& getRhs() const { return rhs_; } - void resetAge(HighsInt cut) { + bool isDuplicate(size_t hash, double norm, const HighsInt* Rindex, + const double* Rvalue, HighsInt Rlen, double rhs); + + void resetAge(HighsInt cut, bool thread_safe = false) { if (ages_[cut] > 0) { + if (thread_safe) { + ageResetWhileLocked_[cut].store(1, std::memory_order_relaxed); + return; + } if (matrix_.columnsLinked(cut)) { propRows.erase(std::make_pair(ages_[cut], cut)); propRows.emplace(0, cut); @@ -101,14 +116,18 @@ class HighsCutPool { ageDistribution[ages_[cut]] -= 1; ageDistribution[0] += 1; ages_[cut] = 0; + ageResetWhileLocked_[cut].store(0, std::memory_order_relaxed); } } double getParallelism(HighsInt row1, HighsInt row2) const; + double getParallelism(HighsInt row1, HighsInt row2, + const HighsCutPool& pool2) const; + void performAging(); - void lpCutRemoved(HighsInt cut); + void lpCutRemoved(HighsInt cut, bool thread_safe = false); void addPropagationDomain(HighsDomain::CutpoolPropagation* domain) { propagationDomains.push_back(domain); @@ -128,8 +147,15 @@ class HighsCutPool { ageDistribution.resize(agelim_ + 1); } - void separate(const std::vector& sol, HighsDomain& domprop, - HighsCutSet& cutset, double feastol); + void increaseNumLps(HighsInt cut, HighsInt n) { + assert(numLps_[cut] >= 1); + numLps_[cut].fetch_add(n, std::memory_order_relaxed); + }; + + void separate(const std::vector& sol, const HighsDomain& domprop, + HighsCutSet& cutset, double feastol, + const std::deque& cutpools, + bool thread_safe = false); void separateLpCutsAfterRestart(HighsCutSet& cutset); @@ -163,6 +189,8 @@ class HighsCutPool { cutinds = matrix_.getARindex() + start; cutvals = matrix_.getARvalue() + start; } + + void syncCutPool(const HighsMipSolver& mipsolver, HighsCutPool& syncpool); }; #endif diff --git a/highs/mip/HighsDebugSol.cpp b/highs/mip/HighsDebugSol.cpp index c7e194efe5..f9d8432547 100644 --- a/highs/mip/HighsDebugSol.cpp +++ b/highs/mip/HighsDebugSol.cpp @@ -74,7 +74,7 @@ void HighsDebugSol::activate() { debugSolObjective = double(debugsolobj + mipsolver->orig_model_->offset_); debugSolActive = true; printf("debug sol active\n"); - registerDomain(mipsolver->mipdata_->domain); + registerDomain(mipsolver->mipdata_->getDomain()); } else { highsLogUser(mipsolver->options_mip_->log_options, HighsLogType::kWarning, "debug solution: could not open file '%s'\n", @@ -83,8 +83,8 @@ void HighsDebugSol::activate() { model.lp_ = *mipsolver->model_; model.lp_.col_names_.clear(); model.lp_.row_names_.clear(); - model.lp_.col_lower_ = mipsolver->mipdata_->domain.col_lower_; - model.lp_.col_upper_ = mipsolver->mipdata_->domain.col_upper_; + model.lp_.col_lower_ = mipsolver->mipdata_->getDomain().col_lower_; + model.lp_.col_upper_ = mipsolver->mipdata_->getDomain().col_upper_; FilereaderMps().writeModelToFile(*mipsolver->options_mip_, "debug_mip.mps", model); } @@ -297,7 +297,7 @@ void HighsDebugSol::checkConflictReconvergenceFrontier( } } - auto reconvChg = mipsolver->mipdata_->domain.flip(reconvDomchg.domchg); + auto reconvChg = mipsolver->mipdata_->getDomain().flip(reconvDomchg.domchg); if (reconvChg.boundtype == HighsBoundType::kLower) { if (debugSolution[reconvChg.column] >= reconvChg.boundval) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 0267136562..bc45f618b3 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -143,7 +143,26 @@ HighsDomain::ConflictPoolPropagation::ConflictPoolPropagation( conflictFlag_(other.conflictFlag_), propagateConflictInds_(other.propagateConflictInds_), watchedLiterals_(other.watchedLiterals_) { - conflictpool_->addPropagationDomain(this); + if (!domain->mipsolver->mipdata_->parallelLockActive() || + conflictpool_ != &domain->mipsolver->mipdata_->getConflictPool()) + conflictpool_->addPropagationDomain(this); +} + +HighsDomain::ConflictPoolPropagation& +HighsDomain::ConflictPoolPropagation::operator=( + const ConflictPoolPropagation& other) { + if (this == &other) return *this; + if (conflictpool_) conflictpool_->removePropagationDomain(this); + conflictpoolindex = other.conflictpoolindex; + domain = other.domain; + conflictpool_ = other.conflictpool_; + colLowerWatched_ = other.colLowerWatched_; + colUpperWatched_ = other.colUpperWatched_; + conflictFlag_ = other.conflictFlag_; + propagateConflictInds_ = other.propagateConflictInds_; + watchedLiterals_ = other.watchedLiterals_; + if (conflictpool_) conflictpool_->addPropagationDomain(this); + return *this; } HighsDomain::ConflictPoolPropagation::~ConflictPoolPropagation() { @@ -372,7 +391,38 @@ HighsDomain::CutpoolPropagation::CutpoolPropagation( propagatecutflags_(other.propagatecutflags_), propagatecutinds_(other.propagatecutinds_), capacityThreshold_(other.capacityThreshold_) { - cutpool->addPropagationDomain(this); + if (!domain->mipsolver->mipdata_->parallelLockActive() || + cutpool != &domain->mipsolver->mipdata_->getCutPool()) + cutpool->addPropagationDomain(this); +} + +// TODO MT: ERORRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR +// When a domain is created (based on another existing domain) +// , e.g., in randomizedRounding (as an object +// that we can propagate and play around with) or in RINS (where one +// is created as part of the HighsSearch object), it is going to notify +// all cutpools / conflictpools that the original was propagating. +// This "notify" is going to append the domain to the vector of +// pools, which is going to be non-deterministic and error-prone. +// We therefore shouldn't notify the global cut pool. +// This is fine as the copied domain is likely temporary, +// and will not be majorly affected by not being notified of new cuts. +// Does this safety rail need to be added to the copy-assign code below??? + +HighsDomain::CutpoolPropagation& HighsDomain::CutpoolPropagation::operator=( + const CutpoolPropagation& other) { + if (this == &other) return *this; + if (cutpool) cutpool->removePropagationDomain(this); + cutpoolindex = other.cutpoolindex; + domain = other.domain; + cutpool = other.cutpool; + activitycuts_ = other.activitycuts_; + activitycutsinf_ = other.activitycutsinf_; + propagatecutflags_ = other.propagatecutflags_; + propagatecutinds_ = other.propagatecutinds_; + capacityThreshold_ = other.capacityThreshold_; + if (cutpool) cutpool->addPropagationDomain(this); + return *this; } HighsDomain::CutpoolPropagation::~CutpoolPropagation() { @@ -402,7 +452,7 @@ void HighsDomain::CutpoolPropagation::recomputeCapacityThreshold(HighsInt cut) { void HighsDomain::CutpoolPropagation::cutAdded(HighsInt cut, bool propagate) { if (!propagate) { - if (domain != &domain->mipsolver->mipdata_->domain) return; + if (domain != &domain->mipsolver->mipdata_->getDomain()) return; HighsInt start = cutpool->getMatrix().getRowStart(cut); HighsInt end = cutpool->getMatrix().getRowEnd(cut); const HighsInt* arindex = cutpool->getMatrix().getARindex(); @@ -443,7 +493,7 @@ void HighsDomain::CutpoolPropagation::cutAdded(HighsInt cut, bool propagate) { void HighsDomain::CutpoolPropagation::cutDeleted( HighsInt cut, bool deletedOnlyForPropagation) { if (deletedOnlyForPropagation && - domain == &domain->mipsolver->mipdata_->domain) { + domain == &domain->mipsolver->mipdata_->getDomain()) { assert(domain->branchPos_.empty()); return; } @@ -461,12 +511,12 @@ void HighsDomain::CutpoolPropagation::markPropagateCut(HighsInt cut) { } } -void HighsDomain::CutpoolPropagation::updateActivityLbChange(HighsInt col, - double oldbound, - double newbound) { - assert(!domain->infeasible_); +void HighsDomain::CutpoolPropagation::updateActivityLbChange( + HighsInt col, double oldbound, double newbound, bool threshold, + bool activity, bool infeasdomain) { + if (!infeasdomain) assert(!domain->infeasible_); - if (newbound < oldbound) { + if (newbound < oldbound && threshold) { cutpool->getMatrix().forEachNegativeColumnEntry( col, [&](HighsInt row, double val) { domain->updateThresholdLbChange(col, newbound, val, @@ -475,38 +525,42 @@ void HighsDomain::CutpoolPropagation::updateActivityLbChange(HighsInt col, }); } - cutpool->getMatrix().forEachPositiveColumnEntry( - col, [&](HighsInt row, double val) { - assert(val > 0); - HighsCDouble deltamin = computeDelta(val, oldbound, newbound, - -kHighsInf, activitycutsinf_[row]); - activitycuts_[row] += deltamin; + if (!activity) return; - if (deltamin <= 0) { - domain->updateThresholdLbChange(col, newbound, val, - capacityThreshold_[row]); - return true; - } + if (!infeasdomain) { + cutpool->getMatrix().forEachPositiveColumnEntry( + col, [&](HighsInt row, double val) { + assert(val > 0); + HighsCDouble deltamin = computeDelta( + val, oldbound, newbound, -kHighsInf, activitycutsinf_[row]); + activitycuts_[row] += deltamin; + + if (deltamin <= 0) { + domain->updateThresholdLbChange(col, newbound, val, + capacityThreshold_[row]); + return true; + } - if (activitycutsinf_[row] == 0 && - activitycuts_[row] - cutpool->getRhs()[row] > - domain->mipsolver->mipdata_->feastol) { - // todo, now that multiple cutpools are possible the index needs to be - // encoded differently - domain->mipsolver->mipdata_->debugSolution.nodePruned(*domain); - domain->infeasible_ = true; - domain->infeasible_pos = domain->domchgstack_.size(); - domain->infeasible_reason = Reason::cut(cutpoolindex, row); - return false; - } + if (activitycutsinf_[row] == 0 && + activitycuts_[row] - cutpool->getRhs()[row] > + domain->mipsolver->mipdata_->feastol) { + // todo, now that multiple cutpools are possible the index needs to + // be encoded differently + domain->mipsolver->mipdata_->debugSolution.nodePruned(*domain); + domain->infeasible_ = true; + domain->infeasible_pos = domain->domchgstack_.size(); + domain->infeasible_reason = Reason::cut(cutpoolindex, row); + return false; + } - markPropagateCut(row); + markPropagateCut(row); - return true; - }); + return true; + }); + } if (domain->infeasible_) { - assert(domain->infeasible_reason.type == cutpoolindex); + // assert(domain->infeasible_reason.type == cutpoolindex); assert(domain->infeasible_reason.index >= 0); std::swap(oldbound, newbound); cutpool->getMatrix().forEachPositiveColumnEntry( @@ -515,19 +569,21 @@ void HighsDomain::CutpoolPropagation::updateActivityLbChange(HighsInt col, activitycuts_[row] += computeDelta(val, oldbound, newbound, -kHighsInf, activitycutsinf_[row]); - if (domain->infeasible_reason.index == row) return false; + if (domain->infeasible_reason.index == row && + domain->infeasible_reason.type == cutpoolindex) + return false; return true; }); } } -void HighsDomain::CutpoolPropagation::updateActivityUbChange(HighsInt col, - double oldbound, - double newbound) { - assert(!domain->infeasible_); +void HighsDomain::CutpoolPropagation::updateActivityUbChange( + HighsInt col, double oldbound, double newbound, bool threshold, + bool activity, bool infeasdomain) { + if (!infeasdomain) assert(!domain->infeasible_); - if (newbound > oldbound) { + if (newbound > oldbound && threshold) { cutpool->getMatrix().forEachPositiveColumnEntry( col, [&](HighsInt row, double val) { domain->updateThresholdUbChange(col, newbound, val, @@ -536,36 +592,43 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange(HighsInt col, }); } - cutpool->getMatrix().forEachNegativeColumnEntry( - col, [&](HighsInt row, double val) { - assert(val < 0); - HighsCDouble deltamin = computeDelta(val, oldbound, newbound, kHighsInf, - activitycutsinf_[row]); - activitycuts_[row] += deltamin; + if (!activity) return; - if (deltamin <= 0) { - domain->updateThresholdUbChange(col, newbound, val, - capacityThreshold_[row]); - return true; - } + if (!infeasdomain) { + cutpool->getMatrix().forEachNegativeColumnEntry( + col, [&](HighsInt row, double val) { + assert(val < 0); + HighsCDouble deltamin = computeDelta( + val, oldbound, newbound, kHighsInf, activitycutsinf_[row]); - if (activitycutsinf_[row] == 0 && - activitycuts_[row] - cutpool->getRhs()[row] > - domain->mipsolver->mipdata_->feastol) { - domain->mipsolver->mipdata_->debugSolution.nodePruned(*domain); - domain->infeasible_ = true; - domain->infeasible_pos = domain->domchgstack_.size(); - domain->infeasible_reason = Reason::cut(cutpoolindex, row); - return false; - } + // std::cout << activitycuts_.size() << std::endl; + + activitycuts_[row] += deltamin; + + if (deltamin <= 0) { + domain->updateThresholdUbChange(col, newbound, val, + capacityThreshold_[row]); + return true; + } + + if (activitycutsinf_[row] == 0 && + activitycuts_[row] - cutpool->getRhs()[row] > + domain->mipsolver->mipdata_->feastol) { + domain->mipsolver->mipdata_->debugSolution.nodePruned(*domain); + domain->infeasible_ = true; + domain->infeasible_pos = domain->domchgstack_.size(); + domain->infeasible_reason = Reason::cut(cutpoolindex, row); + return false; + } - markPropagateCut(row); + markPropagateCut(row); - return true; - }); + return true; + }); + } if (domain->infeasible_) { - assert(domain->infeasible_reason.type == cutpoolindex); + // assert(domain->infeasible_reason.type == cutpoolindex); assert(domain->infeasible_reason.index >= 0); std::swap(oldbound, newbound); cutpool->getMatrix().forEachNegativeColumnEntry( @@ -574,7 +637,9 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange(HighsInt col, activitycuts_[row] += computeDelta(val, oldbound, newbound, kHighsInf, activitycutsinf_[row]); - if (domain->infeasible_reason.index == row) return false; + if (domain->infeasible_reason.index == row && + domain->infeasible_reason.type == cutpoolindex) + return false; return true; }); @@ -1586,13 +1651,31 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, } } - if (!infeasible_) { - for (CutpoolPropagation& cutpoolprop : cutpoolpropagation) - cutpoolprop.updateActivityLbChange(col, oldbound, newbound); - } else { + if (infeasible_) { assert(infeasible_reason.type == Reason::kModelRowLower || infeasible_reason.type == Reason::kModelRowUpper); assert(infeasible_reason.index == mip->a_matrix_.index_[end - 1]); + } else { + // Explanation: If cutpoolpropagation[i] returns infeasible, + // we still need to update the domain threshold values using + // cutpoolpropagation[j], j > i, and reverse the activity + // changes made by cutpoolpropagation[j] j < i. + HighsInt infeascutpool = -1; + HighsInt ncutpoolprop = static_cast(cutpoolpropagation.size()); + for (HighsInt i = 0; i != ncutpoolprop; ++i) { + if (!infeasible_) { + cutpoolpropagation[i].updateActivityLbChange(col, oldbound, newbound, + true, true, infeasible_); + if (infeasible_) infeascutpool = i; + } else { + cutpoolpropagation[i].updateActivityLbChange(col, oldbound, newbound, + true, false, infeasible_); + } + } + for (HighsInt i = 0; i < infeascutpool; ++i) { + cutpoolpropagation[i].updateActivityLbChange(col, oldbound, newbound, + false, true, infeasible_); + } } if (infeasible_) { @@ -1741,13 +1824,31 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, } } - if (!infeasible_) { - for (CutpoolPropagation& cutpoolprop : cutpoolpropagation) - cutpoolprop.updateActivityUbChange(col, oldbound, newbound); - } else { + if (infeasible_) { assert(infeasible_reason.type == Reason::kModelRowLower || infeasible_reason.type == Reason::kModelRowUpper); assert(infeasible_reason.index == mip->a_matrix_.index_[end - 1]); + } else { + // Explanation: If cutpoolpropagation[i] returns infeasible, + // we still need to update the domain threshold values using + // cutpoolpropagation[j], j > i, and reverse the activity + // changes made by cutpoolpropagation[j] j < i. + HighsInt infeascutpool = -1; + HighsInt ncutpoolprop = static_cast(cutpoolpropagation.size()); + for (HighsInt i = 0; i != ncutpoolprop; ++i) { + if (!infeasible_) { + cutpoolpropagation[i].updateActivityUbChange(col, oldbound, newbound, + true, true, infeasible_); + if (infeasible_) infeascutpool = i; + } else { + cutpoolpropagation[i].updateActivityUbChange(col, oldbound, newbound, + true, false, infeasible_); + } + } + for (HighsInt i = 0; i < infeascutpool; ++i) { + cutpoolpropagation[i].updateActivityUbChange(col, oldbound, newbound, + false, true, infeasible_); + } } if (infeasible_) { @@ -2448,7 +2549,8 @@ bool HighsDomain::propagate() { for (HighsInt k = 0; k != numproprows; ++k) { HighsInt i = propagateinds[k]; if (propRowNumChangedBounds_[k].first != 0) { - cutpoolprop.cutpool->resetAge(i); + cutpoolprop.cutpool->resetAge( + i, mipsolver->mipdata_->parallelLockActive()); HighsInt start = cutpoolprop.cutpool->getMatrix().getRowStart(i); HighsInt end = start + propRowNumChangedBounds_[k].first; for (HighsInt j = start; j != end && !infeasible_; ++j) @@ -2489,51 +2591,56 @@ double HighsDomain::getColUpperPos(HighsInt col, HighsInt stackpos, return ub; } -void HighsDomain::conflictAnalysis(HighsConflictPool& conflictPool) { - if (&mipsolver->mipdata_->domain == this) return; - if (mipsolver->mipdata_->domain.infeasible() || !infeasible_) return; +void HighsDomain::conflictAnalysis(HighsConflictPool& conflictPool, + HighsDomain& globaldom, + HighsPseudocost& pseudocost) { + if (&globaldom == this) return; + if (globaldom.infeasible() || !infeasible_) return; - mipsolver->mipdata_->domain.propagate(); - if (mipsolver->mipdata_->domain.infeasible()) return; + // Not sure how this should be modified for the workers. + globaldom.propagate(); + if (globaldom.infeasible()) return; - ConflictSet conflictSet(*this); + ConflictSet conflictSet(*this, globaldom); - conflictSet.conflictAnalysis(conflictPool); + conflictSet.conflictAnalysis(conflictPool, pseudocost); } void HighsDomain::conflictAnalysis(const HighsInt* proofinds, const double* proofvals, HighsInt prooflen, double proofrhs, - HighsConflictPool& conflictPool) { - if (&mipsolver->mipdata_->domain == this) return; + HighsConflictPool& conflictPool, + HighsDomain& globaldom, + HighsPseudocost& pseudocost) { + if (&globaldom == this) return; - if (mipsolver->mipdata_->domain.infeasible()) return; + if (globaldom.infeasible()) return; - mipsolver->mipdata_->domain.propagate(); - if (mipsolver->mipdata_->domain.infeasible()) return; + globaldom.propagate(); + if (globaldom.infeasible()) return; - ConflictSet conflictSet(*this); + ConflictSet conflictSet(*this, globaldom); conflictSet.conflictAnalysis(proofinds, proofvals, prooflen, proofrhs, - conflictPool); + conflictPool, pseudocost); } void HighsDomain::conflictAnalyzeReconvergence( const HighsDomainChange& domchg, const HighsInt* proofinds, const double* proofvals, HighsInt prooflen, double proofrhs, - HighsConflictPool& conflictPool) { - if (&mipsolver->mipdata_->domain == this) return; + HighsConflictPool& conflictPool, HighsDomain& globaldom) { + if (&globaldom == this) return; - if (mipsolver->mipdata_->domain.infeasible()) return; + if (globaldom.infeasible()) return; - mipsolver->mipdata_->domain.propagate(); - if (mipsolver->mipdata_->domain.infeasible()) return; + globaldom.propagate(); + if (globaldom.infeasible()) return; - ConflictSet conflictSet(*this); + ConflictSet conflictSet(*this, globaldom); HighsInt ninfmin; HighsCDouble activitymin; - mipsolver->mipdata_->domain.computeMinActivity( - 0, prooflen, proofinds, proofvals, ninfmin, activitymin); + globaldom.computeMinActivity(0, prooflen, proofinds, proofvals, ninfmin, + activitymin); if (ninfmin != 0) return; if (!conflictSet.explainBoundChangeLeq( @@ -2652,9 +2759,10 @@ HighsDomainChange HighsDomain::flip(const HighsDomainChange& domchg) const { double HighsDomain::feastol() const { return mipsolver->mipdata_->feastol; } -HighsDomain::ConflictSet::ConflictSet(HighsDomain& localdom_) +HighsDomain::ConflictSet::ConflictSet(HighsDomain& localdom_, + const HighsDomain& globaldom_) : localdom(localdom_), - globaldom(localdom.mipsolver->mipdata_->domain), + globaldom(globaldom_), reasonSideFrontier(), reconvergenceFrontier(), resolveQueue(), @@ -3642,13 +3750,16 @@ HighsInt HighsDomain::ConflictSet::resolveDepth(std::set& frontier, for (const LocalDomChg& i : resolvedDomainChanges) { auto insertResult = frontier.insert(i); if (insertResult.second) { - if (increaseConflictScore) { + // TODO MT: Currently this conflict score update is suppressed during + // concurrent search + if (increaseConflictScore && + !localdom.mipsolver->mipdata_->parallelLockActive()) { if (localdom.domchgstack_[i.pos].boundtype == HighsBoundType::kLower) - localdom.mipsolver->mipdata_->pseudocost.increaseConflictScoreUp( - localdom.domchgstack_[i.pos].column); + localdom.mipsolver->mipdata_->getPseudoCost() + .increaseConflictScoreUp(localdom.domchgstack_[i.pos].column); else - localdom.mipsolver->mipdata_->pseudocost.increaseConflictScoreDown( - localdom.domchgstack_[i.pos].column); + localdom.mipsolver->mipdata_->getPseudoCost() + .increaseConflictScoreDown(localdom.domchgstack_[i.pos].column); } if (i.pos >= startPos.pos && resolvable(i.pos)) pushQueue(insertResult.first); @@ -3720,20 +3831,35 @@ HighsInt HighsDomain::ConflictSet::computeCuts( return numConflicts; } -void HighsDomain::ConflictSet::conflictAnalysis( - HighsConflictPool& conflictPool) { +void HighsDomain::ConflictSet::conflictAnalysis(HighsConflictPool& conflictPool, + HighsPseudocost& pseudocost) { resolvedDomainChanges.reserve(localdom.domchgstack_.size()); if (!explainInfeasibility()) return; - localdom.mipsolver->mipdata_->pseudocost.increaseConflictWeight(); + // TODO: Only updating global pseudo cost so solution path is identical to + // original code. This should always actually use the given pseudocost? + if (!localdom.mipsolver->mipdata_->parallelLockActive()) { + localdom.mipsolver->mipdata_->getPseudoCost().increaseConflictWeight(); + } else { + pseudocost.increaseConflictWeight(); + } for (const LocalDomChg& locdomchg : resolvedDomainChanges) { - if (locdomchg.domchg.boundtype == HighsBoundType::kLower) - localdom.mipsolver->mipdata_->pseudocost.increaseConflictScoreUp( - locdomchg.domchg.column); - else - localdom.mipsolver->mipdata_->pseudocost.increaseConflictScoreDown( - locdomchg.domchg.column); + if (locdomchg.domchg.boundtype == HighsBoundType::kLower) { + if (!localdom.mipsolver->mipdata_->parallelLockActive()) { + localdom.mipsolver->mipdata_->getPseudoCost().increaseConflictScoreUp( + locdomchg.domchg.column); + } else { + pseudocost.increaseConflictScoreUp(locdomchg.domchg.column); + } + } else { + if (!localdom.mipsolver->mipdata_->parallelLockActive()) { + localdom.mipsolver->mipdata_->getPseudoCost().increaseConflictScoreDown( + locdomchg.domchg.column); + } else { + pseudocost.increaseConflictScoreDown(locdomchg.domchg.column); + } + } } if (10 * resolvedDomainChanges.size() > @@ -3785,9 +3911,12 @@ void HighsDomain::ConflictSet::conflictAnalysis( conflictPool.addConflictCut(localdom, reasonSideFrontier); } -void HighsDomain::ConflictSet::conflictAnalysis( - const HighsInt* proofinds, const double* proofvals, HighsInt prooflen, - double proofrhs, HighsConflictPool& conflictPool) { +void HighsDomain::ConflictSet::conflictAnalysis(const HighsInt* proofinds, + const double* proofvals, + HighsInt prooflen, + double proofrhs, + HighsConflictPool& conflictPool, + HighsPseudocost& pseudocost) { resolvedDomainChanges.reserve(localdom.domchgstack_.size()); HighsInt ninfmin; @@ -3800,14 +3929,15 @@ void HighsDomain::ConflictSet::conflictAnalysis( double(activitymin))) return; - localdom.mipsolver->mipdata_->pseudocost.increaseConflictWeight(); + HighsPseudocost& ps = localdom.mipsolver->mipdata_->parallelLockActive() + ? pseudocost + : localdom.mipsolver->mipdata_->getPseudoCost(); + ps.increaseConflictWeight(); for (const LocalDomChg& locdomchg : resolvedDomainChanges) { if (locdomchg.domchg.boundtype == HighsBoundType::kLower) - localdom.mipsolver->mipdata_->pseudocost.increaseConflictScoreUp( - locdomchg.domchg.column); + ps.increaseConflictScoreUp(locdomchg.domchg.column); else - localdom.mipsolver->mipdata_->pseudocost.increaseConflictScoreDown( - locdomchg.domchg.column); + ps.increaseConflictScoreDown(locdomchg.domchg.column); } if (10 * resolvedDomainChanges.size() > diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index ec16516d3e..ba10c81c5f 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -14,6 +14,7 @@ #include #include +#include "HighsPseudocost.h" #include "mip/HighsDomainChange.h" #include "mip/HighsMipSolver.h" #include "util/HighsCDouble.h" @@ -61,7 +62,7 @@ class HighsDomain { class ConflictSet { friend class HighsDomain; HighsDomain& localdom; - HighsDomain& globaldom; + const HighsDomain& globaldom; public: struct LocalDomChg { @@ -71,12 +72,14 @@ class HighsDomain { bool operator<(const LocalDomChg& other) const { return pos < other.pos; } }; - ConflictSet(HighsDomain& localdom); + ConflictSet(HighsDomain& localdom, const HighsDomain& globaldom); - void conflictAnalysis(HighsConflictPool& conflictPool); + void conflictAnalysis(HighsConflictPool& conflictPool, + HighsPseudocost& pseudocost); void conflictAnalysis(const HighsInt* proofinds, const double* proofvals, HighsInt prooflen, double proofrhs, - HighsConflictPool& conflictPool); + HighsConflictPool& conflictPool, + HighsPseudocost& pseudocost); private: std::set reasonSideFrontier; @@ -166,6 +169,8 @@ class HighsDomain { CutpoolPropagation(const CutpoolPropagation& other); + CutpoolPropagation& operator=(const CutpoolPropagation& other); + ~CutpoolPropagation(); void recomputeCapacityThreshold(HighsInt cut); @@ -176,9 +181,13 @@ class HighsDomain { void markPropagateCut(HighsInt cut); - void updateActivityLbChange(HighsInt col, double oldbound, double newbound); + void updateActivityLbChange(HighsInt col, double oldbound, double newbound, + bool threshold, bool activity, + bool infeasdomain); - void updateActivityUbChange(HighsInt col, double oldbound, double newbound); + void updateActivityUbChange(HighsInt col, double oldbound, double newbound, + bool threshold, bool activity, + bool infeasdomain); }; struct ConflictPoolPropagation { @@ -203,6 +212,8 @@ class HighsDomain { ConflictPoolPropagation(const ConflictPoolPropagation& other); + ConflictPoolPropagation& operator=(const ConflictPoolPropagation& other); + ~ConflictPoolPropagation(); void linkWatchedLiteral(HighsInt linkPos); @@ -284,7 +295,10 @@ class HighsDomain { void recomputeCapacityThreshold(); }; + // public: std::vector changedcolsflags_; + + // private: std::vector changedcols_; std::vector> propRowNumChangedBounds_; @@ -587,17 +601,20 @@ class HighsDomain { double getColUpperPos(HighsInt col, HighsInt stackpos, HighsInt& pos) const; - void conflictAnalysis(HighsConflictPool& conflictPool); + void conflictAnalysis(HighsConflictPool& conflictPool, HighsDomain& globaldom, + HighsPseudocost& pseudocost); void conflictAnalysis(const HighsInt* proofinds, const double* proofvals, HighsInt prooflen, double proofrhs, - HighsConflictPool& conflictPool); + HighsConflictPool& conflictPool, HighsDomain& globaldom, + HighsPseudocost& pseudocost); void conflictAnalyzeReconvergence(const HighsDomainChange& domchg, const HighsInt* proofinds, const double* proofvals, HighsInt prooflen, double proofrhs, - HighsConflictPool& conflictPool); + HighsConflictPool& conflictPool, + HighsDomain& globaldom); void tightenCoefficients(HighsInt* inds, double* vals, HighsInt len, double& rhs) const; diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index ecb647500f..a57c83aa1a 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -13,7 +13,7 @@ #include "mip/MipTimer.h" bool HighsImplications::computeImplications(HighsInt col, bool val) { - HighsDomain& globaldomain = mipsolver.mipdata_->domain; + HighsDomain& globaldomain = mipsolver.mipdata_->getDomain(); HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; globaldomain.propagate(); if (globaldomain.infeasible() || globaldomain.isFixed(col)) return true; @@ -67,8 +67,8 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { HighsInt stackimplicend = domchgstack.size(); numImplications += stackimplicend; - mipsolver.mipdata_->pseudocost.addInferenceObservation(col, numImplications, - val); + mipsolver.mipdata_->getPseudoCost().addInferenceObservation( + col, numImplications, val); std::vector implics; implics.reserve(numImplications); @@ -155,7 +155,8 @@ static constexpr bool kSkipBadVbds = true; static constexpr bool kUseDualsForBreakingTies = true; std::pair HighsImplications::getBestVub( - HighsInt col, const HighsSolution& lpSolution, double& bestUb) const { + HighsInt col, const HighsSolution& lpSolution, double& bestUb, + const HighsDomain& globaldom) const { std::pair bestVub = std::make_pair(-1, VarBound{0.0, kHighsInf}); double minbestUb = bestUb; @@ -179,8 +180,7 @@ std::pair HighsImplications::getBestVub( return false; }; - double scale = mipsolver.mipdata_->domain.col_upper_[col] - - mipsolver.mipdata_->domain.col_lower_[col]; + double scale = globaldom.col_upper_[col] - globaldom.col_lower_[col]; if (scale == kHighsInf) scale = 1.0; else @@ -188,8 +188,8 @@ std::pair HighsImplications::getBestVub( vubs[col].for_each([&](HighsInt vubCol, const VarBound& vub) { if (vub.coef == kHighsInf) return; - if (mipsolver.mipdata_->domain.isFixed(vubCol)) return; - assert(mipsolver.mipdata_->domain.isBinary(vubCol)); + if (globaldom.isFixed(vubCol)) return; + assert(globaldom.isBinary(vubCol)); double vubval = lpSolution.col_value[vubCol] * vub.coef + vub.constant; double ubDist = std::max(0.0, vubval - lpSolution.col_value[col]); @@ -228,7 +228,8 @@ std::pair HighsImplications::getBestVub( } std::pair HighsImplications::getBestVlb( - HighsInt col, const HighsSolution& lpSolution, double& bestLb) const { + HighsInt col, const HighsSolution& lpSolution, double& bestLb, + const HighsDomain& globaldom) const { std::pair bestVlb = std::make_pair(-1, VarBound{0.0, -kHighsInf}); double maxbestlb = bestLb; @@ -252,8 +253,7 @@ std::pair HighsImplications::getBestVlb( return false; }; - double scale = mipsolver.mipdata_->domain.col_upper_[col] - - mipsolver.mipdata_->domain.col_lower_[col]; + double scale = globaldom.col_upper_[col] - globaldom.col_lower_[col]; if (scale == kHighsInf) scale = 1.0; else @@ -261,8 +261,8 @@ std::pair HighsImplications::getBestVlb( vlbs[col].for_each([&](HighsInt vlbCol, const VarBound& vlb) { if (vlb.coef == -kHighsInf) return; - if (mipsolver.mipdata_->domain.isFixed(vlbCol)) return; - assert(mipsolver.mipdata_->domain.isBinary(vlbCol)); + if (globaldom.isFixed(vlbCol)) return; + assert(globaldom.isBinary(vlbCol)); assert(vlbCol >= 0 && vlbCol < mipsolver.numCol()); double vlbval = lpSolution.col_value[vlbCol] * vlb.coef + vlb.constant; double lbDist = std::max(0.0, lpSolution.col_value[col] - vlbval); @@ -296,7 +296,7 @@ std::pair HighsImplications::getBestVlb( } bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { - HighsDomain& globaldomain = mipsolver.mipdata_->domain; + HighsDomain& globaldomain = mipsolver.mipdata_->getDomain(); if (globaldomain.isBinary(col) && !implicationsCached(col, 1) && !implicationsCached(col, 0) && mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { @@ -363,7 +363,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { substitutions.push_back(substitution); colsubstituted[implcol] = true; ++numReductions; - } else { + } else if (!mipsolver.mipdata_->parallelLockActive()) { double lb = std::min(lbDown, lbUp); double ub = std::max(ubDown, ubUp); @@ -411,7 +411,7 @@ void HighsImplications::strengthenVarBound(VarBound& vbnd, void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant) { addVUB(col, vubcol, vubcoef, vubconstant, - mipsolver.mipdata_->domain.col_upper_[col], + mipsolver.mipdata_->getDomain().col_upper_[col], mipsolver.isColIntegral(col)); } @@ -452,7 +452,7 @@ void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, double vlbconstant) { addVLB(col, vlbcol, vlbcoef, vlbconstant, - mipsolver.mipdata_->domain.col_lower_[col], + mipsolver.mipdata_->getDomain().col_lower_[col], mipsolver.isColIntegral(col)); } @@ -531,7 +531,7 @@ void HighsImplications::rebuild(HighsInt ncols, HighsInt newVubCol = orig2reducedcol[vubCol]; if (newVubCol == -1) return; - if (!mipsolver.mipdata_->domain.isBinary(newVubCol) || + if (!mipsolver.mipdata_->getDomain().isBinary(newVubCol) || !mipsolver.mipdata_->postSolveStack.isColLinearlyTransformable( newVubCol)) return; @@ -543,7 +543,7 @@ void HighsImplications::rebuild(HighsInt ncols, HighsInt newVlbCol = orig2reducedcol[vlbCol]; if (newVlbCol == -1) return; - if (!mipsolver.mipdata_->domain.isBinary(newVlbCol) || + if (!mipsolver.mipdata_->getDomain().isBinary(newVlbCol) || !mipsolver.mipdata_->postSolveStack.isColLinearlyTransformable( newVlbCol)) return; @@ -564,12 +564,12 @@ void HighsImplications::buildFrom(const HighsImplications& init) { for (HighsInt i = 0; i != numcol; ++i) { init.vubs[i].for_each([&](HighsInt vubCol, VarBound vub) { - if (!mipsolver.mipdata_->domain.isBinary(vubCol)) return; + if (!mipsolver.mipdata_->getDomain().isBinary(vubCol)) return; addVUB(i, vubCol, vub.coef, vub.constant); }); init.vlbs[i].for_each([&](HighsInt vlbCol, VarBound vlb) { - if (!mipsolver.mipdata_->domain.isBinary(vlbCol)) return; + if (!mipsolver.mipdata_->getDomain().isBinary(vlbCol)) return; addVLB(i, vlbCol, vlb.coef, vlb.constant); }); @@ -582,9 +582,8 @@ void HighsImplications::buildFrom(const HighsImplications& init) { void HighsImplications::separateImpliedBounds( const HighsLpRelaxation& lpRelaxation, const std::vector& sol, - HighsCutPool& cutpool, double feastol) { - HighsDomain& globaldomain = mipsolver.mipdata_->domain; - + HighsCutPool& cutpool, double feastol, HighsDomain& globaldom, + bool thread_safe) { std::array inds; std::array vals; double rhs; @@ -592,7 +591,7 @@ void HighsImplications::separateImpliedBounds( HighsInt numboundchgs = 0; // first do probing on all candidates that have not been probed yet - if (!mipsolver.mipdata_->cliquetable.isFull()) { + if (!mipsolver.mipdata_->cliquetable.isFull() && !thread_safe) { auto oldNumQueries = mipsolver.mipdata_->cliquetable.numNeighbourhoodQueries; HighsInt oldNumEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); @@ -600,8 +599,8 @@ void HighsImplications::separateImpliedBounds( for (std::pair fracint : lpRelaxation.getFractionalIntegers()) { HighsInt col = fracint.first; - if (globaldomain.col_lower_[col] != 0.0 || - globaldomain.col_upper_[col] != 1.0 || + if (globaldom.col_lower_[col] != 0.0 || + globaldom.col_upper_[col] != 1.0 || (implicationsCached(col, 0) && implicationsCached(col, 1))) continue; @@ -609,7 +608,7 @@ void HighsImplications::separateImpliedBounds( const bool probing_result = runProbing(col, numboundchgs); mipsolver.analysis_.mipTimerStop(kMipClockProbingImplications); if (probing_result) { - if (globaldomain.infeasible()) return; + if (globaldom.infeasible()) return; } if (mipsolver.mipdata_->cliquetable.isFull()) break; @@ -626,7 +625,9 @@ void HighsImplications::separateImpliedBounds( if (nextCleanupCall < 0) { // HighsInt oldNumEntries = // mipsolver.mipdata_->cliquetable.getNumEntries(); - mipsolver.mipdata_->cliquetable.runCliqueMerging(globaldomain); + if (!mipsolver.mipdata_->parallelLockActive()) + mipsolver.mipdata_->cliquetable.runCliqueMerging(globaldom); + // printf("numEntries: %d, beforeMerging: %d\n", // mipsolver.mipdata_->cliquetable.getNumEntries(), oldNumEntries); nextCleanupCall = @@ -635,22 +636,22 @@ void HighsImplications::separateImpliedBounds( // printf("nextCleanupCall: %d\n", nextCleanupCall); } - mipsolver.mipdata_->cliquetable.numNeighbourhoodQueries = oldNumQueries; + if (!mipsolver.mipdata_->parallelLockActive()) + mipsolver.mipdata_->cliquetable.numNeighbourhoodQueries = oldNumQueries; } for (std::pair fracint : lpRelaxation.getFractionalIntegers()) { HighsInt col = fracint.first; // skip non binary variables - if (globaldomain.col_lower_[col] != 0.0 || - globaldomain.col_upper_[col] != 1.0) + if (globaldom.col_lower_[col] != 0.0 || globaldom.col_upper_[col] != 1.0) continue; bool infeas; if (implicationsCached(col, 1)) { const std::vector& implics = getImplications(col, 1, infeas); - if (globaldomain.infeasible()) return; + if (globaldom.infeasible()) return; if (infeas) { vals[0] = 1.0; @@ -664,27 +665,27 @@ void HighsImplications::separateImpliedBounds( for (HighsInt i = 0; i < nimplics; ++i) { if (implics[i].boundtype == HighsBoundType::kUpper) { if (implics[i].boundval + feastol >= - globaldomain.col_upper_[implics[i].column]) + globaldom.col_upper_[implics[i].column]) continue; vals[0] = 1.0; inds[0] = implics[i].column; vals[1] = - globaldomain.col_upper_[implics[i].column] - implics[i].boundval; + globaldom.col_upper_[implics[i].column] - implics[i].boundval; inds[1] = col; - rhs = globaldomain.col_upper_[implics[i].column]; + rhs = globaldom.col_upper_[implics[i].column]; } else { if (implics[i].boundval - feastol <= - globaldomain.col_lower_[implics[i].column]) + globaldom.col_lower_[implics[i].column]) continue; vals[0] = -1.0; inds[0] = implics[i].column; vals[1] = - implics[i].boundval - globaldomain.col_lower_[implics[i].column]; + implics[i].boundval - globaldom.col_lower_[implics[i].column]; inds[1] = col; - rhs = -globaldomain.col_lower_[implics[i].column]; + rhs = -globaldom.col_lower_[implics[i].column]; } double viol = sol[inds[0]] * vals[0] + sol[inds[1]] * vals[1] - rhs; @@ -701,7 +702,7 @@ void HighsImplications::separateImpliedBounds( if (implicationsCached(col, 0)) { const std::vector& implics = getImplications(col, 0, infeas); - if (globaldomain.infeasible()) return; + if (globaldom.infeasible()) return; if (infeas) { vals[0] = -1.0; @@ -715,24 +716,24 @@ void HighsImplications::separateImpliedBounds( for (HighsInt i = 0; i < nimplics; ++i) { if (implics[i].boundtype == HighsBoundType::kUpper) { if (implics[i].boundval + feastol >= - globaldomain.col_upper_[implics[i].column]) + globaldom.col_upper_[implics[i].column]) continue; vals[0] = 1.0; inds[0] = implics[i].column; vals[1] = - implics[i].boundval - globaldomain.col_upper_[implics[i].column]; + implics[i].boundval - globaldom.col_upper_[implics[i].column]; inds[1] = col; rhs = implics[i].boundval; } else { if (implics[i].boundval - feastol <= - globaldomain.col_lower_[implics[i].column]) + globaldom.col_lower_[implics[i].column]) continue; vals[0] = -1.0; inds[0] = implics[i].column; vals[1] = - globaldomain.col_lower_[implics[i].column] - implics[i].boundval; + globaldom.col_lower_[implics[i].column] - implics[i].boundval; inds[1] = col; rhs = -implics[i].boundval; } @@ -751,8 +752,8 @@ void HighsImplications::separateImpliedBounds( } void HighsImplications::cleanupVarbounds(HighsInt col) { - double ub = mipsolver.mipdata_->domain.col_upper_[col]; - double lb = mipsolver.mipdata_->domain.col_lower_[col]; + double ub = mipsolver.mipdata_->getDomain().col_upper_[col]; + double lb = mipsolver.mipdata_->getDomain().col_lower_[col]; if (ub == lb) { HighsInt numVubs = 0; @@ -825,10 +826,10 @@ void HighsImplications::cleanupVlb(HighsInt col, HighsInt vlbCol, mipsolver.mipdata_->debugSolution.checkVlb(col, vlbCol, vlb.coef, vlb.constant); } else if (allowBoundChanges && minlb > lb + mipsolver.mipdata_->epsilon) { - mipsolver.mipdata_->domain.changeBound(HighsBoundType::kLower, col, - static_cast(minlb), - HighsDomain::Reason::unspecified()); - infeasible = mipsolver.mipdata_->domain.infeasible(); + mipsolver.mipdata_->getDomain().changeBound( + HighsBoundType::kLower, col, static_cast(minlb), + HighsDomain::Reason::unspecified()); + infeasible = mipsolver.mipdata_->getDomain().infeasible(); } } @@ -866,10 +867,10 @@ void HighsImplications::cleanupVub(HighsInt col, HighsInt vubCol, mipsolver.mipdata_->debugSolution.checkVub(col, vubCol, vub.coef, vub.constant); } else if (allowBoundChanges && maxub < ub - mipsolver.mipdata_->epsilon) { - mipsolver.mipdata_->domain.changeBound(HighsBoundType::kUpper, col, - static_cast(maxub), - HighsDomain::Reason::unspecified()); - infeasible = mipsolver.mipdata_->domain.infeasible(); + mipsolver.mipdata_->getDomain().changeBound( + HighsBoundType::kUpper, col, static_cast(maxub), + HighsDomain::Reason::unspecified()); + infeasible = mipsolver.mipdata_->getDomain().infeasible(); } } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 6a7c412153..a82cc1d1a9 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -161,11 +161,13 @@ class HighsImplications { std::pair getBestVub(HighsInt col, const HighsSolution& lpSolution, - double& bestUb) const; + double& bestUb, + const HighsDomain& globaldom) const; std::pair getBestVlb(HighsInt col, const HighsSolution& lpSolution, - double& bestLb) const; + double& bestLb, + const HighsDomain& globaldom) const; bool runProbing(HighsInt col, HighsInt& numReductions); @@ -176,7 +178,8 @@ class HighsImplications { void separateImpliedBounds(const HighsLpRelaxation& lpRelaxation, const std::vector& sol, - HighsCutPool& cutpool, double feastol); + HighsCutPool& cutpool, double feastol, + HighsDomain& globaldom, bool thread_safe); void cleanupVarbounds(HighsInt col); diff --git a/highs/mip/HighsLpRelaxation.cpp b/highs/mip/HighsLpRelaxation.cpp index fbded4b022..64f85afaee 100644 --- a/highs/mip/HighsLpRelaxation.cpp +++ b/highs/mip/HighsLpRelaxation.cpp @@ -14,6 +14,7 @@ #include "mip/HighsDomain.h" #include "mip/HighsMipSolver.h" #include "mip/HighsMipSolverData.h" +#include "mip/HighsMipWorker.h" #include "mip/HighsPseudocost.h" #include "mip/MipTimer.h" #include "util/HighsCDouble.h" @@ -85,7 +86,9 @@ void HighsLpRelaxation::LpRow::get(const HighsMipSolver& mipsolver, const double*& vals) const { switch (origin) { case kCutPool: - mipsolver.mipdata_->cutpool.getCut(index, len, inds, vals); + assert(cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + mipsolver.mipdata_->cutpools[cutpoolindex].getCut(index, len, inds, vals); break; case kModel: mipsolver.mipdata_->getRow(index, len, inds, vals); @@ -96,7 +99,9 @@ HighsInt HighsLpRelaxation::LpRow::getRowLen( const HighsMipSolver& mipsolver) const { switch (origin) { case kCutPool: - return mipsolver.mipdata_->cutpool.getRowLength(index); + assert(cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + return mipsolver.mipdata_->cutpools[cutpoolindex].getRowLength(index); case kModel: return mipsolver.mipdata_->ARstart_[index + 1] - mipsolver.mipdata_->ARstart_[index]; @@ -110,7 +115,9 @@ bool HighsLpRelaxation::LpRow::isIntegral( const HighsMipSolver& mipsolver) const { switch (origin) { case kCutPool: - return mipsolver.mipdata_->cutpool.cutIsIntegral(index); + assert(cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + return mipsolver.mipdata_->cutpools[cutpoolindex].cutIsIntegral(index); case kModel: return (mipsolver.mipdata_->rowintegral[index] != 0); }; @@ -123,7 +130,9 @@ double HighsLpRelaxation::LpRow::getMaxAbsVal( const HighsMipSolver& mipsolver) const { switch (origin) { case kCutPool: - return mipsolver.mipdata_->cutpool.getMaxAbsCutCoef(index); + assert(cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + return mipsolver.mipdata_->cutpools[cutpoolindex].getMaxAbsCutCoef(index); case kModel: return mipsolver.mipdata_->maxAbsRowCoef[index]; }; @@ -132,29 +141,34 @@ double HighsLpRelaxation::LpRow::getMaxAbsVal( return 0.0; } -double HighsLpRelaxation::slackLower(HighsInt row) const { +double HighsLpRelaxation::slackLower(HighsInt row, + const HighsDomain& globaldom) const { switch (lprows[row].origin) { case LpRow::kCutPool: - return mipsolver.mipdata_->domain.getMinCutActivity( - mipsolver.mipdata_->cutpool, lprows[row].index); + assert(lprows[row].cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + return globaldom.getMinCutActivity( + mipsolver.mipdata_->cutpools[lprows[row].cutpoolindex], + lprows[row].index); case LpRow::kModel: double rowlower = rowLower(row); if (rowlower != -kHighsInf) return rowlower; - return mipsolver.mipdata_->domain.getMinActivity(lprows[row].index); + return globaldom.getMinActivity(lprows[row].index); }; assert(false); return -kHighsInf; } -double HighsLpRelaxation::slackUpper(HighsInt row) const { +double HighsLpRelaxation::slackUpper(HighsInt row, + const HighsDomain& globaldom) const { double rowupper = rowUpper(row); switch (lprows[row].origin) { case LpRow::kCutPool: return rowupper; case LpRow::kModel: if (rowupper != kHighsInf) return rowupper; - return mipsolver.mipdata_->domain.getMaxActivity(lprows[row].index); + return globaldom.getMaxActivity(lprows[row].index); }; assert(false); @@ -168,7 +182,7 @@ double HighsLpRelaxation::slackUpper(HighsInt row) const { // set true when the HighsLpRelaxation instance is created as part of // a new HighsMipSolverData instance HighsLpRelaxation::HighsLpRelaxation(const HighsMipSolver& mipsolver) - : mipsolver(mipsolver) { + : mipsolver(mipsolver), worker_(nullptr) { lpsolver.setOptionValue("output_flag", false); lpsolver.setOptionValue("random_seed", mipsolver.options_mip_->random_seed); // Set primal feasibility tolerance for LP solves according to @@ -206,7 +220,8 @@ HighsLpRelaxation::HighsLpRelaxation(const HighsLpRelaxation& other) objective(other.objective), basischeckpoint(other.basischeckpoint), currentbasisstored(other.currentbasisstored), - adjustSymBranchingCol(other.adjustSymBranchingCol) { + adjustSymBranchingCol(other.adjustSymBranchingCol), + worker_(nullptr) { lpsolver.setOptionValue("output_flag", false); lpsolver.passOptions(other.lpsolver.getOptions()); lpsolver.passModel(other.lpsolver.getLp()); @@ -227,8 +242,10 @@ HighsLpRelaxation::HighsLpRelaxation(const HighsLpRelaxation& other) void HighsLpRelaxation::loadModel() { HighsLp lpmodel = *mipsolver.model_; - lpmodel.col_lower_ = mipsolver.mipdata_->domain.col_lower_; - lpmodel.col_upper_ = mipsolver.mipdata_->domain.col_upper_; + lpmodel.col_lower_ = worker_ ? worker_->globaldom_->col_lower_ + : mipsolver.mipdata_->getDomain().col_lower_; + lpmodel.col_upper_ = worker_ ? worker_->globaldom_->col_upper_ + : mipsolver.mipdata_->getDomain().col_upper_; lpmodel.offset_ = 0; lprows.clear(); lprows.reserve(lpmodel.num_row_); @@ -243,14 +260,15 @@ void HighsLpRelaxation::loadModel() { colUbBuffer.resize(num_col); } -void HighsLpRelaxation::resetToGlobalDomain() { +void HighsLpRelaxation::resetToGlobalDomain(const HighsDomain& globaldom) { lpsolver.changeColsBounds(0, mipsolver.numCol() - 1, - mipsolver.mipdata_->domain.col_lower_.data(), - mipsolver.mipdata_->domain.col_upper_.data()); + globaldom.col_lower_.data(), + globaldom.col_upper_.data()); } -void HighsLpRelaxation::computeBasicDegenerateDuals(double threshold, - HighsDomain* localdom) { +void HighsLpRelaxation::computeBasicDegenerateDuals( + double threshold, HighsDomain& localdom, HighsDomain& globaldom, + HighsConflictPool& conflictpool, bool getdualproof) { if (!lpsolver.hasInvert()) return; HighsInt k = 0; @@ -368,7 +386,7 @@ void HighsLpRelaxation::computeBasicDegenerateDuals(double threshold, if (degenerateColDual < threshold) continue; - if (degenerateColDual == kHighsInf && localdom) { + if (degenerateColDual == kHighsInf && getdualproof) { HighsCDouble rhs = 0; for (HighsInt i = 0; i < row_ep.count; ++i) { HighsInt iRow = row_ep.index[i]; @@ -399,10 +417,10 @@ void HighsLpRelaxation::computeBasicDegenerateDuals(double threshold, domchg.boundval = lp.col_upper_[var]; } - localdom->conflictAnalyzeReconvergence( + localdom.conflictAnalyzeReconvergence( domchg, row_ap.nonzeroinds.data(), dualproofvals.data(), - row_ap.nonzeroinds.size(), double(rhs), - mipsolver.mipdata_->conflictPool); + static_cast(row_ap.nonzeroinds.size()), + static_cast(rhs), conflictpool, globaldom); continue; } @@ -504,7 +522,7 @@ void HighsLpRelaxation::addCuts(HighsCutSet& cutset) { lprows.reserve(lprows.size() + numcuts); for (HighsInt i = 0; i != numcuts; ++i) - lprows.push_back(LpRow::cut(cutset.cutindices[i])); + lprows.push_back(LpRow::cut(cutset.cutindices[i], cutset.cutpools[i])); bool success = lpsolver.addRows(numcuts, cutset.lower_.data(), cutset.upper_.data(), @@ -531,13 +549,42 @@ void HighsLpRelaxation::removeObsoleteRows(bool notifyPool) { if (ndelcuts == 0) deletemask.resize(nlprows); ++ndelcuts; deletemask[i] = 1; - if (notifyPool) mipsolver.mipdata_->cutpool.lpCutRemoved(lprows[i].index); + if (notifyPool) { + assert(lprows[i].cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + mipsolver.mipdata_->cutpools[lprows[i].cutpoolindex].lpCutRemoved( + lprows[i].index, mipsolver.mipdata_->parallelLockActive()); + } } } removeCuts(ndelcuts, deletemask); } +void HighsLpRelaxation::removeWorkerSpecificRows() { + HighsInt nlprows = numRows(); + HighsInt nummodelrows = getNumModelRows(); + std::vector deletemask; + + HighsInt ndelcuts = 0; + for (HighsInt i = nummodelrows; i != nlprows; ++i) { + assert(lprows[i].origin == LpRow::Origin::kCutPool); + if (lprows[i].cutpoolindex > 0) { + if (ndelcuts == 0) deletemask.resize(nlprows); + ++ndelcuts; + deletemask[i] = 1; + } + } + + if (ndelcuts > 0) { + HighsBasis root_basis = mipsolver.mipdata_->firstrootbasis; + root_basis.row_status.resize(numRows(), HighsBasisStatus::kBasic); + getLpSolver().setBasis(root_basis); + } + + removeCuts(ndelcuts, deletemask); +} + void HighsLpRelaxation::removeCuts(HighsInt ndelcuts, std::vector& deletemask) { assert(lpsolver.getLp().num_row_ == @@ -563,7 +610,7 @@ void HighsLpRelaxation::removeCuts(HighsInt ndelcuts, basis.debug_origin_name = "HighsLpRelaxation::removeCuts"; lpsolver.setBasis(basis); lpsolver.optimizeLp(); - if (!mipsolver.submip) { + if (!mipsolver.submip && !mipsolver.mipdata_->parallelLockActive()) { const HighsSubSolverCallTime& sub_solver_call_time = lpsolver.getSubSolverCallTime(); mipsolver.analysis_.addSubSolverCallTime(sub_solver_call_time); @@ -584,8 +631,12 @@ void HighsLpRelaxation::removeCuts() { lpsolver.deleteRows(modelrows, nlprows - 1); for (HighsInt i = modelrows; i != nlprows; ++i) { - if (lprows[i].origin == LpRow::Origin::kCutPool) - mipsolver.mipdata_->cutpool.lpCutRemoved(lprows[i].index); + if (lprows[i].origin == LpRow::Origin::kCutPool) { + assert(lprows[i].cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + mipsolver.mipdata_->cutpools[lprows[i].cutpoolindex].lpCutRemoved( + lprows[i].index, mipsolver.mipdata_->parallelLockActive()); + } } lprows.resize(modelrows); assert(lpsolver.getLp().num_row_ == @@ -630,7 +681,10 @@ void HighsLpRelaxation::performAging(bool deleteRows) { if (ndelcuts == 0) deletemask.resize(nlprows); ++ndelcuts; deletemask[i] = 1; - mipsolver.mipdata_->cutpool.lpCutRemoved(lprows[i].index); + assert(lprows[i].cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + mipsolver.mipdata_->cutpools[lprows[i].cutpoolindex].lpCutRemoved( + lprows[i].index, mipsolver.mipdata_->parallelLockActive()); } } else if (std::abs(lpsolver.getSolution().row_dual[i]) > lpsolver.getOptions().dual_feasibility_tolerance) { @@ -662,9 +716,22 @@ void HighsLpRelaxation::resetAges() { } } +void HighsLpRelaxation::notifyCutPoolsLpCopied(HighsInt n) { + HighsInt nlprows = numRows(); + HighsInt modelrows = mipsolver.numRow(); + for (HighsInt i = modelrows; i != nlprows; ++i) { + if (lprows[i].origin == LpRow::Origin::kCutPool) { + assert(lprows[i].cutpoolindex < + static_cast(mipsolver.mipdata_->cutpools.size())); + mipsolver.mipdata_->cutpools[lprows[i].cutpoolindex].increaseNumLps( + lprows[i].index, n); + } + } +} + void HighsLpRelaxation::flushDomain(HighsDomain& domain, bool continuous) { if (!domain.getChangedCols().empty()) { - if (&domain == &mipsolver.mipdata_->domain) continuous = true; + if (&domain == &mipsolver.mipdata_->getDomain()) continuous = true; currentbasisstored = false; if (!continuous) domain.removeContinuousChangedCols(); HighsInt numChgCols = domain.getChangedCols().size(); @@ -871,7 +938,7 @@ bool HighsLpRelaxation::computeDualProof(const HighsDomain& globaldomain, mipsolver.mipdata_->debugSolution.checkCut(inds.data(), vals.data(), inds.size(), rhs); - if (extractCliques) + if (extractCliques && !mipsolver.mipdata_->parallelLockActive()) mipsolver.mipdata_->cliquetable.extractCliquesFromCut( mipsolver, inds.data(), vals.data(), inds.size(), rhs); @@ -936,7 +1003,10 @@ void HighsLpRelaxation::storeDualInfProof() { for (HighsInt j = 0; j < len; ++j) row_ap.add(inds[j], weight * vals[j]); } - const HighsDomain& globaldomain = mipsolver.mipdata_->domain; + const HighsDomain& globaldomain = + (worker_ && mipsolver.mipdata_->parallelLockActive()) + ? worker_->getGlobalDomain() + : mipsolver.mipdata_->getDomain(); for (HighsInt i : row_ap.getNonzeros()) { double val = row_ap.getValue(i); @@ -981,17 +1051,18 @@ void HighsLpRelaxation::storeDualInfProof() { } dualproofrhs = double(upper); - mipsolver.mipdata_->domain.tightenCoefficients( - dualproofinds.data(), dualproofvals.data(), dualproofinds.size(), - dualproofrhs); + globaldomain.tightenCoefficients(dualproofinds.data(), dualproofvals.data(), + dualproofinds.size(), dualproofrhs); mipsolver.mipdata_->debugSolution.checkCut( dualproofinds.data(), dualproofvals.data(), dualproofinds.size(), dualproofrhs); - mipsolver.mipdata_->cliquetable.extractCliquesFromCut( - mipsolver, dualproofinds.data(), dualproofvals.data(), - dualproofinds.size(), dualproofrhs); + if (!mipsolver.mipdata_->parallelLockActive()) { + mipsolver.mipdata_->cliquetable.extractCliquesFromCut( + mipsolver, dualproofinds.data(), dualproofvals.data(), + dualproofinds.size(), dualproofrhs); + } } void HighsLpRelaxation::storeDualUBProof() { @@ -1000,12 +1071,17 @@ void HighsLpRelaxation::storeDualUBProof() { dualproofinds.clear(); dualproofvals.clear(); - if (lpsolver.getSolution().dual_valid) - hasdualproof = computeDualProof(mipsolver.mipdata_->domain, - mipsolver.mipdata_->upper_limit, - dualproofinds, dualproofvals, dualproofrhs); - else + if (lpsolver.getSolution().dual_valid) { + bool use_worker_info = worker_ && mipsolver.mipdata_->parallelLockActive(); + hasdualproof = + computeDualProof(use_worker_info ? worker_->getGlobalDomain() + : mipsolver.mipdata_->getDomain(), + use_worker_info ? worker_->upper_limit + : mipsolver.mipdata_->upper_limit, + dualproofinds, dualproofvals, dualproofrhs); + } else { hasdualproof = false; + } if (!hasdualproof) dualproofrhs = kHighsInf; } @@ -1155,7 +1231,7 @@ HighsLpRelaxation::Status HighsLpRelaxation::run(bool resolve_on_error) { } // Revert the value of lpsolver.options_.solver lpsolver.setOptionValue("solver", solver); - if (!mipsolver.submip) { + if (!mipsolver.submip && !mipsolver.mipdata_->parallelLockActive()) { const HighsSubSolverCallTime& sub_solver_call_time = lpsolver.getSubSolverCallTime(); mipsolver.analysis_.addSubSolverCallTime(sub_solver_call_time); @@ -1270,9 +1346,15 @@ HighsLpRelaxation::Status HighsLpRelaxation::run(bool resolve_on_error) { HighsLogType::kWarning, "HighsLpRelaxation::run LP is unbounded with no basis, " "but not returning Status::kError\n"); - if (info.primal_solution_status == kSolutionStatusFeasible) - mipsolver.mipdata_->trySolution(lpsolver.getSolution().col_value, - kSolutionSourceUnbounded); + if (info.primal_solution_status == kSolutionStatusFeasible) { + if (!mipsolver.mipdata_->parallelLockActive() || !worker_) { + mipsolver.mipdata_->trySolution(lpsolver.getSolution().col_value, + kSolutionSourceUnbounded); + } else { + worker_->trySolution(lpsolver.getSolution().col_value, + kSolutionSourceUnbounded); + } + } return Status::kUnbounded; case HighsModelStatus::kUnknown: @@ -1368,11 +1450,11 @@ HighsLpRelaxation::Status HighsLpRelaxation::run(bool resolve_on_error) { } const HighsSubSolverCallTime& sub_solver_call_time = ipm.getSubSolverCallTime(); - mipsolver.analysis_.addSubSolverCallTime(sub_solver_call_time); + // mipsolver.analysis_.addSubSolverCallTime(sub_solver_call_time); // Go through sub_solver_call_time to update any MIP clocks const bool valid_basis = false; - mipsolver.analysis_.mipTimerUpdate(sub_solver_call_time, valid_basis, - use_presolve); + // mipsolver.analysis_.mipTimerUpdate(sub_solver_call_time, valid_basis, + // use_presolve); lpsolver.setBasis(ipm.getBasis(), "HighsLpRelaxation::run IPM basis"); return run(false); @@ -1414,6 +1496,10 @@ HighsLpRelaxation::Status HighsLpRelaxation::resolveLp(HighsDomain* domain) { HighsCDouble objsum = 0; bool roundable = true; + const HighsDomain& globaldom = + (worker_ && mipsolver.mipdata_->parallelLockActive()) + ? worker_->getGlobalDomain() + : mipsolver.mipdata_->getDomain(); for (HighsInt i : mipsolver.mipdata_->integral_cols) { // for the fractionality we assume that LP bounds are not violated @@ -1477,8 +1563,8 @@ HighsLpRelaxation::Status HighsLpRelaxation::resolveLp(HighsDomain* domain) { if (lpsolver.getBasis().col_status[i] == HighsBasisStatus::kBasic) continue; - const double glb = mipsolver.mipdata_->domain.col_lower_[i]; - const double gub = mipsolver.mipdata_->domain.col_upper_[i]; + const double glb = globaldom.col_lower_[i]; + const double gub = globaldom.col_upper_[i]; if (std::min(gub - sol.col_value[i], sol.col_value[i] - glb) <= mipsolver.mipdata_->feastol) @@ -1566,8 +1652,13 @@ HighsLpRelaxation::Status HighsLpRelaxation::resolveLp(HighsDomain* domain) { for (HighsInt i = 0; i != mipsolver.numCol(); ++i) objsum += roundsol[i] * mipsolver.colCost(i); - mipsolver.mipdata_->addIncumbent(roundsol, double(objsum), - kSolutionSourceSolveLp); + if (!mipsolver.mipdata_->parallelLockActive() || !worker_) { + mipsolver.mipdata_->addIncumbent( + roundsol, static_cast(objsum), kSolutionSourceSolveLp); + } else { + worker_->addIncumbent(roundsol, static_cast(objsum), + kSolutionSourceSolveLp); + } objsum = 0; } diff --git a/highs/mip/HighsLpRelaxation.h b/highs/mip/HighsLpRelaxation.h index 15a861a52d..2625bcd54d 100644 --- a/highs/mip/HighsLpRelaxation.h +++ b/highs/mip/HighsLpRelaxation.h @@ -12,11 +12,13 @@ #include #include "Highs.h" +#include "mip/HighsConflictPool.h" #include "mip/HighsMipSolver.h" class HighsDomain; struct HighsCutSet; class HighsPseudocost; +class HighsMipWorker; class HighsLpRelaxation { public: @@ -41,6 +43,7 @@ class HighsLpRelaxation { Origin origin; HighsInt index; HighsInt age; + HighsInt cutpoolindex; void get(const HighsMipSolver& mipsolver, HighsInt& len, const HighsInt*& inds, const double*& vals) const; @@ -51,8 +54,10 @@ class HighsLpRelaxation { double getMaxAbsVal(const HighsMipSolver& mipsolver) const; - static LpRow cut(HighsInt index) { return LpRow{kCutPool, index, 0}; } - static LpRow model(HighsInt index) { return LpRow{kModel, index, 0}; } + static LpRow cut(HighsInt index, HighsInt cutpoolindex) { + return LpRow{kCutPool, index, 0, cutpoolindex}; + } + static LpRow model(HighsInt index) { return LpRow{kModel, index, 0, -1}; } }; const HighsMipSolver& mipsolver; @@ -82,6 +87,7 @@ class HighsLpRelaxation { Status status; bool adjustSymBranchingCol; bool solved_first_lp; + HighsMipWorker* worker_; void storeDualInfProof(); @@ -166,10 +172,12 @@ class HighsLpRelaxation { this->adjustSymBranchingCol = adjustSymBranchingCol; } - void resetToGlobalDomain(); + void resetToGlobalDomain(const HighsDomain& globaldom); - void computeBasicDegenerateDuals(double threshold, - HighsDomain* localdom = nullptr); + void computeBasicDegenerateDuals(double threshold, HighsDomain& localdom, + HighsDomain& globaldom, + HighsConflictPool& conflictpol, + bool getdualproof); double getAvgSolveIters() { return avgSolveIters; } @@ -185,9 +193,9 @@ class HighsLpRelaxation { const HighsSolution& getSolution() const { return lpsolver.getSolution(); } - double slackUpper(HighsInt row) const; + double slackUpper(HighsInt row, const HighsDomain& globaldom) const; - double slackLower(HighsInt row) const; + double slackLower(HighsInt row, const HighsDomain& globaldom) const; double rowLower(HighsInt row) const { return lpsolver.getLp().row_lower_[row]; @@ -197,16 +205,16 @@ class HighsLpRelaxation { return lpsolver.getLp().row_upper_[row]; } - double colLower(HighsInt col) const { + double colLower(HighsInt col, const HighsDomain& globaldom) const { return col < lpsolver.getLp().num_col_ ? lpsolver.getLp().col_lower_[col] - : slackLower(col - lpsolver.getLp().num_col_); + : slackLower(col - lpsolver.getLp().num_col_, globaldom); } - double colUpper(HighsInt col) const { + double colUpper(HighsInt col, const HighsDomain& globaldom) const { return col < lpsolver.getLp().num_col_ ? lpsolver.getLp().col_upper_[col] - : slackUpper(col - lpsolver.getLp().num_col_); + : slackUpper(col - lpsolver.getLp().num_col_, globaldom); } bool isColIntegral(HighsInt col) const { @@ -236,6 +244,8 @@ class HighsLpRelaxation { return false; } + void setMipWorker(HighsMipWorker& worker) { worker_ = &worker; }; + double computeBestEstimate(const HighsPseudocost& ps) const; double computeLPDegneracy(const HighsDomain& localdomain) const; @@ -308,8 +318,12 @@ class HighsLpRelaxation { void resetAges(); + void notifyCutPoolsLpCopied(HighsInt n); + void removeObsoleteRows(bool notifyPool = true); + void removeWorkerSpecificRows(); + void removeCuts(HighsInt ndelcuts, std::vector& deletemask); void removeCuts(); diff --git a/highs/mip/HighsMipSolver.cpp b/highs/mip/HighsMipSolver.cpp index 2c7fdd845f..75d3da9547 100644 --- a/highs/mip/HighsMipSolver.cpp +++ b/highs/mip/HighsMipSolver.cpp @@ -15,6 +15,7 @@ #include "mip/HighsImplications.h" #include "mip/HighsLpRelaxation.h" #include "mip/HighsMipSolverData.h" +#include "mip/HighsMipWorker.h" #include "mip/HighsPseudocost.h" #include "mip/HighsSearch.h" #include "mip/HighsSeparation.h" @@ -70,6 +71,27 @@ HighsMipSolver::HighsMipSolver(HighsCallback& callback, HighsMipSolver::~HighsMipSolver() = default; +template +void HighsMipSolver::runTask(F&& f, highs::parallel::TaskGroup& tg, + bool parallel_lock, bool force_serial, + const std::vector& indices) { + if (indices.empty()) return; + setParallelLock(parallel_lock); + const bool spawn_tasks = !force_serial && indices.size() > 1 && + !options_mip_->mip_search_simulate_concurrency; + for (HighsInt i : indices) { + if (spawn_tasks) { + tg.spawn([&f, i] { f(i); }); + } else { + f(i); + } + } + if (spawn_tasks) { + tg.taskWait(); + } + setParallelLock(false); +} + void HighsMipSolver::run() { modelstatus_ = HighsModelStatus::kNotset; @@ -138,6 +160,19 @@ void HighsMipSolver::run() { if (analysis_.analyse_mip_time && !submip) highsLogUser(options_mip_->log_options, HighsLogType::kInfo, "MIP-Timing: %11.2g - completed setup\n", timer_.read()); + + if (mipdata_->getDomain().infeasible()) { + cleanupSolve(); + return; + } + // Initialise master worker. + mipdata_->workers.emplace_back( + *this, &mipdata_->getLp(), &mipdata_->getDomain(), + &mipdata_->getCutPool(), &mipdata_->getConflictPool(), + &mipdata_->getPseudoCost()); + + HighsMipWorker& master_worker = mipdata_->workers[0]; + restart: if (modelstatus_ == HighsModelStatus::kNotset) { // Check limits have not been reached before evaluating root node @@ -181,7 +216,9 @@ void HighsMipSolver::run() { "MIP-Timing: %11.2g - starting evaluate root node\n", timer_.read()); analysis_.mipTimerStart(kMipClockEvaluateRootNode); - mipdata_->evaluateRootNode(); + + mipdata_->evaluateRootNode(master_worker); + analysis_.mipTimerStop(kMipClockEvaluateRootNode); if (this->terminate()) { modelstatus_ = this->terminationStatus(); @@ -200,11 +237,11 @@ void HighsMipSolver::run() { // age 5 times to remove stored but never violated cuts after root // separation analysis_.mipTimerStart(kMipClockPerformAging0); - mipdata_->cutpool.performAging(); - mipdata_->cutpool.performAging(); - mipdata_->cutpool.performAging(); - mipdata_->cutpool.performAging(); - mipdata_->cutpool.performAging(); + mipdata_->getCutPool().performAging(); + mipdata_->getCutPool().performAging(); + mipdata_->getCutPool().performAging(); + mipdata_->getCutPool().performAging(); + mipdata_->getCutPool().performAging(); analysis_.mipTimerStop(kMipClockPerformAging0); } if (mipdata_->nodequeue.empty() || mipdata_->checkLimits()) { @@ -212,18 +249,233 @@ void HighsMipSolver::run() { return; } - std::shared_ptr basis; - HighsSearch search{*this, mipdata_->pseudocost}; - mipdata_->debugSolution.registerDomain(search.getLocalDomain()); - HighsSeparation sepa(*this); + mipdata_->updateLowerBound(mipdata_->nodequeue.getBestLowerBound()); + mipdata_->printDisplayLine(); - search.setLpRelaxation(&mipdata_->lp); - sepa.setLpRelaxation(&mipdata_->lp); + // Calculate maximum number of workers + const HighsInt mip_search_concurrency = options_mip_->mip_search_concurrency; + const HighsInt max_num_workers = + highs::parallel::num_threads() == 1 || mip_search_concurrency <= 0 || + submip + ? 1 + : mip_search_concurrency * highs::parallel::num_threads(); + HighsInt num_workers = 1; + highs::parallel::TaskGroup tg; + + auto destroyOldWorkers = [&]() { + if (mipdata_->workers.size() <= 1) return; + while (mipdata_->domains.size() > 1) { + mipdata_->domains.pop_back(); + } + while (mipdata_->lps.size() > 1) { + mipdata_->lps.pop_back(); + } + while (mipdata_->pseudocosts.size() > 1) { + mipdata_->pseudocosts.pop_back(); + } + while (mipdata_->workers.size() > 1) { + mipdata_->workers.pop_back(); + } + while (mipdata_->cutpools.size() > 1) { + mipdata_->cutpools.pop_back(); + } + while (mipdata_->conflictpools.size() > 1) { + mipdata_->conflictpools.pop_back(); + } + }; + + auto createNewWorkers = [&](HighsInt num_new_workers) { + if (num_new_workers <= 0) return; + // Remove all cuts from non-global pool for copied LP + mipdata_->lps.emplace_back(mipdata_->getLp()); + mipdata_->lps.back().removeWorkerSpecificRows(); + for (HighsInt i = 0; i != num_new_workers; ++i) { + if (i != 0) { + mipdata_->lps.emplace_back(mipdata_->lps.back()); + } + mipdata_->domains.emplace_back(mipdata_->getDomain()); + mipdata_->cutpools.emplace_back( + numCol(), options_mip_->mip_pool_age_limit, + options_mip_->mip_pool_soft_limit, mipdata_->cutpools.size()); + mipdata_->conflictpools.emplace_back(5 * options_mip_->mip_pool_age_limit, + options_mip_->mip_pool_soft_limit); + mipdata_->domains.back().addCutpool(mipdata_->cutpools.back()); + assert(mipdata_->domains.back().getDomainChangeStack().empty()); + mipdata_->domains.back().addConflictPool(mipdata_->conflictpools.back()); + mipdata_->pseudocosts.emplace_back(*this); + mipdata_->workers.emplace_back( + *this, &mipdata_->lps.back(), &mipdata_->domains.back(), + &mipdata_->cutpools.back(), &mipdata_->conflictpools.back(), + &mipdata_->pseudocosts.back()); + mipdata_->lps.back().setMipWorker(mipdata_->workers.back()); + mipdata_->getLp().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()); + } + }; + + // Use case: Change pointers in master worker to local copies of global info + auto constructAdditionalWorkerData = [&](HighsMipWorker& worker) { + assert(mipdata_->cutpools.size() == 1 && + mipdata_->conflictpools.size() == 1); + assert(&worker == &mipdata_->workers[0]); + mipdata_->cutpools.emplace_back(numCol(), options_mip_->mip_pool_age_limit, + options_mip_->mip_pool_soft_limit, 1); + worker.cutpool_ = &mipdata_->cutpools.back(); + mipdata_->conflictpools.emplace_back(5 * options_mip_->mip_pool_age_limit, + options_mip_->mip_pool_soft_limit); + worker.conflictpool_ = &mipdata_->conflictpools.back(); + mipdata_->domains.emplace_back(mipdata_->getDomain()); + worker.globaldom_ = &mipdata_->domains.back(); + worker.globaldom_->addCutpool(*worker.cutpool_); + assert(worker.globaldom_->getDomainChangeStack().empty()); + worker.globaldom_->addConflictPool(*worker.conflictpool_); + mipdata_->pseudocosts.emplace_back(*this); + worker.pseudocost_ = &mipdata_->pseudocosts.back(); + worker.lp_->setMipWorker(worker); + worker.resetSearch(); + worker.resetSepa(); + worker.nodequeue.clear(); + worker.nodequeue.setNumCol(numCol()); + }; + + auto syncSolutions = [&]() -> void { + // Note: Upper bound / limit of workers updated via addIncumbent + for (HighsMipWorker& worker : mipdata_->workers) { + for (auto& sol : worker.solutions_) { + mipdata_->addIncumbent(std::get<0>(sol), std::get<1>(sol), + std::get<2>(sol)); + } + worker.solutions_.clear(); + } + }; - mipdata_->updateLowerBound(mipdata_->nodequeue.getBestLowerBound()); + auto syncPools = [&](std::vector& indices) -> void { + if (!mipdata_->hasMultipleWorkers() || mipdata_->parallelLockActive()) + return; + for (const HighsInt i : indices) { + mipdata_->workers[i].conflictpool_->syncConflictPool( + mipdata_->getConflictPool()); + mipdata_->workers[i].cutpool_->syncCutPool(*this, mipdata_->getCutPool()); + } + mipdata_->getCutPool().performAging(); + mipdata_->getConflictPool().performAging(); + }; + + auto syncGlobalDomain = [&](std::vector& indices) -> void { + if (!mipdata_->hasMultipleWorkers()) return; + for (const HighsInt i : indices) { + HighsMipWorker& worker = mipdata_->workers[i]; + const auto& domchgstack = worker.getGlobalDomain().getDomainChangeStack(); + for (const HighsDomainChange& domchg : domchgstack) { + if ((domchg.boundtype == HighsBoundType::kLower && + domchg.boundval > + mipdata_->getDomain().col_lower_[domchg.column]) || + (domchg.boundtype == HighsBoundType::kUpper && + domchg.boundval < + mipdata_->getDomain().col_upper_[domchg.column])) { + mipdata_->getDomain().changeBound(domchg, + HighsDomain::Reason::unspecified()); + } + } + } + }; + + auto doResetWorkerDomain = [&](HighsInt i) { + HighsMipWorker& worker = mipdata_->workers[i]; + for (const HighsDomainChange& domchg : + mipdata_->getDomain().getDomainChangeStack()) { + worker.getGlobalDomain().changeBound(domchg, + HighsDomain::Reason::unspecified()); + } +#ifndef NDEBUG + for (HighsInt col = 0; col < numCol(); ++col) { + assert(mipdata_->getDomain().col_lower_[col] == + worker.globaldom_->col_lower_[col]); + assert(mipdata_->getDomain().col_upper_[col] == + worker.globaldom_->col_upper_[col]); + } +#endif + worker.getGlobalDomain().setDomainChangeStack( + std::vector()); + // Warning: Resetting local domain cannot be done in parallel (changes + // propagationDomains of main pool) + worker.search_ptr_->resetLocalDomain(); + worker.getGlobalDomain().clearChangedCols(); + }; + + auto resetGlobalDomain = [&](bool force, bool resetWorkers) -> void { + // if global propagation found bound changes, we update the domain + if (!mipdata_->getDomain().getChangedCols().empty() || force) { + analysis_.mipTimerStart(kMipClockUpdateLocalDomain); + highsLogDev(options_mip_->log_options, HighsLogType::kInfo, + "added %" HIGHSINT_FORMAT " global bound changes\n", + (HighsInt)mipdata_->getDomain().getChangedCols().size()); + mipdata_->cliquetable.cleanupFixed(mipdata_->getDomain()); + if (mipdata_->hasMultipleWorkers() && resetWorkers) { + // Sync worker domains here. cleanupFixed might have found extra changes + std::vector indices(num_workers); + std::iota(indices.begin(), indices.end(), 0); + runTask(doResetWorkerDomain, tg, false, true, indices); + } + for (const HighsInt col : mipdata_->getDomain().getChangedCols()) + mipdata_->implications.cleanupVarbounds(col); - mipdata_->printDisplayLine(); - search.installNode(mipdata_->nodequeue.popBestBoundNode()); + mipdata_->getDomain().setDomainChangeStack( + std::vector()); + if (!mipdata_->hasMultipleWorkers()) + master_worker.search_ptr_->resetLocalDomain(); + mipdata_->getDomain().clearChangedCols(); + mipdata_->removeFixedIndices(); + analysis_.mipTimerStop(kMipClockUpdateLocalDomain); + } + }; + + auto syncGlobalPseudoCost = [&]() -> void { + if (!mipdata_->hasMultipleWorkers()) return; + std::vector nsamplesup = + mipdata_->getPseudoCost().getNSamplesUp(); + std::vector nsamplesdown = + mipdata_->getPseudoCost().getNSamplesDown(); + std::vector ninferencesup = + mipdata_->getPseudoCost().getNInferencesUp(); + std::vector ninferencesdown = + mipdata_->getPseudoCost().getNInferencesDown(); + std::vector ncutoffsup = + mipdata_->getPseudoCost().getNCutoffsUp(); + std::vector ncutoffsdown = + mipdata_->getPseudoCost().getNCutoffsDown(); + for (HighsMipWorker& worker : mipdata_->workers) { + mipdata_->getPseudoCost().flushPseudoCost( + worker.getPseudocost(), nsamplesup, nsamplesdown, ninferencesup, + ninferencesdown, ncutoffsup, ncutoffsdown); + } + }; + + auto resetWorkerPseudoCosts = [&](std::vector& indices) { + if (!mipdata_->hasMultipleWorkers()) return; + auto doResetWorkerPseudoCost = [&](HighsInt i) -> void { + mipdata_->getPseudoCost().syncPseudoCost( + mipdata_->workers[i].getPseudocost()); + }; + runTask(doResetWorkerPseudoCost, tg, false, false, indices); + }; + + 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; + destroyOldWorkers(); + mipdata_->debugSolution.registerDomain( + master_worker.search_ptr_->getLocalDomain()); + + analysis_.mipTimerStart(kMipClockSearch); int64_t numStallNodes = 0; int64_t lastLbLeave = 0; int64_t numQueueLeaves = 0; @@ -233,184 +485,284 @@ void HighsMipSolver::run() { double treeweightLastCheck = 0.0; double upperLimLastCheck = mipdata_->upper_limit; double lowerBoundLastCheck = mipdata_->lower_bound; - analysis_.mipTimerStart(kMipClockSearch); - while (search.hasNode()) { - // Possibly query existence of an external solution - if (!submip) - mipdata_->queryExternalSolution( - solution_objective_, kExternalMipSolutionQueryOriginBeforeDive); - analysis_.mipTimerStart(kMipClockPerformAging1); - mipdata_->conflictPool.performAging(); - analysis_.mipTimerStop(kMipClockPerformAging1); - // set iteration limit for each lp solve during the dive to 10 times the - // average nodes + auto nodesInstalled = [&]() -> bool { + for (HighsMipWorker& worker : mipdata_->workers) { + if (worker.search_ptr_->hasNode()) return true; + } + return false; + }; + + auto getSearchIndicesWithNoNodes = [&](std::vector& indices) { + indices.clear(); + const HighsInt heuristic_allowance_mod = 4; + HighsInt heuristic_random_mod_index = + mipdata_->heuristics.getHeuristicRandom(mipdata_->workers.size()) % + heuristic_allowance_mod; + for (HighsInt i = 0; + i != num_workers && i != mipdata_->nodequeue.numActiveNodes(); i++) { + if (!mipdata_->workers[i].search_ptr_->hasNode()) { + indices.emplace_back(i); + mipdata_->workers[i].setAllowHeuristics(i % heuristic_allowance_mod == + heuristic_random_mod_index); + } + } + }; - HighsInt iterlimit = 10 * std::max(mipdata_->lp.getAvgSolveIters(), - mipdata_->avgrootlpiters); - iterlimit = std::max({HighsInt{10000}, iterlimit, - HighsInt((3 * mipdata_->firstrootlpiters) / 2)}); + auto installNodes = [&](std::vector& indices, + bool& limit_reached) -> void { + for (const HighsInt i : indices) { + if (numQueueLeaves - lastLbLeave >= 10) { + 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)); + } - mipdata_->lp.setIterationLimit(iterlimit); + ++numQueueLeaves; - // perform the dive and put the open nodes to the queue - size_t plungestart = mipdata_->num_nodes; - bool limit_reached = false; - bool considerHeuristics = true; - analysis_.mipTimerStart(kMipClockDive); - while (true) { - // Possibly apply primal heuristics - if (considerHeuristics && mipdata_->moreHeuristicsAllowed()) { - analysis_.mipTimerStart(kMipClockDiveEvaluateNode); - const HighsSearch::NodeResult evaluate_node_result = - search.evaluateNode(); - analysis_.mipTimerStop(kMipClockDiveEvaluateNode); - - if (evaluate_node_result == HighsSearch::NodeResult::kSubOptimal) break; - - if (search.currentNodePruned()) { - ++mipdata_->num_leaves; - search.flushStatistics(); - } else { - analysis_.mipTimerStart(kMipClockDivePrimalHeuristics); - if (mipdata_->incumbent.empty()) { - analysis_.mipTimerStart(kMipClockDiveRandomizedRounding); - mipdata_->heuristics.randomizedRounding( - mipdata_->lp.getLpSolver().getSolution().col_value); - analysis_.mipTimerStop(kMipClockDiveRandomizedRounding); - } - - if (mipdata_->incumbent.empty()) { - if (options_mip_->mip_heuristic_run_rens) { - analysis_.mipTimerStart(kMipClockDiveRens); - mipdata_->heuristics.RENS( - mipdata_->lp.getLpSolver().getSolution().col_value); - analysis_.mipTimerStop(kMipClockDiveRens); - } - } else { - if (options_mip_->mip_heuristic_run_rins) { - analysis_.mipTimerStart(kMipClockDiveRins); - mipdata_->heuristics.RINS( - mipdata_->lp.getLpSolver().getSolution().col_value); - analysis_.mipTimerStop(kMipClockDiveRins); - } - } - - mipdata_->heuristics.flushStatistics(); - analysis_.mipTimerStop(kMipClockDivePrimalHeuristics); - if (mipdata_->terminatorTerminated()) { - cleanupSolve(); - return; - } + 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; + } + }; - considerHeuristics = false; + auto evaluateNode = [&](HighsInt i) -> bool { + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockEvaluateNode1); + if (mipdata_->workers[i].search_ptr_->evaluateNode() == + HighsSearch::NodeResult::kSubOptimal) { + HighsNodeQueue& globalqueue = mipdata_->parallelLockActive() + ? mipdata_->workers[i].nodequeue + : mipdata_->nodequeue; + mipdata_->workers[i].search_ptr_->currentNodeToQueue(globalqueue); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockEvaluateNode1); + return true; + } + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockEvaluateNode1); + return false; + }; - if (mipdata_->domain.infeasible()) break; + auto pruneNode = [&](HighsInt i) -> bool { + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockNodePrunedLoop); + bool pruned = false; + if (mipdata_->workers[i].search_ptr_->currentNodePruned()) { + mipdata_->workers[i].search_ptr_->backtrack(); + mipdata_->workers[i].getGlobalDomain().propagate(); + pruned = true; + ++mipdata_->workers[i].search_ptr_->getLocalNodes(); + ++mipdata_->workers[i].search_ptr_->getLocalLeaves(); + } + return mipdata_->workers[i].getGlobalDomain().infeasible() || pruned; + }; - if (!search.currentNodePruned()) { - double this_dive_time = -analysis_.mipTimerRead(kMipClockTheDive); - analysis_.mipTimerStart(kMipClockTheDive); - const HighsSearch::NodeResult search_dive_result = search.dive(); - analysis_.mipTimerStop(kMipClockTheDive); - if (analysis_.analyse_mip_time) { - this_dive_time += analysis_.mipTimerRead(kMipClockNodeSearch); - analysis_.dive_time.push_back(this_dive_time); - } - if (search_dive_result == HighsSearch::NodeResult::kSubOptimal) break; + auto separateAndStoreBasis = [&](HighsInt i) -> bool { + HighsMipWorker& worker = mipdata_->workers[i]; + if (options_mip_->mip_allow_cut_separation_at_nodes) { + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockNodeSearchSeparation); + worker.sepa_ptr_->separate(worker.search_ptr_->getLocalDomain()); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockNodeSearchSeparation); + } else { + worker.cutpool_->performAging(); + } - ++mipdata_->num_leaves; + if (worker.getGlobalDomain().infeasible()) { + worker.search_ptr_->cutoffNode(); + HighsNodeQueue& globalqueue = mipdata_->parallelLockActive() + ? worker.nodequeue + : mipdata_->nodequeue; + worker.search_ptr_->openNodesToQueue(globalqueue); + return true; + } - search.flushStatistics(); - } + if (worker.lp_->getStatus() != HighsLpRelaxation::Status::kError && + worker.lp_->getStatus() != HighsLpRelaxation::Status::kNotSet) + worker.lp_->storeBasis(); - if (mipdata_->checkLimits()) { - limit_reached = true; - break; - } + std::shared_ptr basis = worker.lp_->getStoredBasis(); + if (!basis || !isBasisConsistent(worker.lp_->getLp(), *basis)) { + HighsBasis b = mipdata_->firstrootbasis; + b.row_status.resize(worker.lp_->numRows(), HighsBasisStatus::kBasic); + basis = std::make_shared(std::move(b)); + worker.lp_->setStoredBasis(basis); + } - HighsInt numPlungeNodes = mipdata_->num_nodes - plungestart; - if (numPlungeNodes >= 100) break; + return false; + }; + auto backtrackPlunge = [&](HighsInt i) { + if (!mipdata_->parallelLockActive()) analysis_.mipTimerStart(kMipClockBacktrackPlunge); - const bool backtrack_plunge = search.backtrackPlunge(mipdata_->nodequeue); + const bool backtrack_plunge = + mipdata_->workers[i].search_ptr_->backtrackPlunge( + mipdata_->parallelLockActive() ? mipdata_->workers[i].nodequeue + : mipdata_->nodequeue); + if (!mipdata_->parallelLockActive()) analysis_.mipTimerStop(kMipClockBacktrackPlunge); - if (!backtrack_plunge) break; - - assert(search.hasNode()); - - if (mipdata_->conflictPool.getNumConflicts() > - options_mip_->mip_pool_soft_limit) { - analysis_.mipTimerStart(kMipClockPerformAging2); - mipdata_->conflictPool.performAging(); - analysis_.mipTimerStop(kMipClockPerformAging2); - } - - search.flushStatistics(); - mipdata_->printDisplayLine(); - // printf("continue plunging due to good estimate\n"); - } // while (true) - analysis_.mipTimerStop(kMipClockDive); - analysis_.mipTimerStart(kMipClockOpenNodesToQueue0); - search.openNodesToQueue(mipdata_->nodequeue); - analysis_.mipTimerStop(kMipClockOpenNodesToQueue0); + if (!backtrack_plunge) return true; - search.flushStatistics(); + assert(mipdata_->workers[i].search_ptr_->hasNode()); - if (limit_reached) { - mipdata_->updateLowerBound(std::min( - mipdata_->upper_bound, mipdata_->nodequeue.getBestLowerBound())); - mipdata_->printDisplayLine(); - break; + if (mipdata_->workers[i].conflictpool_->getNumConflicts() > + options_mip_->mip_pool_soft_limit) { + mipdata_->workers[i].conflictpool_->performAging(); + } + return false; + }; + + auto runHeuristics = [&](HighsInt i) -> bool { + HighsMipWorker& worker = mipdata_->workers[i]; + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockDiveEvaluateNode); + const HighsSearch::NodeResult evaluate_node_result = + worker.search_ptr_->evaluateNode(); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockDiveEvaluateNode); + + if (evaluate_node_result == HighsSearch::NodeResult::kSubOptimal) { + return true; } - // the search datastructure should have no installed node now - assert(!search.hasNode()); - - // propagate the global domain - analysis_.mipTimerStart(kMipClockDomainPropgate); - mipdata_->domain.propagate(); - analysis_.mipTimerStop(kMipClockDomainPropgate); - - analysis_.mipTimerStart(kMipClockPruneInfeasibleNodes); - mipdata_->pruned_treeweight += mipdata_->nodequeue.pruneInfeasibleNodes( - mipdata_->domain, mipdata_->feastol); - analysis_.mipTimerStop(kMipClockPruneInfeasibleNodes); - - // if global propagation detected infeasibility, stop here - if (mipdata_->domain.infeasible()) { - mipdata_->nodequeue.clear(); - mipdata_->pruned_treeweight = 1.0; - mipdata_->updateLowerBound(std::min(kHighsInf, mipdata_->upper_bound)); - mipdata_->printDisplayLine(); - break; + if (worker.search_ptr_->currentNodePruned()) { + ++worker.search_ptr_->getLocalLeaves(); + return false; } - mipdata_->updateLowerBound(std::min( - mipdata_->upper_bound, mipdata_->nodequeue.getBestLowerBound())); - mipdata_->printDisplayLine(); - if (mipdata_->nodequeue.empty()) break; + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockDivePrimalHeuristics); + if (mipdata_->incumbent.empty()) { + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockDiveRandomizedRounding); + mipdata_->heuristics.randomizedRounding( + worker, worker.lp_->getLpSolver().getSolution().col_value); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockDiveRandomizedRounding); + } + if (mipdata_->incumbent.empty()) { + if (options_mip_->mip_heuristic_run_rens) { + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockDiveRens); + mipdata_->heuristics.RENS( + worker, worker.lp_->getLpSolver().getSolution().col_value); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockDiveRens); + } + } else { + if (options_mip_->mip_heuristic_run_rins) { + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockDiveRins); + mipdata_->heuristics.RINS( + worker, worker.lp_->getLpSolver().getSolution().col_value); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockDiveRins); + } + } - // if global propagation found bound changes, we update the local domain - if (!mipdata_->domain.getChangedCols().empty()) { - analysis_.mipTimerStart(kMipClockUpdateLocalDomain); - highsLogDev(options_mip_->log_options, HighsLogType::kInfo, - "added %" HIGHSINT_FORMAT " global bound changes\n", - (HighsInt)mipdata_->domain.getChangedCols().size()); - mipdata_->cliquetable.cleanupFixed(mipdata_->domain); - for (HighsInt col : mipdata_->domain.getChangedCols()) - mipdata_->implications.cleanupVarbounds(col); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockDivePrimalHeuristics); - mipdata_->domain.setDomainChangeStack(std::vector()); - search.resetLocalDomain(); + return worker.getGlobalDomain().infeasible(); + }; - mipdata_->domain.clearChangedCols(); - mipdata_->removeFixedIndices(); - analysis_.mipTimerStop(kMipClockUpdateLocalDomain); + auto dive = [&](HighsInt i, bool ramp_up) -> bool { + HighsMipWorker& worker = mipdata_->workers[i]; + if (!worker.search_ptr_->currentNodePruned()) { + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStart(kMipClockTheDive); + const HighsSearch::NodeResult search_dive_result = + worker.search_ptr_->dive(ramp_up); + if (!mipdata_->parallelLockActive()) + analysis_.mipTimerStop(kMipClockTheDive); + if (search_dive_result == HighsSearch::NodeResult::kSubOptimal) { + return true; + } + worker.search_ptr_->getLocalLeaves()++; } + return ramp_up; + }; + + auto processNodes = [&](std::vector& indices, + const bool skip_separation, const bool ramp_up, + const HighsInt plungeLimit, double avgiter) { + auto processNode = [&](HighsInt i) { + HighsMipWorker& worker = mipdata_->workers[i]; + int64_t nodes_explored = 0; + if (!skip_separation) { + evaluateNode(i); + if (pruneNode(i)) return; + if (!mipdata_->parallelLockActive()) { + if (mipdata_->checkLimits()) return; + mipdata_->printDisplayLine(); + } + if (separateAndStoreBasis(i)) return; + } + worker.conflictpool_->performAging(); + HighsInt iterlimit = 10 * std::max(avgiter, mipdata_->avgrootlpiters); + iterlimit = std::max({HighsInt{10000}, iterlimit, + HighsInt((3 * mipdata_->firstrootlpiters) / 2)}); + worker.getLpRelaxation().setIterationLimit(iterlimit); + bool considerHeuristics = true; + while (true) { + if (considerHeuristics && !ramp_up && worker.getAllowHeuristics() && + mipdata_->moreHeuristicsAllowed()) { + if (runHeuristics(i)) break; + } + considerHeuristics = false; + if (worker.getGlobalDomain().infeasible()) break; + if (dive(i, ramp_up)) break; + if (!mipdata_->parallelLockActive() && + worker.search_ptr_->checkLimits( + worker.search_ptr_->getLocalNodes())) { + break; + } + if (worker.search_ptr_->getLocalNodes() + nodes_explored >= plungeLimit) + break; + if (!mipdata_->parallelLockActive()) { + nodes_explored += worker.search_ptr_->getLocalNodes(); + } + if (backtrackPlunge(i)) break; + if (!mipdata_->parallelLockActive()) { + worker.search_ptr_->flushStatistics(); + mipdata_->printDisplayLine(); + } + } + }; + runTask(processNode, tg, true, false, indices); + }; + + auto syncSepaStats = [&](HighsMipWorker& worker) { + mipdata_->cliquetable.getNumNeighbourhoodQueries() += + worker.sepa_stats.numNeighbourhoodQueries; + worker.sepa_stats.numNeighbourhoodQueries = 0; + mipdata_->sepa_lp_iterations += worker.sepa_stats.sepa_lp_iterations; + mipdata_->total_lp_iterations += worker.sepa_stats.sepa_lp_iterations; + worker.sepa_stats.sepa_lp_iterations = 0; + }; + + auto checkRestart = [&]() -> bool { if (!submip && mipdata_->num_nodes >= nextCheck) { auto nTreeRestarts = mipdata_->numRestarts - mipdata_->numRestartsRoot; double currNodeEstim = @@ -485,175 +837,142 @@ void HighsMipSolver::run() { "\nRestarting search from the root node\n"); mipdata_->performRestart(); analysis_.mipTimerStop(kMipClockSearch); - goto restart; + return true; } - } // if (!submip && mipdata_->num_nodes >= nextCheck)) + } + return false; + }; - // remove the iteration limit when installing a new node - // mipdata_->lp.setIterationLimit(); + // Main solve loop + std::vector search_indices(1, 0); + bool root_node = true; // Don't separate the root node again + while (!mipdata_->nodequeue.empty()) { + // Possibly query existence of an external solution + if (!submip) + mipdata_->queryExternalSolution( + solution_objective_, kExternalMipSolutionQueryOriginBeforeDive); - // loop to install the next node for the search - double this_node_search_time = -analysis_.mipTimerRead(kMipClockNodeSearch); - analysis_.mipTimerStart(kMipClockNodeSearch); + // Update global pseudo-cost with worker information + syncGlobalPseudoCost(); - while (!mipdata_->nodequeue.empty()) { - // printf("popping node from nodequeue (length = %" HIGHSINT_FORMAT ")\n", - // (HighsInt)nodequeue.size()); - assert(!search.hasNode()); + // Get new candidate worker search indices + getSearchIndicesWithNoNodes(search_indices); - if (numQueueLeaves - lastLbLeave >= 10) { - search.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; - search.installNode(std::move(nextNode)); - } + // Only update worker's pseudo-costs that have been assigned a node + resetWorkerPseudoCosts(search_indices); - ++numQueueLeaves; + // Assign nodes to workers + bool limit_reached = false; + if (root_node) { + master_worker.search_ptr_->installNode( + mipdata_->nodequeue.popBestBoundNode()); + } else { + installNodes(search_indices, limit_reached); + } + if (limit_reached) break; - if (search.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; + // Process nodes (separation / heuristics / dives) + processNodes(search_indices, root_node, num_workers < max_num_workers, 100, + mipdata_->getLp().getAvgSolveIters()); - assert(search.hasNode()); + root_node = false; - // we evaluate the node directly here instead of performing a dive - // because we first want to check if the node is not fathomed due to - // new global information before we perform separation rounds for the node - analysis_.mipTimerStart(kMipClockEvaluateNode1); - const HighsSearch::NodeResult evaluate_node_result = - search.evaluateNode(); - analysis_.mipTimerStop(kMipClockEvaluateNode1); - if (evaluate_node_result == HighsSearch::NodeResult::kSubOptimal) { - analysis_.mipTimerStart(kMipClockCurrentNodeToQueue); - search.currentNodeToQueue(mipdata_->nodequeue); - analysis_.mipTimerStop(kMipClockCurrentNodeToQueue); + // Sync statistics, check infeasibility, and flush nodes from worker queues + bool infeasible = false; + for (HighsInt i : search_indices) { + HighsMipWorker& worker = mipdata_->workers[i]; + if (worker.getGlobalDomain().infeasible()) { + infeasible = true; } + analysis_.mipTimerStart(kMipClockOpenNodesToQueue0); + worker.search_ptr_->openNodesToQueue(mipdata_->nodequeue); + while (worker.nodequeue.numNodes() > 0) { + HighsNodeQueue::OpenNode node = + std::move(worker.nodequeue.popBestNode()); + mipdata_->nodequeue.emplaceNode( + std::move(node.domchgstack), std::move(node.branchings), + node.lower_bound, node.estimate, node.depth); + } + analysis_.mipTimerStop(kMipClockOpenNodesToQueue0); + worker.search_ptr_->flushStatistics(); + syncSepaStats(worker); + mipdata_->heuristics.flushStatistics(*this, worker); + } - // if the node was pruned we remove it from the search and install the - // next node from the queue - analysis_.mipTimerStart(kMipClockNodePrunedLoop); - if (search.currentNodePruned()) { - // analysis_.mipTimerStart(kMipClockSearchBacktrack); - search.backtrack(); - // analysis_.mipTimerStop(kMipClockSearchBacktrack); - ++mipdata_->num_leaves; - ++mipdata_->num_nodes; - search.flushStatistics(); - - mipdata_->domain.propagate(); - mipdata_->pruned_treeweight += mipdata_->nodequeue.pruneInfeasibleNodes( - mipdata_->domain, mipdata_->feastol); - - if (mipdata_->domain.infeasible()) { - mipdata_->nodequeue.clear(); - mipdata_->pruned_treeweight = 1.0; - - mipdata_->updateLowerBound( - std::min(kHighsInf, mipdata_->upper_bound)); - - analysis_.mipTimerStop(kMipClockNodePrunedLoop); - break; - } + if (infeasible) { + mipdata_->nodequeue.clear(); + mipdata_->pruned_treeweight = 1.0; + mipdata_->updateLowerBound(std::min(kHighsInf, mipdata_->upper_bound)); + break; + } - if (mipdata_->checkLimits()) { - limit_reached = true; - break; - } + mipdata_->updateLowerBound(std::min( + mipdata_->upper_bound, mipdata_->nodequeue.getBestLowerBound())); - // analysis_.mipTimerStart(kMipClockStoreBasis); - mipdata_->updateLowerBound(std::min( - mipdata_->upper_bound, mipdata_->nodequeue.getBestLowerBound())); + limit_reached = mipdata_->checkLimits(); + if (limit_reached) { + mipdata_->printDisplayLine(); + break; + } - mipdata_->printDisplayLine(); + assert(!nodesInstalled()); - if (!mipdata_->domain.getChangedCols().empty()) { - highsLogDev(options_mip_->log_options, HighsLogType::kInfo, - "added %" HIGHSINT_FORMAT " global bound changes\n", - (HighsInt)mipdata_->domain.getChangedCols().size()); - mipdata_->cliquetable.cleanupFixed(mipdata_->domain); - for (HighsInt col : mipdata_->domain.getChangedCols()) - mipdata_->implications.cleanupVarbounds(col); + // Sync global information + analysis_.mipTimerStart(kMipClockDomainPropgate); + syncSolutions(); + syncPools(search_indices); + syncGlobalDomain(search_indices); + mipdata_->getDomain().propagate(); + analysis_.mipTimerStop(kMipClockDomainPropgate); - mipdata_->domain.setDomainChangeStack( - std::vector()); - search.resetLocalDomain(); + analysis_.mipTimerStart(kMipClockPruneInfeasibleNodes); + mipdata_->pruned_treeweight += mipdata_->nodequeue.pruneInfeasibleNodes( + mipdata_->getDomain(), mipdata_->feastol); + analysis_.mipTimerStop(kMipClockPruneInfeasibleNodes); - mipdata_->domain.clearChangedCols(); - mipdata_->removeFixedIndices(); - } - // analysis_.mipTimerStop(kMipClockStoreBasis); + if (mipdata_->getDomain().infeasible()) { + mipdata_->nodequeue.clear(); + mipdata_->pruned_treeweight = 1.0; + mipdata_->updateLowerBound(std::min(kHighsInf, mipdata_->upper_bound)); + mipdata_->printDisplayLine(); + break; + } - analysis_.mipTimerStop(kMipClockNodePrunedLoop); - continue; - } - analysis_.mipTimerStop(kMipClockNodePrunedLoop); + mipdata_->updateLowerBound(std::min( + mipdata_->upper_bound, mipdata_->nodequeue.getBestLowerBound())); + mipdata_->printDisplayLine(); - // the node is still not fathomed, so perform separation - if (options_mip_->mip_allow_cut_separation_at_nodes) { - analysis_.mipTimerStart(kMipClockNodeSearchSeparation); - sepa.separate(search.getLocalDomain()); - analysis_.mipTimerStop(kMipClockNodeSearchSeparation); - } else { - // perform aging - mipdata_->cutpool.performAging(); - } + if (mipdata_->nodequeue.empty()) break; - if (mipdata_->domain.infeasible()) { - search.cutoffNode(); - analysis_.mipTimerStart(kMipClockOpenNodesToQueue1); - search.openNodesToQueue(mipdata_->nodequeue); - analysis_.mipTimerStop(kMipClockOpenNodesToQueue1); - mipdata_->nodequeue.clear(); - mipdata_->pruned_treeweight = 1.0; - - analysis_.mipTimerStart(kMipClockStoreBasis); - mipdata_->updateLowerBound(std::min(kHighsInf, mipdata_->upper_bound)); - break; - } + // Reset global domain and sync worker's global domains. + bool spawn_more_workers = num_workers < max_num_workers && + mipdata_->nodequeue.numNodes() > num_workers; + resetGlobalDomain(spawn_more_workers, mipdata_->hasMultipleWorkers()); - // after separation we store the new basis and proceed with the outer loop - // to perform a dive from this node - if (mipdata_->lp.getStatus() != HighsLpRelaxation::Status::kError && - mipdata_->lp.getStatus() != HighsLpRelaxation::Status::kNotSet) - mipdata_->lp.storeBasis(); - - basis = mipdata_->lp.getStoredBasis(); - if (!basis || !isBasisConsistent(mipdata_->lp.getLp(), *basis)) { - HighsBasis b = mipdata_->firstrootbasis; - b.row_status.resize(mipdata_->lp.numRows(), HighsBasisStatus::kBasic); - basis = std::make_shared(std::move(b)); - mipdata_->lp.setStoredBasis(basis); - } + if (checkRestart()) goto restart; - break; - } // while(!mipdata_->nodequeue.empty()) - analysis_.mipTimerStop(kMipClockNodeSearch); - if (analysis_.analyse_mip_time) { - this_node_search_time += analysis_.mipTimerRead(kMipClockNodeSearch); - analysis_.node_search_time.push_back(this_node_search_time); + if (spawn_more_workers) { + HighsInt new_max_num_workers = + std::min(static_cast(mipdata_->nodequeue.numNodes()), + max_num_workers); + mipdata_->getPseudoCost().removeChanged(); + if (num_workers == 1) { + constructAdditionalWorkerData(master_worker); + } + createNewWorkers(new_max_num_workers - num_workers); + num_workers = new_max_num_workers; } - if (limit_reached) break; - } // while(search.hasNode()) + } + syncSolutions(); analysis_.mipTimerStop(kMipClockSearch); cleanupSolve(); } void HighsMipSolver::cleanupSolve() { + for (HighsMipWorker& worker : mipdata_->workers) { + assert(worker.solutions_.empty()); + } if (mipdata_->terminatorActive()) { if (mipdata_->terminatorTerminated()) { // Indicate that this instance has been interrupted @@ -851,9 +1170,9 @@ void HighsMipSolver::callbackGetCutPool() const { HighsCallbackOutput& data_out = callback_->data_out; HighsSparseMatrix cut_matrix; - mipdata_->lp.getCutPool(data_out.cutpool_num_col, data_out.cutpool_num_cut, - data_out.cutpool_lower, data_out.cutpool_upper, - cut_matrix); + mipdata_->getLp().getCutPool(data_out.cutpool_num_col, + data_out.cutpool_num_cut, data_out.cutpool_lower, + data_out.cutpool_upper, cut_matrix); // take ownership data_out.cutpool_start = std::move(cut_matrix.start_); @@ -1000,3 +1319,11 @@ void HighsMipSolver::initialiseTerminator(const HighsMipSolver& mip_solver) { mip_solver.mipdata_->terminatorMyInstance(), mip_solver.terminator_.record); } + +void HighsMipSolver::setParallelLock(bool lock) const { + if (!mipdata_->hasMultipleWorkers()) return; + mipdata_->parallel_lock = lock; + for (HighsConflictPool& conflictpool : mipdata_->conflictpools) { + conflictpool.setAgeLock(lock); + } +} diff --git a/highs/mip/HighsMipSolver.h b/highs/mip/HighsMipSolver.h index a70e873699..fa71518f35 100644 --- a/highs/mip/HighsMipSolver.h +++ b/highs/mip/HighsMipSolver.h @@ -12,6 +12,7 @@ #include "lp_data/HighsCallback.h" #include "lp_data/HighsOptions.h" #include "mip/HighsMipAnalysis.h" +#include "parallel/HighsParallel.h" struct HighsMipSolverData; class HighsCutPool; @@ -118,6 +119,12 @@ class HighsMipSolver { ~HighsMipSolver(); + template + void runTask(F&& f, highs::parallel::TaskGroup& tg, bool parallel_lock, + bool force_serial, + const std::vector& indices = std::vector(1, + 0)); + void setModel(const HighsLp& model) { model_ = &model; solution_objective_ = kHighsInf; @@ -151,6 +158,7 @@ class HighsMipSolver { HighsModelStatus terminationStatus() const { return this->termination_status_; } + void setParallelLock(bool lock) const; }; std::array getGapString(const double gap_, diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index f44c7cfa0d..ff38d72ec2 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -19,6 +19,71 @@ #include "presolve/HPresolve.h" #include "util/HighsIntegers.h" +HighsMipSolverData::HighsMipSolverData(HighsMipSolver& mipsolver) + : mipsolver(mipsolver), + lps(1, HighsLpRelaxation(mipsolver)), + conflictpools( + 1, HighsConflictPool(5 * mipsolver.options_mip_->mip_pool_age_limit, + mipsolver.options_mip_->mip_pool_soft_limit)), + domains(1, HighsDomain(mipsolver)), + pseudocosts(1), + parallel_lock(false), + heuristics(mipsolver), + cliquetable(mipsolver.numCol()), + implications(mipsolver), + objectiveFunction(mipsolver), + presolve_status(HighsPresolveStatus::kNotSet), + cliquesExtracted(false), + rowMatrixSet(false), + analyticCenterComputed(false), + analyticCenterStatus(HighsModelStatus::kNotset), + detectSymmetries(false), + numRestarts(0), + numRestartsRoot(0), + numCliqueEntriesAfterPresolve(0), + numCliqueEntriesAfterFirstPresolve(0), + feastol(0.0), + epsilon(0.0), + heuristic_effort(0.0), + dispfreq(0), + firstlpsolobj(-kHighsInf), + rootlpsolobj(-kHighsInf), + numintegercols(0), + maxTreeSizeLog2(0), + pruned_treeweight(0), + avgrootlpiters(0.0), + disptime(0.0), + last_disptime(0.0), + firstrootlpiters(0), + num_nodes(0), + num_leaves(0), + num_leaves_before_run(0), + num_nodes_before_run(0), + total_repair_lp(0), + total_repair_lp_feasible(0), + total_repair_lp_iterations(0), + total_lp_iterations(0), + heuristic_lp_iterations(0), + sepa_lp_iterations(0), + sb_lp_iterations(0), + total_lp_iterations_before_run(0), + heuristic_lp_iterations_before_run(0), + sepa_lp_iterations_before_run(0), + sb_lp_iterations_before_run(0), + num_disp_lines(0), + numImprovingSols(0), + lower_bound(-kHighsInf), + upper_bound(kHighsInf), + upper_limit(kHighsInf), + optimality_limit(kHighsInf), + debugSolution(mipsolver) { + cutpools.emplace_back(mipsolver.numCol(), + mipsolver.options_mip_->mip_pool_age_limit, + mipsolver.options_mip_->mip_pool_soft_limit, 0); + getDomain().addCutpool(getCutPool()); + getDomain().addConflictPool(getConflictPool()); +} + std::string HighsMipSolverData::solutionSourceToString( const int solution_source, const bool code) const { if (solution_source == kSolutionSourceNone) { @@ -450,26 +515,26 @@ void HighsMipSolverData::finishAnalyticCenterComputation( HighsInt nfixed = 0; HighsInt nintfixed = 0; for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { - double boundRange = mipsolver.mipdata_->domain.col_upper_[i] - - mipsolver.mipdata_->domain.col_lower_[i]; + double boundRange = mipsolver.mipdata_->getDomain().col_upper_[i] - + mipsolver.mipdata_->getDomain().col_lower_[i]; if (boundRange == 0.0) continue; double tolerance = mipsolver.mipdata_->feastol * std::min(boundRange, 1.0); if (analyticCenter[i] <= mipsolver.model_->col_lower_[i] + tolerance) { - mipsolver.mipdata_->domain.changeBound( + mipsolver.mipdata_->getDomain().changeBound( HighsBoundType::kUpper, i, mipsolver.model_->col_lower_[i], HighsDomain::Reason::unspecified()); - if (mipsolver.mipdata_->domain.infeasible()) return; + if (mipsolver.mipdata_->getDomain().infeasible()) return; ++nfixed; if (mipsolver.isColInteger(i)) ++nintfixed; } else if (analyticCenter[i] >= mipsolver.model_->col_upper_[i] - tolerance) { - mipsolver.mipdata_->domain.changeBound( + mipsolver.mipdata_->getDomain().changeBound( HighsBoundType::kLower, i, mipsolver.model_->col_upper_[i], HighsDomain::Reason::unspecified()); - if (mipsolver.mipdata_->domain.infeasible()) return; + if (mipsolver.mipdata_->getDomain().infeasible()) return; ++nfixed; if (mipsolver.isColInteger(i)) ++nintfixed; } @@ -479,8 +544,8 @@ void HighsMipSolverData::finishAnalyticCenterComputation( "Fixing %d columns (%d integers) sitting at bound at " "analytic center\n", int(nfixed), int(nintfixed)); - mipsolver.mipdata_->domain.propagate(); - if (mipsolver.mipdata_->domain.infeasible()) return; + mipsolver.mipdata_->getDomain().propagate(); + if (mipsolver.mipdata_->getDomain().infeasible()) return; } } @@ -544,7 +609,7 @@ void HighsMipSolverData::finishSymmetryDetection( orbitope.determineOrbitopeType(cliquetable); if (symmetries.numPerms != 0) - globalOrbits = symmetries.computeStabilizerOrbits(domain); + globalOrbits = symmetries.computeStabilizerOrbits(getDomain()); } double HighsMipSolverData::limitsToGap(const double use_lower_bound, @@ -675,19 +740,19 @@ bool HighsMipSolverData::moreHeuristicsAllowed() const { void HighsMipSolverData::removeFixedIndices() { integral_cols.erase( std::remove_if(integral_cols.begin(), integral_cols.end(), - [&](HighsInt col) { return domain.isFixed(col); }), + [&](HighsInt col) { return getDomain().isFixed(col); }), integral_cols.end()); integer_cols.erase( std::remove_if(integer_cols.begin(), integer_cols.end(), - [&](HighsInt col) { return domain.isFixed(col); }), + [&](HighsInt col) { return getDomain().isFixed(col); }), integer_cols.end()); implint_cols.erase( std::remove_if(implint_cols.begin(), implint_cols.end(), - [&](HighsInt col) { return domain.isFixed(col); }), + [&](HighsInt col) { return getDomain().isFixed(col); }), implint_cols.end()); continuous_cols.erase( std::remove_if(continuous_cols.begin(), continuous_cols.end(), - [&](HighsInt col) { return domain.isFixed(col); }), + [&](HighsInt col) { return getDomain().isFixed(col); }), continuous_cols.end()); } @@ -774,7 +839,7 @@ void HighsMipSolverData::runSetup() { const HighsLp& model = *mipsolver.model_; // Indicate that the first LP has not been solved - this->lp.setSolvedFirstLp(false); + this->getLp().setSolvedFirstLp(false); last_disptime = -kHighsInf; disptime = 0; @@ -846,7 +911,7 @@ void HighsMipSolverData::runSetup() { addIncumbent(std::vector(), 0, kSolutionSourceEmptyMip); redcostfixing = HighsRedcostFixing(); - pseudocost = HighsPseudocost(mipsolver); + getPseudoCost() = HighsPseudocost(mipsolver); nodequeue.setNumCol(mipsolver.numCol()); nodequeue.setOptimalityLimit(optimality_limit); @@ -918,11 +983,11 @@ void HighsMipSolverData::runSetup() { } // compute row activities and propagate all rows once - objectiveFunction.setupCliquePartition(domain, cliquetable); - domain.setupObjectivePropagation(); - domain.computeRowActivities(); - domain.propagate(); - if (domain.infeasible()) { + objectiveFunction.setupCliquePartition(getDomain(), cliquetable); + getDomain().setupObjectivePropagation(); + getDomain().computeRowActivities(); + getDomain().propagate(); + if (getDomain().infeasible()) { mipsolver.modelstatus_ = HighsModelStatus::kInfeasible; updateLowerBound(kHighsInf); @@ -939,14 +1004,15 @@ void HighsMipSolverData::runSetup() { if (checkLimits()) return; // extract cliques if they have not been extracted before - for (HighsInt col : domain.getChangedCols()) + for (HighsInt col : getDomain().getChangedCols()) implications.cleanupVarbounds(col); - domain.clearChangedCols(); + getDomain().clearChangedCols(); - lp.getLpSolver().setOptionValue("presolve", kHighsOffString); + getLp().getLpSolver().setOptionValue("presolve", kHighsOffString); // lp.getLpSolver().setOptionValue("dual_simplex_cost_perturbation_multiplier", // 0.0); lp.getLpSolver().setOptionValue("parallel", kHighsOnString); - lp.getLpSolver().setOptionValue("simplex_initial_condition_check", false); + getLp().getLpSolver().setOptionValue("simplex_initial_condition_check", + false); checkObjIntegrality(); rootlpsol.clear(); @@ -957,14 +1023,14 @@ void HighsMipSolverData::runSetup() { for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { switch (mipsolver.variableType(i)) { case HighsVarType::kContinuous: - if (domain.isFixed(i)) { + if (getDomain().isFixed(i)) { num_domain_fixed++; continue; } continuous_cols.push_back(i); break; case HighsVarType::kImplicitInteger: - if (domain.isFixed(i)) { + if (getDomain().isFixed(i)) { num_domain_fixed++; continue; } @@ -972,9 +1038,9 @@ void HighsMipSolverData::runSetup() { integral_cols.push_back(i); break; case HighsVarType::kInteger: - if (domain.isFixed(i)) { + if (getDomain().isFixed(i)) { num_domain_fixed++; - if (fractionality(domain.col_lower_[i]) > feastol) { + if (fractionality(getDomain().col_lower_[i]) > feastol) { // integer variable is fixed to a fractional value -> infeasible mipsolver.modelstatus_ = HighsModelStatus::kInfeasible; @@ -1263,7 +1329,7 @@ double HighsMipSolverData::percentageInactiveIntegers() const { void HighsMipSolverData::performRestart() { HighsBasis root_basis; HighsPseudocostInitialization pscostinit( - pseudocost, mipsolver.options_mip_->mip_pscost_minreliable, + getPseudoCost(), mipsolver.options_mip_->mip_pscost_minreliable, postSolveStack); mipsolver.pscostinit = &pscostinit; @@ -1275,13 +1341,13 @@ void HighsMipSolverData::performRestart() { heuristic_lp_iterations_before_run = heuristic_lp_iterations; sepa_lp_iterations_before_run = sepa_lp_iterations; sb_lp_iterations_before_run = sb_lp_iterations; - HighsInt numLpRows = lp.getLp().num_row_; + HighsInt numLpRows = getLp().getLp().num_row_; HighsInt numModelRows = mipsolver.numRow(); HighsInt numCuts = numLpRows - numModelRows; if (numCuts > 0) postSolveStack.appendCutsToModel(numCuts); auto integrality = std::move(presolvedModel.integrality_); double offset = presolvedModel.offset_; - presolvedModel = lp.getLp(); + presolvedModel = getLp().getLp(); presolvedModel.offset_ = offset; presolvedModel.integrality_ = std::move(integrality); #ifdef HIGHS_DEBUGSOL @@ -1377,8 +1443,8 @@ void HighsMipSolverData::performRestart() { // updatePrimalDualIntegral (unless solving a sub-MIP) // // Surely there must be a lower bound change - updateLowerBound(upper_bound); - + updateLowerBound(upper_bound, true, + mipsolver.modelstatus_ != HighsModelStatus::kOptimal); if (mipsolver.solution_objective_ != kHighsInf && mipsolver.modelstatus_ == HighsModelStatus::kInfeasible) mipsolver.modelstatus_ = HighsModelStatus::kOptimal; @@ -1397,6 +1463,17 @@ void HighsMipSolverData::performRestart() { // HighsNodeQueue oldNodeQueue; // std::swap(nodequeue, oldNodeQueue); + // Ensure master worker is pointing to the correct cut and conflict pools + if (mipsolver.options_mip_->mip_search_concurrency > 1) { + mipsolver.mipdata_->workers[0].cutpool_ = &getCutPool(); + mipsolver.mipdata_->workers[0].conflictpool_ = &getConflictPool(); + mipsolver.mipdata_->workers[0].globaldom_ = &getDomain(); + mipsolver.mipdata_->workers[0].pseudocost_ = &getPseudoCost(); + mipsolver.mipdata_->workers[0].upper_bound = upper_bound; + mipsolver.mipdata_->workers[0].upper_limit = upper_limit; + mipsolver.mipdata_->workers[0].optimality_limit = optimality_limit; + } + // remove the pointer into the stack-space of this function if (mipsolver.rootbasis == &root_basis) mipsolver.rootbasis = nullptr; mipsolver.pscostinit = nullptr; @@ -1436,6 +1513,7 @@ bool HighsMipSolverData::addIncumbent(const std::vector& sol, double solobj, const int solution_source, const bool print_display_line, const bool is_user_solution) { + assert(!parallelLockActive()); const bool execute_mip_solution_callback = !is_user_solution && !mipsolver.submip && (mipsolver.callback_->user_callback @@ -1481,6 +1559,9 @@ bool HighsMipSolverData::addIncumbent(const std::vector& sol, double prev_upper_bound = upper_bound; upper_bound = solobj; + for (HighsMipWorker& worker : workers) { + worker.upper_bound = upper_bound; + } bool bound_change = upper_bound != prev_upper_bound; if (!mipsolver.submip && bound_change) @@ -1500,15 +1581,20 @@ bool HighsMipSolverData::addIncumbent(const std::vector& sol, computeNewUpperLimit(solobj, mipsolver.options_mip_->mip_abs_gap, mipsolver.options_mip_->mip_rel_gap); nodequeue.setOptimalityLimit(optimality_limit); + for (HighsMipWorker& worker : workers) { + worker.upper_limit = upper_limit; + worker.optimality_limit = optimality_limit; + } debugSolution.newIncumbentFound(); - domain.propagate(); - if (!domain.infeasible()) redcostfixing.propagateRootRedcost(mipsolver); + getDomain().propagate(); + if (!getDomain().infeasible()) + redcostfixing.propagateRootRedcost(mipsolver); // Two calls to printDisplayLine added for completeness, // ensuring that when the root node has an integer solution, a // logging line is issued - if (domain.infeasible()) { + if (getDomain().infeasible()) { pruned_treeweight = 1.0; nodequeue.clear(); if (print_display_line) @@ -1516,7 +1602,7 @@ bool HighsMipSolverData::addIncumbent(const std::vector& sol, return true; } cliquetable.extractObjCliques(mipsolver); - if (domain.infeasible()) { + if (getDomain().infeasible()) { pruned_treeweight = 1.0; nodequeue.clear(); if (print_display_line) @@ -1697,7 +1783,7 @@ void HighsMipSolverData::printDisplayLine(const int solution_source) { auto print_lp_iters = convertToPrintString(total_lp_iterations); HighsInt dynamic_constraints_in_lp = - lp.numRows() > 0 ? lp.numRows() - lp.getNumModelRows() : 0; + getLp().numRows() > 0 ? getLp().numRows() - getLp().getNumModelRows() : 0; if (upper_bound != kHighsInf) { std::array gap_string = {}; if (gap >= 9999.) @@ -1722,8 +1808,8 @@ void HighsMipSolverData::printDisplayLine(const int solution_source) { // clang-format on solutionSourceToString(solution_source).c_str(), print_nodes.data(), queue_nodes.data(), print_leaves.data(), explored, lb_string.data(), - ub_string.data(), gap_string.data(), cutpool.getNumCuts(), - dynamic_constraints_in_lp, conflictPool.getNumConflicts(), + ub_string.data(), gap_string.data(), getCutPool().getNumCuts(), + dynamic_constraints_in_lp, getConflictPool().getNumConflicts(), print_lp_iters.data(), time_string.c_str()); } else { std::array ub_string; @@ -1743,9 +1829,9 @@ void HighsMipSolverData::printDisplayLine(const int solution_source) { // clang-format on solutionSourceToString(solution_source).c_str(), print_nodes.data(), queue_nodes.data(), print_leaves.data(), explored, lb_string.data(), - ub_string.data(), gap, cutpool.getNumCuts(), dynamic_constraints_in_lp, - conflictPool.getNumConflicts(), print_lp_iters.data(), - time_string.c_str()); + ub_string.data(), gap, getCutPool().getNumCuts(), + dynamic_constraints_in_lp, getConflictPool().getNumConflicts(), + print_lp_iters.data(), time_string.c_str()); } // Check that limitsToBounds yields the same values for the // dual_bound, primal_bound (modulo optimization sense) and @@ -1767,39 +1853,42 @@ void HighsMipSolverData::printDisplayLine(const int solution_source) { } bool HighsMipSolverData::rootSeparationRound( - HighsSeparation& sepa, HighsInt& ncuts, HighsLpRelaxation::Status& status) { - int64_t tmpLpIters = -lp.getNumLpIterations(); - ncuts = sepa.separationRound(domain, status); - tmpLpIters += lp.getNumLpIterations(); - avgrootlpiters = lp.getAvgSolveIters(); + HighsMipWorker& worker, HighsSeparation& sepa, HighsInt& ncuts, + HighsLpRelaxation::Status& status) { + int64_t tmpLpIters = -getLp().getNumLpIterations(); + ncuts = sepa.separationRound(getDomain(), status); + tmpLpIters += getLp().getNumLpIterations(); + avgrootlpiters = getLp().getAvgSolveIters(); total_lp_iterations += tmpLpIters; sepa_lp_iterations += tmpLpIters; - status = evaluateRootLp(); + status = evaluateRootLp(worker); if (status == HighsLpRelaxation::Status::kInfeasible) return true; - const std::vector& solvals = lp.getLpSolver().getSolution().col_value; + const std::vector& solvals = + getLp().getLpSolver().getSolution().col_value; if (mipsolver.submip || incumbent.empty()) { - heuristics.randomizedRounding(solvals); + heuristics.randomizedRounding(worker, solvals); if (mipsolver.options_mip_->mip_heuristic_run_shifting) - heuristics.shifting(solvals); - heuristics.flushStatistics(); - status = evaluateRootLp(); + heuristics.shifting(worker, solvals); + heuristics.flushStatistics(mipsolver, worker); + status = evaluateRootLp(worker); if (status == HighsLpRelaxation::Status::kInfeasible) return true; } return false; } -HighsLpRelaxation::Status HighsMipSolverData::evaluateRootLp() { +HighsLpRelaxation::Status HighsMipSolverData::evaluateRootLp( + HighsMipWorker& worker) { do { - domain.propagate(); + getDomain().propagate(); - if (globalOrbits && !domain.infeasible()) - globalOrbits->orbitalFixing(domain); + if (globalOrbits && !getDomain().infeasible()) + globalOrbits->orbitalFixing(getDomain()); - if (domain.infeasible()) { + if (getDomain().infeasible()) { updateLowerBound(std::min(kHighsInf, upper_bound)); pruned_treeweight = 1.0; num_nodes += 1; @@ -1808,21 +1897,21 @@ HighsLpRelaxation::Status HighsMipSolverData::evaluateRootLp() { } bool lpBoundsChanged = false; - if (!domain.getChangedCols().empty()) { + if (!getDomain().getChangedCols().empty()) { lpBoundsChanged = true; removeFixedIndices(); - lp.flushDomain(domain); + getLp().flushDomain(getDomain()); } bool lpWasSolved = false; HighsLpRelaxation::Status status; if (lpBoundsChanged || - lp.getLpSolver().getModelStatus() == HighsModelStatus::kNotset) { - int64_t lpIters = -lp.getNumLpIterations(); - status = lp.resolveLp(&domain); - lpIters += lp.getNumLpIterations(); + getLp().getLpSolver().getModelStatus() == HighsModelStatus::kNotset) { + int64_t lpIters = -getLp().getNumLpIterations(); + status = getLp().resolveLp(&getDomain()); + lpIters += getLp().getNumLpIterations(); total_lp_iterations += lpIters; - avgrootlpiters = lp.getAvgSolveIters(); + avgrootlpiters = getLp().getAvgSolveIters(); lpWasSolved = true; if (status == HighsLpRelaxation::Status::kUnbounded) { @@ -1838,9 +1927,9 @@ HighsLpRelaxation::Status HighsMipSolverData::evaluateRootLp() { } if (status == HighsLpRelaxation::Status::kOptimal && - lp.getFractionalIntegers().empty() && - addIncumbent(lp.getLpSolver().getSolution().col_value, - lp.getObjective(), kSolutionSourceEvaluateNode)) { + getLp().getFractionalIntegers().empty() && + addIncumbent(getLp().getLpSolver().getSolution().col_value, + getLp().getObjective(), kSolutionSourceEvaluateNode)) { mipsolver.modelstatus_ = HighsModelStatus::kOptimal; updateLowerBound(upper_bound); pruned_treeweight = 1.0; @@ -1851,10 +1940,11 @@ HighsLpRelaxation::Status HighsMipSolverData::evaluateRootLp() { if (status == HighsLpRelaxation::Status::kOptimal && mipsolver.options_mip_->mip_heuristic_run_zi_round) - heuristics.ziRound(lp.getLpSolver().getSolution().col_value); + heuristics.ziRound(worker, + getLp().getLpSolver().getSolution().col_value); } else - status = lp.getStatus(); + status = getLp().getStatus(); if (status == HighsLpRelaxation::Status::kInfeasible) { updateLowerBound(std::min(kHighsInf, upper_bound)); @@ -1864,13 +1954,13 @@ HighsLpRelaxation::Status HighsMipSolverData::evaluateRootLp() { return status; } - if (lp.unscaledDualFeasible(lp.getStatus())) { - updateLowerBound(std::max(lp.getObjective(), lower_bound)); + if (getLp().unscaledDualFeasible(getLp().getStatus())) { + updateLowerBound(std::max(getLp().getObjective(), lower_bound)); if (lpWasSolved) { - redcostfixing.addRootRedcost(mipsolver, - lp.getLpSolver().getSolution().col_dual, - lp.getObjective()); + redcostfixing.addRootRedcost( + mipsolver, getLp().getLpSolver().getSolution().col_dual, + getLp().getObjective()); if (upper_limit != kHighsInf) redcostfixing.propagateRootRedcost(mipsolver); } @@ -1883,7 +1973,7 @@ HighsLpRelaxation::Status HighsMipSolverData::evaluateRootLp() { return HighsLpRelaxation::Status::kInfeasible; } - if (domain.getChangedCols().empty()) return status; + if (getDomain().getChangedCols().empty()) return status; } while (true); } @@ -1906,7 +1996,7 @@ static void clockOff(HighsMipAnalysis& analysis) { if (clock2_running) analysis.mipTimerStop(kMipClockEvaluateRootNode2); } -void HighsMipSolverData::evaluateRootNode() { +void HighsMipSolverData::evaluateRootNode(HighsMipWorker& worker) { const bool compute_analytic_centre = true; if (!compute_analytic_centre) printf("NOT COMPUTING ANALYTIC CENTRE!\n"); HighsInt maxSepaRounds = mipsolver.submip ? 5 : kHighsIInf; @@ -1937,12 +2027,12 @@ void HighsMipSolverData::evaluateRootNode() { // lp.getLpSolver().setOptionValue( // "dual_simplex_cost_perturbation_multiplier", 10.0); - lp.setIterationLimit(); - lp.loadModel(); - domain.clearChangedCols(); - lp.setObjectiveLimit(upper_limit); + getLp().setIterationLimit(); + getLp().loadModel(); + getDomain().clearChangedCols(); + getLp().setObjectiveLimit(upper_limit); - updateLowerBound(std::max(lower_bound, domain.getObjectiveLowerBound())); + updateLowerBound(std::max(lower_bound, getDomain().getObjectiveLowerBound())); printDisplayLine(); @@ -1954,38 +2044,39 @@ void HighsMipSolverData::evaluateRootNode() { // check if only root presolve is allowed if (firstrootbasis.valid) - lp.getLpSolver().setBasis(firstrootbasis, - "HighsMipSolverData::evaluateRootNode"); + getLp().getLpSolver().setBasis(firstrootbasis, + "HighsMipSolverData::evaluateRootNode"); else if (mipsolver.options_mip_->mip_root_presolve_only) - lp.getLpSolver().setOptionValue("presolve", kHighsOffString); + getLp().getLpSolver().setOptionValue("presolve", kHighsOffString); else - lp.getLpSolver().setOptionValue("presolve", kHighsOnString); + getLp().getLpSolver().setOptionValue("presolve", kHighsOnString); if (mipsolver.options_mip_->highs_debug_level) - lp.getLpSolver().setOptionValue("output_flag", - mipsolver.options_mip_->output_flag); + getLp().getLpSolver().setOptionValue("output_flag", + mipsolver.options_mip_->output_flag); // lp.getLpSolver().setOptionValue("log_dev_level", kHighsLogDevLevelInfo); // lp.getLpSolver().setOptionValue("log_file", // mipsolver.options_mip_->log_file); analysis.mipTimerStart(kMipClockEvaluateRootLp); - HighsLpRelaxation::Status status = evaluateRootLp(); + HighsLpRelaxation::Status status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (numRestarts == 0) firstrootlpiters = total_lp_iterations; - lp.getLpSolver().setOptionValue("output_flag", false); - lp.getLpSolver().setOptionValue("presolve", kHighsOffString); - lp.getLpSolver().setOptionValue("parallel", kHighsOffString); + getLp().getLpSolver().setOptionValue("output_flag", false); + getLp().getLpSolver().setOptionValue("presolve", kHighsOffString); + getLp().getLpSolver().setOptionValue("parallel", kHighsOffString); if (status == HighsLpRelaxation::Status::kInfeasible || status == HighsLpRelaxation::Status::kUnbounded) return clockOff(analysis); - firstlpsol = lp.getSolution().col_value; - firstlpsolobj = lp.getObjective(); + firstlpsol = getLp().getSolution().col_value; + firstlpsolobj = getLp().getObjective(); rootlpsolobj = firstlpsolobj; - if (lp.getLpSolver().getBasis().valid && lp.numRows() == mipsolver.numRow()) - firstrootbasis = lp.getLpSolver().getBasis(); + if (getLp().getLpSolver().getBasis().valid && + getLp().numRows() == mipsolver.numRow()) + firstrootbasis = getLp().getLpSolver().getBasis(); else { // the root basis is later expected to be consistent for the model without // cuts so set it to the slack basis if the current basis already includes @@ -1998,11 +2089,11 @@ void HighsMipSolverData::evaluateRootNode() { firstrootbasis.useful = true; } - if (cutpool.getNumCuts() != 0) { + if (getCutPool().getNumCuts() != 0) { assert(numRestarts != 0); HighsCutSet cutset; analysis.mipTimerStart(kMipClockSeparateLpCuts); - cutpool.separateLpCutsAfterRestart(cutset); + getCutPool().separateLpCutsAfterRestart(cutset); analysis.mipTimerStop(kMipClockSeparateLpCuts); #ifdef HIGHS_DEBUGSOL for (HighsInt i = 0; i < cutset.numCuts(); ++i) { @@ -2012,33 +2103,33 @@ void HighsMipSolverData::evaluateRootNode() { cutset.upper_[i]); } #endif - lp.addCuts(cutset); + getLp().addCuts(cutset); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); - lp.removeObsoleteRows(); + getLp().removeObsoleteRows(); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); } - lp.setIterationLimit(std::max(10000, int(10 * avgrootlpiters))); + getLp().setIterationLimit(std::max(10000, int(10 * avgrootlpiters))); // make sure first line after solving root LP is printed last_disptime = -kHighsInf; disptime = 0; if (mipsolver.options_mip_->mip_heuristic_run_zi_round) - heuristics.ziRound(firstlpsol); + heuristics.ziRound(worker, firstlpsol); analysis.mipTimerStart(kMipClockRandomizedRounding); - heuristics.randomizedRounding(firstlpsol); + heuristics.randomizedRounding(worker, firstlpsol); analysis.mipTimerStop(kMipClockRandomizedRounding); if (mipsolver.options_mip_->mip_heuristic_run_shifting) - heuristics.shifting(firstlpsol); + heuristics.shifting(worker, firstlpsol); - heuristics.flushStatistics(); + heuristics.flushStatistics(mipsolver, worker); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); @@ -2083,11 +2174,11 @@ void HighsMipSolverData::evaluateRootNode() { HighsInt stall = 0; double smoothprogress = 0.0; HighsInt nseparounds = 0; - HighsSeparation sepa(mipsolver); - sepa.setLpRelaxation(&lp); + HighsSeparation sepa(worker); + sepa.setLpRelaxation(&getLp()); - while (lp.scaledOptimal(status) && !lp.getFractionalIntegers().empty() && - stall < 3) { + while (getLp().scaledOptimal(status) && + !getLp().getFractionalIntegers().empty() && stall < 3) { printDisplayLine(); if (checkLimits()) { @@ -2114,7 +2205,7 @@ void HighsMipSolverData::evaluateRootNode() { analysis.mipTimerStart(kMipClockRootSeparationRound); const bool root_separation_round_result = - rootSeparationRound(sepa, ncuts, status); + rootSeparationRound(worker, sepa, ncuts, status); analysis.mipTimerStop(kMipClockRootSeparationRound); if (root_separation_round_result) { analysis.mipTimerStop(kMipClockRootSeparation); @@ -2133,17 +2224,17 @@ void HighsMipSolverData::evaluateRootNode() { kMipClockRootSeparationFinishAnalyticCentreComputation); analysis.mipTimerStart(kMipClockRootSeparationCentralRounding); - heuristics.centralRounding(); + heuristics.centralRounding(worker); analysis.mipTimerStop(kMipClockRootSeparationCentralRounding); - heuristics.flushStatistics(); + heuristics.flushStatistics(mipsolver, worker); if (checkLimits()) { analysis.mipTimerStop(kMipClockRootSeparation); return clockOff(analysis); } analysis.mipTimerStart(kMipClockRootSeparationEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockRootSeparationEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) { analysis.mipTimerStop(kMipClockRootSeparation); @@ -2152,7 +2243,7 @@ void HighsMipSolverData::evaluateRootNode() { } HighsCDouble sqrnorm = 0.0; - const auto& solvals = lp.getSolution().col_value; + const auto& solvals = getLp().getSolution().col_value; for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { curdirection[i] = firstlpsol[i] - solvals[i]; @@ -2185,7 +2276,7 @@ void HighsMipSolverData::evaluateRootNode() { double nextprogress = (1.0 - alpha) * smoothprogress + alpha * progress; if (nextprogress < smoothprogress * 1.01 && - (lp.getObjective() - firstlpsolobj) <= + (getLp().getObjective() - firstlpsolobj) <= (rootlpsolobj - firstlpsolobj) * 1.001) ++stall; else { @@ -2194,8 +2285,8 @@ void HighsMipSolverData::evaluateRootNode() { smoothprogress = nextprogress; } - rootlpsolobj = lp.getObjective(); - lp.setIterationLimit(std::max(10000, int(10 * avgrootlpiters))); + rootlpsolobj = getLp().getObjective(); + getLp().setIterationLimit(std::max(10000, int(10 * avgrootlpiters))); if (ncuts == 0) break; // Possibly query existence of an external solution @@ -2212,24 +2303,24 @@ void HighsMipSolverData::evaluateRootNode() { fflush(stdout); } - lp.setIterationLimit(); + getLp().setIterationLimit(); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); - rootlpsol = lp.getLpSolver().getSolution().col_value; - rootlpsolobj = lp.getObjective(); - lp.setIterationLimit(std::max(10000, int(10 * avgrootlpiters))); + rootlpsol = getLp().getLpSolver().getSolution().col_value; + rootlpsolobj = getLp().getObjective(); + getLp().setIterationLimit(std::max(10000, int(10 * avgrootlpiters))); if (mipsolver.options_mip_->mip_heuristic_run_zi_round) { - heuristics.ziRound(firstlpsol); - heuristics.flushStatistics(); + heuristics.ziRound(worker, firstlpsol); + heuristics.flushStatistics(mipsolver, worker); } if (mipsolver.options_mip_->mip_heuristic_run_shifting) { - heuristics.shifting(rootlpsol); - heuristics.flushStatistics(); + heuristics.shifting(worker, rootlpsol); + heuristics.flushStatistics(mipsolver, worker); } if (!analyticCenterComputed && compute_analytic_centre) { @@ -2240,25 +2331,25 @@ void HighsMipSolverData::evaluateRootNode() { analysis.mipTimerStop(kMipClockFinishAnalyticCentreComputation); analysis.mipTimerStart(kMipClockRootCentralRounding); - heuristics.centralRounding(); + heuristics.centralRounding(worker); analysis.mipTimerStop(kMipClockRootCentralRounding); - heuristics.flushStatistics(); + heuristics.flushStatistics(mipsolver, worker); // if there are new global bound changes we re-evaluate the LP and do one // more separation round if (checkLimits()) return clockOff(analysis); - bool separate = !domain.getChangedCols().empty(); + bool separate = !getDomain().getChangedCols().empty(); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); - if (separate && lp.scaledOptimal(status)) { + if (separate && getLp().scaledOptimal(status)) { HighsInt ncuts; analysis.mipTimerStart(kMipClockRootSeparationRound0); const bool root_separation_round_result = - rootSeparationRound(sepa, ncuts, status); + rootSeparationRound(worker, sepa, ncuts, status); analysis.mipTimerStop(kMipClockRootSeparationRound0); if (root_separation_round_result) return clockOff(analysis); ++nseparounds; @@ -2287,26 +2378,26 @@ void HighsMipSolverData::evaluateRootNode() { if (mipsolver.options_mip_->mip_heuristic_run_root_reduced_cost) { analysis.mipTimerStart(kMipClockRootHeuristicsReducedCost); - heuristics.rootReducedCost(); + heuristics.rootReducedCost(worker); analysis.mipTimerStop(kMipClockRootHeuristicsReducedCost); - heuristics.flushStatistics(); + heuristics.flushStatistics(mipsolver, worker); } if (checkLimits()) return clockOff(analysis); // if there are new global bound changes we re-evaluate the LP and do one // more separation round - bool separate = !domain.getChangedCols().empty(); + bool separate = !getDomain().getChangedCols().empty(); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); - if (separate && lp.scaledOptimal(status)) { + if (separate && getLp().scaledOptimal(status)) { HighsInt ncuts; analysis.mipTimerStart(kMipClockRootSeparationRound1); const bool root_separation_round_result = - rootSeparationRound(sepa, ncuts, status); + rootSeparationRound(worker, sepa, ncuts, status); analysis.mipTimerStop(kMipClockRootSeparationRound1); if (root_separation_round_result) return clockOff(analysis); ++nseparounds; @@ -2318,25 +2409,25 @@ void HighsMipSolverData::evaluateRootNode() { if (checkLimits()) return clockOff(analysis); if (mipsolver.options_mip_->mip_heuristic_run_rens) { analysis.mipTimerStart(kMipClockRootHeuristicsRens); - heuristics.RENS(rootlpsol); + heuristics.RENS(worker, rootlpsol); analysis.mipTimerStop(kMipClockRootHeuristicsRens); - heuristics.flushStatistics(); + heuristics.flushStatistics(mipsolver, worker); } if (checkLimits()) return clockOff(analysis); // if there are new global bound changes we re-evaluate the LP and do one // more separation round - separate = !domain.getChangedCols().empty(); + separate = !getDomain().getChangedCols().empty(); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); - if (separate && lp.scaledOptimal(status)) { + if (separate && getLp().scaledOptimal(status)) { HighsInt ncuts; analysis.mipTimerStart(kMipClockRootSeparationRound2); const bool root_separation_round_result = - rootSeparationRound(sepa, ncuts, status); + rootSeparationRound(worker, sepa, ncuts, status); analysis.mipTimerStop(kMipClockRootSeparationRound2); if (root_separation_round_result) return clockOff(analysis); ++nseparounds; @@ -2353,13 +2444,13 @@ void HighsMipSolverData::evaluateRootNode() { if (checkLimits()) return clockOff(analysis); analysis.mipTimerStart(kMipClockRootFeasibilityPump); - heuristics.feasibilityPump(); + heuristics.feasibilityPump(worker); analysis.mipTimerStop(kMipClockRootFeasibilityPump); - heuristics.flushStatistics(); + heuristics.flushStatistics(mipsolver, worker); if (checkLimits()) return clockOff(analysis); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); @@ -2377,17 +2468,17 @@ void HighsMipSolverData::evaluateRootNode() { // if there are new global bound changes we re-evaluate the LP and do one // more separation round - bool separate = !domain.getChangedCols().empty(); + bool separate = !getDomain().getChangedCols().empty(); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); - if (separate && lp.scaledOptimal(status)) { + if (separate && getLp().scaledOptimal(status)) { HighsInt ncuts; analysis.mipTimerStart(kMipClockRootSeparationRound3); const bool root_separation_round_result = - rootSeparationRound(sepa, ncuts, status); + rootSeparationRound(worker, sepa, ncuts, status); analysis.mipTimerStop(kMipClockRootSeparationRound3); if (root_separation_round_result) return clockOff(analysis); ++nseparounds; @@ -2401,8 +2492,8 @@ void HighsMipSolverData::evaluateRootNode() { kExternalMipSolutionQueryOriginEvaluateRootNode4); removeFixedIndices(); - if (lp.getLpSolver().getBasis().valid) lp.removeObsoleteRows(); - rootlpsolobj = lp.getObjective(); + if (getLp().getLpSolver().getBasis().valid) getLp().removeObsoleteRows(); + rootlpsolobj = getLp().getObjective(); printDisplayLine(); @@ -2439,16 +2530,16 @@ void HighsMipSolverData::evaluateRootNode() { if (detectSymmetries) { finishSymmetryDetection(tg, symData); analysis.mipTimerStart(kMipClockEvaluateRootLp); - status = evaluateRootLp(); + status = evaluateRootLp(worker); analysis.mipTimerStop(kMipClockEvaluateRootLp); if (status == HighsLpRelaxation::Status::kInfeasible) return clockOff(analysis); } // add the root node to the nodequeue to initialize the search - nodequeue.emplaceNode(std::vector(), - std::vector(), lower_bound, - lp.computeBestEstimate(pseudocost), 1); + nodequeue.emplaceNode( + std::vector(), std::vector(), lower_bound, + getLp().computeBestEstimate(worker.getPseudocost()), 1); } // End of HighsMipSolverData::evaluateRootNode() clockOff(analysis); @@ -2566,7 +2657,7 @@ void HighsMipSolverData::setupDomainPropagation() { model.a_matrix_.index_, model.a_matrix_.value_, ARstart_, ARindex_, ARvalue_); - pseudocost = HighsPseudocost(mipsolver); + getPseudoCost() = HighsPseudocost(mipsolver); // compute the maximal absolute coefficients to filter propagation maxAbsRowCoef.resize(mipsolver.numRow()); @@ -2581,8 +2672,8 @@ void HighsMipSolverData::setupDomainPropagation() { maxAbsRowCoef[i] = maxabsval; } - domain = HighsDomain(mipsolver); - domain.computeRowActivities(); + getDomain() = HighsDomain(mipsolver); + getDomain().computeRowActivities(); } void HighsMipSolverData::saveReportMipSolution(const double new_upper_limit) { @@ -2631,13 +2722,15 @@ void HighsMipSolverData::limitsToBounds(double& dual_bound, } } -void HighsMipSolverData::updateLowerBound(double new_lower_bound) { +void HighsMipSolverData::updateLowerBound(double new_lower_bound, + const bool check_bound_change, + const bool check_prev_data) { // Update lower bound double prev_lower_bound = lower_bound; lower_bound = new_lower_bound; if (!mipsolver.submip && lower_bound != prev_lower_bound) updatePrimalDualIntegral(prev_lower_bound, lower_bound, upper_bound, - upper_bound); + upper_bound, check_bound_change, check_prev_data); } // Interface to callbackAction, with mipsolver_objective_value since @@ -2755,6 +2848,11 @@ bool HighsMipSolverData::terminatorTerminated() const { return mipsolver.termination_status_ != HighsModelStatus::kNotset; } +bool HighsMipSolverData::terminatorTerminatedWorker( + const HighsMipWorker& worker) const { + return worker.heur_stats.termination_status_ != HighsModelStatus::kNotset; +} + void HighsMipSolverData::terminatorReport() const { if (this->terminatorActive()) mipsolver.terminator_.report(mipsolver.options_mip_->log_options); diff --git a/highs/mip/HighsMipSolverData.h b/highs/mip/HighsMipSolverData.h index f06da7ca6b..7da2e922fc 100644 --- a/highs/mip/HighsMipSolverData.h +++ b/highs/mip/HighsMipSolverData.h @@ -18,6 +18,7 @@ #include "mip/HighsDomain.h" #include "mip/HighsImplications.h" #include "mip/HighsLpRelaxation.h" +#include "mip/HighsMipWorker.h" #include "mip/HighsNodeQueue.h" #include "mip/HighsObjectiveFunction.h" #include "mip/HighsPrimalHeuristics.h" @@ -68,14 +69,18 @@ enum MipSolutionSource : int { struct HighsMipSolverData { HighsMipSolver& mipsolver; - HighsCutPool cutpool; - HighsConflictPool conflictPool; - HighsDomain domain; - HighsLpRelaxation lp; - HighsPseudocost pseudocost; + + std::deque lps; + std::deque cutpools; + std::deque conflictpools; + std::deque domains; + std::deque pseudocosts; + std::deque workers; + bool parallel_lock; + + HighsPrimalHeuristics heuristics; HighsCliqueTable cliquetable; HighsImplications implications; - HighsPrimalHeuristics heuristics; HighsRedcostFixing redcostfixing; HighsObjectiveFunction objectiveFunction; presolve::HighsPostsolveStack postSolveStack; @@ -154,67 +159,7 @@ struct HighsMipSolverData { HighsDebugSol debugSolution; - HighsMipSolverData(HighsMipSolver& mipsolver) - : mipsolver(mipsolver), - cutpool(mipsolver.numCol(), mipsolver.options_mip_->mip_pool_age_limit, - mipsolver.options_mip_->mip_pool_soft_limit), - conflictPool(5 * mipsolver.options_mip_->mip_pool_age_limit, - mipsolver.options_mip_->mip_pool_soft_limit), - domain(mipsolver), - lp(mipsolver), - pseudocost(), - cliquetable(mipsolver.numCol()), - implications(mipsolver), - heuristics(mipsolver), - objectiveFunction(mipsolver), - presolve_status(HighsPresolveStatus::kNotSet), - cliquesExtracted(false), - rowMatrixSet(false), - analyticCenterComputed(false), - analyticCenterStatus(HighsModelStatus::kNotset), - detectSymmetries(false), - numRestarts(0), - numRestartsRoot(0), - numCliqueEntriesAfterPresolve(0), - numCliqueEntriesAfterFirstPresolve(0), - feastol(0.0), - epsilon(0.0), - heuristic_effort(0.0), - dispfreq(0), - firstlpsolobj(-kHighsInf), - rootlpsolobj(-kHighsInf), - numintegercols(0), - maxTreeSizeLog2(0), - pruned_treeweight(0), - avgrootlpiters(0.0), - disptime(0.0), - last_disptime(0.0), - firstrootlpiters(0), - num_nodes(0), - num_leaves(0), - num_leaves_before_run(0), - num_nodes_before_run(0), - total_repair_lp(0), - total_repair_lp_feasible(0), - total_repair_lp_iterations(0), - total_lp_iterations(0), - heuristic_lp_iterations(0), - sepa_lp_iterations(0), - sb_lp_iterations(0), - total_lp_iterations_before_run(0), - heuristic_lp_iterations_before_run(0), - sepa_lp_iterations_before_run(0), - sb_lp_iterations_before_run(0), - num_disp_lines(0), - numImprovingSols(0), - lower_bound(-kHighsInf), - upper_bound(kHighsInf), - upper_limit(kHighsInf), - optimality_limit(kHighsInf), - debugSolution(mipsolver) { - domain.addCutpool(cutpool); - domain.addConflictPool(conflictPool); - } + HighsMipSolverData(HighsMipSolver& mipsolver); bool solutionRowFeasible(const std::vector& solution) const; HighsModelStatus feasibilityJump(); @@ -266,10 +211,12 @@ struct HighsMipSolverData { const std::vector& solution) const; bool trySolution(const std::vector& solution, const int solution_source = kSolutionSourceNone); - bool rootSeparationRound(HighsSeparation& sepa, HighsInt& ncuts, - HighsLpRelaxation::Status& status); - HighsLpRelaxation::Status evaluateRootLp(); - void evaluateRootNode(); + bool rootSeparationRound(HighsMipWorker& worker, HighsSeparation& sepa, + HighsInt& ncuts, HighsLpRelaxation::Status& status); + HighsLpRelaxation::Status evaluateRootLp(HighsMipWorker& worker); + + void evaluateRootNode(HighsMipWorker& worker); + bool addIncumbent(const std::vector& sol, double solobj, const int solution_source, const bool print_display_line = true, @@ -293,7 +240,9 @@ struct HighsMipSolverData { bool checkLimits(int64_t nodeOffset = 0) const; void limitsToBounds(double& dual_bound, double& primal_bound, double& mip_rel_gap) const; - void updateLowerBound(double new_lower_bound); + void updateLowerBound(double new_lower_bound, + const bool check_bound_change = true, + const bool check_prev_data = true); void setCallbackDataOut(const double mipsolver_objective_value) const; bool interruptFromCallbackWithData(const int callback_type, const double mipsolver_objective_value, @@ -307,7 +256,25 @@ struct HighsMipSolverData { HighsInt terminatorMyInstance() const; void terminatorTerminate(); bool terminatorTerminated() const; + bool terminatorTerminatedWorker(const HighsMipWorker& worker) const; void terminatorReport() const; + + bool parallelLockActive() const { + return (parallel_lock && hasMultipleWorkers()); + } + + bool hasMultipleWorkers() const { return workers.size() > 1; } + + HighsDomain& getDomain() { return domains[0]; } + HighsConflictPool& getConflictPool() { return conflictpools[0]; } + HighsCutPool& getCutPool() { return cutpools[0]; } + HighsLpRelaxation& getLp() { return lps[0]; } + HighsPseudocost& getPseudoCost() { return pseudocosts[0]; } + const HighsDomain& getDomain() const { return domains[0]; } + const HighsConflictPool& getConflictPool() const { return conflictpools[0]; } + const HighsCutPool& getCutPool() const { return cutpools[0]; } + const HighsLpRelaxation& getLp() const { return lps[0]; } + const HighsPseudocost& getPseudoCost() const { return pseudocosts[0]; } }; #endif diff --git a/highs/mip/HighsMipWorker.cpp b/highs/mip/HighsMipWorker.cpp new file mode 100644 index 0000000000..2009af824f --- /dev/null +++ b/highs/mip/HighsMipWorker.cpp @@ -0,0 +1,146 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Available as open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +#include "mip/HighsMipWorker.h" + +#include "mip/HighsMipSolverData.h" +#include "mip/MipTimer.h" + +HighsMipWorker::HighsMipWorker(const HighsMipSolver& mipsolver, + HighsLpRelaxation* lp, HighsDomain* domain, + HighsCutPool* cutpool, + HighsConflictPool* conflictpool, + HighsPseudocost* pseudocost) + : mipsolver_(mipsolver), + mipdata_(*mipsolver_.mipdata_), + lp_(lp), + globaldom_(domain), + cutpool_(cutpool), + conflictpool_(conflictpool), + pseudocost_(pseudocost), + randgen(mipsolver.options_mip_->random_seed) { + upper_bound = mipdata_.upper_bound; + upper_limit = mipdata_.upper_limit; + optimality_limit = mipdata_.optimality_limit; + heuristics_allowed = true; + search_ptr_ = + std::unique_ptr(new HighsSearch(*this, getPseudocost())); + sepa_ptr_ = std::unique_ptr(new HighsSeparation(*this)); + search_ptr_->setLpRelaxation(lp_); + sepa_ptr_->setLpRelaxation(lp_); +} + +const HighsMipSolver& HighsMipWorker::getMipSolver() const { + return mipsolver_; +} + +void HighsMipWorker::resetSearch() { + search_ptr_.reset(); + search_ptr_ = + std::unique_ptr(new HighsSearch(*this, getPseudocost())); + search_ptr_->setLpRelaxation(lp_); +} + +void HighsMipWorker::resetSepa() { + sepa_ptr_.reset(); + sepa_ptr_ = std::unique_ptr(new HighsSeparation(*this)); + sepa_ptr_->setLpRelaxation(lp_); +} + +bool HighsMipWorker::addIncumbent(const std::vector& sol, double solobj, + int solution_source) { + if (solobj < upper_bound) { + // Get the transformed objective and solution if required + const std::pair transformed_solobj = + transformNewIntegerFeasibleSolution(sol); + if (transformed_solobj.first && transformed_solobj.second < upper_bound) { + upper_bound = transformed_solobj.second; + double new_upper_limit = + mipdata_.computeNewUpperLimit(upper_bound, 0.0, 0.0); + if (new_upper_limit < upper_limit) { + upper_limit = new_upper_limit; + optimality_limit = mipdata_.computeNewUpperLimit( + upper_bound, mipsolver_.options_mip_->mip_abs_gap, + mipsolver_.options_mip_->mip_rel_gap); + } + } + // Can't repair solutions locally, so also buffer infeasible ones + solutions_.emplace_back(sol, solobj, solution_source); + } + return true; +} + +std::pair HighsMipWorker::transformNewIntegerFeasibleSolution( + const std::vector& sol) const { + HighsSolution solution; + solution.col_value = sol; + solution.value_valid = true; + + // Perform primal postsolve to get the original column values + mipsolver_.mipdata_->postSolveStack.undoPrimal(*mipsolver_.options_mip_, + solution, -1, true); + + // Determine the row values, as they aren't computed in primal + // postsolve + HighsStatus return_status = + calculateRowValuesQuad(*mipsolver_.orig_model_, solution); + if (kAllowDeveloperAssert) assert(return_status == HighsStatus::kOk); + + // compute the objective value in the original space + double bound_violation_ = 0; + double row_violation_ = 0; + double integrality_violation_ = 0; + + HighsCDouble mipsolver_quad_objective_value = 0; + + bool feasible = mipsolver_.solutionFeasible( + mipsolver_.orig_model_, solution.col_value, &solution.row_value, + bound_violation_, row_violation_, integrality_violation_, + mipsolver_quad_objective_value); + + const double transformed_solobj = static_cast( + static_cast(mipsolver_.orig_model_->sense_) * + mipsolver_quad_objective_value - + mipsolver_.model_->offset_); + + return std::make_pair(feasible, transformed_solobj); +} + +bool HighsMipWorker::trySolution(const std::vector& solution, + const int solution_source) { + if (static_cast(solution.size()) != mipsolver_.model_->num_col_) + return false; + + HighsCDouble obj = 0; + + for (HighsInt i = 0; i != mipsolver_.model_->num_col_; ++i) { + if (solution[i] < mipsolver_.model_->col_lower_[i] - mipdata_.feastol) + return false; + if (solution[i] > mipsolver_.model_->col_upper_[i] + mipdata_.feastol) + return false; + if (mipsolver_.variableType(i) == HighsVarType::kInteger && + fractionality(solution[i]) > mipdata_.feastol) + return false; + + obj += mipsolver_.colCost(i) * solution[i]; + } + + for (HighsInt i = 0; i != mipsolver_.model_->num_row_; ++i) { + double rowactivity = 0.0; + + HighsInt start = mipdata_.ARstart_[i]; + HighsInt end = mipdata_.ARstart_[i + 1]; + + for (HighsInt j = start; j != end; ++j) + rowactivity += solution[mipdata_.ARindex_[j]] * mipdata_.ARvalue_[j]; + + if (rowactivity > mipsolver_.rowUpper(i) + mipdata_.feastol) return false; + if (rowactivity < mipsolver_.rowLower(i) - mipdata_.feastol) return false; + } + + return addIncumbent(solution, static_cast(obj), solution_source); +} \ No newline at end of file diff --git a/highs/mip/HighsMipWorker.h b/highs/mip/HighsMipWorker.h new file mode 100644 index 0000000000..5bbd62263d --- /dev/null +++ b/highs/mip/HighsMipWorker.h @@ -0,0 +1,125 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Available Hias open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +#ifndef HIGHS_MIP_WORKER_H_ +#define HIGHS_MIP_WORKER_H_ + +#include "mip/HighsConflictPool.h" +#include "mip/HighsCutPool.h" +#include "mip/HighsImplications.h" +#include "mip/HighsLpRelaxation.h" +#include "mip/HighsMipSolver.h" +#include "mip/HighsNodeQueue.h" +#include "mip/HighsPrimalHeuristics.h" +#include "mip/HighsPseudocost.h" +#include "mip/HighsSeparation.h" + +class HighsSearch; + +class HighsMipWorker { + public: + struct SepaStatistics { + SepaStatistics() : numNeighbourhoodQueries(0), sepa_lp_iterations(0) {} + + int64_t numNeighbourhoodQueries; + int64_t sepa_lp_iterations; + }; + struct HeurStatistics { + HeurStatistics() + : total_repair_lp(0), + total_repair_lp_feasible(0), + total_repair_lp_iterations(0), + lp_iterations(0) { + successObservations = 0; + numSuccessObservations = 0; + infeasObservations = 0; + numInfeasObservations = 0; + max_submip_level = 0; + termination_status_ = HighsModelStatus::kNotset; + } + + int64_t total_repair_lp; + int64_t total_repair_lp_feasible; + int64_t total_repair_lp_iterations; + int64_t lp_iterations; + + double successObservations; + HighsInt numSuccessObservations; + double infeasObservations; + HighsInt numInfeasObservations; + HighsInt max_submip_level; + HighsModelStatus termination_status_; + }; + const HighsMipSolver& mipsolver_; + const HighsMipSolverData& mipdata_; + + HighsLpRelaxation* lp_; + HighsDomain* globaldom_; + HighsCutPool* cutpool_; + HighsConflictPool* conflictpool_; + HighsPseudocost* pseudocost_; + + std::unique_ptr search_ptr_; + std::unique_ptr sepa_ptr_; + + HighsNodeQueue nodequeue; + + const HighsMipSolver& getMipSolver() const; + + double upper_bound; + double upper_limit; + double optimality_limit; + + std::vector, double, int>> solutions_; + + bool heuristics_allowed; + + HeurStatistics heur_stats; + SepaStatistics sepa_stats; + + HighsRandom randgen; + + HighsMipWorker(const HighsMipSolver& mipsolver, HighsLpRelaxation* lp, + HighsDomain* domain, HighsCutPool* cutpool, + HighsConflictPool* conflictpool, HighsPseudocost* pseudocost); + + ~HighsMipWorker() { + search_ptr_.reset(); + sepa_ptr_.reset(); + } + + const bool checkLimits(int64_t nodeOffset = 0) const; + + void resetSearch(); + + void resetSepa(); + + HighsDomain& getGlobalDomain() const { return *globaldom_; }; + + HighsPseudocost& getPseudocost() const { return *pseudocost_; }; + + HighsConflictPool& getConflictPool() const { return *conflictpool_; }; + + HighsCutPool& getCutPool() const { return *cutpool_; }; + + HighsLpRelaxation& getLpRelaxation() const { return *lp_; }; + + bool addIncumbent(const std::vector& sol, double solobj, + int solution_source); + + std::pair transformNewIntegerFeasibleSolution( + const std::vector& sol) const; + + bool trySolution(const std::vector& solution, + const int solution_source); + + void setAllowHeuristics(const bool allowed) { heuristics_allowed = allowed; } + + bool getAllowHeuristics() const { return heuristics_allowed; } +}; + +#endif diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index 443a24b98b..d4fa7ad8e1 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -530,7 +530,8 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, inds.resize(numInds); if (transLp.untransform(cutVals, inds, rhs)) - success |= cutGen.finalizeAndAddCut(inds, cutVals, rhs); + success |= cutGen.finalizeAndAddCut(transLp.getGlobaldom(), + inds, cutVals, rhs); // printf("cut is violated for k = %d\n", k); break; diff --git a/highs/mip/HighsPrimalHeuristics.cpp b/highs/mip/HighsPrimalHeuristics.cpp index 6b5209a694..c52d3d9c13 100644 --- a/highs/mip/HighsPrimalHeuristics.cpp +++ b/highs/mip/HighsPrimalHeuristics.cpp @@ -34,16 +34,11 @@ HighsPrimalHeuristics::HighsPrimalHeuristics(HighsMipSolver& mipsolver) : mipsolver(mipsolver), - total_repair_lp(0), - total_repair_lp_feasible(0), - total_repair_lp_iterations(0), - lp_iterations(0), - randgen(mipsolver.options_mip_->random_seed) { - successObservations = 0; - numSuccessObservations = 0; - infeasObservations = 0; - numInfeasObservations = 0; -} + successObservations(0.0), + numSuccessObservations(0), + infeasObservations(0.0), + numInfeasObservations(0), + randgen(mipsolver.options_mip_->random_seed) {} void HighsPrimalHeuristics::setupIntCols() { intcols = mipsolver.mipdata_->integer_cols; @@ -80,9 +75,10 @@ void HighsPrimalHeuristics::setupIntCols() { } bool HighsPrimalHeuristics::solveSubMip( - const HighsLp& lp, const HighsBasis& basis, double fixingRate, - std::vector colLower, std::vector colUpper, - HighsInt maxleaves, HighsInt maxnodes, HighsInt stallnodes) { + HighsMipWorker& worker, const HighsLp& lp, const HighsBasis& basis, + double fixingRate, std::vector colLower, + std::vector colUpper, HighsInt maxleaves, HighsInt maxnodes, + HighsInt stallnodes) { HighsOptions submipoptions = *mipsolver.options_mip_; HighsLp submip = lp; @@ -109,11 +105,10 @@ bool HighsPrimalHeuristics::solveSubMip( submipoptions.mip_max_stall_nodes = stallnodes; submipoptions.mip_pscost_minreliable = 0; submipoptions.time_limit -= mipsolver.timer_.read(); - submipoptions.objective_bound = mipsolver.mipdata_->upper_limit; + submipoptions.objective_bound = worker.upper_limit; if (!mipsolver.submip) { - double curr_abs_gap = - mipsolver.mipdata_->upper_limit - mipsolver.mipdata_->lower_bound; + double curr_abs_gap = worker.upper_limit - mipsolver.mipdata_->lower_bound; if (curr_abs_gap == kHighsInf) { curr_abs_gap = fabs(mipsolver.mipdata_->lower_bound); @@ -137,7 +132,7 @@ bool HighsPrimalHeuristics::solveSubMip( HighsSolution solution; solution.value_valid = false; solution.dual_valid = false; - if (!mipsolver.submip) { + if (!mipsolver.submip && !mipsolver.mipdata_->parallelLockActive()) { mipsolver.analysis_.mipTimerStart(kMipClockSubMipSolve); // Remember to accumulate time for sub-MIP solves! mipsolver.sub_solver_call_time_.run_time[kSubSolverSubMip] -= @@ -150,15 +145,15 @@ bool HighsPrimalHeuristics::solveSubMip( // the sub-MIP submipsolver.initialiseTerminator(mipsolver); submipsolver.rootbasis = &basis; - HighsPseudocostInitialization pscostinit(mipsolver.mipdata_->pseudocost, 1); + HighsPseudocostInitialization pscostinit(worker.getPseudocost(), 1); submipsolver.pscostinit = &pscostinit; submipsolver.clqtableinit = &mipsolver.mipdata_->cliquetable; submipsolver.implicinit = &mipsolver.mipdata_->implications; // Solve the sub-MIP submipsolver.run(); - mipsolver.max_submip_level = - std::max(submipsolver.max_submip_level + 1, mipsolver.max_submip_level); - if (!mipsolver.submip) { + worker.heur_stats.max_submip_level = std::max( + submipsolver.max_submip_level + 1, worker.heur_stats.max_submip_level); + if (!mipsolver.submip && !mipsolver.mipdata_->parallelLockActive()) { mipsolver.analysis_.mipTimerStop(kMipClockSubMipSolve); mipsolver.sub_solver_call_time_.num_call[kSubSolverSubMip]++; mipsolver.sub_solver_call_time_.run_time[kSubSolverSubMip] += @@ -176,7 +171,7 @@ bool HighsPrimalHeuristics::solveSubMip( assert(submipsolver.mipdata_); } if (submipsolver.termination_status_ != HighsModelStatus::kNotset) { - mipsolver.termination_status_ = submipsolver.termination_status_; + worker.heur_stats.termination_status_ = submipsolver.termination_status_; return false; } if (submipsolver.mipdata_) { @@ -189,19 +184,21 @@ bool HighsPrimalHeuristics::solveSubMip( // (double)mipsolver.orig_model_->a_matrix_.value_.size(); int64_t adjusted_lp_iterations = (size_t)(adjustmentfactor * submipsolver.mipdata_->total_lp_iterations); - lp_iterations += adjusted_lp_iterations; - total_repair_lp += submipsolver.mipdata_->total_repair_lp; - total_repair_lp_feasible += submipsolver.mipdata_->total_repair_lp_feasible; - total_repair_lp_iterations += + worker.heur_stats.lp_iterations += adjusted_lp_iterations; + worker.heur_stats.total_repair_lp += submipsolver.mipdata_->total_repair_lp; + worker.heur_stats.total_repair_lp_feasible += + submipsolver.mipdata_->total_repair_lp_feasible; + worker.heur_stats.total_repair_lp_iterations += submipsolver.mipdata_->total_repair_lp_iterations; + // Warning: This will not be deterministic if sub-mips are run in parallel if (mipsolver.submip) mipsolver.mipdata_->num_nodes += std::max( int64_t{1}, int64_t(adjustmentfactor * submipsolver.node_count_)); } if (submipsolver.modelstatus_ == HighsModelStatus::kInfeasible) { - infeasObservations += fixingRate; - ++numInfeasObservations; + worker.heur_stats.infeasObservations += fixingRate; + ++worker.heur_stats.numInfeasObservations; } if (submipsolver.node_count_ <= 1 && submipsolver.modelstatus_ == HighsModelStatus::kInfeasible) @@ -209,31 +206,36 @@ bool HighsPrimalHeuristics::solveSubMip( HighsInt oldNumImprovingSols = mipsolver.mipdata_->numImprovingSols; if (submipsolver.modelstatus_ != HighsModelStatus::kInfeasible && !submipsolver.solution_.empty()) { - mipsolver.mipdata_->trySolution(submipsolver.solution_, - kSolutionSourceSubMip); + trySolution(submipsolver.solution_, kSolutionSourceSubMip, worker); } if (mipsolver.mipdata_->numImprovingSols != oldNumImprovingSols) { // remember fixing rate as good - successObservations += fixingRate; - ++numSuccessObservations; + worker.heur_stats.successObservations += fixingRate; + ++worker.heur_stats.numSuccessObservations; } return true; } -double HighsPrimalHeuristics::determineTargetFixingRate() { +double HighsPrimalHeuristics::determineTargetFixingRate( + HighsMipWorker& worker) { double lowFixingRate = 0.6; double highFixingRate = 0.6; - if (numInfeasObservations != 0) { - double infeasRate = infeasObservations / numInfeasObservations; + HighsRandom& randgen = + mipsolver.mipdata_->parallelLockActive() ? worker.randgen : this->randgen; + + if (getNumInfeasObservations(worker) != 0) { + double infeasRate = + getInfeasObservations(worker) / getNumInfeasObservations(worker); highFixingRate = 0.9 * infeasRate; lowFixingRate = std::min(lowFixingRate, highFixingRate); } - if (numSuccessObservations != 0) { - double successFixingRate = successObservations / numSuccessObservations; + if (getNumSuccessObservations(worker) != 0) { + double successFixingRate = + getSuccessObservations(worker) / getNumSuccessObservations(worker); lowFixingRate = std::min(lowFixingRate, 0.9 * successFixingRate); highFixingRate = std::max(successFixingRate * 1.1, highFixingRate); } @@ -252,7 +254,7 @@ class HeuristicNeighbourhood { HighsInt numTotal; public: - HeuristicNeighbourhood(HighsMipSolver& mipsolver, HighsDomain& localdom) + HeuristicNeighbourhood(const HighsMipSolver& mipsolver, HighsDomain& localdom) : localdom(localdom), numFixed(0), startCheckedChanges(localdom.getDomainChangeStack().size()), @@ -281,9 +283,10 @@ class HeuristicNeighbourhood { } }; -void HighsPrimalHeuristics::rootReducedCost() { +void HighsPrimalHeuristics::rootReducedCost(HighsMipWorker& worker) { std::vector> lurkingBounds = - mipsolver.mipdata_->redcostfixing.getLurkingBounds(mipsolver); + mipsolver.mipdata_->redcostfixing.getLurkingBounds( + mipsolver, worker.getGlobalDomain()); if (10 * lurkingBounds.size() < mipsolver.mipdata_->integral_cols.size()) return; pdqsort(lurkingBounds.begin(), lurkingBounds.end(), @@ -292,7 +295,7 @@ void HighsPrimalHeuristics::rootReducedCost() { return a.first > b.first; }); - auto localdom = mipsolver.mipdata_->domain; + HighsDomain localdom = worker.getGlobalDomain(); HeuristicNeighbourhood neighbourhood(mipsolver, localdom); @@ -311,7 +314,9 @@ void HighsPrimalHeuristics::rootReducedCost() { while (true) { localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); mipsolver.mipdata_->updateLowerBound( std::max(mipsolver.mipdata_->lower_bound, currCutoff)); @@ -333,8 +338,8 @@ void HighsPrimalHeuristics::rootReducedCost() { double fixingRate = neighbourhood.getFixingRate(); if (fixingRate < 0.3) return; - solveSubMip(*mipsolver.model_, mipsolver.mipdata_->firstrootbasis, fixingRate, - localdom.col_lower_, localdom.col_upper_, + solveSubMip(worker, *mipsolver.model_, mipsolver.mipdata_->firstrootbasis, + fixingRate, localdom.col_lower_, localdom.col_upper_, 500, // std::max(50, int(0.05 * // (mipsolver.mipdata_->num_leaves))), 200 + static_cast(mipsolver.mipdata_->num_nodes / 20), @@ -358,24 +363,33 @@ static double calcFixVal(double rootchange, double fracval, double cost) { return std::floor(fracval + 0.5); } -void HighsPrimalHeuristics::RENS(const std::vector& tmp) { +void HighsPrimalHeuristics::RENS(HighsMipWorker& worker, + const std::vector& tmp) { // return if domain is infeasible - if (mipsolver.mipdata_->domain.infeasible()) return; + if (worker.getGlobalDomain().infeasible()) return; + + HighsPseudocost pscost(worker.getPseudocost()); + HighsSearch heur(worker, pscost); - HighsPseudocost pscost(mipsolver.mipdata_->pseudocost); - HighsSearch heur(mipsolver, pscost); HighsDomain& localdom = heur.getLocalDomain(); heur.setHeuristic(true); + std::vector intcols_; + if (mipsolver.mipdata_->parallelLockActive()) { + intcols_ = intcols; + } + std::vector& intcols = + mipsolver.mipdata_->parallelLockActive() ? intcols_ : this->intcols; intcols.erase(std::remove_if(intcols.begin(), intcols.end(), [&](HighsInt i) { - return mipsolver.mipdata_->domain.isFixed(i); + return worker.getGlobalDomain().isFixed(i); }), intcols.end()); - HighsLpRelaxation heurlp(mipsolver.mipdata_->lp); + HighsLpRelaxation heurlp(worker.getLpRelaxation()); + heurlp.setMipWorker(worker); // only use the global upper limit as LP limit so that dual proofs are valid - heurlp.setObjectiveLimit(mipsolver.mipdata_->upper_limit); + heurlp.setObjectiveLimit(worker.upper_limit); heurlp.setAdjustSymmetricBranchingCol(false); heur.setLpRelaxation(&heurlp); @@ -387,7 +401,7 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { // determine the initial number of unfixed variables fixing rate to decide if // the problem is restricted enough to be considered for solving a submip - double maxfixingrate = determineTargetFixingRate(); + double maxfixingrate = determineTargetFixingRate(worker); double fixingrate = 0.0; bool stop = false; // heurlp.setIterationLimit(2 * mipsolver.mipdata_->maxrootlpiters); @@ -404,7 +418,7 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { // heur.getCurrentDepth(), targetdepth); if (heur.getCurrentDepth() > targetdepth) { if (!heur.backtrackUntilDepth(targetdepth)) { - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); return; } } @@ -417,8 +431,8 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { // printf("done evaluating node\n"); if (heur.currentNodePruned()) { ++nbacktracks; - if (mipsolver.mipdata_->domain.infeasible()) { - lp_iterations += heur.getLocalLpIterations(); + if (worker.getGlobalDomain().infeasible()) { + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); return; } @@ -451,7 +465,9 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { heur.branchUpwards(i, downval, downval - 0.5); localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } } @@ -460,7 +476,9 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { heur.branchDownwards(i, upval, upval + 0.5); localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } } @@ -511,7 +529,9 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { heur.branchUpwards(fracint.first, fixval, fracint.second); localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } @@ -523,7 +543,9 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { heur.branchDownwards(fracint.first, fixval, fracint.second); localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } @@ -545,7 +567,7 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { // if there is no node left it means we backtracked to the global domain and // the subproblem was solved with the dive if (!heur.hasNode()) { - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); return; } // determine the fixing rate to decide if the problem is restricted enough to @@ -558,7 +580,7 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { // heur.childselrule = ChildSelectionRule::kBestCost; heur.setMinReliable(0); heur.solveDepthFirst(10); - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); if (mipsolver.submip) mipsolver.mipdata_->num_nodes += heur.getLocalNodes(); // lpiterations += heur.lpiterations; // pseudocost = heur.pseudocost; @@ -566,27 +588,34 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { } heurlp.removeObsoleteRows(false); - const bool solve_sub_mip_return = - solveSubMip(heurlp.getLp(), heurlp.getLpSolver().getBasis(), fixingrate, - localdom.col_lower_, localdom.col_upper_, - 500, // std::max(50, int(0.05 * - // (mipsolver.mipdata_->num_leaves))), - 200 + mipsolver.mipdata_->num_nodes / 20, 12); - if (mipsolver.mipdata_->terminatorTerminated()) return; + HighsInt node_reduction_factor = + mipsolver.mipdata_->parallelLockActive() + ? std::max( + HighsInt{1}, + static_cast(mipsolver.mipdata_->workers.size()) / 4) + : 1; + const bool solve_sub_mip_return = solveSubMip( + worker, heurlp.getLp(), heurlp.getLpSolver().getBasis(), fixingrate, + localdom.col_lower_, localdom.col_upper_, + 500, // std::max(50, int(0.05 * + // (mipsolver.mipdata_->num_leaves))), + 200 + mipsolver.mipdata_->num_nodes / (node_reduction_factor * 20), 12); + if (mipsolver.mipdata_->terminatorTerminatedWorker(worker)) return; if (!solve_sub_mip_return) { - int64_t new_lp_iterations = lp_iterations + heur.getLocalLpIterations(); + int64_t new_lp_iterations = + worker.heur_stats.lp_iterations + heur.getLocalLpIterations(); if (new_lp_iterations + mipsolver.mipdata_->heuristic_lp_iterations > 100000 + ((mipsolver.mipdata_->total_lp_iterations - mipsolver.mipdata_->heuristic_lp_iterations - mipsolver.mipdata_->sb_lp_iterations) >> 1)) { - lp_iterations = new_lp_iterations; + worker.heur_stats.lp_iterations = new_lp_iterations; return; } targetdepth = heur.getCurrentDepth() / 2; if (targetdepth <= 1 || mipsolver.mipdata_->checkLimits()) { - lp_iterations = new_lp_iterations; + worker.heur_stats.lp_iterations = new_lp_iterations; return; } maxfixingrate = fixingrate * 0.5; @@ -595,29 +624,38 @@ void HighsPrimalHeuristics::RENS(const std::vector& tmp) { goto retry; } - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); } -void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { +void HighsPrimalHeuristics::RINS(HighsMipWorker& worker, + const std::vector& relaxationsol) { // return if domain is infeasible - if (mipsolver.mipdata_->domain.infeasible()) return; + if (worker.getGlobalDomain().infeasible()) return; if (relaxationsol.size() != static_cast(mipsolver.numCol())) return; + std::vector intcols_; + if (mipsolver.mipdata_->parallelLockActive()) { + intcols_ = intcols; + } + std::vector& intcols = + mipsolver.mipdata_->parallelLockActive() ? intcols_ : this->intcols; intcols.erase(std::remove_if(intcols.begin(), intcols.end(), [&](HighsInt i) { - return mipsolver.mipdata_->domain.isFixed(i); + return worker.getGlobalDomain().isFixed(i); }), intcols.end()); - HighsPseudocost pscost(mipsolver.mipdata_->pseudocost); - HighsSearch heur(mipsolver, pscost); + HighsPseudocost pscost(worker.getPseudocost()); + HighsSearch heur(worker, pscost); + HighsDomain& localdom = heur.getLocalDomain(); heur.setHeuristic(true); - HighsLpRelaxation heurlp(mipsolver.mipdata_->lp); + HighsLpRelaxation heurlp(worker.getLpRelaxation()); + heurlp.setMipWorker(worker); // only use the global upper limit as LP limit so that dual proofs are valid - heurlp.setObjectiveLimit(mipsolver.mipdata_->upper_limit); + heurlp.setObjectiveLimit(worker.upper_limit); heurlp.setAdjustSymmetricBranchingCol(false); heur.setLpRelaxation(&heurlp); @@ -629,7 +667,7 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { // determine the initial number of unfixed variables fixing rate to decide if // the problem is restricted enough to be considered for solving a submip - double maxfixingrate = determineTargetFixingRate(); + double maxfixingrate = determineTargetFixingRate(worker); double minfixingrate = 0.25; double fixingrate = 0.0; bool stop = false; @@ -644,7 +682,7 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { // targetdepth); if (heur.getCurrentDepth() > targetdepth) { if (!heur.backtrackUntilDepth(targetdepth)) { - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); return; } } @@ -656,8 +694,8 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { if (heur.currentNodePruned()) { ++nbacktracks; // printf("backtrack1\n"); - if (mipsolver.mipdata_->domain.infeasible()) { - lp_iterations += heur.getLocalLpIterations(); + if (worker.getGlobalDomain().infeasible()) { + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); return; } @@ -727,7 +765,9 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { heur.branchUpwards(i, fixval, fixval - 0.5); localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } @@ -738,7 +778,9 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { heur.branchDownwards(i, fixval, fixval + 0.5); localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } @@ -794,7 +836,9 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { ++numBranched; heur.branchUpwards(fracint->first, fixval, fracint->second); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } @@ -805,7 +849,9 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { ++numBranched; heur.branchDownwards(fracint->first, fixval, fracint->second); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); break; } @@ -829,7 +875,7 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { // if there is no node left it means we backtracked to the global domain and // the subproblem was solved with the dive if (!heur.hasNode()) { - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); return; } // determine the fixing rate to decide if the problem is restricted enough @@ -842,7 +888,7 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { // heur.childselrule = ChildSelectionRule::kBestCost; heur.setMinReliable(0); heur.solveDepthFirst(10); - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); if (mipsolver.submip) mipsolver.mipdata_->num_nodes += heur.getLocalNodes(); // lpiterations += heur.lpiterations; // pseudocost = heur.pseudocost; @@ -850,27 +896,34 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { } heurlp.removeObsoleteRows(false); - const bool solve_sub_mip_return = - solveSubMip(heurlp.getLp(), heurlp.getLpSolver().getBasis(), fixingrate, - localdom.col_lower_, localdom.col_upper_, - 500, // std::max(50, int(0.05 * - // (mipsolver.mipdata_->num_leaves))), - 200 + mipsolver.mipdata_->num_nodes / 20, 12); - if (mipsolver.mipdata_->terminatorTerminated()) return; + HighsInt node_reduction_factor = + mipsolver.mipdata_->parallelLockActive() + ? std::max( + HighsInt{1}, + static_cast(mipsolver.mipdata_->workers.size()) / 4) + : 1; + const bool solve_sub_mip_return = solveSubMip( + worker, heurlp.getLp(), heurlp.getLpSolver().getBasis(), fixingrate, + localdom.col_lower_, localdom.col_upper_, + 500, // std::max(50, int(0.05 * + // (mipsolver.mipdata_->num_leaves))), + 200 + mipsolver.mipdata_->num_nodes / (node_reduction_factor * 20), 12); + if (mipsolver.mipdata_->terminatorTerminatedWorker(worker)) return; if (!solve_sub_mip_return) { - int64_t new_lp_iterations = lp_iterations + heur.getLocalLpIterations(); + int64_t new_lp_iterations = + worker.heur_stats.lp_iterations + heur.getLocalLpIterations(); if (new_lp_iterations + mipsolver.mipdata_->heuristic_lp_iterations > 100000 + ((mipsolver.mipdata_->total_lp_iterations - mipsolver.mipdata_->heuristic_lp_iterations - mipsolver.mipdata_->sb_lp_iterations) >> 1)) { - lp_iterations = new_lp_iterations; + worker.heur_stats.lp_iterations = new_lp_iterations; return; } targetdepth = heur.getCurrentDepth() / 2; if (targetdepth <= 1 || mipsolver.mipdata_->checkLimits()) { - lp_iterations = new_lp_iterations; + worker.heur_stats.lp_iterations = new_lp_iterations; return; } // printf("infeasible in root node, trying with lower fixing rate\n"); @@ -878,12 +931,13 @@ void HighsPrimalHeuristics::RINS(const std::vector& relaxationsol) { goto retry; } - lp_iterations += heur.getLocalLpIterations(); + worker.heur_stats.lp_iterations += heur.getLocalLpIterations(); } -bool HighsPrimalHeuristics::tryRoundedPoint(const std::vector& point, +bool HighsPrimalHeuristics::tryRoundedPoint(HighsMipWorker& worker, + const std::vector& point, const int solution_source) { - auto localdom = mipsolver.mipdata_->domain; + HighsDomain localdom = worker.getGlobalDomain(); bool integerFeasible = true; HighsInt numintcols = intcols.size(); @@ -904,18 +958,23 @@ bool HighsPrimalHeuristics::tryRoundedPoint(const std::vector& point, localdom.fixCol(col, intval, HighsDomain::Reason::branching()); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); return false; } localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); return false; } } if (numintcols != mipsolver.numCol()) { HighsLpRelaxation lprelax(mipsolver); + lprelax.setMipWorker(worker); lprelax.loadModel(); lprelax.setIterationLimit( std::max(int64_t{10000}, 2 * mipsolver.mipdata_->firstrootlpiters)); @@ -939,33 +998,33 @@ bool HighsPrimalHeuristics::tryRoundedPoint(const std::vector& point, std::vector inds; std::vector vals; double rhs; - if (lprelax.computeDualInfProof(mipsolver.mipdata_->domain, inds, vals, + if (lprelax.computeDualInfProof(worker.getGlobalDomain(), inds, vals, rhs)) { - HighsCutGeneration cutGen(lprelax, mipsolver.mipdata_->cutpool); - cutGen.generateConflict(localdom, inds, vals, rhs); + HighsCutGeneration cutGen(lprelax, worker.getCutPool()); + cutGen.generateConflict(localdom, worker.getGlobalDomain(), inds, vals, + rhs); } return false; } else if (lprelax.unscaledPrimalFeasible(st)) { const auto& lpsol = lprelax.getLpSolver().getSolution().col_value; if (!integerFeasible) { // there may be fractional integer variables -> try ziRound heuristic - ziRound(lpsol); - return mipsolver.mipdata_->trySolution(lpsol, solution_source); + ziRound(worker, lpsol); + trySolution(lpsol, solution_source, worker); } else { // all integer variables are fixed -> add incumbent - mipsolver.mipdata_->addIncumbent(lpsol, lprelax.getObjective(), - solution_source); + addIncumbent(lpsol, lprelax.getObjective(), solution_source, worker); return true; } } } - return mipsolver.mipdata_->trySolution(localdom.col_lower_, solution_source); + return trySolution(localdom.col_lower_, solution_source, worker); } bool HighsPrimalHeuristics::linesearchRounding( - const std::vector& point1, const std::vector& point2, - const int solution_source) { + HighsMipWorker& worker, const std::vector& point1, + const std::vector& point2, const int solution_source) { std::vector roundedpoint; HighsInt numintcols = intcols.size(); @@ -1009,7 +1068,7 @@ bool HighsPrimalHeuristics::linesearchRounding( if (tmpalpha < nextalpha && tmpalpha > alpha + 1e-2) nextalpha = tmpalpha; } - if (tryRoundedPoint(roundedpoint, solution_source)) return true; + if (tryRoundedPoint(worker, roundedpoint, solution_source)) return true; if (reachedpoint2) return false; @@ -1020,10 +1079,12 @@ bool HighsPrimalHeuristics::linesearchRounding( } void HighsPrimalHeuristics::randomizedRounding( - const std::vector& relaxationsol) { + HighsMipWorker& worker, const std::vector& relaxationsol) { if (relaxationsol.size() != static_cast(mipsolver.numCol())) return; - auto localdom = mipsolver.mipdata_->domain; + HighsDomain localdom = worker.getGlobalDomain(); + HighsRandom& randgen = + mipsolver.mipdata_->parallelLockActive() ? worker.randgen : this->randgen; for (HighsInt i : intcols) { double intval; @@ -1039,12 +1100,16 @@ void HighsPrimalHeuristics::randomizedRounding( localdom.fixCol(i, intval, HighsDomain::Reason::branching()); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); return; } localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); return; } } @@ -1052,6 +1117,7 @@ void HighsPrimalHeuristics::randomizedRounding( if (mipsolver.mipdata_->integer_cols.size() != static_cast(mipsolver.numCol())) { HighsLpRelaxation lprelax(mipsolver); + lprelax.setMipWorker(worker); lprelax.loadModel(); lprelax.setIterationLimit( std::max(int64_t{10000}, 2 * mipsolver.mipdata_->firstrootlpiters)); @@ -1080,29 +1146,34 @@ void HighsPrimalHeuristics::randomizedRounding( std::vector inds; std::vector vals; double rhs; - if (lprelax.computeDualInfProof(mipsolver.mipdata_->domain, inds, vals, + if (lprelax.computeDualInfProof(worker.getGlobalDomain(), inds, vals, rhs)) { - HighsCutGeneration cutGen(lprelax, mipsolver.mipdata_->cutpool); - cutGen.generateConflict(localdom, inds, vals, rhs); + HighsCutGeneration cutGen(lprelax, worker.getCutPool()); + cutGen.generateConflict(localdom, worker.getGlobalDomain(), inds, vals, + rhs); } - } else if (lprelax.unscaledPrimalFeasible(st)) - mipsolver.mipdata_->addIncumbent( - lprelax.getLpSolver().getSolution().col_value, lprelax.getObjective(), - kSolutionSourceRandomizedRounding); + } else if (HighsLpRelaxation::unscaledPrimalFeasible(st)) { + addIncumbent(lprelax.getLpSolver().getSolution().col_value, + lprelax.getObjective(), kSolutionSourceRandomizedRounding, + worker); + } } else { - mipsolver.mipdata_->trySolution(localdom.col_lower_, - kSolutionSourceRandomizedRounding); + trySolution(localdom.col_lower_, kSolutionSourceRandomizedRounding, worker); } } -void HighsPrimalHeuristics::shifting(const std::vector& relaxationsol) { +void HighsPrimalHeuristics::shifting(HighsMipWorker& worker, + const std::vector& relaxationsol) { if (relaxationsol.size() != static_cast(mipsolver.numCol())) return; std::vector current_relax_solution = relaxationsol; HighsInt t = 0; const HighsLp& currentLp = *mipsolver.model_; - HighsLpRelaxation lprelax(mipsolver.mipdata_->lp); + HighsLpRelaxation lprelax(worker.getLpRelaxation()); + lprelax.setMipWorker(worker); + HighsRandom& randgen = + mipsolver.mipdata_->parallelLockActive() ? worker.randgen : this->randgen; std::vector> current_fractional_integers = lprelax.getFractionalIntegers(); std::vector> current_infeasible_rows = @@ -1341,17 +1412,18 @@ void HighsPrimalHeuristics::shifting(const std::vector& relaxationsol) { } // re-check for feasibility and add incumbent if (hasInfeasibleConstraints) { - tryRoundedPoint(current_relax_solution, kSolutionSourceShifting); + tryRoundedPoint(worker, current_relax_solution, kSolutionSourceShifting); } else { - if (current_fractional_integers.size() > 0) - ziRound(current_relax_solution); - else - mipsolver.mipdata_->trySolution(current_relax_solution, - kSolutionSourceShifting); + if (current_fractional_integers.size() > 0) { + ziRound(worker, current_relax_solution); + } else { + trySolution(current_relax_solution, kSolutionSourceShifting, worker); + } } } -void HighsPrimalHeuristics::ziRound(const std::vector& relaxationsol) { +void HighsPrimalHeuristics::ziRound(HighsMipWorker& worker, + const std::vector& relaxationsol) { // if (mipsolver.submip) return; if (relaxationsol.size() != static_cast(mipsolver.numCol())) return; @@ -1362,7 +1434,7 @@ void HighsPrimalHeuristics::ziRound(const std::vector& relaxationsol) { x - std::floor(x + mipsolver.mipdata_->feastol)); }; - // auto localdom = mipsolver.mipdata_->domain; + // auto localdom = mipsolver.mipdata_->getDomain(); HighsCDouble zi_total = 0.0; for (HighsInt i : intcols) { @@ -1460,17 +1532,20 @@ void HighsPrimalHeuristics::ziRound(const std::vector& relaxationsol) { improvement_in_feasibility = previous_zi_total - zi_total; } // re-check for feasibility and add incumbent - mipsolver.mipdata_->trySolution(current_relax_solution, - kSolutionSourceZiRound); + trySolution(current_relax_solution, kSolutionSourceZiRound, worker); } -void HighsPrimalHeuristics::feasibilityPump() { - HighsLpRelaxation lprelax(mipsolver.mipdata_->lp); +void HighsPrimalHeuristics::feasibilityPump(HighsMipWorker& worker) { + HighsLpRelaxation lprelax(worker.getLpRelaxation()); + lprelax.setMipWorker(worker); std::unordered_set, HighsVectorHasher, HighsVectorEqual> referencepoints; std::vector roundedsol; HighsLpRelaxation::Status status = lprelax.resolveLp(); - lp_iterations += lprelax.getNumLpIterations(); + worker.heur_stats.lp_iterations += lprelax.getNumLpIterations(); + + HighsRandom& randgen = + mipsolver.mipdata_->parallelLockActive() ? worker.randgen : this->randgen; std::vector fracintcost; std::vector fracintset; @@ -1493,7 +1568,7 @@ void HighsPrimalHeuristics::feasibilityPump() { std::vector referencepoint; referencepoint.reserve(mipsolver.mipdata_->integer_cols.size()); - auto localdom = mipsolver.mipdata_->domain; + HighsDomain localdom = worker.getGlobalDomain(); for (HighsInt i : mipsolver.mipdata_->integer_cols) { assert(mipsolver.isColInteger(i)); double intval = std::floor(roundedsol[i] + randgen.real(0.4, 0.6)); @@ -1504,12 +1579,16 @@ void HighsPrimalHeuristics::feasibilityPump() { if (!localdom.infeasible()) { localdom.fixCol(i, intval, HighsDomain::Reason::branching()); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); continue; } localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(worker.getConflictPool(), + worker.getGlobalDomain(), + worker.getPseudocost()); continue; } } @@ -1525,10 +1604,10 @@ void HighsPrimalHeuristics::feasibilityPump() { roundedsol[col] = (HighsInt)std::floor(lpsol[col]); else if (roundedsol[col] < lpsol[col]) roundedsol[col] = (HighsInt)std::ceil(lpsol[col]); - else if (roundedsol[col] < mipsolver.mipdata_->domain.col_upper_[col]) - roundedsol[col] = mipsolver.mipdata_->domain.col_upper_[col]; + else if (roundedsol[col] < worker.getGlobalDomain().col_upper_[col]) + roundedsol[col] = worker.getGlobalDomain().col_upper_[col]; else - roundedsol[col] = mipsolver.mipdata_->domain.col_lower_[col]; + roundedsol[col] = worker.getGlobalDomain().col_lower_[col]; referencepoint[flippos] = (HighsInt)roundedsol[col]; } @@ -1537,7 +1616,8 @@ void HighsPrimalHeuristics::feasibilityPump() { if (havecycle) return; - if (linesearchRounding(lpsol, roundedsol, kSolutionSourceFeasibilityPump)) + if (linesearchRounding(worker, lpsol, roundedsol, + kSolutionSourceFeasibilityPump)) return; if (lprelax.getNumLpIterations() >= @@ -1561,31 +1641,32 @@ void HighsPrimalHeuristics::feasibilityPump() { status = lprelax.resolveLp(); niters += lprelax.getNumLpIterations(); if (niters == 0) break; - lp_iterations += niters; + worker.heur_stats.lp_iterations += niters; } if (lprelax.getFractionalIntegers().empty() && - lprelax.unscaledPrimalFeasible(status)) - mipsolver.mipdata_->addIncumbent( - lprelax.getLpSolver().getSolution().col_value, lprelax.getObjective(), - kSolutionSourceFeasibilityPump); + HighsLpRelaxation::unscaledPrimalFeasible(status)) { + addIncumbent(lprelax.getLpSolver().getSolution().col_value, + lprelax.getObjective(), kSolutionSourceFeasibilityPump, + worker); + } } -void HighsPrimalHeuristics::centralRounding() { +void HighsPrimalHeuristics::centralRounding(HighsMipWorker& worker) { if (mipsolver.mipdata_->analyticCenter.size() != static_cast(mipsolver.numCol())) return; if (!mipsolver.mipdata_->firstlpsol.empty()) - linesearchRounding(mipsolver.mipdata_->firstlpsol, + linesearchRounding(worker, mipsolver.mipdata_->firstlpsol, mipsolver.mipdata_->analyticCenter, kSolutionSourceCentralRounding); else if (!mipsolver.mipdata_->rootlpsol.empty()) - linesearchRounding(mipsolver.mipdata_->rootlpsol, + linesearchRounding(worker, mipsolver.mipdata_->rootlpsol, mipsolver.mipdata_->analyticCenter, kSolutionSourceCentralRounding); else - linesearchRounding(mipsolver.mipdata_->analyticCenter, + linesearchRounding(worker, mipsolver.mipdata_->analyticCenter, mipsolver.mipdata_->analyticCenter, kSolutionSourceCentralRounding); } @@ -1595,7 +1676,7 @@ void HighsPrimalHeuristics::clique() { HighsHashTable entries; double offset = 0.0; - HighsDomain& globaldom = mipsolver.mipdata_->domain; + HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); for (HighsInt j = 0; j != mipsolver.numCol(); ++j) { HighsInt col = j; double val = mipsolver.colCost(col); @@ -1636,7 +1717,7 @@ void HighsPrimalHeuristics::clique() { HighsInt numcliques; cliques = mipsolver.mipdata_->cliquetable.separateCliques( - solution, mipsolver.mipdata_->domain, mipsolver.mipdata_->feastol); + solution, mipsolver.mipdata_->getDomain(), mipsolver.mipdata_->feastol); numcliques = cliques.size(); while (numcliques != 0) { bestviol = 0.5; @@ -1654,20 +1735,81 @@ void HighsPrimalHeuristics::clique() { } cliques = mipsolver.mipdata_->cliquetable.separateCliques( - solution, mipsolver.mipdata_->domain, mipsolver.mipdata_->feastol); + solution, mipsolver.mipdata_->getDomain(), mipsolver.mipdata_->feastol); numcliques = cliques.size(); } } #endif -void HighsPrimalHeuristics::flushStatistics() { - mipsolver.mipdata_->total_repair_lp += total_repair_lp; - mipsolver.mipdata_->total_repair_lp_feasible += total_repair_lp_feasible; - mipsolver.mipdata_->total_repair_lp_iterations += total_repair_lp_iterations; - total_repair_lp = 0; - total_repair_lp_feasible = 0; - total_repair_lp_iterations = 0; - mipsolver.mipdata_->heuristic_lp_iterations += lp_iterations; - mipsolver.mipdata_->total_lp_iterations += lp_iterations; - lp_iterations = 0; +bool HighsPrimalHeuristics::addIncumbent(const std::vector& sol, + double solobj, + const int solution_source, + HighsMipWorker& worker) { + if (mipsolver.mipdata_->parallelLockActive()) { + return worker.addIncumbent(sol, solobj, solution_source); + } else { + return mipsolver.mipdata_->addIncumbent(sol, solobj, solution_source); + } +} + +bool HighsPrimalHeuristics::trySolution(const std::vector& solution, + const int solution_source, + HighsMipWorker& worker) { + if (mipsolver.mipdata_->parallelLockActive()) { + return worker.trySolution(solution, solution_source); + } else { + return mipsolver.mipdata_->trySolution(solution, solution_source); + } +} + +HighsInt HighsPrimalHeuristics::getNumSuccessObservations( + HighsMipWorker& worker) const { + return numSuccessObservations + worker.heur_stats.numSuccessObservations; +} + +HighsInt HighsPrimalHeuristics::getNumInfeasObservations( + HighsMipWorker& worker) const { + return numInfeasObservations + worker.heur_stats.numInfeasObservations; +} + +double HighsPrimalHeuristics::getSuccessObservations( + HighsMipWorker& worker) const { + return successObservations + worker.heur_stats.successObservations; +} + +double HighsPrimalHeuristics::getInfeasObservations( + HighsMipWorker& worker) const { + return infeasObservations + worker.heur_stats.infeasObservations; +} + +void HighsPrimalHeuristics::flushStatistics(HighsMipSolver& mipsolver, + HighsMipWorker& worker) { + HighsMipWorker::HeurStatistics& heur_stats = worker.heur_stats; + mipsolver.mipdata_->total_repair_lp += heur_stats.total_repair_lp; + mipsolver.mipdata_->total_repair_lp_feasible += + heur_stats.total_repair_lp_feasible; + mipsolver.mipdata_->total_repair_lp_iterations += + heur_stats.total_repair_lp_iterations; + heur_stats.total_repair_lp = 0; + heur_stats.total_repair_lp_feasible = 0; + heur_stats.total_repair_lp_iterations = 0; + mipsolver.mipdata_->heuristic_lp_iterations += heur_stats.lp_iterations; + mipsolver.mipdata_->total_lp_iterations += worker.heur_stats.lp_iterations; + heur_stats.lp_iterations = 0; + mipsolver.max_submip_level = + std::max(mipsolver.max_submip_level, heur_stats.max_submip_level); + heur_stats.max_submip_level = 0; + if (heur_stats.termination_status_ != HighsModelStatus::kNotset && + mipsolver.termination_status_ == HighsModelStatus::kNotset) { + mipsolver.termination_status_ = heur_stats.termination_status_; + } + heur_stats.termination_status_ = HighsModelStatus::kNotset; + successObservations += heur_stats.successObservations; + heur_stats.successObservations = 0; + numSuccessObservations += heur_stats.numSuccessObservations; + heur_stats.numSuccessObservations = 0; + infeasObservations += heur_stats.infeasObservations; + heur_stats.infeasObservations = 0; + numInfeasObservations += heur_stats.numInfeasObservations; + heur_stats.numInfeasObservations = 0; } diff --git a/highs/mip/HighsPrimalHeuristics.h b/highs/mip/HighsPrimalHeuristics.h index 3d29f7b89c..34c989a1fb 100644 --- a/highs/mip/HighsPrimalHeuristics.h +++ b/highs/mip/HighsPrimalHeuristics.h @@ -15,16 +15,13 @@ #include "util/HighsRandom.h" class HighsMipSolver; +class HighsMipWorker; class HighsLpRelaxation; class HighsPrimalHeuristics { private: - HighsMipSolver& mipsolver; - size_t total_repair_lp; - size_t total_repair_lp_feasible; - size_t total_repair_lp_iterations; - size_t lp_iterations; - + const HighsMipSolver& mipsolver; + std::vector intcols; double successObservations; HighsInt numSuccessObservations; double infeasObservations; @@ -32,44 +29,64 @@ class HighsPrimalHeuristics { HighsRandom randgen; - std::vector intcols; - public: HighsPrimalHeuristics(HighsMipSolver& mipsolver); void setupIntCols(); - bool solveSubMip(const HighsLp& lp, const HighsBasis& basis, - double fixingRate, std::vector colLower, - std::vector colUpper, HighsInt maxleaves, - HighsInt maxnodes, HighsInt stallnodes); + bool solveSubMip(HighsMipWorker& worker, const HighsLp& lp, + const HighsBasis& basis, double fixingRate, + std::vector colLower, std::vector colUpper, + HighsInt maxleaves, HighsInt maxnodes, HighsInt stallnodes); - double determineTargetFixingRate(); + double determineTargetFixingRate(HighsMipWorker& worker); - void rootReducedCost(); + void rootReducedCost(HighsMipWorker& worker); - void RENS(const std::vector& relaxationsol); + void RENS(HighsMipWorker& worker, const std::vector& relaxationsol); - void RINS(const std::vector& relaxationsol); + void RINS(HighsMipWorker& worker, const std::vector& relaxationsol); - void feasibilityPump(); + void feasibilityPump(HighsMipWorker& worker); - void centralRounding(); + void centralRounding(HighsMipWorker& worker); - void flushStatistics(); + void flushStatistics(HighsMipSolver& mipsolver, HighsMipWorker& worker); - bool tryRoundedPoint(const std::vector& point, + bool tryRoundedPoint(HighsMipWorker& worker, const std::vector& point, const int solution_source); - bool linesearchRounding(const std::vector& point1, + bool linesearchRounding(HighsMipWorker& worker, + const std::vector& point1, const std::vector& point2, const int solution_source); - void randomizedRounding(const std::vector& relaxationsol); + void randomizedRounding(HighsMipWorker& worker, + const std::vector& relaxationsol); + + void shifting(HighsMipWorker& worker, + const std::vector& relaxationsol); + + void ziRound(HighsMipWorker& worker, + const std::vector& relaxationsol); + + bool addIncumbent(const std::vector& sol, double solobj, + const int solution_source, HighsMipWorker& worker); + + bool trySolution(const std::vector& solution, + const int solution_source, HighsMipWorker& worker); + + HighsInt getNumSuccessObservations(HighsMipWorker& worker) const; + + HighsInt getNumInfeasObservations(HighsMipWorker& worker) const; + + double getSuccessObservations(HighsMipWorker& worker) const; - void shifting(const std::vector& relaxationsol); + double getInfeasObservations(HighsMipWorker& worker) const; - void ziRound(const std::vector& relaxationsol); + HighsInt getHeuristicRandom(const HighsInt sup) { + return randgen.integer(sup); + } }; #endif diff --git a/highs/mip/HighsPseudocost.cpp b/highs/mip/HighsPseudocost.cpp index a702eed242..83ef34adf8 100644 --- a/highs/mip/HighsPseudocost.cpp +++ b/highs/mip/HighsPseudocost.cpp @@ -22,6 +22,7 @@ HighsPseudocost::HighsPseudocost(const HighsMipSolver& mipsolver) ncutoffsdown(mipsolver.numCol()), conflictscoreup(mipsolver.numCol()), conflictscoredown(mipsolver.numCol()), + changed(mipsolver.numCol()), conflict_weight(1.0), conflict_avg_score(0.0), cost_total(0), @@ -31,6 +32,7 @@ HighsPseudocost::HighsPseudocost(const HighsMipSolver& mipsolver) ncutoffstotal(0), minreliable(mipsolver.options_mip_->mip_pscost_minreliable), degeneracyFactor(1.0) { + indschanged.reserve(mipsolver.numCol()); if (mipsolver.pscostinit != nullptr) { cost_total = mipsolver.pscostinit->cost_total; inferences_total = mipsolver.pscostinit->inferences_total; diff --git a/highs/mip/HighsPseudocost.h b/highs/mip/HighsPseudocost.h index 342a4f66e1..bc1b2daf54 100644 --- a/highs/mip/HighsPseudocost.h +++ b/highs/mip/HighsPseudocost.h @@ -64,6 +64,8 @@ class HighsPseudocost { std::vector ncutoffsdown; std::vector conflictscoreup; std::vector conflictscoredown; + std::vector changed; + std::vector indschanged; double conflict_weight; double conflict_avg_score; @@ -111,11 +113,19 @@ class HighsPseudocost { void increaseConflictScoreUp(HighsInt col) { conflictscoreup[col] += conflict_weight; conflict_avg_score += conflict_weight; + if (!changed[col]) { + changed[col] = true; + indschanged.push_back(col); + } } void increaseConflictScoreDown(HighsInt col) { conflictscoredown[col] += conflict_weight; conflict_avg_score += conflict_weight; + if (!changed[col]) { + changed[col] = true; + indschanged.push_back(col); + } } void setMinReliable(HighsInt minreliable) { this->minreliable = minreliable; } @@ -138,6 +148,10 @@ class HighsPseudocost { ncutoffsup[col] += 1; else ncutoffsdown[col] += 1; + if (!changed[col]) { + changed[col] = true; + indschanged.push_back(col); + } } void addObservation(HighsInt col, double delta, double objdelta) { @@ -162,6 +176,10 @@ class HighsPseudocost { ++nsamplestotal; cost_total += d / static_cast(nsamplestotal); } + if (!changed[col]) { + changed[col] = true; + indschanged.push_back(col); + } } void addInferenceObservation(HighsInt col, HighsInt ninferences, @@ -178,6 +196,10 @@ class HighsPseudocost { ninferencesdown[col] += 1; inferencesdown[col] += d / ninferencesdown[col]; } + if (!changed[col]) { + changed[col] = true; + indschanged.push_back(col); + } } bool isReliable(HighsInt col) const { @@ -361,6 +383,153 @@ class HighsPseudocost { double getAvgInferencesDown(HighsInt col) const { return inferencesdown[col]; } + + std::vector getNSamplesUp() const { return nsamplesup; } + + std::vector getNSamplesDown() const { return nsamplesdown; } + + std::vector getNInferencesUp() const { return ninferencesup; } + + std::vector getNInferencesDown() const { return ninferencesdown; } + + std::vector getNCutoffsUp() const { return ncutoffsup; } + + std::vector getNCutoffsDown() const { return ncutoffsdown; } + + void flushPseudoCostObservations(double& curr_observation, + const double& new_observation, + const HighsInt n_new, const HighsInt n_prev, + const HighsInt n_curr, bool inference) { + const HighsInt n = n_new - n_prev; + if (n > 0) { + const double r = static_cast(n) / (n_curr + n); + const double average = (1 - r) * curr_observation + r * new_observation; + curr_observation = average; + if (inference) { + this->ninferencestotal += n; + } else { + this->nsamplestotal += n; + } + } + } + + void flushCutoffObservations(HighsInt& curr_observation, + const HighsInt& prev_observation, + const HighsInt& new_observation) { + HighsInt delta = new_observation - prev_observation; + curr_observation += delta; + this->ncutoffstotal += delta; + } + + void flushConflictObservations(double& curr_observation, + double new_observation, + double conflict_weight) { + double s = this->conflict_weight * + std::max(curr_observation / this->conflict_weight, + new_observation / conflict_weight); + if (s > curr_observation + minThreshold) { + this->conflict_avg_score += s - curr_observation; + } + curr_observation = s; + } + + void flushPseudoCost(HighsPseudocost& pseudocost, + std::vector& nsamplesup, + std::vector& nsamplesdown, + std::vector& ninferencesup, + std::vector& ninferencesdown, + std::vector& ncutoffsup, + std::vector& ncutoffsdown) { + int64_t orig_nsamplestotal = this->nsamplestotal; + int64_t orig_ninferencestotal = this->ninferencestotal; + assert(pseudocost.ncutoffsup.size() == ncutoffsup.size() && + pseudocost.ncutoffsup.size() == this->ncutoffsup.size()); + for (HighsInt col : pseudocost.indschanged) { + assert(col >= 0 && + col < static_cast(pseudocost.ncutoffsup.size())); + flushPseudoCostObservations(this->pseudocostup[col], + pseudocost.pseudocostup[col], nsamplesup[col], + pseudocost.nsamplesup[col], + this->nsamplesup[col], false); + flushPseudoCostObservations( + this->pseudocostdown[col], pseudocost.pseudocostdown[col], + nsamplesdown[col], pseudocost.nsamplesdown[col], + this->nsamplesdown[col], false); + flushPseudoCostObservations( + this->inferencesup[col], pseudocost.inferencesup[col], + ninferencesup[col], pseudocost.ninferencesup[col], + this->ninferencesup[col], true); + flushPseudoCostObservations( + this->inferencesdown[col], pseudocost.inferencesdown[col], + ninferencesdown[col], pseudocost.ninferencesdown[col], + this->ninferencesdown[col], true); + // Take the max conflict score (no way to guess num observations) + flushConflictObservations(this->conflictscoreup[col], + pseudocost.conflictscoreup[col], + pseudocost.conflict_weight); + flushConflictObservations(this->conflictscoredown[col], + pseudocost.conflictscoredown[col], + pseudocost.conflict_weight); + flushCutoffObservations(this->ncutoffsup[col], ncutoffsup[col], + pseudocost.ncutoffsup[col]); + flushCutoffObservations(this->ncutoffsdown[col], ncutoffsdown[col], + pseudocost.ncutoffsdown[col]); + pseudocost.changed[col] = false; + } + pseudocost.indschanged.clear(); + if (this->ninferencestotal > orig_ninferencestotal) { + const double r = static_cast(orig_ninferencestotal) / + static_cast(this->ninferencestotal); + this->inferences_total = + r * this->inferences_total + (1 - r) * pseudocost.inferences_total; + } + if (this->nsamplestotal > orig_nsamplestotal) { + const double r = static_cast(orig_nsamplestotal) / + static_cast(this->nsamplestotal); + this->cost_total = r * this->cost_total + (1 - r) * pseudocost.cost_total; + } + } + + void syncPseudoCost(HighsPseudocost& pseudocost) { + std::copy(pseudocostup.begin(), pseudocostup.end(), + pseudocost.pseudocostup.begin()); + std::copy(pseudocostdown.begin(), pseudocostdown.end(), + pseudocost.pseudocostdown.begin()); + std::copy(nsamplesup.begin(), nsamplesup.end(), + pseudocost.nsamplesup.begin()); + std::copy(nsamplesdown.begin(), nsamplesdown.end(), + pseudocost.nsamplesdown.begin()); + std::copy(inferencesup.begin(), inferencesup.end(), + pseudocost.inferencesup.begin()); + std::copy(inferencesdown.begin(), inferencesdown.end(), + pseudocost.inferencesdown.begin()); + std::copy(ninferencesup.begin(), ninferencesup.end(), + pseudocost.ninferencesup.begin()); + std::copy(ninferencesdown.begin(), ninferencesdown.end(), + pseudocost.ninferencesdown.begin()); + std::copy(ncutoffsup.begin(), ncutoffsup.end(), + pseudocost.ncutoffsup.begin()); + std::copy(ncutoffsdown.begin(), ncutoffsdown.end(), + pseudocost.ncutoffsdown.begin()); + std::copy(conflictscoreup.begin(), conflictscoreup.end(), + pseudocost.conflictscoreup.begin()); + std::copy(conflictscoredown.begin(), conflictscoredown.end(), + pseudocost.conflictscoredown.begin()); + pseudocost.conflict_weight = conflict_weight; + pseudocost.conflict_avg_score = conflict_avg_score; + pseudocost.cost_total = cost_total; + pseudocost.inferences_total = inferences_total; + pseudocost.nsamplestotal = nsamplestotal; + pseudocost.ninferencestotal = ninferencestotal; + pseudocost.ncutoffstotal = ncutoffstotal; + } + + void removeChanged() { + for (HighsInt col : indschanged) { + changed[col] = false; + } + indschanged.clear(); + } }; #endif diff --git a/highs/mip/HighsRedcostFixing.cpp b/highs/mip/HighsRedcostFixing.cpp index 3a5720e5b5..578ead6900 100644 --- a/highs/mip/HighsRedcostFixing.cpp +++ b/highs/mip/HighsRedcostFixing.cpp @@ -10,20 +10,21 @@ #include "mip/HighsMipSolverData.h" std::vector> -HighsRedcostFixing::getLurkingBounds(const HighsMipSolver& mipsolver) const { +HighsRedcostFixing::getLurkingBounds(const HighsMipSolver& mipsolver, + const HighsDomain& globaldom) const { std::vector> domchgs; if (lurkingColLower.empty()) return domchgs; for (HighsInt col : mipsolver.mipdata_->integral_cols) { for (const auto& lower : lurkingColLower[col]) { - if (lower.second > mipsolver.mipdata_->domain.col_lower_[col]) + if (lower.second > globaldom.col_lower_[col]) domchgs.emplace_back( lower.first, HighsDomainChange{static_cast(lower.second), col, HighsBoundType::kLower}); } for (const auto& upper : lurkingColUpper[col]) { - if (upper.second < mipsolver.mipdata_->domain.col_upper_[col]) + if (upper.second < globaldom.col_upper_[col]) domchgs.emplace_back( upper.first, HighsDomainChange{static_cast(upper.second), col, HighsBoundType::kUpper}); @@ -47,32 +48,34 @@ void HighsRedcostFixing::propagateRootRedcost(const HighsMipSolver& mipsolver) { for (auto it = lurkingColLower[col].lower_bound(mipsolver.mipdata_->upper_limit); it != lurkingColLower[col].end(); ++it) { - if (it->second > mipsolver.mipdata_->domain.col_lower_[col]) { - mipsolver.mipdata_->domain.changeBound( + if (it->second > mipsolver.mipdata_->getDomain().col_lower_[col]) { + mipsolver.mipdata_->getDomain().changeBound( HighsBoundType::kLower, col, (double)it->second, HighsDomain::Reason::unspecified()); - if (mipsolver.mipdata_->domain.infeasible()) return; + if (mipsolver.mipdata_->getDomain().infeasible()) return; } } for (auto it = lurkingColUpper[col].lower_bound(mipsolver.mipdata_->upper_limit); it != lurkingColUpper[col].end(); ++it) { - if (it->second < mipsolver.mipdata_->domain.col_upper_[col]) { - mipsolver.mipdata_->domain.changeBound( + if (it->second < mipsolver.mipdata_->getDomain().col_upper_[col]) { + mipsolver.mipdata_->getDomain().changeBound( HighsBoundType::kUpper, col, (double)it->second, HighsDomain::Reason::unspecified()); - if (mipsolver.mipdata_->domain.infeasible()) return; + if (mipsolver.mipdata_->getDomain().infeasible()) return; } } } - mipsolver.mipdata_->domain.propagate(); + mipsolver.mipdata_->getDomain().propagate(); } void HighsRedcostFixing::propagateRedCost(const HighsMipSolver& mipsolver, HighsDomain& localdomain, - const HighsLpRelaxation& lp) { + HighsDomain& globaldom, + const HighsLpRelaxation& lp, + HighsConflictPool& conflictpool) { const std::vector& lpredcost = lp.getSolution().col_dual; double lpobjective = lp.getObjective(); HighsCDouble gap = @@ -108,7 +111,7 @@ void HighsRedcostFixing::propagateRedCost(const HighsMipSolver& mipsolver, if (newub >= localdomain.col_upper_[col]) continue; assert(newub < localdomain.col_upper_[col]); - if (mipsolver.mipdata_->domain.isBinary(col)) { + if (globaldom.isBinary(col)) { boundChanges.emplace_back( HighsDomainChange{newub, col, HighsBoundType::kUpper}); } else { @@ -126,7 +129,7 @@ void HighsRedcostFixing::propagateRedCost(const HighsMipSolver& mipsolver, if (newlb <= localdomain.col_lower_[col]) continue; assert(newlb > localdomain.col_lower_[col]); - if (mipsolver.mipdata_->domain.isBinary(col)) { + if (globaldom.isBinary(col)) { boundChanges.emplace_back( HighsDomainChange{newlb, col, HighsBoundType::kLower}); } else { @@ -143,21 +146,18 @@ void HighsRedcostFixing::propagateRedCost(const HighsMipSolver& mipsolver, double rhs; if (boundChanges.size() <= 100 && - lp.computeDualProof(mipsolver.mipdata_->domain, - mipsolver.mipdata_->upper_limit, inds, vals, rhs, - false)) { + lp.computeDualProof(globaldom, mipsolver.mipdata_->upper_limit, inds, + vals, rhs, false)) { bool addedConstraints = false; - HighsInt oldNumConflicts = - mipsolver.mipdata_->conflictPool.getNumConflicts(); + HighsInt oldNumConflicts = conflictpool.getNumConflicts(); for (const HighsDomainChange& domchg : boundChanges) { if (localdomain.isActive(domchg)) continue; - localdomain.conflictAnalyzeReconvergence( - domchg, inds.data(), vals.data(), inds.size(), rhs, - mipsolver.mipdata_->conflictPool); + localdomain.conflictAnalyzeReconvergence(domchg, inds.data(), + vals.data(), inds.size(), rhs, + conflictpool, globaldom); } - addedConstraints = - mipsolver.mipdata_->conflictPool.getNumConflicts() != oldNumConflicts; + addedConstraints = conflictpool.getNumConflicts() != oldNumConflicts; if (addedConstraints) { localdomain.propagate(); @@ -197,8 +197,11 @@ void HighsRedcostFixing::addRootRedcost(const HighsMipSolver& mipsolver, lurkingColLower.resize(mipsolver.numCol()); lurkingColUpper.resize(mipsolver.numCol()); - mipsolver.mipdata_->lp.computeBasicDegenerateDuals( - mipsolver.mipdata_->feastol); + // Provided domains won't be used (only used for dual proof) + mipsolver.mipdata_->getLp().computeBasicDegenerateDuals( + mipsolver.mipdata_->feastol, mipsolver.mipdata_->getDomain(), + mipsolver.mipdata_->getDomain(), mipsolver.mipdata_->getConflictPool(), + false); // Compute maximum number of steps per column with large domain // max_steps = 2 ** k, k = max(5, min(10 ,round(log(|D| / 10)))), @@ -206,8 +209,8 @@ void HighsRedcostFixing::addRootRedcost(const HighsMipSolver& mipsolver, // This is to avoid doing 2**10 steps when there's many unbounded columns HighsInt numRedcostLargeDomainCols = 0; for (HighsInt col : mipsolver.mipdata_->integral_cols) { - if ((mipsolver.mipdata_->domain.col_upper_[col] - - mipsolver.mipdata_->domain.col_lower_[col]) >= 512 && + if ((mipsolver.mipdata_->getDomain().col_upper_[col] - + mipsolver.mipdata_->getDomain().col_lower_[col]) >= 512 && std::abs(lpredcost[col]) > mipsolver.mipdata_->feastol) { numRedcostLargeDomainCols++; } @@ -293,11 +296,13 @@ void HighsRedcostFixing::addRootRedcost(const HighsMipSolver& mipsolver, // cutoffbound = (lurkub - lb) * redcost + lpobj findLurkingBounds( col, HighsInt{1}, - static_cast(mipsolver.mipdata_->domain.col_lower_[col]), - static_cast(mipsolver.mipdata_->domain.col_upper_[col]), - mipsolver.mipdata_->domain.col_upper_[col] != kHighsInf, lpobjective, - lpredcost[col], maxNumSteps, maxNumStepsExp, lurkingColUpper[col], - lurkingColLower[col]); + static_cast( + mipsolver.mipdata_->getDomain().col_lower_[col]), + static_cast( + mipsolver.mipdata_->getDomain().col_upper_[col]), + mipsolver.mipdata_->getDomain().col_upper_[col] != kHighsInf, + lpobjective, lpredcost[col], maxNumSteps, maxNumStepsExp, + lurkingColUpper[col], lurkingColLower[col]); } else if (lpredcost[col] < -mipsolver.mipdata_->feastol) { // col >= (cutoffbound - lpobj)/redcost + ub // so for lurklb = lb + 1 to ub we can compute the necessary cutoff @@ -306,11 +311,13 @@ void HighsRedcostFixing::addRootRedcost(const HighsMipSolver& mipsolver, // cutoffbound = (lurklb - ub) * redcost + lpobj findLurkingBounds( col, HighsInt{-1}, - static_cast(mipsolver.mipdata_->domain.col_upper_[col]), - static_cast(mipsolver.mipdata_->domain.col_lower_[col]), - mipsolver.mipdata_->domain.col_lower_[col] != -kHighsInf, lpobjective, - lpredcost[col], maxNumSteps, maxNumStepsExp, lurkingColLower[col], - lurkingColUpper[col]); + static_cast( + mipsolver.mipdata_->getDomain().col_upper_[col]), + static_cast( + mipsolver.mipdata_->getDomain().col_lower_[col]), + mipsolver.mipdata_->getDomain().col_lower_[col] != -kHighsInf, + lpobjective, lpredcost[col], maxNumSteps, maxNumStepsExp, + lurkingColLower[col], lurkingColUpper[col]); } } } diff --git a/highs/mip/HighsRedcostFixing.h b/highs/mip/HighsRedcostFixing.h index 4e069b9134..9ab5f12f16 100644 --- a/highs/mip/HighsRedcostFixing.h +++ b/highs/mip/HighsRedcostFixing.h @@ -15,6 +15,7 @@ #include #include +#include "HighsConflictPool.h" #include "mip/HighsDomainChange.h" class HighsDomain; @@ -27,13 +28,14 @@ class HighsRedcostFixing { public: std::vector> getLurkingBounds( - const HighsMipSolver& mipsolver) const; + const HighsMipSolver& mipsolver, const HighsDomain& globaldom) const; void propagateRootRedcost(const HighsMipSolver& mipsolver); static void propagateRedCost(const HighsMipSolver& mipsolver, - HighsDomain& localdomain, - const HighsLpRelaxation& lp); + HighsDomain& localdomain, HighsDomain& globaldom, + const HighsLpRelaxation& lp, + HighsConflictPool& conflictpool); void addRootRedcost(const HighsMipSolver& mipsolver, const std::vector& lpredcost, double lpobjective); diff --git a/highs/mip/HighsSearch.cpp b/highs/mip/HighsSearch.cpp index e1db055515..2659bde497 100644 --- a/highs/mip/HighsSearch.cpp +++ b/highs/mip/HighsSearch.cpp @@ -14,12 +14,14 @@ #include "mip/HighsDomainChange.h" #include "mip/HighsMipSolverData.h" -HighsSearch::HighsSearch(HighsMipSolver& mipsolver, HighsPseudocost& pseudocost) - : mipsolver(mipsolver), +HighsSearch::HighsSearch(HighsMipWorker& mipworker, HighsPseudocost& pseudocost) + : mipworker(mipworker), + mipsolver(mipworker.mipsolver_), lp(nullptr), - localdom(mipsolver.mipdata_->domain), + localdom(mipworker.getGlobalDomain()), pseudocost(pseudocost) { nnodes = 0; + nleaves = 0; treeweight = 0.0; depthoffset = 0; lpiterations = 0; @@ -47,7 +49,7 @@ double HighsSearch::checkSol(const std::vector& sol, if (!integerfeasible || !mipsolver.isColInteger(i)) continue; - if (fractionality(sol[i]) > mipsolver.mipdata_->feastol) { + if (fractionality(sol[i]) > getFeasTol()) { integerfeasible = false; } } @@ -75,7 +77,7 @@ bool HighsSearch::orbitsValidInChildNode( } double HighsSearch::getCutoffBound() const { - return std::min(mipsolver.mipdata_->upper_limit, upper_limit); + return std::min(getUpperLimit(), upper_limit); } void HighsSearch::setRINSNeighbourhood(const std::vector& basesol, @@ -85,7 +87,7 @@ void HighsSearch::setRINSNeighbourhood(const std::vector& basesol, if (localdom.col_lower_[i] == localdom.col_upper_[i]) continue; double intval = std::floor(basesol[i] + 0.5); - if (std::abs(relaxsol[i] - intval) < mipsolver.mipdata_->feastol) { + if (std::abs(relaxsol[i] - intval) < getFeasTol()) { if (localdom.col_lower_[i] < intval) localdom.changeBound(HighsBoundType::kLower, i, std::min(intval, localdom.col_upper_[i]), @@ -103,8 +105,8 @@ void HighsSearch::setRENSNeighbourhood(const std::vector& lpsol) { if (!mipsolver.isColInteger(i)) continue; if (localdom.col_lower_[i] == localdom.col_upper_[i]) continue; - double downval = std::floor(lpsol[i] + mipsolver.mipdata_->feastol); - double upval = std::ceil(lpsol[i] - mipsolver.mipdata_->feastol); + double downval = std::floor(lpsol[i] + getFeasTol()); + double upval = std::ceil(lpsol[i] - getFeasTol()); if (localdom.col_lower_[i] < downval) { localdom.changeBound(HighsBoundType::kLower, i, @@ -179,19 +181,19 @@ void HighsSearch::branchUpwards(HighsInt col, double newlb, } void HighsSearch::addBoundExceedingConflict() { - if (mipsolver.mipdata_->upper_limit != kHighsInf) { + if (getUpperLimit() != kHighsInf) { double rhs; - if (lp->computeDualProof(mipsolver.mipdata_->domain, - mipsolver.mipdata_->upper_limit, inds, vals, - rhs)) { - if (mipsolver.mipdata_->domain.infeasible()) return; + if (lp->computeDualProof(getDomain(), getUpperLimit(), inds, vals, rhs)) { + if (getDomain().infeasible()) return; localdom.conflictAnalysis(inds.data(), vals.data(), inds.size(), rhs, - mipsolver.mipdata_->conflictPool); + getConflictPool(), mipworker.getGlobalDomain(), + pseudocost); - HighsCutGeneration cutGen(*lp, mipsolver.mipdata_->cutpool); + HighsCutGeneration cutGen(*lp, getCutPool()); mipsolver.mipdata_->debugSolution.checkCut(inds.data(), vals.data(), inds.size(), rhs); - cutGen.generateConflict(localdom, inds, vals, rhs); + cutGen.generateConflict(localdom, mipworker.getGlobalDomain(), inds, vals, + rhs); } } } @@ -201,8 +203,8 @@ void HighsSearch::addInfeasibleConflict() { if (lp->getLpSolver().getModelStatus() == HighsModelStatus::kObjectiveBound) lp->performAging(); - if (lp->computeDualInfProof(mipsolver.mipdata_->domain, inds, vals, rhs)) { - if (mipsolver.mipdata_->domain.infeasible()) return; + if (lp->computeDualInfProof(getDomain(), inds, vals, rhs)) { + if (getDomain().infeasible()) return; // double minactlocal = 0.0; // double minactglobal = 0.0; // for (HighsInt i = 0; i < int(inds.size()); ++i) { @@ -216,12 +218,14 @@ void HighsSearch::addInfeasibleConflict() { //} // HighsInt oldnumcuts = cutpool.getNumCuts(); localdom.conflictAnalysis(inds.data(), vals.data(), inds.size(), rhs, - mipsolver.mipdata_->conflictPool); + getConflictPool(), mipworker.getGlobalDomain(), + pseudocost); - HighsCutGeneration cutGen(*lp, mipsolver.mipdata_->cutpool); + HighsCutGeneration cutGen(*lp, getCutPool()); mipsolver.mipdata_->debugSolution.checkCut(inds.data(), vals.data(), inds.size(), rhs); - cutGen.generateConflict(localdom, inds, vals, rhs); + cutGen.generateConflict(localdom, mipworker.getGlobalDomain(), inds, vals, + rhs); // if (cutpool.getNumCuts() > oldnumcuts) { // printf( @@ -270,38 +274,34 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, double fracval = fracints[k].second; const double lower_residual = - (fracval - localdom.col_lower_[col]) - mipsolver.mipdata_->feastol; + (fracval - localdom.col_lower_[col]) - getFeasTol(); const bool lower_ok = lower_residual > 0; if (!lower_ok) highsLogUser(mipsolver.options_mip_->log_options, HighsLogType::kError, "HighsSearch::selectBranchingCandidate Error fracval = %g " "<= %g = %g + %g = " - "localdom.col_lower_[col] + mipsolver.mipdata_->feastol: " + "localdom.col_lower_[col] + getFeasTol(): " "Residual %g\n", - fracval, - localdom.col_lower_[col] + mipsolver.mipdata_->feastol, - localdom.col_lower_[col], mipsolver.mipdata_->feastol, - lower_residual); + fracval, localdom.col_lower_[col] + getFeasTol(), + localdom.col_lower_[col], getFeasTol(), lower_residual); const double upper_residual = - (localdom.col_upper_[col] - fracval) - mipsolver.mipdata_->feastol; + (localdom.col_upper_[col] - fracval) - getFeasTol(); const bool upper_ok = upper_residual > 0; if (!upper_ok) highsLogUser(mipsolver.options_mip_->log_options, HighsLogType::kError, "HighsSearch::selectBranchingCandidate Error fracval = %g " ">= %g = %g - %g = " - "localdom.col_upper_[col] - mipsolver.mipdata_->feastol: " + "localdom.col_upper_[col] - getFeasTol(): " "Residual %g\n", - fracval, - localdom.col_upper_[col] - mipsolver.mipdata_->feastol, - localdom.col_upper_[col], mipsolver.mipdata_->feastol, - upper_residual); + fracval, localdom.col_upper_[col] - getFeasTol(), + localdom.col_upper_[col], getFeasTol(), upper_residual); assert(lower_residual > -1e-12 && upper_residual > -1e-12); // assert(fracval > localdom.col_lower_[col] + - // mipsolver.mipdata_->feastol); assert(fracval < - // localdom.col_upper_[col] - mipsolver.mipdata_->feastol); + // getFeasTol()); assert(fracval < + // localdom.col_upper_[col] - getFeasTol()); if (pseudocost.isReliable(col)) { upscore[k] = pseudocost.getPseudocostUp(col, fracval); @@ -327,14 +327,14 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, std::iota(evalqueue.begin(), evalqueue.end(), 0); auto numNodesUp = [&](HighsInt k) { - return mipsolver.mipdata_->nodequeue.numNodesUp(fracints[k].first); + return getNodeQueue().numNodesUp(fracints[k].first); }; auto numNodesDown = [&](HighsInt k) { - return mipsolver.mipdata_->nodequeue.numNodesDown(fracints[k].first); + return getNodeQueue().numNodesDown(fracints[k].first); }; - double minScore = mipsolver.mipdata_->feastol; + double minScore = getFeasTol(); auto selectBestScore = [&](bool finalSelection) { HighsInt best = -1; @@ -374,10 +374,9 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, if (upnodes != 0 || downnodes != 0) nodes = (downnodes / (double)(numnodes)) * (upnodes / (double)(numnodes)); - if (score > bestscore || - (score > bestscore - mipsolver.mipdata_->feastol && - std::make_pair(nodes, numnodes) > - std::make_pair(bestnodes, bestnumnodes))) { + if (score > bestscore || (score > bestscore - getFeasTol() && + std::make_pair(nodes, numnodes) > + std::make_pair(bestnodes, bestnumnodes))) { bestscore = score; best = k; bestnodes = nodes; @@ -391,8 +390,8 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, HighsLpRelaxation::Playground playground = lp->playground(); while (true) { - bool mustStop = getStrongBranchingLpIterations() >= maxSbIters || - mipsolver.mipdata_->checkLimits(); + bool mustStop = + getStrongBranchingLpIterations() >= maxSbIters || checkLimits(); HighsInt candidate = selectBestScore(mustStop); @@ -403,7 +402,7 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, return candidate; } - lp->setObjectiveLimit(mipsolver.mipdata_->upper_limit); + lp->setObjectiveLimit(getUpperLimit()); HighsInt col = fracints[candidate].first; double fracval = fracints[candidate].second; @@ -421,21 +420,22 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, double otherfracval = fracints[k].second; double otherdownval = std::floor(fracints[k].second); double otherupval = std::ceil(fracints[k].second); - if (sol[fracints[k].first] <= - otherdownval + mipsolver.mipdata_->feastol) { + if (sol[fracints[k].first] <= otherdownval + getFeasTol()) { if (localdom.col_upper_[fracints[k].first] > - otherdownval + mipsolver.mipdata_->feastol) { + otherdownval + getFeasTol()) { localdom.changeBound(HighsBoundType::kUpper, fracints[k].first, otherdownval); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis( + getConflictPool(), mipworker.getGlobalDomain(), pseudocost); localdom.backtrack(); localdom.clearChangedCols(numChangedCols); continue; } localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis( + getConflictPool(), mipworker.getGlobalDomain(), pseudocost); localdom.backtrack(); localdom.clearChangedCols(numChangedCols); continue; @@ -447,13 +447,13 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, for (HighsInt j = domchgStackSize + 1; j < newStackSize; ++j) { if (domchgstack[j].boundtype == HighsBoundType::kLower) { if (domchgstack[j].boundval > - sol[domchgstack[j].column] + mipsolver.mipdata_->feastol) { + sol[domchgstack[j].column] + getFeasTol()) { solutionValid = false; break; } } else { if (domchgstack[j].boundval < - sol[domchgstack[j].column] - mipsolver.mipdata_->feastol) { + sol[domchgstack[j].column] - getFeasTol()) { solutionValid = false; break; } @@ -465,29 +465,30 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, if (!solutionValid) continue; } - if (objdelta <= mipsolver.mipdata_->feastol) { + if (objdelta <= getFeasTol()) { pseudocost.addObservation(fracints[k].first, otherdownval - otherfracval, objdelta); markBranchingVarDownReliableAtNode(fracints[k].first); } downscore[k] = std::min(downscore[k], objdelta); - } else if (sol[fracints[k].first] >= - otherupval - mipsolver.mipdata_->feastol) { + } else if (sol[fracints[k].first] >= otherupval - getFeasTol()) { if (localdom.col_lower_[fracints[k].first] < - otherupval - mipsolver.mipdata_->feastol) { + otherupval - getFeasTol()) { localdom.changeBound(HighsBoundType::kLower, fracints[k].first, otherupval); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis( + getConflictPool(), mipworker.getGlobalDomain(), pseudocost); localdom.backtrack(); localdom.clearChangedCols(numChangedCols); continue; } localdom.propagate(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis( + getConflictPool(), mipworker.getGlobalDomain(), pseudocost); localdom.backtrack(); localdom.clearChangedCols(numChangedCols); continue; @@ -499,13 +500,13 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, for (HighsInt j = domchgStackSize + 1; j < newStackSize; ++j) { if (domchgstack[j].boundtype == HighsBoundType::kLower) { if (domchgstack[j].boundval > - sol[domchgstack[j].column] + mipsolver.mipdata_->feastol) { + sol[domchgstack[j].column] + getFeasTol()) { solutionValid = false; break; } } else { if (domchgstack[j].boundval < - sol[domchgstack[j].column] - mipsolver.mipdata_->feastol) { + sol[domchgstack[j].column] - getFeasTol()) { solutionValid = false; break; } @@ -518,7 +519,7 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, if (!solutionValid) continue; } - if (objdelta <= mipsolver.mipdata_->feastol) { + if (objdelta <= getFeasTol()) { pseudocost.addObservation(fracints[k].first, otherupval - otherfracval, objdelta); markBranchingVarUpReliableAtNode(fracints[k].first); @@ -545,12 +546,13 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, if (orbitalFixing) nodestack.back().stabilizerOrbits->orbitalFixing(localdom); else - mipsolver.mipdata_->symmetries.propagateOrbitopes(localdom); + getSymmetries().propagateOrbitopes(localdom); } inferences += localdom.getDomainChangeStack().size(); if (localdom.infeasible()) { - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(getConflictPool(), + mipworker.getGlobalDomain(), pseudocost); pseudocost.addCutoffObservation(col, upbranch); localdom.backtrack(); localdom.clearChangedCols(); @@ -584,7 +586,7 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, double solobj = checkSol(sol, integerfeasible); double objdelta = std::max(solobj - lp->getObjective(), 0.0); - if (objdelta <= mipsolver.mipdata_->epsilon) objdelta = 0.0; + if (objdelta <= getEpsilon()) objdelta = 0.0; if (upbranch) { upscore[candidate] = objdelta; @@ -600,13 +602,12 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, if (lp->unscaledPrimalFeasible(status) && integerfeasible) { double cutoffbnd = getCutoffBound(); - mipsolver.mipdata_->addIncumbent( - lp->getLpSolver().getSolution().col_value, solobj, - inheuristic ? kSolutionSourceHeuristic - : kSolutionSourceBranching); + addIncumbent(lp->getLpSolver().getSolution().col_value, solobj, + inheuristic ? kSolutionSourceHeuristic + : kSolutionSourceBranching); - if (mipsolver.mipdata_->upper_limit < cutoffbnd) - lp->setObjectiveLimit(mipsolver.mipdata_->upper_limit); + if (getUpperLimit() < cutoffbnd) + lp->setObjectiveLimit(getUpperLimit()); } if (lp->unscaledDualFeasible(status)) { @@ -615,7 +616,7 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, } else { downbound[candidate] = solobj; } - if (solobj > mipsolver.mipdata_->optimality_limit) { + if (solobj > getOptimalityLimit()) { addBoundExceedingConflict(); bool pruned = solobj > getCutoffBound(); @@ -722,7 +723,9 @@ void HighsSearch::currentNodeToQueue(HighsNodeQueue& nodequeue) { localdom.propagate(); localdom.clearChangedCols(oldchangedcols); prune = localdom.infeasible(); - if (prune) localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + if (prune) + localdom.conflictAnalysis(getConflictPool(), mipworker.getGlobalDomain(), + pseudocost); } if (!prune) { std::vector branchPositions; @@ -761,7 +764,9 @@ void HighsSearch::openNodesToQueue(HighsNodeQueue& nodequeue) { localdom.propagate(); localdom.clearChangedCols(oldchangedcols); prune = localdom.infeasible(); - if (prune) localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + if (prune) + localdom.conflictAnalysis(getConflictPool(), + mipworker.getGlobalDomain(), pseudocost); } if (!prune) { std::vector branchPositions; @@ -789,19 +794,22 @@ void HighsSearch::openNodesToQueue(HighsNodeQueue& nodequeue) { } void HighsSearch::flushStatistics() { - mipsolver.mipdata_->num_nodes += nnodes; + getNumNodes() += nnodes; nnodes = 0; - mipsolver.mipdata_->pruned_treeweight += treeweight; + getNumLeaves() += nleaves; + nleaves = 0; + + getPrunedTreeweight() += treeweight; treeweight = 0; - mipsolver.mipdata_->total_lp_iterations += lpiterations; + getTotalLpIterations() += lpiterations; lpiterations = 0; - mipsolver.mipdata_->heuristic_lp_iterations += heurlpiterations; + getHeuristicLpIterations() += heurlpiterations; heurlpiterations = 0; - mipsolver.mipdata_->sb_lp_iterations += sblpiterations; + getSbLpIterations() += sblpiterations; sblpiterations = 0; } @@ -815,15 +823,17 @@ int64_t HighsSearch::getTotalLpIterations() const { int64_t HighsSearch::getLocalLpIterations() const { return lpiterations; } -int64_t HighsSearch::getLocalNodes() const { return nnodes; } +int64_t& HighsSearch::getLocalNodes() { return nnodes; } + +int64_t& HighsSearch::getLocalLeaves() { return nleaves; } int64_t HighsSearch::getStrongBranchingLpIterations() const { - return sblpiterations + mipsolver.mipdata_->sb_lp_iterations; + return sblpiterations + getSbLpIterations(); } void HighsSearch::resetLocalDomain() { - this->lp->resetToGlobalDomain(); - localdom = mipsolver.mipdata_->domain; + this->lp->resetToGlobalDomain(getDomain()); + localdom = getDomain(); #ifndef NDEBUG for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { @@ -844,9 +854,9 @@ void HighsSearch::installNode(HighsNodeQueue::OpenNode&& node) { const auto& domchgstack = localdom.getDomainChangeStack(); for (HighsInt i : localdom.getBranchingPositions()) { HighsInt col = domchgstack[i].column; - if (mipsolver.mipdata_->symmetries.columnPosition[col] == -1) continue; + if (getSymmetries().columnPosition[col] == -1) continue; - if (!mipsolver.mipdata_->domain.isBinary(col) || + if (!getDomain().isBinary(col) || (domchgstack[i].boundtype == HighsBoundType::kLower && domchgstack[i].boundval == 1.0)) { globalSymmetriesValid = false; @@ -868,25 +878,24 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { const auto& domchgstack = localdom.getDomainChangeStack(); - if (!inheuristic && - currnode.lower_bound > mipsolver.mipdata_->optimality_limit) + if (!inheuristic && currnode.lower_bound > getOptimalityLimit()) return NodeResult::kSubOptimal; localdom.propagate(); if (!inheuristic && !localdom.infeasible()) { - if (mipsolver.mipdata_->symmetries.numPerms > 0 && - !currnode.stabilizerOrbits && + if (getSymmetries().numPerms > 0 && !currnode.stabilizerOrbits && (parent == nullptr || !parent->stabilizerOrbits || - !parent->stabilizerOrbits->orbitCols.empty())) { + !parent->stabilizerOrbits->orbitCols.empty()) && + !mipsolver.mipdata_->parallelLockActive()) { currnode.stabilizerOrbits = - mipsolver.mipdata_->symmetries.computeStabilizerOrbits(localdom); + getSymmetries().computeStabilizerOrbits(localdom); } if (currnode.stabilizerOrbits) currnode.stabilizerOrbits->orbitalFixing(localdom); else - mipsolver.mipdata_->symmetries.propagateOrbitopes(localdom); + getSymmetries().propagateOrbitopes(localdom); } if (parent != nullptr) { int64_t inferences = domchgstack.size() - (currnode.domgchgStackPos + 1); @@ -909,10 +918,11 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { upbranch); } - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(getConflictPool(), mipworker.getGlobalDomain(), + pseudocost); } else { lp->flushDomain(localdom); - lp->setObjectiveLimit(mipsolver.mipdata_->upper_limit); + lp->setObjectiveLimit(getUpperLimit()); #ifndef NDEBUG for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { @@ -942,7 +952,8 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { upbranch); } - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis(getConflictPool(), mipworker.getGlobalDomain(), + pseudocost); } else if (lp->scaledOptimal(status)) { lp->storeBasis(); lp->performAging(); @@ -965,12 +976,12 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { if (lp->unscaledPrimalFeasible(status)) { if (lp->getFractionalIntegers().empty()) { double cutoffbnd = getCutoffBound(); - mipsolver.mipdata_->addIncumbent( - lp->getLpSolver().getSolution().col_value, lp->getObjective(), - inheuristic ? kSolutionSourceHeuristic - : kSolutionSourceEvaluateNode); - if (mipsolver.mipdata_->upper_limit < cutoffbnd) - lp->setObjectiveLimit(mipsolver.mipdata_->upper_limit); + addIncumbent(lp->getLpSolver().getSolution().col_value, + lp->getObjective(), + inheuristic ? kSolutionSourceHeuristic + : kSolutionSourceEvaluateNode); + if (getUpperLimit() < cutoffbnd) + lp->setObjectiveLimit(getUpperLimit()); if (lp->unscaledDualFeasible(status)) { addBoundExceedingConflict(); @@ -987,15 +998,16 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { if (currnode.lower_bound > getCutoffBound()) { result = NodeResult::kBoundExceeding; addBoundExceedingConflict(); - } else if (mipsolver.mipdata_->upper_limit != kHighsInf) { + } else if (getUpperLimit() != kHighsInf) { if (!inheuristic) { - double gap = mipsolver.mipdata_->upper_limit - lp->getObjective(); + double gap = getUpperLimit() - lp->getObjective(); lp->computeBasicDegenerateDuals( - gap + std::max(10 * mipsolver.mipdata_->feastol, - mipsolver.mipdata_->epsilon * gap), - &localdom); + gap + std::max(10 * getFeasTol(), getEpsilon() * gap), + localdom, getDomain(), getConflictPool(), true); } - HighsRedcostFixing::propagateRedCost(mipsolver, localdom, *lp); + HighsRedcostFixing::propagateRedCost(mipsolver, localdom, + mipworker.getGlobalDomain(), + *lp, getConflictPool()); localdom.propagate(); if (localdom.infeasible()) { result = NodeResult::kDomainInfeasible; @@ -1009,13 +1021,15 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { parent->branchingdecision.column, upbranch); } - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis( + getConflictPool(), mipworker.getGlobalDomain(), pseudocost); } else if (!localdom.getChangedCols().empty()) { return evaluateNode(); } } else { if (!inheuristic) { - lp->computeBasicDegenerateDuals(kHighsInf, &localdom); + lp->computeBasicDegenerateDuals(kHighsInf, localdom, getDomain(), + getConflictPool(), true); localdom.propagate(); if (localdom.infeasible()) { result = NodeResult::kDomainInfeasible; @@ -1029,7 +1043,8 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { parent->branchingdecision.column, upbranch); } - localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + localdom.conflictAnalysis( + getConflictPool(), mipworker.getGlobalDomain(), pseudocost); } else if (!localdom.getChangedCols().empty()) { return evaluateNode(); } @@ -1071,7 +1086,7 @@ HighsSearch::NodeResult HighsSearch::evaluateNode() { treeweight += std::ldexp(1.0, 1 - getCurrentDepth()); currnode.opensubtrees = 0; } else if (!inheuristic) { - if (currnode.lower_bound > mipsolver.mipdata_->optimality_limit) { + if (currnode.lower_bound > getOptimalityLimit()) { result = NodeResult::kSubOptimal; addBoundExceedingConflict(); } @@ -1148,10 +1163,8 @@ HighsSearch::NodeResult HighsSearch::branch() { childLb = downNodeLb; break; case ChildSelectionRule::kRootSol: { - double downPrio = pseudocost.getAvgInferencesDown(col) + - mipsolver.mipdata_->epsilon; - double upPrio = - pseudocost.getAvgInferencesUp(col) + mipsolver.mipdata_->epsilon; + double downPrio = pseudocost.getAvgInferencesDown(col) + getEpsilon(); + double upPrio = pseudocost.getAvgInferencesUp(col) + getEpsilon(); double downVal = std::floor(currnode.branching_point); double upVal = std::ceil(currnode.branching_point); if (!subrootsol.empty()) { @@ -1167,8 +1180,8 @@ HighsSearch::NodeResult HighsSearch::branch() { } else { if (currnode.lp_objective != -kHighsInf) subrootsol = lp->getSolution().col_value; - if (!mipsolver.mipdata_->rootlpsol.empty()) { - double rootsol = mipsolver.mipdata_->rootlpsol[col]; + if (!getRootLpSol().empty()) { + double rootsol = getRootLpSol()[col]; if (rootsol < downVal) rootsol = downVal; else if (rootsol > upVal) @@ -1178,7 +1191,7 @@ HighsSearch::NodeResult HighsSearch::branch() { downPrio *= (1.0 + (rootsol - currnode.branching_point)); } } - if (upPrio + mipsolver.mipdata_->epsilon >= downPrio) { + if (upPrio + getEpsilon() >= downPrio) { currnode.branchingdecision.boundtype = HighsBoundType::kLower; currnode.branchingdecision.boundval = upVal; currnode.other_child_lb = downNodeLb; @@ -1223,9 +1236,9 @@ HighsSearch::NodeResult HighsSearch::branch() { break; case ChildSelectionRule::kBestCost: { if (pseudocost.getPseudocostUp(col, currnode.branching_point, - mipsolver.mipdata_->feastol) > + getFeasTol()) > pseudocost.getPseudocostDown(col, currnode.branching_point, - mipsolver.mipdata_->feastol)) { + getFeasTol())) { currnode.branchingdecision.boundtype = HighsBoundType::kUpper; currnode.branchingdecision.boundval = std::floor(currnode.branching_point); @@ -1259,8 +1272,8 @@ HighsSearch::NodeResult HighsSearch::branch() { case ChildSelectionRule::kDisjunction: { int64_t numnodesup; int64_t numnodesdown; - numnodesup = mipsolver.mipdata_->nodequeue.numNodesUp(col); - numnodesdown = mipsolver.mipdata_->nodequeue.numNodesDown(col); + numnodesup = getNodeQueue().numNodesUp(col); + numnodesdown = getNodeQueue().numNodesDown(col); if (numnodesup > numnodesdown) { currnode.branchingdecision.boundtype = HighsBoundType::kLower; currnode.branchingdecision.boundval = @@ -1293,14 +1306,12 @@ HighsSearch::NodeResult HighsSearch::branch() { case ChildSelectionRule::kHybridInferenceCost: { double upVal = std::ceil(currnode.branching_point); double downVal = std::floor(currnode.branching_point); - double upScore = - (1 + pseudocost.getAvgInferencesUp(col)) / - pseudocost.getPseudocostUp(col, currnode.branching_point, - mipsolver.mipdata_->feastol); - double downScore = - (1 + pseudocost.getAvgInferencesDown(col)) / - pseudocost.getPseudocostDown(col, currnode.branching_point, - mipsolver.mipdata_->feastol); + double upScore = (1 + pseudocost.getAvgInferencesUp(col)) / + pseudocost.getPseudocostUp( + col, currnode.branching_point, getFeasTol()); + double downScore = (1 + pseudocost.getAvgInferencesDown(col)) / + pseudocost.getPseudocostDown( + col, currnode.branching_point, getFeasTol()); if (upScore >= downScore) { currnode.branchingdecision.boundtype = HighsBoundType::kLower; @@ -1340,7 +1351,7 @@ HighsSearch::NodeResult HighsSearch::branch() { // fail in the LP solution process pseudocost.setDegeneracyFactor(1e6); - for (HighsInt i : mipsolver.mipdata_->integral_cols) { + for (HighsInt i : getIntegralCols()) { if (localdom.col_upper_[i] - localdom.col_lower_[i] < 0.5) continue; double fracval; @@ -1365,20 +1376,17 @@ HighsSearch::NodeResult HighsSearch::branch() { double cost = lp->unscaledDualFeasible(lp->getStatus()) ? lp->getSolution().col_dual[i] : mipsolver.colCost(i); - if (std::fabs(cost) > mipsolver.mipdata_->feastol && - getCutoffBound() < kHighsInf) { + if (std::fabs(cost) > getFeasTol() && getCutoffBound() < kHighsInf) { // branch in direction of worsening cost first in case the column has // cost and we do have an upper bound branchUpwards = cost > 0; } else if (pseudocost.getAvgInferencesUp(i) > - pseudocost.getAvgInferencesDown(i) + - mipsolver.mipdata_->feastol) { + pseudocost.getAvgInferencesDown(i) + getFeasTol()) { // column does not have (reduced) cost above tolerance so branch in // direction of more inferences branchUpwards = true; } else if (pseudocost.getAvgInferencesUp(i) < - pseudocost.getAvgInferencesDown(i) - - mipsolver.mipdata_->feastol) { + pseudocost.getAvgInferencesDown(i) - getFeasTol()) { branchUpwards = false; } else { // number of inferences give a tie, so we branch in the direction that @@ -1524,7 +1532,7 @@ bool HighsSearch::backtrack(bool recoverBasis) { if (nodestack.back().stabilizerOrbits) nodestack.back().stabilizerOrbits->orbitalFixing(localdom); else - mipsolver.mipdata_->symmetries.propagateOrbitopes(localdom); + getSymmetries().propagateOrbitopes(localdom); } if (localdom.infeasible()) { localdom.clearChangedCols(oldNumChangedCols); @@ -1573,10 +1581,12 @@ bool HighsSearch::backtrack(bool recoverBasis) { if (!prune) { localdom.propagate(); prune = localdom.infeasible(); - if (prune) localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + if (prune) + localdom.conflictAnalysis(getConflictPool(), + mipworker.getGlobalDomain(), pseudocost); } if (!prune) { - mipsolver.mipdata_->symmetries.propagateOrbitopes(localdom); + getSymmetries().propagateOrbitopes(localdom); prune = localdom.infeasible(); } if (!prune && passStabilizerToChildNode && currnode.stabilizerOrbits) { @@ -1647,7 +1657,7 @@ bool HighsSearch::backtrackPlunge(HighsNodeQueue& nodequeue) { if (nodestack.back().stabilizerOrbits) nodestack.back().stabilizerOrbits->orbitalFixing(localdom); else - mipsolver.mipdata_->symmetries.propagateOrbitopes(localdom); + getSymmetries().propagateOrbitopes(localdom); } if (localdom.infeasible()) { localdom.clearChangedCols(oldNumChangedCols); @@ -1703,10 +1713,12 @@ bool HighsSearch::backtrackPlunge(HighsNodeQueue& nodequeue) { if (!prune) { localdom.propagate(); prune = localdom.infeasible(); - if (prune) localdom.conflictAnalysis(mipsolver.mipdata_->conflictPool); + if (prune) + localdom.conflictAnalysis(getConflictPool(), + mipworker.getGlobalDomain(), pseudocost); } if (!prune) { - mipsolver.mipdata_->symmetries.propagateOrbitopes(localdom); + getSymmetries().propagateOrbitopes(localdom); prune = localdom.infeasible(); } if (!prune && passStabilizerToChildNode && currnode.stabilizerOrbits) { @@ -1721,7 +1733,7 @@ bool HighsSearch::backtrackPlunge(HighsNodeQueue& nodequeue) { } nodelb = std::max(nodelb, localdom.getObjectiveLowerBound()); - bool nodeToQueue = nodelb > mipsolver.mipdata_->optimality_limit; + bool nodeToQueue = nodelb > 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. @@ -1752,7 +1764,7 @@ bool HighsSearch::backtrackPlunge(HighsNodeQueue& nodequeue) { // printf("nodeScore: %g, ancestorScore: %g\n", nodeScore, // ancestorScore); nodeToQueue = ancestorScoreInactive - ancestorScoreActive > - nodeScore + mipsolver.mipdata_->feastol; + nodeScore + getFeasTol(); break; } } @@ -1851,19 +1863,20 @@ bool HighsSearch::backtrackUntilDepth(HighsInt targetDepth) { return true; } -HighsSearch::NodeResult HighsSearch::dive() { +HighsSearch::NodeResult HighsSearch::dive(bool ramp) { reliableatnode.clear(); do { ++nnodes; NodeResult result = evaluateNode(); - if (mipsolver.mipdata_->checkLimits(nnodes)) return result; + if (checkLimits(nnodes)) return result; if (result != NodeResult::kOpen) return result; result = branch(); if (result != NodeResult::kBranched) return result; + if (ramp) return result; } while (true); } @@ -1879,3 +1892,89 @@ void HighsSearch::solveDepthFirst(int64_t maxbacktracks) { } while (backtrack()); } + +double HighsSearch::getFeasTol() const { return mipsolver.mipdata_->feastol; } + +double HighsSearch::getUpperLimit() const { + if (!mipsolver.mipdata_->parallelLockActive()) { + return mipsolver.mipdata_->upper_limit; + } else { + return mipworker.upper_limit; + } +} + +double HighsSearch::getEpsilon() const { return mipsolver.mipdata_->epsilon; } + +double HighsSearch::getOptimalityLimit() const { + if (!mipsolver.mipdata_->parallelLockActive()) { + return mipsolver.mipdata_->optimality_limit; + } else { + return mipworker.optimality_limit; + } +} + +const std::vector& HighsSearch::getRootLpSol() const { + return mipsolver.mipdata_->rootlpsol; +} + +const std::vector& HighsSearch::getIntegralCols() const { + return mipsolver.mipdata_->integral_cols; +} + +HighsDomain& HighsSearch::getDomain() const { + return mipworker.getGlobalDomain(); +} + +HighsConflictPool& HighsSearch::getConflictPool() const { + return *mipworker.conflictpool_; +} + +HighsCutPool& HighsSearch::getCutPool() const { return *mipworker.cutpool_; } + +const HighsNodeQueue& HighsSearch::getNodeQueue() const { + return mipsolver.mipdata_->nodequeue; +} + +bool HighsSearch::checkLimits(int64_t nodeOffset) const { + if (mipsolver.mipdata_->parallelLockActive()) return false; + return mipsolver.mipdata_->checkLimits(nodeOffset); +} + +HighsSymmetries& HighsSearch::getSymmetries() const { + return mipsolver.mipdata_->symmetries; +} + +bool HighsSearch::addIncumbent(const std::vector& sol, double solobj, + const int solution_source, + const bool print_display_line) { + if (mipsolver.mipdata_->parallelLockActive()) { + return mipworker.addIncumbent(sol, solobj, solution_source); + } else { + return mipsolver.mipdata_->addIncumbent(sol, solobj, solution_source, + print_display_line); + } +} + +int64_t& HighsSearch::getNumNodes() { return mipsolver.mipdata_->num_nodes; } + +int64_t& HighsSearch::getNumLeaves() { return mipsolver.mipdata_->num_leaves; } + +HighsCDouble& HighsSearch::getPrunedTreeweight() { + return mipsolver.mipdata_->pruned_treeweight; +} + +int64_t& HighsSearch::getTotalLpIterations() { + return mipsolver.mipdata_->total_lp_iterations; +} + +int64_t& HighsSearch::getHeuristicLpIterations() { + return mipsolver.mipdata_->heuristic_lp_iterations; +} + +int64_t& HighsSearch::getSbLpIterations() { + return mipsolver.mipdata_->sb_lp_iterations; +} + +int64_t& HighsSearch::getSbLpIterations() const { + return mipsolver.mipdata_->sb_lp_iterations; +} \ No newline at end of file diff --git a/highs/mip/HighsSearch.h b/highs/mip/HighsSearch.h index 50bf0d25bc..528cce8bd0 100644 --- a/highs/mip/HighsSearch.h +++ b/highs/mip/HighsSearch.h @@ -13,9 +13,11 @@ #include #include "mip/HighsConflictPool.h" +#include "mip/HighsDebugSol.h" #include "mip/HighsDomain.h" #include "mip/HighsLpRelaxation.h" #include "mip/HighsMipSolver.h" +#include "mip/HighsMipWorker.h" #include "mip/HighsNodeQueue.h" #include "mip/HighsPseudocost.h" #include "mip/HighsSeparation.h" @@ -23,16 +25,21 @@ #include "util/HighsHash.h" class HighsMipSolver; +class HighsMipWorker; class HighsImplications; class HighsCliqueTable; class HighsSearch { - HighsMipSolver& mipsolver; + public: + HighsMipWorker& mipworker; + + const HighsMipSolver& mipsolver; HighsLpRelaxation* lp; HighsDomain localdom; HighsPseudocost& pseudocost; HighsRandom random; int64_t nnodes; + int64_t nleaves; int64_t lpiterations; int64_t heurlpiterations; int64_t sblpiterations; @@ -138,7 +145,7 @@ class HighsSearch { bool orbitsValidInChildNode(const HighsDomainChange& branchChg) const; public: - HighsSearch(HighsMipSolver& mipsolver, HighsPseudocost& pseudocost); + HighsSearch(HighsMipWorker& mipworker, HighsPseudocost& pseudocost); void setRINSNeighbourhood(const std::vector& basesol, const std::vector& relaxsol); @@ -176,7 +183,9 @@ class HighsSearch { int64_t getLocalLpIterations() const; - int64_t getLocalNodes() const; + int64_t& getLocalNodes(); + + int64_t& getLocalLeaves(); int64_t getStrongBranchingLpIterations() const; @@ -225,7 +234,7 @@ class HighsSearch { void printDisplayLine(char first, bool header = false); - NodeResult dive(); + NodeResult dive(const bool ramp = false); HighsDomain& getLocalDomain() { return localdom; } @@ -236,6 +245,36 @@ class HighsSearch { const HighsPseudocost& getPseudoCost() const { return pseudocost; } void solveDepthFirst(int64_t maxbacktracks = 1); + + double getFeasTol() const; + double getUpperLimit() const; + double getEpsilon() const; + double getOptimalityLimit() const; + + const std::vector& getRootLpSol() const; + const std::vector& getIntegralCols() const; + + HighsDomain& getDomain() const; + HighsConflictPool& getConflictPool() const; + HighsCutPool& getCutPool() const; + + const HighsNodeQueue& getNodeQueue() const; + + bool checkLimits(int64_t nodeOffset = 0) const; + + HighsSymmetries& getSymmetries() const; + + bool addIncumbent(const std::vector& sol, double solobj, + const int solution_source, + const bool print_display_line = true); + + int64_t& getNumNodes(); + int64_t& getNumLeaves(); + HighsCDouble& getPrunedTreeweight(); + int64_t& getTotalLpIterations(); + int64_t& getHeuristicLpIterations(); + int64_t& getSbLpIterations(); + int64_t& getSbLpIterations() const; }; #endif diff --git a/highs/mip/HighsSeparation.cpp b/highs/mip/HighsSeparation.cpp index a85691a1d0..37d8ed4d45 100644 --- a/highs/mip/HighsSeparation.cpp +++ b/highs/mip/HighsSeparation.cpp @@ -22,15 +22,17 @@ #include "mip/HighsTableauSeparator.h" #include "mip/HighsTransformedLp.h" -HighsSeparation::HighsSeparation(const HighsMipSolver& mipsolver) { - if (mipsolver.analysis_.analyse_mip_time) { +HighsSeparation::HighsSeparation(HighsMipWorker& mipworker) + : mipworker_(mipworker) { + if (mipworker.mipsolver_.analysis_.analyse_mip_time) { implBoundClock = - mipsolver.analysis_.getSepaClockIndex(kImplboundSepaString); - cliqueClock = mipsolver.analysis_.getSepaClockIndex(kCliqueSepaString); + mipworker.mipsolver_.analysis_.getSepaClockIndex(kImplboundSepaString); + cliqueClock = + mipworker.mipsolver_.analysis_.getSepaClockIndex(kCliqueSepaString); } - separators.emplace_back(new HighsTableauSeparator(mipsolver)); - separators.emplace_back(new HighsPathSeparator(mipsolver)); - separators.emplace_back(new HighsModkSeparator(mipsolver)); + separators.emplace_back(new HighsTableauSeparator(mipworker.mipsolver_)); + separators.emplace_back(new HighsPathSeparator(mipworker.mipsolver_)); + separators.emplace_back(new HighsModkSeparator(mipworker.mipsolver_)); } HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, @@ -40,7 +42,7 @@ HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, HighsMipSolverData& mipdata = *lp->getMipSolver().mipdata_; auto propagateAndResolve = [&]() { - if (propdomain.infeasible() || mipdata.domain.infeasible()) { + if (propdomain.infeasible() || mipworker_.getGlobalDomain().infeasible()) { status = HighsLpRelaxation::Status::kInfeasible; propdomain.clearChangedCols(); return -1; @@ -53,8 +55,11 @@ HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, return -1; } - mipdata.cliquetable.cleanupFixed(mipdata.domain); - if (mipdata.domain.infeasible()) { + // only modify cliquetable for master worker. + if (&propdomain == &mipdata.getDomain()) + mipdata.cliquetable.cleanupFixed(mipdata.getDomain()); + + if (mipworker_.getGlobalDomain().infeasible()) { status = HighsLpRelaxation::Status::kInfeasible; propdomain.clearChangedCols(); return -1; @@ -67,7 +72,8 @@ HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, status = lp->resolveLp(&propdomain); if (!lp->scaledOptimal(status)) return -1; - if (&propdomain == &mipdata.domain && lp->unscaledDualFeasible(status)) { + if (&propdomain == &mipdata.getDomain() && + lp->unscaledDualFeasible(status)) { mipdata.redcostfixing.addRootRedcost( mipdata.mipsolver, lp->getSolution().col_dual, lp->getObjective()); if (mipdata.upper_limit != kHighsInf) @@ -78,10 +84,14 @@ HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, return numBoundChgs; }; - lp->getMipSolver().analysis_.mipTimerStart(implBoundClock); - mipdata.implications.separateImpliedBounds(*lp, lp->getSolution().col_value, - mipdata.cutpool, mipdata.feastol); - lp->getMipSolver().analysis_.mipTimerStop(implBoundClock); + if (!mipdata.parallelLockActive()) + lp->getMipSolver().analysis_.mipTimerStart(implBoundClock); + mipdata.implications.separateImpliedBounds( + *lp, lp->getSolution().col_value, mipworker_.getCutPool(), + mipdata.feastol, mipworker_.getGlobalDomain(), + mipdata.parallelLockActive()); + if (!mipdata.parallelLockActive()) + lp->getMipSolver().analysis_.mipTimerStop(implBoundClock); HighsInt ncuts = 0; HighsInt numboundchgs = propagateAndResolve(); @@ -90,10 +100,18 @@ HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, else ncuts += numboundchgs; - lp->getMipSolver().analysis_.mipTimerStart(cliqueClock); - mipdata.cliquetable.separateCliques(lp->getMipSolver(), sol.col_value, - mipdata.cutpool, mipdata.feastol); - lp->getMipSolver().analysis_.mipTimerStop(cliqueClock); + if (!mipdata.parallelLockActive()) + lp->getMipSolver().analysis_.mipTimerStart(cliqueClock); + mipdata.cliquetable.separateCliques( + lp->getMipSolver(), sol.col_value, mipworker_.getCutPool(), + mipdata.feastol, + mipdata.parallelLockActive() ? mipworker_.randgen + : mipdata.cliquetable.getRandgen(), + mipdata.parallelLockActive() + ? mipworker_.sepa_stats.numNeighbourhoodQueries + : mipdata.cliquetable.getNumNeighbourhoodQueries()); + if (!mipdata.parallelLockActive()) + lp->getMipSolver().analysis_.mipTimerStop(cliqueClock); numboundchgs = propagateAndResolve(); if (numboundchgs == -1) @@ -101,19 +119,22 @@ HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, else ncuts += numboundchgs; - if (&propdomain != &mipdata.domain) - lp->computeBasicDegenerateDuals(mipdata.feastol, &propdomain); + if (&propdomain != &mipworker_.getGlobalDomain()) + lp->computeBasicDegenerateDuals(mipdata.feastol, propdomain, + mipworker_.getGlobalDomain(), + mipworker_.getConflictPool(), true); - HighsTransformedLp transLp(*lp, mipdata.implications); - if (mipdata.domain.infeasible()) { + HighsTransformedLp transLp(*lp, mipdata.implications, + mipworker_.getGlobalDomain()); + if (mipworker_.getGlobalDomain().infeasible()) { status = HighsLpRelaxation::Status::kInfeasible; return 0; } HighsLpAggregator lpAggregator(*lp); for (const std::unique_ptr& separator : separators) { - separator->run(*lp, lpAggregator, transLp, mipdata.cutpool); - if (mipdata.domain.infeasible()) { + separator->run(*lp, lpAggregator, transLp, mipworker_.getCutPool()); + if (mipworker_.getGlobalDomain().infeasible()) { status = HighsLpRelaxation::Status::kInfeasible; return 0; } @@ -125,14 +146,23 @@ HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, else ncuts += numboundchgs; - mipdata.cutpool.separate(sol.col_value, propdomain, cutset, mipdata.feastol); + mipworker_.cutpool_->separate(sol.col_value, propdomain, cutset, + mipdata.feastol, mipdata.cutpools); + // Also separate the global cut pool + if (mipworker_.cutpool_ != &mipdata.getCutPool()) { + mipdata.getCutPool().separate(sol.col_value, propdomain, cutset, + mipdata.feastol, mipdata.cutpools, true); + } if (cutset.numCuts() > 0) { ncuts += cutset.numCuts(); lp->addCuts(cutset); status = lp->resolveLp(&propdomain); lp->performAging(true); - if (&propdomain == &mipdata.domain && lp->unscaledDualFeasible(status)) { + + // only for the master domain. + if (&propdomain == &mipdata.getDomain() && + lp->unscaledDualFeasible(status)) { mipdata.redcostfixing.addRootRedcost( mipdata.mipsolver, lp->getSolution().col_dual, lp->getObjective()); if (mipdata.upper_limit != kHighsInf) @@ -154,11 +184,17 @@ void HighsSeparation::separate(HighsDomain& propdomain) { while (lp->getObjective() < mipsolver.mipdata_->optimality_limit) { double lastobj = lp->getObjective(); - size_t nlpiters = -lp->getNumLpIterations(); + int64_t nlpiters = -lp->getNumLpIterations(); HighsInt ncuts = separationRound(propdomain, status); nlpiters += lp->getNumLpIterations(); - mipsolver.mipdata_->sepa_lp_iterations += nlpiters; - mipsolver.mipdata_->total_lp_iterations += nlpiters; + + if (mipsolver.mipdata_->parallelLockActive()) { + mipworker_.sepa_stats.sepa_lp_iterations += nlpiters; + } else { + mipsolver.mipdata_->sepa_lp_iterations += nlpiters; + mipsolver.mipdata_->total_lp_iterations += nlpiters; + } + // printf("separated %" HIGHSINT_FORMAT " cuts\n", ncuts); // printf( @@ -181,6 +217,7 @@ void HighsSeparation::separate(HighsDomain& propdomain) { // printf("no separation, just aging. status: %" HIGHSINT_FORMAT "\n", // (HighsInt)status); lp->performAging(true); - mipsolver.mipdata_->cutpool.performAging(); + + mipworker_.cutpool_->performAging(); } } diff --git a/highs/mip/HighsSeparation.h b/highs/mip/HighsSeparation.h index ea09959b61..cb79d11227 100644 --- a/highs/mip/HighsSeparation.h +++ b/highs/mip/HighsSeparation.h @@ -16,6 +16,7 @@ #include "mip/HighsSeparator.h" class HighsMipSolver; +class HighsMipWorker; class HighsImplications; class HighsCliqueTable; @@ -28,9 +29,10 @@ class HighsSeparation { void setLpRelaxation(HighsLpRelaxation* lp) { this->lp = lp; } - HighsSeparation(const HighsMipSolver& mipsolver); + HighsSeparation(HighsMipWorker& mipworker); private: + HighsMipWorker& mipworker_; HighsInt implBoundClock; HighsInt cliqueClock; std::vector> separators; diff --git a/highs/mip/HighsSeparator.cpp b/highs/mip/HighsSeparator.cpp index 0415c556ef..af2fb4e827 100644 --- a/highs/mip/HighsSeparator.cpp +++ b/highs/mip/HighsSeparator.cpp @@ -9,6 +9,7 @@ #include +#include "HighsMipSolverData.h" #include "mip/HighsCutPool.h" #include "mip/HighsLpRelaxation.h" #include "mip/HighsMipSolver.h" @@ -31,9 +32,11 @@ void HighsSeparator::run(HighsLpRelaxation& lpRelaxation, ++numCalls; HighsInt currNumCuts = cutpool.getNumCuts(); - lpRelaxation.getMipSolver().analysis_.mipTimerStart(clockIndex); + if (!lpRelaxation.getMipSolver().mipdata_->parallelLockActive()) + lpRelaxation.getMipSolver().analysis_.mipTimerStart(clockIndex); separateLpSolution(lpRelaxation, lpAggregator, transLp, cutpool); - lpRelaxation.getMipSolver().analysis_.mipTimerStop(clockIndex); + if (!lpRelaxation.getMipSolver().mipdata_->parallelLockActive()) + lpRelaxation.getMipSolver().analysis_.mipTimerStop(clockIndex); numCutsFound += cutpool.getNumCuts() - currNumCuts; } diff --git a/highs/mip/HighsTableauSeparator.cpp b/highs/mip/HighsTableauSeparator.cpp index 3d4cd0d5a2..8259d374e0 100644 --- a/highs/mip/HighsTableauSeparator.cpp +++ b/highs/mip/HighsTableauSeparator.cpp @@ -230,12 +230,12 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, double rhs = 0; cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs); - if (mip.mipdata_->domain.infeasible()) break; + if (transLp.getGlobaldom().infeasible()) break; lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); rhs = 0; cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs); - if (mip.mipdata_->domain.infeasible()) break; + if (transLp.getGlobaldom().infeasible()) break; lpAggregator.clear(); if (bestScore == -1.0 && cutpool.getNumCuts() != numCuts) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 703b377e09..609c7dd15f 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -13,8 +13,9 @@ #include "util/HighsIntegers.h" HighsTransformedLp::HighsTransformedLp(const HighsLpRelaxation& lprelaxation, - HighsImplications& implications) - : lprelaxation(lprelaxation) { + HighsImplications& implications, + const HighsDomain& globaldom) + : lprelaxation(lprelaxation), globaldom_(globaldom) { assert(lprelaxation.scaledOptimal(lprelaxation.getStatus())); const HighsMipSolver& mipsolver = implications.mipsolver; const HighsSolution& lpSolution = lprelaxation.getLpSolver().getSolution(); @@ -34,22 +35,24 @@ HighsTransformedLp::HighsTransformedLp(const HighsLpRelaxation& lprelaxation, vectorsum.setDimension(numTransformedCol); for (HighsInt col : mipsolver.mipdata_->continuous_cols) { - mipsolver.mipdata_->implications.cleanupVarbounds(col); - if (mipsolver.mipdata_->domain.infeasible()) return; + if (!mipsolver.mipdata_->parallelLockActive()) + mipsolver.mipdata_->implications.cleanupVarbounds(col); - if (mipsolver.mipdata_->domain.isFixed(col)) continue; + if (globaldom_.infeasible()) return; - double bestub = mipsolver.mipdata_->domain.col_upper_[col]; + if (globaldom_.isFixed(col)) continue; + + double bestub = globaldom_.col_upper_[col]; simpleUbDist[col] = bestub - lpSolution.col_value[col]; if (simpleUbDist[col] <= mipsolver.mipdata_->feastol) simpleUbDist[col] = 0.0; - bestVub[col] = implications.getBestVub(col, lpSolution, bestub); + bestVub[col] = implications.getBestVub(col, lpSolution, bestub, globaldom_); - double bestlb = mipsolver.mipdata_->domain.col_lower_[col]; + double bestlb = globaldom_.col_lower_[col]; simpleLbDist[col] = lpSolution.col_value[col] - bestlb; if (simpleLbDist[col] <= mipsolver.mipdata_->feastol) simpleLbDist[col] = 0.0; - bestVlb[col] = implications.getBestVlb(col, lpSolution, bestlb); + bestVlb[col] = implications.getBestVlb(col, lpSolution, bestlb, globaldom_); lbDist[col] = lpSolution.col_value[col] - bestlb; if (lbDist[col] <= mipsolver.mipdata_->feastol) lbDist[col] = 0.0; @@ -60,11 +63,13 @@ HighsTransformedLp::HighsTransformedLp(const HighsLpRelaxation& lprelaxation, } for (HighsInt col : mipsolver.mipdata_->integral_cols) { - double bestub = mipsolver.mipdata_->domain.col_upper_[col]; - double bestlb = mipsolver.mipdata_->domain.col_lower_[col]; + double bestub = globaldom_.col_upper_[col]; + double bestlb = globaldom_.col_lower_[col]; + + if (!mipsolver.mipdata_->parallelLockActive()) + mipsolver.mipdata_->implications.cleanupVarbounds(col); - mipsolver.mipdata_->implications.cleanupVarbounds(col); - if (mipsolver.mipdata_->domain.infeasible()) return; + if (globaldom_.infeasible()) return; simpleUbDist[col] = bestub - lpSolution.col_value[col]; if (simpleUbDist[col] <= mipsolver.mipdata_->feastol) simpleUbDist[col] = 0.0; @@ -76,10 +81,10 @@ HighsTransformedLp::HighsTransformedLp(const HighsLpRelaxation& lprelaxation, if (simpleBndDist > 0 && std::fabs(HighsIntegers::nearestInteger(lpSolution.col_value[col]) - lpSolution.col_value[col]) < mipsolver.mipdata_->feastol) { - bestVub[col] = - mipsolver.mipdata_->implications.getBestVub(col, lpSolution, bestub); - bestVlb[col] = - mipsolver.mipdata_->implications.getBestVlb(col, lpSolution, bestlb); + bestVub[col] = mipsolver.mipdata_->implications.getBestVub( + col, lpSolution, bestub, globaldom_); + bestVlb[col] = mipsolver.mipdata_->implications.getBestVlb( + col, lpSolution, bestlb, globaldom_); lbDist[col] = lpSolution.col_value[col] - bestlb; if (lbDist[col] <= mipsolver.mipdata_->feastol) lbDist[col] = 0.0; @@ -105,8 +110,8 @@ HighsTransformedLp::HighsTransformedLp(const HighsLpRelaxation& lprelaxation, HighsInt indexOffset = mipsolver.numCol(); for (HighsInt row = 0; row != numLpRow; ++row) { HighsInt slackIndex = indexOffset + row; - double bestub = lprelaxation.slackUpper(row); - double bestlb = lprelaxation.slackLower(row); + double bestub = lprelaxation.slackUpper(row, globaldom_); + double bestlb = lprelaxation.slackLower(row, globaldom_); if (bestlb == bestub) continue; @@ -140,13 +145,15 @@ bool HighsTransformedLp::transform(std::vector& vals, HighsInt numNz = inds.size(); auto getLb = [&](HighsInt col) { - return (col < slackOffset ? mip.mipdata_->domain.col_lower_[col] - : lprelaxation.slackLower(col - slackOffset)); + return (col < slackOffset + ? globaldom_.col_lower_[col] + : lprelaxation.slackLower(col - slackOffset, globaldom_)); }; auto getUb = [&](HighsInt col) { - return (col < slackOffset ? mip.mipdata_->domain.col_upper_[col] - : lprelaxation.slackUpper(col - slackOffset)); + return (col < slackOffset + ? globaldom_.col_upper_[col] + : lprelaxation.slackUpper(col - slackOffset, globaldom_)); }; auto remove = [&](HighsInt position) { @@ -467,11 +474,11 @@ bool HighsTransformedLp::untransform(std::vector& vals, } case BoundType::kSimpleLb: { if (col < slackOffset) { - tmpRhs += vals[i] * mip.mipdata_->domain.col_lower_[col]; + tmpRhs += vals[i] * globaldom_.col_lower_[col]; vectorsum.add(col, vals[i]); } else { HighsInt row = col - slackOffset; - tmpRhs += vals[i] * lprelaxation.slackLower(row); + tmpRhs += vals[i] * lprelaxation.slackLower(row, globaldom_); HighsInt rowlen; const HighsInt* rowinds; @@ -485,11 +492,11 @@ bool HighsTransformedLp::untransform(std::vector& vals, } case BoundType::kSimpleUb: { if (col < slackOffset) { - tmpRhs -= vals[i] * mip.mipdata_->domain.col_upper_[col]; + tmpRhs -= vals[i] * globaldom_.col_upper_[col]; vectorsum.add(col, -vals[i]); } else { HighsInt row = col - slackOffset; - tmpRhs -= vals[i] * lprelaxation.slackUpper(row); + tmpRhs -= vals[i] * lprelaxation.slackUpper(row, globaldom_); vals[i] = -vals[i]; HighsInt rowlen; @@ -525,15 +532,15 @@ bool HighsTransformedLp::untransform(std::vector& vals, if (absval <= mip.mipdata_->feastol) { if (val > 0) { - if (mip.mipdata_->domain.col_lower_[col] == -kHighsInf) + if (globaldom_.col_lower_[col] == -kHighsInf) abort = true; else - tmpRhs -= val * mip.mipdata_->domain.col_lower_[col]; + tmpRhs -= val * globaldom_.col_lower_[col]; } else { - if (mip.mipdata_->domain.col_upper_[col] == kHighsInf) + if (globaldom_.col_upper_[col] == kHighsInf) abort = true; else - tmpRhs -= val * mip.mipdata_->domain.col_upper_[col]; + tmpRhs -= val * globaldom_.col_upper_[col]; } return true; } diff --git a/highs/mip/HighsTransformedLp.h b/highs/mip/HighsTransformedLp.h index 5c3d2d01db..bb7e929224 100644 --- a/highs/mip/HighsTransformedLp.h +++ b/highs/mip/HighsTransformedLp.h @@ -29,6 +29,7 @@ class HighsLpRelaxation; class HighsTransformedLp { private: const HighsLpRelaxation& lprelaxation; + const HighsDomain& globaldom_; std::vector> bestVub; std::vector> bestVlb; @@ -48,7 +49,8 @@ class HighsTransformedLp { public: HighsTransformedLp(const HighsLpRelaxation& lprelaxation, - HighsImplications& implications); + HighsImplications& implications, + const HighsDomain& globaldom); double boundDistance(HighsInt col) const { return boundDist[col]; } @@ -58,6 +60,8 @@ class HighsTransformedLp { bool untransform(std::vector& vals, std::vector& inds, double& rhs, bool integral = false); + + const HighsDomain& getGlobaldom() const { return globaldom_; } }; #endif diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 8f55b22be3..d30a91744d 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -161,9 +161,9 @@ bool HPresolve::okSetInput(HighsMipSolver& mipsolver, mipsolver.model_ = &mipsolver.mipdata_->presolvedModel; } else { mipsolver.mipdata_->presolvedModel.col_lower_ = - mipsolver.mipdata_->domain.col_lower_; + mipsolver.mipdata_->getDomain().col_lower_; mipsolver.mipdata_->presolvedModel.col_upper_ = - mipsolver.mipdata_->domain.col_upper_; + mipsolver.mipdata_->getDomain().col_upper_; } return okSetInput(mipsolver.mipdata_->presolvedModel, *mipsolver.options_mip_, @@ -1008,17 +1008,17 @@ void HPresolve::shrinkProblem(HighsPostsolveStack& postsolve_stack) { if (mipsolver != nullptr) { mipsolver->mipdata_->rowMatrixSet = false; mipsolver->mipdata_->objectiveFunction = HighsObjectiveFunction(*mipsolver); - mipsolver->mipdata_->domain = HighsDomain(*mipsolver); + mipsolver->mipdata_->getDomain() = HighsDomain(*mipsolver); mipsolver->mipdata_->cliquetable.rebuild(model->num_col_, postsolve_stack, - mipsolver->mipdata_->domain, + mipsolver->mipdata_->getDomain(), newColIndex, newRowIndex); mipsolver->mipdata_->implications.rebuild(model->num_col_, newColIndex, newRowIndex); - mipsolver->mipdata_->cutpool = + mipsolver->mipdata_->getCutPool() = HighsCutPool(mipsolver->model_->num_col_, mipsolver->options_mip_->mip_pool_age_limit, - mipsolver->options_mip_->mip_pool_soft_limit); - mipsolver->mipdata_->conflictPool = + mipsolver->options_mip_->mip_pool_soft_limit, 0); + mipsolver->mipdata_->getConflictPool() = HighsConflictPool(5 * mipsolver->options_mip_->mip_pool_age_limit, mipsolver->options_mip_->mip_pool_soft_limit); @@ -1445,7 +1445,7 @@ HPresolve::Result HPresolve::dominatedColumns( HPresolve::Result HPresolve::prepareProbing( HighsPostsolveStack& postsolve_stack, bool& firstCall) { - HighsDomain& domain = mipsolver->mipdata_->domain; + HighsDomain& domain = mipsolver->mipdata_->getDomain(); HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; shrinkProblem(postsolve_stack); @@ -1514,7 +1514,7 @@ HPresolve::Result HPresolve::finaliseProbing( HighsPostsolveStack& postsolve_stack, bool firstCall, HighsInt& numVarsFixed, HighsInt& numBndsTightened, HighsInt& numVarsSubstituted, HighsInt& liftedNonZeros) { - HighsDomain& domain = mipsolver->mipdata_->domain; + HighsDomain& domain = mipsolver->mipdata_->getDomain(); HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; cliquetable.cleanupFixed(domain); @@ -1602,7 +1602,7 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { return prepareResult; } - HighsDomain& domain = mipsolver->mipdata_->domain; + HighsDomain& domain = mipsolver->mipdata_->getDomain(); HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; HighsImplications& implications = mipsolver->mipdata_->implications; @@ -1868,7 +1868,7 @@ HPresolve::Result HPresolve::liftingForProbing( // al. (2019) Presolve Reductions in Mixed Integer Programming. INFORMS // Journal on Computing 32(2):473-506. HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; - const HighsDomain& domain = mipsolver->mipdata_->domain; + const HighsDomain& domain = mipsolver->mipdata_->getDomain(); // collect best lifting opportunity for each row in a vector typedef std::pair liftingvar; @@ -5161,7 +5161,7 @@ HPresolve::Result HPresolve::enumerateSolutions( return prepareResult; } - HighsDomain& domain = mipsolver->mipdata_->domain; + HighsDomain& domain = mipsolver->mipdata_->getDomain(); HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; typedef std::tuple candidateRow; @@ -6286,9 +6286,10 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { if (mipsolver != nullptr) { mipsolver->mipdata_->cliquetable.setPresolveFlag(false); mipsolver->mipdata_->cliquetable.setMaxEntries(numNonzeros()); - mipsolver->mipdata_->domain.addCutpool(mipsolver->mipdata_->cutpool); - mipsolver->mipdata_->domain.addConflictPool( - mipsolver->mipdata_->conflictPool); + mipsolver->mipdata_->getDomain().addCutpool( + mipsolver->mipdata_->getCutPool()); + mipsolver->mipdata_->getDomain().addConflictPool( + mipsolver->mipdata_->getConflictPool()); if (mipsolver->mipdata_->numRestarts != 0) { std::vector cutinds; @@ -6312,7 +6313,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { cutvals.push_back(Avalue[j]); } - mipsolver->mipdata_->cutpool.addCut( + mipsolver->mipdata_->getCutPool().addCut( *mipsolver, cutinds.data(), cutvals.data(), cutinds.size(), model->row_upper_[i], rowsizeInteger[i] + rowsizeImplInt[i] == rowsize[i] && diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 8e43470a3a..593ab2f7d9 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -609,8 +609,23 @@ class HighsPostsolveStack { /// undo presolve steps for primal dual solution and basis void undo(const HighsOptions& options, HighsSolution& solution, - HighsBasis& basis, const HighsInt report_col = -1) { - reductionValues.resetPosition(); + HighsBasis& basis, const HighsInt report_col = -1, + const bool thread_safe = false) { + HighsDataStack reductionValuesCopy; + std::vector colValuesCopy; + std::vector rowValuesCopy; + if (!thread_safe) { + reductionValues.resetPosition(); + } else { + reductionValuesCopy = reductionValues; + reductionValuesCopy.resetPosition(); + colValuesCopy = colValues; + rowValuesCopy = rowValues; + } + HighsDataStack& reductionValues_ = + thread_safe ? reductionValuesCopy : reductionValues; + std::vector& colValues_ = thread_safe ? colValuesCopy : colValues; + std::vector& rowValues_ = thread_safe ? rowValuesCopy : rowValues; // Verify that undo can be performed assert(solution.value_valid); @@ -648,97 +663,97 @@ class HighsPostsolveStack { switch (reductions[i - 1].first) { case ReductionType::kLinearTransform: { LinearTransform reduction; - reductionValues.pop(reduction); + reductionValues_.pop(reduction); reduction.undo(options, solution); break; } case ReductionType::kFreeColSubstitution: { FreeColSubstitution reduction; - reductionValues.pop(colValues); - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(options, rowValues, colValues, solution, basis); + reductionValues_.pop(colValues_); + reductionValues_.pop(rowValues_); + reductionValues_.pop(reduction); + reduction.undo(options, rowValues_, colValues_, solution, basis); break; } case ReductionType::kDoubletonEquation: { DoubletonEquation reduction; - reductionValues.pop(colValues); - reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reductionValues_.pop(colValues_); + reductionValues_.pop(reduction); + reduction.undo(options, colValues_, solution, basis); break; } case ReductionType::kEqualityRowAddition: { EqualityRowAddition reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reductionValues_.pop(rowValues_); + reductionValues_.pop(reduction); + reduction.undo(options, rowValues_, solution, basis); break; } case ReductionType::kEqualityRowAdditions: { EqualityRowAdditions reduction; - reductionValues.pop(colValues); - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(options, rowValues, colValues, solution, basis); + reductionValues_.pop(colValues_); + reductionValues_.pop(rowValues_); + reductionValues_.pop(reduction); + reduction.undo(options, rowValues_, colValues_, solution, basis); break; } case ReductionType::kSingletonRow: { SingletonRow reduction; - reductionValues.pop(reduction); + reductionValues_.pop(reduction); reduction.undo(options, solution, basis); break; } case ReductionType::kFixedCol: { FixedCol reduction; - reductionValues.pop(colValues); - reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reductionValues_.pop(colValues_); + reductionValues_.pop(reduction); + reduction.undo(options, colValues_, solution, basis); break; } case ReductionType::kRedundantRow: { RedundantRow reduction; - reductionValues.pop(reduction); + reductionValues_.pop(reduction); reduction.undo(options, solution, basis); break; } case ReductionType::kForcingRow: { ForcingRow reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reductionValues_.pop(rowValues_); + reductionValues_.pop(reduction); + reduction.undo(options, rowValues_, solution, basis); break; } case ReductionType::kForcingColumn: { ForcingColumn reduction; - reductionValues.pop(colValues); - reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reductionValues_.pop(colValues_); + reductionValues_.pop(reduction); + reduction.undo(options, colValues_, solution, basis); break; } case ReductionType::kForcingColumnRemovedRow: { ForcingColumnRemovedRow reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reductionValues_.pop(rowValues_); + reductionValues_.pop(reduction); + reduction.undo(options, rowValues_, solution, basis); break; } case ReductionType::kDuplicateRow: { DuplicateRow reduction; - reductionValues.pop(reduction); + reductionValues_.pop(reduction); reduction.undo(options, solution, basis); break; } case ReductionType::kDuplicateColumn: { DuplicateColumn reduction; - reductionValues.pop(reduction); + reductionValues_.pop(reduction); reduction.undo(options, solution, basis); break; } case ReductionType::kSlackColSubstitution: { SlackColSubstitution reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reductionValues_.pop(rowValues_); + reductionValues_.pop(reduction); + reduction.undo(options, rowValues_, solution, basis); break; } default: @@ -763,14 +778,15 @@ class HighsPostsolveStack { /// undo presolve steps for primal solution void undoPrimal(const HighsOptions& options, HighsSolution& solution, - const HighsInt report_col = -1) { + const HighsInt report_col = -1, + const bool thread_safe = false) { // Call to reductionValues.resetPosition(); seems unnecessary as // it's the first thing done in undo - reductionValues.resetPosition(); + if (!thread_safe) reductionValues.resetPosition(); HighsBasis basis; basis.valid = false; solution.dual_valid = false; - undo(options, solution, basis, report_col); + undo(options, solution, basis, report_col, thread_safe); } /* diff --git a/highs/util/HighsHash.h b/highs/util/HighsHash.h index b879ebed90..f74034d823 100644 --- a/highs/util/HighsHash.h +++ b/highs/util/HighsHash.h @@ -990,6 +990,21 @@ class HighsHashTable { makeEmptyTable(initCapacity); } + HighsHashTable(const HighsHashTable& hashTable) + : tableSizeMask(hashTable.tableSizeMask), + numHashShift(hashTable.numHashShift), + numElements(hashTable.numElements) { + u64 capacity = tableSizeMask + 1; + metadata = decltype(metadata)(new u8[capacity]); + entries = + decltype(entries)((Entry*)::operator new(sizeof(Entry) * capacity)); + + std::copy(hashTable.metadata.get(), hashTable.metadata.get() + capacity, + metadata.get()); + std::copy(hashTable.entries.get(), hashTable.entries.get() + capacity, + entries.get()); + } + iterator end() { u64 capacity = tableSizeMask + 1; return iterator{metadata.get() + capacity, metadata.get() + capacity, diff --git a/parallel.md b/parallel.md new file mode 100644 index 0000000000..c3175d2d4b --- /dev/null +++ b/parallel.md @@ -0,0 +1,154 @@ +# Parallel Search Design + +General design and summary of draft-implementation of parallel tree search in HiGHS + +## HighsMipWorker + +This is the only new class! The goal of HighsMipWorker is that nearly everything after presolve should run through it. +Consider it a surrogate for +HighsMipData. When you want to evaluate the root node, run heuristics, run separators, or dive, the code will be +accessing all "global" data from HighsMipWorker. It is therefore now commonly passed as a parameter. + +The HighsMipWorker contains the following relevant information: + +- HighsDomain (A representation of the global domain that is globally valid but local to the worker) +- HighsCutPool (A pool of globally valid conflicts only accessed by the worker excluding sync points) +- HighsConflictPool (A pool of globally valid cuts only accessed by the worker excluding sync points) +- HighsLpRelaxation (A copy of the LP. Note that this will have cuts both from the global pool and from the worker pool) +- HighsPseudoCost (A copy of the global HighsPseudoCost object with local changes made) +- HighsSearch (A unique pointer) +- HighsSepa (A unique pointer) +- HighsNodeQueue (A local queue that is used when a worker performs backtrack plunge) +- HeurStatistics / SepaStatistics (A way to store statistics and buffer them without changing the global state) +- upper_bound / upper_limit / optimality_limit (Benefit from found solutions without changing the global state) +- Const references to HighsMipSolver and HighsMipSolverData + +HighsDomain / HighsCutPool / HighsConflictPool / HighsLpRelaxation / HighsPseudoCost are all currently pointers and +stored in std::deque objects in HighsMipSolverData. This is done for two reasons: + +- When starting the parallel search, we need to reassign the master HighsMipWorker to point at new not-the-true-global + objects. References can't be reassigned, so we'd have to destroy and recreate the worker. On the opposite side, if we + never reach the tree search, then there's no need to create any new objects. +- Cuts need to have an associated index of where they come from when they're in the LP. Therefore we need to have a + unique identifier from cut -> cutpool. If the pools are stored in a std:deque, then the index of the pool in the deque + is that identifier. If the CutPool was only stored to the worker, then there'd need to be a more confusing mapping + that first goes through the workers. + +## General Design Principles + +- Parallelism only starts after processing the root node. Before that the master worker has pointers to all the "true" + global data structures +- No information is shared between workers excluding the dedicated sync points +- There is a central parallel lock, called `parallel_lock` in `HighsMipSolverData`. It is accessed by a function + `parallelLockActive`, which also consider whether there are additional workers. +- There is a central spawn task function, called `runTask`. It sets the lock, then depending on the amount of tasks and + user parameters, e.g., `simulate_concurrency`, it either runs them sequentially or in parallel. +- Currently only cuts from a worker's pool that are added to the LP are copied into the global pool. +- Currently, all conflicts from a worker's pool are flushed to the global pool. +- There is a parameter `mip_search_concurrency`, which will create `x` workers per core that HiGHS is using, so if your + machine has 16 cores, where HiGHS by default will use 8 (half of what's available), and the parameter is set to 2 ( + default currently without any testing), then 16 workers will be spawned, and at most 8 workers will be running at + once. +- We want more workers than cores (threads that HiGHS will use). That way the chance of waiting on a single task for a + long time is minimised, and we hope to get more reliable average case performance. +- The general pseudocode (subject to change) of the entire parallel search would be: + - while (true) + - Run heuristics (parallel) + - Sync solutions + sync statistics + prune suboptimal nodes + break if infeasible (serial) + - Dive (parallel) + - Sync solutions + sync statistics + prune suboptimal nodes + break if infeasible or some node limit for + dive round reached (serial) + - Backtrack plunge (parallel) + - Break if cant backtrack + - Push open nodes from workers to global queue (serial) + - Flush statistics (serial) + - Sync pools + sync global domain changes found by workers (serial) + - Propagate global domain with new information (serial) + - Consider restart + - While node queue is not empty + - Sync pseudo costs (serial) + - Install nodes (serial) + - Evaluate nodes (parallel) + - Handle pruned nodes (parallel) + - If all nodes pruned, sync domain changes + continue + - Separate (parallel) + - Stop if infeasible + - Store basis (parallel) + - break + +## Changes to existing classes + +### HighsCutPool + +- A std::atomic has been added representing the number of LPs a cut is in. This seems to be a necessary evil ( + alternative would be to create a boolean per worker per cut). Consider the global cut pool, which all workers will + separate. If worker A and worker B both add the cut into their respective LP, then we need a way to track exactly how + many LPs each cut is currently in. +- There's also now a buffer called `ageResetWhileLocked_`, which is a flag that mentions that some worker used + information related to this cut, and it should therefore be having its age reset, but we haven't because doing this + would produce non-determinism. This has affected the aging code. +- General sync function introduced. + +### HighsConflictPool + +- Similar to HighsCutPool, there's also now a buffer called `ageResetWhileLocked_`. +- General sync function introduced. + +### HighsDomain + +- Minor changes to propagation logic to accommodate multiple cut and conflict pools. + +### HighsLpRelaxation + +- An LpRow now has an index associated to which CutPool the row is from + +### HighsPseudoCost + +- Has two additional vectors to keep track of which columns have been actively changed. This was done to make the + syncing phase quicker +- General sync function introduced + +### HighsSeparation + +- Clique table is now only cleaned up during root node separation +- `separateImpliedBounds` does not produce any new implications during the tree search + +### HighsTransformedLp + +- Is now passed the global domain (or what the worker believes the global domain currently is) + +### HighsPostsolveStack + +- Added a thread safe option (copy some data structures) when undoing transformation for a given solution. + +### HighsHash + +- No clue at all. Some C++ wizardry. + +## Expected Questions: + +- Why is everything a lambda function `HighsMipSolver`? Answer: Because it was easy to prototype and I didn't have to + worry about what parameters to pass. They could be changed to standard functions. +- Why have some many files been touched? Answer: Many files were accessing something like `mipsolver.mipdata_->domain`, + and now `worker.globaldom` has had to be passed through all the functions along that path. +- Is the current code deterministic? Answer: It should be, but there's likely still some non-determinism bugs. +- If I run in serial is the code identical to v1.12? Answer: No. I have tried to make it so, but tracking down these + small variations is time-consuming and not necessarily beneficial. +- What still needs to be done? + - Answers: + - Many hard-coded parameters will need to now consider `num_workers`. The only one I've currently changed is + `numPlungeNodes`, which had an incredible impact on performance. Which ones and to what values are an open + question, but this will be necessary for performance. Current example observation: Some problems will + instantly restart five times in a row because so many nodes are now dumped quickly to the global queue. + - Extensive determinism and bug checks. I'd like to believe there's not many bugs, but with this much code that + cannot be. + - General design review. Examples: (1) Should `HighsMipWorker` be changed (2) Is the pointer to `HighsMipWorker` + in `HighsLpRelaxation` acceptable (3) Does the way cuts and conflicts are synced make sense w.r.t. + performance? (4) Are there any potential sync opportunities being missed? (5) Do we have a new performance + bottleneck? + - Timers. I have not hacked them in to HighsMipWorker, and have therefore commented many ouy. + - Merging latest into the branch is going to be a few hours of annoyance. I've held off on doing it so we have + v1.12 to compare to. +- Is the code in a reviewable state? Answer: I've cleaned up everything, although there's still some lingering WARNINGS + and TODOS that I've left in case the design changes, e.g., for cut management. So it should be readable and relatively + clean. \ No newline at end of file