From 594ec3f42556c76bf6e3af0e959cda18febe061b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 31 Oct 2025 04:40:56 -0700 Subject: [PATCH 001/147] add initial files --- cpp/src/mip/presolve/cliques.cu | 22 ++++++++++++++++++++++ cpp/src/mip/presolve/cliques.cuh | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 cpp/src/mip/presolve/cliques.cu create mode 100644 cpp/src/mip/presolve/cliques.cuh diff --git a/cpp/src/mip/presolve/cliques.cu b/cpp/src/mip/presolve/cliques.cu new file mode 100644 index 0000000000..c02d64d824 --- /dev/null +++ b/cpp/src/mip/presolve/cliques.cu @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cliques.cuh" + +namespace cuopt::linear_programming::detail { + +} diff --git a/cpp/src/mip/presolve/cliques.cuh b/cpp/src/mip/presolve/cliques.cuh new file mode 100644 index 0000000000..73c4e704d0 --- /dev/null +++ b/cpp/src/mip/presolve/cliques.cuh @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace cuopt::linear_programming::detail { + +} From f10535fe58ca3abf8c2401ee2d09aaf1459f36e3 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 4 Nov 2025 06:51:59 -0800 Subject: [PATCH 002/147] add some comments and file name changes --- .../{cliques.cu => conflict_graph/gub_linked_list.cu} | 4 ++-- .../{cliques.cuh => conflict_graph/gub_linked_list.cuh} | 2 +- cpp/src/mip/presolve/probing_cache.cuh | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) rename cpp/src/mip/presolve/{cliques.cu => conflict_graph/gub_linked_list.cu} (84%) rename cpp/src/mip/presolve/{cliques.cuh => conflict_graph/gub_linked_list.cuh} (87%) diff --git a/cpp/src/mip/presolve/cliques.cu b/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cu similarity index 84% rename from cpp/src/mip/presolve/cliques.cu rename to cpp/src/mip/presolve/conflict_graph/gub_linked_list.cu index c02d64d824..cd4d12e78b 100644 --- a/cpp/src/mip/presolve/cliques.cu +++ b/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights * reserved. SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,7 @@ * limitations under the License. */ -#include "cliques.cuh" +#include "gub_linked_list.cuh" namespace cuopt::linear_programming::detail { diff --git a/cpp/src/mip/presolve/cliques.cuh b/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh similarity index 87% rename from cpp/src/mip/presolve/cliques.cuh rename to cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh index 73c4e704d0..e3b4f00159 100644 --- a/cpp/src/mip/presolve/cliques.cuh +++ b/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights * reserved. SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/cpp/src/mip/presolve/probing_cache.cuh b/cpp/src/mip/presolve/probing_cache.cuh index 755c18b0bb..dfae17eb33 100644 --- a/cpp/src/mip/presolve/probing_cache.cuh +++ b/cpp/src/mip/presolve/probing_cache.cuh @@ -97,7 +97,9 @@ class probing_cache_t { f_t first_probe, f_t second_probe, f_t integrality_tolerance); - + // add the results of probing cache to secondary CG structure if not already in a gub constraint. + // use the same activity computation that we will use in BP rounding. + // use GUB constraints to find fixings in bulk rounding std::unordered_map, 2>> probing_cache; std::mutex probing_cache_mutex; }; From 649062c12b84e82adf745d1a26f4c8e88f6b84b8 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 10 Nov 2025 04:44:16 -0800 Subject: [PATCH 003/147] initial data structures --- .../conflict_graph/gub_linked_list.cuh | 121 +++++++++++++++++- .../{gub_linked_list.cu => maximal_clique.cu} | 0 2 files changed, 120 insertions(+), 1 deletion(-) rename cpp/src/mip/presolve/conflict_graph/{gub_linked_list.cu => maximal_clique.cu} (100%) diff --git a/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh b/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh index e3b4f00159..e9a3abff29 100644 --- a/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh +++ b/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh @@ -19,4 +19,123 @@ namespace cuopt::linear_programming::detail { -} +template +struct gub_node_t { + i_t var_idx; + i_t cstr_idx; +}; + +// this is the GUB constraint implementation from Conflict graphs in solving integer programming +// problems (Atamturk et.al.) this is a four-way linked list, vertical direction keeps the GUB +// constraint that a variable takes part horizontal direction keeps all the vars in the current GUB +// constraint the directions are sorted by the index to make the search easier +template +struct gub_linked_list_t { + view_t view() { return view_t{nodes.data(), right.data(), left.data(), up.data(), down.data()}; } + + struct view_t { + raft::device_span> nodes; + raft::device_span right; + raft::device_span left; + raft::device_span up; + raft::device_span down; + }; + rmm::device_uvector> nodes; + // the vectors keep the indices to the nodes above + rmm::device_uvector right; + rmm::device_uvector left; + rmm::device_uvector up; + rmm::device_uvector down; +}; + +} // namespace cuopt::linear_programming::detail + +// Rounding Procedure: + +// fix set of variables x_1, x_2, x_3,... in a bulk. Consider sorting according largest size GUB +// constraint(or some other criteria). + +// compute new activities on changed constraints, given that x_1=v_1, x_2=v_2, x_3=v_3: + +// if the current constraint is GUB + +// if at least two binary vars(note that some can be full integer) are common: (needs +// binary_vars_in_bulk^2 number of checks) + +// return infeasible + +// else + +// set L_r to 1. + +// else(non-GUB constraints) + +// greedy clique partitioning algorithm: + +// set L_r = sum(all positive coefficients on binary vars) + sum(min_activity contribution on +// non-binary vars) # note that the paper doesn't contain this part, since it only deals with binary + +// # iterate only on binary variables(i.e. vertices of B- and complements of B+) + +// start with highest weight vertex (v) among unmarked and mark it + +// find maximal clique among unmarked containing the vertex: (there are various algorithms to +// find maximal clique) + +// max_clique = {v} + +// L_r -= w_v + +// # prioritization is on higher weight vertex when there are equivalent max cliques? +// # we could try BFS to search multiple greedy paths +// for each unmarked vertex(w): + +// counter = 0 + +// for each vertex(k) in max_clique: + +// if(check_if_pair_shares_an_edge(w,k)) + +// counter++ + +// if counter == max_clique.size() + +// max_clique = max_clique U {w} + +// mark w as marked + +// if(L_r > UB) return infeasible + +// remove all fixed variables(original and newly propagated) from the conflict graph. !!!!!! still a +// bit unclear how to remove it from the adjaceny list data structure since it only supports +// additions!!!! + +// add newly discovered GUB constraints into dynamic adjacency list + +// do double probing to infer new edges(we need a heuristic to choose which pairs to probe) + +// check_if_pair_shares_an_edge(w,v): + +// check GUB constraints by traversing the double linked list: + +// on the column of variable w: + +// for each row: + +// if v is contained on the row + +// return true + +// check added edges on adjacency list: + +// k <- last[w] + +// while k != 0 + +// if(adj[k] == v) + +// return true + +// k <-next[k] + +// return false diff --git a/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cu b/cpp/src/mip/presolve/conflict_graph/maximal_clique.cu similarity index 100% rename from cpp/src/mip/presolve/conflict_graph/gub_linked_list.cu rename to cpp/src/mip/presolve/conflict_graph/maximal_clique.cu From fe4cc7abfd990f4e8ea83e739a056879362060ca Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 12 Nov 2025 08:02:52 -0800 Subject: [PATCH 004/147] find all initial cliques --- cpp/CMakeLists.txt | 6 + cpp/src/dual_simplex/sparse_matrix.cpp | 6 + cpp/src/dual_simplex/sparse_matrix.hpp | 5 +- cpp/src/mip/CMakeLists.txt | 1 + .../presolve/conflict_graph/clique_table.cu | 235 ++++++++++++++++++ .../{gub_linked_list.cuh => clique_table.cuh} | 52 ++-- .../presolve/conflict_graph/maximal_clique.cu | 22 -- cpp/src/mip/solver.cu | 4 +- 8 files changed, 284 insertions(+), 47 deletions(-) create mode 100644 cpp/src/mip/presolve/conflict_graph/clique_table.cu rename cpp/src/mip/presolve/conflict_graph/{gub_linked_list.cuh => clique_table.cuh} (76%) delete mode 100644 cpp/src/mip/presolve/conflict_graph/maximal_clique.cu diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 70c6ea0cc7..b7493af5d4 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -456,6 +456,12 @@ endif() option(BUILD_MIP_BENCHMARKS "Build MIP benchmarks" OFF) if(BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY) add_executable(solve_MIP ../benchmarks/linear_programming/cuopt/run_mip.cpp) + target_include_directories(solve_MIP + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" + PUBLIC + "$" + ) target_compile_options(solve_MIP PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" "$<$:${CUOPT_CUDA_FLAGS}>" diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index 427f1049d3..45471d5dbb 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -572,6 +572,12 @@ void csr_matrix_t::check_matrix() const } } +template +std::pair csr_matrix_t::get_constraint_range(i_t cstr_idx) const +{ + return std::make_pair(this->row_start[cstr_idx], this->row_start[cstr_idx + 1]); +} + // x <- x + alpha * A(:, j) template void scatter_dense(const csc_matrix_t& A, i_t j, f_t alpha, std::vector& x) diff --git a/cpp/src/dual_simplex/sparse_matrix.hpp b/cpp/src/dual_simplex/sparse_matrix.hpp index a3db2f8ebe..40ca7aaf2f 100644 --- a/cpp/src/dual_simplex/sparse_matrix.hpp +++ b/cpp/src/dual_simplex/sparse_matrix.hpp @@ -149,12 +149,15 @@ class csr_matrix_t { // Ensures no repeated column indices within a row void check_matrix() const; + // get constraint range + std::pair get_constraint_range(i_t cstr_idx) const; + i_t nz_max; // maximum number of nonzero entries i_t m; // number of rows i_t n; // number of cols std::vector row_start; // row pointers (size m + 1) std::vector j; // column inidices, size nz_max - std::vector x; // numerical valuse, size nz_max + std::vector x; // numerical values, size nz_max static_assert(std::is_signed_v); }; diff --git a/cpp/src/mip/CMakeLists.txt b/cpp/src/mip/CMakeLists.txt index 2d11a8dcb2..aeeeeb2437 100644 --- a/cpp/src/mip/CMakeLists.txt +++ b/cpp/src/mip/CMakeLists.txt @@ -46,6 +46,7 @@ set(MIP_NON_LP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/presolve/multi_probe.cu ${CMAKE_CURRENT_SOURCE_DIR}/presolve/probing_cache.cu ${CMAKE_CURRENT_SOURCE_DIR}/presolve/trivial_presolve.cu + ${CMAKE_CURRENT_SOURCE_DIR}/presolve/conflict_graph/clique_table.cu ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/feasibility_jump.cu ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/feasibility_jump_kernels.cu ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/fj_cpu.cu) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu new file mode 100644 index 0000000000..09e5c2b0ef --- /dev/null +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -0,0 +1,235 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define DEBUG_KNAPSACK_CONSTRAINTS 1 + +#include "clique_table.cuh" + +#include +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +// do constraints with only binary variables. +template +void find_cliques_from_constraint(const knapsack_constraint_t& kc, + clique_table_t& clique_table) +{ + i_t size = kc.entries.size(); + cuopt_assert(size > 1, "Constraint has not enough variables"); + if (kc.entries[size - 1].val + kc.entries[size - 2].val <= kc.rhs) { return; } + std::vector clique; + i_t k = size - 1; + // find the first clique, which is the largest + // FIXME: do binary search + while (k >= 0) { + if (kc.entries[k].val + kc.entries[k - 1].val <= kc.rhs) { break; } + clique.push_back(kc.entries[k].col); + k--; + } + clique_table.first.push_back(clique); + const i_t original_clique_start_idx = k; + // find the additional cliques + k--; + while (k >= 0) { + f_t curr_val = kc.entries[k].val; + i_t curr_col = kc.entries[k].col; + // do a binary search in the clique coefficients to find f, such that coeff_k + coeff_f > rhs + // this means that we get a subset of the original clique and extend it with a variable + f_t val_to_find = kc.rhs - curr_val + 1e-6; + auto it = std::lower_bound( + kc.entries.begin() + original_clique_start_idx, kc.entries.end(), val_to_find); + if (it != kc.entries.end()) { + i_t position_on_knapsack_constraint = std::distance(kc.entries.begin(), it); + i_t start_pos_on_clique = position_on_knapsack_constraint - original_clique_start_idx; + cuopt_assert(start_pos_on_clique >= 1, "Start position on clique is negative"); + cuopt_assert(it->val + curr_val > kc.rhs, "RHS mismatch"); +#if DEBUG_KNAPSACK_CONSTRAINTS + CUOPT_LOG_DEBUG("Found additional clique: %d, %d, %d", + curr_col, + clique_table.first.size() - 1, + start_pos_on_clique); +#endif + clique_table.addtl_cliques.push_back( + {curr_col, (i_t)clique_table.first.size() - 1, start_pos_on_clique}); + } else { + break; + } + k--; + } +} + +// sort CSR by constraint coefficients +template +void sort_csr_by_constraint_coefficients( + std::vector>& knapsack_constraints) +{ + // sort the rows of the CSR matrix by the coefficients of the constraint + for (auto& knapsack_constraint : knapsack_constraints) { + std::sort(knapsack_constraint.entries.begin(), knapsack_constraint.entries.end()); + } +} + +template +void make_coeff_positive_knapsack_constraint( + const dual_simplex::user_problem_t& problem, + std::vector>& knapsack_constraints) +{ + for (auto& knapsack_constraint : knapsack_constraints) { + f_t rhs_offset = 0; + for (auto& entry : knapsack_constraint.entries) { + if (entry.val < 0) { + entry.val = -entry.val; + rhs_offset += entry.val; + // negation of a variable is var + num_cols + entry.col = entry.col + problem.num_cols; + } + } + knapsack_constraint.rhs += rhs_offset; + cuopt_assert(knapsack_constraint.rhs >= 0, "RHS must be non-negative"); + } +} + +// convert all the knapsack constraints +// if a binary variable has a negative coefficient, put its negation in the constraint +template +void fill_knapsack_constraints(const dual_simplex::user_problem_t& problem, + std::vector>& knapsack_constraints) +{ + dual_simplex::csr_matrix_t A(0, 0, 0); + problem.A.to_compressed_row(A); + // we might add additional constraints for the equality constraints + i_t added_constraints = 0; + for (i_t i = 0; i < A.m; i++) { + std::pair constraint_range = A.get_constraint_range(i); + if (constraint_range.second - constraint_range.first < 2) { + CUOPT_LOG_DEBUG("Constraint %d has less than 2 variables, skipping", i); + continue; + } + bool all_binary = true; + // check if all variables are binary + for (i_t j = constraint_range.first; j < constraint_range.second; j++) { + if (problem.var_types[A.j[j]] != dual_simplex::variable_type_t::INTEGER || + problem.lower[A.j[j]] != 0 || problem.upper[A.j[j]] != 1) { + all_binary = false; + break; + } + } + // if all variables are binary, convert the constraint to a knapsack constraint + if (!all_binary) { continue; } + knapsack_constraint_t knapsack_constraint; + + knapsack_constraint.cstr_idx = i; + if (problem.row_sense[i] == 'L') { + knapsack_constraint.rhs = problem.rhs[i]; + for (i_t j = constraint_range.first; j < constraint_range.second; j++) { + knapsack_constraint.entries.push_back({A.j[j], A.x[j]}); + } + } else if (problem.row_sense[i] == 'G') { + knapsack_constraint.rhs = -problem.rhs[i]; + for (i_t j = constraint_range.first; j < constraint_range.second; j++) { + knapsack_constraint.entries.push_back({A.j[j], -A.x[j]}); + } + } else if (problem.row_sense[i] == 'E') { + // less than part + knapsack_constraint.rhs = problem.rhs[i]; + for (i_t j = constraint_range.first; j < constraint_range.second; j++) { + knapsack_constraint.entries.push_back({A.j[j], A.x[j]}); + } + // greater than part: convert it to less than + knapsack_constraint_t knapsack_constraint2; + knapsack_constraint2.cstr_idx = A.m + added_constraints++; + knapsack_constraint2.rhs = -problem.rhs[i]; + for (i_t j = constraint_range.first; j < constraint_range.second; j++) { + knapsack_constraint2.entries.push_back({A.j[j], -A.x[j]}); + } + knapsack_constraints.push_back(knapsack_constraint2); + } + knapsack_constraints.push_back(knapsack_constraint); + } + CUOPT_LOG_DEBUG("Number of knapsack constraints: %d added %d constraints", + knapsack_constraints.size(), + added_constraints); +} + +template +void print_knapsack_constraints( + const std::vector>& knapsack_constraints) +{ +#if DEBUG_KNAPSACK_CONSTRAINTS + std::cout << "Number of knapsack constraints: " << knapsack_constraints.size() << "\n"; + for (const auto& knapsack : knapsack_constraints) { + std::cout << "Knapsack constraint idx: " << knapsack.cstr_idx << "\n"; + std::cout << " RHS: " << knapsack.rhs << "\n"; + std::cout << " Entries:\n"; + for (const auto& entry : knapsack.entries) { + std::cout << " col: " << entry.col << ", val: " << entry.val << "\n"; + } + std::cout << "----------\n"; + } +#endif +} + +template +void print_clique_table(const clique_table_t& clique_table) +{ +#if DEBUG_KNAPSACK_CONSTRAINTS + std::cout << "Number of cliques: " << clique_table.first.size() << "\n"; + for (const auto& clique : clique_table.first) { + std::cout << "Clique: "; + for (const auto& var : clique) { + std::cout << var << " "; + } + } + std::cout << "Number of additional cliques: " << clique_table.addtl_cliques.size() << "\n"; + for (const auto& addtl_clique : clique_table.addtl_cliques) { + std::cout << "Additional clique: " << addtl_clique.vertex_idx << ", " << addtl_clique.clique_idx + << ", " << addtl_clique.start_pos_on_clique << "\n"; + } +#endif +} + +template +void find_initial_cliques(const dual_simplex::user_problem_t& problem) +{ + std::vector> knapsack_constraints; + fill_knapsack_constraints(problem, knapsack_constraints); + make_coeff_positive_knapsack_constraint(problem, knapsack_constraints); + sort_csr_by_constraint_coefficients(knapsack_constraints); + // print_knapsack_constraints(knapsack_constraints); + clique_table_t clique_table; + for (const auto& knapsack_constraint : knapsack_constraints) { + find_cliques_from_constraint(knapsack_constraint, clique_table); + } + print_clique_table(clique_table); + exit(0); +} + +#define INSTANTIATE(F_TYPE) \ + template void find_initial_cliques( \ + const dual_simplex::user_problem_t& problem); +#if MIP_INSTANTIATE_FLOAT +INSTANTIATE(float) +#endif +#if MIP_INSTANTIATE_DOUBLE +INSTANTIATE(double) +#endif +#undef INSTANTIATE + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh similarity index 76% rename from cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh rename to cpp/src/mip/presolve/conflict_graph/clique_table.cuh index e9a3abff29..a1e7036b13 100644 --- a/cpp/src/mip/presolve/conflict_graph/gub_linked_list.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -17,37 +17,45 @@ #pragma once +#include + +#include + namespace cuopt::linear_programming::detail { template -struct gub_node_t { - i_t var_idx; +struct entry_t { + i_t col; + f_t val; + bool operator<(const entry_t& other) const { return val < other.val; } + bool operator<(double other) const { return val < other; } +}; + +template +struct knapsack_constraint_t { + std::vector> entries; + f_t rhs; i_t cstr_idx; }; -// this is the GUB constraint implementation from Conflict graphs in solving integer programming -// problems (Atamturk et.al.) this is a four-way linked list, vertical direction keeps the GUB -// constraint that a variable takes part horizontal direction keeps all the vars in the current GUB -// constraint the directions are sorted by the index to make the search easier template -struct gub_linked_list_t { - view_t view() { return view_t{nodes.data(), right.data(), left.data(), up.data(), down.data()}; } - - struct view_t { - raft::device_span> nodes; - raft::device_span right; - raft::device_span left; - raft::device_span up; - raft::device_span down; - }; - rmm::device_uvector> nodes; - // the vectors keep the indices to the nodes above - rmm::device_uvector right; - rmm::device_uvector left; - rmm::device_uvector up; - rmm::device_uvector down; +struct addtl_clique_t { + i_t vertex_idx; + i_t clique_idx; + i_t start_pos_on_clique; }; +template +struct clique_table_t { + // keeps the large cliques in each constraint + std::vector> first; + // keeps the additional cliques + std::vector> addtl_cliques; +}; + +template +void find_initial_cliques(const dual_simplex::user_problem_t& problem); + } // namespace cuopt::linear_programming::detail // Rounding Procedure: diff --git a/cpp/src/mip/presolve/conflict_graph/maximal_clique.cu b/cpp/src/mip/presolve/conflict_graph/maximal_clique.cu deleted file mode 100644 index cd4d12e78b..0000000000 --- a/cpp/src/mip/presolve/conflict_graph/maximal_clique.cu +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights - * reserved. SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "gub_linked_list.cuh" - -namespace cuopt::linear_programming::detail { - -} diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 0114882b03..7e0b10637d 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -15,12 +15,11 @@ * limitations under the License. */ -#include "feasibility_jump/feasibility_jump.cuh" - #include #include "diversity/diversity_manager.cuh" #include "local_search/local_search.cuh" #include "local_search/rounding/simple_rounding.cuh" +#include "presolve/conflict_graph/clique_table.cuh" #include "solver.cuh" #include @@ -162,6 +161,7 @@ solution_t mip_solver_t::run_solver() if (!context.settings.heuristics_only) { // Convert the presolved problem to dual_simplex::user_problem_t op_problem_.get_host_user_problem(branch_and_bound_problem); + find_initial_cliques(branch_and_bound_problem); // Resize the solution now that we know the number of columns/variables branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); From 1d46ec9b2dd647c0d061676234543085dae1e48e Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 13 Nov 2025 03:11:08 -0800 Subject: [PATCH 005/147] remove small cliques --- .../presolve/conflict_graph/clique_table.cu | 71 +++++++++++++++++-- .../presolve/conflict_graph/clique_table.cuh | 29 +++++++- cpp/src/mip/problem/problem.cu | 1 - cpp/src/mip/solver.cu | 2 +- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 09e5c2b0ef..cd8dddb2d9 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -52,7 +52,7 @@ void find_cliques_from_constraint(const knapsack_constraint_t& kc, i_t curr_col = kc.entries[k].col; // do a binary search in the clique coefficients to find f, such that coeff_k + coeff_f > rhs // this means that we get a subset of the original clique and extend it with a variable - f_t val_to_find = kc.rhs - curr_val + 1e-6; + f_t val_to_find = kc.rhs - curr_val + clique_table.tolerances.absolute_tolerance; auto it = std::lower_bound( kc.entries.begin() + original_clique_start_idx, kc.entries.end(), val_to_find); if (it != kc.entries.end()) { @@ -168,6 +168,55 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro added_constraints); } +template +void remove_small_cliques(clique_table_t& clique_table) +{ + i_t num_removed_first = 0; + i_t num_removed_addtl = 0; + std::vector to_delete(clique_table.addtl_cliques.size(), false); + // if a clique is small, we remove it from the cliques and add it to adjlist + for (size_t clique_idx = 0; clique_idx < clique_table.first.size(); clique_idx++) { + const auto& clique = clique_table.first[clique_idx]; + if (clique.size() < (size_t)clique_table.min_clique_size) { + for (size_t i = 0; i < clique.size(); i++) { + for (size_t j = 0; j < clique.size(); j++) { + if (i == j) { continue; } + clique_table.adj_list_small_cliques[clique[i]].insert(clique[j]); + } + } + clique_table.first.erase(clique_table.first.begin() + clique_idx); + num_removed_first++; + to_delete[clique_idx] = true; + } + } + for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { + const auto& addtl_clique = clique_table.addtl_cliques[addtl_c]; + if (clique_table.first[addtl_clique.clique_idx].size() < (size_t)clique_table.min_clique_size) { + // the items from first clique are already added to the adjlist + // only add the items that are coming from the new var in the additional clique + for (size_t i = addtl_clique.start_pos_on_clique; + i < clique_table.first[addtl_clique.clique_idx].size(); + i++) { + // insert conflicts both way + clique_table.adj_list_small_cliques[clique_table.first[addtl_clique.clique_idx][i]].insert( + addtl_clique.vertex_idx); + clique_table.adj_list_small_cliques[addtl_clique.vertex_idx].insert( + clique_table.first[addtl_clique.clique_idx][i]); + } + clique_table.addtl_cliques.erase(clique_table.addtl_cliques.begin() + addtl_c); + num_removed_addtl++; + } + } + CUOPT_LOG_DEBUG("Number of removed cliques from first: %d, additional: %d", + num_removed_first, + num_removed_addtl); + size_t i = 0; + auto it = std::remove_if(clique_table.first.begin(), clique_table.first.end(), [&](auto& clique) { + return to_delete[i++]; + }); + clique_table.first.erase(it, clique_table.first.end()); +} + template void print_knapsack_constraints( const std::vector>& knapsack_constraints) @@ -206,24 +255,32 @@ void print_clique_table(const clique_table_t& clique_table) } template -void find_initial_cliques(const dual_simplex::user_problem_t& problem) +void find_initial_cliques(const dual_simplex::user_problem_t& problem, + typename mip_solver_settings_t::tolerances_t tolerances) { std::vector> knapsack_constraints; fill_knapsack_constraints(problem, knapsack_constraints); make_coeff_positive_knapsack_constraint(problem, knapsack_constraints); sort_csr_by_constraint_coefficients(knapsack_constraints); // print_knapsack_constraints(knapsack_constraints); - clique_table_t clique_table; + // TODO think about getting min_clique_size according to some problem property + clique_config_t clique_config; + clique_table_t clique_table(2 * problem.num_cols, clique_config.min_clique_size); + clique_table.tolerances = tolerances; for (const auto& knapsack_constraint : knapsack_constraints) { find_cliques_from_constraint(knapsack_constraint, clique_table); } - print_clique_table(clique_table); + // print_clique_table(clique_table); + // remove small cliques and add them to adj_list + remove_small_cliques(clique_table); + exit(0); } -#define INSTANTIATE(F_TYPE) \ - template void find_initial_cliques( \ - const dual_simplex::user_problem_t& problem); +#define INSTANTIATE(F_TYPE) \ + template void find_initial_cliques( \ + const dual_simplex::user_problem_t& problem, \ + typename mip_solver_settings_t::tolerances_t tolerances); #if MIP_INSTANTIATE_FLOAT INSTANTIATE(float) #endif diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index a1e7036b13..3523f98966 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -17,12 +17,19 @@ #pragma once +#include #include +#include +#include #include namespace cuopt::linear_programming::detail { +struct clique_config_t { + int min_clique_size = 3; +}; + template struct entry_t { i_t col; @@ -47,18 +54,36 @@ struct addtl_clique_t { template struct clique_table_t { + clique_table_t(i_t n_vertices, i_t min_clique_size_) + : min_clique_size(min_clique_size_), + var_clique_map_first(n_vertices), + var_clique_map_addtl(n_vertices), + adj_list_small_cliques(n_vertices) + { + } // keeps the large cliques in each constraint std::vector> first; // keeps the additional cliques std::vector> addtl_cliques; + // keeps the indices of original(first) cliques that contain variable x + std::vector> var_clique_map_first; + // keeps the indices of additional cliques that contain variable x + std::vector> var_clique_map_addtl; + // adjacency list to keep small cliques, this basically keeps the vars share a small clique + // constraint + std::unordered_map> adj_list_small_cliques; + + const i_t min_clique_size; + typename mip_solver_settings_t::tolerances_t tolerances; }; template -void find_initial_cliques(const dual_simplex::user_problem_t& problem); +void find_initial_cliques(const dual_simplex::user_problem_t& problem, + typename mip_solver_settings_t::tolerances_t tolerances); } // namespace cuopt::linear_programming::detail -// Rounding Procedure: +// Possible application to rounding procedure, keeping it as reference // fix set of variables x_1, x_2, x_3,... in a bulk. Consider sorting according largest size GUB // constraint(or some other criteria). diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 91836dd102..c239f39a4d 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -1561,7 +1561,6 @@ void problem_t::get_host_user_problem( csr_A.row_start = cuopt::host_copy(offsets); csr_A.to_compressed_col(user_problem.A); - user_problem.rhs.resize(m); user_problem.row_sense.resize(m); user_problem.range_rows.clear(); diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 7e0b10637d..f25e8cd986 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -161,7 +161,7 @@ solution_t mip_solver_t::run_solver() if (!context.settings.heuristics_only) { // Convert the presolved problem to dual_simplex::user_problem_t op_problem_.get_host_user_problem(branch_and_bound_problem); - find_initial_cliques(branch_and_bound_problem); + find_initial_cliques(branch_and_bound_problem, context.settings.tolerances); // Resize the solution now that we know the number of columns/variables branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); From 1474bc501742c0f2a249fb55b49f7294d8366044 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 13 Nov 2025 04:36:06 -0800 Subject: [PATCH 006/147] renumber cliques on addlt --- .../presolve/conflict_graph/clique_table.cu | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index cd8dddb2d9..cdce286900 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -184,14 +184,15 @@ void remove_small_cliques(clique_table_t& clique_table) clique_table.adj_list_small_cliques[clique[i]].insert(clique[j]); } } - clique_table.first.erase(clique_table.first.begin() + clique_idx); num_removed_first++; to_delete[clique_idx] = true; } } for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { const auto& addtl_clique = clique_table.addtl_cliques[addtl_c]; - if (clique_table.first[addtl_clique.clique_idx].size() < (size_t)clique_table.min_clique_size) { + i_t size_of_clique = + clique_table.first[addtl_clique.clique_idx].size() - addtl_clique.start_pos_on_clique + 1; + if (size_of_clique < clique_table.min_clique_size) { // the items from first clique are already added to the adjlist // only add the items that are coming from the new var in the additional clique for (size_t i = addtl_clique.start_pos_on_clique; @@ -204,17 +205,40 @@ void remove_small_cliques(clique_table_t& clique_table) clique_table.first[addtl_clique.clique_idx][i]); } clique_table.addtl_cliques.erase(clique_table.addtl_cliques.begin() + addtl_c); + addtl_c--; num_removed_addtl++; } } CUOPT_LOG_DEBUG("Number of removed cliques from first: %d, additional: %d", num_removed_first, num_removed_addtl); - size_t i = 0; + size_t i = 0; + size_t old_idx = 0; + std::vector index_mapping(clique_table.first.size(), -1); auto it = std::remove_if(clique_table.first.begin(), clique_table.first.end(), [&](auto& clique) { - return to_delete[i++]; + bool res = false; + if (to_delete[old_idx]) { + res = true; + } else { + index_mapping[old_idx] = i++; + } + old_idx++; + return res; }); clique_table.first.erase(it, clique_table.first.end()); + // renumber the reference indices in the additional cliques, since we removed some cliques + for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { + i_t new_clique_idx = index_mapping[clique_table.addtl_cliques[addtl_c].clique_idx]; + CUOPT_LOG_DEBUG("New clique index: %d original: %d", + new_clique_idx, + clique_table.addtl_cliques[addtl_c].clique_idx); + cuopt_assert(new_clique_idx != -1, "New clique index is -1"); + clique_table.addtl_cliques[addtl_c].clique_idx = new_clique_idx; + cuopt_assert(clique_table.first[new_clique_idx].size() - + clique_table.addtl_cliques[addtl_c].start_pos_on_clique + 1 >= + (size_t)clique_table.min_clique_size, + "A small clique remained after removing small cliques"); + } } template @@ -270,6 +294,9 @@ void find_initial_cliques(const dual_simplex::user_problem_t& problem, for (const auto& knapsack_constraint : knapsack_constraints) { find_cliques_from_constraint(knapsack_constraint, clique_table); } + CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d", + clique_table.first.size(), + clique_table.addtl_cliques.size()); // print_clique_table(clique_table); // remove small cliques and add them to adj_list remove_small_cliques(clique_table); From b82f63f2bf177f92bec01eb09a59a2cc56b014a3 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 17 Nov 2025 02:01:29 -0800 Subject: [PATCH 007/147] clique extension is working --- .../presolve/conflict_graph/clique_table.cu | 158 ++++++++++++++++-- .../presolve/conflict_graph/clique_table.cuh | 23 ++- 2 files changed, 165 insertions(+), 16 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index cdce286900..73b5c21dfd 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -89,10 +90,13 @@ void sort_csr_by_constraint_coefficients( template void make_coeff_positive_knapsack_constraint( const dual_simplex::user_problem_t& problem, - std::vector>& knapsack_constraints) + std::vector>& knapsack_constraints, + typename mip_solver_settings_t::tolerances_t tolerances) { for (auto& knapsack_constraint : knapsack_constraints) { - f_t rhs_offset = 0; + f_t rhs_offset = 0; + bool all_coeff_are_equal = true; + f_t first_coeff = std::abs(knapsack_constraint.entries[0].val); for (auto& entry : knapsack_constraint.entries) { if (entry.val < 0) { entry.val = -entry.val; @@ -100,8 +104,15 @@ void make_coeff_positive_knapsack_constraint( // negation of a variable is var + num_cols entry.col = entry.col + problem.num_cols; } + if (!integer_equal(entry.val, first_coeff, tolerances.absolute_tolerance)) { + all_coeff_are_equal = false; + } } knapsack_constraint.rhs += rhs_offset; + if (!integer_equal(knapsack_constraint.rhs, first_coeff, tolerances.absolute_tolerance)) { + all_coeff_are_equal = false; + } + knapsack_constraint.is_set_packing = all_coeff_are_equal; cuopt_assert(knapsack_constraint.rhs >= 0, "RHS must be non-negative"); } } @@ -173,7 +184,7 @@ void remove_small_cliques(clique_table_t& clique_table) { i_t num_removed_first = 0; i_t num_removed_addtl = 0; - std::vector to_delete(clique_table.addtl_cliques.size(), false); + std::vector to_delete(clique_table.first.size(), false); // if a clique is small, we remove it from the cliques and add it to adjlist for (size_t clique_idx = 0; clique_idx < clique_table.first.size(); clique_idx++) { const auto& clique = clique_table.first[clique_idx]; @@ -229,9 +240,6 @@ void remove_small_cliques(clique_table_t& clique_table) // renumber the reference indices in the additional cliques, since we removed some cliques for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { i_t new_clique_idx = index_mapping[clique_table.addtl_cliques[addtl_c].clique_idx]; - CUOPT_LOG_DEBUG("New clique index: %d original: %d", - new_clique_idx, - clique_table.addtl_cliques[addtl_c].clique_idx); cuopt_assert(new_clique_idx != -1, "New clique index is -1"); clique_table.addtl_cliques[addtl_c].clique_idx = new_clique_idx; cuopt_assert(clique_table.first[new_clique_idx].size() - @@ -241,15 +249,130 @@ void remove_small_cliques(clique_table_t& clique_table) } } +template +std::unordered_set clique_table_t::get_adj_set_of_var(i_t var_idx) +{ + std::unordered_set adj_set; + for (const auto& clique_idx : var_clique_map_first[var_idx]) { + adj_set.insert(first[clique_idx].begin(), first[clique_idx].end()); + } + + for (const auto& addtl_clique_idx : var_clique_map_addtl[var_idx]) { + adj_set.insert(first[addtl_cliques[addtl_clique_idx].clique_idx].begin(), + first[addtl_cliques[addtl_clique_idx].clique_idx].end()); + } + + for (const auto& adj_vertex : adj_list_small_cliques[var_idx]) { + adj_set.insert(adj_vertex); + } + return adj_set; +} + +template +i_t clique_table_t::get_degree_of_var(i_t var_idx) +{ + // if it is not already computed, compute it and return + if (var_degrees[var_idx] == -1) { var_degrees[var_idx] = get_adj_set_of_var(var_idx).size(); } + return var_degrees[var_idx]; +} + +template +bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) +{ + return var_clique_map_first[var_idx1].count(var_idx2) > 0 || + var_clique_map_addtl[var_idx1].count(var_idx2) > 0 || + adj_list_small_cliques[var_idx1].count(var_idx2) > 0; +} + +template +void extend_clique(const std::vector& clique, clique_table_t& clique_table) +{ + i_t smallest_degree = std::numeric_limits::max(); + i_t smallest_degree_var = -1; + // find smallest degree vertex in the current set packing constraint + for (size_t idx = 0; idx < clique.size(); idx++) { + i_t var_idx = clique[idx]; + i_t degree = clique_table.get_degree_of_var(var_idx); + if (degree < smallest_degree) { + smallest_degree = degree; + smallest_degree_var = var_idx; + } + } + std::vector extension_candidates; + auto smallest_degree_adj_set = clique_table.get_adj_set_of_var(smallest_degree_var); + extension_candidates.insert( + extension_candidates.end(), smallest_degree_adj_set.begin(), smallest_degree_adj_set.end()); + std::sort(extension_candidates.begin(), extension_candidates.end(), [&](i_t a, i_t b) { + return clique_table.get_degree_of_var(a) > clique_table.get_degree_of_var(b); + }); + auto new_clique = clique; + for (size_t idx = 0; idx < extension_candidates.size(); idx++) { + i_t var_idx = extension_candidates[idx]; + bool add = true; + for (size_t i = 0; i < new_clique.size(); i++) { + // check if the tested variable conflicts with all vars in the new clique + if (!clique_table.check_adjacency(var_idx, new_clique[i])) { + add = false; + break; + } + } + if (add) { new_clique.push_back(var_idx); } + } + // if we found a larger cliqe, replace it in the clique table and replace the problem formulation + if (new_clique.size() > clique.size()) { + clique_table.first.push_back(new_clique); + CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); + } +} + +// Also known as clique merging. Infer larger clique constraints which allows inclusion of vars from +// other constraints. This only extends the original cliques in the formulation for now. +// TODO: consider a heuristic on how much of the cliques derived from knapsacks to include here +template +void extend_cliques(const std::vector>& knapsack_constraints, + clique_table_t& clique_table) +{ + // we try extending cliques on set packing constraints + for (const auto& knapsack_constraint : knapsack_constraints) { + if (!knapsack_constraint.is_set_packing) { continue; } + if (knapsack_constraint.entries.size() < (size_t)clique_table.max_clique_size_for_extension) { + std::vector clique; + for (const auto& entry : knapsack_constraint.entries) { + clique.push_back(entry.col); + } + extend_clique(clique, clique_table); + } + } +} + +template +void fill_var_clique_maps(clique_table_t& clique_table) +{ + for (size_t clique_idx = 0; clique_idx < clique_table.first.size(); clique_idx++) { + const auto& clique = clique_table.first[clique_idx]; + for (size_t idx = 0; idx < clique.size(); idx++) { + i_t var_idx = clique[idx]; + clique_table.var_clique_map_first[var_idx].insert(clique_idx); + } + } + for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { + const auto& addtl_clique = clique_table.addtl_cliques[addtl_c]; + clique_table.var_clique_map_addtl[addtl_clique.vertex_idx].insert(addtl_c); + } +} + template void print_knapsack_constraints( - const std::vector>& knapsack_constraints) + const std::vector>& knapsack_constraints, + bool print_only_set_packing = false) { #if DEBUG_KNAPSACK_CONSTRAINTS std::cout << "Number of knapsack constraints: " << knapsack_constraints.size() << "\n"; for (const auto& knapsack : knapsack_constraints) { + if (print_only_set_packing && !knapsack.is_set_packing) { continue; } std::cout << "Knapsack constraint idx: " << knapsack.cstr_idx << "\n"; std::cout << " RHS: " << knapsack.rhs << "\n"; + std::cout << " Is set packing: " << knapsack.is_set_packing << "\n"; std::cout << " Entries:\n"; for (const auto& entry : knapsack.entries) { std::cout << " col: " << entry.col << ", val: " << entry.val << "\n"; @@ -284,12 +407,14 @@ void find_initial_cliques(const dual_simplex::user_problem_t& problem, { std::vector> knapsack_constraints; fill_knapsack_constraints(problem, knapsack_constraints); - make_coeff_positive_knapsack_constraint(problem, knapsack_constraints); + make_coeff_positive_knapsack_constraint(problem, knapsack_constraints, tolerances); sort_csr_by_constraint_coefficients(knapsack_constraints); - // print_knapsack_constraints(knapsack_constraints); + print_knapsack_constraints(knapsack_constraints); // TODO think about getting min_clique_size according to some problem property clique_config_t clique_config; - clique_table_t clique_table(2 * problem.num_cols, clique_config.min_clique_size); + clique_table_t clique_table(2 * problem.num_cols, + clique_config.min_clique_size, + clique_config.max_clique_size_for_extension); clique_table.tolerances = tolerances; for (const auto& knapsack_constraint : knapsack_constraints) { find_cliques_from_constraint(knapsack_constraint, clique_table); @@ -300,7 +425,9 @@ void find_initial_cliques(const dual_simplex::user_problem_t& problem, // print_clique_table(clique_table); // remove small cliques and add them to adj_list remove_small_cliques(clique_table); - + // fill var clique maps + fill_var_clique_maps(clique_table); + extend_cliques(knapsack_constraints, clique_table); exit(0); } @@ -308,6 +435,7 @@ void find_initial_cliques(const dual_simplex::user_problem_t& problem, template void find_initial_cliques( \ const dual_simplex::user_problem_t& problem, \ typename mip_solver_settings_t::tolerances_t tolerances); + #if MIP_INSTANTIATE_FLOAT INSTANTIATE(float) #endif @@ -316,4 +444,12 @@ INSTANTIATE(double) #endif #undef INSTANTIATE +// #if MIP_INSTANTIATE_FLOAT +// template class bound_presolve_t; +// #endif + +// #if MIP_INSTANTIATE_DOUBLE +// template class bound_presolve_t; +// #endif + } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index 3523f98966..6c7be483bc 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -27,7 +27,8 @@ namespace cuopt::linear_programming::detail { struct clique_config_t { - int min_clique_size = 3; + int min_clique_size = 512; + int max_clique_size_for_extension = 128; }; template @@ -43,6 +44,7 @@ struct knapsack_constraint_t { std::vector> entries; f_t rhs; i_t cstr_idx; + bool is_set_packing = false; }; template @@ -54,26 +56,37 @@ struct addtl_clique_t { template struct clique_table_t { - clique_table_t(i_t n_vertices, i_t min_clique_size_) + clique_table_t(i_t n_vertices, i_t min_clique_size_, i_t max_clique_size_for_extension_) : min_clique_size(min_clique_size_), + max_clique_size_for_extension(max_clique_size_for_extension_), var_clique_map_first(n_vertices), var_clique_map_addtl(n_vertices), - adj_list_small_cliques(n_vertices) + adj_list_small_cliques(n_vertices), + var_degrees(n_vertices, -1) { } + + std::unordered_set get_adj_set_of_var(i_t var_idx); + i_t get_degree_of_var(i_t var_idx); + bool check_adjacency(i_t var_idx1, i_t var_idx2); + // keeps the large cliques in each constraint std::vector> first; // keeps the additional cliques std::vector> addtl_cliques; + // TODO figure out the performance of lookup for the following: unordered_set vs vector // keeps the indices of original(first) cliques that contain variable x - std::vector> var_clique_map_first; + std::vector> var_clique_map_first; // keeps the indices of additional cliques that contain variable x - std::vector> var_clique_map_addtl; + std::vector> var_clique_map_addtl; // adjacency list to keep small cliques, this basically keeps the vars share a small clique // constraint std::unordered_map> adj_list_small_cliques; + // degrees of each vertex + std::vector var_degrees; const i_t min_clique_size; + const i_t max_clique_size_for_extension; typename mip_solver_settings_t::tolerances_t tolerances; }; From 60edd8eb3adadafa7866c0aeb6bfe78b6a71a5ad Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 18 Nov 2025 03:30:23 -0800 Subject: [PATCH 008/147] add extended cliques into formulation --- cpp/src/dual_simplex/sparse_matrix.cpp | 12 ++++ cpp/src/dual_simplex/sparse_matrix.hpp | 3 + cpp/src/dual_simplex/user_problem.hpp | 8 +++ .../presolve/conflict_graph/clique_table.cu | 70 +++++++++++++++---- .../presolve/conflict_graph/clique_table.cuh | 2 +- 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index b4fda11f56..395b0f9958 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -581,6 +581,18 @@ void scatter_dense(const csc_matrix_t& A, i_t j, f_t alpha, std::vecto } } +template +void csr_matrix_t::insert_row(const std::vector& vars, + const std::vector& coeffs) +{ + // insert the row into the matrix + this->row_start.push_back(this->m); + this->m++; + this->nz_max += vars.size(); + this->j.insert(this->j.end(), vars.begin(), vars.end()); + this->x.insert(this->x.end(), coeffs.begin(), coeffs.end()); +} + // x <- x + alpha * A(:, j) template void scatter_dense(const csc_matrix_t& A, diff --git a/cpp/src/dual_simplex/sparse_matrix.hpp b/cpp/src/dual_simplex/sparse_matrix.hpp index 4397e70683..e8b440a12b 100644 --- a/cpp/src/dual_simplex/sparse_matrix.hpp +++ b/cpp/src/dual_simplex/sparse_matrix.hpp @@ -142,6 +142,9 @@ class csr_matrix_t { // get constraint range std::pair get_constraint_range(i_t cstr_idx) const; + // insert a constraint into the matrix + void insert_row(const std::vector& vars, const std::vector& coeffs); + i_t nz_max; // maximum number of nonzero entries i_t m; // number of rows i_t n; // number of cols diff --git a/cpp/src/dual_simplex/user_problem.hpp b/cpp/src/dual_simplex/user_problem.hpp index 9823279f21..6b3b4553a1 100644 --- a/cpp/src/dual_simplex/user_problem.hpp +++ b/cpp/src/dual_simplex/user_problem.hpp @@ -29,6 +29,14 @@ struct user_problem_t { : handle_ptr(handle_ptr_), A(1, 1, 1), obj_constant(0.0), obj_scale(1.0) { } + + void insert_constraint(const std::vector& vars, + const std::vector& coeffs, + f_t rhs, + char sense) + { + } + raft::handle_t const* handle_ptr; i_t num_rows; i_t num_cols; diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 73b5c21dfd..9b30907753 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -121,10 +121,9 @@ void make_coeff_positive_knapsack_constraint( // if a binary variable has a negative coefficient, put its negation in the constraint template void fill_knapsack_constraints(const dual_simplex::user_problem_t& problem, - std::vector>& knapsack_constraints) + std::vector>& knapsack_constraints, + dual_simplex::csr_matrix_t& A) { - dual_simplex::csr_matrix_t A(0, 0, 0); - problem.A.to_compressed_row(A); // we might add additional constraints for the equality constraints i_t added_constraints = 0; for (i_t i = 0; i < A.m; i++) { @@ -284,8 +283,41 @@ bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) adj_list_small_cliques[var_idx1].count(var_idx2) > 0; } +// this function should only be called within extend clique +// if this is called outside extend clique, csr matrix should be converted into csc and copied into +// problem because the problem is partly modified +template +void insert_clique_into_problem(const std::vector& clique, + dual_simplex::user_problem_t& problem, + dual_simplex::csr_matrix_t& A) +{ + // convert vertices into original vars + f_t rhs_offset = 0.; + std::vector new_vars; + std::vector new_coeffs; + for (size_t i = 0; i < clique.size(); i++) { + f_t coeff = 1.; + i_t var_idx = clique[i]; + if (var_idx >= problem.num_cols) { + coeff = -1.; + var_idx = var_idx - problem.num_cols; + rhs_offset -= coeff; + } + new_vars.push_back(var_idx); + new_coeffs.push_back(coeff); + } + f_t rhs = 1 + rhs_offset; + // insert the new clique into the problem as a new constraint + A.insert_row(new_vars, new_coeffs); + problem.row_sense.push_back('L'); + problem.rhs.push_back(rhs); +} + template -void extend_clique(const std::vector& clique, clique_table_t& clique_table) +void extend_clique(const std::vector& clique, + clique_table_t& clique_table, + dual_simplex::user_problem_t& problem, + dual_simplex::csr_matrix_t& A) { i_t smallest_degree = std::numeric_limits::max(); i_t smallest_degree_var = -1; @@ -322,6 +354,8 @@ void extend_clique(const std::vector& clique, clique_table_t& cli if (new_clique.size() > clique.size()) { clique_table.first.push_back(new_clique); CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); + // insert the new clique into the problem as a new constraint + insert_clique_into_problem(new_clique, problem, A); } } @@ -330,7 +364,9 @@ void extend_clique(const std::vector& clique, clique_table_t& cli // TODO: consider a heuristic on how much of the cliques derived from knapsacks to include here template void extend_cliques(const std::vector>& knapsack_constraints, - clique_table_t& clique_table) + clique_table_t& clique_table, + dual_simplex::user_problem_t& problem, + dual_simplex::csr_matrix_t& A) { // we try extending cliques on set packing constraints for (const auto& knapsack_constraint : knapsack_constraints) { @@ -340,9 +376,11 @@ void extend_cliques(const std::vector>& knapsack for (const auto& entry : knapsack_constraint.entries) { clique.push_back(entry.col); } - extend_clique(clique, clique_table); + extend_clique(clique, clique_table, problem, A); } } + // copy modified matrix back to problem + A.to_compressed_col(problem.A); } template @@ -361,6 +399,11 @@ void fill_var_clique_maps(clique_table_t& clique_table) } } +template +void remove_dominated_constraint(const dual_simplex::user_problem_t& problem) +{ +} + template void print_knapsack_constraints( const std::vector>& knapsack_constraints, @@ -402,11 +445,13 @@ void print_clique_table(const clique_table_t& clique_table) } template -void find_initial_cliques(const dual_simplex::user_problem_t& problem, +void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances) { std::vector> knapsack_constraints; - fill_knapsack_constraints(problem, knapsack_constraints); + dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); + problem.A.to_compressed_row(A); + fill_knapsack_constraints(problem, knapsack_constraints, A); make_coeff_positive_knapsack_constraint(problem, knapsack_constraints, tolerances); sort_csr_by_constraint_coefficients(knapsack_constraints); print_knapsack_constraints(knapsack_constraints); @@ -427,13 +472,14 @@ void find_initial_cliques(const dual_simplex::user_problem_t& problem, remove_small_cliques(clique_table); // fill var clique maps fill_var_clique_maps(clique_table); - extend_cliques(knapsack_constraints, clique_table); + extend_cliques(knapsack_constraints, clique_table, problem, A); + remove_dominated_constraint(problem); exit(0); } -#define INSTANTIATE(F_TYPE) \ - template void find_initial_cliques( \ - const dual_simplex::user_problem_t& problem, \ +#define INSTANTIATE(F_TYPE) \ + template void find_initial_cliques( \ + dual_simplex::user_problem_t & problem, \ typename mip_solver_settings_t::tolerances_t tolerances); #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index 6c7be483bc..22625b208b 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -91,7 +91,7 @@ struct clique_table_t { }; template -void find_initial_cliques(const dual_simplex::user_problem_t& problem, +void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances); } // namespace cuopt::linear_programming::detail From 103b4c2a2020d39b07e3b17862ec0a22e404a2ac Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 18 Nov 2025 09:35:03 -0800 Subject: [PATCH 009/147] find constraints to remove --- cpp/src/dual_simplex/user_problem.hpp | 7 -- .../presolve/conflict_graph/clique_table.cu | 85 ++++++++++++++++--- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/cpp/src/dual_simplex/user_problem.hpp b/cpp/src/dual_simplex/user_problem.hpp index 6b3b4553a1..a56966224c 100644 --- a/cpp/src/dual_simplex/user_problem.hpp +++ b/cpp/src/dual_simplex/user_problem.hpp @@ -30,13 +30,6 @@ struct user_problem_t { { } - void insert_constraint(const std::vector& vars, - const std::vector& coeffs, - f_t rhs, - char sense) - { - } - raft::handle_t const* handle_ptr; i_t num_rows; i_t num_cols; diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 9b30907753..a189a47ac9 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -91,6 +91,7 @@ template void make_coeff_positive_knapsack_constraint( const dual_simplex::user_problem_t& problem, std::vector>& knapsack_constraints, + std::unordered_set& set_packing_constraints, typename mip_solver_settings_t::tolerances_t tolerances) { for (auto& knapsack_constraint : knapsack_constraints) { @@ -113,6 +114,9 @@ void make_coeff_positive_knapsack_constraint( all_coeff_are_equal = false; } knapsack_constraint.is_set_packing = all_coeff_are_equal; + if (knapsack_constraint.is_set_packing) { + set_packing_constraints.insert(knapsack_constraint.cstr_idx); + } cuopt_assert(knapsack_constraint.rhs >= 0, "RHS must be non-negative"); } } @@ -314,7 +318,7 @@ void insert_clique_into_problem(const std::vector& clique, } template -void extend_clique(const std::vector& clique, +bool extend_clique(const std::vector& clique, clique_table_t& clique_table, dual_simplex::user_problem_t& problem, dual_simplex::csr_matrix_t& A) @@ -350,24 +354,26 @@ void extend_clique(const std::vector& clique, } if (add) { new_clique.push_back(var_idx); } } - // if we found a larger cliqe, replace it in the clique table and replace the problem formulation + // if we found a larger cliqe, insert it into the formulation if (new_clique.size() > clique.size()) { clique_table.first.push_back(new_clique); CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); // insert the new clique into the problem as a new constraint insert_clique_into_problem(new_clique, problem, A); } + return new_clique.size() > clique.size(); } // Also known as clique merging. Infer larger clique constraints which allows inclusion of vars from // other constraints. This only extends the original cliques in the formulation for now. // TODO: consider a heuristic on how much of the cliques derived from knapsacks to include here template -void extend_cliques(const std::vector>& knapsack_constraints, - clique_table_t& clique_table, - dual_simplex::user_problem_t& problem, - dual_simplex::csr_matrix_t& A) +i_t extend_cliques(const std::vector>& knapsack_constraints, + clique_table_t& clique_table, + dual_simplex::user_problem_t& problem, + dual_simplex::csr_matrix_t& A) { + i_t n_extended_cliques = 0; // we try extending cliques on set packing constraints for (const auto& knapsack_constraint : knapsack_constraints) { if (!knapsack_constraint.is_set_packing) { continue; } @@ -376,11 +382,13 @@ void extend_cliques(const std::vector>& knapsack for (const auto& entry : knapsack_constraint.entries) { clique.push_back(entry.col); } - extend_clique(clique, clique_table, problem, A); + bool extended_clique = extend_clique(clique, clique_table, problem, A); + if (extended_clique) { n_extended_cliques++; } } } // copy modified matrix back to problem A.to_compressed_col(problem.A); + return n_extended_cliques; } template @@ -399,9 +407,62 @@ void fill_var_clique_maps(clique_table_t& clique_table) } } +// we want to remove constraints that are covered by extended cliques +// for set partitioning constraints, we will keep the constraint on original problem but fix +// extended vars to zero For a set partitioning constraint: v1+v2+...+vk = 1 and discovered: +// v1+v2+...+vk + vl1+vl2 +...+vli <= 1 +// Then substituting the first to the second you have: +// 1 + vl1+vl2 +...+vli <= 1 +// Which is simply: +// vl1+vl2 +...+vli <= 0 +// so we can fix them template -void remove_dominated_constraint(const dual_simplex::user_problem_t& problem) +void remove_dominated_cliques(dual_simplex::user_problem_t& problem, + dual_simplex::csr_matrix_t& A, + clique_table_t& clique_table, + std::unordered_set& set_packing_constraints, + i_t n_extended_cliques) { + // TODO check if we need to add the dominance for the table itself + i_t extended_clique_start_idx = clique_table.first.size() - n_extended_cliques; + CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); + std::vector constraints_to_remove; + for (i_t i = 0; i < n_extended_cliques; i++) { + i_t clique_idx = extended_clique_start_idx + i; + std::set curr_clique_vars; + const auto& curr_clique = clique_table.first[clique_idx]; + // check all original set packing constraints. if a set packing constraint is covered remove it. + // if it is a set partitioning constraint. keep the set partitioning and fix extensions to zero. + for (auto var_idx : curr_clique) { + curr_clique_vars.insert(var_idx); + } + for (size_t cstr_idx = 0; cstr_idx < problem.row_sense.size(); cstr_idx++) { + // only process set packing constraints + if (set_packing_constraints.count(cstr_idx) == 0) { continue; } + auto range = A.get_constraint_range(cstr_idx); + std::set curr_cstr_vars; + bool negate = false; + if (problem.row_sense[cstr_idx] == 'E') { + // equality constraints are not considered + continue; + } + if (problem.row_sense[cstr_idx] == 'G') { negate = true; } + for (i_t j = range.first; j < range.second; j++) { + i_t var_idx = A.j[j]; + f_t coeff = A.x[j]; + if (coeff < 0 != negate) { var_idx = var_idx + problem.num_cols; } + curr_cstr_vars.insert(var_idx); + } + bool constraint_covered = std::includes(curr_clique_vars.begin(), + curr_clique_vars.end(), + curr_cstr_vars.begin(), + curr_cstr_vars.end()); + if (constraint_covered) { + CUOPT_LOG_TRACE("Constraint %d is covered by clique %d", cstr_idx, clique_idx); + constraints_to_remove.push_back(cstr_idx); + } + } + } } template @@ -449,10 +510,12 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances) { std::vector> knapsack_constraints; + std::unordered_set set_packing_constraints; dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); problem.A.to_compressed_row(A); fill_knapsack_constraints(problem, knapsack_constraints, A); - make_coeff_positive_knapsack_constraint(problem, knapsack_constraints, tolerances); + make_coeff_positive_knapsack_constraint( + problem, knapsack_constraints, set_packing_constraints, tolerances); sort_csr_by_constraint_coefficients(knapsack_constraints); print_knapsack_constraints(knapsack_constraints); // TODO think about getting min_clique_size according to some problem property @@ -472,8 +535,8 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, remove_small_cliques(clique_table); // fill var clique maps fill_var_clique_maps(clique_table); - extend_cliques(knapsack_constraints, clique_table, problem, A); - remove_dominated_constraint(problem); + i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A); + remove_dominated_cliques(problem, A, clique_table, set_packing_constraints, n_extended_cliques); exit(0); } From adc9c73c448644624e628b15fb129a12e5ed1731 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 19 Nov 2025 08:24:57 -0800 Subject: [PATCH 010/147] wip --- cpp/src/dual_simplex/sparse_matrix.cpp | 2 +- .../presolve/conflict_graph/clique_table.cu | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index 395b0f9958..1bafac04e5 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -586,7 +586,7 @@ void csr_matrix_t::insert_row(const std::vector& vars, const std::vector& coeffs) { // insert the row into the matrix - this->row_start.push_back(this->m); + this->row_start.push_back(this->row_start.back() + vars.size()); this->m++; this->nz_max += vars.size(); this->j.insert(this->j.end(), vars.begin(), vars.end()); diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index a189a47ac9..9920ec7793 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -386,6 +386,7 @@ i_t extend_cliques(const std::vector>& knapsack_ if (extended_clique) { n_extended_cliques++; } } } + // problem.A.check_matrix(); // copy modified matrix back to problem A.to_compressed_col(problem.A); return n_extended_cliques; @@ -426,7 +427,7 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, // TODO check if we need to add the dominance for the table itself i_t extended_clique_start_idx = clique_table.first.size() - n_extended_cliques; CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); - std::vector constraints_to_remove; + std::vector removal_marker(problem.row_sense.size(), false); for (i_t i = 0; i < n_extended_cliques; i++) { i_t clique_idx = extended_clique_start_idx + i; std::set curr_clique_vars; @@ -459,10 +460,25 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, curr_cstr_vars.end()); if (constraint_covered) { CUOPT_LOG_TRACE("Constraint %d is covered by clique %d", cstr_idx, clique_idx); - constraints_to_remove.push_back(cstr_idx); + removal_marker[cstr_idx] = true; } } } + dual_simplex::csr_matrix_t A_removed(0, 0, 0); + A.remove_rows(removal_marker, A_removed); + problem.num_rows = A_removed.m; + // Remove problem.row_sense entries for which removal_marker is true, using remove_if + auto new_end = + std::remove_if(problem.row_sense.begin(), + problem.row_sense.end(), + [&removal_marker, n = i_t(0)](char) mutable { return removal_marker[n++]; }); + problem.row_sense.erase(new_end, problem.row_sense.end()); + // Remove problem.rhs entries for which removal_marker is true, using remove_if + auto new_end_rhs = std::remove_if( + problem.rhs.begin(), problem.rhs.end(), [&removal_marker, n = i_t(0)](f_t) mutable { + return removal_marker[n++]; + }); + problem.rhs.erase(new_end_rhs, problem.rhs.end()); } template @@ -509,6 +525,10 @@ template void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances) { + if (problem.num_range_rows > 0) { + CUOPT_LOG_ERROR("Range rows are not supported yet"); + exit(1); + } std::vector> knapsack_constraints; std::unordered_set set_packing_constraints; dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); @@ -517,7 +537,7 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, make_coeff_positive_knapsack_constraint( problem, knapsack_constraints, set_packing_constraints, tolerances); sort_csr_by_constraint_coefficients(knapsack_constraints); - print_knapsack_constraints(knapsack_constraints); + // print_knapsack_constraints(knapsack_constraints); // TODO think about getting min_clique_size according to some problem property clique_config_t clique_config; clique_table_t clique_table(2 * problem.num_cols, From 3001b5221def666ad48dc9e2d55985ea13860bfb Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 23 Jan 2026 05:59:51 -0800 Subject: [PATCH 011/147] habdle range constraints --- .../presolve/conflict_graph/clique_table.cu | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 9920ec7793..76cbc4570e 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights * reserved. SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -130,6 +130,9 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro { // we might add additional constraints for the equality constraints i_t added_constraints = 0; + // in user problems, ranged constraint ids monotonically increase. + // when a row sense is "E", check if it is ranged constraint and treat accordingly + i_t ranged_constraint_counter = 0; for (i_t i = 0; i < A.m; i++) { std::pair constraint_range = A.get_constraint_range(i); if (constraint_range.second - constraint_range.first < 2) { @@ -160,9 +163,17 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro for (i_t j = constraint_range.first; j < constraint_range.second; j++) { knapsack_constraint.entries.push_back({A.j[j], -A.x[j]}); } - } else if (problem.row_sense[i] == 'E') { + } + // equality part + else { + bool ranged_constraint = ranged_constraint_counter < problem.num_range_rows && + problem.range_rows[ranged_constraint_counter] == i; // less than part knapsack_constraint.rhs = problem.rhs[i]; + if (ranged_constraint) { + knapsack_constraint.rhs += problem.range_value[ranged_constraint_counter]; + ranged_constraint_counter++; + } for (i_t j = constraint_range.first; j < constraint_range.second; j++) { knapsack_constraint.entries.push_back({A.j[j], A.x[j]}); } @@ -473,12 +484,14 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, problem.row_sense.end(), [&removal_marker, n = i_t(0)](char) mutable { return removal_marker[n++]; }); problem.row_sense.erase(new_end, problem.row_sense.end()); + int n = 0; // Remove problem.rhs entries for which removal_marker is true, using remove_if - auto new_end_rhs = std::remove_if( - problem.rhs.begin(), problem.rhs.end(), [&removal_marker, n = i_t(0)](f_t) mutable { + auto new_end_rhs = + std::remove_if(problem.rhs.begin(), problem.rhs.end(), [&removal_marker, &n](f_t) mutable { return removal_marker[n++]; }); problem.rhs.erase(new_end_rhs, problem.rhs.end()); + CUOPT_LOG_DEBUG("Number of removed constraints by clique covering: %d", n); } template @@ -525,10 +538,6 @@ template void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances) { - if (problem.num_range_rows > 0) { - CUOPT_LOG_ERROR("Range rows are not supported yet"); - exit(1); - } std::vector> knapsack_constraints; std::unordered_set set_packing_constraints; dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); From b45c27eb2e1d8c1c05ea72488dc7f77a4ab5c1db Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 26 Jan 2026 01:35:34 -0800 Subject: [PATCH 012/147] fix bugs and covert to gpu problem --- .../presolve/conflict_graph/clique_table.cu | 47 ++++++++-- cpp/src/mip/presolve/probing_cache.cu | 3 +- cpp/src/mip/problem/problem.cu | 87 +++++++++++++++++++ cpp/src/mip/problem/problem.cuh | 2 + cpp/src/mip/solver.cu | 1 + 5 files changed, 133 insertions(+), 7 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 76cbc4570e..f6b205509a 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -475,16 +475,20 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, } } } + // TODO if more row removal is needed somewher else(e.g another presolve), standardize this dual_simplex::csr_matrix_t A_removed(0, 0, 0); A.remove_rows(removal_marker, A_removed); + A_removed.to_compressed_col(problem.A); problem.num_rows = A_removed.m; + cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); + i_t n = 0; // Remove problem.row_sense entries for which removal_marker is true, using remove_if - auto new_end = - std::remove_if(problem.row_sense.begin(), - problem.row_sense.end(), - [&removal_marker, n = i_t(0)](char) mutable { return removal_marker[n++]; }); + auto new_end = std::remove_if( + problem.row_sense.begin(), problem.row_sense.end(), [&removal_marker, &n](char) mutable { + return removal_marker[n++]; + }); problem.row_sense.erase(new_end, problem.row_sense.end()); - int n = 0; + n = 0; // Remove problem.rhs entries for which removal_marker is true, using remove_if auto new_end_rhs = std::remove_if(problem.rhs.begin(), problem.rhs.end(), [&removal_marker, &n](f_t) mutable { @@ -492,6 +496,37 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, }); problem.rhs.erase(new_end_rhs, problem.rhs.end()); CUOPT_LOG_DEBUG("Number of removed constraints by clique covering: %d", n); + cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); + cuopt_assert(problem.A.m == problem.rhs.size(), "matrix and num rows mismatch after removal"); + // Renumber the ranged row indices in problem.range_rows to ensure consistency after constraint + // removals. Create a mapping from old indices to new indices. + if (!problem.range_rows.empty()) { + std::vector old_to_new_indices; + old_to_new_indices.reserve(removal_marker.size()); + i_t new_idx = 0; + for (size_t i = 0; i < removal_marker.size(); ++i) { + if (!removal_marker[i]) { + old_to_new_indices.push_back(new_idx++); + } else { + old_to_new_indices.push_back(-1); // removed constraint + } + } + // Remove entries from range_rows and range_value where the underlying row has been removed. + std::vector new_range_rows; + std::vector new_range_values; + for (size_t i = 0; i < problem.range_rows.size(); ++i) { + i_t old_row = problem.range_rows[i]; + if (old_row >= 0 && old_row < (i_t)removal_marker.size() && !removal_marker[old_row]) { + i_t new_row = old_to_new_indices[old_row]; + cuopt_assert(new_row != -1, "Invalid new row index for ranged row renumbering"); + new_range_rows.push_back(new_row); + new_range_values.push_back(problem.range_value[i]); + } + // else: the ranged row was removed, so we skip it + } + problem.range_rows = std::move(new_range_rows); + problem.range_value = std::move(new_range_values); + } } template @@ -566,7 +601,7 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, fill_var_clique_maps(clique_table); i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A); remove_dominated_cliques(problem, A, clique_table, set_packing_constraints, n_extended_cliques); - exit(0); + // exit(0); } #define INSTANTIATE(F_TYPE) \ diff --git a/cpp/src/mip/presolve/probing_cache.cu b/cpp/src/mip/presolve/probing_cache.cu index fc2d974e33..cdd31147d2 100644 --- a/cpp/src/mip/presolve/probing_cache.cu +++ b/cpp/src/mip/presolve/probing_cache.cu @@ -6,7 +6,6 @@ /* clang-format on */ #include "probing_cache.cuh" -#include "trivial_presolve.cuh" #include #include @@ -19,6 +18,8 @@ #include #include +#include + namespace cuopt::linear_programming::detail { template diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 3acd16f31e..4420e9392e 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -1113,6 +1113,7 @@ void problem_t::resize_constraints(size_t matrix_size, size_t n_variables) { raft::common::nvtx::range fun_scope("resize_constraints"); + auto prev_dual_size = lp_state.prev_dual.size(); coefficients.resize(matrix_size, handle_ptr->get_stream()); variables.resize(matrix_size, handle_ptr->get_stream()); reverse_constraints.resize(matrix_size, handle_ptr->get_stream()); @@ -1123,6 +1124,13 @@ void problem_t::resize_constraints(size_t matrix_size, combined_bounds.resize(constraint_size, handle_ptr->get_stream()); offsets.resize(constraint_size + 1, handle_ptr->get_stream()); reverse_offsets.resize(n_variables + 1, handle_ptr->get_stream()); + lp_state.prev_dual.resize(constraint_size, handle_ptr->get_stream()); + if (constraint_size > prev_dual_size) { + thrust::fill(handle_ptr->get_thrust_policy(), + lp_state.prev_dual.begin() + prev_dual_size, + lp_state.prev_dual.end(), + f_t{0}); + } } // note that these don't change the reverse structure @@ -1892,6 +1900,85 @@ void problem_t::preprocess_problem() preprocess_called = true; } +template +void problem_t::set_constraints_from_host_user_problem( + const cuopt::linear_programming::dual_simplex::user_problem_t& user_problem) +{ + raft::common::nvtx::range fun_scope("set_constraints_from_host_user_problem"); + cuopt_assert(user_problem.handle_ptr == handle_ptr, "handle mismatch"); + cuopt_assert(user_problem.num_cols == n_variables, "num cols mismatch"); + n_constraints = user_problem.num_rows; + cuopt_assert(user_problem.rhs.size() == static_cast(n_constraints), "rhs size mismatch"); + cuopt_assert(user_problem.row_sense.size() == static_cast(n_constraints), + "row sense size mismatch"); + cuopt_assert(user_problem.range_rows.size() == user_problem.range_value.size(), + "range rows/value size mismatch"); + + dual_simplex::csr_matrix_t csr_A(n_constraints, n_variables, user_problem.A.nnz()); + user_problem.A.to_compressed_row(csr_A); + nnz = csr_A.row_start[n_constraints]; + empty = (nnz == 0 && n_constraints == 0 && n_variables == 0); + + auto stream = handle_ptr->get_stream(); + cuopt::device_copy(coefficients, csr_A.x, stream); + cuopt::device_copy(variables, csr_A.j, stream); + cuopt::device_copy(offsets, csr_A.row_start, stream); + + std::vector h_constraint_lower_bounds(n_constraints); + std::vector h_constraint_upper_bounds(n_constraints); + std::vector range_value_per_row(n_constraints, f_t{0}); + std::vector is_range_row(n_constraints, 0); + for (size_t idx = 0; idx < user_problem.range_rows.size(); ++idx) { + auto row = user_problem.range_rows[idx]; + cuopt_assert(row >= 0 && row < n_constraints, "range row out of bounds"); + is_range_row[row] = 1; + range_value_per_row[row] = user_problem.range_value[idx]; + } + + const auto inf = std::numeric_limits::infinity(); + for (i_t i = 0; i < n_constraints; ++i) { + const f_t rhs = user_problem.rhs[i]; + const char sense = user_problem.row_sense[i]; + if (sense == 'E') { + h_constraint_lower_bounds[i] = rhs; + h_constraint_upper_bounds[i] = rhs; + if (is_range_row[i]) { h_constraint_upper_bounds[i] = rhs + range_value_per_row[i]; } + } else if (sense == 'G') { + h_constraint_lower_bounds[i] = rhs; + h_constraint_upper_bounds[i] = inf; + } else if (sense == 'L') { + h_constraint_lower_bounds[i] = -inf; + h_constraint_upper_bounds[i] = rhs; + } else { + cuopt_assert(false, "Unsupported row sense"); + } + } + + cuopt::device_copy(constraint_lower_bounds, h_constraint_lower_bounds, stream); + cuopt::device_copy(constraint_upper_bounds, h_constraint_upper_bounds, stream); + + if (!user_problem.row_names.empty()) { + row_names = user_problem.row_names; + } else if (row_names.size() != static_cast(n_constraints)) { + row_names.clear(); + } + + integer_fixed_problem = nullptr; + fixing_helpers.reduction_in_rhs.resize(n_constraints, stream); + auto prev_dual_size = lp_state.prev_dual.size(); + lp_state.prev_dual.resize(n_constraints, stream); + if (n_constraints > (i_t)prev_dual_size) { + thrust::fill(handle_ptr->get_thrust_policy(), + lp_state.prev_dual.begin() + prev_dual_size, + lp_state.prev_dual.end(), + f_t{0}); + } + + compute_transpose_of_problem(); + combined_bounds.resize(n_constraints, stream); + combine_constraint_bounds(*this, combined_bounds); +} + template void problem_t::get_host_user_problem( cuopt::linear_programming::dual_simplex::user_problem_t& user_problem) const diff --git a/cpp/src/mip/problem/problem.cuh b/cpp/src/mip/problem/problem.cuh index 9719f0b548..0e3b7a627f 100644 --- a/cpp/src/mip/problem/problem.cuh +++ b/cpp/src/mip/problem/problem.cuh @@ -103,6 +103,8 @@ class problem_t { void get_host_user_problem( cuopt::linear_programming::dual_simplex::user_problem_t& user_problem) const; + void set_constraints_from_host_user_problem( + const cuopt::linear_programming::dual_simplex::user_problem_t& user_problem); void add_cutting_plane_at_objective(f_t objective); void compute_vars_with_objective_coeffs(); diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 2a082d2422..d325e0e375 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -158,6 +158,7 @@ solution_t mip_solver_t::run_solver() // Convert the presolved problem to dual_simplex::user_problem_t op_problem_.get_host_user_problem(branch_and_bound_problem); find_initial_cliques(branch_and_bound_problem, context.settings.tolerances); + context.problem_ptr->set_constraints_from_host_user_problem(branch_and_bound_problem); // Resize the solution now that we know the number of columns/variables branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); From 21e565a247d5d30f80c72af78e064354aad87047 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 26 Jan 2026 10:47:27 -0800 Subject: [PATCH 013/147] fix a log --- cpp/src/mip/presolve/conflict_graph/clique_table.cu | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index f6b205509a..8c00c6fb7f 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -495,7 +495,8 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, return removal_marker[n++]; }); problem.rhs.erase(new_end_rhs, problem.rhs.end()); - CUOPT_LOG_DEBUG("Number of removed constraints by clique covering: %d", n); + size_t n_of_removed_constraints = std::distance(new_end, problem.row_sense.end()); + CUOPT_LOG_DEBUG("Number of removed constraints by clique covering: %d", n_of_removed_constraints); cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); cuopt_assert(problem.A.m == problem.rhs.size(), "matrix and num rows mismatch after removal"); // Renumber the ranged row indices in problem.range_rows to ensure consistency after constraint From 3ae60475eb5330c3f06ea60044b06f11262abce7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 27 Jan 2026 08:57:27 -0800 Subject: [PATCH 014/147] move preprocessing to presolve --- cpp/src/mip/diversity/diversity_manager.cu | 22 +++++++++++++++------- cpp/src/mip/solver.cu | 2 -- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index cfe9876dee..26452ea15b 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -9,10 +9,12 @@ #include "diversity_manager.cuh" #include +#include #include #include #include +#include #include #include @@ -200,16 +202,22 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // May overconstrain if Papilo presolve has been run before - if (!context.settings.presolve) { - if (!problem_ptr->empty) { - // do the resizing no-matter what, bounds presolve might not change the bounds but initial - // trivial presolve might have - ls.constraint_prop.bounds_update.resize(*problem_ptr); + if (!context.settings.heuristics_only && !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + find_initial_cliques(host_problem, context.settings.tolerances); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + } + if (!problem_ptr->empty) { + // do the resizing no-matter what, bounds presolve might not change the bounds but + // initial trivial presolve might have + ls.constraint_prop.bounds_update.resize(*problem_ptr); + // May overconstrain if Papilo presolve has been run before + if (!context.settings.presolve) { ls.constraint_prop.conditional_bounds_update.update_constraint_bounds( *problem_ptr, ls.constraint_prop.bounds_update); - if (!check_bounds_sanity(*problem_ptr)) { return false; } } + if (!check_bounds_sanity(*problem_ptr)) { return false; } } stats.presolve_time = presolve_timer.elapsed_time(); lp_optimal_solution.resize(problem_ptr->n_variables, problem_ptr->handle_ptr->get_stream()); diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index d325e0e375..367041c78d 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -9,7 +9,6 @@ #include "diversity/diversity_manager.cuh" #include "local_search/local_search.cuh" #include "local_search/rounding/simple_rounding.cuh" -#include "presolve/conflict_graph/clique_table.cuh" #include "solver.cuh" #include @@ -157,7 +156,6 @@ solution_t mip_solver_t::run_solver() if (!context.settings.heuristics_only) { // Convert the presolved problem to dual_simplex::user_problem_t op_problem_.get_host_user_problem(branch_and_bound_problem); - find_initial_cliques(branch_and_bound_problem, context.settings.tolerances); context.problem_ptr->set_constraints_from_host_user_problem(branch_and_bound_problem); // Resize the solution now that we know the number of columns/variables branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); From 2c78d0206ffc19c75fc8dced09241d8fd579bca4 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 30 Jan 2026 09:31:10 -0800 Subject: [PATCH 015/147] fix issues and handle ai reviews --- cpp/src/dual_simplex/sparse_matrix.cpp | 3 +- .../presolve/conflict_graph/clique_table.cu | 35 +++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index 7cd04f8ed8..8578ffae7e 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -598,6 +598,7 @@ template void csr_matrix_t::insert_row(const std::vector& vars, const std::vector& coeffs) { + assert(vars.size() == coeffs.size()); // insert the row into the matrix this->row_start.push_back(this->row_start.back() + vars.size()); this->m++; diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 8c00c6fb7f..9cadfd279d 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -19,6 +19,7 @@ #include "clique_table.cuh" +#include #include #include #include @@ -39,7 +40,8 @@ void find_cliques_from_constraint(const knapsack_constraint_t& kc, i_t k = size - 1; // find the first clique, which is the largest // FIXME: do binary search - while (k >= 0) { + // require k >= 1 so kc.entries[k-1] is always valid + while (k >= 1) { if (kc.entries[k].val + kc.entries[k - 1].val <= kc.rhs) { break; } clique.push_back(kc.entries[k].col); k--; @@ -140,9 +142,9 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro continue; } bool all_binary = true; - // check if all variables are binary + // check if all variables are binary (any non-continuous with bounds [0,1]) for (i_t j = constraint_range.first; j < constraint_range.second; j++) { - if (problem.var_types[A.j[j]] != dual_simplex::variable_type_t::INTEGER || + if (problem.var_types[A.j[j]] == dual_simplex::variable_type_t::CONTINUOUS || problem.lower[A.j[j]] != 0 || problem.upper[A.j[j]] != 1) { all_binary = false; break; @@ -293,9 +295,26 @@ i_t clique_table_t::get_degree_of_var(i_t var_idx) template bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) { - return var_clique_map_first[var_idx1].count(var_idx2) > 0 || - var_clique_map_addtl[var_idx1].count(var_idx2) > 0 || - adj_list_small_cliques[var_idx1].count(var_idx2) > 0; + // Check first cliques: var_clique_map_first stores clique indices + for (const auto& clique_idx : var_clique_map_first[var_idx1]) { + const auto& clique = first[clique_idx]; + if (std::binary_search(clique.begin(), clique.end(), var_idx2)) { return true; } + } + + // Check additional cliques: var_clique_map_addtl stores indices into addtl_cliques + for (const auto& addtl_idx : var_clique_map_addtl[var_idx1]) { + const auto& addtl = addtl_cliques[addtl_idx]; + const auto& clique = first[addtl.clique_idx]; + // addtl clique is: vertex_idx + first[clique_idx][start_pos_on_clique:] + if (addtl.vertex_idx == var_idx2) { return true; } + if (addtl.start_pos_on_clique < static_cast(clique.size())) { + if (std::binary_search(clique.begin() + addtl.start_pos_on_clique, clique.end(), var_idx2)) { + return true; + } + } + } + + return adj_list_small_cliques[var_idx1].count(var_idx2) > 0; } // this function should only be called within extend clique @@ -316,7 +335,7 @@ void insert_clique_into_problem(const std::vector& clique, if (var_idx >= problem.num_cols) { coeff = -1.; var_idx = var_idx - problem.num_cols; - rhs_offset -= coeff; + rhs_offset--; } new_vars.push_back(var_idx); new_coeffs.push_back(coeff); @@ -487,6 +506,8 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, problem.row_sense.begin(), problem.row_sense.end(), [&removal_marker, &n](char) mutable { return removal_marker[n++]; }); + // Compute count before erase invalidates the iterator + size_t n_of_removed_constraints = std::distance(new_end, problem.row_sense.end()); problem.row_sense.erase(new_end, problem.row_sense.end()); n = 0; // Remove problem.rhs entries for which removal_marker is true, using remove_if From 8409f1741127048bcee8ccdf79919eee8903ee2a Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 2 Feb 2026 06:00:14 -0800 Subject: [PATCH 016/147] fix bugs adj list --- .../presolve/conflict_graph/clique_table.cu | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 9cadfd279d..d69802c9a3 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -281,6 +281,7 @@ std::unordered_set clique_table_t::get_adj_set_of_var(i_t var_idx for (const auto& adj_vertex : adj_list_small_cliques[var_idx]) { adj_set.insert(adj_vertex); } + adj_set.erase(var_idx); return adj_set; } @@ -295,10 +296,13 @@ i_t clique_table_t::get_degree_of_var(i_t var_idx) template bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) { + if (adj_list_small_cliques[var_idx1].count(var_idx2) > 0) { return true; } // Check first cliques: var_clique_map_first stores clique indices for (const auto& clique_idx : var_clique_map_first[var_idx1]) { const auto& clique = first[clique_idx]; - if (std::binary_search(clique.begin(), clique.end(), var_idx2)) { return true; } + // TODO: we can also keep a set of the clique if the memory allows, instead of doing linear + // search + if (std::find(clique.begin(), clique.end(), var_idx2) != clique.end()) { return true; } } // Check additional cliques: var_clique_map_addtl stores indices into addtl_cliques @@ -308,13 +312,14 @@ bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) // addtl clique is: vertex_idx + first[clique_idx][start_pos_on_clique:] if (addtl.vertex_idx == var_idx2) { return true; } if (addtl.start_pos_on_clique < static_cast(clique.size())) { - if (std::binary_search(clique.begin() + addtl.start_pos_on_clique, clique.end(), var_idx2)) { + if (std::find(clique.begin() + addtl.start_pos_on_clique, clique.end(), var_idx2) != + clique.end()) { return true; } } } - return adj_list_small_cliques[var_idx1].count(var_idx2) > 0; + return false; } // this function should only be called within extend clique @@ -366,8 +371,12 @@ bool extend_clique(const std::vector& clique, } std::vector extension_candidates; auto smallest_degree_adj_set = clique_table.get_adj_set_of_var(smallest_degree_var); - extension_candidates.insert( - extension_candidates.end(), smallest_degree_adj_set.begin(), smallest_degree_adj_set.end()); + std::unordered_set clique_members(clique.begin(), clique.end()); + for (const auto& candidate : smallest_degree_adj_set) { + if (clique_members.find(candidate) == clique_members.end()) { + extension_candidates.push_back(candidate); + } + } std::sort(extension_candidates.begin(), extension_candidates.end(), [&](i_t a, i_t b) { return clique_table.get_degree_of_var(a) > clique_table.get_degree_of_var(b); }); @@ -516,7 +525,6 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, return removal_marker[n++]; }); problem.rhs.erase(new_end_rhs, problem.rhs.end()); - size_t n_of_removed_constraints = std::distance(new_end, problem.row_sense.end()); CUOPT_LOG_DEBUG("Number of removed constraints by clique covering: %d", n_of_removed_constraints); cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); cuopt_assert(problem.A.m == problem.rhs.size(), "matrix and num rows mismatch after removal"); From 22f778b3d5bf6d051e28fc3333590c4d9d3f3789 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Feb 2026 01:13:47 -0800 Subject: [PATCH 017/147] style checks --- cpp/src/dual_simplex/sparse_matrix.hpp | 2 +- cpp/src/mip/CMakeLists.txt | 2 +- cpp/src/mip/presolve/conflict_graph/clique_table.cuh | 2 +- docs/cuopt/source/versions1.json | 8 ++++++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cpp/src/dual_simplex/sparse_matrix.hpp b/cpp/src/dual_simplex/sparse_matrix.hpp index c48db84e65..924065ba14 100644 --- a/cpp/src/dual_simplex/sparse_matrix.hpp +++ b/cpp/src/dual_simplex/sparse_matrix.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/cpp/src/mip/CMakeLists.txt b/cpp/src/mip/CMakeLists.txt index a52dd176ec..9e011ddd74 100644 --- a/cpp/src/mip/CMakeLists.txt +++ b/cpp/src/mip/CMakeLists.txt @@ -1,5 +1,5 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index 22625b208b..a867bd833e 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights * reserved. SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/cuopt/source/versions1.json b/docs/cuopt/source/versions1.json index 12bebcd036..1dede408c8 100644 --- a/docs/cuopt/source/versions1.json +++ b/docs/cuopt/source/versions1.json @@ -1,10 +1,14 @@ [ { - "version": "26.02.00", - "url": "https://docs.nvidia.com/cuopt/user-guide/26.02.00/", + "version": "26.04.00", + "url": "../26.04.00/", "name": "latest", "preferred": true }, + { + "version": "26.02.00", + "url": "https://docs.nvidia.com/cuopt/user-guide/26.02.00/" + }, { "version": "25.12.00", "url": "https://docs.nvidia.com/cuopt/user-guide/25.12.00/" From 12c8fcfa3282383ed5113ba0f055c348f35824ba Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Feb 2026 02:03:53 -0800 Subject: [PATCH 018/147] fix excluded cliques and fix extended set packing constraints --- .../presolve/conflict_graph/clique_table.cu | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index d69802c9a3..d31845c719 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -41,11 +41,12 @@ void find_cliques_from_constraint(const knapsack_constraint_t& kc, // find the first clique, which is the largest // FIXME: do binary search // require k >= 1 so kc.entries[k-1] is always valid - while (k >= 1) { - if (kc.entries[k].val + kc.entries[k - 1].val <= kc.rhs) { break; } - clique.push_back(kc.entries[k].col); + while (k >= 1 && kc.entries[k].val + kc.entries[k - 1].val > kc.rhs) { k--; } + for (i_t idx = k; idx < size; idx++) { + clique.push_back(kc.entries[idx].col); + } clique_table.first.push_back(clique); const i_t original_clique_start_idx = k; // find the additional cliques @@ -204,7 +205,7 @@ void remove_small_cliques(clique_table_t& clique_table) // if a clique is small, we remove it from the cliques and add it to adjlist for (size_t clique_idx = 0; clique_idx < clique_table.first.size(); clique_idx++) { const auto& clique = clique_table.first[clique_idx]; - if (clique.size() < (size_t)clique_table.min_clique_size) { + if (clique.size() <= (size_t)clique_table.min_clique_size) { for (size_t i = 0; i < clique.size(); i++) { for (size_t j = 0; j < clique.size(); j++) { if (i == j) { continue; } @@ -481,11 +482,8 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, if (set_packing_constraints.count(cstr_idx) == 0) { continue; } auto range = A.get_constraint_range(cstr_idx); std::set curr_cstr_vars; - bool negate = false; - if (problem.row_sense[cstr_idx] == 'E') { - // equality constraints are not considered - continue; - } + bool negate = false; + bool is_set_partitioning = problem.row_sense[cstr_idx] == 'E'; if (problem.row_sense[cstr_idx] == 'G') { negate = true; } for (i_t j = range.first; j < range.second; j++) { i_t var_idx = A.j[j]; @@ -499,7 +497,21 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, curr_cstr_vars.end()); if (constraint_covered) { CUOPT_LOG_TRACE("Constraint %d is covered by clique %d", cstr_idx, clique_idx); - removal_marker[cstr_idx] = true; + if (is_set_partitioning) { + for (auto var_idx : curr_clique_vars) { + if (curr_cstr_vars.count(var_idx) != 0) { continue; } + if (var_idx >= problem.num_cols) { + i_t orig_idx = var_idx - problem.num_cols; + problem.lower[orig_idx] = 1; + problem.upper[orig_idx] = 1; + } else { + problem.lower[var_idx] = 0; + problem.upper[var_idx] = 0; + } + } + } else { + removal_marker[cstr_idx] = true; + } } } } From 5d1624645cec4ca24b82e7b4d4d9d82020d23d87 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Feb 2026 03:23:12 -0800 Subject: [PATCH 019/147] tests if threre are any complements of a variable in the extended clique --- .../presolve/conflict_graph/clique_table.cu | 27 +++++++++++++++++++ .../presolve/conflict_graph/clique_table.cuh | 6 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index d31845c719..0882d9a455 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -297,6 +297,10 @@ i_t clique_table_t::get_degree_of_var(i_t var_idx) template bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) { + // if passed same variable + if (var_idx1 == var_idx2) { return false; } + // in case they are complements of each other + if (var_idx1 % n_variables == var_idx2 % n_variables) { return true; } if (adj_list_small_cliques[var_idx1].count(var_idx2) > 0) { return true; } // Check first cliques: var_clique_map_first stores clique indices for (const auto& clique_idx : var_clique_map_first[var_idx1]) { @@ -396,6 +400,29 @@ bool extend_clique(const std::vector& clique, } // if we found a larger cliqe, insert it into the formulation if (new_clique.size() > clique.size()) { + // Before inserting the new clique, check if a variable and its complement are both present + // Assuming complement of variable x is x + n_variables (as typical in these encodings) + bool has_var_and_complement = false; + for (size_t i = 0; i < new_clique.size(); ++i) { + i_t var = new_clique[i]; + i_t complement = -1; + // determine complement only if var is in the range of variables + if (var < clique_table.n_variables) { + complement = var + clique_table.n_variables; + } else if (var < 2 * clique_table.n_variables) { + complement = var - clique_table.n_variables; + } + // check if complement exists in the clique + if (complement != -1 && + std::find(new_clique.begin(), new_clique.end(), complement) != new_clique.end()) { + has_var_and_complement = true; + break; + } + } + if (has_var_and_complement) { + CUOPT_LOG_DEBUG("Not adding clique: contains variable and its complement in the same clique"); + exit(0); + } clique_table.first.push_back(new_clique); CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); // insert the new clique into the problem as a new constraint diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index a867bd833e..2c40d51ebb 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -62,7 +62,8 @@ struct clique_table_t { var_clique_map_first(n_vertices), var_clique_map_addtl(n_vertices), adj_list_small_cliques(n_vertices), - var_degrees(n_vertices, -1) + var_degrees(n_vertices, -1), + n_variables(n_vertices / 2) { } @@ -84,7 +85,8 @@ struct clique_table_t { std::unordered_map> adj_list_small_cliques; // degrees of each vertex std::vector var_degrees; - + // number of variables in the original problem + const i_t n_variables; const i_t min_clique_size; const i_t max_clique_size_for_extension; typename mip_solver_settings_t::tolerances_t tolerances; From 6319046f6260f50e5be633345ebe73b7fb47f897 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Feb 2026 04:17:31 -0800 Subject: [PATCH 020/147] fix variables if complements share a clique --- .../presolve/conflict_graph/clique_table.cu | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 0882d9a455..28cf10efa5 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -385,11 +385,17 @@ bool extend_clique(const std::vector& clique, std::sort(extension_candidates.begin(), extension_candidates.end(), [&](i_t a, i_t b) { return clique_table.get_degree_of_var(a) > clique_table.get_degree_of_var(b); }); - auto new_clique = clique; + auto new_clique = clique; + i_t n_of_complement_conflicts = 0; + i_t complement_conflict_var = -1; for (size_t idx = 0; idx < extension_candidates.size(); idx++) { i_t var_idx = extension_candidates[idx]; bool add = true; for (size_t i = 0; i < new_clique.size(); i++) { + if (var_idx % clique_table.n_variables == new_clique[i] % clique_table.n_variables) { + n_of_complement_conflicts++; + complement_conflict_var = var_idx % clique_table.n_variables; + } // check if the tested variable conflicts with all vars in the new clique if (!clique_table.check_adjacency(var_idx, new_clique[i])) { add = false; @@ -400,33 +406,30 @@ bool extend_clique(const std::vector& clique, } // if we found a larger cliqe, insert it into the formulation if (new_clique.size() > clique.size()) { - // Before inserting the new clique, check if a variable and its complement are both present - // Assuming complement of variable x is x + n_variables (as typical in these encodings) - bool has_var_and_complement = false; - for (size_t i = 0; i < new_clique.size(); ++i) { - i_t var = new_clique[i]; - i_t complement = -1; - // determine complement only if var is in the range of variables - if (var < clique_table.n_variables) { - complement = var + clique_table.n_variables; - } else if (var < 2 * clique_table.n_variables) { - complement = var - clique_table.n_variables; - } - // check if complement exists in the clique - if (complement != -1 && - std::find(new_clique.begin(), new_clique.end(), complement) != new_clique.end()) { - has_var_and_complement = true; - break; + if (n_of_complement_conflicts > 0) { + CUOPT_LOG_DEBUG("Found %d complement conflicts on var %d", + n_of_complement_conflicts, + complement_conflict_var); + cuopt_assert(n_of_complement_conflicts == 1, "There can only be one complement conflict"); + // fix all other variables other than complementing var + for (size_t i = 0; i < new_clique.size(); i++) { + if (new_clique[i] % clique_table.n_variables != complement_conflict_var) { + if (new_clique[i] >= problem.num_cols) { + problem.lower[new_clique[i] - problem.num_cols] = 1; + problem.upper[new_clique[i] - problem.num_cols] = 1; + } else { + problem.lower[new_clique[i]] = 0; + problem.upper[new_clique[i]] = 0; + } + } } + return false; + } else { + clique_table.first.push_back(new_clique); + CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); + // insert the new clique into the problem as a new constraint + insert_clique_into_problem(new_clique, problem, A); } - if (has_var_and_complement) { - CUOPT_LOG_DEBUG("Not adding clique: contains variable and its complement in the same clique"); - exit(0); - } - clique_table.first.push_back(new_clique); - CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); - // insert the new clique into the problem as a new constraint - insert_clique_into_problem(new_clique, problem, A); } return new_clique.size() > clique.size(); } From 96385febc9ef3f098d076c92927fb813b21ccd79 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Feb 2026 05:43:10 -0800 Subject: [PATCH 021/147] add timing --- .../presolve/conflict_graph/clique_table.cu | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 28cf10efa5..de86276f67 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -21,10 +21,12 @@ #include #include +#include #include #include #include #include +#include namespace cuopt::linear_programming::detail { @@ -645,14 +647,26 @@ template void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances) { + cuopt::timer_t timer(std::numeric_limits::infinity()); + double t_fill = 0.; + double t_coeff = 0.; + double t_sort = 0.; + double t_find = 0.; + double t_small = 0.; + double t_maps = 0.; + double t_extend = 0.; + double t_remove = 0.; std::vector> knapsack_constraints; std::unordered_set set_packing_constraints; dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); problem.A.to_compressed_row(A); fill_knapsack_constraints(problem, knapsack_constraints, A); + t_fill = timer.elapsed_time(); make_coeff_positive_knapsack_constraint( problem, knapsack_constraints, set_packing_constraints, tolerances); + t_coeff = timer.elapsed_time(); sort_csr_by_constraint_coefficients(knapsack_constraints); + t_sort = timer.elapsed_time(); // print_knapsack_constraints(knapsack_constraints); // TODO think about getting min_clique_size according to some problem property clique_config_t clique_config; @@ -663,16 +677,33 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, for (const auto& knapsack_constraint : knapsack_constraints) { find_cliques_from_constraint(knapsack_constraint, clique_table); } + t_find = timer.elapsed_time(); CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d", clique_table.first.size(), clique_table.addtl_cliques.size()); // print_clique_table(clique_table); // remove small cliques and add them to adj_list remove_small_cliques(clique_table); + t_small = timer.elapsed_time(); // fill var clique maps fill_var_clique_maps(clique_table); + t_maps = timer.elapsed_time(); i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A); + t_extend = timer.elapsed_time(); remove_dominated_cliques(problem, A, clique_table, set_packing_constraints, n_extended_cliques); + t_remove = timer.elapsed_time(); + CUOPT_LOG_DEBUG( + "Clique table timing (s): fill=%.6f coeff=%.6f sort=%.6f find=%.6f small=%.6f maps=%.6f " + "extend=%.6f remove=%.6f total=%.6f", + t_fill, + t_coeff - t_fill, + t_sort - t_coeff, + t_find - t_sort, + t_small - t_find, + t_maps - t_small, + t_extend - t_maps, + t_remove - t_extend, + t_remove); // exit(0); } From 7cd0a4a4e8fe542c2a8597737278e6c764d66685 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Feb 2026 08:02:26 -0800 Subject: [PATCH 022/147] wip --- .../presolve/conflict_graph/clique_table.cu | 165 +++++++++++++----- 1 file changed, 123 insertions(+), 42 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index de86276f67..50ec07700d 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -500,56 +500,137 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, i_t extended_clique_start_idx = clique_table.first.size() - n_extended_cliques; CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); std::vector removal_marker(problem.row_sense.size(), false); + std::vector> cstr_vars(problem.row_sense.size()); + std::vector is_set_partitioning(problem.row_sense.size(), false); + CUOPT_LOG_DEBUG("Building constraint variable lists"); + for (const auto cstr_idx : set_packing_constraints) { + if (cstr_idx < 0 || cstr_idx >= static_cast(problem.row_sense.size())) { + CUOPT_LOG_ERROR( + "Invalid set packing constraint idx: %d (rows=%zu)", cstr_idx, problem.row_sense.size()); + continue; + } + auto range = A.get_constraint_range(cstr_idx); + if (range.first < 0 || range.second < 0 || range.first > range.second || + range.second > A.nz_max) { + CUOPT_LOG_ERROR("Invalid range for constraint %d: [%d, %d) nnz=%d", + cstr_idx, + range.first, + range.second, + A.nz_max); + continue; + } + bool negate = problem.row_sense[cstr_idx] == 'G'; + is_set_partitioning[cstr_idx] = problem.row_sense[cstr_idx] == 'E'; + auto& vars = cstr_vars[cstr_idx]; + vars.reserve(range.second - range.first); + for (i_t j = range.first; j < range.second; j++) { + i_t var_idx = A.j[j]; + f_t coeff = A.x[j]; + if (coeff < 0 != negate) { var_idx = var_idx + problem.num_cols; } + vars.push_back(var_idx); + } + std::sort(vars.begin(), vars.end()); + vars.erase(std::unique(vars.begin(), vars.end()), vars.end()); + } + CUOPT_LOG_DEBUG("Constraint variable lists built: %zu", set_packing_constraints.size()); + constexpr size_t dominance_window = 1000; + struct clique_sig_t { + i_t cstr_idx; + i_t size; + long long signature; + }; + std::vector sp_sigs; + sp_sigs.reserve(set_packing_constraints.size()); + CUOPT_LOG_DEBUG("Building set packing signatures"); + for (const auto cstr_idx : set_packing_constraints) { + const auto& vars = cstr_vars[cstr_idx]; + if (vars.empty()) { continue; } + long long signature = 0; + for (auto v : vars) { + signature += static_cast(v); + } + sp_sigs.push_back({cstr_idx, static_cast(vars.size()), signature}); + } + CUOPT_LOG_DEBUG("Sorting signatures: %zu", sp_sigs.size()); + std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { + if (a.signature != b.signature) { return a.signature < b.signature; } + return a.size < b.size; + }); + auto is_subset = [](const std::vector& a, const std::vector& b) { + size_t i = 0; + size_t j = 0; + while (i < a.size() && j < b.size()) { + if (a[i] == b[j]) { + i++; + j++; + } else if (a[i] > b[j]) { + j++; + } else { + return false; + } + } + return i == a.size(); + }; + auto fix_difference = [&](const std::vector& superset, const std::vector& subset) { + for (auto var_idx : superset) { + if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } + if (var_idx >= problem.num_cols) { + i_t orig_idx = var_idx - problem.num_cols; + problem.lower[orig_idx] = 1; + problem.upper[orig_idx] = 1; + } else { + problem.lower[var_idx] = 0; + problem.upper[var_idx] = 0; + } + } + }; + auto find_window_start = [&](long long signature) { + auto it = std::lower_bound( + sp_sigs.begin(), sp_sigs.end(), signature, [](const auto& a, long long value) { + return a.signature < value; + }); + return static_cast(std::distance(sp_sigs.begin(), it)); + }; + CUOPT_LOG_DEBUG("Scanning extended cliques for dominance"); for (i_t i = 0; i < n_extended_cliques; i++) { - i_t clique_idx = extended_clique_start_idx + i; - std::set curr_clique_vars; + i_t clique_idx = extended_clique_start_idx + i; const auto& curr_clique = clique_table.first[clique_idx]; - // check all original set packing constraints. if a set packing constraint is covered remove it. - // if it is a set partitioning constraint. keep the set partitioning and fix extensions to zero. - for (auto var_idx : curr_clique) { - curr_clique_vars.insert(var_idx); - } - for (size_t cstr_idx = 0; cstr_idx < problem.row_sense.size(); cstr_idx++) { - // only process set packing constraints - if (set_packing_constraints.count(cstr_idx) == 0) { continue; } - auto range = A.get_constraint_range(cstr_idx); - std::set curr_cstr_vars; - bool negate = false; - bool is_set_partitioning = problem.row_sense[cstr_idx] == 'E'; - if (problem.row_sense[cstr_idx] == 'G') { negate = true; } - for (i_t j = range.first; j < range.second; j++) { - i_t var_idx = A.j[j]; - f_t coeff = A.x[j]; - if (coeff < 0 != negate) { var_idx = var_idx + problem.num_cols; } - curr_cstr_vars.insert(var_idx); - } - bool constraint_covered = std::includes(curr_clique_vars.begin(), - curr_clique_vars.end(), - curr_cstr_vars.begin(), - curr_cstr_vars.end()); - if (constraint_covered) { - CUOPT_LOG_TRACE("Constraint %d is covered by clique %d", cstr_idx, clique_idx); - if (is_set_partitioning) { - for (auto var_idx : curr_clique_vars) { - if (curr_cstr_vars.count(var_idx) != 0) { continue; } - if (var_idx >= problem.num_cols) { - i_t orig_idx = var_idx - problem.num_cols; - problem.lower[orig_idx] = 1; - problem.upper[orig_idx] = 1; - } else { - problem.lower[var_idx] = 0; - problem.upper[var_idx] = 0; - } - } - } else { - removal_marker[cstr_idx] = true; - } + if (curr_clique.empty()) { continue; } + std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); + std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); + curr_clique_vars.erase(std::unique(curr_clique_vars.begin(), curr_clique_vars.end()), + curr_clique_vars.end()); + long long signature = 0; + for (auto v : curr_clique_vars) { + signature += static_cast(v); + } + size_t start = find_window_start(signature); + size_t end = std::min(sp_sigs.size(), start + dominance_window); + for (size_t idx = start; idx < end; idx++) { + const auto& sp = sp_sigs[idx]; + if (removal_marker[sp.cstr_idx]) { continue; } + const auto& vars_sp = cstr_vars[sp.cstr_idx]; + if (vars_sp.size() > curr_clique_vars.size()) { continue; } + if (!is_subset(vars_sp, curr_clique_vars)) { continue; } + if (is_set_partitioning[sp.cstr_idx]) { + CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", + clique_idx, + sp.cstr_idx); + fix_difference(curr_clique_vars, vars_sp); + } else { + removal_marker[sp.cstr_idx] = true; } } + if ((i % 128) == 0) { + CUOPT_LOG_DEBUG("Processed extended clique %d/%d", i + 1, n_extended_cliques); + } } + CUOPT_LOG_DEBUG("Dominance scan complete"); // TODO if more row removal is needed somewher else(e.g another presolve), standardize this dual_simplex::csr_matrix_t A_removed(0, 0, 0); + CUOPT_LOG_DEBUG("Removing dominated rows"); A.remove_rows(removal_marker, A_removed); + CUOPT_LOG_DEBUG("Rows removed, updating problem"); A_removed.to_compressed_col(problem.A); problem.num_rows = A_removed.m; cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); From 447713f77c163d68ed2c53b9be82a952e89ca35f Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 4 Feb 2026 08:42:27 -0800 Subject: [PATCH 023/147] fix the knapsack indices --- cpp/src/mip/diversity/diversity_manager.cu | 1 + cpp/src/mip/local_search/local_search.cu | 11 +- .../presolve/conflict_graph/clique_table.cu | 109 +++++++++--------- .../presolve/conflict_graph/clique_table.cuh | 1 + 4 files changed, 69 insertions(+), 53 deletions(-) diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index 26452ea15b..4b9f164149 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -207,6 +207,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit) problem_ptr->get_host_user_problem(host_problem); find_initial_cliques(host_problem, context.settings.tolerances); problem_ptr->set_constraints_from_host_user_problem(host_problem); + trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty) { // do the resizing no-matter what, bounds presolve might not change the bounds but diff --git a/cpp/src/mip/local_search/local_search.cu b/cpp/src/mip/local_search/local_search.cu index ecd2770651..13f7012494 100644 --- a/cpp/src/mip/local_search/local_search.cu +++ b/cpp/src/mip/local_search/local_search.cu @@ -151,7 +151,16 @@ bool local_search_t::do_fj_solve(solution_t& solution, if (time_limit == 0.) return solution.get_feasible(); timer_t timer(time_limit); - + // in case this is the first time run, resize + if (in_fj.cstr_weights.size() != (size_t)solution.problem_ptr->n_constraints) { + i_t old_size = in_fj.cstr_weights.size(); + in_fj.cstr_weights.resize(solution.problem_ptr->n_constraints, + solution.handle_ptr->get_stream()); + thrust::uninitialized_fill(solution.handle_ptr->get_thrust_policy(), + in_fj.cstr_weights.begin() + old_size, + in_fj.cstr_weights.end(), + 1.); + } auto h_weights = cuopt::host_copy(in_fj.cstr_weights, solution.handle_ptr->get_stream()); auto h_objective_weight = in_fj.objective_weight.value(solution.handle_ptr->get_stream()); for (auto& cpu_fj : ls_cpu_fj) { diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 50ec07700d..06fd71edae 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -99,10 +99,11 @@ void make_coeff_positive_knapsack_constraint( std::unordered_set& set_packing_constraints, typename mip_solver_settings_t::tolerances_t tolerances) { - for (auto& knapsack_constraint : knapsack_constraints) { - f_t rhs_offset = 0; - bool all_coeff_are_equal = true; - f_t first_coeff = std::abs(knapsack_constraint.entries[0].val); + for (i_t i = 0; i < (i_t)knapsack_constraints.size(); i++) { + auto& knapsack_constraint = knapsack_constraints[i]; + f_t rhs_offset = 0; + bool all_coeff_are_equal = true; + f_t first_coeff = std::abs(knapsack_constraint.entries[0].val); for (auto& entry : knapsack_constraint.entries) { if (entry.val < 0) { entry.val = -entry.val; @@ -119,9 +120,7 @@ void make_coeff_positive_knapsack_constraint( all_coeff_are_equal = false; } knapsack_constraint.is_set_packing = all_coeff_are_equal; - if (knapsack_constraint.is_set_packing) { - set_packing_constraints.insert(knapsack_constraint.cstr_idx); - } + if (knapsack_constraint.is_set_packing) { set_packing_constraints.insert(i); } cuopt_assert(knapsack_constraint.rhs >= 0, "RHS must be non-negative"); } } @@ -189,6 +188,8 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro for (i_t j = constraint_range.first; j < constraint_range.second; j++) { knapsack_constraint2.entries.push_back({A.j[j], -A.x[j]}); } + knapsack_constraint.pair_idx = knapsack_constraint2.cstr_idx; + knapsack_constraint2.pair_idx = knapsack_constraint.cstr_idx; knapsack_constraints.push_back(knapsack_constraint2); } knapsack_constraints.push_back(knapsack_constraint); @@ -416,10 +417,16 @@ bool extend_clique(const std::vector& clique, // fix all other variables other than complementing var for (size_t i = 0; i < new_clique.size(); i++) { if (new_clique[i] % clique_table.n_variables != complement_conflict_var) { + CUOPT_LOG_DEBUG("Fixing variable %d", new_clique[i]); if (new_clique[i] >= problem.num_cols) { + cuopt_assert(problem.lower[new_clique[i] - problem.num_cols] != 0 || + problem.upper[new_clique[i] - problem.num_cols] != 0, + "Variable is fixed to other side"); problem.lower[new_clique[i] - problem.num_cols] = 1; problem.upper[new_clique[i] - problem.num_cols] = 1; } else { + cuopt_assert(problem.lower[new_clique[i]] != 1 || problem.upper[new_clique[i]] != 1, + "Variable is fixed to other side"); problem.lower[new_clique[i]] = 0; problem.upper[new_clique[i]] = 0; } @@ -490,47 +497,34 @@ void fill_var_clique_maps(clique_table_t& clique_table) // vl1+vl2 +...+vli <= 0 // so we can fix them template -void remove_dominated_cliques(dual_simplex::user_problem_t& problem, - dual_simplex::csr_matrix_t& A, - clique_table_t& clique_table, - std::unordered_set& set_packing_constraints, - i_t n_extended_cliques) +void remove_dominated_cliques( + dual_simplex::user_problem_t& problem, + dual_simplex::csr_matrix_t& A, + clique_table_t& clique_table, + std::unordered_set& set_packing_constraints, + const std::vector>& knapsack_constraints, + i_t n_extended_cliques) { // TODO check if we need to add the dominance for the table itself i_t extended_clique_start_idx = clique_table.first.size() - n_extended_cliques; CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); - std::vector removal_marker(problem.row_sense.size(), false); - std::vector> cstr_vars(problem.row_sense.size()); - std::vector is_set_partitioning(problem.row_sense.size(), false); - CUOPT_LOG_DEBUG("Building constraint variable lists"); - for (const auto cstr_idx : set_packing_constraints) { - if (cstr_idx < 0 || cstr_idx >= static_cast(problem.row_sense.size())) { - CUOPT_LOG_ERROR( - "Invalid set packing constraint idx: %d (rows=%zu)", cstr_idx, problem.row_sense.size()); - continue; - } - auto range = A.get_constraint_range(cstr_idx); - if (range.first < 0 || range.second < 0 || range.first > range.second || - range.second > A.nz_max) { - CUOPT_LOG_ERROR("Invalid range for constraint %d: [%d, %d) nnz=%d", - cstr_idx, - range.first, - range.second, - A.nz_max); - continue; - } - bool negate = problem.row_sense[cstr_idx] == 'G'; - is_set_partitioning[cstr_idx] = problem.row_sense[cstr_idx] == 'E'; - auto& vars = cstr_vars[cstr_idx]; - vars.reserve(range.second - range.first); - for (i_t j = range.first; j < range.second; j++) { - i_t var_idx = A.j[j]; - f_t coeff = A.x[j]; - if (coeff < 0 != negate) { var_idx = var_idx + problem.num_cols; } - vars.push_back(var_idx); - } - std::sort(vars.begin(), vars.end()); - vars.erase(std::unique(vars.begin(), vars.end()), vars.end()); + std::vector removal_marker(problem.row_sense.size(), 0); + std::vector> cstr_vars(knapsack_constraints.size()); + std::vector is_set_partitioning(knapsack_constraints.size(), false); + for (const auto knapsack_idx : set_packing_constraints) { + cuopt_assert(knapsack_constraints[knapsack_idx].is_set_packing, + "Set packing constraint is not a set packing constraint"); + const auto& vars = knapsack_constraints[knapsack_idx].entries; + cstr_vars[knapsack_idx].reserve(vars.size()); + for (const auto& entry : vars) { + cstr_vars[knapsack_idx].push_back(entry.col); + } + // if the constraint has a pair index, it means it is an equality constraint + // an equality set packing constraint is a set partitioning constraint + // we can use both representation of set packing constraint to fix some other varibles in the + // larger cliques + is_set_partitioning[knapsack_idx] = knapsack_constraints[knapsack_idx].pair_idx != -1; + std::sort(cstr_vars[knapsack_idx].begin(), cstr_vars[knapsack_idx].end()); } CUOPT_LOG_DEBUG("Constraint variable lists built: %zu", set_packing_constraints.size()); constexpr size_t dominance_window = 1000; @@ -542,14 +536,15 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, std::vector sp_sigs; sp_sigs.reserve(set_packing_constraints.size()); CUOPT_LOG_DEBUG("Building set packing signatures"); - for (const auto cstr_idx : set_packing_constraints) { - const auto& vars = cstr_vars[cstr_idx]; + for (const auto knapsack_idx : set_packing_constraints) { + const auto& vars = cstr_vars[knapsack_idx]; if (vars.empty()) { continue; } long long signature = 0; for (auto v : vars) { signature += static_cast(v); } - sp_sigs.push_back({cstr_idx, static_cast(vars.size()), signature}); + sp_sigs.push_back( + {knapsack_constraints[knapsack_idx].cstr_idx, static_cast(vars.size()), signature}); } CUOPT_LOG_DEBUG("Sorting signatures: %zu", sp_sigs.size()); std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { @@ -575,10 +570,16 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, for (auto var_idx : superset) { if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } if (var_idx >= problem.num_cols) { - i_t orig_idx = var_idx - problem.num_cols; + i_t orig_idx = var_idx - problem.num_cols; + CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); + cuopt_assert(problem.lower[orig_idx] != 1 || problem.upper[orig_idx] != 1, + "Variable is fixed to other side"); problem.lower[orig_idx] = 1; problem.upper[orig_idx] = 1; } else { + CUOPT_LOG_DEBUG("Fixing variable %d", var_idx); + cuopt_assert(problem.lower[var_idx] != 1 || problem.upper[var_idx] != 1, + "Variable is fixed to other side"); problem.lower[var_idx] = 0; problem.upper[var_idx] = 0; } @@ -598,8 +599,9 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, if (curr_clique.empty()) { continue; } std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); - curr_clique_vars.erase(std::unique(curr_clique_vars.begin(), curr_clique_vars.end()), - curr_clique_vars.end()); + cuopt_assert( + std::unique(curr_clique_vars.begin(), curr_clique_vars.end()) == curr_clique_vars.end(), + "Clique variables are not unique"); long long signature = 0; for (auto v : curr_clique_vars) { signature += static_cast(v); @@ -616,13 +618,15 @@ void remove_dominated_cliques(dual_simplex::user_problem_t& problem, CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", clique_idx, sp.cstr_idx); + // note that we never deleter set partitioning constraints but it fixes some other variables fix_difference(curr_clique_vars, vars_sp); } else { + cuopt_assert(sp.cstr_idx < A.m, "Set packing constraint index is out of bounds"); removal_marker[sp.cstr_idx] = true; } } if ((i % 128) == 0) { - CUOPT_LOG_DEBUG("Processed extended clique %d/%d", i + 1, n_extended_cliques); + CUOPT_LOG_TRACE("Processed extended clique %d/%d", i + 1, n_extended_cliques); } } CUOPT_LOG_DEBUG("Dominance scan complete"); @@ -771,7 +775,8 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, t_maps = timer.elapsed_time(); i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A); t_extend = timer.elapsed_time(); - remove_dominated_cliques(problem, A, clique_table, set_packing_constraints, n_extended_cliques); + remove_dominated_cliques( + problem, A, clique_table, set_packing_constraints, knapsack_constraints, n_extended_cliques); t_remove = timer.elapsed_time(); CUOPT_LOG_DEBUG( "Clique table timing (s): fill=%.6f coeff=%.6f sort=%.6f find=%.6f small=%.6f maps=%.6f " diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index 2c40d51ebb..5bc8a6638c 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -45,6 +45,7 @@ struct knapsack_constraint_t { f_t rhs; i_t cstr_idx; bool is_set_packing = false; + i_t pair_idx = -1; }; template From db8951a2db38e5c985f20bf73bd52831153e5911 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 5 Feb 2026 05:22:24 -0800 Subject: [PATCH 024/147] fix weight and set packing issue --- cpp/src/mip/local_search/local_search.cu | 4 +- .../presolve/conflict_graph/clique_table.cu | 41 ++++++++++--------- .../presolve/conflict_graph/clique_table.cuh | 4 +- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/cpp/src/mip/local_search/local_search.cu b/cpp/src/mip/local_search/local_search.cu index 13f7012494..76e852f002 100644 --- a/cpp/src/mip/local_search/local_search.cu +++ b/cpp/src/mip/local_search/local_search.cu @@ -153,11 +153,11 @@ bool local_search_t::do_fj_solve(solution_t& solution, timer_t timer(time_limit); // in case this is the first time run, resize if (in_fj.cstr_weights.size() != (size_t)solution.problem_ptr->n_constraints) { - i_t old_size = in_fj.cstr_weights.size(); in_fj.cstr_weights.resize(solution.problem_ptr->n_constraints, solution.handle_ptr->get_stream()); + // reset weights since this is most likely the first call thrust::uninitialized_fill(solution.handle_ptr->get_thrust_policy(), - in_fj.cstr_weights.begin() + old_size, + in_fj.cstr_weights.begin(), in_fj.cstr_weights.end(), 1.); } diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 06fd71edae..85b81b8a88 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -120,6 +120,7 @@ void make_coeff_positive_knapsack_constraint( all_coeff_are_equal = false; } knapsack_constraint.is_set_packing = all_coeff_are_equal; + if (!all_coeff_are_equal) { knapsack_constraint.is_set_partitioning = false; } if (knapsack_constraint.is_set_packing) { set_packing_constraints.insert(i); } cuopt_assert(knapsack_constraint.rhs >= 0, "RHS must be non-negative"); } @@ -170,12 +171,15 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro } // equality part else { - bool ranged_constraint = ranged_constraint_counter < problem.num_range_rows && + bool is_set_partitioning = problem.rhs[i] == 1.; + bool ranged_constraint = ranged_constraint_counter < problem.num_range_rows && problem.range_rows[ranged_constraint_counter] == i; // less than part knapsack_constraint.rhs = problem.rhs[i]; if (ranged_constraint) { knapsack_constraint.rhs += problem.range_value[ranged_constraint_counter]; + is_set_partitioning = + problem.range_value[ranged_constraint_counter] == 0. && problem.rhs[i] == 1.; ranged_constraint_counter++; } for (i_t j = constraint_range.first; j < constraint_range.second; j++) { @@ -188,8 +192,8 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro for (i_t j = constraint_range.first; j < constraint_range.second; j++) { knapsack_constraint2.entries.push_back({A.j[j], -A.x[j]}); } - knapsack_constraint.pair_idx = knapsack_constraint2.cstr_idx; - knapsack_constraint2.pair_idx = knapsack_constraint.cstr_idx; + knapsack_constraint.is_set_partitioning = is_set_partitioning; + knapsack_constraint2.is_set_partitioning = is_set_partitioning; knapsack_constraints.push_back(knapsack_constraint2); } knapsack_constraints.push_back(knapsack_constraint); @@ -510,7 +514,6 @@ void remove_dominated_cliques( CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); std::vector removal_marker(problem.row_sense.size(), 0); std::vector> cstr_vars(knapsack_constraints.size()); - std::vector is_set_partitioning(knapsack_constraints.size(), false); for (const auto knapsack_idx : set_packing_constraints) { cuopt_assert(knapsack_constraints[knapsack_idx].is_set_packing, "Set packing constraint is not a set packing constraint"); @@ -519,17 +522,13 @@ void remove_dominated_cliques( for (const auto& entry : vars) { cstr_vars[knapsack_idx].push_back(entry.col); } - // if the constraint has a pair index, it means it is an equality constraint - // an equality set packing constraint is a set partitioning constraint - // we can use both representation of set packing constraint to fix some other varibles in the - // larger cliques - is_set_partitioning[knapsack_idx] = knapsack_constraints[knapsack_idx].pair_idx != -1; std::sort(cstr_vars[knapsack_idx].begin(), cstr_vars[knapsack_idx].end()); } CUOPT_LOG_DEBUG("Constraint variable lists built: %zu", set_packing_constraints.size()); constexpr size_t dominance_window = 1000; struct clique_sig_t { - i_t cstr_idx; + i_t knapsack_idx; + i_t row_idx; i_t size; long long signature; }; @@ -543,8 +542,10 @@ void remove_dominated_cliques( for (auto v : vars) { signature += static_cast(v); } - sp_sigs.push_back( - {knapsack_constraints[knapsack_idx].cstr_idx, static_cast(vars.size()), signature}); + sp_sigs.push_back({knapsack_idx, + knapsack_constraints[knapsack_idx].cstr_idx, + static_cast(vars.size()), + signature}); } CUOPT_LOG_DEBUG("Sorting signatures: %zu", sp_sigs.size()); std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { @@ -572,7 +573,7 @@ void remove_dominated_cliques( if (var_idx >= problem.num_cols) { i_t orig_idx = var_idx - problem.num_cols; CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); - cuopt_assert(problem.lower[orig_idx] != 1 || problem.upper[orig_idx] != 1, + cuopt_assert(problem.lower[orig_idx] != 0 || problem.upper[orig_idx] != 0, "Variable is fixed to other side"); problem.lower[orig_idx] = 1; problem.upper[orig_idx] = 1; @@ -610,19 +611,21 @@ void remove_dominated_cliques( size_t end = std::min(sp_sigs.size(), start + dominance_window); for (size_t idx = start; idx < end; idx++) { const auto& sp = sp_sigs[idx]; - if (removal_marker[sp.cstr_idx]) { continue; } - const auto& vars_sp = cstr_vars[sp.cstr_idx]; + if (sp.row_idx >= 0 && sp.row_idx < static_cast(removal_marker.size()) && + removal_marker[sp.row_idx]) { + continue; + } + const auto& vars_sp = cstr_vars[sp.knapsack_idx]; if (vars_sp.size() > curr_clique_vars.size()) { continue; } if (!is_subset(vars_sp, curr_clique_vars)) { continue; } - if (is_set_partitioning[sp.cstr_idx]) { + if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", clique_idx, - sp.cstr_idx); + sp.row_idx); // note that we never deleter set partitioning constraints but it fixes some other variables fix_difference(curr_clique_vars, vars_sp); } else { - cuopt_assert(sp.cstr_idx < A.m, "Set packing constraint index is out of bounds"); - removal_marker[sp.cstr_idx] = true; + if (sp.row_idx >= 0 && sp.row_idx < A.m) { removal_marker[sp.row_idx] = true; } } } if ((i % 128) == 0) { diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index 5bc8a6638c..cead4c5e8b 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -44,8 +44,8 @@ struct knapsack_constraint_t { std::vector> entries; f_t rhs; i_t cstr_idx; - bool is_set_packing = false; - i_t pair_idx = -1; + bool is_set_packing = false; + bool is_set_partitioning = false; }; template From ab2339b4a1f93dfc55461c9fa5c38664dd0cb122 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 5 Feb 2026 10:51:29 -0800 Subject: [PATCH 025/147] fix obj scale issue --- .../presolve/conflict_graph/clique_table.cu | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 85b81b8a88..ea04f6e59c 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -340,24 +340,25 @@ bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) template void insert_clique_into_problem(const std::vector& clique, dual_simplex::user_problem_t& problem, - dual_simplex::csr_matrix_t& A) + dual_simplex::csr_matrix_t& A, + f_t coeff_scale) { // convert vertices into original vars f_t rhs_offset = 0.; std::vector new_vars; std::vector new_coeffs; for (size_t i = 0; i < clique.size(); i++) { - f_t coeff = 1.; + f_t coeff = coeff_scale; i_t var_idx = clique[i]; if (var_idx >= problem.num_cols) { - coeff = -1.; + coeff = -coeff_scale; var_idx = var_idx - problem.num_cols; - rhs_offset--; + rhs_offset += coeff_scale; } new_vars.push_back(var_idx); new_coeffs.push_back(coeff); } - f_t rhs = 1 + rhs_offset; + f_t rhs = coeff_scale + rhs_offset; // insert the new clique into the problem as a new constraint A.insert_row(new_vars, new_coeffs); problem.row_sense.push_back('L'); @@ -368,7 +369,8 @@ template bool extend_clique(const std::vector& clique, clique_table_t& clique_table, dual_simplex::user_problem_t& problem, - dual_simplex::csr_matrix_t& A) + dual_simplex::csr_matrix_t& A, + f_t coeff_scale) { i_t smallest_degree = std::numeric_limits::max(); i_t smallest_degree_var = -1; @@ -441,7 +443,7 @@ bool extend_clique(const std::vector& clique, clique_table.first.push_back(new_clique); CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); // insert the new clique into the problem as a new constraint - insert_clique_into_problem(new_clique, problem, A); + insert_clique_into_problem(new_clique, problem, A, coeff_scale); } } return new_clique.size() > clique.size(); @@ -465,7 +467,8 @@ i_t extend_cliques(const std::vector>& knapsack_ for (const auto& entry : knapsack_constraint.entries) { clique.push_back(entry.col); } - bool extended_clique = extend_clique(clique, clique_table, problem, A); + f_t coeff_scale = knapsack_constraint.entries[0].val; + bool extended_clique = extend_clique(clique, clique_table, problem, A, coeff_scale); if (extended_clique) { n_extended_cliques++; } } } @@ -809,12 +812,4 @@ INSTANTIATE(double) #endif #undef INSTANTIATE -// #if MIP_INSTANTIATE_FLOAT -// template class bound_presolve_t; -// #endif - -// #if MIP_INSTANTIATE_DOUBLE -// template class bound_presolve_t; -// #endif - } // namespace cuopt::linear_programming::detail From d7f6a809ab0d599f496d648037b352889ea75053 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Feb 2026 03:57:29 -0800 Subject: [PATCH 026/147] without cliques --- cpp/src/mip/diversity/diversity_manager.cu | 14 +++++++------- .../mip/presolve/conflict_graph/clique_table.cu | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index d9f2195c4f..1a11fec8e9 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -202,13 +202,13 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!context.settings.heuristics_only && !problem_ptr->empty) { - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - find_initial_cliques(host_problem, context.settings.tolerances); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - trivial_presolve(*problem_ptr, remap_cache_ids); - } + // if (!context.settings.heuristics_only && !problem_ptr->empty) { + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // find_initial_cliques(host_problem, context.settings.tolerances); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // } if (!problem_ptr->empty) { // do the resizing no-matter what, bounds presolve might not change the bounds but // initial trivial presolve might have diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index ea04f6e59c..0d536e6949 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -441,7 +441,9 @@ bool extend_clique(const std::vector& clique, return false; } else { clique_table.first.push_back(new_clique); +#if DEBUG_KNAPSACK_CONSTRAINTS CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); +#endif // insert the new clique into the problem as a new constraint insert_clique_into_problem(new_clique, problem, A, coeff_scale); } From f58b8c5767aedc2bad001ca46d84539c85f44fc0 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Feb 2026 03:58:06 -0800 Subject: [PATCH 027/147] with cliques --- cpp/src/mip/diversity/diversity_manager.cu | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index 1a11fec8e9..d9f2195c4f 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -202,13 +202,13 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // if (!context.settings.heuristics_only && !problem_ptr->empty) { - // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - // problem_ptr->get_host_user_problem(host_problem); - // find_initial_cliques(host_problem, context.settings.tolerances); - // problem_ptr->set_constraints_from_host_user_problem(host_problem); - // trivial_presolve(*problem_ptr, remap_cache_ids); - // } + if (!context.settings.heuristics_only && !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + find_initial_cliques(host_problem, context.settings.tolerances); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + trivial_presolve(*problem_ptr, remap_cache_ids); + } if (!problem_ptr->empty) { // do the resizing no-matter what, bounds presolve might not change the bounds but // initial trivial presolve might have From 4d7166a1f8e4678855f799a231f11b6d4a2e7bcc Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Feb 2026 07:50:05 -0800 Subject: [PATCH 028/147] initial clique cut implemnetation --- .../cuopt/linear_programming/constants.h | 1 + .../mip/solver_settings.hpp | 1 + cpp/src/dual_simplex/branch_and_bound.cpp | 15 +- cpp/src/dual_simplex/branch_and_bound.hpp | 11 +- cpp/src/dual_simplex/cuts.cpp | 407 ++++++++++++++++++ cpp/src/dual_simplex/cuts.hpp | 34 +- .../dual_simplex/simplex_solver_settings.hpp | 2 + cpp/src/math_optimization/solver_settings.cu | 1 + cpp/src/mip/diversity/diversity_manager.cu | 7 +- .../presolve/conflict_graph/clique_table.cu | 85 +++- .../presolve/conflict_graph/clique_table.cuh | 11 +- cpp/src/mip/problem/problem.cu | 4 + cpp/src/mip/problem/problem.cuh | 6 + cpp/src/mip/solver.cu | 6 +- 14 files changed, 561 insertions(+), 30 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 7caf7aeeb2..e5665c6c9a 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -61,6 +61,7 @@ #define CUOPT_MIP_MIXED_INTEGER_ROUNDING_CUTS "mip_mixed_integer_rounding_cuts" #define CUOPT_MIP_MIXED_INTEGER_GOMORY_CUTS "mip_mixed_integer_gomory_cuts" #define CUOPT_MIP_KNAPSACK_CUTS "mip_knapsack_cuts" +#define CUOPT_MIP_CLIQUE_CUTS "mip_clique_cuts" #define CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS "mip_strong_chvatal_gomory_cuts" #define CUOPT_MIP_REDUCED_COST_STRENGTHENING "mip_reduced_cost_strengthening" #define CUOPT_MIP_CUT_CHANGE_THRESHOLD "mip_cut_change_threshold" diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 863e5d66d6..95bcc3d9f0 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -91,6 +91,7 @@ class mip_solver_settings_t { i_t mir_cuts = -1; i_t mixed_integer_gomory_cuts = -1; i_t knapsack_cuts = -1; + i_t clique_cuts = -1; i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index d354159046..8e81c2d60a 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -232,9 +232,11 @@ template branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, - f_t start_time) + f_t start_time, + std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> clique_table) : original_problem_(user_problem), settings_(solver_settings), + clique_table_(std::move(clique_table)), original_lp_(user_problem.handle_ptr, 1, 1, 1), Arow_(1, 1, 0), incumbent_(1), @@ -1822,8 +1824,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } cut_pool_t cut_pool(original_lp_.num_cols, settings_); - cut_generation_t cut_generation( - cut_pool, original_lp_, settings_, Arow_, new_slacks_, var_types_); + cut_generation_t cut_generation(cut_pool, + original_lp_, + settings_, + Arow_, + new_slacks_, + var_types_, + original_problem_, + clique_table_); std::vector saved_solution; #ifdef CHECK_CUTS_AGAINST_SAVED_SOLUTION @@ -1861,6 +1869,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut var_types_, basis_update, root_relax_soln_.x, + root_relax_soln_.z, basic_list, nonbasic_list); f_t cut_generation_time = toc(cut_start_time); diff --git a/cpp/src/dual_simplex/branch_and_bound.hpp b/cpp/src/dual_simplex/branch_and_bound.hpp index 6db45e1531..10ef27b821 100644 --- a/cpp/src/dual_simplex/branch_and_bound.hpp +++ b/cpp/src/dual_simplex/branch_and_bound.hpp @@ -23,8 +23,14 @@ #include #include +#include #include +namespace cuopt::linear_programming::detail { +template +struct clique_table_t; +} + namespace cuopt::linear_programming::dual_simplex { enum class mip_status_t { @@ -74,7 +80,9 @@ class branch_and_bound_t { public: branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, - f_t start_time); + f_t start_time, + std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> + clique_table = nullptr); // Set an initial guess based on the user_problem. This should be called before solve. void set_initial_guess(const std::vector& user_guess) { guess_ = user_guess; } @@ -137,6 +145,7 @@ class branch_and_bound_t { private: const user_problem_t& original_problem_; const simplex_solver_settings_t settings_; + std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> clique_table_; // Initial guess. std::vector guess_; diff --git a/cpp/src/dual_simplex/cuts.cpp b/cpp/src/dual_simplex/cuts.cpp index be3f3001d8..7ace8048d7 100644 --- a/cpp/src/dual_simplex/cuts.cpp +++ b/cpp/src/dual_simplex/cuts.cpp @@ -9,9 +9,269 @@ #include #include #include +#include +#include + +#include +#include +#include namespace cuopt::linear_programming::dual_simplex { +namespace { + +template +bool build_clique_cut(const std::vector& clique_vertices, + i_t num_vars, + const std::vector& var_types, + const std::vector& lower_bounds, + const std::vector& upper_bounds, + const std::vector& xstar, + f_t bound_tol, + f_t min_violation, + sparse_vector_t& cut, + f_t& cut_rhs) +{ + if (clique_vertices.size() < 2) { return false; } + + cuopt_assert(num_vars > 0, "Clique cut num_vars must be positive"); + cuopt_assert(static_cast(num_vars) <= lower_bounds.size(), + "Clique cut lower bounds size mismatch"); + cuopt_assert(static_cast(num_vars) <= upper_bounds.size(), + "Clique cut upper bounds size mismatch"); + cuopt_assert(static_cast(num_vars) <= var_types.size(), + "Clique cut var_types size mismatch"); + cuopt_assert(static_cast(num_vars) <= xstar.size(), "Clique cut xstar size mismatch"); + + cut.i.clear(); + cut.x.clear(); + i_t num_complements = 0; + std::unordered_set seen_original; + std::unordered_set seen_complement; + seen_original.reserve(clique_vertices.size()); + seen_complement.reserve(clique_vertices.size()); + + for (const auto vertex_idx : clique_vertices) { + cuopt_assert(vertex_idx >= 0 && vertex_idx < 2 * num_vars, "Clique vertex out of range"); + const i_t var_idx = vertex_idx % num_vars; + const bool complement = vertex_idx >= num_vars; + const f_t lower_bound = lower_bounds[var_idx]; + const f_t upper_bound = upper_bounds[var_idx]; + + cuopt_assert(var_types[var_idx] != variable_type_t::CONTINUOUS, + "Clique contains continuous variable"); + cuopt_assert(lower_bound >= -bound_tol, "Clique variable lower bound below zero"); + cuopt_assert(upper_bound <= 1 + bound_tol, "Clique variable upper bound above one"); + + if (complement) { + cuopt_assert(seen_original.count(var_idx) == 0, + "Clique contains a variable and its complement"); + cuopt_assert(seen_complement.insert(var_idx).second, "Duplicate complement in clique"); + num_complements++; + cut.i.push_back(var_idx); + cut.x.push_back(1.0); + } else { + cuopt_assert(seen_complement.count(var_idx) == 0, + "Clique contains a variable and its complement"); + cuopt_assert(seen_original.insert(var_idx).second, "Duplicate variable in clique"); + cut.i.push_back(var_idx); + cut.x.push_back(-1.0); + } + } + + if (cut.i.empty()) { return false; } + + cut_rhs = static_cast(num_complements - 1); + cut.sort(); + + const f_t dot = cut.dot(xstar); + const f_t violation = cut_rhs - dot; + return violation > min_violation; +} + +template +struct bk_bitset_context_t { + const std::vector>& adj; + const std::vector& weights; + f_t min_weight; + i_t max_calls; + size_t words; + i_t num_calls{0}; + std::vector> cliques; +}; + +inline size_t bitset_words(size_t n) { return (n + 63) / 64; } + +inline bool bitset_any(const std::vector& bs) +{ + for (auto word : bs) { + if (word != 0) { return true; } + } + return false; +} + +inline void bitset_set(std::vector& bs, size_t idx) +{ + bs[idx >> 6] |= (uint64_t(1) << (idx & 63)); +} + +inline void bitset_clear(std::vector& bs, size_t idx) +{ + bs[idx >> 6] &= ~(uint64_t(1) << (idx & 63)); +} + +template +f_t sum_weights_bitset(const std::vector& bs, const std::vector& weights) +{ + f_t sum = 0.0; + for (size_t w = 0; w < bs.size(); ++w) { + uint64_t word = bs[w]; + while (word) { + const int bit = __builtin_ctzll(word); + const size_t idx = w * 64 + static_cast(bit); + sum += weights[idx]; + word &= (word - 1); + } + } + return sum; +} + +template +void bron_kerbosch_bitset(bk_bitset_context_t& ctx, + std::vector& R, + std::vector& P, + std::vector& X, + f_t weight_R) +{ + ctx.num_calls++; + if (ctx.num_calls > ctx.max_calls) { return; } + + if (!bitset_any(P) && !bitset_any(X)) { + if (weight_R >= ctx.min_weight) { ctx.cliques.push_back(R); } + return; + } + + const f_t sumP = sum_weights_bitset(P, ctx.weights); + if (weight_R + sumP < ctx.min_weight) { return; } + + i_t pivot = -1; + i_t max_deg = -1; + for (size_t w = 0; w < ctx.words; ++w) { + uint64_t word = P[w] | X[w]; + while (word) { + const int bit = __builtin_ctzll(word); + const i_t v = static_cast(w * 64 + static_cast(bit)); + word &= (word - 1); + i_t count = 0; + for (size_t k = 0; k < ctx.words; ++k) { + count += __builtin_popcountll(P[k] & ctx.adj[v][k]); + } + if (count > max_deg) { + max_deg = count; + pivot = v; + } + } + } + + std::vector candidates; + candidates.reserve(ctx.weights.size()); + if (pivot >= 0) { + for (size_t w = 0; w < ctx.words; ++w) { + uint64_t word = P[w] & ~ctx.adj[pivot][w]; + while (word) { + const int bit = __builtin_ctzll(word); + const i_t v = static_cast(w * 64 + static_cast(bit)); + word &= (word - 1); + candidates.push_back(v); + } + } + } else { + for (size_t w = 0; w < ctx.words; ++w) { + uint64_t word = P[w]; + while (word) { + const int bit = __builtin_ctzll(word); + const i_t v = static_cast(w * 64 + static_cast(bit)); + word &= (word - 1); + candidates.push_back(v); + } + } + } + + for (auto v : candidates) { + R.push_back(v); + std::vector P_next(ctx.words, 0); + std::vector X_next(ctx.words, 0); + for (size_t k = 0; k < ctx.words; ++k) { + P_next[k] = P[k] & ctx.adj[v][k]; + X_next[k] = X[k] & ctx.adj[v][k]; + } + bron_kerbosch_bitset(ctx, R, P_next, X_next, weight_R + ctx.weights[v]); + R.pop_back(); + bitset_clear(P, static_cast(v)); + bitset_set(X, static_cast(v)); + } +} + +template +void extend_clique_vertices(std::vector& clique_vertices, + ::cuopt::linear_programming::detail::clique_table_t& graph, + const std::vector& xstar, + const std::vector& reduced_costs, + i_t num_vars, + f_t integer_tol) +{ + if (clique_vertices.empty()) { return; } + + i_t smallest_degree = std::numeric_limits::max(); + i_t smallest_degree_var = -1; + for (auto v : clique_vertices) { + i_t degree = graph.get_degree_of_var(v); + if (degree < smallest_degree) { + smallest_degree = degree; + smallest_degree_var = v; + } + } + + auto adj_set = graph.get_adj_set_of_var(smallest_degree_var); + std::unordered_set clique_members(clique_vertices.begin(), clique_vertices.end()); + std::vector candidates; + candidates.reserve(adj_set.size()); + for (const auto& candidate : adj_set) { + if (clique_members.count(candidate) != 0) { continue; } + i_t var_idx = candidate % num_vars; + f_t value = candidate >= num_vars ? (1.0 - xstar[var_idx]) : xstar[var_idx]; + if (std::abs(value - std::round(value)) <= integer_tol) { candidates.push_back(candidate); } + } + + auto reduced_cost = [&](i_t vertex_idx) -> f_t { + i_t var_idx = vertex_idx % num_vars; + if (var_idx < 0 || var_idx >= static_cast(reduced_costs.size())) { return 0.0; } + f_t rc = reduced_costs[var_idx]; + if (!std::isfinite(rc)) { rc = 0.0; } + return vertex_idx >= num_vars ? -rc : rc; + }; + + std::sort(candidates.begin(), candidates.end(), [&](i_t a, i_t b) { + return reduced_cost(a) < reduced_cost(b); + }); + + for (const auto candidate : candidates) { + bool add = true; + for (const auto v : clique_vertices) { + if (!graph.check_adjacency(candidate, v)) { + add = false; + break; + } + } + if (add) { + clique_vertices.push_back(candidate); + clique_members.insert(candidate); + } + } +} + +} // namespace + template void cut_pool_t::add_cut(cut_type_t cut_type, const sparse_vector_t& cut, @@ -558,6 +818,7 @@ void cut_generation_t::generate_cuts(const lp_problem_t& lp, const std::vector& var_types, basis_update_mpf_t& basis_update, const std::vector& xstar, + const std::vector& reduced_costs, const std::vector& basic_list, const std::vector& nonbasic_list) { @@ -582,6 +843,16 @@ void cut_generation_t::generate_cuts(const lp_problem_t& lp, } } + // Generate Clique cuts + if (settings.clique_cuts != 0) { + f_t cut_start_time = tic(); + generate_clique_cuts(lp, settings, var_types, xstar, reduced_costs); + f_t cut_generation_time = toc(cut_start_time); + if (cut_generation_time > 1.0) { + settings.log.debug("Clique cut generation time %.2f seconds\n", cut_generation_time); + } + } + // Generate MIR and CG cuts if (settings.mir_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { f_t cut_start_time = tic(); @@ -613,6 +884,142 @@ void cut_generation_t::generate_knapsack_cuts( } } +template +void cut_generation_t::generate_clique_cuts( + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& var_types, + const std::vector& xstar, + const std::vector& reduced_costs) +{ + if (settings.clique_cuts == 0) { return; } + + const i_t num_vars = user_problem_.num_cols; + if (num_vars <= 0) { return; } + + if (clique_table_ == nullptr) { + ::cuopt::linear_programming::detail::clique_config_t clique_config; + clique_config.min_clique_size = 1; + clique_table_ = std::make_shared<::cuopt::linear_programming::detail::clique_table_t>( + 2 * num_vars, clique_config.min_clique_size, clique_config.max_clique_size_for_extension); + + typename ::cuopt::linear_programming::mip_solver_settings_t::tolerances_t tolerances; + tolerances.presolve_absolute_tolerance = settings.integer_tol; + tolerances.absolute_tolerance = settings.integer_tol; + tolerances.relative_tolerance = settings.zero_tol; + tolerances.integrality_tolerance = settings.integer_tol; + tolerances.absolute_mip_gap = settings.absolute_mip_gap_tol; + tolerances.relative_mip_gap = settings.relative_mip_gap_tol; + + ::cuopt::linear_programming::detail::build_clique_table( + user_problem_, *clique_table_, tolerances, true, true); + } + + if (clique_table_->first.empty() && clique_table_->addtl_cliques.empty()) { return; } + + cuopt_assert(clique_table_->n_variables == num_vars, "Clique table variable count mismatch"); + cuopt_assert(static_cast(num_vars) <= xstar.size(), "Clique cut xstar size mismatch"); + + const f_t min_violation = std::max(settings.primal_tol, static_cast(1e-6)); + const f_t bound_tol = settings.primal_tol; + const f_t min_weight = 1.0 + min_violation; + const i_t max_calls = 100000; + + cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), + "User problem var_types size mismatch"); + + std::vector vertices; + std::vector weights; + vertices.reserve(num_vars * 2); + weights.reserve(num_vars * 2); + + for (i_t j = 0; j < num_vars; ++j) { + if (user_problem_.var_types[j] == variable_type_t::CONTINUOUS) { continue; } + const f_t lower_bound = user_problem_.lower[j]; + const f_t upper_bound = user_problem_.upper[j]; + if (lower_bound < -bound_tol || upper_bound > 1 + bound_tol) { continue; } + const f_t xj = xstar[j]; + if (std::abs(xj - std::round(xj)) <= settings.integer_tol) { continue; } + vertices.push_back(j); + weights.push_back(xj); + vertices.push_back(j + num_vars); + weights.push_back(1.0 - xj); + } + + if (vertices.empty()) { return; } + + std::vector vertex_to_local(2 * num_vars, -1); + std::vector in_subgraph(2 * num_vars, 0); + for (size_t idx = 0; idx < vertices.size(); ++idx) { + vertex_to_local[vertices[idx]] = static_cast(idx); + in_subgraph[vertices[idx]] = 1; + } + + std::vector> adj_local(vertices.size()); + for (size_t idx = 0; idx < vertices.size(); ++idx) { + i_t vertex_idx = vertices[idx]; + auto adj_set = clique_table_->get_adj_set_of_var(vertex_idx); + auto& adj = adj_local[idx]; + adj.reserve(adj_set.size() + 1); + for (const auto neighbor : adj_set) { + if (neighbor >= 0 && neighbor < 2 * num_vars && in_subgraph[neighbor]) { + i_t local_neighbor = vertex_to_local[neighbor]; + if (local_neighbor >= 0) { adj.push_back(local_neighbor); } + } + } + i_t complement = vertex_idx < num_vars ? vertex_idx + num_vars : vertex_idx - num_vars; + if (in_subgraph[complement]) { + i_t local_neighbor = vertex_to_local[complement]; + if (local_neighbor >= 0) { adj.push_back(local_neighbor); } + } + std::sort(adj.begin(), adj.end()); + adj.erase(std::unique(adj.begin(), adj.end()), adj.end()); + } + + const size_t words = bitset_words(vertices.size()); + std::vector> adj_bitset(vertices.size(), std::vector(words, 0)); + for (size_t v = 0; v < adj_local.size(); ++v) { + for (const auto neighbor : adj_local[v]) { + if (neighbor >= 0 && static_cast(neighbor) < vertices.size()) { + bitset_set(adj_bitset[v], static_cast(neighbor)); + } + } + } + + bk_bitset_context_t ctx{adj_bitset, weights, min_weight, max_calls, words}; + std::vector R; + std::vector P(words, 0); + std::vector X(words, 0); + for (size_t idx = 0; idx < vertices.size(); ++idx) { + bitset_set(P, idx); + } + bron_kerbosch_bitset(ctx, R, P, X, 0.0); + + sparse_vector_t cut(lp.num_cols, 0); + f_t cut_rhs = 0.0; + for (auto& clique_local : ctx.cliques) { + std::vector clique_vertices; + clique_vertices.reserve(clique_local.size()); + for (auto local_idx : clique_local) { + clique_vertices.push_back(vertices[local_idx]); + } + extend_clique_vertices( + clique_vertices, *clique_table_, xstar, reduced_costs, num_vars, settings.integer_tol); + if (build_clique_cut(clique_vertices, + num_vars, + var_types, + user_problem_.lower, + user_problem_.upper, + xstar, + bound_tol, + min_violation, + cut, + cut_rhs)) { + cut_pool_.add_cut(cut_type_t::CLIQUE, cut, cut_rhs); + } + } +} + template void cut_generation_t::generate_mir_cuts( const lp_problem_t& lp, diff --git a/cpp/src/dual_simplex/cuts.hpp b/cpp/src/dual_simplex/cuts.hpp index a4a36d75b2..2a88094de4 100644 --- a/cpp/src/dual_simplex/cuts.hpp +++ b/cpp/src/dual_simplex/cuts.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -22,6 +23,11 @@ #include #include +namespace cuopt::linear_programming::detail { +template +struct clique_table_t; +} + namespace cuopt::linear_programming::dual_simplex { enum cut_type_t : int8_t { @@ -29,7 +35,8 @@ enum cut_type_t : int8_t { MIXED_INTEGER_ROUNDING = 1, KNAPSACK = 2, CHVATAL_GOMORY = 3, - MAX_CUT_TYPE = 4 + CLIQUE = 4, + MAX_CUT_TYPE = 5 }; template @@ -48,8 +55,9 @@ struct cut_info_t { num_cuts[static_cast(cut_type)]++; } } - const char* cut_type_names[MAX_CUT_TYPE] = {"Gomory ", "MIR ", "Knapsack ", "Strong CG"}; - std::array num_cuts = {0}; + const char* cut_type_names[MAX_CUT_TYPE] = { + "Gomory ", "MIR ", "Knapsack ", "Strong CG", "Clique "}; + std::array num_cuts = {0}; }; template @@ -226,8 +234,14 @@ class cut_generation_t { const simplex_solver_settings_t& settings, csr_matrix_t& Arow, const std::vector& new_slacks, - const std::vector& var_types) - : cut_pool_(cut_pool), knapsack_generation_(lp, settings, Arow, new_slacks, var_types) + const std::vector& var_types, + const user_problem_t& user_problem, + std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> + clique_table = nullptr) + : cut_pool_(cut_pool), + knapsack_generation_(lp, settings, Arow, new_slacks, var_types), + user_problem_(user_problem), + clique_table_(std::move(clique_table)) { } @@ -238,6 +252,7 @@ class cut_generation_t { const std::vector& var_types, basis_update_mpf_t& basis_update, const std::vector& xstar, + const std::vector& reduced_costs, const std::vector& basic_list, const std::vector& nonbasic_list); @@ -269,8 +284,17 @@ class cut_generation_t { const std::vector& var_types, const std::vector& xstar); + // Generate clique cuts from conflict graph cliques + void generate_clique_cuts(const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + const std::vector& var_types, + const std::vector& xstar, + const std::vector& reduced_costs); + cut_pool_t& cut_pool_; knapsack_generation_t knapsack_generation_; + const user_problem_t& user_problem_; + std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> clique_table_; }; template diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index f9911ee53a..e3f10f7d54 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -91,6 +91,7 @@ struct simplex_solver_settings_t { mir_cuts(-1), mixed_integer_gomory_cuts(-1), knapsack_cuts(-1), + clique_cuts(-1), strong_chvatal_gomory_cuts(-1), reduced_cost_strengthening(-1), cut_change_threshold(1e-3), @@ -169,6 +170,7 @@ struct simplex_solver_settings_t { i_t mixed_integer_gomory_cuts; // -1 automatic, 0 to disable, >0 to enable mixed integer Gomory // cuts i_t knapsack_cuts; // -1 automatic, 0 to disable, >0 to enable knapsack cuts + i_t clique_cuts; // -1 automatic, 0 to disable, >0 to enable clique cuts i_t strong_chvatal_gomory_cuts; // -1 automatic, 0 to disable, >0 to enable strong Chvatal Gomory // cuts i_t reduced_cost_strengthening; // -1 automatic, 0 to disable, >0 to enable reduced cost diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 9dc6ac9c5e..a9c56ab204 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -93,6 +93,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_MIP_MIXED_INTEGER_ROUNDING_CUTS, &mip_settings.mir_cuts, -1, 1, -1}, {CUOPT_MIP_MIXED_INTEGER_GOMORY_CUTS, &mip_settings.mixed_integer_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_KNAPSACK_CUTS, &mip_settings.knapsack_cuts, -1, 1, -1}, + {CUOPT_MIP_CLIQUE_CUTS, &mip_settings.clique_cuts, -1, 1, -1}, {CUOPT_MIP_STRONG_CHVATAL_GOMORY_CUTS, &mip_settings.strong_chvatal_gomory_cuts, -1, 1, -1}, {CUOPT_MIP_REDUCED_COST_STRENGTHENING, &mip_settings.reduced_cost_strengthening, -1, std::numeric_limits::max(), -1}, {CUOPT_NUM_GPUS, &pdlp_settings.num_gpus, 1, 2, 1}, diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index d9f2195c4f..6613d1ecc5 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -19,6 +19,8 @@ #include +#include + constexpr bool fj_only_run = false; namespace cuopt::linear_programming::detail { @@ -205,9 +207,12 @@ bool diversity_manager_t::run_presolve(f_t time_limit) if (!context.settings.heuristics_only && !problem_ptr->empty) { dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); - find_initial_cliques(host_problem, context.settings.tolerances); + std::shared_ptr> clique_table; + auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + find_initial_cliques(host_problem, context.settings.tolerances, clique_table_ptr); problem_ptr->set_constraints_from_host_user_problem(host_problem); trivial_presolve(*problem_ptr, remap_cache_ids); + if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } } if (!problem_ptr->empty) { // do the resizing no-matter what, bounds presolve might not change the bounds but diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 0d536e6949..c1f9e03489 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -496,6 +496,32 @@ void fill_var_clique_maps(clique_table_t& clique_table) } } +template +void build_clique_table(const dual_simplex::user_problem_t& problem, + clique_table_t& clique_table, + typename mip_solver_settings_t::tolerances_t tolerances, + bool remove_small_cliques_flag, + bool fill_var_clique_maps_flag) +{ + cuopt_assert(clique_table.n_variables == problem.num_cols, "Clique table size mismatch"); + cuopt_assert(problem.var_types.size() == static_cast(problem.num_cols), + "Problem variable types size mismatch"); + std::vector> knapsack_constraints; + std::unordered_set set_packing_constraints; + dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); + problem.A.to_compressed_row(A); + fill_knapsack_constraints(problem, knapsack_constraints, A); + make_coeff_positive_knapsack_constraint( + problem, knapsack_constraints, set_packing_constraints, tolerances); + sort_csr_by_constraint_coefficients(knapsack_constraints); + clique_table.tolerances = tolerances; + for (const auto& knapsack_constraint : knapsack_constraints) { + find_cliques_from_constraint(knapsack_constraint, clique_table); + } + if (remove_small_cliques_flag) { remove_small_cliques(clique_table); } + if (fill_var_clique_maps_flag) { fill_var_clique_maps(clique_table); } +} + // we want to remove constraints that are covered by extended cliques // for set partitioning constraints, we will keep the constraint on original problem but fix // extended vars to zero For a set partitioning constraint: v1+v2+...+vk = 1 and discovered: @@ -738,7 +764,8 @@ void print_clique_table(const clique_table_t& clique_table) template void find_initial_cliques(dual_simplex::user_problem_t& problem, - typename mip_solver_settings_t::tolerances_t tolerances) + typename mip_solver_settings_t::tolerances_t tolerances, + std::shared_ptr>* clique_table_out) { cuopt::timer_t timer(std::numeric_limits::infinity()); double t_fill = 0.; @@ -763,28 +790,42 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, // print_knapsack_constraints(knapsack_constraints); // TODO think about getting min_clique_size according to some problem property clique_config_t clique_config; - clique_table_t clique_table(2 * problem.num_cols, - clique_config.min_clique_size, - clique_config.max_clique_size_for_extension); - clique_table.tolerances = tolerances; + std::shared_ptr> clique_table_shared; + clique_table_t clique_table_local(2 * problem.num_cols, + clique_config.min_clique_size, + clique_config.max_clique_size_for_extension); + clique_table_t* clique_table_ptr = &clique_table_local; + if (clique_table_out != nullptr) { + clique_table_shared = + std::make_shared>(2 * problem.num_cols, + clique_config.min_clique_size, + clique_config.max_clique_size_for_extension); + clique_table_ptr = clique_table_shared.get(); + } + clique_table_ptr->tolerances = tolerances; for (const auto& knapsack_constraint : knapsack_constraints) { - find_cliques_from_constraint(knapsack_constraint, clique_table); + find_cliques_from_constraint(knapsack_constraint, *clique_table_ptr); } t_find = timer.elapsed_time(); CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d", - clique_table.first.size(), - clique_table.addtl_cliques.size()); + clique_table_ptr->first.size(), + clique_table_ptr->addtl_cliques.size()); // print_clique_table(clique_table); // remove small cliques and add them to adj_list - remove_small_cliques(clique_table); + remove_small_cliques(*clique_table_ptr); t_small = timer.elapsed_time(); // fill var clique maps - fill_var_clique_maps(clique_table); - t_maps = timer.elapsed_time(); - i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A); + fill_var_clique_maps(*clique_table_ptr); + t_maps = timer.elapsed_time(); + if (clique_table_out != nullptr) { *clique_table_out = std::move(clique_table_shared); } + i_t n_extended_cliques = extend_cliques(knapsack_constraints, *clique_table_ptr, problem, A); t_extend = timer.elapsed_time(); - remove_dominated_cliques( - problem, A, clique_table, set_packing_constraints, knapsack_constraints, n_extended_cliques); + remove_dominated_cliques(problem, + A, + *clique_table_ptr, + set_packing_constraints, + knapsack_constraints, + n_extended_cliques); t_remove = timer.elapsed_time(); CUOPT_LOG_DEBUG( "Clique table timing (s): fill=%.6f coeff=%.6f sort=%.6f find=%.6f small=%.6f maps=%.6f " @@ -801,10 +842,18 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, // exit(0); } -#define INSTANTIATE(F_TYPE) \ - template void find_initial_cliques( \ - dual_simplex::user_problem_t & problem, \ - typename mip_solver_settings_t::tolerances_t tolerances); +#define INSTANTIATE(F_TYPE) \ + template void find_initial_cliques( \ + dual_simplex::user_problem_t & problem, \ + typename mip_solver_settings_t::tolerances_t tolerances, \ + std::shared_ptr> * clique_table_out); \ + template void build_clique_table( \ + const dual_simplex::user_problem_t& problem, \ + clique_table_t& clique_table, \ + typename mip_solver_settings_t::tolerances_t tolerances, \ + bool remove_small_cliques_flag, \ + bool fill_var_clique_maps_flag); \ + template class clique_table_t; #if MIP_INSTANTIATE_FLOAT INSTANTIATE(float) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index cead4c5e8b..db3db3c84c 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -95,7 +96,15 @@ struct clique_table_t { template void find_initial_cliques(dual_simplex::user_problem_t& problem, - typename mip_solver_settings_t::tolerances_t tolerances); + typename mip_solver_settings_t::tolerances_t tolerances, + std::shared_ptr>* clique_table_out = nullptr); + +template +void build_clique_table(const dual_simplex::user_problem_t& problem, + clique_table_t& clique_table, + typename mip_solver_settings_t::tolerances_t tolerances, + bool remove_small_cliques, + bool fill_var_clique_maps); } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 56124f5f9b..6147ff598c 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -139,6 +139,7 @@ problem_t::problem_t( objective_offset(problem_.get_objective_offset()), lp_state(*this, problem_.get_handle_ptr()->get_stream()), fixing_helpers(n_constraints, n_variables, handle_ptr), + clique_table(nullptr), Q_offsets(problem_.get_quadratic_objective_offsets()), Q_indices(problem_.get_quadratic_objective_indices()), Q_values(problem_.get_quadratic_objective_values()) @@ -194,6 +195,7 @@ problem_t::problem_t(const problem_t& problem_) objective_is_integral(problem_.objective_is_integral), lp_state(problem_.lp_state), fixing_helpers(problem_.fixing_helpers, handle_ptr), + clique_table(problem_.clique_table), vars_with_objective_coeffs(problem_.vars_with_objective_coeffs), expensive_to_fix_vars(problem_.expensive_to_fix_vars), Q_offsets(problem_.Q_offsets), @@ -249,6 +251,7 @@ problem_t::problem_t(const problem_t& problem_, objective_is_integral(problem_.objective_is_integral), lp_state(problem_.lp_state, handle_ptr), fixing_helpers(problem_.fixing_helpers, handle_ptr), + clique_table(problem_.clique_table), vars_with_objective_coeffs(problem_.vars_with_objective_coeffs), expensive_to_fix_vars(problem_.expensive_to_fix_vars), Q_offsets(problem_.Q_offsets), @@ -272,6 +275,7 @@ problem_t::problem_t(const problem_t& problem_, bool no_deep maximize(problem_.maximize), empty(problem_.empty), is_binary_pb(problem_.is_binary_pb), + clique_table(problem_.clique_table), // Copy constructor used by PDLP and MIP // PDLP uses the version with no_deep_copy = false which deep copy some fields but doesn't // allocate others that are not needed in PDLP diff --git a/cpp/src/mip/problem/problem.cuh b/cpp/src/mip/problem/problem.cuh index 6a9a0cabc7..7838d27418 100644 --- a/cpp/src/mip/problem/problem.cuh +++ b/cpp/src/mip/problem/problem.cuh @@ -24,6 +24,7 @@ #include +#include #include #include #include @@ -37,6 +38,9 @@ namespace cuopt { namespace linear_programming::detail { +template +struct clique_table_t; + template class solution_t; @@ -116,6 +120,8 @@ class problem_t { bool is_integer(f_t val) const; bool integer_equal(f_t val1, f_t val2) const; + std::shared_ptr> clique_table; + void get_host_user_problem( cuopt::linear_programming::dual_simplex::user_problem_t& user_problem) const; void set_constraints_from_host_user_problem( diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 9ee22c47fe..0ad757a7c6 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -187,6 +187,7 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.mixed_integer_gomory_cuts = context.settings.mixed_integer_gomory_cuts; branch_and_bound_settings.knapsack_cuts = context.settings.knapsack_cuts; + branch_and_bound_settings.clique_cuts = context.settings.clique_cuts; branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; branch_and_bound_settings.reduced_cost_strengthening = @@ -232,7 +233,10 @@ solution_t mip_solver_t::run_solver() // Create the branch and bound object branch_and_bound = std::make_unique>( - branch_and_bound_problem, branch_and_bound_settings, timer_.get_tic_start()); + branch_and_bound_problem, + branch_and_bound_settings, + timer_.get_tic_start(), + context.problem_ptr->clique_table); context.branch_and_bound_ptr = branch_and_bound.get(); branch_and_bound->set_concurrent_lp_root_solve(true); auto* stats_ptr = &context.stats; From cb1099291cfbce91d8a2e8f6accee2436cd2d381 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Feb 2026 02:32:58 -0800 Subject: [PATCH 029/147] add timer to cliques --- cpp/src/mip/diversity/diversity_manager.cu | 2 +- .../presolve/conflict_graph/clique_table.cu | 58 ++++++++++++------- .../presolve/conflict_graph/clique_table.cuh | 5 +- cpp/src/mip/solver.cu | 7 ++- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index d9f2195c4f..ca756ebccb 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -205,7 +205,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit) if (!context.settings.heuristics_only && !problem_ptr->empty) { dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); - find_initial_cliques(host_problem, context.settings.tolerances); + find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); problem_ptr->set_constraints_from_host_user_problem(host_problem); trivial_presolve(*problem_ptr, remap_cache_ids); } diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 0d536e6949..e2c12b31ce 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -458,11 +458,13 @@ template i_t extend_cliques(const std::vector>& knapsack_constraints, clique_table_t& clique_table, dual_simplex::user_problem_t& problem, - dual_simplex::csr_matrix_t& A) + dual_simplex::csr_matrix_t& A, + cuopt::timer_t& timer) { i_t n_extended_cliques = 0; // we try extending cliques on set packing constraints for (const auto& knapsack_constraint : knapsack_constraints) { + if (timer.check_time_limit()) { break; } if (!knapsack_constraint.is_set_packing) { continue; } if (knapsack_constraint.entries.size() < (size_t)clique_table.max_clique_size_for_extension) { std::vector clique; @@ -512,14 +514,17 @@ void remove_dominated_cliques( clique_table_t& clique_table, std::unordered_set& set_packing_constraints, const std::vector>& knapsack_constraints, - i_t n_extended_cliques) + i_t n_extended_cliques, + cuopt::timer_t& timer) { + if (timer.check_time_limit()) { goto finalize_problem; } // TODO check if we need to add the dominance for the table itself i_t extended_clique_start_idx = clique_table.first.size() - n_extended_cliques; CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); std::vector removal_marker(problem.row_sense.size(), 0); std::vector> cstr_vars(knapsack_constraints.size()); for (const auto knapsack_idx : set_packing_constraints) { + if (timer.check_time_limit()) { goto finalize_problem; } cuopt_assert(knapsack_constraints[knapsack_idx].is_set_packing, "Set packing constraint is not a set packing constraint"); const auto& vars = knapsack_constraints[knapsack_idx].entries; @@ -530,7 +535,7 @@ void remove_dominated_cliques( std::sort(cstr_vars[knapsack_idx].begin(), cstr_vars[knapsack_idx].end()); } CUOPT_LOG_DEBUG("Constraint variable lists built: %zu", set_packing_constraints.size()); - constexpr size_t dominance_window = 1000; + constexpr size_t dominance_window = 100; struct clique_sig_t { i_t knapsack_idx; i_t row_idx; @@ -541,6 +546,7 @@ void remove_dominated_cliques( sp_sigs.reserve(set_packing_constraints.size()); CUOPT_LOG_DEBUG("Building set packing signatures"); for (const auto knapsack_idx : set_packing_constraints) { + if (timer.check_time_limit()) { goto finalize_problem; } const auto& vars = cstr_vars[knapsack_idx]; if (vars.empty()) { continue; } long long signature = 0; @@ -600,6 +606,8 @@ void remove_dominated_cliques( }; CUOPT_LOG_DEBUG("Scanning extended cliques for dominance"); for (i_t i = 0; i < n_extended_cliques; i++) { + // Break here so that the discovered dominance is applied + if (timer.check_time_limit()) { goto finalize_problem; } i_t clique_idx = extended_clique_start_idx + i; const auto& curr_clique = clique_table.first[clique_idx]; if (curr_clique.empty()) { continue; } @@ -638,6 +646,7 @@ void remove_dominated_cliques( } } CUOPT_LOG_DEBUG("Dominance scan complete"); +finalize_problem: // TODO if more row removal is needed somewher else(e.g another presolve), standardize this dual_simplex::csr_matrix_t A_removed(0, 0, 0); CUOPT_LOG_DEBUG("Removing dominated rows"); @@ -738,9 +747,10 @@ void print_clique_table(const clique_table_t& clique_table) template void find_initial_cliques(dual_simplex::user_problem_t& problem, - typename mip_solver_settings_t::tolerances_t tolerances) + typename mip_solver_settings_t::tolerances_t tolerances, + cuopt::timer_t& timer) { - cuopt::timer_t timer(std::numeric_limits::infinity()); + cuopt::timer_t stage_timer(std::numeric_limits::infinity()); double t_fill = 0.; double t_coeff = 0.; double t_sort = 0.; @@ -754,12 +764,12 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); problem.A.to_compressed_row(A); fill_knapsack_constraints(problem, knapsack_constraints, A); - t_fill = timer.elapsed_time(); + t_fill = stage_timer.elapsed_time(); make_coeff_positive_knapsack_constraint( problem, knapsack_constraints, set_packing_constraints, tolerances); - t_coeff = timer.elapsed_time(); + t_coeff = stage_timer.elapsed_time(); sort_csr_by_constraint_coefficients(knapsack_constraints); - t_sort = timer.elapsed_time(); + t_sort = stage_timer.elapsed_time(); // print_knapsack_constraints(knapsack_constraints); // TODO think about getting min_clique_size according to some problem property clique_config_t clique_config; @@ -768,24 +778,31 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, clique_config.max_clique_size_for_extension); clique_table.tolerances = tolerances; for (const auto& knapsack_constraint : knapsack_constraints) { + if (timer.check_time_limit()) { break; } find_cliques_from_constraint(knapsack_constraint, clique_table); } - t_find = timer.elapsed_time(); + if (timer.check_time_limit()) { return; } + t_find = stage_timer.elapsed_time(); CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d", clique_table.first.size(), clique_table.addtl_cliques.size()); // print_clique_table(clique_table); // remove small cliques and add them to adj_list remove_small_cliques(clique_table); - t_small = timer.elapsed_time(); + t_small = stage_timer.elapsed_time(); // fill var clique maps fill_var_clique_maps(clique_table); - t_maps = timer.elapsed_time(); - i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A); - t_extend = timer.elapsed_time(); - remove_dominated_cliques( - problem, A, clique_table, set_packing_constraints, knapsack_constraints, n_extended_cliques); - t_remove = timer.elapsed_time(); + t_maps = stage_timer.elapsed_time(); + i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A, timer); + t_extend = stage_timer.elapsed_time(); + remove_dominated_cliques(problem, + A, + clique_table, + set_packing_constraints, + knapsack_constraints, + n_extended_cliques, + timer); + t_remove = stage_timer.elapsed_time(); CUOPT_LOG_DEBUG( "Clique table timing (s): fill=%.6f coeff=%.6f sort=%.6f find=%.6f small=%.6f maps=%.6f " "extend=%.6f remove=%.6f total=%.6f", @@ -801,10 +818,11 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, // exit(0); } -#define INSTANTIATE(F_TYPE) \ - template void find_initial_cliques( \ - dual_simplex::user_problem_t & problem, \ - typename mip_solver_settings_t::tolerances_t tolerances); +#define INSTANTIATE(F_TYPE) \ + template void find_initial_cliques( \ + dual_simplex::user_problem_t & problem, \ + typename mip_solver_settings_t::tolerances_t tolerances, \ + cuopt::timer_t & timer); #if MIP_INSTANTIATE_FLOAT INSTANTIATE(float) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index cead4c5e8b..3beb1b880c 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -20,6 +20,8 @@ #include #include +#include + #include #include #include @@ -95,7 +97,8 @@ struct clique_table_t { template void find_initial_cliques(dual_simplex::user_problem_t& problem, - typename mip_solver_settings_t::tolerances_t tolerances); + typename mip_solver_settings_t::tolerances_t tolerances, + cuopt::timer_t& timer); } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 9ee22c47fe..555da85047 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -106,9 +106,10 @@ solution_t mip_solver_t::run_solver() context.problem_ptr->post_process_solution(sol); return sol; } - dm.timer = timer_; - const bool run_presolve = context.settings.presolve; - bool presolve_success = run_presolve ? dm.run_presolve(timer_.remaining_time()) : true; + dm.timer = timer_; + const bool run_presolve = context.settings.presolve; + const double presolve_time_limit = std::min(0.1 * timer_.remaining_time(), 60.0); + bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); solution_t sol(*context.problem_ptr); From 78aa3f153b1b810faf200f5d869de785f35856b8 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Feb 2026 06:17:24 -0800 Subject: [PATCH 030/147] add comments fix redundant code --- cpp/src/dual_simplex/cuts.cpp | 98 +++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/cpp/src/dual_simplex/cuts.cpp b/cpp/src/dual_simplex/cuts.cpp index 7ace8048d7..103a6d463f 100644 --- a/cpp/src/dual_simplex/cuts.cpp +++ b/cpp/src/dual_simplex/cuts.cpp @@ -37,20 +37,17 @@ bool build_clique_cut(const std::vector& clique_vertices, cuopt_assert(num_vars > 0, "Clique cut num_vars must be positive"); cuopt_assert(static_cast(num_vars) <= lower_bounds.size(), "Clique cut lower bounds size mismatch"); - cuopt_assert(static_cast(num_vars) <= upper_bounds.size(), - "Clique cut upper bounds size mismatch"); - cuopt_assert(static_cast(num_vars) <= var_types.size(), - "Clique cut var_types size mismatch"); cuopt_assert(static_cast(num_vars) <= xstar.size(), "Clique cut xstar size mismatch"); cut.i.clear(); cut.x.clear(); i_t num_complements = 0; +#ifdef ASSERT_MODE std::unordered_set seen_original; std::unordered_set seen_complement; seen_original.reserve(clique_vertices.size()); seen_complement.reserve(clique_vertices.size()); - +#endif for (const auto vertex_idx : clique_vertices) { cuopt_assert(vertex_idx >= 0 && vertex_idx < 2 * num_vars, "Clique vertex out of range"); const i_t var_idx = vertex_idx % num_vars; @@ -63,6 +60,8 @@ bool build_clique_cut(const std::vector& clique_vertices, cuopt_assert(lower_bound >= -bound_tol, "Clique variable lower bound below zero"); cuopt_assert(upper_bound <= 1 + bound_tol, "Clique variable upper bound above one"); + // we store the cut in the form of >= 1, for easy violation check with dot product + // that's why compelements have 1 as coeff and normal vars have -1 if (complement) { cuopt_assert(seen_original.count(var_idx) == 0, "Clique contains a variable and its complement"); @@ -137,35 +136,50 @@ f_t sum_weights_bitset(const std::vector& bs, const std::vector& } template -void bron_kerbosch_bitset(bk_bitset_context_t& ctx, - std::vector& R, - std::vector& P, - std::vector& X, - f_t weight_R) +void bron_kerbosch(bk_bitset_context_t& ctx, + std::vector& R, // current clique + std::vector& P, // potential candidates + std::vector& X, // already in the clique + f_t weight_R) { ctx.num_calls++; + // stop the recursion, for perf reasons if (ctx.num_calls > ctx.max_calls) { return; } + // if P and X are empty, we are at maximal clique if (!bitset_any(P) && !bitset_any(X)) { + // if the weight is enough, add and exit if (weight_R >= ctx.min_weight) { ctx.cliques.push_back(R); } return; } const f_t sumP = sum_weights_bitset(P, ctx.weights); + // check if all P is added to clique, would we exceed the weight? if (weight_R + sumP < ctx.min_weight) { return; } i_t pivot = -1; i_t max_deg = -1; + // pivoting rule according to the highest degree vertex + // TODO try other pivoting strategies, we can also implement some online learning like MAB for (size_t w = 0; w < ctx.words; ++w) { + // union of P and X uint64_t word = P[w] | X[w]; while (word) { + // least significant set bit idnex const int bit = __builtin_ctzll(word); - const i_t v = static_cast(w * 64 + static_cast(bit)); + // overall vertex index + const i_t v = static_cast(w * 64 + static_cast(bit)); + // clear the least significant set bit (v) word &= (word - 1); i_t count = 0; + // count the number of neighbors of v in P for (size_t k = 0; k < ctx.words; ++k) { count += __builtin_popcountll(P[k] & ctx.adj[v][k]); } + // chose the highest degree v as the pivot + // we choose the highest degree as the pivot to reduce the recursion size + // later in this function we recurse on the candidate P / N(v) + // so it is good to maximize P n N(v) if (count > max_deg) { max_deg = count; pivot = v; @@ -175,28 +189,19 @@ void bron_kerbosch_bitset(bk_bitset_context_t& ctx, std::vector candidates; candidates.reserve(ctx.weights.size()); - if (pivot >= 0) { - for (size_t w = 0; w < ctx.words; ++w) { - uint64_t word = P[w] & ~ctx.adj[pivot][w]; - while (word) { - const int bit = __builtin_ctzll(word); - const i_t v = static_cast(w * 64 + static_cast(bit)); - word &= (word - 1); - candidates.push_back(v); - } - } - } else { - for (size_t w = 0; w < ctx.words; ++w) { - uint64_t word = P[w]; - while (word) { - const int bit = __builtin_ctzll(word); - const i_t v = static_cast(w * 64 + static_cast(bit)); - word &= (word - 1); - candidates.push_back(v); - } + cuopt_assert(pivot >= 0, "Pivot must be valid when P or X is non-empty"); + for (size_t w = 0; w < ctx.words; ++w) { + // P / N(pivot) + uint64_t word = P[w] & ~ctx.adj[pivot][w]; + while (word) { + const int bit = __builtin_ctzll(word); + const i_t v = static_cast(w * 64 + static_cast(bit)); + word &= (word - 1); + candidates.push_back(v); } } + // note that candidates will include pivot if it is in P for (auto v : candidates) { R.push_back(v); std::vector P_next(ctx.words, 0); @@ -205,7 +210,7 @@ void bron_kerbosch_bitset(bk_bitset_context_t& ctx, P_next[k] = P[k] & ctx.adj[v][k]; X_next[k] = X[k] & ctx.adj[v][k]; } - bron_kerbosch_bitset(ctx, R, P_next, X_next, weight_R + ctx.weights[v]); + bron_kerbosch(ctx, R, P_next, X_next, weight_R + ctx.weights[v]); R.pop_back(); bitset_clear(P, static_cast(v)); bitset_set(X, static_cast(v)); @@ -236,6 +241,7 @@ void extend_clique_vertices(std::vector& clique_vertices, std::unordered_set clique_members(clique_vertices.begin(), clique_vertices.end()); std::vector candidates; candidates.reserve(adj_set.size()); + // the candidate list if only the integer valued vertices for (const auto& candidate : adj_set) { if (clique_members.count(candidate) != 0) { continue; } i_t var_idx = candidate % num_vars; @@ -243,6 +249,11 @@ void extend_clique_vertices(std::vector& clique_vertices, if (std::abs(value - std::round(value)) <= integer_tol) { candidates.push_back(candidate); } } + // sort the candidates by reduced cost. + // smaller reduce cost disturbs dual simplex less + // less refactors and less iterations after resolve. + // it also increases the cut's effectiveness by keeping xstart not disturbed much + // if it is disturbed too much, the cut might become non-binding auto reduced_cost = [&](i_t vertex_idx) -> f_t { i_t var_idx = vertex_idx % num_vars; if (var_idx < 0 || var_idx >= static_cast(reduced_costs.size())) { return 0.0; } @@ -895,7 +906,6 @@ void cut_generation_t::generate_clique_cuts( if (settings.clique_cuts == 0) { return; } const i_t num_vars = user_problem_.num_cols; - if (num_vars <= 0) { return; } if (clique_table_ == nullptr) { ::cuopt::linear_programming::detail::clique_config_t clique_config; @@ -904,8 +914,8 @@ void cut_generation_t::generate_clique_cuts( 2 * num_vars, clique_config.min_clique_size, clique_config.max_clique_size_for_extension); typename ::cuopt::linear_programming::mip_solver_settings_t::tolerances_t tolerances; - tolerances.presolve_absolute_tolerance = settings.integer_tol; - tolerances.absolute_tolerance = settings.integer_tol; + tolerances.presolve_absolute_tolerance = settings.primal_tol; + tolerances.absolute_tolerance = settings.primal_tol; tolerances.relative_tolerance = settings.zero_tol; tolerances.integrality_tolerance = settings.integer_tol; tolerances.absolute_mip_gap = settings.absolute_mip_gap_tol; @@ -923,7 +933,8 @@ void cut_generation_t::generate_clique_cuts( const f_t min_violation = std::max(settings.primal_tol, static_cast(1e-6)); const f_t bound_tol = settings.primal_tol; const f_t min_weight = 1.0 + min_violation; - const i_t max_calls = 100000; + // TODO this can be problem dependent + const i_t max_calls = 100000; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); @@ -933,6 +944,7 @@ void cut_generation_t::generate_clique_cuts( vertices.reserve(num_vars * 2); weights.reserve(num_vars * 2); + // create the sub graph induced by fractional binary variables for (i_t j = 0; j < num_vars; ++j) { if (user_problem_.var_types[j] == variable_type_t::CONTINUOUS) { continue; } const f_t lower_bound = user_problem_.lower[j]; @@ -962,15 +974,11 @@ void cut_generation_t::generate_clique_cuts( auto& adj = adj_local[idx]; adj.reserve(adj_set.size() + 1); for (const auto neighbor : adj_set) { - if (neighbor >= 0 && neighbor < 2 * num_vars && in_subgraph[neighbor]) { - i_t local_neighbor = vertex_to_local[neighbor]; - if (local_neighbor >= 0) { adj.push_back(local_neighbor); } - } - } - i_t complement = vertex_idx < num_vars ? vertex_idx + num_vars : vertex_idx - num_vars; - if (in_subgraph[complement]) { - i_t local_neighbor = vertex_to_local[complement]; - if (local_neighbor >= 0) { adj.push_back(local_neighbor); } + cuopt_assert(neighbor >= 0 && neighbor < 2 * num_vars, "Neighbor out of range"); + if (!in_subgraph[neighbor]) { continue; } + i_t local_neighbor = vertex_to_local[neighbor]; + cuopt_assert(local_neighbor >= 0, "Local neighbor out of range"); + adj.push_back(local_neighbor); } std::sort(adj.begin(), adj.end()); adj.erase(std::unique(adj.begin(), adj.end()), adj.end()); @@ -993,7 +1001,7 @@ void cut_generation_t::generate_clique_cuts( for (size_t idx = 0; idx < vertices.size(); ++idx) { bitset_set(P, idx); } - bron_kerbosch_bitset(ctx, R, P, X, 0.0); + bron_kerbosch(ctx, R, P, X, 0.0); sparse_vector_t cut(lp.num_cols, 0); f_t cut_rhs = 0.0; From be32e734ec833a2e0966fa2775f964550508f9f6 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Feb 2026 06:20:51 -0800 Subject: [PATCH 031/147] convert unique check to assert --- cpp/src/dual_simplex/cuts.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/cuts.cpp b/cpp/src/dual_simplex/cuts.cpp index 103a6d463f..9c16056f3c 100644 --- a/cpp/src/dual_simplex/cuts.cpp +++ b/cpp/src/dual_simplex/cuts.cpp @@ -980,8 +980,15 @@ void cut_generation_t::generate_clique_cuts( cuopt_assert(local_neighbor >= 0, "Local neighbor out of range"); adj.push_back(local_neighbor); } - std::sort(adj.begin(), adj.end()); - adj.erase(std::unique(adj.begin(), adj.end()), adj.end()); +#ifdef ASSERT_MODE + { + std::unordered_set adj_check; + adj_check.reserve(adj.size()); + for (const auto neighbor : adj) { + cuopt_assert(adj_check.insert(neighbor).second, "Duplicate neighbor in adjacency list"); + } + } +#endif } const size_t words = bitset_words(vertices.size()); From 933beb0814725b011d9c16d5cbc81f1f255af200 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Feb 2026 06:22:40 -0800 Subject: [PATCH 032/147] add complement of the var to the adj list --- cpp/src/mip/presolve/conflict_graph/clique_table.cu | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index e2c12b31ce..398ee0df05 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -289,6 +289,9 @@ std::unordered_set clique_table_t::get_adj_set_of_var(i_t var_idx for (const auto& adj_vertex : adj_list_small_cliques[var_idx]) { adj_set.insert(adj_vertex); } + // Add the complement of var_idx to the adjacency set + i_t complement_idx = (var_idx >= n_variables) ? (var_idx - n_variables) : (var_idx + n_variables); + adj_set.insert(complement_idx); adj_set.erase(var_idx); return adj_set; } From c196ee2d4e5b2cdccae20e75492d1332be58f5ee Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Feb 2026 07:32:18 -0800 Subject: [PATCH 033/147] adjust timer --- .../presolve/conflict_graph/clique_table.cu | 227 +++++++++--------- 1 file changed, 119 insertions(+), 108 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 398ee0df05..d2e7b6edbc 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -520,14 +520,17 @@ void remove_dominated_cliques( i_t n_extended_cliques, cuopt::timer_t& timer) { - if (timer.check_time_limit()) { goto finalize_problem; } // TODO check if we need to add the dominance for the table itself i_t extended_clique_start_idx = clique_table.first.size() - n_extended_cliques; CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); std::vector removal_marker(problem.row_sense.size(), 0); std::vector> cstr_vars(knapsack_constraints.size()); + bool time_limit_reached = timer.check_time_limit(); for (const auto knapsack_idx : set_packing_constraints) { - if (timer.check_time_limit()) { goto finalize_problem; } + if (timer.check_time_limit()) { + time_limit_reached = true; + break; + } cuopt_assert(knapsack_constraints[knapsack_idx].is_set_packing, "Set packing constraint is not a set packing constraint"); const auto& vars = knapsack_constraints[knapsack_idx].entries; @@ -537,119 +540,127 @@ void remove_dominated_cliques( } std::sort(cstr_vars[knapsack_idx].begin(), cstr_vars[knapsack_idx].end()); } - CUOPT_LOG_DEBUG("Constraint variable lists built: %zu", set_packing_constraints.size()); - constexpr size_t dominance_window = 100; - struct clique_sig_t { - i_t knapsack_idx; - i_t row_idx; - i_t size; - long long signature; - }; - std::vector sp_sigs; - sp_sigs.reserve(set_packing_constraints.size()); - CUOPT_LOG_DEBUG("Building set packing signatures"); - for (const auto knapsack_idx : set_packing_constraints) { - if (timer.check_time_limit()) { goto finalize_problem; } - const auto& vars = cstr_vars[knapsack_idx]; - if (vars.empty()) { continue; } - long long signature = 0; - for (auto v : vars) { - signature += static_cast(v); - } - sp_sigs.push_back({knapsack_idx, - knapsack_constraints[knapsack_idx].cstr_idx, - static_cast(vars.size()), - signature}); - } - CUOPT_LOG_DEBUG("Sorting signatures: %zu", sp_sigs.size()); - std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { - if (a.signature != b.signature) { return a.signature < b.signature; } - return a.size < b.size; - }); - auto is_subset = [](const std::vector& a, const std::vector& b) { - size_t i = 0; - size_t j = 0; - while (i < a.size() && j < b.size()) { - if (a[i] == b[j]) { - i++; - j++; - } else if (a[i] > b[j]) { - j++; - } else { - return false; + if (!time_limit_reached) { + CUOPT_LOG_DEBUG("Constraint variable lists built: %zu", set_packing_constraints.size()); + constexpr size_t dominance_window = 100; + struct clique_sig_t { + i_t knapsack_idx; + i_t row_idx; + i_t size; + long long signature; + }; + std::vector sp_sigs; + sp_sigs.reserve(set_packing_constraints.size()); + CUOPT_LOG_DEBUG("Building set packing signatures"); + for (const auto knapsack_idx : set_packing_constraints) { + if (timer.check_time_limit()) { + time_limit_reached = true; + break; } - } - return i == a.size(); - }; - auto fix_difference = [&](const std::vector& superset, const std::vector& subset) { - for (auto var_idx : superset) { - if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } - if (var_idx >= problem.num_cols) { - i_t orig_idx = var_idx - problem.num_cols; - CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); - cuopt_assert(problem.lower[orig_idx] != 0 || problem.upper[orig_idx] != 0, - "Variable is fixed to other side"); - problem.lower[orig_idx] = 1; - problem.upper[orig_idx] = 1; - } else { - CUOPT_LOG_DEBUG("Fixing variable %d", var_idx); - cuopt_assert(problem.lower[var_idx] != 1 || problem.upper[var_idx] != 1, - "Variable is fixed to other side"); - problem.lower[var_idx] = 0; - problem.upper[var_idx] = 0; + const auto& vars = cstr_vars[knapsack_idx]; + if (vars.empty()) { continue; } + long long signature = 0; + for (auto v : vars) { + signature += static_cast(v); } + sp_sigs.push_back({knapsack_idx, + knapsack_constraints[knapsack_idx].cstr_idx, + static_cast(vars.size()), + signature}); } - }; - auto find_window_start = [&](long long signature) { - auto it = std::lower_bound( - sp_sigs.begin(), sp_sigs.end(), signature, [](const auto& a, long long value) { - return a.signature < value; - }); - return static_cast(std::distance(sp_sigs.begin(), it)); - }; - CUOPT_LOG_DEBUG("Scanning extended cliques for dominance"); - for (i_t i = 0; i < n_extended_cliques; i++) { - // Break here so that the discovered dominance is applied - if (timer.check_time_limit()) { goto finalize_problem; } - i_t clique_idx = extended_clique_start_idx + i; - const auto& curr_clique = clique_table.first[clique_idx]; - if (curr_clique.empty()) { continue; } - std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); - std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); - cuopt_assert( - std::unique(curr_clique_vars.begin(), curr_clique_vars.end()) == curr_clique_vars.end(), - "Clique variables are not unique"); - long long signature = 0; - for (auto v : curr_clique_vars) { - signature += static_cast(v); - } - size_t start = find_window_start(signature); - size_t end = std::min(sp_sigs.size(), start + dominance_window); - for (size_t idx = start; idx < end; idx++) { - const auto& sp = sp_sigs[idx]; - if (sp.row_idx >= 0 && sp.row_idx < static_cast(removal_marker.size()) && - removal_marker[sp.row_idx]) { - continue; + CUOPT_LOG_DEBUG("Sorting signatures: %zu", sp_sigs.size()); + std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { + if (a.signature != b.signature) { return a.signature < b.signature; } + return a.size < b.size; + }); + auto is_subset = [](const std::vector& a, const std::vector& b) { + size_t i = 0; + size_t j = 0; + while (i < a.size() && j < b.size()) { + if (a[i] == b[j]) { + i++; + j++; + } else if (a[i] > b[j]) { + j++; + } else { + return false; + } } - const auto& vars_sp = cstr_vars[sp.knapsack_idx]; - if (vars_sp.size() > curr_clique_vars.size()) { continue; } - if (!is_subset(vars_sp, curr_clique_vars)) { continue; } - if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { - CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", - clique_idx, - sp.row_idx); - // note that we never deleter set partitioning constraints but it fixes some other variables - fix_difference(curr_clique_vars, vars_sp); - } else { - if (sp.row_idx >= 0 && sp.row_idx < A.m) { removal_marker[sp.row_idx] = true; } + return i == a.size(); + }; + auto fix_difference = [&](const std::vector& superset, const std::vector& subset) { + for (auto var_idx : superset) { + if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } + if (var_idx >= problem.num_cols) { + i_t orig_idx = var_idx - problem.num_cols; + CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); + cuopt_assert(problem.lower[orig_idx] != 0 || problem.upper[orig_idx] != 0, + "Variable is fixed to other side"); + problem.lower[orig_idx] = 1; + problem.upper[orig_idx] = 1; + } else { + CUOPT_LOG_DEBUG("Fixing variable %d", var_idx); + cuopt_assert(problem.lower[var_idx] != 1 || problem.upper[var_idx] != 1, + "Variable is fixed to other side"); + problem.lower[var_idx] = 0; + problem.upper[var_idx] = 0; + } + } + }; + auto find_window_start = [&](long long signature) { + auto it = std::lower_bound( + sp_sigs.begin(), sp_sigs.end(), signature, [](const auto& a, long long value) { + return a.signature < value; + }); + return static_cast(std::distance(sp_sigs.begin(), it)); + }; + CUOPT_LOG_DEBUG("Scanning extended cliques for dominance"); + for (i_t i = 0; i < n_extended_cliques; i++) { + // Break here so that the discovered dominance is applied + if (timer.check_time_limit()) { + time_limit_reached = true; + break; + } + i_t clique_idx = extended_clique_start_idx + i; + const auto& curr_clique = clique_table.first[clique_idx]; + if (curr_clique.empty()) { continue; } + std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); + std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); + cuopt_assert( + std::unique(curr_clique_vars.begin(), curr_clique_vars.end()) == curr_clique_vars.end(), + "Clique variables are not unique"); + long long signature = 0; + for (auto v : curr_clique_vars) { + signature += static_cast(v); + } + size_t start = find_window_start(signature); + size_t end = std::min(sp_sigs.size(), start + dominance_window); + for (size_t idx = start; idx < end; idx++) { + const auto& sp = sp_sigs[idx]; + if (sp.row_idx >= 0 && sp.row_idx < static_cast(removal_marker.size()) && + removal_marker[sp.row_idx]) { + continue; + } + const auto& vars_sp = cstr_vars[sp.knapsack_idx]; + if (vars_sp.size() > curr_clique_vars.size()) { continue; } + if (!is_subset(vars_sp, curr_clique_vars)) { continue; } + if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { + CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", + clique_idx, + sp.row_idx); + // note that we never deleter set partitioning constraints but it fixes some other + // variables + fix_difference(curr_clique_vars, vars_sp); + } else { + if (sp.row_idx >= 0 && sp.row_idx < A.m) { removal_marker[sp.row_idx] = true; } + } + } + if ((i % 128) == 0) { + CUOPT_LOG_TRACE("Processed extended clique %d/%d", i + 1, n_extended_cliques); } } - if ((i % 128) == 0) { - CUOPT_LOG_TRACE("Processed extended clique %d/%d", i + 1, n_extended_cliques); - } + CUOPT_LOG_DEBUG("Dominance scan complete"); } - CUOPT_LOG_DEBUG("Dominance scan complete"); -finalize_problem: // TODO if more row removal is needed somewher else(e.g another presolve), standardize this dual_simplex::csr_matrix_t A_removed(0, 0, 0); CUOPT_LOG_DEBUG("Removing dominated rows"); From 524207ada8f4da0abf0b06f7a9d78697d34acd8e Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Feb 2026 00:41:27 -0800 Subject: [PATCH 034/147] fix the bug of fixing more vars --- .../presolve/conflict_graph/clique_table.cu | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index d2e7b6edbc..0dd24cc7fb 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -401,12 +401,14 @@ bool extend_clique(const std::vector& clique, i_t n_of_complement_conflicts = 0; i_t complement_conflict_var = -1; for (size_t idx = 0; idx < extension_candidates.size(); idx++) { - i_t var_idx = extension_candidates[idx]; - bool add = true; + i_t var_idx = extension_candidates[idx]; + bool add = true; + bool complement_conflict = false; + i_t complement_conflict_idx = -1; for (size_t i = 0; i < new_clique.size(); i++) { if (var_idx % clique_table.n_variables == new_clique[i] % clique_table.n_variables) { - n_of_complement_conflicts++; - complement_conflict_var = var_idx % clique_table.n_variables; + complement_conflict = true; + complement_conflict_idx = var_idx % clique_table.n_variables; } // check if the tested variable conflicts with all vars in the new clique if (!clique_table.check_adjacency(var_idx, new_clique[i])) { @@ -414,7 +416,13 @@ bool extend_clique(const std::vector& clique, break; } } - if (add) { new_clique.push_back(var_idx); } + if (add) { + new_clique.push_back(var_idx); + if (complement_conflict) { + n_of_complement_conflicts++; + complement_conflict_var = complement_conflict_idx; + } + } } // if we found a larger cliqe, insert it into the formulation if (new_clique.size() > clique.size()) { From bc345bbfed5ceb4529a3ea8f82c946f60e57600d Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Feb 2026 05:49:20 -0800 Subject: [PATCH 035/147] fix issues on clique table --- .../presolve/conflict_graph/clique_table.cu | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 0dd24cc7fb..912d61f6c5 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -171,15 +171,17 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro } // equality part else { - bool is_set_partitioning = problem.rhs[i] == 1.; + // For equality rows, partitioning status should not depend on raw rhs scale here. + // The exact set-packing/partitioning check is finalized later in + // make_coeff_positive_knapsack_constraint after coefficient normalization. + bool is_set_partitioning = true; bool ranged_constraint = ranged_constraint_counter < problem.num_range_rows && problem.range_rows[ranged_constraint_counter] == i; // less than part knapsack_constraint.rhs = problem.rhs[i]; if (ranged_constraint) { knapsack_constraint.rhs += problem.range_value[ranged_constraint_counter]; - is_set_partitioning = - problem.range_value[ranged_constraint_counter] == 0. && problem.rhs[i] == 1.; + is_set_partitioning = problem.range_value[ranged_constraint_counter] == 0.; ranged_constraint_counter++; } for (i_t j = constraint_range.first; j < constraint_range.second; j++) { @@ -361,7 +363,10 @@ void insert_clique_into_problem(const std::vector& clique, new_vars.push_back(var_idx); new_coeffs.push_back(coeff); } - f_t rhs = coeff_scale + rhs_offset; + // For complemented literals (1 - x), expansion contributes a constant term on the left: + // coeff_scale * (1 - x) = coeff_scale - coeff_scale * x + // Move constants to the right, so rhs must decrease by rhs_offset. + f_t rhs = coeff_scale - rhs_offset; // insert the new clique into the problem as a new constraint A.insert_row(new_vars, new_coeffs); problem.row_sense.push_back('L'); @@ -597,6 +602,8 @@ void remove_dominated_cliques( return i == a.size(); }; auto fix_difference = [&](const std::vector& superset, const std::vector& subset) { + cuopt_assert(std::is_sorted(subset.begin(), subset.end()), + "subset vector passed to fix_difference is not sorted"); for (auto var_idx : superset) { if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } if (var_idx >= problem.num_cols) { @@ -644,23 +651,33 @@ void remove_dominated_cliques( size_t start = find_window_start(signature); size_t end = std::min(sp_sigs.size(), start + dominance_window); for (size_t idx = start; idx < end; idx++) { - const auto& sp = sp_sigs[idx]; + const auto& sp = sp_sigs[idx]; + const auto& vars_sp = cstr_vars[sp.knapsack_idx]; + if (vars_sp.size() > curr_clique_vars.size()) { continue; } + cuopt_assert(std::is_sorted(vars_sp.begin(), vars_sp.end()), + "vars_sp vector passed to is_subset is not sorted"); + if (!is_subset(vars_sp, curr_clique_vars)) { continue; } + // If this is a real model row and it is already marked for removal, it must not drive + // additional fixings/removals. if (sp.row_idx >= 0 && sp.row_idx < static_cast(removal_marker.size()) && removal_marker[sp.row_idx]) { continue; } - const auto& vars_sp = cstr_vars[sp.knapsack_idx]; - if (vars_sp.size() > curr_clique_vars.size()) { continue; } - if (!is_subset(vars_sp, curr_clique_vars)) { continue; } if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", clique_idx, sp.row_idx); // note that we never deleter set partitioning constraints but it fixes some other // variables - fix_difference(curr_clique_vars, vars_sp); + if (vars_sp.size() != curr_clique_vars.size()) { + fix_difference(curr_clique_vars, vars_sp); + } } else { - if (sp.row_idx >= 0 && sp.row_idx < A.m) { removal_marker[sp.row_idx] = true; } + // knapsack cstr_idx may refer to virtual rows; only real model row indices can be + // removed from A. + if (sp.row_idx < 0 || sp.row_idx >= static_cast(removal_marker.size())) { continue; } + if (removal_marker[sp.row_idx]) { continue; } + removal_marker[sp.row_idx] = true; } } if ((i % 128) == 0) { From dd7ff4a3139d16342793d5691390a2884e74cd9b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Feb 2026 09:52:40 -0800 Subject: [PATCH 036/147] handle infeasibility in bb and cliques --- cpp/src/dual_simplex/branch_and_bound.cpp | 37 ++++++----- cpp/src/dual_simplex/cuts.cpp | 80 +++++++++++++---------- cpp/src/dual_simplex/cuts.hpp | 4 +- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 8e81c2d60a..6c97a79480 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1757,12 +1757,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); - // FIXME: rarely dual simplex detects infeasible whereas it is feasible. - // to add a small safety net, check if there is a primal solution already. - // Uncomment this if the issue with cost266-UUE is resolved - // if (settings.heuristic_preemption_callback != nullptr) { - // settings.heuristic_preemption_callback(); - // } + if (settings.heuristic_preemption_callback != nullptr) { + settings.heuristic_preemption_callback(); + } return mip_status_t::INFEASIBLE; } if (root_status == lp_status_t::UNBOUNDED) { @@ -1861,17 +1858,23 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #endif // Generate cuts and add them to the cut pool - f_t cut_start_time = tic(); - cut_generation.generate_cuts(original_lp_, - settings_, - Arow_, - new_slacks_, - var_types_, - basis_update, - root_relax_soln_.x, - root_relax_soln_.z, - basic_list, - nonbasic_list); + f_t cut_start_time = tic(); + bool problem_feasible = cut_generation.generate_cuts(original_lp_, + settings_, + Arow_, + new_slacks_, + var_types_, + basis_update, + root_relax_soln_.x, + root_relax_soln_.z, + basic_list, + nonbasic_list); + if (!problem_feasible) { + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); + } + return mip_status_t::INFEASIBLE; + } f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); diff --git a/cpp/src/dual_simplex/cuts.cpp b/cpp/src/dual_simplex/cuts.cpp index 9c16056f3c..7cc53ee15f 100644 --- a/cpp/src/dual_simplex/cuts.cpp +++ b/cpp/src/dual_simplex/cuts.cpp @@ -20,19 +20,21 @@ namespace cuopt::linear_programming::dual_simplex { namespace { +enum class clique_cut_build_status_t : int8_t { NO_CUT = 0, CUT_ADDED = 1, INFEASIBLE = 2 }; + template -bool build_clique_cut(const std::vector& clique_vertices, - i_t num_vars, - const std::vector& var_types, - const std::vector& lower_bounds, - const std::vector& upper_bounds, - const std::vector& xstar, - f_t bound_tol, - f_t min_violation, - sparse_vector_t& cut, - f_t& cut_rhs) +clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertices, + i_t num_vars, + const std::vector& var_types, + const std::vector& lower_bounds, + const std::vector& upper_bounds, + const std::vector& xstar, + f_t bound_tol, + f_t min_violation, + sparse_vector_t& cut, + f_t& cut_rhs) { - if (clique_vertices.size() < 2) { return false; } + if (clique_vertices.size() < 2) { return clique_cut_build_status_t::NO_CUT; } cuopt_assert(num_vars > 0, "Clique cut num_vars must be positive"); cuopt_assert(static_cast(num_vars) <= lower_bounds.size(), @@ -42,12 +44,10 @@ bool build_clique_cut(const std::vector& clique_vertices, cut.i.clear(); cut.x.clear(); i_t num_complements = 0; -#ifdef ASSERT_MODE std::unordered_set seen_original; std::unordered_set seen_complement; seen_original.reserve(clique_vertices.size()); seen_complement.reserve(clique_vertices.size()); -#endif for (const auto vertex_idx : clique_vertices) { cuopt_assert(vertex_idx >= 0 && vertex_idx < 2 * num_vars, "Clique vertex out of range"); const i_t var_idx = vertex_idx % num_vars; @@ -63,29 +63,28 @@ bool build_clique_cut(const std::vector& clique_vertices, // we store the cut in the form of >= 1, for easy violation check with dot product // that's why compelements have 1 as coeff and normal vars have -1 if (complement) { - cuopt_assert(seen_original.count(var_idx) == 0, - "Clique contains a variable and its complement"); + if (seen_original.count(var_idx) > 0) { return clique_cut_build_status_t::INFEASIBLE; } cuopt_assert(seen_complement.insert(var_idx).second, "Duplicate complement in clique"); num_complements++; cut.i.push_back(var_idx); cut.x.push_back(1.0); } else { - cuopt_assert(seen_complement.count(var_idx) == 0, - "Clique contains a variable and its complement"); + if (seen_complement.count(var_idx) > 0) { return clique_cut_build_status_t::INFEASIBLE; } cuopt_assert(seen_original.insert(var_idx).second, "Duplicate variable in clique"); cut.i.push_back(var_idx); cut.x.push_back(-1.0); } } - if (cut.i.empty()) { return false; } + if (cut.i.empty()) { return clique_cut_build_status_t::NO_CUT; } cut_rhs = static_cast(num_complements - 1); cut.sort(); const f_t dot = cut.dot(xstar); const f_t violation = cut_rhs - dot; - return violation > min_violation; + if (violation > min_violation) { return clique_cut_build_status_t::CUT_ADDED; } + return clique_cut_build_status_t::NO_CUT; } template @@ -822,7 +821,7 @@ f_t knapsack_generation_t::solve_knapsack_problem(const std::vector -void cut_generation_t::generate_cuts(const lp_problem_t& lp, +bool cut_generation_t::generate_cuts(const lp_problem_t& lp, const simplex_solver_settings_t& settings, csr_matrix_t& Arow, const std::vector& new_slacks, @@ -857,7 +856,11 @@ void cut_generation_t::generate_cuts(const lp_problem_t& lp, // Generate Clique cuts if (settings.clique_cuts != 0) { f_t cut_start_time = tic(); - generate_clique_cuts(lp, settings, var_types, xstar, reduced_costs); + bool feasible = generate_clique_cuts(lp, settings, var_types, xstar, reduced_costs); + if (!feasible) { + settings.log.printf("Clique cuts proved infeasible\n"); + return false; + } f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings.log.debug("Clique cut generation time %.2f seconds\n", cut_generation_time); @@ -873,6 +876,7 @@ void cut_generation_t::generate_cuts(const lp_problem_t& lp, settings.log.debug("MIR and CG cut generation time %.2f seconds\n", cut_generation_time); } } + return true; } template @@ -896,14 +900,14 @@ void cut_generation_t::generate_knapsack_cuts( } template -void cut_generation_t::generate_clique_cuts( +bool cut_generation_t::generate_clique_cuts( const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& var_types, const std::vector& xstar, const std::vector& reduced_costs) { - if (settings.clique_cuts == 0) { return; } + if (settings.clique_cuts == 0) { return true; } const i_t num_vars = user_problem_.num_cols; @@ -925,7 +929,7 @@ void cut_generation_t::generate_clique_cuts( user_problem_, *clique_table_, tolerances, true, true); } - if (clique_table_->first.empty() && clique_table_->addtl_cliques.empty()) { return; } + if (clique_table_->first.empty() && clique_table_->addtl_cliques.empty()) { return true; } cuopt_assert(clique_table_->n_variables == num_vars, "Clique table variable count mismatch"); cuopt_assert(static_cast(num_vars) <= xstar.size(), "Clique cut xstar size mismatch"); @@ -958,7 +962,7 @@ void cut_generation_t::generate_clique_cuts( weights.push_back(1.0 - xj); } - if (vertices.empty()) { return; } + if (vertices.empty()) { return true; } std::vector vertex_to_local(2 * num_vars, -1); std::vector in_subgraph(2 * num_vars, 0); @@ -1020,19 +1024,25 @@ void cut_generation_t::generate_clique_cuts( } extend_clique_vertices( clique_vertices, *clique_table_, xstar, reduced_costs, num_vars, settings.integer_tol); - if (build_clique_cut(clique_vertices, - num_vars, - var_types, - user_problem_.lower, - user_problem_.upper, - xstar, - bound_tol, - min_violation, - cut, - cut_rhs)) { + const auto build_status = build_clique_cut(clique_vertices, + num_vars, + var_types, + user_problem_.lower, + user_problem_.upper, + xstar, + bound_tol, + min_violation, + cut, + cut_rhs); + if (build_status == clique_cut_build_status_t::INFEASIBLE) { + settings.log.debug("Detected contradictory variable/complement clique\n"); + return false; + } + if (build_status == clique_cut_build_status_t::CUT_ADDED) { cut_pool_.add_cut(cut_type_t::CLIQUE, cut, cut_rhs); } } + return true; } template diff --git a/cpp/src/dual_simplex/cuts.hpp b/cpp/src/dual_simplex/cuts.hpp index 2a88094de4..5d79af33f4 100644 --- a/cpp/src/dual_simplex/cuts.hpp +++ b/cpp/src/dual_simplex/cuts.hpp @@ -245,7 +245,7 @@ class cut_generation_t { { } - void generate_cuts(const lp_problem_t& lp, + bool generate_cuts(const lp_problem_t& lp, const simplex_solver_settings_t& settings, csr_matrix_t& Arow, const std::vector& new_slacks, @@ -285,7 +285,7 @@ class cut_generation_t { const std::vector& xstar); // Generate clique cuts from conflict graph cliques - void generate_clique_cuts(const lp_problem_t& lp, + bool generate_clique_cuts(const lp_problem_t& lp, const simplex_solver_settings_t& settings, const std::vector& var_types, const std::vector& xstar, From b758b9f3424e6599987b93a140e2513c9ea76a9b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 11 Feb 2026 04:12:07 -0800 Subject: [PATCH 037/147] fix timer issues --- cpp/src/dual_simplex/branch_and_bound.cpp | 90 +++++++---- cpp/src/dual_simplex/cuts.cpp | 142 ++++++++++++------ cpp/src/dual_simplex/cuts.hpp | 49 +++--- cpp/src/dual_simplex/pseudo_costs.cpp | 15 +- cpp/src/mip/solver.cu | 2 + .../unit_tests/presolve_test.cu | 2 - 6 files changed, 204 insertions(+), 96 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index e74f69d13a..176ff719db 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1989,12 +1989,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); - // FIXME: rarely dual simplex detects infeasible whereas it is feasible. - // to add a small safety net, check if there is a primal solution already. - // Uncomment this if the issue with cost266-UUE is resolved - // if (settings.heuristic_preemption_callback != nullptr) { - // settings.heuristic_preemption_callback(); - // } + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); + } return mip_status_t::INFEASIBLE; } if (root_status == lp_status_t::UNBOUNDED) { @@ -2074,9 +2071,19 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t last_upper_bound = std::numeric_limits::infinity(); f_t last_objective = root_objective_; f_t root_relax_objective = root_objective_; + auto stop_for_time_limit = [&]() -> bool { + const f_t elapsed = toc(exploration_stats_.start_time); + if (elapsed > settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return true; + } + return false; + }; i_t cut_pool_size = 0; for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { + if (stop_for_time_limit()) { return solver_status_; } if (num_fractional == 0) { set_solution_at_root(solution, cut_info); return mip_status_t::OPTIMAL; @@ -2094,6 +2101,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #endif // Generate cuts and add them to the cut pool + if (stop_for_time_limit()) { return solver_status_; } f_t cut_start_time = tic(); cut_generation.generate_cuts(original_lp_, settings_, @@ -2103,21 +2111,27 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, root_relax_soln_.x, basic_list, - nonbasic_list); + nonbasic_list, + exploration_stats_.start_time); + if (stop_for_time_limit()) { return solver_status_; } f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); } // Score the cuts + if (stop_for_time_limit()) { return solver_status_; } f_t score_start_time = tic(); - cut_pool.score_cuts(root_relax_soln_.x); + cut_pool.score_cuts(root_relax_soln_.x, exploration_stats_.start_time); + if (stop_for_time_limit()) { return solver_status_; } f_t score_time = toc(score_start_time); if (score_time > 1.0) { settings_.log.debug("Cut scoring time %.2f seconds\n", score_time); } // Get the best cuts from the cut pool csr_matrix_t cuts_to_add(0, original_lp_.num_cols, 0); std::vector cut_rhs; std::vector cut_types; - i_t num_cuts = cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types); + i_t num_cuts = + cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types, exploration_stats_.start_time); + if (stop_for_time_limit()) { return solver_status_; } if (num_cuts == 0) { break; } cut_info.record_cut_types(cut_types); #ifdef PRINT_CUT_POOL_TYPES @@ -2150,6 +2164,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut cuts_to_add.m + original_lp_.num_rows); lp_settings.log.log = false; + if (stop_for_time_limit()) { return solver_status_; } f_t add_cuts_start_time = tic(); mutex_original_lp_.lock(); i_t add_cuts_status = add_cuts(settings_, @@ -2162,14 +2177,20 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basic_list, nonbasic_list, root_vstatus_, - edge_norms_); + edge_norms_, + exploration_stats_.start_time); var_types_.resize(original_lp_.num_cols, variable_type_t::CONTINUOUS); mutex_original_lp_.unlock(); + if (stop_for_time_limit()) { return solver_status_; } f_t add_cuts_time = toc(add_cuts_start_time); if (add_cuts_time > 1.0) { settings_.log.debug("Add cuts time %.2f seconds\n", add_cuts_time); } - if (add_cuts_status != 0) { + if (add_cuts_status == -2) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } else if (add_cuts_status != 0) { settings_.log.printf("Failed to add cuts\n"); return mip_status_t::NUMERICAL; } @@ -2196,6 +2217,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #endif original_lp_.A.to_compressed_row(Arow_); + if (stop_for_time_limit()) { return solver_status_; } f_t node_presolve_start_time = tic(); bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); std::vector new_lower = original_lp_.lower; @@ -2206,6 +2228,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_.lower = new_lower; original_lp_.upper = new_upper; mutex_original_lp_.unlock(); + if (stop_for_time_limit()) { return solver_status_; } f_t node_presolve_time = toc(node_presolve_start_time); if (node_presolve_time > 1.0) { settings_.log.debug("Node presolve time %.2f seconds\n", node_presolve_time); @@ -2218,8 +2241,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t iter = 0; bool initialize_basis = false; lp_settings.concurrent_halt = NULL; - f_t dual_phase2_start_time = tic(); - dual::status_t cut_status = dual_phase2_with_advanced_basis(2, + if (stop_for_time_limit()) { return solver_status_; } + f_t dual_phase2_start_time = tic(); + dual::status_t cut_status = dual_phase2_with_advanced_basis(2, 0, initialize_basis, exploration_stats_.start_time, @@ -2238,6 +2262,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (dual_phase2_time > 1.0) { settings_.log.debug("Dual phase2 time %.2f seconds\n", dual_phase2_time); } + if (stop_for_time_limit()) { return solver_status_; } if (cut_status == dual::status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, root_objective_); @@ -2245,6 +2270,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } if (cut_status != dual::status_t::OPTIMAL) { + if (stop_for_time_limit()) { return solver_status_; } settings_.log.printf("Numerical issue at root node. Resolving from scratch\n"); lp_status_t scratch_status = solve_linear_program_with_advanced_basis(original_lp_, @@ -2256,6 +2282,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut nonbasic_list, root_vstatus_, edge_norms_); + if (stop_for_time_limit() || scratch_status == lp_status_t::TIME_LIMIT) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } if (scratch_status == lp_status_t::OPTIMAL) { // We recovered cut_status = convert_lp_status_to_dual_status(scratch_status); @@ -2267,23 +2298,26 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } + if (stop_for_time_limit()) { return solver_status_; } f_t remove_cuts_start_time = tic(); mutex_original_lp_.lock(); - remove_cuts(original_lp_, - settings_, - Arow_, - new_slacks_, - original_rows, - var_types_, - root_vstatus_, - edge_norms_, - root_relax_soln_.x, - root_relax_soln_.y, - root_relax_soln_.z, - basic_list, - nonbasic_list, - basis_update); + i_t remove_cuts_status = remove_cuts(original_lp_, + settings_, + Arow_, + new_slacks_, + original_rows, + var_types_, + root_vstatus_, + edge_norms_, + root_relax_soln_.x, + root_relax_soln_.y, + root_relax_soln_.z, + basic_list, + nonbasic_list, + basis_update, + exploration_stats_.start_time); mutex_original_lp_.unlock(); + if (stop_for_time_limit()) { return solver_status_; } f_t remove_cuts_time = toc(remove_cuts_start_time); if (remove_cuts_time > 1.0) { settings_.log.debug("Remove cuts time %.2f seconds\n", remove_cuts_time); @@ -2335,7 +2369,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); - { + if (toc(exploration_stats_.start_time) < settings_.time_limit) { raft::common::nvtx::range scope_sb("BB::strong_branching"); strong_branching(original_problem_, original_lp_, diff --git a/cpp/src/dual_simplex/cuts.cpp b/cpp/src/dual_simplex/cuts.cpp index 0bb4e17444..ced10be89f 100644 --- a/cpp/src/dual_simplex/cuts.cpp +++ b/cpp/src/dual_simplex/cuts.cpp @@ -95,7 +95,7 @@ f_t cut_pool_t::cut_orthogonality(i_t i, i_t j) } template -void cut_pool_t::score_cuts(std::vector& x_relax) +void cut_pool_t::score_cuts(std::vector& x_relax, f_t start_time) { const f_t min_cut_distance = 1e-4; cut_distances_.resize(cut_storage_.m, 0.0); @@ -103,6 +103,11 @@ void cut_pool_t::score_cuts(std::vector& x_relax) const bool verbose = false; for (i_t i = 0; i < cut_storage_.m; i++) { + if (toc(start_time) >= settings_.time_limit) { + best_cuts_.clear(); + scored_cuts_ = 0; + return; + } f_t violation; f_t cut_dist = cut_distance(i, x_relax, violation, cut_norms_[i]); cut_distances_[i] = cut_dist <= min_cut_distance ? 0.0 : cut_dist; @@ -133,6 +138,7 @@ void cut_pool_t::score_cuts(std::vector& x_relax) } while (scored_cuts_ < max_cuts && !sorted_indices.empty()) { + if (toc(start_time) >= settings_.time_limit) { return; } const i_t i = sorted_indices.back(); sorted_indices.pop_back(); @@ -141,6 +147,7 @@ void cut_pool_t::score_cuts(std::vector& x_relax) f_t cut_ortho = 1.0; const i_t best_cuts_size = best_cuts_.size(); for (i_t k = 0; k < best_cuts_size; k++) { + if (toc(start_time) >= settings_.time_limit) { return; } const i_t j = best_cuts_[k]; cut_ortho = std::min(cut_ortho, cut_orthogonality(i, j)); } @@ -154,7 +161,8 @@ void cut_pool_t::score_cuts(std::vector& x_relax) template i_t cut_pool_t::get_best_cuts(csr_matrix_t& best_cuts, std::vector& best_rhs, - std::vector& best_cut_types) + std::vector& best_cut_types, + f_t start_time) { best_cuts.m = 0; best_cuts.n = original_vars_; @@ -168,7 +176,9 @@ i_t cut_pool_t::get_best_cuts(csr_matrix_t& best_cuts, best_cut_types.clear(); best_cut_types.reserve(scored_cuts_); - for (i_t i : best_cuts_) { + for (i_t k = 0; k < static_cast(best_cuts_.size()); ++k) { + if (toc(start_time) >= settings_.time_limit) { break; } + const i_t i = best_cuts_[k]; sparse_vector_t cut(cut_storage_, i); cut.negate(); best_cuts.append_row(cut); @@ -559,33 +569,46 @@ void cut_generation_t::generate_cuts(const lp_problem_t& lp, basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list) + const std::vector& nonbasic_list, + f_t start_time) { + if (toc(start_time) >= settings.time_limit) { return; } + // Generate Gomory and CG Cuts if (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { f_t cut_start_time = tic(); - generate_gomory_cuts( - lp, settings, Arow, new_slacks, var_types, basis_update, xstar, basic_list, nonbasic_list); + generate_gomory_cuts(lp, + settings, + Arow, + new_slacks, + var_types, + basis_update, + xstar, + basic_list, + nonbasic_list, + start_time); f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings.log.debug("Gomory and CG cut generation time %.2f seconds\n", cut_generation_time); } } + if (toc(start_time) >= settings.time_limit) { return; } // Generate Knapsack cuts if (settings.knapsack_cuts != 0) { f_t cut_start_time = tic(); - generate_knapsack_cuts(lp, settings, Arow, new_slacks, var_types, xstar); + generate_knapsack_cuts(lp, settings, Arow, new_slacks, var_types, xstar, start_time); f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings.log.debug("Knapsack cut generation time %.2f seconds\n", cut_generation_time); } } + if (toc(start_time) >= settings.time_limit) { return; } // Generate MIR and CG cuts if (settings.mir_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { f_t cut_start_time = tic(); - generate_mir_cuts(lp, settings, Arow, new_slacks, var_types, xstar); + generate_mir_cuts(lp, settings, Arow, new_slacks, var_types, xstar, start_time); f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings.log.debug("MIR and CG cut generation time %.2f seconds\n", cut_generation_time); @@ -600,10 +623,12 @@ void cut_generation_t::generate_knapsack_cuts( csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar) + const std::vector& xstar, + f_t start_time) { if (knapsack_generation_.num_knapsack_constraints() > 0) { for (i_t knapsack_row : knapsack_generation_.get_knapsack_constraints()) { + if (toc(start_time) >= settings.time_limit) { return; } sparse_vector_t cut(lp.num_cols, 0); f_t cut_rhs; i_t knapsack_status = knapsack_generation_.generate_knapsack_cuts( @@ -620,8 +645,10 @@ void cut_generation_t::generate_mir_cuts( csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar) + const std::vector& xstar, + f_t start_time) { + if (toc(start_time) >= settings.time_limit) { return; } f_t mir_start_time = tic(); mixed_integer_rounding_cut_t mir(lp, settings, new_slacks, xstar); strong_cg_cut_t cg(lp, var_types, xstar); @@ -639,6 +666,7 @@ void cut_generation_t::generate_mir_cuts( // Compute initial scores for all rows std::vector score(lp.num_rows, 0.0); for (i_t i = 0; i < lp.num_rows; i++) { + if (toc(start_time) >= settings.time_limit) { return; } const i_t row_start = Arow.row_start[i]; const i_t row_end = Arow.row_start[i + 1]; @@ -688,6 +716,7 @@ void cut_generation_t::generate_mir_cuts( const i_t max_cuts = std::min(lp.num_rows, 1000); f_t work_estimate = 0.0; for (i_t h = 0; h < max_cuts; h++) { + if (toc(start_time) >= settings.time_limit) { return; } // Get the row with the highest score const i_t i = sorted_indices.back(); sorted_indices.pop_back(); @@ -768,6 +797,7 @@ void cut_generation_t::generate_mir_cuts( work_estimate += lp.num_cols; while (!add_cut && num_aggregated < max_aggregated) { + if (toc(start_time) >= settings.time_limit) { return; } sparse_vector_t transformed_inequality; inequality.squeeze(transformed_inequality); f_t transformed_rhs = inequality_rhs; @@ -979,13 +1009,15 @@ void cut_generation_t::generate_gomory_cuts( basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list) + const std::vector& nonbasic_list, + f_t start_time) { tableau_equality_t tableau(lp, basis_update, nonbasic_list); mixed_integer_rounding_cut_t mir(lp, settings, new_slacks, xstar); strong_cg_cut_t cg(lp, var_types, xstar); for (i_t i = 0; i < lp.num_rows; i++) { + if (toc(start_time) >= settings.time_limit) { return; } sparse_vector_t inequality(lp.num_cols, 0); f_t inequality_rhs; const i_t j = basic_list[i]; @@ -2312,9 +2344,13 @@ i_t add_cuts(const simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, - std::vector& edge_norms) + std::vector& edge_norms, + f_t start_time) { + constexpr i_t time_limit_status = -2; + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } + // Given a set of cuts: C*x <= d that are currently violated // by the current solution x* (i.e. C*x* > d), this function // adds the cuts into the LP and solves again. @@ -2342,6 +2378,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, settings.log.debug("Original lp rows %d\n", lp.num_rows); settings.log.debug("Original lp cols %d\n", lp.num_cols); + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } csr_matrix_t new_A_row(lp.num_rows, lp.num_cols, 1); lp.A.to_compressed_row(new_A_row); @@ -2351,6 +2388,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, assert(append_status == 0); } + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } csc_matrix_t new_A_col(lp.num_rows + p, lp.num_cols, 1); new_A_row.to_compressed_col(new_A_col); @@ -2405,6 +2443,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, settings.log.debug("Done adding rhs\n"); // Construct C_B = C(:, basic_list) + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } std::vector C_col_degree(lp.num_cols, 0); i_t cuts_nz = cuts.row_start[p]; for (i_t q = 0; q < cuts_nz; q++) { @@ -2434,6 +2473,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, } settings.log.debug("Done estimating C_B_nz\n"); + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } csr_matrix_t C_B(p, num_basic, C_B_nz); nz = 0; for (i_t i = 0; i < p; i++) { @@ -2458,6 +2498,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, settings.log.debug("C_B rows %d cols %d nz %d\n", C_B.m, C_B.n, nz); // Adjust the basis update to include the new cuts + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } basis_update.append_cuts(C_B); basic_list.resize(lp.num_rows, 0); @@ -2495,25 +2536,30 @@ i_t add_cuts(const simplex_solver_settings_t& settings, solution.y.resize(lp.num_rows, 0.0); solution.z.resize(lp.num_cols, 0.0); + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } return 0; } template -void remove_cuts(lp_problem_t& lp, - const simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - std::vector& new_slacks, - i_t original_rows, - std::vector& var_types, - std::vector& vstatus, - std::vector& edge_norms, - std::vector& x, - std::vector& y, - std::vector& z, - std::vector& basic_list, - std::vector& nonbasic_list, - basis_update_mpf_t& basis_update) +i_t remove_cuts(lp_problem_t& lp, + const simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + std::vector& new_slacks, + i_t original_rows, + std::vector& var_types, + std::vector& vstatus, + std::vector& edge_norms, + std::vector& x, + std::vector& y, + std::vector& z, + std::vector& basic_list, + std::vector& nonbasic_list, + basis_update_mpf_t& basis_update, + f_t start_time) { + constexpr i_t time_limit_status = -2; + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } + std::vector cuts_to_remove; cuts_to_remove.reserve(lp.num_rows - original_rows); std::vector slacks_to_remove; @@ -2522,6 +2568,7 @@ void remove_cuts(lp_problem_t& lp, std::vector is_slack(lp.num_cols, 0); for (i_t j : new_slacks) { + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } is_slack[j] = 1; #ifdef CHECK_SLACKS // Check that slack column length is 1 @@ -2536,12 +2583,14 @@ void remove_cuts(lp_problem_t& lp, } for (i_t k = original_rows; k < lp.num_rows; k++) { + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } if (std::abs(y[k]) < dual_tol) { const i_t row_start = Arow.row_start[k]; const i_t row_end = Arow.row_start[k + 1]; i_t last_slack = -1; const f_t slack_tol = 1e-3; for (i_t p = row_start; p < row_end; p++) { + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } const i_t j = Arow.j[p]; if (is_slack[j]) { if (vstatus[j] == variable_status_t::BASIC && x[j] > slack_tol) { last_slack = j; } @@ -2555,6 +2604,7 @@ void remove_cuts(lp_problem_t& lp, } if (cuts_to_remove.size() > 0) { + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } std::vector marked_rows(lp.num_rows, 0); for (i_t i : cuts_to_remove) { marked_rows[i] = 1; @@ -2568,6 +2618,7 @@ void remove_cuts(lp_problem_t& lp, std::vector new_solution_y(lp.num_rows - cuts_to_remove.size()); i_t h = 0; for (i_t i = 0; i < lp.num_rows; i++) { + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } if (!marked_rows[i]) { new_rhs[h] = lp.rhs[i]; new_solution_y[h] = y[i]; @@ -2594,6 +2645,7 @@ void remove_cuts(lp_problem_t& lp, std::vector new_is_slacks(lp.num_cols - slacks_to_remove.size(), 0); h = 0; for (i_t k = 0; k < lp.num_cols; k++) { + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } if (!marked_cols[k]) { new_objective[h] = lp.objective[k]; new_lower[h] = lp.lower[k]; @@ -2641,10 +2693,14 @@ void remove_cuts(lp_problem_t& lp, lp.num_cols, lp.A.col_start[lp.A.n]); + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } basis_update.resize(lp.num_rows); basis_update.refactor_basis( lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus); } + + if (toc(start_time) >= settings.time_limit) { return time_limit_status; } + return 0; } template @@ -2789,22 +2845,24 @@ template int add_cuts(const simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, - std::vector& edge_norms); - -template void remove_cuts(lp_problem_t& lp, - const simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - std::vector& new_slacks, - int original_rows, - std::vector& var_types, - std::vector& vstatus, - std::vector& edge_norms, - std::vector& x, - std::vector& y, - std::vector& z, - std::vector& basic_list, - std::vector& nonbasic_list, - basis_update_mpf_t& basis_update); + std::vector& edge_norms, + double start_time); + +template int remove_cuts(lp_problem_t& lp, + const simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + std::vector& new_slacks, + int original_rows, + std::vector& var_types, + std::vector& vstatus, + std::vector& edge_norms, + std::vector& x, + std::vector& y, + std::vector& z, + std::vector& basic_list, + std::vector& nonbasic_list, + basis_update_mpf_t& basis_update, + double start_time); template void read_saved_solution_for_cut_verification( const lp_problem_t& lp, diff --git a/cpp/src/dual_simplex/cuts.hpp b/cpp/src/dual_simplex/cuts.hpp index a4a36d75b2..17f5e032af 100644 --- a/cpp/src/dual_simplex/cuts.hpp +++ b/cpp/src/dual_simplex/cuts.hpp @@ -138,12 +138,13 @@ class cut_pool_t { // cut'*xstart < rhs void add_cut(cut_type_t cut_type, const sparse_vector_t& cut, f_t rhs); - void score_cuts(std::vector& x_relax); + void score_cuts(std::vector& x_relax, f_t start_time); // We return the cuts in the form best_cuts*x <= best_rhs i_t get_best_cuts(csr_matrix_t& best_cuts, std::vector& best_rhs, - std::vector& best_cut_types); + std::vector& best_cut_types, + f_t start_time); void age_cuts(); @@ -239,7 +240,8 @@ class cut_generation_t { basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list); + const std::vector& nonbasic_list, + f_t start_time); private: // Generate all mixed integer gomory cuts @@ -251,7 +253,8 @@ class cut_generation_t { basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list); + const std::vector& nonbasic_list, + f_t start_time); // Generate all mixed integer rounding cuts void generate_mir_cuts(const lp_problem_t& lp, @@ -259,7 +262,8 @@ class cut_generation_t { csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar); + const std::vector& xstar, + f_t start_time); // Generate all knapsack cuts void generate_knapsack_cuts(const lp_problem_t& lp, @@ -267,7 +271,8 @@ class cut_generation_t { csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar); + const std::vector& xstar, + f_t start_time); cut_pool_t& cut_pool_; knapsack_generation_t knapsack_generation_; @@ -458,22 +463,24 @@ i_t add_cuts(const simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, - std::vector& edge_norms); + std::vector& edge_norms, + f_t start_time); template -void remove_cuts(lp_problem_t& lp, - const simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - std::vector& new_slacks, - i_t original_rows, - std::vector& var_types, - std::vector& vstatus, - std::vector& edge_norms, - std::vector& x, - std::vector& y, - std::vector& z, - std::vector& basic_list, - std::vector& nonbasic_list, - basis_update_mpf_t& basis_update); +i_t remove_cuts(lp_problem_t& lp, + const simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + std::vector& new_slacks, + i_t original_rows, + std::vector& var_types, + std::vector& vstatus, + std::vector& edge_norms, + std::vector& x, + std::vector& y, + std::vector& z, + std::vector& basic_list, + std::vector& nonbasic_list, + basis_update_mpf_t& basis_update, + f_t start_time); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index de8994e801..40949391e8 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -313,6 +313,9 @@ void strong_branching(const user_problem_t& original_problem, pc.strong_branch_up.assign(fractional.size(), 0); pc.num_strong_branches_completed = 0; + const f_t elapsed_time = toc(start_time); + if (elapsed_time > settings.time_limit) { return; } + if (settings.mip_batch_pdlp_strong_branching) { settings.log.printf("Batch PDLP strong branching enabled\n"); @@ -333,9 +336,15 @@ void strong_branching(const user_problem_t& original_problem, fraction_values.push_back(original_root_soln_x[j]); } - const auto mps_model = simplex_problem_to_mps_data_model(original_problem); - const auto solutions = - batch_pdlp_solve(original_problem.handle_ptr, mps_model, fractional, fraction_values); + const auto mps_model = simplex_problem_to_mps_data_model(original_problem); + const f_t batch_elapsed_time = toc(start_time); + const f_t batch_remaining_time = + std::max(static_cast(0.0), settings.time_limit - batch_elapsed_time); + if (batch_remaining_time <= 0.0) { return; } + pdlp_solver_settings_t pdlp_settings; + pdlp_settings.time_limit = batch_remaining_time; + const auto solutions = batch_pdlp_solve( + original_problem.handle_ptr, mps_model, fractional, fraction_values, pdlp_settings); f_t batch_pdlp_strong_branching_time = toc(start_batch); // Find max iteration on how many are done accross the batch diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 21fa93bfaa..201cf3b62a 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -310,9 +310,11 @@ solution_t mip_solver_t::run_solver() if (!is_feasible.value(sol.handle_ptr->get_stream())) { CUOPT_LOG_ERROR( "Solution is not feasible due to variable bounds, returning infeasible solution!"); + context.stats.total_solve_time = timer_.elapsed_time(); context.problem_ptr->post_process_solution(sol); return sol; } + context.stats.total_solve_time = timer_.elapsed_time(); context.problem_ptr->post_process_solution(sol); dm.rins.stop_rins(); return sol; diff --git a/cpp/tests/linear_programming/unit_tests/presolve_test.cu b/cpp/tests/linear_programming/unit_tests/presolve_test.cu index 3fd05d97ff..77a26cff7e 100644 --- a/cpp/tests/linear_programming/unit_tests/presolve_test.cu +++ b/cpp/tests/linear_programming/unit_tests/presolve_test.cu @@ -166,7 +166,6 @@ TEST(pslp_presolve, postsolve_accuracy_afiro) TEST(pslp_presolve, postsolve_dual_accuracy_afiro) { const raft::handle_t handle_{}; - constexpr double tolerance = 1e-5; auto path = make_path_absolute("linear_programming/afiro_original.mps"); auto mps_data_model = cuopt::mps_parser::parse_mps(path, true); @@ -351,7 +350,6 @@ TEST(pslp_presolve, postsolve_reduced_costs) TEST(pslp_presolve, postsolve_multiple_problems) { const raft::handle_t handle_{}; - constexpr double tolerance = 1e-4; std::vector> instances{ {"afiro_original", -464.75314}, From b246e8ce1770651c48e730952290bf55e9f3f9f3 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 11 Feb 2026 04:23:23 -0800 Subject: [PATCH 038/147] fix compilation --- cpp/src/dual_simplex/branch_and_bound.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 6c97a79480..ea01c9d085 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1757,8 +1757,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); - if (settings.heuristic_preemption_callback != nullptr) { - settings.heuristic_preemption_callback(); + if (settings_.heuristic_preemption_callback != nullptr) { + settings_.heuristic_preemption_callback(); } return mip_status_t::INFEASIBLE; } From 66d459827e133e9b0f37e1a42d60f18dff953808 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 11 Feb 2026 05:02:51 -0800 Subject: [PATCH 039/147] fix compile issues --- cpp/src/dual_simplex/branch_and_bound.cpp | 1 + cpp/src/dual_simplex/cuts.cpp | 6 +++--- cpp/src/dual_simplex/sparse_matrix.cpp | 6 ++++-- cpp/src/mip/problem/problem.cu | 6 +++--- cpp/src/mip/solver.cu | 20 ++++++++++---------- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 786c3509da..9564c86fd8 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -2118,6 +2118,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut var_types_, basis_update, root_relax_soln_.x, + root_relax_soln_.z, basic_list, nonbasic_list, exploration_stats_.start_time); diff --git a/cpp/src/dual_simplex/cuts.cpp b/cpp/src/dual_simplex/cuts.cpp index 2076072680..4d54fc8100 100644 --- a/cpp/src/dual_simplex/cuts.cpp +++ b/cpp/src/dual_simplex/cuts.cpp @@ -843,7 +843,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, const std::vector& nonbasic_list, f_t start_time) { - if (toc(start_time) >= settings.time_limit) { return; } + if (toc(start_time) >= settings.time_limit) { return true; } // Generate Gomory and CG Cuts if (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { @@ -863,7 +863,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, settings.log.debug("Gomory and CG cut generation time %.2f seconds\n", cut_generation_time); } } - if (toc(start_time) >= settings.time_limit) { return; } + if (toc(start_time) >= settings.time_limit) { return true; } // Generate Knapsack cuts if (settings.knapsack_cuts != 0) { @@ -874,7 +874,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, settings.log.debug("Knapsack cut generation time %.2f seconds\n", cut_generation_time); } } - if (toc(start_time) >= settings.time_limit) { return; } + if (toc(start_time) >= settings.time_limit) { return true; } // Generate Clique cuts if (settings.clique_cuts != 0) { diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index dfce00558b..9d3b7efb72 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -685,8 +685,10 @@ void csr_matrix_t::insert_row(const std::vector& vars, this->row_start.push_back(this->row_start.back() + vars.size()); this->m++; this->nz_max += vars.size(); - this->j.insert(this->j.end(), vars.begin(), vars.end()); - this->x.insert(this->x.end(), coeffs.begin(), coeffs.end()); + this->j.reserve(this->nz_max); + this->x.reserve(this->nz_max); + this->j.underlying().insert(this->j.underlying().end(), vars.begin(), vars.end()); + this->x.underlying().insert(this->x.underlying().end(), coeffs.begin(), coeffs.end()); } // x <- x + alpha * A(:, j) diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 0fb32dc55b..2a7af1a522 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -1914,9 +1914,9 @@ void problem_t::set_constraints_from_host_user_problem( empty = (nnz == 0 && n_constraints == 0 && n_variables == 0); auto stream = handle_ptr->get_stream(); - cuopt::device_copy(coefficients, csr_A.x, stream); - cuopt::device_copy(variables, csr_A.j, stream); - cuopt::device_copy(offsets, csr_A.row_start, stream); + cuopt::device_copy(coefficients, csr_A.x.underlying(), stream); + cuopt::device_copy(variables, csr_A.j.underlying(), stream); + cuopt::device_copy(offsets, csr_A.row_start.underlying(), stream); std::vector h_constraint_lower_bounds(n_constraints); std::vector h_constraint_upper_bounds(n_constraints); diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 90c5a421f1..24e8c982a8 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -107,16 +107,16 @@ solution_t mip_solver_t::run_solver() context.problem_ptr->post_process_solution(sol); return sol; } - dm.timer = timer_; - const bool run_presolve = context.settings.presolver != presolver_t::None; - f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC - ? std::numeric_limits::infinity() - : timer_.remaining_time(); - const double presolve_time_limit = std::min(0.1 * time_limit, 60.0); - presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC - ? std::numeric_limits::infinity() - : presolve_time_limit; - bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit) : true; + dm.timer = timer_; + const bool run_presolve = context.settings.presolver != presolver_t::None; + f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC + ? std::numeric_limits::infinity() + : timer_.remaining_time(); + double presolve_time_limit = std::min(0.1 * time_limit, 60.0); + presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC + ? std::numeric_limits::infinity() + : presolve_time_limit; + bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); solution_t sol(*context.problem_ptr); From 0392a6247a4402e0fca3ce3c82e97bff9f6456ae Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 11 Feb 2026 15:30:43 +0000 Subject: [PATCH 040/147] disable jobserver flag when not actually using jobserver --- build.sh | 10 +++++++++- cpp/CMakeLists.txt | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index b5c35f510b..f790ae16d5 100755 --- a/build.sh +++ b/build.sh @@ -360,6 +360,13 @@ fi ################################################################################ # Configure, build, and install libcuopt if buildAll || hasArg libcuopt; then + # Enable nvcc --jobserver only when building through the Makefile, + # which starts a GNU Make jobserver that nvcc can participate in. + if hasArg -n; then + USE_NVCC_JOBSERVER=ON + else + USE_NVCC_JOBSERVER=OFF + fi mkdir -p "${LIBCUOPT_BUILD_DIR}" cd "${LIBCUOPT_BUILD_DIR}" cmake -DDEFINE_ASSERT=${DEFINE_ASSERT} \ @@ -381,12 +388,13 @@ if buildAll || hasArg libcuopt; then -DWRITE_FATBIN=${WRITE_FATBIN} \ -DHOST_LINEINFO=${HOST_LINEINFO} \ -DPARALLEL_LEVEL="${PARALLEL_LEVEL}" \ + -DUSE_NVCC_JOBSERVER="${USE_NVCC_JOBSERVER}" \ -DINSTALL_TARGET="${INSTALL_TARGET}" \ "${CACHE_ARGS[@]}" \ "${EXTRA_CMAKE_ARGS[@]}" \ "${REPODIR}"/cpp JFLAG="${PARALLEL_LEVEL:+-j${PARALLEL_LEVEL}}" - if hasArg -n; then + if [ "${USE_NVCC_JOBSERVER}" = "ON" ]; then # Manual make invocation to start its jobserver make ${JFLAG} -C "${REPODIR}/cpp" LIBCUOPT_BUILD_DIR="${LIBCUOPT_BUILD_DIR}" VERBOSE_FLAG="${VERBOSE_FLAG}" PARALLEL_LEVEL="${PARALLEL_LEVEL}" ninja-build else diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index af7b7c1c38..aa377b3155 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -153,11 +153,11 @@ if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.9 AND CMAKE_CUDA_COMPILE endif() list(APPEND CUOPT_CUDA_FLAGS -fopenmp) -# Add jobserver flags for parallel compilation if PARALLEL_LEVEL is set +# Add parallel compilation flags if PARALLEL_LEVEL is set if(PARALLEL_LEVEL AND NOT "${PARALLEL_LEVEL}" STREQUAL "") message(STATUS "Enabling nvcc parallel compilation support") list(APPEND CUOPT_CUDA_FLAGS --threads=0 --split-compile=0) - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 13.0) + if(USE_NVCC_JOBSERVER AND CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 13.0) message(STATUS "Enabling nvcc jobserver support (NVCC >= 13.0)") list(APPEND CUOPT_CUDA_FLAGS --jobserver) endif() From ee544770a1cfbf7642e527957ffdea40664c2da1 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 11 Feb 2026 15:58:10 +0000 Subject: [PATCH 041/147] disable jobserver unless explicitely requested --- build.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build.sh b/build.sh index f790ae16d5..927acd6646 100755 --- a/build.sh +++ b/build.sh @@ -362,11 +362,7 @@ fi if buildAll || hasArg libcuopt; then # Enable nvcc --jobserver only when building through the Makefile, # which starts a GNU Make jobserver that nvcc can participate in. - if hasArg -n; then - USE_NVCC_JOBSERVER=ON - else - USE_NVCC_JOBSERVER=OFF - fi + USE_NVCC_JOBSERVER=${USE_NVCC_JOBSERVER:-OFF} mkdir -p "${LIBCUOPT_BUILD_DIR}" cd "${LIBCUOPT_BUILD_DIR}" cmake -DDEFINE_ASSERT=${DEFINE_ASSERT} \ From f876fc09db2fe5b6c03612d8a85a863a470b2838 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 11 Feb 2026 16:23:22 +0000 Subject: [PATCH 042/147] better workaround fix build --- build.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build.sh b/build.sh index 927acd6646..b5c35f510b 100755 --- a/build.sh +++ b/build.sh @@ -360,9 +360,6 @@ fi ################################################################################ # Configure, build, and install libcuopt if buildAll || hasArg libcuopt; then - # Enable nvcc --jobserver only when building through the Makefile, - # which starts a GNU Make jobserver that nvcc can participate in. - USE_NVCC_JOBSERVER=${USE_NVCC_JOBSERVER:-OFF} mkdir -p "${LIBCUOPT_BUILD_DIR}" cd "${LIBCUOPT_BUILD_DIR}" cmake -DDEFINE_ASSERT=${DEFINE_ASSERT} \ @@ -384,13 +381,12 @@ if buildAll || hasArg libcuopt; then -DWRITE_FATBIN=${WRITE_FATBIN} \ -DHOST_LINEINFO=${HOST_LINEINFO} \ -DPARALLEL_LEVEL="${PARALLEL_LEVEL}" \ - -DUSE_NVCC_JOBSERVER="${USE_NVCC_JOBSERVER}" \ -DINSTALL_TARGET="${INSTALL_TARGET}" \ "${CACHE_ARGS[@]}" \ "${EXTRA_CMAKE_ARGS[@]}" \ "${REPODIR}"/cpp JFLAG="${PARALLEL_LEVEL:+-j${PARALLEL_LEVEL}}" - if [ "${USE_NVCC_JOBSERVER}" = "ON" ]; then + if hasArg -n; then # Manual make invocation to start its jobserver make ${JFLAG} -C "${REPODIR}/cpp" LIBCUOPT_BUILD_DIR="${LIBCUOPT_BUILD_DIR}" VERBOSE_FLAG="${VERBOSE_FLAG}" PARALLEL_LEVEL="${PARALLEL_LEVEL}" ninja-build else From b97809623dbbdbc74350efccf6af4c98130419dc Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 11 Feb 2026 08:32:48 -0800 Subject: [PATCH 043/147] fix timer issues on clique cuts --- cpp/src/dual_simplex/cuts.cpp | 51 ++++++++++++++++--- cpp/src/dual_simplex/cuts.hpp | 3 +- .../presolve/conflict_graph/clique_table.cu | 20 ++++++-- .../presolve/conflict_graph/clique_table.cuh | 3 +- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/cpp/src/dual_simplex/cuts.cpp b/cpp/src/dual_simplex/cuts.cpp index 4d54fc8100..f3481ab337 100644 --- a/cpp/src/dual_simplex/cuts.cpp +++ b/cpp/src/dual_simplex/cuts.cpp @@ -93,6 +93,8 @@ struct bk_bitset_context_t { const std::vector& weights; f_t min_weight; i_t max_calls; + f_t start_time; + f_t time_limit; size_t words; i_t num_calls{0}; std::vector> cliques; @@ -141,6 +143,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, std::vector& X, // already in the clique f_t weight_R) { + if (toc(ctx.start_time) >= ctx.time_limit) { return; } ctx.num_calls++; // stop the recursion, for perf reasons if (ctx.num_calls > ctx.max_calls) { return; } @@ -161,9 +164,11 @@ void bron_kerbosch(bk_bitset_context_t& ctx, // pivoting rule according to the highest degree vertex // TODO try other pivoting strategies, we can also implement some online learning like MAB for (size_t w = 0; w < ctx.words; ++w) { + if (toc(ctx.start_time) >= ctx.time_limit) { return; } // union of P and X uint64_t word = P[w] | X[w]; while (word) { + if (toc(ctx.start_time) >= ctx.time_limit) { return; } // least significant set bit idnex const int bit = __builtin_ctzll(word); // overall vertex index @@ -190,9 +195,11 @@ void bron_kerbosch(bk_bitset_context_t& ctx, candidates.reserve(ctx.weights.size()); cuopt_assert(pivot >= 0, "Pivot must be valid when P or X is non-empty"); for (size_t w = 0; w < ctx.words; ++w) { + if (toc(ctx.start_time) >= ctx.time_limit) { return; } // P / N(pivot) uint64_t word = P[w] & ~ctx.adj[pivot][w]; while (word) { + if (toc(ctx.start_time) >= ctx.time_limit) { return; } const int bit = __builtin_ctzll(word); const i_t v = static_cast(w * 64 + static_cast(bit)); word &= (word - 1); @@ -202,6 +209,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, // note that candidates will include pivot if it is in P for (auto v : candidates) { + if (toc(ctx.start_time) >= ctx.time_limit) { return; } R.push_back(v); std::vector P_next(ctx.words, 0); std::vector X_next(ctx.words, 0); @@ -222,13 +230,17 @@ void extend_clique_vertices(std::vector& clique_vertices, const std::vector& xstar, const std::vector& reduced_costs, i_t num_vars, - f_t integer_tol) + f_t integer_tol, + f_t start_time, + f_t time_limit) { + if (toc(start_time) >= time_limit) { return; } if (clique_vertices.empty()) { return; } i_t smallest_degree = std::numeric_limits::max(); i_t smallest_degree_var = -1; for (auto v : clique_vertices) { + if (toc(start_time) >= time_limit) { return; } i_t degree = graph.get_degree_of_var(v); if (degree < smallest_degree) { smallest_degree = degree; @@ -242,6 +254,7 @@ void extend_clique_vertices(std::vector& clique_vertices, candidates.reserve(adj_set.size()); // the candidate list if only the integer valued vertices for (const auto& candidate : adj_set) { + if (toc(start_time) >= time_limit) { return; } if (clique_members.count(candidate) != 0) { continue; } i_t var_idx = candidate % num_vars; f_t value = candidate >= num_vars ? (1.0 - xstar[var_idx]) : xstar[var_idx]; @@ -266,8 +279,10 @@ void extend_clique_vertices(std::vector& clique_vertices, }); for (const auto candidate : candidates) { + if (toc(start_time) >= time_limit) { return; } bool add = true; for (const auto v : clique_vertices) { + if (toc(start_time) >= time_limit) { return; } if (!graph.check_adjacency(candidate, v)) { add = false; break; @@ -879,7 +894,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, // Generate Clique cuts if (settings.clique_cuts != 0) { f_t cut_start_time = tic(); - bool feasible = generate_clique_cuts(lp, settings, var_types, xstar, reduced_costs); + bool feasible = generate_clique_cuts(lp, settings, var_types, xstar, reduced_costs, start_time); if (!feasible) { settings.log.printf("Clique cuts proved infeasible\n"); return false; @@ -930,9 +945,11 @@ bool cut_generation_t::generate_clique_cuts( const simplex_solver_settings_t& settings, const std::vector& var_types, const std::vector& xstar, - const std::vector& reduced_costs) + const std::vector& reduced_costs, + f_t start_time) { if (settings.clique_cuts == 0) { return true; } + if (toc(start_time) >= settings.time_limit) { return true; } const i_t num_vars = user_problem_.num_cols; @@ -950,8 +967,12 @@ bool cut_generation_t::generate_clique_cuts( tolerances.absolute_mip_gap = settings.absolute_mip_gap_tol; tolerances.relative_mip_gap = settings.relative_mip_gap_tol; + const f_t remaining_time = + std::max(static_cast(0.), settings.time_limit - toc(start_time)); + cuopt::timer_t clique_build_timer(static_cast(remaining_time)); ::cuopt::linear_programming::detail::build_clique_table( - user_problem_, *clique_table_, tolerances, true, true); + user_problem_, *clique_table_, tolerances, true, true, clique_build_timer); + if (clique_build_timer.check_time_limit()) { return true; } } if (clique_table_->first.empty() && clique_table_->addtl_cliques.empty()) { return true; } @@ -975,6 +996,7 @@ bool cut_generation_t::generate_clique_cuts( // create the sub graph induced by fractional binary variables for (i_t j = 0; j < num_vars; ++j) { + if (toc(start_time) >= settings.time_limit) { return true; } if (user_problem_.var_types[j] == variable_type_t::CONTINUOUS) { continue; } const f_t lower_bound = user_problem_.lower[j]; const f_t upper_bound = user_problem_.upper[j]; @@ -992,17 +1014,20 @@ bool cut_generation_t::generate_clique_cuts( std::vector vertex_to_local(2 * num_vars, -1); std::vector in_subgraph(2 * num_vars, 0); for (size_t idx = 0; idx < vertices.size(); ++idx) { + if (toc(start_time) >= settings.time_limit) { return true; } vertex_to_local[vertices[idx]] = static_cast(idx); in_subgraph[vertices[idx]] = 1; } std::vector> adj_local(vertices.size()); for (size_t idx = 0; idx < vertices.size(); ++idx) { + if (toc(start_time) >= settings.time_limit) { return true; } i_t vertex_idx = vertices[idx]; auto adj_set = clique_table_->get_adj_set_of_var(vertex_idx); auto& adj = adj_local[idx]; adj.reserve(adj_set.size() + 1); for (const auto neighbor : adj_set) { + if (toc(start_time) >= settings.time_limit) { return true; } cuopt_assert(neighbor >= 0 && neighbor < 2 * num_vars, "Neighbor out of range"); if (!in_subgraph[neighbor]) { continue; } i_t local_neighbor = vertex_to_local[neighbor]; @@ -1023,14 +1048,17 @@ bool cut_generation_t::generate_clique_cuts( const size_t words = bitset_words(vertices.size()); std::vector> adj_bitset(vertices.size(), std::vector(words, 0)); for (size_t v = 0; v < adj_local.size(); ++v) { + if (toc(start_time) >= settings.time_limit) { return true; } for (const auto neighbor : adj_local[v]) { + if (toc(start_time) >= settings.time_limit) { return true; } if (neighbor >= 0 && static_cast(neighbor) < vertices.size()) { bitset_set(adj_bitset[v], static_cast(neighbor)); } } } - bk_bitset_context_t ctx{adj_bitset, weights, min_weight, max_calls, words}; + bk_bitset_context_t ctx{ + adj_bitset, weights, min_weight, max_calls, start_time, settings.time_limit, words}; std::vector R; std::vector P(words, 0); std::vector X(words, 0); @@ -1038,17 +1066,26 @@ bool cut_generation_t::generate_clique_cuts( bitset_set(P, idx); } bron_kerbosch(ctx, R, P, X, 0.0); + if (toc(start_time) >= settings.time_limit) { return true; } sparse_vector_t cut(lp.num_cols, 0); f_t cut_rhs = 0.0; for (auto& clique_local : ctx.cliques) { + if (toc(start_time) >= settings.time_limit) { return true; } std::vector clique_vertices; clique_vertices.reserve(clique_local.size()); for (auto local_idx : clique_local) { clique_vertices.push_back(vertices[local_idx]); } - extend_clique_vertices( - clique_vertices, *clique_table_, xstar, reduced_costs, num_vars, settings.integer_tol); + extend_clique_vertices(clique_vertices, + *clique_table_, + xstar, + reduced_costs, + num_vars, + settings.integer_tol, + start_time, + settings.time_limit); + if (toc(start_time) >= settings.time_limit) { return true; } const auto build_status = build_clique_cut(clique_vertices, num_vars, var_types, diff --git a/cpp/src/dual_simplex/cuts.hpp b/cpp/src/dual_simplex/cuts.hpp index efe47a2d9c..498237e723 100644 --- a/cpp/src/dual_simplex/cuts.hpp +++ b/cpp/src/dual_simplex/cuts.hpp @@ -294,7 +294,8 @@ class cut_generation_t { const simplex_solver_settings_t& settings, const std::vector& var_types, const std::vector& xstar, - const std::vector& reduced_costs); + const std::vector& reduced_costs, + f_t start_time); cut_pool_t& cut_pool_; knapsack_generation_t knapsack_generation_; diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip/presolve/conflict_graph/clique_table.cu index 7043cdb89b..881f3e1290 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cu @@ -206,13 +206,16 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro } template -void remove_small_cliques(clique_table_t& clique_table) +void remove_small_cliques(clique_table_t& clique_table, cuopt::timer_t& timer) { + if (timer.check_time_limit()) { return; } + i_t num_removed_first = 0; i_t num_removed_addtl = 0; std::vector to_delete(clique_table.first.size(), false); // if a clique is small, we remove it from the cliques and add it to adjlist for (size_t clique_idx = 0; clique_idx < clique_table.first.size(); clique_idx++) { + if (timer.check_time_limit()) { return; } const auto& clique = clique_table.first[clique_idx]; if (clique.size() <= (size_t)clique_table.min_clique_size) { for (size_t i = 0; i < clique.size(); i++) { @@ -226,6 +229,7 @@ void remove_small_cliques(clique_table_t& clique_table) } } for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { + if (timer.check_time_limit()) { return; } const auto& addtl_clique = clique_table.addtl_cliques[addtl_c]; i_t size_of_clique = clique_table.first[addtl_clique.clique_idx].size() - addtl_clique.start_pos_on_clique + 1; @@ -519,8 +523,10 @@ void build_clique_table(const dual_simplex::user_problem_t& problem, clique_table_t& clique_table, typename mip_solver_settings_t::tolerances_t tolerances, bool remove_small_cliques_flag, - bool fill_var_clique_maps_flag) + bool fill_var_clique_maps_flag, + cuopt::timer_t& timer) { + if (timer.check_time_limit()) { return; } cuopt_assert(clique_table.n_variables == problem.num_cols, "Clique table size mismatch"); cuopt_assert(problem.var_types.size() == static_cast(problem.num_cols), "Problem variable types size mismatch"); @@ -534,9 +540,12 @@ void build_clique_table(const dual_simplex::user_problem_t& problem, sort_csr_by_constraint_coefficients(knapsack_constraints); clique_table.tolerances = tolerances; for (const auto& knapsack_constraint : knapsack_constraints) { + if (timer.check_time_limit()) { return; } find_cliques_from_constraint(knapsack_constraint, clique_table); } - if (remove_small_cliques_flag) { remove_small_cliques(clique_table); } + if (timer.check_time_limit()) { return; } + if (remove_small_cliques_flag) { remove_small_cliques(clique_table, timer); } + if (timer.check_time_limit()) { return; } if (fill_var_clique_maps_flag) { fill_var_clique_maps(clique_table); } } @@ -863,7 +872,7 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, clique_table_ptr->addtl_cliques.size()); // print_clique_table(clique_table); // remove small cliques and add them to adj_list - remove_small_cliques(*clique_table_ptr); + remove_small_cliques(*clique_table_ptr, timer); t_small = stage_timer.elapsed_time(); // fill var clique maps fill_var_clique_maps(*clique_table_ptr); @@ -906,7 +915,8 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, clique_table_t& clique_table, \ typename mip_solver_settings_t::tolerances_t tolerances, \ bool remove_small_cliques_flag, \ - bool fill_var_clique_maps_flag); \ + bool fill_var_clique_maps_flag, \ + cuopt::timer_t& timer); \ template class clique_table_t; #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh index 9b544cc5e9..71fe612145 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip/presolve/conflict_graph/clique_table.cuh @@ -108,7 +108,8 @@ void build_clique_table(const dual_simplex::user_problem_t& problem, clique_table_t& clique_table, typename mip_solver_settings_t::tolerances_t tolerances, bool remove_small_cliques, - bool fill_var_clique_maps); + bool fill_var_clique_maps, + cuopt::timer_t& timer); } // namespace cuopt::linear_programming::detail From 41ba6eae3611018cb383dfa299f58b1383c78eb8 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 11 Feb 2026 10:31:46 -0800 Subject: [PATCH 044/147] fix resize issue --- cpp/src/dual_simplex/sparse_matrix.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index 9d3b7efb72..55013e3b9b 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -685,10 +685,11 @@ void csr_matrix_t::insert_row(const std::vector& vars, this->row_start.push_back(this->row_start.back() + vars.size()); this->m++; this->nz_max += vars.size(); - this->j.reserve(this->nz_max); - this->x.reserve(this->nz_max); - this->j.underlying().insert(this->j.underlying().end(), vars.begin(), vars.end()); - this->x.underlying().insert(this->x.underlying().end(), coeffs.begin(), coeffs.end()); + const i_t old_size = this->j.size(); + this->j.resize(this->j.size() + vars.size()); + std::copy(vars.data(), vars.data() + vars.size(), this->j.underlying().data() + old_size); + this->x.resize(this->x.size() + coeffs.size()); + std::copy(coeffs.data(), coeffs.data() + coeffs.size(), this->x.underlying().data() + old_size); } // x <- x + alpha * A(:, j) From 0993c2f60ccd9f9a85ed8e64e7439af106c0c05d Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 12 Feb 2026 03:12:38 -0800 Subject: [PATCH 045/147] fix merge conflicts --- cpp/src/cuts/cuts.cpp | 2 +- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 1 + .../presolve/conflict_graph/clique_table.cu | 4 ++-- .../presolve/conflict_graph/clique_table.cuh | 0 4 files changed, 4 insertions(+), 3 deletions(-) rename cpp/src/{mip => mip_heuristics}/presolve/conflict_graph/clique_table.cu (99%) rename cpp/src/{mip => mip_heuristics}/presolve/conflict_graph/clique_table.cuh (100%) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 816de3f715..0364b44393 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index b4d79a7078..abfb14637f 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -9,6 +9,7 @@ #include "diversity_manager.cuh" #include +#include #include #include #include diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu similarity index 99% rename from cpp/src/mip/presolve/conflict_graph/clique_table.cu rename to cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 881f3e1290..1ad799c760 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -22,8 +22,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh similarity index 100% rename from cpp/src/mip/presolve/conflict_graph/clique_table.cuh rename to cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh From 373ede6cd22ca7bd6509c1bc7c30264e17eb6272 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 12 Feb 2026 06:00:08 -0800 Subject: [PATCH 046/147] without clique cuts --- cpp/include/cuopt/linear_programming/mip/solver_settings.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index a9e404d14e..fd57e15ad6 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -93,7 +93,7 @@ class mip_solver_settings_t { i_t mir_cuts = -1; i_t mixed_integer_gomory_cuts = -1; i_t knapsack_cuts = -1; - i_t clique_cuts = -1; + i_t clique_cuts = 0; i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; From 64ae63c5d623e3542b805e9f0244e888f8fe0eb7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 12 Feb 2026 06:37:58 -0800 Subject: [PATCH 047/147] fix infeasible proof and with cuts --- .../cuopt/linear_programming/mip/solver_settings.hpp | 2 +- cpp/src/cuts/cuts.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index fd57e15ad6..a9e404d14e 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -93,7 +93,7 @@ class mip_solver_settings_t { i_t mir_cuts = -1; i_t mixed_integer_gomory_cuts = -1; i_t knapsack_cuts = -1; - i_t clique_cuts = 0; + i_t clique_cuts = -1; i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 0364b44393..118cac9557 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -66,13 +66,15 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic // that's why compelements have 1 as coeff and normal vars have -1 if (complement) { if (seen_original.count(var_idx) > 0) { return clique_cut_build_status_t::INFEASIBLE; } - cuopt_assert(seen_complement.insert(var_idx).second, "Duplicate complement in clique"); + cuopt_assert(seen_complement.count(var_idx) == 0, "Duplicate complement in clique"); + seen_complement.insert(var_idx); num_complements++; cut.i.push_back(var_idx); cut.x.push_back(1.0); } else { if (seen_complement.count(var_idx) > 0) { return clique_cut_build_status_t::INFEASIBLE; } - cuopt_assert(seen_original.insert(var_idx).second, "Duplicate variable in clique"); + cuopt_assert(seen_original.count(var_idx) == 0, "Duplicate variable in clique"); + seen_original.insert(var_idx); cut.i.push_back(var_idx); cut.x.push_back(-1.0); } From eed37684b2f1f1dd2f24228febc8220377d66654 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 12 Feb 2026 06:46:37 -0800 Subject: [PATCH 048/147] clique cuts without preprocessing --- .../diversity/diversity_manager.cu | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index abfb14637f..b13cb40719 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,17 +207,17 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!context.settings.heuristics_only && !problem_ptr->empty) { - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - find_initial_cliques( - host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - trivial_presolve(*problem_ptr, remap_cache_ids); - if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - } + // if (!context.settings.heuristics_only && !problem_ptr->empty) { + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + // find_initial_cliques( + // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 39c4a261643216d1f2e5bae268c753a50ebeb17f Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 12 Feb 2026 06:47:03 -0800 Subject: [PATCH 049/147] clique cut with proprocessing --- .../diversity/diversity_manager.cu | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index b13cb40719..abfb14637f 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,17 +207,17 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // if (!context.settings.heuristics_only && !problem_ptr->empty) { - // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - // problem_ptr->get_host_user_problem(host_problem); - // std::shared_ptr> clique_table; - // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - // find_initial_cliques( - // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - // problem_ptr->set_constraints_from_host_user_problem(host_problem); - // trivial_presolve(*problem_ptr, remap_cache_ids); - // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - // } + if (!context.settings.heuristics_only && !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + find_initial_cliques( + host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + trivial_presolve(*problem_ptr, remap_cache_ids); + if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From bf5ddc31614cc91f713c2411967934e7cbc030b7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 12 Feb 2026 06:48:15 -0800 Subject: [PATCH 050/147] no clique cuts with preprocessing --- cpp/include/cuopt/linear_programming/mip/solver_settings.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index a9e404d14e..fd57e15ad6 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -93,7 +93,7 @@ class mip_solver_settings_t { i_t mir_cuts = -1; i_t mixed_integer_gomory_cuts = -1; i_t knapsack_cuts = -1; - i_t clique_cuts = -1; + i_t clique_cuts = 0; i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; From 674fa64608842b142029898eed47d9e11bb0c705 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 12 Feb 2026 06:49:07 -0800 Subject: [PATCH 051/147] no clique cut no preprocessing --- .../diversity/diversity_manager.cu | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index abfb14637f..b13cb40719 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,17 +207,17 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!context.settings.heuristics_only && !problem_ptr->empty) { - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - find_initial_cliques( - host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - trivial_presolve(*problem_ptr, remap_cache_ids); - if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - } + // if (!context.settings.heuristics_only && !problem_ptr->empty) { + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + // find_initial_cliques( + // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 7de08d2dfd124bf5fba7faa3ea391c3cf08c85f8 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 02:45:54 -0800 Subject: [PATCH 052/147] add timers to right_looking_lu and refactoring the basis --- cpp/src/branch_and_bound/branch_and_bound.cpp | 18 +++++- cpp/src/cuts/cuts.cpp | 6 +- cpp/src/dual_simplex/basis_solves.cpp | 20 ++++-- cpp/src/dual_simplex/basis_solves.hpp | 3 +- cpp/src/dual_simplex/basis_updates.cpp | 11 +++- cpp/src/dual_simplex/basis_updates.hpp | 3 +- cpp/src/dual_simplex/crossover.cpp | 61 +++++++++++-------- cpp/src/dual_simplex/phase2.cpp | 31 +++++++--- cpp/src/dual_simplex/presolve.cpp | 2 + cpp/src/dual_simplex/primal.cpp | 19 ++++-- cpp/src/dual_simplex/right_looking_lu.cpp | 20 +++--- cpp/src/dual_simplex/right_looking_lu.hpp | 3 +- cpp/src/dual_simplex/solve.cpp | 2 + cpp/src/dual_simplex/types.hpp | 2 + 14 files changed, 134 insertions(+), 67 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 842845b0dd..bd06dd3a0f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1888,8 +1888,13 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( original_lp_.upper, basic_list, nonbasic_list, - crossover_vstatus_); - if (refactor_status != 0) { + crossover_vstatus_, + exploration_stats_.start_time); + if (refactor_status == TIME_LIMIT_RETURN) { + root_status = lp_status_t::TIME_LIMIT; + } else if (refactor_status == CONCURRENT_HALT_RETURN) { + root_status = lp_status_t::TIME_LIMIT; + } else if (refactor_status != 0) { settings_.log.printf("Failed to refactor basis. %d deficient columns.\n", refactor_status); assert(refactor_status == 0); root_status = lp_status_t::NUMERICAL_ISSUES; @@ -1901,6 +1906,15 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( user_objective = root_crossover_soln_.user_objective; iter = root_crossover_soln_.iterations; solver_name = "Barrier/PDLP and Crossover"; + } else if (crossover_status == crossover_status_t::TIME_LIMIT || + toc(exploration_stats_.start_time) > settings_.time_limit) { + set_root_concurrent_halt(1); + root_status = root_status_future.get(); + set_root_concurrent_halt(0); + root_status = lp_status_t::TIME_LIMIT; + user_objective = root_relax_soln_.user_objective; + iter = root_relax_soln_.iterations; + solver_name = "Dual Simplex"; } else { root_status = root_status_future.get(); user_objective = root_relax_soln_.user_objective; diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index aae893b22a..2d98badb84 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -2697,8 +2697,10 @@ i_t remove_cuts(lp_problem_t& lp, if (toc(start_time) >= settings.time_limit) { return time_limit_status; } basis_update.resize(lp.num_rows); - basis_update.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus); + i_t refactor_status = basis_update.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + if (refactor_status == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } + if (refactor_status == TIME_LIMIT_RETURN) { return time_limit_status; } } if (toc(start_time) >= settings.time_limit) { return time_limit_status; } diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index 17f997f4a9..9a8cfc143e 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -165,7 +165,8 @@ i_t factorize_basis(const csc_matrix_t& A, std::vector& pinv, std::vector& q, std::vector& deficient, - std::vector& slacks_needed) + std::vector& slacks_needed, + f_t start_time) { raft::common::nvtx::range scope("LU::factorize_basis"); const i_t m = basic_list.size(); @@ -363,11 +364,12 @@ i_t factorize_basis(const csc_matrix_t& A, S_col_perm, SL, SU, - S_perm_inv); + S_perm_inv, + start_time); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { - settings.log.printf("Concurrent halt\n"); return CONCURRENT_HALT_RETURN; } + if (Srank < 0) { return Srank; } if (Srank != Sdim) { // Get the rank deficient columns deficient.clear(); @@ -568,7 +570,13 @@ i_t factorize_basis(const csc_matrix_t& A, } q.resize(m); f_t fact_start = tic(); - rank = right_looking_lu(A, settings, medium_tol, basic_list, q, L, U, pinv); + rank = right_looking_lu(A, settings, medium_tol, basic_list, q, L, U, pinv, start_time); + if (rank < 0) { + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { + return CONCURRENT_HALT_RETURN; + } + return rank; + } inverse_permutation(pinv, p); if (rank != m) { // Get the rank deficient columns @@ -584,7 +592,6 @@ i_t factorize_basis(const csc_matrix_t& A, } } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { - settings.log.printf("Concurrent halt\n"); return CONCURRENT_HALT_RETURN; } if (verbose) { @@ -874,7 +881,8 @@ template int factorize_basis(const csc_matrix_t& A, std::vector& pinv, std::vector& q, std::vector& deficient, - std::vector& slacks_needed); + std::vector& slacks_needed, + double start_time); template int basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, diff --git a/cpp/src/dual_simplex/basis_solves.hpp b/cpp/src/dual_simplex/basis_solves.hpp index 59b4725e42..13227a8324 100644 --- a/cpp/src/dual_simplex/basis_solves.hpp +++ b/cpp/src/dual_simplex/basis_solves.hpp @@ -36,7 +36,8 @@ i_t factorize_basis(const csc_matrix_t& A, std::vector& pinv, std::vector& q, std::vector& deficient, - std::vector& slacks_need); + std::vector& slacks_need, + f_t start_time); // Repair the basis by bringing in slacks template diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 71dce2e39c..5f8ef49343 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2264,7 +2264,8 @@ int basis_update_mpf_t::refactor_basis( const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& vstatus) + std::vector& vstatus, + f_t start_time) { raft::common::nvtx::range scope("LU::refactor_basis"); std::vector deficient; @@ -2282,8 +2283,10 @@ int basis_update_mpf_t::refactor_basis( inverse_row_permutation_, q, deficient, - slacks_needed); + slacks_needed, + start_time); if (status == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } + if (status == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } if (status == -1) { settings.log.debug("Initial factorization failed\n"); basis_repair(A, @@ -2323,8 +2326,10 @@ int basis_update_mpf_t::refactor_basis( inverse_row_permutation_, q, deficient, - slacks_needed); + slacks_needed, + start_time); if (status == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } + if (status == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } if (status == -1) { #ifdef CHECK_L_FACTOR if (L0_.check_matrix() == -1) { settings.log.printf("Bad L after basis repair\n"); } diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index d9783053f7..6fd7b13f34 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -382,7 +382,8 @@ class basis_update_mpf_t { const std::vector& upper, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& vstatus); + std::vector& vstatus, + f_t start_time); void set_refactor_frequency(i_t new_frequency) { refactor_frequency_ = new_frequency; } diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 597628e735..65da8d45c0 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -25,7 +25,7 @@ namespace { crossover_status_t return_to_status(int status) { - if (status == -1) { + if (status == TIME_LIMIT_RETURN) { return crossover_status_t::TIME_LIMIT; } else if (status == CONCURRENT_HALT_RETURN) { return crossover_status_t::CONCURRENT_LIMIT; @@ -505,10 +505,12 @@ i_t dual_push(const lp_problem_t& lp, std::vector q(m); std::vector deficient; std::vector slacks_needed; - i_t rank = - factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + i_t rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; + } else if (rank < 0) { + return rank; } else if (rank != m) { settings.log.printf("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, @@ -521,13 +523,12 @@ i_t dual_push(const lp_problem_t& lp, nonbasic_list, superbasic_list, vstatus); - rank = - factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; - } else if (rank == -1) { - settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); - return -1; + } else if (rank < 0) { + return rank; } else { settings.log.printf("Basis repaired\n"); } @@ -560,7 +561,7 @@ i_t dual_push(const lp_problem_t& lp, } if (toc(start_time) > settings.time_limit) { settings.log.printf("Crossover time exceeded\n"); - return -1; + return TIME_LIMIT_RETURN; } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Concurrent halt\n"); @@ -860,10 +861,12 @@ i_t primal_push(const lp_problem_t& lp, std::vector q(m); std::vector deficient; std::vector slacks_needed; - i_t rank = - factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + i_t rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; + } else if (rank < 0) { + return rank; } else if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, @@ -879,13 +882,12 @@ i_t primal_push(const lp_problem_t& lp, // We need to be careful. As basis_repair may have changed the superbasic list find_primal_superbasic_variables( lp, settings, solution, solution, vstatus, nonbasic_list, superbasic_list); - rank = - factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; - } else if (rank == -1) { - settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); - return -1; + } else if (rank < 0) { + return rank; } else { settings.log.debug("Basis repaired\n"); } @@ -915,7 +917,7 @@ i_t primal_push(const lp_problem_t& lp, if (toc(start_time) > settings.time_limit) { settings.log.printf("Crossover time limit exceeded\n"); - return -1; + return TIME_LIMIT_RETURN; } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Concurrent halt\n"); @@ -1223,8 +1225,10 @@ crossover_status_t crossover(const lp_problem_t& lp, std::vector deficient; std::vector slacks_needed; - rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return crossover_status_t::CONCURRENT_LIMIT; } + if (rank < 0) { return return_to_status(rank); } if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, @@ -1237,12 +1241,13 @@ crossover_status_t crossover(const lp_problem_t& lp, nonbasic_list, superbasic_list, vstatus); - rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return crossover_status_t::CONCURRENT_LIMIT; - } else if (rank == -1) { + } else if (rank < 0) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); - return crossover_status_t::NUMERICAL_ISSUES; + return return_to_status(rank); } else { settings.log.debug("Basis repaired\n"); } @@ -1392,10 +1397,12 @@ crossover_status_t crossover(const lp_problem_t& lp, nonbasic_list.clear(); superbasic_list.clear(); get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); - rank = - factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return crossover_status_t::CONCURRENT_LIMIT; + } else if (rank < 0) { + return return_to_status(rank); } else if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, @@ -1408,13 +1415,13 @@ crossover_status_t crossover(const lp_problem_t& lp, nonbasic_list, superbasic_list, vstatus); - rank = - factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return crossover_status_t::CONCURRENT_LIMIT; - } else if (rank == -1) { + } else if (rank < 0) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); - return crossover_status_t::NUMERICAL_ISSUES; + return return_to_status(rank); } else { settings.log.debug("Basis repaired\n"); } diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 1e057a16b8..25d1c5fbe5 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2504,10 +2504,11 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); - if (ft.refactor_basis(lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > - 0) { - return dual::status_t::NUMERICAL; - } + i_t refactor_status = ft.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + if (refactor_status == CONCURRENT_HALT_RETURN) { return dual::status_t::CONCURRENT_LIMIT; } + if (refactor_status == TIME_LIMIT_RETURN) { return dual::status_t::TIME_LIMIT; } + if (refactor_status > 0) { return dual::status_t::NUMERICAL; } if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } } @@ -3371,15 +3372,24 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, PHASE2_NVTX_RANGE("DualSimplex::refactorization"); num_refactors++; bool should_recompute_x = false; - if (ft.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus) > 0) { + i_t refactor_status = ft.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + if (refactor_status == CONCURRENT_HALT_RETURN) { return dual::status_t::CONCURRENT_LIMIT; } + if (refactor_status == TIME_LIMIT_RETURN) { return dual::status_t::TIME_LIMIT; } + if (refactor_status > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } - i_t count = 0; - i_t deficient_size; - while ((deficient_size = ft.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus)) > 0) { + i_t count = 0; + i_t deficient_size = 0; + while (true) { + deficient_size = ft.refactor_basis( + lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + if (deficient_size == CONCURRENT_HALT_RETURN) { + return dual::status_t::CONCURRENT_LIMIT; + } + if (deficient_size == TIME_LIMIT_RETURN) { return dual::status_t::TIME_LIMIT; } + if (deficient_size <= 0) { break; } settings.log.printf("Failed to repair basis. Iteration %d. %d deficient columns.\n", iter, static_cast(deficient_size)); @@ -3390,6 +3400,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, count++; if (count > 10) { return dual::status_t::NUMERICAL; } } + if (deficient_size < 0) { return dual::status_t::NUMERICAL; } settings.log.printf("Successfully repaired basis. Iteration %d\n", iter); } diff --git a/cpp/src/dual_simplex/presolve.cpp b/cpp/src/dual_simplex/presolve.cpp index 8d4a533e93..f4602ac563 100644 --- a/cpp/src/dual_simplex/presolve.cpp +++ b/cpp/src/dual_simplex/presolve.cpp @@ -445,6 +445,7 @@ i_t find_dependent_rows(lp_problem_t& problem, i_t pivots = right_looking_lu_row_permutation_only(C, settings, 1e-13, tic(), q, pinv); if (pivots == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } + if (pivots == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } if (pivots < m) { settings.log.printf("Found %d dependent rows\n", m - pivots); const i_t num_dependent = m - pivots; @@ -1101,6 +1102,7 @@ i_t presolve(const lp_problem_t& original, f_t dependent_row_start = tic(); const i_t independent_rows = find_dependent_rows(problem, settings, dependent_rows, infeasible); if (independent_rows == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } + if (independent_rows == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } if (infeasible != kOk) { settings.log.printf("Found problem infeasible in presolve\n"); return -1; diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 98f5f4193b..2d0944542b 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -294,10 +294,15 @@ primal::status_t primal_phase2(i_t phase, std::vector q(m); std::vector deficient; std::vector slacks_needed; - i_t rank = - factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + i_t rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return primal::status_t::CONCURRENT_LIMIT; + } else if (rank == TIME_LIMIT_RETURN) { + return primal::status_t::TIME_LIMIT; + } else if (rank < 0) { + return toc(start_time) > settings.time_limit ? primal::status_t::TIME_LIMIT + : primal::status_t::NUMERICAL; } else if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, @@ -310,12 +315,16 @@ primal::status_t primal_phase2(i_t phase, nonbasic_list, superbasic_list, vstatus); - rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + rank = factorize_basis( + lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); if (rank == CONCURRENT_HALT_RETURN) { return primal::status_t::CONCURRENT_LIMIT; - } else if (rank == -1) { + } else if (rank == TIME_LIMIT_RETURN) { + return primal::status_t::TIME_LIMIT; + } else if (rank < 0) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); - return primal::status_t::NUMERICAL; + return toc(start_time) > settings.time_limit ? primal::status_t::TIME_LIMIT + : primal::status_t::NUMERICAL; } else { settings.log.debug("Basis repaired\n"); } diff --git a/cpp/src/dual_simplex/right_looking_lu.cpp b/cpp/src/dual_simplex/right_looking_lu.cpp index cb9834705c..4c55e1f19e 100644 --- a/cpp/src/dual_simplex/right_looking_lu.cpp +++ b/cpp/src/dual_simplex/right_looking_lu.cpp @@ -580,7 +580,8 @@ i_t right_looking_lu(const csc_matrix_t& A, VectorI& q, csc_matrix_t& L, csc_matrix_t& U, - VectorI& pinv) + VectorI& pinv, + f_t start_time) { raft::common::nvtx::range scope("LU::right_looking_lu"); const i_t n = column_list.size(); @@ -634,7 +635,10 @@ i_t right_looking_lu(const csc_matrix_t& A, i_t pivots = 0; for (i_t k = 0; k < n; ++k) { - if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return -1; } + if (toc(start_time) > settings.time_limit) { return TIME_LIMIT_RETURN; } + if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { + return CONCURRENT_HALT_RETURN; + } // Find pivot that satisfies // abs(pivot) >= abstol, // abs(pivot) >= threshold_tol * max abs[pivot column] @@ -1114,11 +1118,7 @@ i_t right_looking_lu_row_permutation_only(const csc_matrix_t& A, toc(factorization_start_time)); last_print = tic(); } - if (toc(factorization_start_time) > settings.time_limit) { - settings.log.printf("Right-looking LU factorization time exceeded\n"); - return -1; - } - + if (toc(start_time) > settings.time_limit) { return TIME_LIMIT_RETURN; } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Concurrent halt\n"); return CONCURRENT_HALT_RETURN; @@ -1157,7 +1157,8 @@ template int right_looking_lu>( std::vector& q, csc_matrix_t& L, csc_matrix_t& U, - std::vector& pinv); + std::vector& pinv, + double start_time); template int right_looking_lu>( const csc_matrix_t& A, @@ -1167,7 +1168,8 @@ template int right_looking_lu>( ins_vector& q, csc_matrix_t& L, csc_matrix_t& U, - ins_vector& pinv); + ins_vector& pinv, + double start_time); template int right_looking_lu_row_permutation_only( const csc_matrix_t& A, diff --git a/cpp/src/dual_simplex/right_looking_lu.hpp b/cpp/src/dual_simplex/right_looking_lu.hpp index 179fff01b2..3f83da42f8 100644 --- a/cpp/src/dual_simplex/right_looking_lu.hpp +++ b/cpp/src/dual_simplex/right_looking_lu.hpp @@ -22,7 +22,8 @@ i_t right_looking_lu(const csc_matrix_t& A, VectorI& q, csc_matrix_t& L, csc_matrix_t& U, - VectorI& pinv); + VectorI& pinv, + f_t start_time); template i_t right_looking_lu_row_permutation_only(const csc_matrix_t& A, diff --git a/cpp/src/dual_simplex/solve.cpp b/cpp/src/dual_simplex/solve.cpp index 1e825b5e49..42f99a6292 100644 --- a/cpp/src/dual_simplex/solve.cpp +++ b/cpp/src/dual_simplex/solve.cpp @@ -154,6 +154,7 @@ lp_status_t solve_linear_program_with_advanced_basis( ok = presolve(original_lp, settings, presolved_lp, presolve_info); } if (ok == CONCURRENT_HALT_RETURN) { return lp_status_t::CONCURRENT_LIMIT; } + if (ok == TIME_LIMIT_RETURN) { return lp_status_t::TIME_LIMIT; } if (ok == -1) { return lp_status_t::INFEASIBLE; } constexpr bool write_out_matlab = false; @@ -349,6 +350,7 @@ lp_status_t solve_linear_program_with_barrier(const user_problem_t& us lp_problem_t presolved_lp(user_problem.handle_ptr, 1, 1, 1); const i_t ok = presolve(original_lp, barrier_settings, presolved_lp, presolve_info); if (ok == CONCURRENT_HALT_RETURN) { return lp_status_t::CONCURRENT_LIMIT; } + if (ok == TIME_LIMIT_RETURN) { return lp_status_t::TIME_LIMIT; } if (ok == -1) { return lp_status_t::INFEASIBLE; } // Apply columns scaling to the presolve LP diff --git a/cpp/src/dual_simplex/types.hpp b/cpp/src/dual_simplex/types.hpp index 9de33ed3b3..ea46a1f67e 100644 --- a/cpp/src/dual_simplex/types.hpp +++ b/cpp/src/dual_simplex/types.hpp @@ -21,5 +21,7 @@ constexpr float64_t inf = std::numeric_limits::infinity(); // We return this constant to signal that a concurrent halt has occurred #define CONCURRENT_HALT_RETURN -2 +// We return this constant to signal that a time limit has occurred +#define TIME_LIMIT_RETURN -3 } // namespace cuopt::linear_programming::dual_simplex From 942de9c5937debdecf58ba4cd27139670629fa21 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 02:56:56 -0800 Subject: [PATCH 053/147] remove timers from cuts --- cpp/src/branch_and_bound/branch_and_bound.cpp | 17 ++-- cpp/src/cuts/cuts.cpp | 79 +++---------------- cpp/src/cuts/cuts.hpp | 20 ++--- 3 files changed, 25 insertions(+), 91 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index bd06dd3a0f..33a67acbff 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2139,8 +2139,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, root_relax_soln_.x, basic_list, - nonbasic_list, - exploration_stats_.start_time); + nonbasic_list); if (stop_for_time_limit()) { return solver_status_; } f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { @@ -2149,7 +2148,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // Score the cuts if (stop_for_time_limit()) { return solver_status_; } f_t score_start_time = tic(); - cut_pool.score_cuts(root_relax_soln_.x, exploration_stats_.start_time); + cut_pool.score_cuts(root_relax_soln_.x); if (stop_for_time_limit()) { return solver_status_; } f_t score_time = toc(score_start_time); if (score_time > 1.0) { settings_.log.debug("Cut scoring time %.2f seconds\n", score_time); } @@ -2157,8 +2156,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut csr_matrix_t cuts_to_add(0, original_lp_.num_cols, 0); std::vector cut_rhs; std::vector cut_types; - i_t num_cuts = - cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types, exploration_stats_.start_time); + i_t num_cuts = cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types); if (stop_for_time_limit()) { return solver_status_; } if (num_cuts == 0) { break; } cut_info.record_cut_types(cut_types); @@ -2205,8 +2203,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basic_list, nonbasic_list, root_vstatus_, - edge_norms_, - exploration_stats_.start_time); + edge_norms_); var_types_.resize(original_lp_.num_cols, variable_type_t::CONTINUOUS); mutex_original_lp_.unlock(); if (stop_for_time_limit()) { return solver_status_; } @@ -2214,11 +2211,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (add_cuts_time > 1.0) { settings_.log.debug("Add cuts time %.2f seconds\n", add_cuts_time); } - if (add_cuts_status == -2) { - solver_status_ = mip_status_t::TIME_LIMIT; - set_final_solution(solution, root_objective_); - return solver_status_; - } else if (add_cuts_status != 0) { + if (add_cuts_status != 0) { settings_.log.printf("Failed to add cuts\n"); return mip_status_t::NUMERICAL; } diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 2d98badb84..724233b1f0 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -97,7 +97,7 @@ f_t cut_pool_t::cut_orthogonality(i_t i, i_t j) } template -void cut_pool_t::score_cuts(std::vector& x_relax, f_t start_time) +void cut_pool_t::score_cuts(std::vector& x_relax) { const f_t min_cut_distance = 1e-4; cut_distances_.resize(cut_storage_.m, 0.0); @@ -105,11 +105,6 @@ void cut_pool_t::score_cuts(std::vector& x_relax, f_t start_time) const bool verbose = false; for (i_t i = 0; i < cut_storage_.m; i++) { - if (toc(start_time) >= settings_.time_limit) { - best_cuts_.clear(); - scored_cuts_ = 0; - return; - } f_t violation; f_t cut_dist = cut_distance(i, x_relax, violation, cut_norms_[i]); cut_distances_[i] = cut_dist <= min_cut_distance ? 0.0 : cut_dist; @@ -140,7 +135,6 @@ void cut_pool_t::score_cuts(std::vector& x_relax, f_t start_time) } while (scored_cuts_ < max_cuts && !sorted_indices.empty()) { - if (toc(start_time) >= settings_.time_limit) { return; } const i_t i = sorted_indices.back(); sorted_indices.pop_back(); @@ -149,7 +143,6 @@ void cut_pool_t::score_cuts(std::vector& x_relax, f_t start_time) f_t cut_ortho = 1.0; const i_t best_cuts_size = best_cuts_.size(); for (i_t k = 0; k < best_cuts_size; k++) { - if (toc(start_time) >= settings_.time_limit) { return; } const i_t j = best_cuts_[k]; cut_ortho = std::min(cut_ortho, cut_orthogonality(i, j)); } @@ -163,8 +156,7 @@ void cut_pool_t::score_cuts(std::vector& x_relax, f_t start_time) template i_t cut_pool_t::get_best_cuts(csr_matrix_t& best_cuts, std::vector& best_rhs, - std::vector& best_cut_types, - f_t start_time) + std::vector& best_cut_types) { best_cuts.m = 0; best_cuts.n = original_vars_; @@ -179,7 +171,6 @@ i_t cut_pool_t::get_best_cuts(csr_matrix_t& best_cuts, best_cut_types.reserve(scored_cuts_); for (i_t k = 0; k < static_cast(best_cuts_.size()); ++k) { - if (toc(start_time) >= settings_.time_limit) { break; } const i_t i = best_cuts_[k]; sparse_vector_t cut(cut_storage_, i); cut.negate(); @@ -571,46 +562,33 @@ void cut_generation_t::generate_cuts(const lp_problem_t& lp, basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list, - f_t start_time) + const std::vector& nonbasic_list) { - if (toc(start_time) >= settings.time_limit) { return; } - // Generate Gomory and CG Cuts if (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { f_t cut_start_time = tic(); - generate_gomory_cuts(lp, - settings, - Arow, - new_slacks, - var_types, - basis_update, - xstar, - basic_list, - nonbasic_list, - start_time); + generate_gomory_cuts( + lp, settings, Arow, new_slacks, var_types, basis_update, xstar, basic_list, nonbasic_list); f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings.log.debug("Gomory and CG cut generation time %.2f seconds\n", cut_generation_time); } } - if (toc(start_time) >= settings.time_limit) { return; } // Generate Knapsack cuts if (settings.knapsack_cuts != 0) { f_t cut_start_time = tic(); - generate_knapsack_cuts(lp, settings, Arow, new_slacks, var_types, xstar, start_time); + generate_knapsack_cuts(lp, settings, Arow, new_slacks, var_types, xstar); f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings.log.debug("Knapsack cut generation time %.2f seconds\n", cut_generation_time); } } - if (toc(start_time) >= settings.time_limit) { return; } // Generate MIR and CG cuts if (settings.mir_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { f_t cut_start_time = tic(); - generate_mir_cuts(lp, settings, Arow, new_slacks, var_types, xstar, start_time); + generate_mir_cuts(lp, settings, Arow, new_slacks, var_types, xstar); f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings.log.debug("MIR and CG cut generation time %.2f seconds\n", cut_generation_time); @@ -625,12 +603,10 @@ void cut_generation_t::generate_knapsack_cuts( csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar, - f_t start_time) + const std::vector& xstar) { if (knapsack_generation_.num_knapsack_constraints() > 0) { for (i_t knapsack_row : knapsack_generation_.get_knapsack_constraints()) { - if (toc(start_time) >= settings.time_limit) { return; } sparse_vector_t cut(lp.num_cols, 0); f_t cut_rhs; i_t knapsack_status = knapsack_generation_.generate_knapsack_cuts( @@ -647,10 +623,8 @@ void cut_generation_t::generate_mir_cuts( csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar, - f_t start_time) + const std::vector& xstar) { - if (toc(start_time) >= settings.time_limit) { return; } f_t mir_start_time = tic(); mixed_integer_rounding_cut_t mir(lp, settings, new_slacks, xstar); strong_cg_cut_t cg(lp, var_types, xstar); @@ -668,7 +642,6 @@ void cut_generation_t::generate_mir_cuts( // Compute initial scores for all rows std::vector score(lp.num_rows, 0.0); for (i_t i = 0; i < lp.num_rows; i++) { - if (toc(start_time) >= settings.time_limit) { return; } const i_t row_start = Arow.row_start[i]; const i_t row_end = Arow.row_start[i + 1]; @@ -718,7 +691,6 @@ void cut_generation_t::generate_mir_cuts( const i_t max_cuts = std::min(lp.num_rows, 1000); f_t work_estimate = 0.0; for (i_t h = 0; h < max_cuts; h++) { - if (toc(start_time) >= settings.time_limit) { return; } // Get the row with the highest score const i_t i = sorted_indices.back(); sorted_indices.pop_back(); @@ -799,7 +771,6 @@ void cut_generation_t::generate_mir_cuts( work_estimate += lp.num_cols; while (!add_cut && num_aggregated < max_aggregated) { - if (toc(start_time) >= settings.time_limit) { return; } sparse_vector_t transformed_inequality; inequality.squeeze(transformed_inequality); f_t transformed_rhs = inequality_rhs; @@ -1011,15 +982,13 @@ void cut_generation_t::generate_gomory_cuts( basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list, - f_t start_time) + const std::vector& nonbasic_list) { tableau_equality_t tableau(lp, basis_update, nonbasic_list); mixed_integer_rounding_cut_t mir(lp, settings, new_slacks, xstar); strong_cg_cut_t cg(lp, var_types, xstar); for (i_t i = 0; i < lp.num_rows; i++) { - if (toc(start_time) >= settings.time_limit) { return; } sparse_vector_t inequality(lp.num_cols, 0); f_t inequality_rhs; const i_t j = basic_list[i]; @@ -2346,13 +2315,9 @@ i_t add_cuts(const simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, - std::vector& edge_norms, - f_t start_time) + std::vector& edge_norms) { - constexpr i_t time_limit_status = -2; - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } - // Given a set of cuts: C*x <= d that are currently violated // by the current solution x* (i.e. C*x* > d), this function // adds the cuts into the LP and solves again. @@ -2380,7 +2345,6 @@ i_t add_cuts(const simplex_solver_settings_t& settings, settings.log.debug("Original lp rows %d\n", lp.num_rows); settings.log.debug("Original lp cols %d\n", lp.num_cols); - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } csr_matrix_t new_A_row(lp.num_rows, lp.num_cols, 1); lp.A.to_compressed_row(new_A_row); @@ -2390,7 +2354,6 @@ i_t add_cuts(const simplex_solver_settings_t& settings, assert(append_status == 0); } - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } csc_matrix_t new_A_col(lp.num_rows + p, lp.num_cols, 1); new_A_row.to_compressed_col(new_A_col); @@ -2445,7 +2408,6 @@ i_t add_cuts(const simplex_solver_settings_t& settings, settings.log.debug("Done adding rhs\n"); // Construct C_B = C(:, basic_list) - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } std::vector C_col_degree(lp.num_cols, 0); i_t cuts_nz = cuts.row_start[p]; for (i_t q = 0; q < cuts_nz; q++) { @@ -2475,7 +2437,6 @@ i_t add_cuts(const simplex_solver_settings_t& settings, } settings.log.debug("Done estimating C_B_nz\n"); - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } csr_matrix_t C_B(p, num_basic, C_B_nz); nz = 0; for (i_t i = 0; i < p; i++) { @@ -2500,7 +2461,6 @@ i_t add_cuts(const simplex_solver_settings_t& settings, settings.log.debug("C_B rows %d cols %d nz %d\n", C_B.m, C_B.n, nz); // Adjust the basis update to include the new cuts - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } basis_update.append_cuts(C_B); basic_list.resize(lp.num_rows, 0); @@ -2538,7 +2498,6 @@ i_t add_cuts(const simplex_solver_settings_t& settings, solution.y.resize(lp.num_rows, 0.0); solution.z.resize(lp.num_cols, 0.0); - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } return 0; } @@ -2559,9 +2518,6 @@ i_t remove_cuts(lp_problem_t& lp, basis_update_mpf_t& basis_update, f_t start_time) { - constexpr i_t time_limit_status = -2; - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } - std::vector cuts_to_remove; cuts_to_remove.reserve(lp.num_rows - original_rows); std::vector slacks_to_remove; @@ -2570,7 +2526,6 @@ i_t remove_cuts(lp_problem_t& lp, std::vector is_slack(lp.num_cols, 0); for (i_t j : new_slacks) { - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } is_slack[j] = 1; #ifdef CHECK_SLACKS // Check that slack column length is 1 @@ -2585,14 +2540,12 @@ i_t remove_cuts(lp_problem_t& lp, } for (i_t k = original_rows; k < lp.num_rows; k++) { - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } if (std::abs(y[k]) < dual_tol) { const i_t row_start = Arow.row_start[k]; const i_t row_end = Arow.row_start[k + 1]; i_t last_slack = -1; const f_t slack_tol = 1e-3; for (i_t p = row_start; p < row_end; p++) { - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } const i_t j = Arow.j[p]; if (is_slack[j]) { if (vstatus[j] == variable_status_t::BASIC && x[j] > slack_tol) { last_slack = j; } @@ -2606,7 +2559,6 @@ i_t remove_cuts(lp_problem_t& lp, } if (cuts_to_remove.size() > 0) { - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } std::vector marked_rows(lp.num_rows, 0); for (i_t i : cuts_to_remove) { marked_rows[i] = 1; @@ -2620,7 +2572,6 @@ i_t remove_cuts(lp_problem_t& lp, std::vector new_solution_y(lp.num_rows - cuts_to_remove.size()); i_t h = 0; for (i_t i = 0; i < lp.num_rows; i++) { - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } if (!marked_rows[i]) { new_rhs[h] = lp.rhs[i]; new_solution_y[h] = y[i]; @@ -2647,7 +2598,6 @@ i_t remove_cuts(lp_problem_t& lp, std::vector new_is_slacks(lp.num_cols - slacks_to_remove.size(), 0); h = 0; for (i_t k = 0; k < lp.num_cols; k++) { - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } if (!marked_cols[k]) { new_objective[h] = lp.objective[k]; new_lower[h] = lp.lower[k]; @@ -2695,15 +2645,13 @@ i_t remove_cuts(lp_problem_t& lp, lp.num_cols, lp.A.col_start[lp.A.n]); - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } basis_update.resize(lp.num_rows); i_t refactor_status = basis_update.refactor_basis( lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); if (refactor_status == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } - if (refactor_status == TIME_LIMIT_RETURN) { return time_limit_status; } + if (refactor_status == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } } - if (toc(start_time) >= settings.time_limit) { return time_limit_status; } return 0; } @@ -2849,8 +2797,7 @@ template int add_cuts(const simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, - std::vector& edge_norms, - double start_time); + std::vector& edge_norms); template int remove_cuts(lp_problem_t& lp, const simplex_solver_settings_t& settings, diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 17f5e032af..96df6240be 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -138,13 +138,12 @@ class cut_pool_t { // cut'*xstart < rhs void add_cut(cut_type_t cut_type, const sparse_vector_t& cut, f_t rhs); - void score_cuts(std::vector& x_relax, f_t start_time); + void score_cuts(std::vector& x_relax); // We return the cuts in the form best_cuts*x <= best_rhs i_t get_best_cuts(csr_matrix_t& best_cuts, std::vector& best_rhs, - std::vector& best_cut_types, - f_t start_time); + std::vector& best_cut_types); void age_cuts(); @@ -240,8 +239,7 @@ class cut_generation_t { basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list, - f_t start_time); + const std::vector& nonbasic_list); private: // Generate all mixed integer gomory cuts @@ -253,8 +251,7 @@ class cut_generation_t { basis_update_mpf_t& basis_update, const std::vector& xstar, const std::vector& basic_list, - const std::vector& nonbasic_list, - f_t start_time); + const std::vector& nonbasic_list); // Generate all mixed integer rounding cuts void generate_mir_cuts(const lp_problem_t& lp, @@ -262,8 +259,7 @@ class cut_generation_t { csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar, - f_t start_time); + const std::vector& xstar); // Generate all knapsack cuts void generate_knapsack_cuts(const lp_problem_t& lp, @@ -271,8 +267,7 @@ class cut_generation_t { csr_matrix_t& Arow, const std::vector& new_slacks, const std::vector& var_types, - const std::vector& xstar, - f_t start_time); + const std::vector& xstar); cut_pool_t& cut_pool_; knapsack_generation_t knapsack_generation_; @@ -463,8 +458,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, std::vector& basic_list, std::vector& nonbasic_list, std::vector& vstatus, - std::vector& edge_norms, - f_t start_time); + std::vector& edge_norms); template i_t remove_cuts(lp_problem_t& lp, From 0b944d1e01661ae4c39eff7036324f329ee6895a Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 04:17:26 -0800 Subject: [PATCH 054/147] convert lambda to function and remove unnecessary checks --- cpp/src/branch_and_bound/branch_and_bound.cpp | 57 +++++++------------ cpp/src/branch_and_bound/branch_and_bound.hpp | 2 + 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 33a67acbff..6624e42e8a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1946,6 +1946,18 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( return root_status; } +template +bool branch_and_bound_t::stop_for_time_limit(mip_solution_t& solution) +{ + const f_t elapsed = toc(exploration_stats_.start_time); + if (elapsed > settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return true; + } + return false; +}; + template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -2099,19 +2111,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t last_upper_bound = std::numeric_limits::infinity(); f_t last_objective = root_objective_; f_t root_relax_objective = root_objective_; - auto stop_for_time_limit = [&]() -> bool { - const f_t elapsed = toc(exploration_stats_.start_time); - if (elapsed > settings_.time_limit) { - solver_status_ = mip_status_t::TIME_LIMIT; - set_final_solution(solution, root_objective_); - return true; - } - return false; - }; i_t cut_pool_size = 0; for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { - if (stop_for_time_limit()) { return solver_status_; } + if (stop_for_time_limit(solution)) { return solver_status_; } if (num_fractional == 0) { set_solution_at_root(solution, cut_info); return mip_status_t::OPTIMAL; @@ -2128,8 +2131,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } #endif - // Generate cuts and add them to the cut pool - if (stop_for_time_limit()) { return solver_status_; } f_t cut_start_time = tic(); cut_generation.generate_cuts(original_lp_, settings_, @@ -2140,16 +2141,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.x, basic_list, nonbasic_list); - if (stop_for_time_limit()) { return solver_status_; } f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); } // Score the cuts - if (stop_for_time_limit()) { return solver_status_; } f_t score_start_time = tic(); cut_pool.score_cuts(root_relax_soln_.x); - if (stop_for_time_limit()) { return solver_status_; } + if (stop_for_time_limit(solution)) { return solver_status_; } f_t score_time = toc(score_start_time); if (score_time > 1.0) { settings_.log.debug("Cut scoring time %.2f seconds\n", score_time); } // Get the best cuts from the cut pool @@ -2157,7 +2156,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut std::vector cut_rhs; std::vector cut_types; i_t num_cuts = cut_pool.get_best_cuts(cuts_to_add, cut_rhs, cut_types); - if (stop_for_time_limit()) { return solver_status_; } if (num_cuts == 0) { break; } cut_info.record_cut_types(cut_types); #ifdef PRINT_CUT_POOL_TYPES @@ -2190,7 +2188,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut cuts_to_add.m + original_lp_.num_rows); lp_settings.log.log = false; - if (stop_for_time_limit()) { return solver_status_; } f_t add_cuts_start_time = tic(); mutex_original_lp_.lock(); i_t add_cuts_status = add_cuts(settings_, @@ -2206,7 +2203,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut edge_norms_); var_types_.resize(original_lp_.num_cols, variable_type_t::CONTINUOUS); mutex_original_lp_.unlock(); - if (stop_for_time_limit()) { return solver_status_; } f_t add_cuts_time = toc(add_cuts_start_time); if (add_cuts_time > 1.0) { settings_.log.debug("Add cuts time %.2f seconds\n", add_cuts_time); @@ -2238,7 +2234,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #endif original_lp_.A.to_compressed_row(Arow_); - if (stop_for_time_limit()) { return solver_status_; } f_t node_presolve_start_time = tic(); bounds_strengthening_t node_presolve(original_lp_, Arow_, row_sense, var_types_); std::vector new_lower = original_lp_.lower; @@ -2249,7 +2244,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_.lower = new_lower; original_lp_.upper = new_upper; mutex_original_lp_.unlock(); - if (stop_for_time_limit()) { return solver_status_; } f_t node_presolve_time = toc(node_presolve_start_time); if (node_presolve_time > 1.0) { settings_.log.debug("Node presolve time %.2f seconds\n", node_presolve_time); @@ -2262,9 +2256,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t iter = 0; bool initialize_basis = false; lp_settings.concurrent_halt = NULL; - if (stop_for_time_limit()) { return solver_status_; } - f_t dual_phase2_start_time = tic(); - dual::status_t cut_status = dual_phase2_with_advanced_basis(2, + f_t dual_phase2_start_time = tic(); + dual::status_t cut_status = dual_phase2_with_advanced_basis(2, 0, initialize_basis, exploration_stats_.start_time, @@ -2283,15 +2276,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (dual_phase2_time > 1.0) { settings_.log.debug("Dual phase2 time %.2f seconds\n", dual_phase2_time); } - if (stop_for_time_limit()) { return solver_status_; } - if (cut_status == dual::status_t::TIME_LIMIT) { - solver_status_ = mip_status_t::TIME_LIMIT; - set_final_solution(solution, root_objective_); - return solver_status_; - } + + if (stop_for_time_limit(solution)) { return solver_status_; } if (cut_status != dual::status_t::OPTIMAL) { - if (stop_for_time_limit()) { return solver_status_; } settings_.log.printf("Numerical issue at root node. Resolving from scratch\n"); lp_status_t scratch_status = solve_linear_program_with_advanced_basis(original_lp_, @@ -2303,11 +2291,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut nonbasic_list, root_vstatus_, edge_norms_); - if (stop_for_time_limit() || scratch_status == lp_status_t::TIME_LIMIT) { - solver_status_ = mip_status_t::TIME_LIMIT; - set_final_solution(solution, root_objective_); - return solver_status_; - } + if (stop_for_time_limit(solution)) { return solver_status_; } if (scratch_status == lp_status_t::OPTIMAL) { // We recovered cut_status = convert_lp_status_to_dual_status(scratch_status); @@ -2319,7 +2303,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - if (stop_for_time_limit()) { return solver_status_; } f_t remove_cuts_start_time = tic(); mutex_original_lp_.lock(); i_t remove_cuts_status = remove_cuts(original_lp_, @@ -2338,7 +2321,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut basis_update, exploration_stats_.start_time); mutex_original_lp_.unlock(); - if (stop_for_time_limit()) { return solver_status_; } + if (stop_for_time_limit(solution)) { return solver_status_; } f_t remove_cuts_time = toc(remove_cuts_start_time); if (remove_cuts_time > 1.0) { settings_.log.debug("Remove cuts time %.2f seconds\n", remove_cuts_time); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 84ee986c41..a13d5cedcf 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -106,6 +106,8 @@ class branch_and_bound_t { void set_concurrent_lp_root_solve(bool enable) { enable_concurrent_lp_root_solve_ = enable; } + bool stop_for_time_limit(mip_solution_t& solution); + // Repair a low-quality solution from the heuristics. bool repair_solution(const std::vector& leaf_edge_norms, const std::vector& potential_solution, From ba1df82146f2b878db9b34c3e8f0570b5acb5adf Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 04:39:45 -0800 Subject: [PATCH 055/147] fix merge conflicts, reduce timers --- cpp/src/dual_simplex/sparse_matrix.cpp | 7 ++++-- .../diversity/diversity_manager.cu | 11 +++++++++ .../presolve/conflict_graph/clique_table.cu | 5 ++-- .../presolve/conflict_graph/clique_table.cuh | 0 cpp/src/mip_heuristics/problem/problem.cu | 6 ++--- cpp/src/mip_heuristics/solver.cu | 24 +++++++++---------- .../unit_tests/presolve_test.cu | 2 -- 7 files changed, 33 insertions(+), 22 deletions(-) rename cpp/src/{mip => mip_heuristics}/presolve/conflict_graph/clique_table.cu (99%) rename cpp/src/{mip => mip_heuristics}/presolve/conflict_graph/clique_table.cuh (100%) diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index dfce00558b..55013e3b9b 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -685,8 +685,11 @@ void csr_matrix_t::insert_row(const std::vector& vars, this->row_start.push_back(this->row_start.back() + vars.size()); this->m++; this->nz_max += vars.size(); - this->j.insert(this->j.end(), vars.begin(), vars.end()); - this->x.insert(this->x.end(), coeffs.begin(), coeffs.end()); + const i_t old_size = this->j.size(); + this->j.resize(this->j.size() + vars.size()); + std::copy(vars.data(), vars.data() + vars.size(), this->j.underlying().data() + old_size); + this->x.resize(this->x.size() + coeffs.size()); + std::copy(coeffs.data(), coeffs.data() + coeffs.size(), this->x.underlying().data() + old_size); } // x <- x + alpha * A(:, j) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index dc01133c87..dcc1bf1833 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -9,6 +9,7 @@ #include "diversity_manager.cuh" #include +#include #include #include #include @@ -17,6 +18,8 @@ #include +#include + constexpr bool fj_only_run = false; namespace cuopt::linear_programming::detail { @@ -204,6 +207,14 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } + if (!context.settings.heuristics_only && !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + trivial_presolve(*problem_ptr, remap_cache_ids); + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu similarity index 99% rename from cpp/src/mip/presolve/conflict_graph/clique_table.cu rename to cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 912d61f6c5..2c421fc51c 100644 --- a/cpp/src/mip/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -22,8 +22,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -854,7 +854,6 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, t_extend - t_maps, t_remove - t_extend, t_remove); - // exit(0); } #define INSTANTIATE(F_TYPE) \ diff --git a/cpp/src/mip/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh similarity index 100% rename from cpp/src/mip/presolve/conflict_graph/clique_table.cuh rename to cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh diff --git a/cpp/src/mip_heuristics/problem/problem.cu b/cpp/src/mip_heuristics/problem/problem.cu index da8289b365..b3d452b86e 100644 --- a/cpp/src/mip_heuristics/problem/problem.cu +++ b/cpp/src/mip_heuristics/problem/problem.cu @@ -2065,9 +2065,9 @@ void problem_t::set_constraints_from_host_user_problem( empty = (nnz == 0 && n_constraints == 0 && n_variables == 0); auto stream = handle_ptr->get_stream(); - cuopt::device_copy(coefficients, csr_A.x, stream); - cuopt::device_copy(variables, csr_A.j, stream); - cuopt::device_copy(offsets, csr_A.row_start, stream); + cuopt::device_copy(coefficients, csr_A.x.underlying(), stream); + cuopt::device_copy(variables, csr_A.j.underlying(), stream); + cuopt::device_copy(offsets, csr_A.row_start.underlying(), stream); std::vector h_constraint_lower_bounds(n_constraints); std::vector h_constraint_upper_bounds(n_constraints); diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index b1675f2594..159de75a82 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -107,12 +107,16 @@ solution_t mip_solver_t::run_solver() context.problem_ptr->post_process_solution(sol); return sol; } - dm.timer = timer_; - const bool run_presolve = context.settings.presolver != presolver_t::None; - f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC - ? std::numeric_limits::infinity() - : timer_.remaining_time(); - bool presolve_success = run_presolve ? dm.run_presolve(time_limit) : true; + dm.timer = timer_; + const bool run_presolve = context.settings.presolver != presolver_t::None; + f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC + ? std::numeric_limits::infinity() + : timer_.remaining_time(); + double presolve_time_limit = std::min(0.04 * time_limit, 60.0); + presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC + ? std::numeric_limits::infinity() + : presolve_time_limit; + bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); solution_t sol(*context.problem_ptr); @@ -169,12 +173,6 @@ solution_t mip_solver_t::run_solver() namespace dual_simplex = cuopt::linear_programming::dual_simplex; std::future branch_and_bound_status_future; dual_simplex::user_problem_t branch_and_bound_problem(context.problem_ptr->handle_ptr); - context.problem_ptr->recompute_objective_integrality(); - if (context.problem_ptr->is_objective_integral()) { - CUOPT_LOG_INFO("Objective function is integral, scale %g", - context.problem_ptr->presolve_data.objective_scaling_factor); - } - branch_and_bound_problem.objective_is_integral = context.problem_ptr->is_objective_integral(); dual_simplex::simplex_solver_settings_t branch_and_bound_settings; std::unique_ptr> branch_and_bound; branch_and_bound_solution_helper_t solution_helper(&dm, branch_and_bound_settings); @@ -317,9 +315,11 @@ solution_t mip_solver_t::run_solver() if (!is_feasible.value(sol.handle_ptr->get_stream())) { CUOPT_LOG_ERROR( "Solution is not feasible due to variable bounds, returning infeasible solution!"); + context.stats.total_solve_time = timer_.elapsed_time(); context.problem_ptr->post_process_solution(sol); return sol; } + context.stats.total_solve_time = timer_.elapsed_time(); context.problem_ptr->post_process_solution(sol); dm.rins.stop_rins(); return sol; diff --git a/cpp/tests/linear_programming/unit_tests/presolve_test.cu b/cpp/tests/linear_programming/unit_tests/presolve_test.cu index 69fbb17bb0..97dfe0957e 100644 --- a/cpp/tests/linear_programming/unit_tests/presolve_test.cu +++ b/cpp/tests/linear_programming/unit_tests/presolve_test.cu @@ -166,7 +166,6 @@ TEST(pslp_presolve, postsolve_accuracy_afiro) TEST(pslp_presolve, postsolve_dual_accuracy_afiro) { const raft::handle_t handle_{}; - constexpr double tolerance = 1e-5; auto path = make_path_absolute("linear_programming/afiro_original.mps"); auto mps_data_model = cuopt::mps_parser::parse_mps(path, true); @@ -351,7 +350,6 @@ TEST(pslp_presolve, postsolve_reduced_costs) TEST(pslp_presolve, postsolve_multiple_problems) { const raft::handle_t handle_{}; - constexpr double tolerance = 1e-4; std::vector> instances{ {"afiro_original", -464.75314}, From b1b30a7d9918fd037b1df3c4de001fcc9060cffd Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 06:14:48 -0800 Subject: [PATCH 056/147] revert changes --- .../mip/solver_settings.hpp | 2 +- .../diversity/diversity_manager.cu | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index fd57e15ad6..a9e404d14e 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -93,7 +93,7 @@ class mip_solver_settings_t { i_t mir_cuts = -1; i_t mixed_integer_gomory_cuts = -1; i_t knapsack_cuts = -1; - i_t clique_cuts = 0; + i_t clique_cuts = -1; i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index b13cb40719..abfb14637f 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,17 +207,17 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // if (!context.settings.heuristics_only && !problem_ptr->empty) { - // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - // problem_ptr->get_host_user_problem(host_problem); - // std::shared_ptr> clique_table; - // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - // find_initial_cliques( - // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - // problem_ptr->set_constraints_from_host_user_problem(host_problem); - // trivial_presolve(*problem_ptr, remap_cache_ids); - // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - // } + if (!context.settings.heuristics_only && !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + find_initial_cliques( + host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + trivial_presolve(*problem_ptr, remap_cache_ids); + if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From f3956725d9a30a9a29156f0d46c2887b2b0e4ff0 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 06:30:27 -0800 Subject: [PATCH 057/147] fix reverse_iterator --- .../presolve/conflict_graph/clique_table.cu | 22 +++++++++++++++++-- .../restart_strategy/pdlp_restart_strategy.cu | 6 ++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 2c421fc51c..5d93ceac49 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -790,6 +790,7 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, cuopt::timer_t& timer) { cuopt::timer_t stage_timer(std::numeric_limits::infinity()); +#ifdef DEBUG_CLIQUE_TABLE double t_fill = 0.; double t_coeff = 0.; double t_sort = 0.; @@ -798,17 +799,24 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, double t_maps = 0.; double t_extend = 0.; double t_remove = 0.; +#endif std::vector> knapsack_constraints; std::unordered_set set_packing_constraints; dual_simplex::csr_matrix_t A(problem.num_rows, problem.num_cols, 0); problem.A.to_compressed_row(A); fill_knapsack_constraints(problem, knapsack_constraints, A); +#ifdef DEBUG_CLIQUE_TABLE t_fill = stage_timer.elapsed_time(); +#endif make_coeff_positive_knapsack_constraint( problem, knapsack_constraints, set_packing_constraints, tolerances); +#ifdef DEBUG_CLIQUE_TABLE t_coeff = stage_timer.elapsed_time(); +#endif sort_csr_by_constraint_coefficients(knapsack_constraints); +#ifdef DEBUG_CLIQUE_TABLE t_sort = stage_timer.elapsed_time(); +#endif // print_knapsack_constraints(knapsack_constraints); // TODO think about getting min_clique_size according to some problem property clique_config_t clique_config; @@ -821,19 +829,27 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, find_cliques_from_constraint(knapsack_constraint, clique_table); } if (timer.check_time_limit()) { return; } +#ifdef DEBUG_CLIQUE_TABLE t_find = stage_timer.elapsed_time(); +#endif CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d", clique_table.first.size(), clique_table.addtl_cliques.size()); // print_clique_table(clique_table); // remove small cliques and add them to adj_list remove_small_cliques(clique_table); +#ifdef DEBUG_CLIQUE_TABLE t_small = stage_timer.elapsed_time(); +#endif // fill var clique maps fill_var_clique_maps(clique_table); - t_maps = stage_timer.elapsed_time(); +#ifdef DEBUG_CLIQUE_TABLE + t_maps = stage_timer.elapsed_time(); +#endif i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A, timer); - t_extend = stage_timer.elapsed_time(); +#ifdef DEBUG_CLIQUE_TABLE + t_extend = stage_timer.elapsed_time(); +#endif remove_dominated_cliques(problem, A, clique_table, @@ -841,6 +857,7 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, knapsack_constraints, n_extended_cliques, timer); +#ifdef DEBUG_CLIQUE_TABLE t_remove = stage_timer.elapsed_time(); CUOPT_LOG_DEBUG( "Clique table timing (s): fill=%.6f coeff=%.6f sort=%.6f find=%.6f small=%.6f maps=%.6f " @@ -854,6 +871,7 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, t_extend - t_maps, t_remove - t_extend, t_remove); +#endif } #define INSTANTIATE(F_TYPE) \ diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index bc12fb360f..31916e2c32 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -1995,14 +1995,14 @@ void pdlp_restart_strategy_t::solve_bound_constrained_trust_region( f_t* end = threshold_.data() + primal_size_h_ + dual_size_h_; auto highest_negInf_primal = thrust::find(handle_ptr_->get_thrust_policy(), - thrust::make_reverse_iterator(thrust::device_ptr(end)), - thrust::make_reverse_iterator(thrust::device_ptr(start)), + cuda::std::reverse_iterator(thrust::device_ptr(end)), + cuda::std::reverse_iterator(thrust::device_ptr(start)), -std::numeric_limits::infinity()); // Set ranges accordingly i_t index_start_primal = 0; i_t index_end_primal = primal_size_h_ + dual_size_h_; - if (highest_negInf_primal != thrust::make_reverse_iterator(thrust::device_ptr(start))) { + if (highest_negInf_primal != cuda::std::reverse_iterator(thrust::device_ptr(start))) { cuopt_assert(device_to_host_value(thrust::raw_pointer_cast(&*highest_negInf_primal)) == -std::numeric_limits::infinity(), "Incorrect primal reverse iterator"); From cf452ca4d1099aaabd7457948a8f813a8a641fb0 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 07:51:39 -0800 Subject: [PATCH 058/147] wip work units --- cpp/src/cuts/cuts.cpp | 133 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 11 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 118cac9557..ce37466b0a 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -34,9 +34,23 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic f_t bound_tol, f_t min_violation, sparse_vector_t& cut, - f_t& cut_rhs) + f_t& cut_rhs, + f_t* work_estimate, + f_t max_work_estimate) { + auto add_work = [&](f_t accesses) -> bool { + if (work_estimate == nullptr) { return false; } + *work_estimate += accesses; + return *work_estimate > max_work_estimate; + }; + if (clique_vertices.size() < 2) { return clique_cut_build_status_t::NO_CUT; } + const f_t clique_size = static_cast(clique_vertices.size()); + // Coarse function-level estimate: + // validate/transform vertices + duplicate checks + cut construction + sort + violation check + if (add_work(16.0 * clique_size + 4.0 * clique_size * std::log2(clique_size + 1.0))) { + return clique_cut_build_status_t::NO_CUT; + } cuopt_assert(num_vars > 0, "Clique cut num_vars must be positive"); cuopt_assert(static_cast(num_vars) <= lower_bounds.size(), @@ -100,8 +114,29 @@ struct bk_bitset_context_t { f_t start_time; f_t time_limit; size_t words; + f_t* work_estimate; + f_t max_work_estimate; i_t num_calls{0}; + bool work_limit_reached{false}; std::vector> cliques; + + bool add_work(f_t accesses) + { + if (work_estimate == nullptr) { return false; } + *work_estimate += accesses; + if (*work_estimate > max_work_estimate) { + work_limit_reached = true; + return true; + } + return false; + } + + bool over_work_limit() const + { + if (work_limit_reached) { return true; } + if (work_estimate == nullptr) { return false; } + return *work_estimate > max_work_estimate; + } }; inline size_t bitset_words(size_t n) { return (n + 63) / 64; } @@ -147,15 +182,21 @@ void bron_kerbosch(bk_bitset_context_t& ctx, std::vector& X, // already in the clique f_t weight_R) { + if (ctx.over_work_limit()) { return; } if (toc(ctx.start_time) >= ctx.time_limit) { return; } ctx.num_calls++; // stop the recursion, for perf reasons if (ctx.num_calls > ctx.max_calls) { return; } + // Coarse recursive cost: touch call state + frontiers + weight bookkeeping + if (ctx.add_work(static_cast(6 * ctx.words + R.size() + 4))) { return; } // if P and X are empty, we are at maximal clique if (!bitset_any(P) && !bitset_any(X)) { // if the weight is enough, add and exit - if (weight_R >= ctx.min_weight) { ctx.cliques.push_back(R); } + if (weight_R >= ctx.min_weight) { + if (ctx.add_work(static_cast(R.size()))) { return; } + ctx.cliques.push_back(R); + } return; } @@ -163,8 +204,9 @@ void bron_kerbosch(bk_bitset_context_t& ctx, // check if all P is added to clique, would we exceed the weight? if (weight_R + sumP < ctx.min_weight) { return; } - i_t pivot = -1; - i_t max_deg = -1; + i_t pivot = -1; + i_t max_deg = -1; + i_t pivot_vertices_examined = 0; // pivoting rule according to the highest degree vertex // TODO try other pivoting strategies, we can also implement some online learning like MAB for (size_t w = 0; w < ctx.words; ++w) { @@ -173,6 +215,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, uint64_t word = P[w] | X[w]; while (word) { if (toc(ctx.start_time) >= ctx.time_limit) { return; } + pivot_vertices_examined++; // least significant set bit idnex const int bit = __builtin_ctzll(word); // overall vertex index @@ -194,6 +237,11 @@ void bron_kerbosch(bk_bitset_context_t& ctx, } } } + // Coarse cost of pivot scan: for each visited vertex, scan one adjacency row over all words. + if (ctx.add_work(static_cast(pivot_vertices_examined) * + static_cast(2 * ctx.words + 4))) { + return; + } std::vector candidates; candidates.reserve(ctx.weights.size()); @@ -210,6 +258,13 @@ void bron_kerbosch(bk_bitset_context_t& ctx, candidates.push_back(v); } } + const i_t num_candidates = static_cast(candidates.size()); + // Coarse cost of candidate extraction from P \ N(pivot) + if (ctx.add_work(static_cast(ctx.words + 3 * num_candidates))) { return; } + // Coarse cost for all branch setups in this recursion frame. + if (ctx.add_work(static_cast(num_candidates) * static_cast(4 * ctx.words + 8))) { + return; + } // note that candidates will include pivot if it is in P for (auto v : candidates) { @@ -222,6 +277,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, X_next[k] = X[k] & ctx.adj[v][k]; } bron_kerbosch(ctx, R, P_next, X_next, weight_R + ctx.weights[v]); + if (ctx.over_work_limit()) { return; } R.pop_back(); bitset_clear(P, static_cast(v)); bitset_set(X, static_cast(v)); @@ -236,10 +292,19 @@ void extend_clique_vertices(std::vector& clique_vertices, i_t num_vars, f_t integer_tol, f_t start_time, - f_t time_limit) + f_t time_limit, + f_t* work_estimate, + f_t max_work_estimate) { + auto add_work = [&](f_t accesses) -> bool { + if (work_estimate == nullptr) { return false; } + *work_estimate += accesses; + return *work_estimate > max_work_estimate; + }; + if (toc(start_time) >= time_limit) { return; } if (clique_vertices.empty()) { return; } + const f_t initial_clique_size = static_cast(clique_vertices.size()); i_t smallest_degree = std::numeric_limits::max(); i_t smallest_degree_var = -1; @@ -264,6 +329,15 @@ void extend_clique_vertices(std::vector& clique_vertices, f_t value = candidate >= num_vars ? (1.0 - xstar[var_idx]) : xstar[var_idx]; if (std::abs(value - std::round(value)) <= integer_tol) { candidates.push_back(candidate); } } + const f_t candidate_size = static_cast(candidates.size()); + const f_t sort_work = + candidate_size > 0.0 ? 6.0 * candidate_size * std::log2(candidate_size + 1.0) : 0.0; + // Coarse function-level estimate: + // degree scan + candidate filtering + sort + extension checks + const f_t estimated_extension_work = + 2.0 * initial_clique_size + 4.0 * static_cast(adj_set.size()) + sort_work + + 2.0 * candidate_size * initial_clique_size + 2.0 * candidate_size; + if (add_work(estimated_extension_work)) { return; } // sort the candidates by reduced cost. // smaller reduce cost disturbs dual simplex less @@ -988,7 +1062,9 @@ bool cut_generation_t::generate_clique_cuts( const f_t bound_tol = settings.primal_tol; const f_t min_weight = 1.0 + min_violation; // TODO this can be problem dependent - const i_t max_calls = 100000; + const i_t max_calls = 100000; + f_t work_estimate = 0.0; + const f_t max_work_estimate = 2e9; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); @@ -1012,6 +1088,9 @@ bool cut_generation_t::generate_clique_cuts( vertices.push_back(j + num_vars); weights.push_back(1.0 - xj); } + // Coarse loop estimate: variable scans + selected vertex/weight writes + work_estimate += 4.0 * static_cast(num_vars) + 2.0 * static_cast(vertices.size()); + if (work_estimate > max_work_estimate) { return true; } if (vertices.empty()) { return true; } @@ -1022,13 +1101,18 @@ bool cut_generation_t::generate_clique_cuts( vertex_to_local[vertices[idx]] = static_cast(idx); in_subgraph[vertices[idx]] = 1; } + work_estimate += 3.0 * static_cast(vertices.size()); + if (work_estimate > max_work_estimate) { return true; } std::vector> adj_local(vertices.size()); + size_t total_adj_entries = 0; + size_t kept_adj_entries = 0; for (size_t idx = 0; idx < vertices.size(); ++idx) { if (toc(start_time) >= settings.time_limit) { return true; } i_t vertex_idx = vertices[idx]; auto adj_set = clique_table_->get_adj_set_of_var(vertex_idx); - auto& adj = adj_local[idx]; + total_adj_entries += adj_set.size(); + auto& adj = adj_local[idx]; adj.reserve(adj_set.size() + 1); for (const auto neighbor : adj_set) { if (toc(start_time) >= settings.time_limit) { return true; } @@ -1038,6 +1122,7 @@ bool cut_generation_t::generate_clique_cuts( cuopt_assert(local_neighbor >= 0, "Local neighbor out of range"); adj.push_back(local_neighbor); } + kept_adj_entries += adj.size(); #ifdef ASSERT_MODE { std::unordered_set adj_check; @@ -1048,11 +1133,16 @@ bool cut_generation_t::generate_clique_cuts( } #endif } + work_estimate += static_cast(vertices.size()) + static_cast(total_adj_entries) + + 2.0 * static_cast(kept_adj_entries); + if (work_estimate > max_work_estimate) { return true; } const size_t words = bitset_words(vertices.size()); std::vector> adj_bitset(vertices.size(), std::vector(words, 0)); + size_t local_adj_entries = 0; for (size_t v = 0; v < adj_local.size(); ++v) { if (toc(start_time) >= settings.time_limit) { return true; } + local_adj_entries += adj_local[v].size(); for (const auto neighbor : adj_local[v]) { if (toc(start_time) >= settings.time_limit) { return true; } if (neighbor >= 0 && static_cast(neighbor) < vertices.size()) { @@ -1060,17 +1150,30 @@ bool cut_generation_t::generate_clique_cuts( } } } + work_estimate += static_cast(adj_local.size()) + 3.0 * static_cast(local_adj_entries); + if (work_estimate > max_work_estimate) { return true; } - bk_bitset_context_t ctx{ - adj_bitset, weights, min_weight, max_calls, start_time, settings.time_limit, words}; + bk_bitset_context_t ctx{adj_bitset, + weights, + min_weight, + max_calls, + start_time, + settings.time_limit, + words, + &work_estimate, + max_work_estimate}; std::vector R; std::vector P(words, 0); std::vector X(words, 0); for (size_t idx = 0; idx < vertices.size(); ++idx) { bitset_set(P, idx); } + work_estimate += 2.0 * static_cast(vertices.size()); + if (work_estimate > max_work_estimate) { return true; } bron_kerbosch(ctx, R, P, X, 0.0); + if (ctx.over_work_limit()) { return true; } if (toc(start_time) >= settings.time_limit) { return true; } + if (work_estimate > max_work_estimate) { return true; } sparse_vector_t cut(lp.num_cols, 0); f_t cut_rhs = 0.0; @@ -1081,6 +1184,8 @@ bool cut_generation_t::generate_clique_cuts( for (auto local_idx : clique_local) { clique_vertices.push_back(vertices[local_idx]); } + work_estimate += 3.0 * static_cast(clique_local.size()) + 1.0; + if (work_estimate > max_work_estimate) { return true; } extend_clique_vertices(clique_vertices, *clique_table_, xstar, @@ -1088,7 +1193,10 @@ bool cut_generation_t::generate_clique_cuts( num_vars, settings.integer_tol, start_time, - settings.time_limit); + settings.time_limit, + &work_estimate, + max_work_estimate); + if (work_estimate > max_work_estimate) { return true; } if (toc(start_time) >= settings.time_limit) { return true; } const auto build_status = build_clique_cut(clique_vertices, num_vars, @@ -1099,7 +1207,10 @@ bool cut_generation_t::generate_clique_cuts( bound_tol, min_violation, cut, - cut_rhs); + cut_rhs, + &work_estimate, + max_work_estimate); + if (work_estimate > max_work_estimate) { return true; } if (build_status == clique_cut_build_status_t::INFEASIBLE) { settings.log.debug("Detected contradictory variable/complement clique\n"); return false; From 71b7f2f41247b6b0bcec382457f7cfeb694cc6b7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 08:01:00 -0800 Subject: [PATCH 059/147] fix thrust changes --- cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu index bc12fb360f..31916e2c32 100644 --- a/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu +++ b/cpp/src/pdlp/restart_strategy/pdlp_restart_strategy.cu @@ -1995,14 +1995,14 @@ void pdlp_restart_strategy_t::solve_bound_constrained_trust_region( f_t* end = threshold_.data() + primal_size_h_ + dual_size_h_; auto highest_negInf_primal = thrust::find(handle_ptr_->get_thrust_policy(), - thrust::make_reverse_iterator(thrust::device_ptr(end)), - thrust::make_reverse_iterator(thrust::device_ptr(start)), + cuda::std::reverse_iterator(thrust::device_ptr(end)), + cuda::std::reverse_iterator(thrust::device_ptr(start)), -std::numeric_limits::infinity()); // Set ranges accordingly i_t index_start_primal = 0; i_t index_end_primal = primal_size_h_ + dual_size_h_; - if (highest_negInf_primal != thrust::make_reverse_iterator(thrust::device_ptr(start))) { + if (highest_negInf_primal != cuda::std::reverse_iterator(thrust::device_ptr(start))) { cuopt_assert(device_to_host_value(thrust::raw_pointer_cast(&*highest_negInf_primal)) == -std::numeric_limits::infinity(), "Incorrect primal reverse iterator"); From 82b2d64408abcaced475e47fda80df332ff48d9c Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 18 Feb 2026 02:12:13 -0800 Subject: [PATCH 060/147] handle review comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 10 ++++++++-- cpp/src/cuts/cuts.cpp | 3 +-- cpp/src/dual_simplex/crossover.cpp | 9 ++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 6624e42e8a..8c32f54940 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -1893,7 +1894,7 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( if (refactor_status == TIME_LIMIT_RETURN) { root_status = lp_status_t::TIME_LIMIT; } else if (refactor_status == CONCURRENT_HALT_RETURN) { - root_status = lp_status_t::TIME_LIMIT; + root_status = lp_status_t::CONCURRENT_LIMIT; } else if (refactor_status != 0) { settings_.log.printf("Failed to refactor basis. %d deficient columns.\n", refactor_status); assert(refactor_status == 0); @@ -2373,7 +2374,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); - if (toc(exploration_stats_.start_time) < settings_.time_limit) { + if (toc(exploration_stats_.start_time) >= settings_.time_limit) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } + { raft::common::nvtx::range scope_sb("BB::strong_branching"); strong_branching(original_problem_, original_lp_, diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 724233b1f0..fab6297deb 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -170,8 +170,7 @@ i_t cut_pool_t::get_best_cuts(csr_matrix_t& best_cuts, best_cut_types.clear(); best_cut_types.reserve(scored_cuts_); - for (i_t k = 0; k < static_cast(best_cuts_.size()); ++k) { - const i_t i = best_cuts_[k]; + for (i_t i : best_cuts_) { sparse_vector_t cut(cut_storage_, i); cut.negate(); best_cuts.append_row(cut); diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 65da8d45c0..626b150358 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -1227,7 +1227,6 @@ crossover_status_t crossover(const lp_problem_t& lp, rank = factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); - if (rank == CONCURRENT_HALT_RETURN) { return crossover_status_t::CONCURRENT_LIMIT; } if (rank < 0) { return return_to_status(rank); } if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); @@ -1399,9 +1398,7 @@ crossover_status_t crossover(const lp_problem_t& lp, get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); rank = factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); - if (rank == CONCURRENT_HALT_RETURN) { - return crossover_status_t::CONCURRENT_LIMIT; - } else if (rank < 0) { + if (rank < 0) { return return_to_status(rank); } else if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); @@ -1417,9 +1414,7 @@ crossover_status_t crossover(const lp_problem_t& lp, vstatus); rank = factorize_basis( lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); - if (rank == CONCURRENT_HALT_RETURN) { - return crossover_status_t::CONCURRENT_LIMIT; - } else if (rank < 0) { + if (rank < 0) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); return return_to_status(rank); } else { From d074884ffc38aefe663cd774d207d441026e2cfd Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 18 Feb 2026 05:29:44 -0800 Subject: [PATCH 061/147] add extension heuristics and fix mip gap issues --- .../presolve/conflict_graph/clique_table.cu | 152 +++++++++++++++--- cpp/src/mip_heuristics/utils.cuh | 5 +- 2 files changed, 130 insertions(+), 27 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 5d93ceac49..ed1e82cd31 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -189,8 +189,11 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro } // greater than part: convert it to less than knapsack_constraint_t knapsack_constraint2; - knapsack_constraint2.cstr_idx = A.m + added_constraints++; - knapsack_constraint2.rhs = -problem.rhs[i]; + // Mark synthetic rows from equality splitting with negative ids so they never alias real row + // indices (including rows appended later by clique extension). + knapsack_constraint2.cstr_idx = -(added_constraints + 1); + added_constraints++; + knapsack_constraint2.rhs = -problem.rhs[i]; for (i_t j = constraint_range.first; j < constraint_range.second; j++) { knapsack_constraint2.entries.push_back({A.j[j], -A.x[j]}); } @@ -378,8 +381,13 @@ bool extend_clique(const std::vector& clique, clique_table_t& clique_table, dual_simplex::user_problem_t& problem, dual_simplex::csr_matrix_t& A, - f_t coeff_scale) + f_t coeff_scale, + i_t min_extension_gain, + i_t remaining_rows_budget, + i_t remaining_nnz_budget, + i_t& inserted_row_nnz) { + inserted_row_nnz = 0; i_t smallest_degree = std::numeric_limits::max(); i_t smallest_degree_var = -1; // find smallest degree vertex in the current set packing constraint @@ -436,6 +444,8 @@ bool extend_clique(const std::vector& clique, n_of_complement_conflicts, complement_conflict_var); cuopt_assert(n_of_complement_conflicts == 1, "There can only be one complement conflict"); + // Keep the discovered extension in the clique table for downstream dominance checks. + clique_table.first.push_back(new_clique); // fix all other variables other than complementing var for (size_t i = 0; i < new_clique.size(); i++) { if (new_clique[i] % clique_table.n_variables != complement_conflict_var) { @@ -454,14 +464,23 @@ bool extend_clique(const std::vector& clique, } } } - return false; + return true; } else { + // Keep the discovered extension in the clique table even when row insertion is skipped by + // row/nnz budgets. clique_table.first.push_back(new_clique); #if DEBUG_KNAPSACK_CONSTRAINTS CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); #endif + i_t extension_gain = static_cast(new_clique.size() - clique.size()); + if (extension_gain < min_extension_gain) { return true; } + if (remaining_rows_budget <= 0 || + remaining_nnz_budget < static_cast(new_clique.size())) { + return true; + } // insert the new clique into the problem as a new constraint insert_clique_into_problem(new_clique, problem, A, coeff_scale); + inserted_row_nnz = static_cast(new_clique.size()); } } return new_clique.size() > clique.size(); @@ -477,19 +496,100 @@ i_t extend_cliques(const std::vector>& knapsack_ dual_simplex::csr_matrix_t& A, cuopt::timer_t& timer) { - i_t n_extended_cliques = 0; - // we try extending cliques on set packing constraints - for (const auto& knapsack_constraint : knapsack_constraints) { + constexpr i_t min_extension_gain = 2; + constexpr i_t extension_yield_window = 64; + constexpr i_t min_successes_per_window = 1; + + i_t base_rows = A.m; + i_t base_nnz = A.row_start[A.m]; + i_t max_added_rows = std::max(8, base_rows / 50); + i_t max_added_nnz = std::max(8 * clique_table.max_clique_size_for_extension, base_nnz / 50); + + i_t added_rows = 0; + i_t added_nnz = 0; + i_t window_attempts = 0; + i_t window_successes = 0; + + CUOPT_LOG_DEBUG("Clique extension heuristics: min_gain=%d row_budget=%d nnz_budget=%d", + min_extension_gain, + max_added_rows, + max_added_nnz); + struct extension_candidate_t { + i_t knapsack_idx; + i_t estimated_gain; + i_t clique_size; + }; + std::vector extension_worklist; + extension_worklist.reserve(knapsack_constraints.size()); + for (i_t knapsack_idx = 0; knapsack_idx < static_cast(knapsack_constraints.size()); + knapsack_idx++) { if (timer.check_time_limit()) { break; } + const auto& knapsack_constraint = knapsack_constraints[knapsack_idx]; if (!knapsack_constraint.is_set_packing) { continue; } - if (knapsack_constraint.entries.size() < (size_t)clique_table.max_clique_size_for_extension) { - std::vector clique; - for (const auto& entry : knapsack_constraint.entries) { - clique.push_back(entry.col); + i_t clique_size = static_cast(knapsack_constraint.entries.size()); + if (clique_size >= clique_table.max_clique_size_for_extension) { continue; } + i_t smallest_degree = std::numeric_limits::max(); + for (const auto& entry : knapsack_constraint.entries) { + smallest_degree = std::min(smallest_degree, clique_table.get_degree_of_var(entry.col)); + } + // The smallest-degree vertex upper-bounds how many new literals can be added. + i_t estimated_gain = std::max(0, smallest_degree - (clique_size - 1)); + if (estimated_gain < min_extension_gain) { continue; } + extension_worklist.push_back({knapsack_idx, estimated_gain, clique_size}); + } + std::stable_sort(extension_worklist.begin(), + extension_worklist.end(), + [](const extension_candidate_t& a, const extension_candidate_t& b) { + if (a.estimated_gain != b.estimated_gain) { + return a.estimated_gain > b.estimated_gain; + } + if (a.clique_size != b.clique_size) { return a.clique_size < b.clique_size; } + return a.knapsack_idx < b.knapsack_idx; + }); + CUOPT_LOG_DEBUG("Clique extension candidates after scoring: %zu", extension_worklist.size()); + + i_t n_extended_cliques = 0; + // Try highest estimated gain candidates first so budget is spent on promising rows. + for (const auto& candidate : extension_worklist) { + if (timer.check_time_limit()) { break; } + if (added_rows >= max_added_rows || added_nnz >= max_added_nnz) { + CUOPT_LOG_DEBUG( + "Stopping clique extension: budget reached (rows=%d nnz=%d)", added_rows, added_nnz); + break; + } + window_attempts++; + const auto& knapsack_constraint = knapsack_constraints[candidate.knapsack_idx]; + std::vector clique; + for (const auto& entry : knapsack_constraint.entries) { + clique.push_back(entry.col); + } + i_t inserted_row_nnz = 0; + f_t coeff_scale = knapsack_constraint.entries[0].val; + bool extended_clique = extend_clique(clique, + clique_table, + problem, + A, + coeff_scale, + min_extension_gain, + max_added_rows - added_rows, + max_added_nnz - added_nnz, + inserted_row_nnz); + if (extended_clique) { + n_extended_cliques++; + window_successes++; + if (inserted_row_nnz > 0) { + added_rows++; + added_nnz += inserted_row_nnz; + } + } + if (window_attempts >= extension_yield_window) { + if (window_successes < min_successes_per_window) { + CUOPT_LOG_DEBUG( + "Stopping clique extension: low yield (%d/%d)", window_successes, window_attempts); + break; } - f_t coeff_scale = knapsack_constraint.entries[0].val; - bool extended_clique = extend_clique(clique, clique_table, problem, A, coeff_scale); - if (extended_clique) { n_extended_cliques++; } + window_attempts = 0; + window_successes = 0; } } // problem.A.check_matrix(); @@ -622,10 +722,10 @@ void remove_dominated_cliques( } } }; - auto find_window_start = [&](long long signature) { - auto it = std::lower_bound( - sp_sigs.begin(), sp_sigs.end(), signature, [](const auto& a, long long value) { - return a.signature < value; + auto find_window_end = [&](long long signature) { + auto it = std::upper_bound( + sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { + return value < a.signature; }); return static_cast(std::distance(sp_sigs.begin(), it)); }; @@ -648,10 +748,12 @@ void remove_dominated_cliques( for (auto v : curr_clique_vars) { signature += static_cast(v); } - size_t start = find_window_start(signature); - size_t end = std::min(sp_sigs.size(), start + dominance_window); - for (size_t idx = start; idx < end; idx++) { - const auto& sp = sp_sigs[idx]; + // Subsets must have signature <= current clique signature. Scan only that side. + size_t end = find_window_end(signature); + size_t start = (end > dominance_window) ? (end - dominance_window) : 0; + for (size_t idx = end; idx > start; idx--) { + size_t cand_idx = idx - 1; + const auto& sp = sp_sigs[cand_idx]; const auto& vars_sp = cstr_vars[sp.knapsack_idx]; if (vars_sp.size() > curr_clique_vars.size()) { continue; } cuopt_assert(std::is_sorted(vars_sp.begin(), vars_sp.end()), @@ -664,12 +766,12 @@ void remove_dominated_cliques( continue; } if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { - CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", - clique_idx, - sp.row_idx); // note that we never deleter set partitioning constraints but it fixes some other // variables if (vars_sp.size() != curr_clique_vars.size()) { + CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", + clique_idx, + sp.row_idx); fix_difference(curr_clique_vars, vars_sp); } } else { diff --git a/cpp/src/mip_heuristics/utils.cuh b/cpp/src/mip_heuristics/utils.cuh index 33712635e9..ffadc1f510 100644 --- a/cpp/src/mip_heuristics/utils.cuh +++ b/cpp/src/mip_heuristics/utils.cuh @@ -339,8 +339,9 @@ static void inline run_device_lambda(const rmm::cuda_stream_view& stream, Func f template f_t compute_rel_mip_gap(f_t user_obj, f_t solution_bound) { - if (user_obj == 0.0) { - return solution_bound == 0.0 ? 0.0 : std::numeric_limits::infinity(); + if (integer_equal(user_obj, 0.0, 1e-6)) { + return integer_equal(solution_bound, 0.0, 1e-6) ? 0.0 + : std::numeric_limits::infinity(); } return std::abs(user_obj - solution_bound) / std::abs(user_obj); } From 0c811732b97d9e945e843c8eaa4647b0172126ae Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 18 Feb 2026 06:04:46 -0800 Subject: [PATCH 062/147] handle review comments --- cpp/src/branch_and_bound/branch_and_bound.cpp | 36 +++++++++---------- cpp/src/cuts/cuts.cpp | 8 ++--- cpp/src/cuts/cuts.hpp | 4 +-- cpp/src/dual_simplex/basis_solves.cpp | 6 ++-- cpp/src/dual_simplex/right_looking_lu.cpp | 14 ++++---- cpp/src/dual_simplex/right_looking_lu.hpp | 4 +-- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 8c32f54940..30b913e6e2 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include @@ -1957,7 +1956,7 @@ bool branch_and_bound_t::stop_for_time_limit(mip_solution_t& return true; } return false; -}; +} template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) @@ -2115,7 +2114,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut i_t cut_pool_size = 0; for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { - if (stop_for_time_limit(solution)) { return solver_status_; } if (num_fractional == 0) { set_solution_at_root(solution, cut_info); return mip_status_t::OPTIMAL; @@ -2132,6 +2130,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } #endif + // Generate cuts and add them to the cut pool f_t cut_start_time = tic(); cut_generation.generate_cuts(original_lp_, settings_, @@ -2306,23 +2305,22 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t remove_cuts_start_time = tic(); mutex_original_lp_.lock(); - i_t remove_cuts_status = remove_cuts(original_lp_, - settings_, - Arow_, - new_slacks_, - original_rows, - var_types_, - root_vstatus_, - edge_norms_, - root_relax_soln_.x, - root_relax_soln_.y, - root_relax_soln_.z, - basic_list, - nonbasic_list, - basis_update, - exploration_stats_.start_time); + remove_cuts(original_lp_, + settings_, + exploration_stats_.start_time, + Arow_, + new_slacks_, + original_rows, + var_types_, + root_vstatus_, + edge_norms_, + root_relax_soln_.x, + root_relax_soln_.y, + root_relax_soln_.z, + basic_list, + nonbasic_list, + basis_update); mutex_original_lp_.unlock(); - if (stop_for_time_limit(solution)) { return solver_status_; } f_t remove_cuts_time = toc(remove_cuts_start_time); if (remove_cuts_time > 1.0) { settings_.log.debug("Remove cuts time %.2f seconds\n", remove_cuts_time); diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index fab6297deb..bbb4d1f15e 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -2503,6 +2503,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, template i_t remove_cuts(lp_problem_t& lp, const simplex_solver_settings_t& settings, + f_t start_time, csr_matrix_t& Arow, std::vector& new_slacks, i_t original_rows, @@ -2514,8 +2515,7 @@ i_t remove_cuts(lp_problem_t& lp, std::vector& z, std::vector& basic_list, std::vector& nonbasic_list, - basis_update_mpf_t& basis_update, - f_t start_time) + basis_update_mpf_t& basis_update) { std::vector cuts_to_remove; cuts_to_remove.reserve(lp.num_rows - original_rows); @@ -2800,6 +2800,7 @@ template int add_cuts(const simplex_solver_settings_t& settings, template int remove_cuts(lp_problem_t& lp, const simplex_solver_settings_t& settings, + double start_time, csr_matrix_t& Arow, std::vector& new_slacks, int original_rows, @@ -2811,8 +2812,7 @@ template int remove_cuts(lp_problem_t& lp, std::vector& z, std::vector& basic_list, std::vector& nonbasic_list, - basis_update_mpf_t& basis_update, - double start_time); + basis_update_mpf_t& basis_update); template void read_saved_solution_for_cut_verification( const lp_problem_t& lp, diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 96df6240be..4f55e96e4d 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -463,6 +463,7 @@ i_t add_cuts(const simplex_solver_settings_t& settings, template i_t remove_cuts(lp_problem_t& lp, const simplex_solver_settings_t& settings, + f_t start_time, csr_matrix_t& Arow, std::vector& new_slacks, i_t original_rows, @@ -474,7 +475,6 @@ i_t remove_cuts(lp_problem_t& lp, std::vector& z, std::vector& basic_list, std::vector& nonbasic_list, - basis_update_mpf_t& basis_update, - f_t start_time); + basis_update_mpf_t& basis_update); } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index 9a8cfc143e..1c17fc557c 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -361,11 +361,11 @@ i_t factorize_basis(const csc_matrix_t& A, settings, settings.threshold_partial_pivoting_tol, identity, + start_time, S_col_perm, SL, SU, - S_perm_inv, - start_time); + S_perm_inv); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return CONCURRENT_HALT_RETURN; } @@ -570,7 +570,7 @@ i_t factorize_basis(const csc_matrix_t& A, } q.resize(m); f_t fact_start = tic(); - rank = right_looking_lu(A, settings, medium_tol, basic_list, q, L, U, pinv, start_time); + rank = right_looking_lu(A, settings, medium_tol, basic_list, start_time, q, L, U, pinv); if (rank < 0) { if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return CONCURRENT_HALT_RETURN; diff --git a/cpp/src/dual_simplex/right_looking_lu.cpp b/cpp/src/dual_simplex/right_looking_lu.cpp index 4c55e1f19e..0f3a9bc2bd 100644 --- a/cpp/src/dual_simplex/right_looking_lu.cpp +++ b/cpp/src/dual_simplex/right_looking_lu.cpp @@ -577,11 +577,11 @@ i_t right_looking_lu(const csc_matrix_t& A, const simplex_solver_settings_t& settings, f_t tol, const VectorI& column_list, + f_t start_time, VectorI& q, csc_matrix_t& L, csc_matrix_t& U, - VectorI& pinv, - f_t start_time) + VectorI& pinv) { raft::common::nvtx::range scope("LU::right_looking_lu"); const i_t n = column_list.size(); @@ -635,10 +635,10 @@ i_t right_looking_lu(const csc_matrix_t& A, i_t pivots = 0; for (i_t k = 0; k < n; ++k) { - if (toc(start_time) > settings.time_limit) { return TIME_LIMIT_RETURN; } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return CONCURRENT_HALT_RETURN; } + if (toc(start_time) > settings.time_limit) { return TIME_LIMIT_RETURN; } // Find pivot that satisfies // abs(pivot) >= abstol, // abs(pivot) >= threshold_tol * max abs[pivot column] @@ -1154,22 +1154,22 @@ template int right_looking_lu>( const simplex_solver_settings_t& settings, double tol, const std::vector& column_list, + double start_time, std::vector& q, csc_matrix_t& L, csc_matrix_t& U, - std::vector& pinv, - double start_time); + std::vector& pinv); template int right_looking_lu>( const csc_matrix_t& A, const simplex_solver_settings_t& settings, double tol, const ins_vector& column_list, + double start_time, ins_vector& q, csc_matrix_t& L, csc_matrix_t& U, - ins_vector& pinv, - double start_time); + ins_vector& pinv); template int right_looking_lu_row_permutation_only( const csc_matrix_t& A, diff --git a/cpp/src/dual_simplex/right_looking_lu.hpp b/cpp/src/dual_simplex/right_looking_lu.hpp index 3f83da42f8..02883ef75a 100644 --- a/cpp/src/dual_simplex/right_looking_lu.hpp +++ b/cpp/src/dual_simplex/right_looking_lu.hpp @@ -19,11 +19,11 @@ i_t right_looking_lu(const csc_matrix_t& A, const simplex_solver_settings_t& settings, f_t tol, const VectorI& column_list, + f_t start_time, VectorI& q, csc_matrix_t& L, csc_matrix_t& U, - VectorI& pinv, - f_t start_time); + VectorI& pinv); template i_t right_looking_lu_row_permutation_only(const csc_matrix_t& A, From 609c578c19312fc5c284892841120e8637f1145e Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 18 Feb 2026 22:15:36 -0800 Subject: [PATCH 063/147] move timer with inout parameters --- cpp/src/branch_and_bound/branch_and_bound.cpp | 54 +++++-------------- cpp/src/cuts/cuts.cpp | 2 +- cpp/src/dual_simplex/basis_solves.cpp | 8 +-- cpp/src/dual_simplex/basis_solves.hpp | 4 +- cpp/src/dual_simplex/basis_updates.cpp | 12 ++--- cpp/src/dual_simplex/basis_updates.hpp | 4 +- cpp/src/dual_simplex/crossover.cpp | 16 +++--- cpp/src/dual_simplex/phase2.cpp | 6 +-- cpp/src/dual_simplex/primal.cpp | 4 +- 9 files changed, 42 insertions(+), 68 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 30b913e6e2..054c7e23a2 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -1886,15 +1886,11 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( root_crossover_settings, original_lp_.lower, original_lp_.upper, + exploration_stats_.start_time, basic_list, nonbasic_list, - crossover_vstatus_, - exploration_stats_.start_time); - if (refactor_status == TIME_LIMIT_RETURN) { - root_status = lp_status_t::TIME_LIMIT; - } else if (refactor_status == CONCURRENT_HALT_RETURN) { - root_status = lp_status_t::CONCURRENT_LIMIT; - } else if (refactor_status != 0) { + crossover_vstatus_); + if (refactor_status != 0) { settings_.log.printf("Failed to refactor basis. %d deficient columns.\n", refactor_status); assert(refactor_status == 0); root_status = lp_status_t::NUMERICAL_ISSUES; @@ -1906,15 +1902,6 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( user_objective = root_crossover_soln_.user_objective; iter = root_crossover_soln_.iterations; solver_name = "Barrier/PDLP and Crossover"; - } else if (crossover_status == crossover_status_t::TIME_LIMIT || - toc(exploration_stats_.start_time) > settings_.time_limit) { - set_root_concurrent_halt(1); - root_status = root_status_future.get(); - set_root_concurrent_halt(0); - root_status = lp_status_t::TIME_LIMIT; - user_objective = root_relax_soln_.user_objective; - iter = root_relax_soln_.iterations; - solver_name = "Dual Simplex"; } else { root_status = root_status_future.get(); user_objective = root_relax_soln_.user_objective; @@ -1946,18 +1933,6 @@ lp_status_t branch_and_bound_t::solve_root_relaxation( return root_status; } -template -bool branch_and_bound_t::stop_for_time_limit(mip_solution_t& solution) -{ - const f_t elapsed = toc(exploration_stats_.start_time); - if (elapsed > settings_.time_limit) { - solver_status_ = mip_status_t::TIME_LIMIT; - set_final_solution(solution, root_objective_); - return true; - } - return false; -} - template mip_status_t branch_and_bound_t::solve(mip_solution_t& solution) { @@ -2029,9 +2004,12 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); - if (settings_.heuristic_preemption_callback != nullptr) { - settings_.heuristic_preemption_callback(); - } + // FIXME: rarely dual simplex detects infeasible whereas it is feasible. + // to add a small safety net, check if there is a primal solution already. + // Uncomment this if the issue with cost266-UUE is resolved + // if (settings.heuristic_preemption_callback != nullptr) { + // settings.heuristic_preemption_callback(); + // } return mip_status_t::INFEASIBLE; } if (root_status == lp_status_t::UNBOUNDED) { @@ -2148,7 +2126,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut // Score the cuts f_t score_start_time = tic(); cut_pool.score_cuts(root_relax_soln_.x); - if (stop_for_time_limit(solution)) { return solver_status_; } f_t score_time = toc(score_start_time); if (score_time > 1.0) { settings_.log.debug("Cut scoring time %.2f seconds\n", score_time); } // Get the best cuts from the cut pool @@ -2276,8 +2253,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (dual_phase2_time > 1.0) { settings_.log.debug("Dual phase2 time %.2f seconds\n", dual_phase2_time); } - - if (stop_for_time_limit(solution)) { return solver_status_; } + if (cut_status == dual::status_t::TIME_LIMIT) { + solver_status_ = mip_status_t::TIME_LIMIT; + set_final_solution(solution, root_objective_); + return solver_status_; + } if (cut_status != dual::status_t::OPTIMAL) { settings_.log.printf("Numerical issue at root node. Resolving from scratch\n"); @@ -2291,7 +2271,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut nonbasic_list, root_vstatus_, edge_norms_); - if (stop_for_time_limit(solution)) { return solver_status_; } if (scratch_status == lp_status_t::OPTIMAL) { // We recovered cut_status = convert_lp_status_to_dual_status(scratch_status); @@ -2372,11 +2351,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); - if (toc(exploration_stats_.start_time) >= settings_.time_limit) { - solver_status_ = mip_status_t::TIME_LIMIT; - set_final_solution(solution, root_objective_); - return solver_status_; - } { raft::common::nvtx::range scope_sb("BB::strong_branching"); strong_branching(original_problem_, diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index bbb4d1f15e..ad1d34f7aa 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -2646,7 +2646,7 @@ i_t remove_cuts(lp_problem_t& lp, basis_update.resize(lp.num_rows); i_t refactor_status = basis_update.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + lp.A, settings, lp.lower, lp.upper, start_time, basic_list, nonbasic_list, vstatus); if (refactor_status == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } if (refactor_status == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } } diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index 1c17fc557c..2189dd57b1 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -159,14 +159,14 @@ template i_t factorize_basis(const csc_matrix_t& A, const simplex_solver_settings_t& settings, const std::vector& basic_list, + f_t start_time, csc_matrix_t& L, csc_matrix_t& U, std::vector& p, std::vector& pinv, std::vector& q, std::vector& deficient, - std::vector& slacks_needed, - f_t start_time) + std::vector& slacks_needed) { raft::common::nvtx::range scope("LU::factorize_basis"); const i_t m = basic_list.size(); @@ -875,14 +875,14 @@ template void get_basis_from_vstatus(int m, template int factorize_basis(const csc_matrix_t& A, const simplex_solver_settings_t& settings, const std::vector& basis_list, + double start_time, csc_matrix_t& L, csc_matrix_t& U, std::vector& p, std::vector& pinv, std::vector& q, std::vector& deficient, - std::vector& slacks_needed, - double start_time); + std::vector& slacks_needed); template int basis_repair(const csc_matrix_t& A, const simplex_solver_settings_t& settings, diff --git a/cpp/src/dual_simplex/basis_solves.hpp b/cpp/src/dual_simplex/basis_solves.hpp index 13227a8324..3050e791e4 100644 --- a/cpp/src/dual_simplex/basis_solves.hpp +++ b/cpp/src/dual_simplex/basis_solves.hpp @@ -30,14 +30,14 @@ template i_t factorize_basis(const csc_matrix_t& A, const simplex_solver_settings_t& settings, const std::vector& basis_list, + f_t start_time, csc_matrix_t& L, csc_matrix_t& U, std::vector& p, std::vector& pinv, std::vector& q, std::vector& deficient, - std::vector& slacks_need, - f_t start_time); + std::vector& slacks_need); // Repair the basis by bringing in slacks template diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 5f8ef49343..7814d57e78 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -2262,10 +2262,10 @@ int basis_update_mpf_t::refactor_basis( const simplex_solver_settings_t& settings, const std::vector& lower, const std::vector& upper, + f_t start_time, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& vstatus, - f_t start_time) + std::vector& vstatus) { raft::common::nvtx::range scope("LU::refactor_basis"); std::vector deficient; @@ -2277,14 +2277,14 @@ int basis_update_mpf_t::refactor_basis( i_t status = factorize_basis(A, settings, basic_list, + start_time, L0_, U0_, row_permutation_, inverse_row_permutation_, q, deficient, - slacks_needed, - start_time); + slacks_needed); if (status == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } if (status == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } if (status == -1) { @@ -2320,14 +2320,14 @@ int basis_update_mpf_t::refactor_basis( status = factorize_basis(A, settings, basic_list, + start_time, L0_, U0_, row_permutation_, inverse_row_permutation_, q, deficient, - slacks_needed, - start_time); + slacks_needed); if (status == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } if (status == TIME_LIMIT_RETURN) { return TIME_LIMIT_RETURN; } if (status == -1) { diff --git a/cpp/src/dual_simplex/basis_updates.hpp b/cpp/src/dual_simplex/basis_updates.hpp index 6fd7b13f34..8c4c6da291 100644 --- a/cpp/src/dual_simplex/basis_updates.hpp +++ b/cpp/src/dual_simplex/basis_updates.hpp @@ -380,10 +380,10 @@ class basis_update_mpf_t { const simplex_solver_settings_t& settings, const std::vector& lower, const std::vector& upper, + f_t start_time, std::vector& basic_list, std::vector& nonbasic_list, - std::vector& vstatus, - f_t start_time); + std::vector& vstatus); void set_refactor_frequency(i_t new_frequency) { refactor_frequency_ = new_frequency; } diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 626b150358..b044a86783 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -506,7 +506,7 @@ i_t dual_push(const lp_problem_t& lp, std::vector deficient; std::vector slacks_needed; i_t rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } else if (rank < 0) { @@ -524,7 +524,7 @@ i_t dual_push(const lp_problem_t& lp, superbasic_list, vstatus); rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } else if (rank < 0) { @@ -862,7 +862,7 @@ i_t primal_push(const lp_problem_t& lp, std::vector deficient; std::vector slacks_needed; i_t rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } else if (rank < 0) { @@ -883,7 +883,7 @@ i_t primal_push(const lp_problem_t& lp, find_primal_superbasic_variables( lp, settings, solution, solution, vstatus, nonbasic_list, superbasic_list); rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank == CONCURRENT_HALT_RETURN) { return CONCURRENT_HALT_RETURN; } else if (rank < 0) { @@ -1226,7 +1226,7 @@ crossover_status_t crossover(const lp_problem_t& lp, std::vector slacks_needed; rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank < 0) { return return_to_status(rank); } if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); @@ -1241,7 +1241,7 @@ crossover_status_t crossover(const lp_problem_t& lp, superbasic_list, vstatus); rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank == CONCURRENT_HALT_RETURN) { return crossover_status_t::CONCURRENT_LIMIT; } else if (rank < 0) { @@ -1397,7 +1397,7 @@ crossover_status_t crossover(const lp_problem_t& lp, superbasic_list.clear(); get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank < 0) { return return_to_status(rank); } else if (rank != m) { @@ -1413,7 +1413,7 @@ crossover_status_t crossover(const lp_problem_t& lp, superbasic_list, vstatus); rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank < 0) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); return return_to_status(rank); diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 25d1c5fbe5..5de12cc8cf 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2505,7 +2505,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, assert(nonbasic_list.size() == n - m); i_t refactor_status = ft.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + lp.A, settings, lp.lower, lp.upper, start_time, basic_list, nonbasic_list, vstatus); if (refactor_status == CONCURRENT_HALT_RETURN) { return dual::status_t::CONCURRENT_LIMIT; } if (refactor_status == TIME_LIMIT_RETURN) { return dual::status_t::TIME_LIMIT; } if (refactor_status > 0) { return dual::status_t::NUMERICAL; } @@ -3373,7 +3373,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, num_refactors++; bool should_recompute_x = false; i_t refactor_status = ft.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + lp.A, settings, lp.lower, lp.upper, start_time, basic_list, nonbasic_list, vstatus); if (refactor_status == CONCURRENT_HALT_RETURN) { return dual::status_t::CONCURRENT_LIMIT; } if (refactor_status == TIME_LIMIT_RETURN) { return dual::status_t::TIME_LIMIT; } if (refactor_status > 0) { @@ -3384,7 +3384,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, i_t deficient_size = 0; while (true) { deficient_size = ft.refactor_basis( - lp.A, settings, lp.lower, lp.upper, basic_list, nonbasic_list, vstatus, start_time); + lp.A, settings, lp.lower, lp.upper, start_time, basic_list, nonbasic_list, vstatus); if (deficient_size == CONCURRENT_HALT_RETURN) { return dual::status_t::CONCURRENT_LIMIT; } diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 2d0944542b..628fadcbe7 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -295,7 +295,7 @@ primal::status_t primal_phase2(i_t phase, std::vector deficient; std::vector slacks_needed; i_t rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank == CONCURRENT_HALT_RETURN) { return primal::status_t::CONCURRENT_LIMIT; } else if (rank == TIME_LIMIT_RETURN) { @@ -316,7 +316,7 @@ primal::status_t primal_phase2(i_t phase, superbasic_list, vstatus); rank = factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed, start_time); + lp.A, settings, basic_list, start_time, L, U, p, pinv, q, deficient, slacks_needed); if (rank == CONCURRENT_HALT_RETURN) { return primal::status_t::CONCURRENT_LIMIT; } else if (rank == TIME_LIMIT_RETURN) { From 0c54ecfb3045025bdfb6325fddccf40e8f7532e9 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 18 Feb 2026 22:38:23 -0800 Subject: [PATCH 064/147] fix merge conflicts --- cpp/src/dual_simplex/crossover.cpp | 9 ++++++--- cpp/src/dual_simplex/primal.cpp | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 931f515e77..988c9c50ad 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -533,7 +533,8 @@ i_t dual_push(const lp_problem_t& lp, basic_list, nonbasic_list, superbasic_list, - vstatus); + vstatus, + work_estimate); rank = factorize_basis(lp.A, settings, basic_list, @@ -1293,7 +1294,8 @@ crossover_status_t crossover(const lp_problem_t& lp, basic_list, nonbasic_list, superbasic_list, - vstatus); + vstatus, + work_estimate); rank = factorize_basis(lp.A, settings, basic_list, @@ -1485,7 +1487,8 @@ crossover_status_t crossover(const lp_problem_t& lp, basic_list, nonbasic_list, superbasic_list, - vstatus); + vstatus, + work_estimate); rank = factorize_basis(lp.A, settings, basic_list, diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index b50e674c2d..d4c6743dc6 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -325,7 +325,8 @@ primal::status_t primal_phase2(i_t phase, basic_list, nonbasic_list, superbasic_list, - vstatus); + vstatus, + work_estimate); rank = factorize_basis(lp.A, settings, basic_list, From d5791dc31e80b3abbc048b300fc54abce30db9ed Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 18 Feb 2026 23:04:56 -0800 Subject: [PATCH 065/147] handle ai reviewS --- .../diversity/diversity_manager.cu | 1 + .../local_search/local_search.cu | 20 +++++++++++++------ .../presolve/conflict_graph/clique_table.cu | 20 ++++++++++++++----- cpp/src/mip_heuristics/problem/problem.cu | 8 +++++--- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index dcc1bf1833..709e2554e0 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -221,6 +221,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit) // do the resizing no-matter what, bounds presolve might not change the bounds but initial // trivial presolve might have ls.constraint_prop.bounds_update.resize(*problem_ptr); + ls.constraint_prop.bounds_update.upd.init_changed_constraints(problem_ptr->handle_ptr); ls.constraint_prop.conditional_bounds_update.update_constraint_bounds( *problem_ptr, ls.constraint_prop.bounds_update); } diff --git a/cpp/src/mip_heuristics/local_search/local_search.cu b/cpp/src/mip_heuristics/local_search/local_search.cu index 3247ae3568..118b7181ab 100644 --- a/cpp/src/mip_heuristics/local_search/local_search.cu +++ b/cpp/src/mip_heuristics/local_search/local_search.cu @@ -209,15 +209,23 @@ bool local_search_t::do_fj_solve(solution_t& solution, if (time_limit == 0.) return solution.get_feasible(); timer_t timer(time_limit); + const auto old_n_cstr_weights = in_fj.cstr_weights.size(); + const auto expected_n_cstr_weights = static_cast(solution.problem_ptr->n_constraints); // in case this is the first time run, resize - if (in_fj.cstr_weights.size() != (size_t)solution.problem_ptr->n_constraints) { + if (old_n_cstr_weights != expected_n_cstr_weights) { in_fj.cstr_weights.resize(solution.problem_ptr->n_constraints, solution.handle_ptr->get_stream()); - // reset weights since this is most likely the first call - thrust::uninitialized_fill(solution.handle_ptr->get_thrust_policy(), - in_fj.cstr_weights.begin(), - in_fj.cstr_weights.end(), - 1.); + cuopt_assert(in_fj.cstr_weights.size() == expected_n_cstr_weights, + "Constraint weights must match constraint count after resize"); + // Initialize only newly grown entries; shrinking does not need initialization. + if (old_n_cstr_weights < expected_n_cstr_weights) { + cuopt_assert(old_n_cstr_weights <= in_fj.cstr_weights.size(), + "Constraint weight fill start must be within range"); + thrust::uninitialized_fill(solution.handle_ptr->get_thrust_policy(), + in_fj.cstr_weights.begin() + old_n_cstr_weights, + in_fj.cstr_weights.end(), + 1.); + } } auto h_weights = cuopt::host_copy(in_fj.cstr_weights, solution.handle_ptr->get_stream()); auto h_objective_weight = in_fj.objective_weight.value(solution.handle_ptr->get_stream()); diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index ed1e82cd31..432fb56e5b 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -229,20 +229,30 @@ void remove_small_cliques(clique_table_t& clique_table) } } for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { - const auto& addtl_clique = clique_table.addtl_cliques[addtl_c]; + const auto& addtl_clique = clique_table.addtl_cliques[addtl_c]; + const auto base_clique_idx = static_cast(addtl_clique.clique_idx); + cuopt_assert(base_clique_idx < to_delete.size(), + "Additional clique points to invalid base clique index"); + // Remove additional cliques whose base clique is scheduled for deletion. + if (to_delete[base_clique_idx]) { + clique_table.addtl_cliques.erase(clique_table.addtl_cliques.begin() + addtl_c); + addtl_c--; + num_removed_addtl++; + continue; + } i_t size_of_clique = - clique_table.first[addtl_clique.clique_idx].size() - addtl_clique.start_pos_on_clique + 1; + clique_table.first[base_clique_idx].size() - addtl_clique.start_pos_on_clique + 1; if (size_of_clique < clique_table.min_clique_size) { // the items from first clique are already added to the adjlist // only add the items that are coming from the new var in the additional clique for (size_t i = addtl_clique.start_pos_on_clique; - i < clique_table.first[addtl_clique.clique_idx].size(); + i < clique_table.first[base_clique_idx].size(); i++) { // insert conflicts both way - clique_table.adj_list_small_cliques[clique_table.first[addtl_clique.clique_idx][i]].insert( + clique_table.adj_list_small_cliques[clique_table.first[base_clique_idx][i]].insert( addtl_clique.vertex_idx); clique_table.adj_list_small_cliques[addtl_clique.vertex_idx].insert( - clique_table.first[addtl_clique.clique_idx][i]); + clique_table.first[base_clique_idx][i]); } clique_table.addtl_cliques.erase(clique_table.addtl_cliques.begin() + addtl_c); addtl_c--; diff --git a/cpp/src/mip_heuristics/problem/problem.cu b/cpp/src/mip_heuristics/problem/problem.cu index 9b3e7a229c..bc93a9d988 100644 --- a/cpp/src/mip_heuristics/problem/problem.cu +++ b/cpp/src/mip_heuristics/problem/problem.cu @@ -2065,9 +2065,9 @@ void problem_t::set_constraints_from_host_user_problem( empty = (nnz == 0 && n_constraints == 0 && n_variables == 0); auto stream = handle_ptr->get_stream(); - cuopt::device_copy(coefficients, csr_A.x.underlying(), stream); - cuopt::device_copy(variables, csr_A.j.underlying(), stream); - cuopt::device_copy(offsets, csr_A.row_start.underlying(), stream); + cuopt::device_copy(coefficients, csr_A.x, stream); + cuopt::device_copy(variables, csr_A.j, stream); + cuopt::device_copy(offsets, csr_A.row_start, stream); std::vector h_constraint_lower_bounds(n_constraints); std::vector h_constraint_upper_bounds(n_constraints); @@ -2118,6 +2118,8 @@ void problem_t::set_constraints_from_host_user_problem( lp_state.prev_dual.end(), f_t{0}); } + handle_ptr->sync_stream(); + RAFT_CHECK_CUDA(stream); compute_transpose_of_problem(); combined_bounds.resize(n_constraints, stream); From fc414e748f06a7058ff91c77affa69648a08e0d3 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 18 Feb 2026 23:06:59 -0800 Subject: [PATCH 066/147] revert cmake comment --- cpp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index aa377b3155..6a684be70b 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -153,7 +153,7 @@ if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.9 AND CMAKE_CUDA_COMPILE endif() list(APPEND CUOPT_CUDA_FLAGS -fopenmp) -# Add parallel compilation flags if PARALLEL_LEVEL is set +# Add jobserver flags for parallel compilation if PARALLEL_LEVEL is set if(PARALLEL_LEVEL AND NOT "${PARALLEL_LEVEL}" STREQUAL "") message(STATUS "Enabling nvcc parallel compilation support") list(APPEND CUOPT_CUDA_FLAGS --threads=0 --split-compile=0) From 3acb6a9eb0fdd910023e552e45f7a55cbf15534a Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 20 Feb 2026 03:03:47 -0800 Subject: [PATCH 067/147] fix adjacency checks --- .../presolve/conflict_graph/clique_table.cu | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 432fb56e5b..7dcadc767e 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -297,7 +297,9 @@ std::unordered_set clique_table_t::get_adj_set_of_var(i_t var_idx } for (const auto& addtl_clique_idx : var_clique_map_addtl[var_idx]) { - adj_set.insert(first[addtl_cliques[addtl_clique_idx].clique_idx].begin(), + adj_set.insert(addtl_cliques[addtl_clique_idx].vertex_idx); + adj_set.insert(first[addtl_cliques[addtl_clique_idx].clique_idx].begin() + + addtl_cliques[addtl_clique_idx].start_pos_on_clique, first[addtl_cliques[addtl_clique_idx].clique_idx].end()); } @@ -349,6 +351,19 @@ bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) } } + // var_clique_map_addtl is keyed by addtl.vertex_idx, so also check the reverse direction. + for (const auto& addtl_idx : var_clique_map_addtl[var_idx2]) { + const auto& addtl = addtl_cliques[addtl_idx]; + const auto& clique = first[addtl.clique_idx]; + if (addtl.vertex_idx == var_idx1) { return true; } + if (addtl.start_pos_on_clique < static_cast(clique.size())) { + if (std::find(clique.begin() + addtl.start_pos_on_clique, clique.end(), var_idx1) != + clique.end()) { + return true; + } + } + } + return false; } From 6f6783b02618de3f9fa46e7e4166d4ebca8cf1b5 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 20 Feb 2026 03:30:29 -0800 Subject: [PATCH 068/147] fix adjacency set and var degree --- .../presolve/conflict_graph/clique_table.cu | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 7dcadc767e..b65c72d7c0 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -235,6 +235,17 @@ void remove_small_cliques(clique_table_t& clique_table) "Additional clique points to invalid base clique index"); // Remove additional cliques whose base clique is scheduled for deletion. if (to_delete[base_clique_idx]) { + // Materialize conflicts represented by: + // addtl_clique.vertex_idx + first[base_clique_idx][start_pos_on_clique:] + // before deleting both the additional and base clique entries. + for (size_t i = addtl_clique.start_pos_on_clique; + i < clique_table.first[base_clique_idx].size(); + i++) { + clique_table.adj_list_small_cliques[clique_table.first[base_clique_idx][i]].insert( + addtl_clique.vertex_idx); + clique_table.adj_list_small_cliques[addtl_clique.vertex_idx].insert( + clique_table.first[base_clique_idx][i]); + } clique_table.addtl_cliques.erase(clique_table.addtl_cliques.begin() + addtl_c); addtl_c--; num_removed_addtl++; @@ -286,6 +297,8 @@ void remove_small_cliques(clique_table_t& clique_table) (size_t)clique_table.min_clique_size, "A small clique remained after removing small cliques"); } + // Clique removals/edge materialization can change degrees; force recompute on next query. + std::fill(clique_table.var_degrees.begin(), clique_table.var_degrees.end(), -1); } template @@ -302,6 +315,17 @@ std::unordered_set clique_table_t::get_adj_set_of_var(i_t var_idx addtl_cliques[addtl_clique_idx].start_pos_on_clique, first[addtl_cliques[addtl_clique_idx].clique_idx].end()); } + // Memory-neutral reverse lookup for additional cliques: + // if var_idx is in first[clique_idx][start_pos_on_clique:], it is adjacent to vertex_idx. + for (const auto& addtl : addtl_cliques) { + if (addtl.vertex_idx == var_idx) { continue; } + const auto& clique = first[addtl.clique_idx]; + size_t start_pos = static_cast(addtl.start_pos_on_clique); + if (start_pos < clique.size() && + std::find(clique.begin() + start_pos, clique.end(), var_idx) != clique.end()) { + adj_set.insert(addtl.vertex_idx); + } + } for (const auto& adj_vertex : adj_list_small_cliques[var_idx]) { adj_set.insert(adj_vertex); @@ -471,6 +495,9 @@ bool extend_clique(const std::vector& clique, cuopt_assert(n_of_complement_conflicts == 1, "There can only be one complement conflict"); // Keep the discovered extension in the clique table for downstream dominance checks. clique_table.first.push_back(new_clique); + for (const auto& var_idx : new_clique) { + clique_table.var_degrees[var_idx] = -1; + } // fix all other variables other than complementing var for (size_t i = 0; i < new_clique.size(); i++) { if (new_clique[i] % clique_table.n_variables != complement_conflict_var) { @@ -494,6 +521,9 @@ bool extend_clique(const std::vector& clique, // Keep the discovered extension in the clique table even when row insertion is skipped by // row/nnz budgets. clique_table.first.push_back(new_clique); + for (const auto& var_idx : new_clique) { + clique_table.var_degrees[var_idx] = -1; + } #if DEBUG_KNAPSACK_CONSTRAINTS CUOPT_LOG_DEBUG("Extended clique: %lu from %lu", new_clique.size(), clique.size()); #endif From 8bc9fb0ae879a906e3248d69d2d3c367bf9d644b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 20 Feb 2026 03:53:00 -0800 Subject: [PATCH 069/147] add copy of variable bounds --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 709e2554e0..86d256eca5 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -213,6 +213,13 @@ bool diversity_manager_t::run_presolve(f_t time_limit) std::shared_ptr> clique_table; find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); problem_ptr->set_constraints_from_host_user_problem(host_problem); + cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + "host lower bound size mismatch"); + cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + "host upper bound size mismatch"); + std::vector all_var_indices(problem_ptr->n_variables); + std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); trivial_presolve(*problem_ptr, remap_cache_ids); } // May overconstrain if Papilo presolve has been run before From ba7b9ff617f80832e87bbb25292be8a49dc5c337 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 20 Feb 2026 04:11:56 -0800 Subject: [PATCH 070/147] remove the set packing if it covers set partitioning --- .../presolve/conflict_graph/clique_table.cu | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index b65c72d7c0..4bd9150582 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -829,13 +829,12 @@ void remove_dominated_cliques( sp.row_idx); fix_difference(curr_clique_vars, vars_sp); } - } else { - // knapsack cstr_idx may refer to virtual rows; only real model row indices can be - // removed from A. - if (sp.row_idx < 0 || sp.row_idx >= static_cast(removal_marker.size())) { continue; } - if (removal_marker[sp.row_idx]) { continue; } - removal_marker[sp.row_idx] = true; } + // knapsack cstr_idx may refer to virtual rows; only real model row indices can be + // removed from A. + if (sp.row_idx < 0 || sp.row_idx >= static_cast(removal_marker.size())) { continue; } + if (removal_marker[sp.row_idx]) { continue; } + removal_marker[sp.row_idx] = true; } if ((i % 128) == 0) { CUOPT_LOG_TRACE("Processed extended clique %d/%d", i + 1, n_extended_cliques); From d82e59283c39d8a31e282c779f07cc05350a1aa9 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 23 Feb 2026 05:13:01 -0800 Subject: [PATCH 071/147] use append row --- cpp/src/dual_simplex/sparse_matrix.cpp | 16 ---------------- cpp/src/dual_simplex/sparse_matrix.hpp | 3 --- .../presolve/conflict_graph/clique_table.cu | 6 +++++- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/cpp/src/dual_simplex/sparse_matrix.cpp b/cpp/src/dual_simplex/sparse_matrix.cpp index 6484498baf..8ccccd57cf 100644 --- a/cpp/src/dual_simplex/sparse_matrix.cpp +++ b/cpp/src/dual_simplex/sparse_matrix.cpp @@ -676,22 +676,6 @@ void scatter_dense(const csc_matrix_t& A, i_t j, f_t alpha, std::vecto } } -template -void csr_matrix_t::insert_row(const std::vector& vars, - const std::vector& coeffs) -{ - assert(vars.size() == coeffs.size()); - // insert the row into the matrix - this->row_start.push_back(this->row_start.back() + vars.size()); - this->m++; - this->nz_max += vars.size(); - const i_t old_size = this->j.size(); - this->j.resize(this->j.size() + vars.size()); - std::copy(vars.data(), vars.data() + vars.size(), this->j.data() + old_size); - this->x.resize(this->x.size() + coeffs.size()); - std::copy(coeffs.data(), coeffs.data() + coeffs.size(), this->x.data() + old_size); -} - // x <- x + alpha * A(:, j) template void scatter_dense(const csc_matrix_t& A, diff --git a/cpp/src/dual_simplex/sparse_matrix.hpp b/cpp/src/dual_simplex/sparse_matrix.hpp index 237d4c6068..b6d3ee9aae 100644 --- a/cpp/src/dual_simplex/sparse_matrix.hpp +++ b/cpp/src/dual_simplex/sparse_matrix.hpp @@ -175,9 +175,6 @@ class csr_matrix_t { // get constraint range std::pair get_constraint_range(i_t cstr_idx) const; - - // insert a constraint into the matrix - void insert_row(const std::vector& vars, const std::vector& coeffs); i_t nz_max; // maximum number of nonzero entries i_t m; // number of rows i_t n; // number of cols diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 4bd9150582..7e5cb98491 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -420,7 +421,10 @@ void insert_clique_into_problem(const std::vector& clique, // Move constants to the right, so rhs must decrease by rhs_offset. f_t rhs = coeff_scale - rhs_offset; // insert the new clique into the problem as a new constraint - A.insert_row(new_vars, new_coeffs); + dual_simplex::sparse_vector_t new_row(A.n, new_vars.size()); + new_row.i = std::move(new_vars); + new_row.x = std::move(new_coeffs); + A.append_row(new_row); problem.row_sense.push_back('L'); problem.rhs.push_back(rhs); } From 8e9ecb17474a857e418a2311022d833533f18ac5 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 23 Feb 2026 06:15:15 -0800 Subject: [PATCH 072/147] fix merge conflicts --- cpp/src/branch_and_bound/branch_and_bound.cpp | 5 ++--- cpp/src/branch_and_bound/branch_and_bound.hpp | 2 -- cpp/src/cuts/cuts.cpp | 3 ++- cpp/src/cuts/cuts.hpp | 3 ++- .../mip_heuristics/presolve/conflict_graph/clique_table.cu | 1 + cpp/src/mip_heuristics/problem/problem.cu | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index b60794fb9d..9092dfd6f1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2117,7 +2117,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut #endif // Generate cuts and add them to the cut pool - if (stop_for_time_limit()) { return solver_status_; } f_t cut_start_time = tic(); bool problem_feasible = cut_generation.generate_cuts(original_lp_, settings_, @@ -2128,14 +2127,14 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.x, root_relax_soln_.z, basic_list, - nonbasic_list); + nonbasic_list, + exploration_stats_.start_time); if (!problem_feasible) { if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } return mip_status_t::INFEASIBLE; } - if (stop_for_time_limit()) { return solver_status_; } f_t cut_generation_time = toc(cut_start_time); if (cut_generation_time > 1.0) { settings_.log.debug("Cut generation time %.2f seconds\n", cut_generation_time); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 6130cc1661..f508fb145a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -114,8 +114,6 @@ class branch_and_bound_t { void set_concurrent_lp_root_solve(bool enable) { enable_concurrent_lp_root_solve_ = enable; } - bool stop_for_time_limit(mip_solution_t& solution); - // Repair a low-quality solution from the heuristics. bool repair_solution(const std::vector& leaf_edge_norms, const std::vector& potential_solution, diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 5aef64ace2..d829b74d66 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -923,7 +923,8 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, const std::vector& xstar, const std::vector& reduced_costs, const std::vector& basic_list, - const std::vector& nonbasic_list) + const std::vector& nonbasic_list, + f_t start_time) { // Generate Gomory and CG Cuts if (settings.mixed_integer_gomory_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 3ada63d5d3..924b5e4631 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -254,7 +254,8 @@ class cut_generation_t { const std::vector& xstar, const std::vector& reduced_costs, const std::vector& basic_list, - const std::vector& nonbasic_list); + const std::vector& nonbasic_list, + f_t start_time); private: // Generate all mixed integer gomory cuts diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 04ca6079b9..3f0d11324d 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -1043,6 +1043,7 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, // print_clique_table(clique_table); // remove small cliques and add them to adj_list remove_small_cliques(*clique_table_ptr, timer); +#ifdef DEBUG_CLIQUE_TABLE t_small = stage_timer.elapsed_time(); #endif // fill var clique maps diff --git a/cpp/src/mip_heuristics/problem/problem.cu b/cpp/src/mip_heuristics/problem/problem.cu index b28ec17f4c..75d1222eec 100644 --- a/cpp/src/mip_heuristics/problem/problem.cu +++ b/cpp/src/mip_heuristics/problem/problem.cu @@ -2069,9 +2069,9 @@ void problem_t::set_constraints_from_host_user_problem( empty = (nnz == 0 && n_constraints == 0 && n_variables == 0); auto stream = handle_ptr->get_stream(); - cuopt::device_copy(coefficients, csr_A.x.underlying(), stream); - cuopt::device_copy(variables, csr_A.j.underlying(), stream); - cuopt::device_copy(offsets, csr_A.row_start.underlying(), stream); + cuopt::device_copy(coefficients, csr_A.x, stream); + cuopt::device_copy(variables, csr_A.j, stream); + cuopt::device_copy(offsets, csr_A.row_start, stream); std::vector h_constraint_lower_bounds(n_constraints); std::vector h_constraint_upper_bounds(n_constraints); From 0283afea0098b0f079860cbb94947e38bae9d8c2 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 23 Feb 2026 07:23:57 -0800 Subject: [PATCH 073/147] without initial presolve --- .../diversity/diversity_manager.cu | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index f88e4d0456..1ad85877b8 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,24 +207,24 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!context.settings.heuristics_only && !problem_ptr->empty) { - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - find_initial_cliques( - host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - "host lower bound size mismatch"); - cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - "host upper bound size mismatch"); - std::vector all_var_indices(problem_ptr->n_variables); - std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - trivial_presolve(*problem_ptr, remap_cache_ids); - if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - } + // if (!context.settings.heuristics_only && !problem_ptr->empty) { + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + // find_initial_cliques( + // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + // "host lower bound size mismatch"); + // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + // "host upper bound size mismatch"); + // std::vector all_var_indices(problem_ptr->n_variables); + // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 232a8b2ab83c622a6b6f06c66be4ebc81301fcb9 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 23 Feb 2026 10:41:57 -0800 Subject: [PATCH 074/147] add clique tests --- cpp/src/cuts/cuts.cpp | 46 +++ cpp/src/cuts/cuts.hpp | 9 + cpp/tests/mip/cuts_test.cu | 615 +++++++++++++++++++++++++++++++++++++ 3 files changed, 670 insertions(+) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index d829b74d66..c9a685cf6c 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -375,6 +375,52 @@ void extend_clique_vertices(std::vector& clique_vertices, } // namespace +std::vector> find_maximal_cliques_for_test( + const std::vector>& adjacency_list, + const std::vector& weights, + double min_weight, + int max_calls, + double time_limit) +{ + const size_t n_vertices = adjacency_list.size(); + if (n_vertices == 0) { return {}; } + cuopt_assert(weights.size() == n_vertices, "Weights size mismatch in clique test helper"); + cuopt_assert(max_calls > 0, "max_calls must be positive in clique test helper"); + + const size_t words = bitset_words(n_vertices); + std::vector> adj_bitset(n_vertices, std::vector(words, 0)); + for (size_t v = 0; v < n_vertices; ++v) { + for (const auto& nbr : adjacency_list[v]) { + cuopt_assert(nbr >= 0 && static_cast(nbr) < n_vertices, + "Neighbor index out of range in clique test helper"); + bitset_set(adj_bitset[v], static_cast(nbr)); + } + } + + double work_estimate = 0.0; + const double max_work_estimate = std::numeric_limits::infinity(); + const double start_time = tic(); + + bk_bitset_context_t ctx{adj_bitset, + weights, + min_weight, + max_calls, + start_time, + time_limit, + words, + &work_estimate, + max_work_estimate}; + + std::vector R; + std::vector P(words, 0); + std::vector X(words, 0); + for (size_t idx = 0; idx < n_vertices; ++idx) { + bitset_set(P, idx); + } + bron_kerbosch(ctx, R, P, X, 0.0); + return ctx.cliques; +} + template void cut_pool_t::add_cut(cut_type_t cut_type, const sparse_vector_t& cut, diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 924b5e4631..b352de70ae 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -127,6 +127,15 @@ void verify_cuts_against_saved_solution(const csr_matrix_t& cuts, const std::vector& cut_rhs, const std::vector& saved_solution); +// Test-only helper to run the production maximal-clique algorithm used by clique cuts. +// adjacency_list must contain local vertex indices in [0, n_vertices). +std::vector> find_maximal_cliques_for_test( + const std::vector>& adjacency_list, + const std::vector& weights, + double min_weight, + int max_calls, + double time_limit); + template class cut_pool_t { public: diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 1a360b41eb..1b283f19b5 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -8,25 +8,430 @@ #include "../linear_programming/utilities/pdlp_test_utilities.cuh" #include "mip_utils.cuh" +#include +#include #include +#include +#include +#include #include #include +#include #include +#include #include #include #include +#include #include #include #include +#include #include #include +#include #include namespace cuopt::linear_programming::test { +namespace { + +constexpr double kCliqueTestTol = 1e-6; + +mps_parser::mps_data_model_t create_pairwise_triangle_set_packing_problem() +{ + // Maximize x0 + x1 + x2 via minimizing -x0 - x1 - x2. + // Pairwise conflicts: + // x0 + x1 <= 1 + // x1 + x2 <= 1 + // x0 + x2 <= 1 + mps_parser::mps_data_model_t problem; + std::vector offsets = {0, 2, 4, 6}; + std::vector indices = {0, 1, 1, 2, 0, 2}; + std::vector coefficients = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; + problem.set_csr_constraint_matrix(coefficients.data(), + coefficients.size(), + indices.data(), + indices.size(), + offsets.data(), + offsets.size()); + std::vector lower_bounds = {-std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + std::vector upper_bounds = {1.0, 1.0, 1.0}; + problem.set_constraint_lower_bounds(lower_bounds.data(), lower_bounds.size()); + problem.set_constraint_upper_bounds(upper_bounds.data(), upper_bounds.size()); + std::vector var_lower_bounds = {0.0, 0.0, 0.0}; + std::vector var_upper_bounds = {1.0, 1.0, 1.0}; + problem.set_variable_lower_bounds(var_lower_bounds.data(), var_lower_bounds.size()); + problem.set_variable_upper_bounds(var_upper_bounds.data(), var_upper_bounds.size()); + std::vector objective_coefficients = {-1.0, -1.0, -1.0}; + problem.set_objective_coefficients(objective_coefficients.data(), objective_coefficients.size()); + std::vector variable_types = {'I', 'I', 'I'}; + problem.set_variable_types(variable_types); + problem.set_maximize(false); + return problem; +} + +mps_parser::mps_data_model_t create_pairwise_triangle_with_isolated_variable_problem() +{ + // Same triangle conflicts as create_pairwise_triangle_set_packing_problem(), + // plus an isolated binary variable x3 with no conflict rows. + mps_parser::mps_data_model_t problem; + std::vector offsets = {0, 2, 4, 6}; + std::vector indices = {0, 1, 1, 2, 0, 2}; + std::vector coefficients = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; + problem.set_csr_constraint_matrix(coefficients.data(), + coefficients.size(), + indices.data(), + indices.size(), + offsets.data(), + offsets.size()); + std::vector lower_bounds = {-std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + std::vector upper_bounds = {1.0, 1.0, 1.0}; + problem.set_constraint_lower_bounds(lower_bounds.data(), lower_bounds.size()); + problem.set_constraint_upper_bounds(upper_bounds.data(), upper_bounds.size()); + std::vector var_lower_bounds = {0.0, 0.0, 0.0, 0.0}; + std::vector var_upper_bounds = {1.0, 1.0, 1.0, 1.0}; + problem.set_variable_lower_bounds(var_lower_bounds.data(), var_lower_bounds.size()); + problem.set_variable_upper_bounds(var_upper_bounds.data(), var_upper_bounds.size()); + std::vector objective_coefficients = {-1.0, -1.0, -1.0, 0.0}; + problem.set_objective_coefficients(objective_coefficients.data(), objective_coefficients.size()); + std::vector variable_types = {'I', 'I', 'I', 'I'}; + problem.set_variable_types(variable_types); + problem.set_maximize(false); + return problem; +} + +mps_parser::mps_data_model_t create_binary_continuous_mixed_conflict_problem() +{ + // x0 + y1 <= 1 (must be ignored for clique graph because y1 is continuous) + // x0 + x2 <= 1 (must generate a conflict edge) + mps_parser::mps_data_model_t problem; + std::vector offsets = {0, 2, 4}; + std::vector indices = {0, 1, 0, 2}; + std::vector coefficients = {1.0, 1.0, 1.0, 1.0}; + problem.set_csr_constraint_matrix(coefficients.data(), + coefficients.size(), + indices.data(), + indices.size(), + offsets.data(), + offsets.size()); + std::vector lower_bounds = {-std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + std::vector upper_bounds = {1.0, 1.0}; + problem.set_constraint_lower_bounds(lower_bounds.data(), lower_bounds.size()); + problem.set_constraint_upper_bounds(upper_bounds.data(), upper_bounds.size()); + std::vector var_lower_bounds = {0.0, 0.0, 0.0}; + std::vector var_upper_bounds = {1.0, 1.0, 1.0}; + problem.set_variable_lower_bounds(var_lower_bounds.data(), var_lower_bounds.size()); + problem.set_variable_upper_bounds(var_upper_bounds.data(), var_upper_bounds.size()); + std::vector objective_coefficients = {0.0, 0.0, 0.0}; + problem.set_objective_coefficients(objective_coefficients.data(), objective_coefficients.size()); + std::vector variable_types = {'I', 'C', 'I'}; + problem.set_variable_types(variable_types); + problem.set_maximize(false); + return problem; +} + +mps_parser::mps_data_model_t create_near_binary_bound_conflict_problem() +{ + // x0 + x1 <= 1 but x1 has upper bound 0.9999999, so this row should not be + // treated as a binary conflict row. + mps_parser::mps_data_model_t problem; + std::vector offsets = {0, 2}; + std::vector indices = {0, 1}; + std::vector coefficients = {1.0, 1.0}; + problem.set_csr_constraint_matrix(coefficients.data(), + coefficients.size(), + indices.data(), + indices.size(), + offsets.data(), + offsets.size()); + std::vector lower_bounds = {-std::numeric_limits::infinity()}; + std::vector upper_bounds = {1.0}; + problem.set_constraint_lower_bounds(lower_bounds.data(), lower_bounds.size()); + problem.set_constraint_upper_bounds(upper_bounds.data(), upper_bounds.size()); + std::vector var_lower_bounds = {0.0, 0.0}; + std::vector var_upper_bounds = {1.0, 0.9999999}; + problem.set_variable_lower_bounds(var_lower_bounds.data(), var_lower_bounds.size()); + problem.set_variable_upper_bounds(var_upper_bounds.data(), var_upper_bounds.size()); + std::vector objective_coefficients = {0.0, 0.0}; + problem.set_objective_coefficients(objective_coefficients.data(), objective_coefficients.size()); + std::vector variable_types = {'I', 'I'}; + problem.set_variable_types(variable_types); + problem.set_maximize(false); + return problem; +} + +mps_parser::mps_data_model_t create_weighted_addtl_conflict_problem() +{ + // One weighted binary knapsack row: + // 1*x0 + 2*x1 + 3*x2 + 4*x3 <= 5 + // This creates base clique {x2, x3} and additional clique inducing conflict {x1, x3}. + mps_parser::mps_data_model_t problem; + std::vector offsets = {0, 4}; + std::vector indices = {0, 1, 2, 3}; + std::vector coefficients = {1.0, 2.0, 3.0, 4.0}; + problem.set_csr_constraint_matrix(coefficients.data(), + coefficients.size(), + indices.data(), + indices.size(), + offsets.data(), + offsets.size()); + std::vector lower_bounds = {-std::numeric_limits::infinity()}; + std::vector upper_bounds = {5.0}; + problem.set_constraint_lower_bounds(lower_bounds.data(), lower_bounds.size()); + problem.set_constraint_upper_bounds(upper_bounds.data(), upper_bounds.size()); + std::vector var_lower_bounds = {0.0, 0.0, 0.0, 0.0}; + std::vector var_upper_bounds = {1.0, 1.0, 1.0, 1.0}; + problem.set_variable_lower_bounds(var_lower_bounds.data(), var_lower_bounds.size()); + problem.set_variable_upper_bounds(var_upper_bounds.data(), var_upper_bounds.size()); + std::vector objective_coefficients = {0.0, 0.0, 0.0, 0.0}; + problem.set_objective_coefficients(objective_coefficients.data(), objective_coefficients.size()); + std::vector variable_types = {'I', 'I', 'I', 'I'}; + problem.set_variable_types(variable_types); + problem.set_maximize(false); + return problem; +} + +detail::clique_table_t build_clique_table_for_model_with_min_size( + const raft::handle_t& handle, + const mps_parser::mps_data_model_t& model, + int min_clique_size) +{ + auto op_problem = mps_data_model_to_optimization_problem(&handle, model); + detail::problem_t mip_problem(op_problem); + dual_simplex::user_problem_t host_problem(op_problem.get_handle_ptr()); + mip_problem.get_host_user_problem(host_problem); + + detail::clique_config_t clique_config; + clique_config.min_clique_size = min_clique_size; + detail::clique_table_t clique_table(2 * host_problem.num_cols, + clique_config.min_clique_size, + clique_config.max_clique_size_for_extension); + + mip_solver_settings_t settings; + cuopt::timer_t timer(std::numeric_limits::infinity()); + detail::build_clique_table(host_problem, clique_table, settings.tolerances, true, true, timer); + return clique_table; +} + +detail::clique_table_t build_clique_table_for_model( + const raft::handle_t& handle, const mps_parser::mps_data_model_t& model) +{ + return build_clique_table_for_model_with_min_size(handle, model, 1); +} + +std::vector> build_original_adjacency_matrix( + detail::clique_table_t& clique_table, int num_vars) +{ + std::vector> adj(num_vars, std::vector(num_vars, 0)); + for (int i = 0; i < num_vars; ++i) { + for (int j = i + 1; j < num_vars; ++j) { + if (clique_table.check_adjacency(i, j)) { + adj[i][j] = 1; + adj[j][i] = 1; + } + } + } + return adj; +} + +std::vector> maximal_cliques_bruteforce(const std::vector>& adj) +{ + const int n = static_cast(adj.size()); + if (n <= 0 || n > 20) { return {}; } + const uint64_t total_masks = (uint64_t{1} << n); + std::vector> maximal_cliques; + + auto is_mask_clique = [&](uint64_t mask) { + for (int i = 0; i < n; ++i) { + if ((mask & (uint64_t{1} << i)) == 0) { continue; } + for (int j = i + 1; j < n; ++j) { + if ((mask & (uint64_t{1} << j)) == 0) { continue; } + if (!adj[i][j]) { return false; } + } + } + return true; + }; + + for (uint64_t mask = 1; mask < total_masks; ++mask) { + if (!is_mask_clique(mask)) { continue; } + bool is_maximal = true; + for (int v = 0; v < n && is_maximal; ++v) { + if (mask & (uint64_t{1} << v)) { continue; } + bool can_extend = true; + for (int u = 0; u < n; ++u) { + if ((mask & (uint64_t{1} << u)) == 0) { continue; } + if (!adj[v][u]) { + can_extend = false; + break; + } + } + if (can_extend) { is_maximal = false; } + } + if (!is_maximal) { continue; } + std::vector clique; + for (int u = 0; u < n; ++u) { + if (mask & (uint64_t{1} << u)) { clique.push_back(u); } + } + maximal_cliques.push_back(std::move(clique)); + } + return maximal_cliques; +} + +std::vector> canonicalize_cliques(std::vector> cliques) +{ + for (auto& clique : cliques) { + std::sort(clique.begin(), clique.end()); + } + std::sort(cliques.begin(), cliques.end(), [](const auto& a, const auto& b) { + if (a.size() != b.size()) { return a.size() < b.size(); } + return a < b; + }); + cliques.erase(std::unique(cliques.begin(), cliques.end()), cliques.end()); + return cliques; +} + +std::vector> adjacency_matrix_to_list(const std::vector>& adj) +{ + const int n = static_cast(adj.size()); + std::vector> adj_list(n); + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + if (adj[i][j]) { adj_list[i].push_back(j); } + } + } + return adj_list; +} + +std::vector> maximal_cliques_from_production_algorithm( + const std::vector>& adj) +{ + const auto adj_list = adjacency_matrix_to_list(adj); + std::vector weights(adj_list.size(), 1.0); + auto cliques = dual_simplex::find_maximal_cliques_for_test( + adj_list, weights, 0.0, 100000, std::numeric_limits::infinity()); + return canonicalize_cliques(std::move(cliques)); +} + +double original_clique_sum(const std::vector& clique_vars, + const std::vector& assignment) +{ + double lhs = 0.0; + for (const auto var : clique_vars) { + lhs += assignment[var]; + } + return lhs; +} + +std::string format_phase2_panic_dump(const mps_parser::mps_data_model_t& problem, + const std::vector& clique_vars, + const std::vector& x_star) +{ + std::ostringstream out; + const auto& var_lb = problem.get_variable_lower_bounds(); + const auto& var_ub = problem.get_variable_upper_bounds(); + out << "\nClique vars:"; + for (auto v : clique_vars) { + out << " x" << v << "(value=" << x_star[v] << ", lb=" << var_lb[v] << ", ub=" << var_ub[v] + << ")"; + } + + std::unordered_set clique_var_set(clique_vars.begin(), clique_vars.end()); + const auto& values = problem.get_constraint_matrix_values(); + const auto& cols = problem.get_constraint_matrix_indices(); + const auto& rows = problem.get_constraint_matrix_offsets(); + const auto& clb = problem.get_constraint_lower_bounds(); + const auto& cub = problem.get_constraint_upper_bounds(); + + out << "\nRelated constraints:"; + for (size_t row = 0; row + 1 < rows.size(); ++row) { + bool touches_clique = false; + for (int p = rows[row]; p < rows[row + 1]; ++p) { + if (clique_var_set.count(cols[p]) > 0) { + touches_clique = true; + break; + } + } + if (!touches_clique) { continue; } + out << "\n row " << row << ": "; + for (int p = rows[row]; p < rows[row + 1]; ++p) { + if (p > rows[row]) { out << " + "; } + out << values[p] << "*x" << cols[p]; + } + out << " in [" << clb[row] << ", " << cub[row] << "]"; + } + return out.str(); +} + +void disable_non_clique_cuts(mip_solver_settings_t& settings) +{ + settings.clique_cuts = 1; + settings.max_cut_passes = 10; + settings.mixed_integer_gomory_cuts = 0; + settings.knapsack_cuts = 0; + settings.mir_cuts = 0; + settings.strong_chvatal_gomory_cuts = 0; +} + +void disable_all_cuts(mip_solver_settings_t& settings) +{ + settings.max_cut_passes = 0; + settings.clique_cuts = 0; + settings.mixed_integer_gomory_cuts = 0; + settings.knapsack_cuts = 0; + settings.mir_cuts = 0; + settings.strong_chvatal_gomory_cuts = 0; +} + +bool cut_is_invalid_for_incumbent(const std::vector& cut_vars, + const std::vector& incumbent, + double tol) +{ + return original_clique_sum(cut_vars, incumbent) > 1.0 + tol; +} + +bool prefix_has_invalid_cut(const std::vector>& dumped_cuts, + size_t prefix_end_exclusive, + const std::vector& incumbent, + double tol) +{ + for (size_t i = 0; i < prefix_end_exclusive; ++i) { + if (cut_is_invalid_for_incumbent(dumped_cuts[i], incumbent, tol)) { return true; } + } + return false; +} + +std::optional isolate_first_invalid_cut_by_bisection( + const std::vector>& dumped_cuts, + const std::vector& incumbent, + double tol) +{ + if (!prefix_has_invalid_cut(dumped_cuts, dumped_cuts.size(), incumbent, tol)) { + return std::nullopt; + } + size_t lo = 0; + size_t hi = dumped_cuts.size() - 1; + while (lo < hi) { + const size_t mid = lo + (hi - lo) / 2; + if (prefix_has_invalid_cut(dumped_cuts, mid + 1, incumbent, tol)) { + hi = mid; + } else { + lo = mid + 1; + } + } + return lo; +} + +} // namespace + // Problem data for the mixed integer linear programming problem mps_parser::mps_data_model_t create_cuts_problem_1() { @@ -165,4 +570,214 @@ TEST(cuts, test_cuts_2) EXPECT_EQ(solution.get_num_nodes(), 0); } +TEST(cuts, clique_phase1_smoke_conflict_graph_edges) +{ + const raft::handle_t handle{}; + auto problem = create_pairwise_triangle_with_isolated_variable_problem(); + auto clique_table = build_clique_table_for_model(handle, problem); + + // Positive edges from triangle. + EXPECT_TRUE(clique_table.check_adjacency(0, 1)); + EXPECT_TRUE(clique_table.check_adjacency(1, 0)); + EXPECT_TRUE(clique_table.check_adjacency(1, 2)); + EXPECT_TRUE(clique_table.check_adjacency(2, 1)); + EXPECT_TRUE(clique_table.check_adjacency(0, 2)); + EXPECT_TRUE(clique_table.check_adjacency(2, 0)); + + // Negative edges to isolated x3. + EXPECT_FALSE(clique_table.check_adjacency(0, 3)); + EXPECT_FALSE(clique_table.check_adjacency(3, 0)); + EXPECT_FALSE(clique_table.check_adjacency(1, 3)); + EXPECT_FALSE(clique_table.check_adjacency(3, 1)); + EXPECT_FALSE(clique_table.check_adjacency(2, 3)); + EXPECT_FALSE(clique_table.check_adjacency(3, 2)); + + // Self is never an edge. + EXPECT_FALSE(clique_table.check_adjacency(3, 3)); +} + +TEST(cuts, clique_phase1_unit_maximal_clique_finder_hardcoded_adj) +{ + // Hardcoded graph: + // triangle (0,1,2) and an extra edge (2,3) + std::vector> adj = { + {0, 1, 1, 0}, + {1, 0, 1, 0}, + {1, 1, 0, 1}, + {0, 0, 1, 0}, + }; + + auto maximal_bruteforce = canonicalize_cliques(maximal_cliques_bruteforce(adj)); + auto maximal_internal = maximal_cliques_from_production_algorithm(adj); + EXPECT_EQ(maximal_internal, maximal_bruteforce); + bool found_triangle = false; + for (const auto& clique : maximal_internal) { + if (clique.size() == 3 && clique[0] == 0 && clique[1] == 1 && clique[2] == 2) { + found_triangle = true; + break; + } + } + EXPECT_TRUE(found_triangle); +} + +TEST(cuts, clique_phase1_addtl_conflict_symmetry_and_reverse_lookup) +{ + const raft::handle_t handle{}; + auto problem = create_weighted_addtl_conflict_problem(); + auto clique_table = build_clique_table_for_model_with_min_size(handle, problem, 1); + + ASSERT_FALSE(clique_table.addtl_cliques.empty()); + + // Conflict introduced through additional clique path must be symmetric. + EXPECT_TRUE(clique_table.check_adjacency(1, 3)); + EXPECT_TRUE(clique_table.check_adjacency(3, 1)); + + // get_adj_set_of_var() must also include reverse lookup for addtl membership. + auto adj_of_1 = clique_table.get_adj_set_of_var(1); + auto adj_of_3 = clique_table.get_adj_set_of_var(3); + EXPECT_TRUE(adj_of_1.count(3) > 0); + EXPECT_TRUE(adj_of_3.count(1) > 0); +} + +TEST(cuts, clique_phase1_remove_small_cliques_preserves_addtl_conflicts) +{ + const raft::handle_t handle{}; + auto problem = create_weighted_addtl_conflict_problem(); + // Force base clique {x2,x3} to be considered "small" and removed. + auto clique_table = build_clique_table_for_model_with_min_size(handle, problem, 2); + + EXPECT_TRUE(clique_table.first.empty()); + EXPECT_TRUE(clique_table.addtl_cliques.empty()); + + // Conflicts must remain materialized in adj_list_small_cliques after removals. + EXPECT_TRUE(clique_table.check_adjacency(1, 3)); + EXPECT_TRUE(clique_table.check_adjacency(3, 1)); + EXPECT_TRUE(clique_table.check_adjacency(2, 3)); + EXPECT_TRUE(clique_table.check_adjacency(3, 2)); + EXPECT_FALSE(clique_table.check_adjacency(0, 3)); +} + +TEST(cuts, clique_phase2_no_cut_off_optimal_solution_validation) +{ + const raft::handle_t handle{}; + auto problem = create_pairwise_triangle_set_packing_problem(); + + mip_solver_settings_t settings; + settings.time_limit = 10.0; + settings.presolver = presolver_t::None; + disable_all_cuts(settings); + + auto mip_solution = solve_mip(&handle, problem, settings); + ASSERT_EQ(mip_solution.get_termination_status(), mip_termination_status_t::Optimal); + auto x_star = cuopt::host_copy(mip_solution.get_solution(), handle.get_stream()); + + auto clique_table = build_clique_table_for_model(handle, problem); + auto adj = build_original_adjacency_matrix(clique_table, problem.get_n_variables()); + auto maximal = maximal_cliques_bruteforce(adj); + ASSERT_FALSE(maximal.empty()); + + for (const auto& clique_vars : maximal) { + if (clique_vars.size() < 2) { continue; } + const double lhs = original_clique_sum(clique_vars, x_star); + ASSERT_LE(lhs, 1.0 + kCliqueTestTol) << format_phase2_panic_dump(problem, clique_vars, x_star); + } +} + +TEST(cuts, clique_phase3_fractional_separation_must_cut_off) +{ + const raft::handle_t handle{}; + auto mip_problem = create_pairwise_triangle_set_packing_problem(); + + auto lp_relaxation = mip_problem; + std::vector all_continuous(lp_relaxation.get_n_variables(), 'C'); + lp_relaxation.set_variable_types(all_continuous); + + pdlp_solver_settings_t lp_settings{}; + lp_settings.time_limit = 10.0; + lp_settings.presolver = presolver_t::None; + lp_settings.set_optimality_tolerance(1e-8); + + auto lp_solution = solve_lp(&handle, lp_relaxation, lp_settings); + ASSERT_EQ(lp_solution.get_termination_status(), pdlp_termination_status_t::Optimal); + auto x_bar = cuopt::host_copy(lp_solution.get_primal_solution(), handle.get_stream()); + + auto clique_table = build_clique_table_for_model(handle, mip_problem); + auto adj = build_original_adjacency_matrix(clique_table, mip_problem.get_n_variables()); + auto maximal = maximal_cliques_from_production_algorithm(adj); + + bool found_separating_clique = false; + for (const auto& clique_vars : maximal) { + if (clique_vars.size() < 2) { continue; } + const double lhs = original_clique_sum(clique_vars, x_bar); + if (lhs > 1.0 + kCliqueTestTol) { + found_separating_clique = true; + break; + } + } + EXPECT_TRUE(found_separating_clique); +} + +TEST(cuts, clique_phase4_fault_isolation_binary_search) +{ + // Simulated incumbent x* and dumped cuts. + // First invalid cut is at index 2: {0,1} gives 2 > 1. + const std::vector incumbent = {1.0, 1.0, 0.0, 0.0}; + const std::vector> dumped_cuts = { + {0, 2}, // valid + {1, 3}, // valid + {0, 1}, // invalid + {2, 3}, // valid + }; + + auto first_invalid = + isolate_first_invalid_cut_by_bisection(dumped_cuts, incumbent, kCliqueTestTol); + ASSERT_TRUE(first_invalid.has_value()); + EXPECT_EQ(first_invalid.value(), 2); +} + +TEST(cuts, clique_phase4_tree_depth_limit_smoke) +{ + const raft::handle_t handle{}; + auto problem = create_pairwise_triangle_set_packing_problem(); + + mip_solver_settings_t root_only_settings; + root_only_settings.time_limit = 10.0; + root_only_settings.presolver = presolver_t::None; + root_only_settings.node_limit = 0; + disable_non_clique_cuts(root_only_settings); + + mip_solver_settings_t deeper_settings = root_only_settings; + deeper_settings.node_limit = 100; + + auto root_only_solution = solve_mip(&handle, problem, root_only_settings); + auto deeper_solution = solve_mip(&handle, problem, deeper_settings); + + EXPECT_EQ(deeper_solution.get_termination_status(), mip_termination_status_t::Optimal); + EXPECT_NE(root_only_solution.get_termination_status(), mip_termination_status_t::Infeasible); + if (root_only_solution.get_termination_status() == mip_termination_status_t::Optimal) { + EXPECT_NEAR( + root_only_solution.get_objective_value(), deeper_solution.get_objective_value(), 1e-6); + } +} + +TEST(cuts, clique_phase5_ignores_non_binary_variables) +{ + const raft::handle_t handle{}; + auto problem = create_binary_continuous_mixed_conflict_problem(); + auto clique_table = build_clique_table_for_model(handle, problem); + + EXPECT_TRUE(clique_table.check_adjacency(0, 2)); + EXPECT_FALSE(clique_table.check_adjacency(0, 1)); + EXPECT_FALSE(clique_table.check_adjacency(1, 2)); +} + +TEST(cuts, clique_phase5_ignores_fractional_binary_bounds) +{ + const raft::handle_t handle{}; + auto problem = create_near_binary_bound_conflict_problem(); + auto clique_table = build_clique_table_for_model(handle, problem); + + EXPECT_FALSE(clique_table.check_adjacency(0, 1)); +} + } // namespace cuopt::linear_programming::test From 721f49143071676b994c0995611f69a69c6f2a5c Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 23 Feb 2026 22:17:52 -0800 Subject: [PATCH 075/147] wip clique cuts test --- cpp/src/cuts/cuts.cpp | 186 ++++++++++-- cpp/src/cuts/cuts.hpp | 13 + cpp/tests/mip/cuts_test.cu | 598 +++++++++++++++++++++++++++++++++++++ 3 files changed, 768 insertions(+), 29 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index c9a685cf6c..820dd5f64a 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -24,6 +25,19 @@ namespace { enum class clique_cut_build_status_t : int8_t { NO_CUT = 0, CUT_ADDED = 1, INFEASIBLE = 2 }; +#ifdef DEBUG_CLIQUE_CUTS +#define CLIQUE_CUTS_DEBUG(...) \ + do { \ + std::fprintf(stderr, "[DEBUG_CLIQUE_CUTS] "); \ + std::fprintf(stderr, __VA_ARGS__); \ + std::fprintf(stderr, "\n"); \ + } while (0) +#else +#define CLIQUE_CUTS_DEBUG(...) \ + do { \ + } while (0) +#endif + template clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertices, i_t num_vars, @@ -38,17 +52,19 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic f_t* work_estimate, f_t max_work_estimate) { - auto add_work = [&](f_t accesses) -> bool { - if (work_estimate == nullptr) { return false; } - *work_estimate += accesses; - return *work_estimate > max_work_estimate; - }; - if (clique_vertices.size() < 2) { return clique_cut_build_status_t::NO_CUT; } const f_t clique_size = static_cast(clique_vertices.size()); + CLIQUE_CUTS_DEBUG("build_clique_cut start clique_size=%lld", + static_cast(clique_vertices.size())); // Coarse function-level estimate: // validate/transform vertices + duplicate checks + cut construction + sort + violation check - if (add_work(16.0 * clique_size + 4.0 * clique_size * std::log2(clique_size + 1.0))) { + if (add_work_estimate(16.0 * clique_size + 4.0 * clique_size * std::log2(clique_size + 1.0), + work_estimate, + max_work_estimate)) { + CLIQUE_CUTS_DEBUG("build_clique_cut skip work_limit clique_size=%lld work=%g limit=%g", + static_cast(clique_vertices.size()), + work_estimate == nullptr ? -1.0 : static_cast(*work_estimate), + static_cast(max_work_estimate)); return clique_cut_build_status_t::NO_CUT; } @@ -79,14 +95,22 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic // we store the cut in the form of >= 1, for easy violation check with dot product // that's why compelements have 1 as coeff and normal vars have -1 if (complement) { - if (seen_original.count(var_idx) > 0) { return clique_cut_build_status_t::INFEASIBLE; } + if (seen_original.count(var_idx) > 0) { + CLIQUE_CUTS_DEBUG("build_clique_cut infeasible var=%lld appears as variable and complement", + static_cast(var_idx)); + return clique_cut_build_status_t::INFEASIBLE; + } cuopt_assert(seen_complement.count(var_idx) == 0, "Duplicate complement in clique"); seen_complement.insert(var_idx); num_complements++; cut.i.push_back(var_idx); cut.x.push_back(1.0); } else { - if (seen_complement.count(var_idx) > 0) { return clique_cut_build_status_t::INFEASIBLE; } + if (seen_complement.count(var_idx) > 0) { + CLIQUE_CUTS_DEBUG("build_clique_cut infeasible var=%lld appears as variable and complement", + static_cast(var_idx)); + return clique_cut_build_status_t::INFEASIBLE; + } cuopt_assert(seen_original.count(var_idx) == 0, "Duplicate variable in clique"); seen_original.insert(var_idx); cut.i.push_back(var_idx); @@ -94,14 +118,35 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic } } - if (cut.i.empty()) { return clique_cut_build_status_t::NO_CUT; } + if (cut.i.empty()) { + CLIQUE_CUTS_DEBUG("build_clique_cut no_cut empty support"); + return clique_cut_build_status_t::NO_CUT; + } cut_rhs = static_cast(num_complements - 1); cut.sort(); const f_t dot = cut.dot(xstar); const f_t violation = cut_rhs - dot; - if (violation > min_violation) { return clique_cut_build_status_t::CUT_ADDED; } + if (violation > min_violation) { + CLIQUE_CUTS_DEBUG( + "build_clique_cut accepted nz=%lld rhs=%g dot=%g violation=%g threshold=%g complements=%lld", + static_cast(cut.i.size()), + static_cast(cut_rhs), + static_cast(dot), + static_cast(violation), + static_cast(min_violation), + static_cast(num_complements)); + return clique_cut_build_status_t::CUT_ADDED; + } + CLIQUE_CUTS_DEBUG( + "build_clique_cut rejected nz=%lld rhs=%g dot=%g violation=%g threshold=%g complements=%lld", + static_cast(cut.i.size()), + static_cast(cut_rhs), + static_cast(dot), + static_cast(violation), + static_cast(min_violation), + static_cast(num_complements)); return clique_cut_build_status_t::NO_CUT; } @@ -122,13 +167,7 @@ struct bk_bitset_context_t { bool add_work(f_t accesses) { - if (work_estimate == nullptr) { return false; } - *work_estimate += accesses; - if (*work_estimate > max_work_estimate) { - work_limit_reached = true; - return true; - } - return false; + return add_work_estimate(accesses, work_estimate, max_work_estimate, &work_limit_reached); } bool over_work_limit() const @@ -296,14 +335,13 @@ void extend_clique_vertices(std::vector& clique_vertices, f_t* work_estimate, f_t max_work_estimate) { - auto add_work = [&](f_t accesses) -> bool { - if (work_estimate == nullptr) { return false; } - *work_estimate += accesses; - return *work_estimate > max_work_estimate; - }; - if (toc(start_time) >= time_limit) { return; } if (clique_vertices.empty()) { return; } +#ifdef DEBUG_CLIQUE_CUTS + const size_t initial_clique_vertices = clique_vertices.size(); +#endif + CLIQUE_CUTS_DEBUG("extend_clique_vertices start size=%lld", + static_cast(clique_vertices.size())); const f_t initial_clique_size = static_cast(clique_vertices.size()); i_t smallest_degree = std::numeric_limits::max(); @@ -329,6 +367,12 @@ void extend_clique_vertices(std::vector& clique_vertices, f_t value = candidate >= num_vars ? (1.0 - xstar[var_idx]) : xstar[var_idx]; if (std::abs(value - std::round(value)) <= integer_tol) { candidates.push_back(candidate); } } + CLIQUE_CUTS_DEBUG( + "extend_clique_vertices anchor=%lld degree=%lld adj_size=%lld integer_candidates=%lld", + static_cast(smallest_degree_var), + static_cast(smallest_degree), + static_cast(adj_set.size()), + static_cast(candidates.size())); const f_t candidate_size = static_cast(candidates.size()); const f_t sort_work = candidate_size > 0.0 ? 6.0 * candidate_size * std::log2(candidate_size + 1.0) : 0.0; @@ -337,7 +381,12 @@ void extend_clique_vertices(std::vector& clique_vertices, const f_t estimated_extension_work = 2.0 * initial_clique_size + 4.0 * static_cast(adj_set.size()) + sort_work + 2.0 * candidate_size * initial_clique_size + 2.0 * candidate_size; - if (add_work(estimated_extension_work)) { return; } + if (add_work_estimate(estimated_extension_work, work_estimate, max_work_estimate)) { + CLIQUE_CUTS_DEBUG("extend_clique_vertices skip work_limit work=%g limit=%g", + work_estimate == nullptr ? -1.0 : static_cast(*work_estimate), + static_cast(max_work_estimate)); + return; + } // sort the candidates by reduced cost. // smaller reduce cost disturbs dual simplex less @@ -371,6 +420,12 @@ void extend_clique_vertices(std::vector& clique_vertices, clique_members.insert(candidate); } } +#ifdef DEBUG_CLIQUE_CUTS + CLIQUE_CUTS_DEBUG("extend_clique_vertices done start=%lld final=%lld added=%lld", + static_cast(initial_clique_vertices), + static_cast(clique_vertices.size()), + static_cast(clique_vertices.size() - initial_clique_vertices)); +#endif } } // namespace @@ -1052,8 +1107,13 @@ bool cut_generation_t::generate_clique_cuts( if (toc(start_time) >= settings.time_limit) { return true; } const i_t num_vars = user_problem_.num_cols; + CLIQUE_CUTS_DEBUG("generate_clique_cuts start num_vars=%lld time_limit=%g elapsed=%g", + static_cast(num_vars), + static_cast(settings.time_limit), + static_cast(toc(start_time))); if (clique_table_ == nullptr) { + CLIQUE_CUTS_DEBUG("generate_clique_cuts building clique table"); ::cuopt::linear_programming::detail::clique_config_t clique_config; clique_config.min_clique_size = 1; clique_table_ = std::make_shared<::cuopt::linear_programming::detail::clique_table_t>( @@ -1073,9 +1133,19 @@ bool cut_generation_t::generate_clique_cuts( ::cuopt::linear_programming::detail::build_clique_table( user_problem_, *clique_table_, tolerances, true, true, clique_build_timer); if (clique_build_timer.check_time_limit()) { return true; } + CLIQUE_CUTS_DEBUG("generate_clique_cuts clique table built first=%lld addtl=%lld", + static_cast(clique_table_->first.size()), + static_cast(clique_table_->addtl_cliques.size())); + } else { + CLIQUE_CUTS_DEBUG("generate_clique_cuts reusing clique table first=%lld addtl=%lld", + static_cast(clique_table_->first.size()), + static_cast(clique_table_->addtl_cliques.size())); } - if (clique_table_->first.empty() && clique_table_->addtl_cliques.empty()) { return true; } + if (clique_table_->first.empty() && clique_table_->addtl_cliques.empty()) { + CLIQUE_CUTS_DEBUG("generate_clique_cuts empty clique table, nothing to separate"); + return true; + } cuopt_assert(clique_table_->n_variables == num_vars, "Clique table variable count mismatch"); cuopt_assert(static_cast(num_vars) <= xstar.size(), "Clique cut xstar size mismatch"); @@ -1084,9 +1154,9 @@ bool cut_generation_t::generate_clique_cuts( const f_t bound_tol = settings.primal_tol; const f_t min_weight = 1.0 + min_violation; // TODO this can be problem dependent - const i_t max_calls = 100000; + const i_t max_calls = 1000000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 2e9; + const f_t max_work_estimate = 2e11; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); @@ -1114,7 +1184,13 @@ bool cut_generation_t::generate_clique_cuts( work_estimate += 4.0 * static_cast(num_vars) + 2.0 * static_cast(vertices.size()); if (work_estimate > max_work_estimate) { return true; } - if (vertices.empty()) { return true; } + if (vertices.empty()) { + CLIQUE_CUTS_DEBUG("generate_clique_cuts no fractional binary vertices"); + return true; + } + CLIQUE_CUTS_DEBUG("generate_clique_cuts fractional subgraph vertices=%lld (literals=%lld)", + static_cast(vertices.size() / 2), + static_cast(vertices.size())); std::vector vertex_to_local(2 * num_vars, -1); std::vector in_subgraph(2 * num_vars, 0); @@ -1158,6 +1234,9 @@ bool cut_generation_t::generate_clique_cuts( work_estimate += static_cast(vertices.size()) + static_cast(total_adj_entries) + 2.0 * static_cast(kept_adj_entries); if (work_estimate > max_work_estimate) { return true; } + CLIQUE_CUTS_DEBUG("generate_clique_cuts adjacency raw_entries=%lld kept_entries=%lld", + static_cast(total_adj_entries), + static_cast(kept_adj_entries)); const size_t words = bitset_words(vertices.size()); std::vector> adj_bitset(vertices.size(), std::vector(words, 0)); @@ -1174,6 +1253,9 @@ bool cut_generation_t::generate_clique_cuts( } work_estimate += static_cast(adj_local.size()) + 3.0 * static_cast(local_adj_entries); if (work_estimate > max_work_estimate) { return true; } + CLIQUE_CUTS_DEBUG("generate_clique_cuts bitset graph words=%lld local_entries=%lld", + static_cast(words), + static_cast(local_adj_entries)); bk_bitset_context_t ctx{adj_bitset, weights, @@ -1193,14 +1275,29 @@ bool cut_generation_t::generate_clique_cuts( work_estimate += 2.0 * static_cast(vertices.size()); if (work_estimate > max_work_estimate) { return true; } bron_kerbosch(ctx, R, P, X, 0.0); + CLIQUE_CUTS_DEBUG( + "generate_clique_cuts maximal cliques found=%lld bk_calls=%lld work=%g work_limit=%d", + static_cast(ctx.cliques.size()), + static_cast(ctx.num_calls), + static_cast(work_estimate), + ctx.over_work_limit() ? 1 : 0); if (ctx.over_work_limit()) { return true; } if (toc(start_time) >= settings.time_limit) { return true; } if (work_estimate > max_work_estimate) { return true; } sparse_vector_t cut(lp.num_cols, 0); f_t cut_rhs = 0.0; +#ifdef DEBUG_CLIQUE_CUTS + size_t candidate_cliques = 0; + size_t added_cuts = 0; + size_t rejected_cliques = 0; + size_t extension_gain = 0; +#endif for (auto& clique_local : ctx.cliques) { if (toc(start_time) >= settings.time_limit) { return true; } +#ifdef DEBUG_CLIQUE_CUTS + candidate_cliques++; +#endif std::vector clique_vertices; clique_vertices.reserve(clique_local.size()); for (auto local_idx : clique_local) { @@ -1208,6 +1305,9 @@ bool cut_generation_t::generate_clique_cuts( } work_estimate += 3.0 * static_cast(clique_local.size()) + 1.0; if (work_estimate > max_work_estimate) { return true; } +#ifdef DEBUG_CLIQUE_CUTS + const size_t size_before_extension = clique_vertices.size(); +#endif extend_clique_vertices(clique_vertices, *clique_table_, xstar, @@ -1218,6 +1318,9 @@ bool cut_generation_t::generate_clique_cuts( settings.time_limit, &work_estimate, max_work_estimate); +#ifdef DEBUG_CLIQUE_CUTS + extension_gain += clique_vertices.size() - size_before_extension; +#endif if (work_estimate > max_work_estimate) { return true; } if (toc(start_time) >= settings.time_limit) { return true; } const auto build_status = build_clique_cut(clique_vertices, @@ -1235,12 +1338,37 @@ bool cut_generation_t::generate_clique_cuts( if (work_estimate > max_work_estimate) { return true; } if (build_status == clique_cut_build_status_t::INFEASIBLE) { settings.log.debug("Detected contradictory variable/complement clique\n"); + CLIQUE_CUTS_DEBUG( + "generate_clique_cuts infeasible clique detected after processing=%lld cliques", + static_cast(candidate_cliques)); return false; } if (build_status == clique_cut_build_status_t::CUT_ADDED) { cut_pool_.add_cut(cut_type_t::CLIQUE, cut, cut_rhs); +#ifdef DEBUG_CLIQUE_CUTS + added_cuts++; + CLIQUE_CUTS_DEBUG("generate_clique_cuts added cut nz=%lld rhs=%g clique_size=%lld", + static_cast(cut.i.size()), + static_cast(cut_rhs), + static_cast(clique_vertices.size())); +#endif } +#ifdef DEBUG_CLIQUE_CUTS + else { + rejected_cliques++; + } +#endif } +#ifdef DEBUG_CLIQUE_CUTS + CLIQUE_CUTS_DEBUG( + "generate_clique_cuts done candidate_cliques=%lld added=%lld rejected=%lld extension_gain=%lld " + "final_work=%g", + static_cast(candidate_cliques), + static_cast(added_cuts), + static_cast(rejected_cliques), + static_cast(extension_gain), + static_cast(work_estimate)); +#endif return true; } diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index b352de70ae..3302f75111 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -92,6 +92,19 @@ f_t fractional_part(f_t a) return a - std::floor(a); } +template +bool add_work_estimate(f_t accesses, + f_t* work_estimate, + f_t max_work_estimate, + bool* work_limit_reached = nullptr) +{ + if (work_estimate == nullptr) { return false; } + *work_estimate += accesses; + const bool over_work_limit = *work_estimate > max_work_estimate; + if (over_work_limit && work_limit_reached != nullptr) { *work_limit_reached = true; } + return over_work_limit; +} + // Computes a permutation of a score vector that puts the highest scores first template void best_score_first_permutation(std::vector& scores, std::vector& permutation) diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 1b283f19b5..aec6b0850b 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include #include #include @@ -228,6 +230,34 @@ detail::clique_table_t build_clique_table_for_model( return build_clique_table_for_model_with_min_size(handle, model, 1); } +mps_parser::mps_data_model_t& get_neos8_model_cached() +{ + static std::once_flag init_flag; + static std::unique_ptr> model_ptr; + std::call_once(init_flag, []() { + const auto neos8_path = make_path_absolute("mip/neos8.mps"); + auto neos8_model = cuopt::mps_parser::parse_mps(neos8_path, false); + model_ptr = std::make_unique>(std::move(neos8_model)); + }); + cuopt_assert(model_ptr != nullptr, "Failed to initialize cached neos8 model"); + return *model_ptr; +} + +detail::clique_table_t& get_neos8_clique_table_cached() +{ + static std::once_flag init_flag; + static std::unique_ptr> clique_table_ptr; + std::call_once(init_flag, []() { + const raft::handle_t handle{}; + auto& neos8_model = get_neos8_model_cached(); + auto clique_table = build_clique_table_for_model(handle, neos8_model); + clique_table_ptr = + std::make_unique>(std::move(clique_table)); + }); + cuopt_assert(clique_table_ptr != nullptr, "Failed to initialize cached neos8 clique table"); + return *clique_table_ptr; +} + std::vector> build_original_adjacency_matrix( detail::clique_table_t& clique_table, int num_vars) { @@ -430,6 +460,372 @@ std::optional isolate_first_invalid_cut_by_bisection( return lo; } +struct neos8_mip_solution_cache_t { + mip_termination_status_t status; + std::vector primal; + double objective; +}; + +struct neos8_lp_solution_cache_t { + pdlp_termination_status_t status; + std::vector primal; +}; + +neos8_mip_solution_cache_t& get_neos8_optimal_solution_no_cuts_cached() +{ + static std::once_flag init_flag; + static std::unique_ptr solution_ptr; + std::call_once(init_flag, []() { + const raft::handle_t handle{}; + auto& neos8_model = get_neos8_model_cached(); + mip_solver_settings_t settings; + settings.time_limit = 120.0; + settings.presolver = presolver_t::None; + disable_all_cuts(settings); + + auto mip_solution = solve_mip(&handle, neos8_model, settings); + auto cache = std::make_unique(); + cache->status = mip_solution.get_termination_status(); + cache->objective = mip_solution.get_objective_value(); + cache->primal = cuopt::host_copy(mip_solution.get_solution(), handle.get_stream()); + solution_ptr = std::move(cache); + }); + cuopt_assert(solution_ptr != nullptr, "Failed to initialize cached neos8 no-cut MIP solution"); + return *solution_ptr; +} + +neos8_lp_solution_cache_t& get_neos8_lp_relaxation_solution_cached() +{ + static std::once_flag init_flag; + static std::unique_ptr solution_ptr; + std::call_once(init_flag, []() { + const raft::handle_t handle{}; + auto lp_relaxation = get_neos8_model_cached(); + std::vector all_continuous(lp_relaxation.get_n_variables(), 'C'); + lp_relaxation.set_variable_types(all_continuous); + + pdlp_solver_settings_t lp_settings{}; + lp_settings.time_limit = 120.0; + lp_settings.presolver = presolver_t::None; + lp_settings.set_optimality_tolerance(1e-8); + + auto lp_solution = solve_lp(&handle, lp_relaxation, lp_settings); + auto cache = std::make_unique(); + cache->status = lp_solution.get_termination_status(); + cache->primal = cuopt::host_copy(lp_solution.get_primal_solution(), handle.get_stream()); + solution_ptr = std::move(cache); + }); + cuopt_assert(solution_ptr != nullptr, "Failed to initialize cached neos8 LP relaxation solution"); + return *solution_ptr; +} + +bool is_binary_var_for_clique_literals(const mps_parser::mps_data_model_t& problem, + int var_idx, + double bound_tol) +{ + const auto& var_types = problem.get_variable_types(); + const auto& var_lb = problem.get_variable_lower_bounds(); + const auto& var_ub = problem.get_variable_upper_bounds(); + return var_types[var_idx] != 'C' && var_lb[var_idx] >= -bound_tol && + var_ub[var_idx] <= 1.0 + bound_tol; +} + +std::vector> build_fractional_literal_cliques_for_assignment( + const mps_parser::mps_data_model_t& problem, + detail::clique_table_t& clique_table, + const std::vector& assignment, + double integer_tol, + double bound_tol, + int max_calls) +{ + const int num_vars = problem.get_n_variables(); + cuopt_assert(static_cast(assignment.size()) >= num_vars, + "Assignment size mismatch in fractional literal clique builder"); + + std::vector vertices; + std::vector weights; + vertices.reserve(2 * num_vars); + weights.reserve(2 * num_vars); + for (int j = 0; j < num_vars; ++j) { + if (!is_binary_var_for_clique_literals(problem, j, bound_tol)) { continue; } + const double xj = assignment[j]; + if (std::abs(xj - std::round(xj)) <= integer_tol) { continue; } + vertices.push_back(j); + weights.push_back(xj); + vertices.push_back(j + num_vars); + weights.push_back(1.0 - xj); + } + if (vertices.empty()) { return {}; } + + std::vector vertex_to_local(2 * num_vars, -1); + std::vector in_subgraph(2 * num_vars, 0); + for (size_t idx = 0; idx < vertices.size(); ++idx) { + vertex_to_local[vertices[idx]] = static_cast(idx); + in_subgraph[vertices[idx]] = 1; + } + + std::vector> adj_local(vertices.size()); + for (size_t idx = 0; idx < vertices.size(); ++idx) { + const auto vertex_idx = vertices[idx]; + auto adj_set = clique_table.get_adj_set_of_var(vertex_idx); + auto& adj = adj_local[idx]; + adj.reserve(adj_set.size()); + for (const auto neighbor : adj_set) { + cuopt_assert(neighbor >= 0 && neighbor < 2 * num_vars, + "Neighbor out of range in fractional literal clique builder"); + if (!in_subgraph[neighbor]) { continue; } + const auto local_neighbor = vertex_to_local[neighbor]; + if (local_neighbor >= 0) { adj.push_back(local_neighbor); } + } + } + + auto cliques_local = dual_simplex::find_maximal_cliques_for_test( + adj_local, weights, 1.0 + kCliqueTestTol, max_calls, std::numeric_limits::infinity()); + std::vector> cliques_global; + cliques_global.reserve(cliques_local.size()); + for (auto& local_clique : cliques_local) { + std::vector global_clique; + global_clique.reserve(local_clique.size()); + for (const auto local_idx : local_clique) { + cuopt_assert(local_idx >= 0 && static_cast(local_idx) < vertices.size(), + "Local clique index out of range"); + global_clique.push_back(vertices[local_idx]); + } + cliques_global.push_back(std::move(global_clique)); + } + return canonicalize_cliques(std::move(cliques_global)); +} + +std::vector>& get_neos8_fractional_literal_cliques_cached() +{ + static std::once_flag init_flag; + static std::unique_ptr>> cliques_ptr; + std::call_once(init_flag, []() { + auto& neos8_model = get_neos8_model_cached(); + auto& clique_table = get_neos8_clique_table_cached(); + auto& lp_relaxation = get_neos8_lp_relaxation_solution_cached(); + auto cliques = build_fractional_literal_cliques_for_assignment( + neos8_model, clique_table, lp_relaxation.primal, kCliqueTestTol, kCliqueTestTol, 100000); + cliques_ptr = std::make_unique>>(std::move(cliques)); + }); + cuopt_assert(cliques_ptr != nullptr, "Failed to initialize cached neos8 dumped literal cliques"); + return *cliques_ptr; +} + +double literal_clique_cut_violation(const std::vector& literal_clique, + const std::vector& assignment, + int num_vars) +{ + cuopt_assert(static_cast(assignment.size()) >= num_vars, + "Assignment size mismatch in literal clique violation"); + double dot = 0.0; + int num_complement_vars = 0; + for (const auto literal : literal_clique) { + cuopt_assert(literal >= 0 && literal < 2 * num_vars, "Literal out of range"); + const int var_idx = literal % num_vars; + const bool is_complement = literal >= num_vars; + if (is_complement) { + num_complement_vars++; + dot += assignment[var_idx]; + } else { + dot -= assignment[var_idx]; + } + } + const double rhs = static_cast(num_complement_vars - 1); + return rhs - dot; +} + +std::string format_phase2_literal_panic_dump(const std::vector& literal_clique, + const std::vector& incumbent, + int num_vars) +{ + std::ostringstream out; + out << "\nLiteral clique:"; + for (const auto literal : literal_clique) { + const bool is_complement = literal >= num_vars; + const int var_idx = literal % num_vars; + out << " " << (is_complement ? "~x" : "x") << var_idx << "(value=" << incumbent[var_idx] << ")"; + } + out << "\nViolation: " << literal_clique_cut_violation(literal_clique, incumbent, num_vars); + return out.str(); +} + +bool literal_cut_is_invalid_for_incumbent(const std::vector& literal_clique, + const std::vector& incumbent, + int num_vars, + double tol) +{ + return literal_clique_cut_violation(literal_clique, incumbent, num_vars) > tol; +} + +bool prefix_has_invalid_literal_cut(const std::vector>& dumped_cuts, + size_t prefix_end_exclusive, + const std::vector& incumbent, + int num_vars, + double tol) +{ + for (size_t i = 0; i < prefix_end_exclusive; ++i) { + if (literal_cut_is_invalid_for_incumbent(dumped_cuts[i], incumbent, num_vars, tol)) { + return true; + } + } + return false; +} + +std::optional isolate_first_invalid_literal_cut_by_bisection( + const std::vector>& dumped_cuts, + const std::vector& incumbent, + int num_vars, + double tol) +{ + if (!prefix_has_invalid_literal_cut(dumped_cuts, dumped_cuts.size(), incumbent, num_vars, tol)) { + return std::nullopt; + } + size_t lo = 0; + size_t hi = dumped_cuts.size() - 1; + while (lo < hi) { + const size_t mid = lo + (hi - lo) / 2; + if (prefix_has_invalid_literal_cut(dumped_cuts, mid + 1, incumbent, num_vars, tol)) { + hi = mid; + } else { + lo = mid + 1; + } + } + return lo; +} + +mps_parser::mps_data_model_t& get_neos8_lp_relaxation_model_cached() +{ + static std::once_flag init_flag; + static std::unique_ptr> model_ptr; + std::call_once(init_flag, []() { + auto lp_relaxation = get_neos8_model_cached(); + std::vector all_continuous(lp_relaxation.get_n_variables(), 'C'); + lp_relaxation.set_variable_types(all_continuous); + model_ptr = + std::make_unique>(std::move(lp_relaxation)); + }); + cuopt_assert(model_ptr != nullptr, "Failed to initialize cached neos8 LP relaxation model"); + return *model_ptr; +} + +mps_parser::mps_data_model_t append_literal_cut_prefix_to_lp_model( + const mps_parser::mps_data_model_t& base_lp_model, + const std::vector>& dumped_cuts, + size_t prefix_end_exclusive, + int num_vars) +{ + auto model_with_cuts = base_lp_model; + if (prefix_end_exclusive == 0) { return model_with_cuts; } + + std::vector matrix_values = base_lp_model.get_constraint_matrix_values(); + std::vector matrix_indices = base_lp_model.get_constraint_matrix_indices(); + std::vector matrix_offsets = base_lp_model.get_constraint_matrix_offsets(); + std::vector constraint_lbs = base_lp_model.get_constraint_lower_bounds(); + std::vector constraint_ubs = base_lp_model.get_constraint_upper_bounds(); + if (matrix_offsets.empty()) { matrix_offsets.push_back(0); } + + const size_t cuts_to_apply = std::min(prefix_end_exclusive, dumped_cuts.size()); + for (size_t cut_idx = 0; cut_idx < cuts_to_apply; ++cut_idx) { + const auto& literal_cut = dumped_cuts[cut_idx]; + + std::vector row_vars; + std::vector row_coeffs; + row_vars.reserve(literal_cut.size()); + row_coeffs.reserve(literal_cut.size()); + + int num_complements = 0; + for (const auto literal : literal_cut) { + cuopt_assert(literal >= 0 && literal < 2 * num_vars, + "Literal out of range for LP cut append"); + const int var_idx = literal % num_vars; + const bool is_complement = literal >= num_vars; + if (is_complement) { num_complements++; } + const double coeff = is_complement ? 1.0 : -1.0; + + bool found = false; + for (size_t t = 0; t < row_vars.size(); ++t) { + if (row_vars[t] == var_idx) { + row_coeffs[t] += coeff; + found = true; + break; + } + } + if (!found) { + row_vars.push_back(var_idx); + row_coeffs.push_back(coeff); + } + } + + std::vector order(row_vars.size()); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [&](int a, int b) { return row_vars[a] < row_vars[b]; }); + for (const auto pos : order) { + const double coeff = row_coeffs[pos]; + if (std::abs(coeff) <= 1e-12) { continue; } + matrix_indices.push_back(row_vars[pos]); + matrix_values.push_back(coeff); + } + matrix_offsets.push_back(static_cast(matrix_indices.size())); + constraint_lbs.push_back(static_cast(num_complements - 1)); + constraint_ubs.push_back(std::numeric_limits::infinity()); + } + + model_with_cuts.set_csr_constraint_matrix(matrix_values.data(), + matrix_values.size(), + matrix_indices.data(), + matrix_indices.size(), + matrix_offsets.data(), + matrix_offsets.size()); + model_with_cuts.set_constraint_lower_bounds(constraint_lbs.data(), constraint_lbs.size()); + model_with_cuts.set_constraint_upper_bounds(constraint_ubs.data(), constraint_ubs.size()); + return model_with_cuts; +} + +pdlp_termination_status_t solve_lp_with_literal_cut_prefix( + const std::vector>& dumped_cuts, size_t prefix_end_exclusive, int num_vars) +{ + const raft::handle_t handle{}; + auto& base_lp_model = get_neos8_lp_relaxation_model_cached(); + auto model_with_cuts = append_literal_cut_prefix_to_lp_model( + base_lp_model, dumped_cuts, prefix_end_exclusive, num_vars); + + pdlp_solver_settings_t lp_settings{}; + lp_settings.time_limit = 120.0; + lp_settings.presolver = presolver_t::None; + lp_settings.set_optimality_tolerance(1e-8); + + auto lp_solution = solve_lp(&handle, model_with_cuts, lp_settings); + return lp_solution.get_termination_status(); +} + +bool prefix_makes_lp_relaxation_infeasible(const std::vector>& dumped_cuts, + size_t prefix_end_exclusive, + int num_vars) +{ + const auto status = solve_lp_with_literal_cut_prefix(dumped_cuts, prefix_end_exclusive, num_vars); + return status == pdlp_termination_status_t::PrimalInfeasible; +} + +std::optional isolate_first_lp_infeasible_literal_cut_by_bisection( + const std::vector>& dumped_cuts, int num_vars) +{ + if (!prefix_makes_lp_relaxation_infeasible(dumped_cuts, dumped_cuts.size(), num_vars)) { + return std::nullopt; + } + size_t lo = 0; + size_t hi = dumped_cuts.size() - 1; + while (lo < hi) { + const size_t mid = lo + (hi - lo) / 2; + if (prefix_makes_lp_relaxation_infeasible(dumped_cuts, mid + 1, num_vars)) { + hi = mid; + } else { + lo = mid + 1; + } + } + return lo; +} + } // namespace // Problem data for the mixed integer linear programming problem @@ -780,4 +1176,206 @@ TEST(cuts, clique_phase5_ignores_fractional_binary_bounds) EXPECT_FALSE(clique_table.check_adjacency(0, 1)); } +TEST(cuts, clique_neos8_phase1_addtl_indices_and_nonempty_graph) +{ + auto& clique_table = get_neos8_clique_table_cached(); + EXPECT_TRUE(!clique_table.first.empty() || !clique_table.addtl_cliques.empty()); + + const size_t max_addtl_to_check = std::min(clique_table.addtl_cliques.size(), 400); + for (size_t k = 0; k < max_addtl_to_check; ++k) { + const auto& addtl = clique_table.addtl_cliques[k]; + ASSERT_GE(addtl.clique_idx, 0); + ASSERT_LT(static_cast(addtl.clique_idx), clique_table.first.size()); + const auto& base = clique_table.first[addtl.clique_idx]; + ASSERT_GE(addtl.start_pos_on_clique, 0); + ASSERT_LE(static_cast(addtl.start_pos_on_clique), base.size()); + } +} + +TEST(cuts, clique_neos8_phase1_addtl_suffix_conflicts_materialized) +{ + auto& clique_table = get_neos8_clique_table_cached(); + if (clique_table.addtl_cliques.empty()) { + GTEST_SKIP() << "neos8 produced no additional cliques in this configuration"; + } + + size_t checked_addtl = 0; + const size_t max_addtl_to_check = std::min(clique_table.addtl_cliques.size(), 200); + for (size_t k = 0; k < max_addtl_to_check; ++k) { + const auto& addtl = clique_table.addtl_cliques[k]; + if (addtl.clique_idx < 0 || + static_cast(addtl.clique_idx) >= clique_table.first.size()) { + continue; + } + const auto& base = clique_table.first[addtl.clique_idx]; + const size_t start_at = static_cast(addtl.start_pos_on_clique); + if (start_at >= base.size()) { continue; } + + const size_t end_at = std::min(base.size(), start_at + 8); + for (size_t p = start_at; p < end_at; ++p) { + EXPECT_TRUE(clique_table.check_adjacency(addtl.vertex_idx, base[p])); + EXPECT_TRUE(clique_table.check_adjacency(base[p], addtl.vertex_idx)); + } + checked_addtl++; + } + EXPECT_GT(checked_addtl, 0); +} + +TEST(cuts, clique_neos8_phase1_symmetry_and_degree_cache_consistency) +{ + auto& clique_table = get_neos8_clique_table_cached(); + const int n_vertices = static_cast(clique_table.var_clique_map_first.size()); + ASSERT_GT(n_vertices, 0); + + const int sample_size = std::min(n_vertices, 24); + const int stride = std::max(1, n_vertices / sample_size); + std::vector sampled_vertices(sample_size); + for (int i = 0; i < sample_size; ++i) { + sampled_vertices[i] = (i * stride) % n_vertices; + } + + for (const auto v : sampled_vertices) { + const auto deg_cached = clique_table.get_degree_of_var(v); + const auto adj_set = clique_table.get_adj_set_of_var(v); + EXPECT_EQ(deg_cached, static_cast(adj_set.size())); + EXPECT_EQ(deg_cached, clique_table.get_degree_of_var(v)); + } + + for (int i = 0; i < sample_size; ++i) { + for (int j = i + 1; j < sample_size; ++j) { + const auto v1 = sampled_vertices[i]; + const auto v2 = sampled_vertices[j]; + EXPECT_EQ(clique_table.check_adjacency(v1, v2), clique_table.check_adjacency(v2, v1)); + } + } +} + +TEST(cuts, clique_neos8_phase2_no_cut_off_optimal_solution_validation) +{ + auto& no_cut_mip = get_neos8_optimal_solution_no_cuts_cached(); + ASSERT_EQ(no_cut_mip.status, mip_termination_status_t::Optimal); + + auto& lp_relaxation = get_neos8_lp_relaxation_solution_cached(); + ASSERT_EQ(lp_relaxation.status, pdlp_termination_status_t::Optimal); + + auto& dumped_literal_cuts = get_neos8_fractional_literal_cliques_cached(); + if (dumped_literal_cuts.empty()) { + GTEST_SKIP() << "neos8 produced no candidate literal cliques from LP relaxation"; + } + + const int num_vars = get_neos8_model_cached().get_n_variables(); + for (size_t i = 0; i < dumped_literal_cuts.size(); ++i) { + const double violation = + literal_clique_cut_violation(dumped_literal_cuts[i], no_cut_mip.primal, num_vars); + ASSERT_LE(violation, kCliqueTestTol) + << "Invalid clique cut at index " << i + << format_phase2_literal_panic_dump(dumped_literal_cuts[i], no_cut_mip.primal, num_vars); + } +} + +TEST(cuts, clique_neos8_phase3_fractional_separation_must_cut_off) +{ + auto& lp_relaxation = get_neos8_lp_relaxation_solution_cached(); + ASSERT_EQ(lp_relaxation.status, pdlp_termination_status_t::Optimal); + + auto& dumped_literal_cuts = get_neos8_fractional_literal_cliques_cached(); + if (dumped_literal_cuts.empty()) { + GTEST_SKIP() << "neos8 produced no candidate literal cliques from LP relaxation"; + } + + const int num_vars = get_neos8_model_cached().get_n_variables(); + for (size_t i = 0; i < dumped_literal_cuts.size(); ++i) { + const double violation = + literal_clique_cut_violation(dumped_literal_cuts[i], lp_relaxation.primal, num_vars); + ASSERT_GT(violation, kCliqueTestTol) + << "Non-separating clique cut at index " << i + << format_phase2_literal_panic_dump(dumped_literal_cuts[i], lp_relaxation.primal, num_vars); + } +} + +TEST(cuts, clique_neos8_phase4_fault_isolation_binary_search) +{ + auto& no_cut_mip = get_neos8_optimal_solution_no_cuts_cached(); + ASSERT_EQ(no_cut_mip.status, mip_termination_status_t::Optimal); + + auto& dumped_literal_cuts = get_neos8_fractional_literal_cliques_cached(); + if (dumped_literal_cuts.empty()) { + GTEST_SKIP() << "neos8 produced no candidate literal cliques from LP relaxation"; + } + + const auto& model = get_neos8_model_cached(); + const int num_vars = model.get_n_variables(); + + // Real dumped cuts should not invalidate the no-cut incumbent. + EXPECT_FALSE(prefix_has_invalid_literal_cut( + dumped_literal_cuts, dumped_literal_cuts.size(), no_cut_mip.primal, num_vars, kCliqueTestTol)); + + // Inject a known-invalid cut and verify bisection isolates it. + std::vector incumbent_ones; + incumbent_ones.reserve(2); + for (int j = 0; j < num_vars && incumbent_ones.size() < 2; ++j) { + if (!is_binary_var_for_clique_literals(model, j, kCliqueTestTol)) { continue; } + if (no_cut_mip.primal[j] >= 1.0 - kCliqueTestTol) { incumbent_ones.push_back(j); } + } + if (incumbent_ones.size() < 2) { + GTEST_SKIP() << "Could not find two binary variables fixed to one in neos8 incumbent"; + } + + auto cuts_with_injected_bug = dumped_literal_cuts; + const size_t injected_index = cuts_with_injected_bug.size(); + cuts_with_injected_bug.push_back({incumbent_ones[0], incumbent_ones[1]}); + + auto first_invalid = isolate_first_invalid_literal_cut_by_bisection( + cuts_with_injected_bug, no_cut_mip.primal, num_vars, kCliqueTestTol); + ASSERT_TRUE(first_invalid.has_value()); + EXPECT_EQ(first_invalid.value(), injected_index); +} + +TEST(cuts, clique_neos8_phase4_lp_infeasibility_binary_search) +{ + auto& dumped_literal_cuts = get_neos8_fractional_literal_cliques_cached(); + if (dumped_literal_cuts.empty()) { + GTEST_SKIP() << "neos8 produced no candidate literal cliques from LP relaxation"; + } + + const auto& model = get_neos8_model_cached(); + const int num_vars = model.get_n_variables(); + + std::vector> cuts_for_lp_search; + const size_t max_real_cuts = std::min(dumped_literal_cuts.size(), 64); + cuts_for_lp_search.insert(cuts_for_lp_search.end(), + dumped_literal_cuts.begin(), + dumped_literal_cuts.begin() + max_real_cuts); + + int inject_var = -1; + for (int j = 0; j < num_vars; ++j) { + if (is_binary_var_for_clique_literals(model, j, kCliqueTestTol)) { + inject_var = j; + break; + } + } + if (inject_var < 0) { + GTEST_SKIP() << "Could not find a binary variable for LP infeasibility injection"; + } + + const size_t injected_index = cuts_for_lp_search.size(); + cuts_for_lp_search.push_back( + {inject_var, inject_var, inject_var + num_vars, inject_var + num_vars}); + + // Prefix before injected cut should remain LP-feasible. + const auto status_before_injection = + solve_lp_with_literal_cut_prefix(cuts_for_lp_search, injected_index, num_vars); + EXPECT_NE(status_before_injection, pdlp_termination_status_t::PrimalInfeasible); + + // Full prefix should be LP-infeasible due to injected contradictory cut. + const auto status_with_injection = + solve_lp_with_literal_cut_prefix(cuts_for_lp_search, cuts_for_lp_search.size(), num_vars); + EXPECT_EQ(status_with_injection, pdlp_termination_status_t::PrimalInfeasible); + + auto first_infeasible = + isolate_first_lp_infeasible_literal_cut_by_bisection(cuts_for_lp_search, num_vars); + ASSERT_TRUE(first_infeasible.has_value()); + EXPECT_EQ(first_infeasible.value(), injected_index); +} + } // namespace cuopt::linear_programming::test From 86cfe6a7ea0f8b812b541c57f32b513f787410a6 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 00:07:45 -0800 Subject: [PATCH 076/147] add tests that problem concersions are correct --- cpp/tests/mip/problem_test.cu | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/cpp/tests/mip/problem_test.cu b/cpp/tests/mip/problem_test.cu index 28f4f1f955..92fa6d41d1 100644 --- a/cpp/tests/mip/problem_test.cu +++ b/cpp/tests/mip/problem_test.cu @@ -34,6 +34,7 @@ #include #include +#include #include #include #include @@ -204,6 +205,95 @@ TEST(problem, run_small_tests) } } +namespace ds = cuopt::linear_programming::dual_simplex; + +template +void test_roundtrip_equivalence(i_t n_cnst, i_t n_var) +{ + raft::handle_t handle; + auto op_problem = create_problem(&handle, n_cnst, n_var); + dtl::problem_t problem(op_problem); + problem.preprocess_problem(); + + auto stream = handle.get_stream(); + + const auto n_constraints_before = problem.n_constraints; + const auto n_variables_before = problem.n_variables; + const auto nnz_before = problem.nnz; + + auto coefficients_before = cuopt::host_copy(problem.coefficients, stream); + auto variables_before = cuopt::host_copy(problem.variables, stream); + auto offsets_before = cuopt::host_copy(problem.offsets, stream); + auto constraint_lower_before = cuopt::host_copy(problem.constraint_lower_bounds, stream); + auto constraint_upper_before = cuopt::host_copy(problem.constraint_upper_bounds, stream); + auto variable_bounds_before = cuopt::host_copy(problem.variable_bounds, stream); + auto objective_before = cuopt::host_copy(problem.objective_coefficients, stream); + auto reverse_coefficients_before = cuopt::host_copy(problem.reverse_coefficients, stream); + auto reverse_constraints_before = cuopt::host_copy(problem.reverse_constraints, stream); + auto reverse_offsets_before = cuopt::host_copy(problem.reverse_offsets, stream); + + ds::user_problem_t host_problem(problem.handle_ptr); + problem.get_host_user_problem(host_problem); + + problem.set_constraints_from_host_user_problem(host_problem); + ASSERT_EQ(host_problem.lower.size(), static_cast(problem.n_variables)); + ASSERT_EQ(host_problem.upper.size(), static_cast(problem.n_variables)); + std::vector all_var_indices(problem.n_variables); + std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + problem.update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + + EXPECT_EQ(problem.n_constraints, n_constraints_before); + EXPECT_EQ(problem.n_variables, n_variables_before); + EXPECT_EQ(problem.nnz, nnz_before); + + auto coefficients_after = cuopt::host_copy(problem.coefficients, stream); + auto variables_after = cuopt::host_copy(problem.variables, stream); + auto offsets_after = cuopt::host_copy(problem.offsets, stream); + auto constraint_lower_after = cuopt::host_copy(problem.constraint_lower_bounds, stream); + auto constraint_upper_after = cuopt::host_copy(problem.constraint_upper_bounds, stream); + auto variable_bounds_after = cuopt::host_copy(problem.variable_bounds, stream); + auto objective_after = cuopt::host_copy(problem.objective_coefficients, stream); + auto reverse_coefficients_after = cuopt::host_copy(problem.reverse_coefficients, stream); + auto reverse_constraints_after = cuopt::host_copy(problem.reverse_constraints, stream); + auto reverse_offsets_after = cuopt::host_copy(problem.reverse_offsets, stream); + + EXPECT_EQ(coefficients_before, coefficients_after) << "CSR coefficients differ"; + EXPECT_EQ(variables_before, variables_after) << "CSR column indices differ"; + EXPECT_EQ(offsets_before, offsets_after) << "CSR row offsets differ"; + EXPECT_EQ(objective_before, objective_after) << "objective coefficients differ"; + EXPECT_EQ(reverse_constraints_before, reverse_constraints_after) << "reverse constraints differ"; + EXPECT_EQ(reverse_offsets_before, reverse_offsets_after) << "reverse offsets differ"; + EXPECT_EQ(reverse_coefficients_before, reverse_coefficients_after) + << "reverse coefficients differ"; + + ASSERT_EQ(constraint_lower_before.size(), constraint_lower_after.size()); + for (size_t i = 0; i < constraint_lower_before.size(); ++i) { + EXPECT_NEAR(constraint_lower_before[i], constraint_lower_after[i], 1e-10) + << "constraint_lower_bounds[" << i << "]"; + } + ASSERT_EQ(constraint_upper_before.size(), constraint_upper_after.size()); + for (size_t i = 0; i < constraint_upper_before.size(); ++i) { + EXPECT_NEAR(constraint_upper_before[i], constraint_upper_after[i], 1e-10) + << "constraint_upper_bounds[" << i << "]"; + } + + ASSERT_EQ(variable_bounds_before.size(), variable_bounds_after.size()); + for (size_t i = 0; i < variable_bounds_before.size(); ++i) { + EXPECT_DOUBLE_EQ(variable_bounds_before[i].x, variable_bounds_after[i].x) + << "variable_bounds[" << i << "].lower"; + EXPECT_DOUBLE_EQ(variable_bounds_before[i].y, variable_bounds_after[i].y) + << "variable_bounds[" << i << "].upper"; + } +} + +TEST(problem, get_set_host_user_problem_roundtrip_preserves_problem) +{ + std::vector> cnst_var_vals = {{5, 20}, {20, 80}, {40, 200}}; + for (const auto& [nc, nv] : cnst_var_vals) { + test_roundtrip_equivalence(nc, nv); + } +} + static void fill_problem(optimization_problem_t& op_problem) { // Set A_CSR_matrix From b7bb761fd6b81a5321c327ea434d719a7af37597 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 01:22:05 -0800 Subject: [PATCH 077/147] cuts wip --- cpp/src/branch_and_bound/branch_and_bound.cpp | 5 +- cpp/src/cuts/cuts.cpp | 54 +++++++++++++------ 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 9092dfd6f1..3075360bc4 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -43,6 +43,9 @@ namespace cuopt::linear_programming::dual_simplex { +#define CHECK_CUTS_AGAINST_SAVED_SOLUTION 1 +#define PRINT_FRACTIONAL_INFO 1 + namespace { template @@ -2363,7 +2366,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_.num_cols, original_lp_.A.col_start[original_lp_.A.n]); } - + exit(0); set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 820dd5f64a..aa2b39338e 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -23,9 +23,14 @@ namespace cuopt::linear_programming::dual_simplex { namespace { +#ifndef DEBUG_CLIQUE_CUTS +#define DEBUG_CLIQUE_CUTS 0 +#endif +#define CHECK_WORKSPACE 1 + enum class clique_cut_build_status_t : int8_t { NO_CUT = 0, CUT_ADDED = 1, INFEASIBLE = 2 }; -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS #define CLIQUE_CUTS_DEBUG(...) \ do { \ std::fprintf(stderr, "[DEBUG_CLIQUE_CUTS] "); \ @@ -163,6 +168,7 @@ struct bk_bitset_context_t { f_t max_work_estimate; i_t num_calls{0}; bool work_limit_reached{false}; + bool call_limit_reached{false}; std::vector> cliques; bool add_work(f_t accesses) @@ -176,6 +182,8 @@ struct bk_bitset_context_t { if (work_estimate == nullptr) { return false; } return *work_estimate > max_work_estimate; } + + bool over_call_limit() const { return call_limit_reached || num_calls >= max_calls; } }; inline size_t bitset_words(size_t n) { return (n + 63) / 64; } @@ -221,11 +229,14 @@ void bron_kerbosch(bk_bitset_context_t& ctx, std::vector& X, // already in the clique f_t weight_R) { - if (ctx.over_work_limit()) { return; } + if (ctx.over_work_limit() || ctx.over_call_limit()) { return; } if (toc(ctx.start_time) >= ctx.time_limit) { return; } ctx.num_calls++; // stop the recursion, for perf reasons - if (ctx.num_calls > ctx.max_calls) { return; } + if (ctx.num_calls > ctx.max_calls) { + ctx.call_limit_reached = true; + return; + } // Coarse recursive cost: touch call state + frontiers + weight bookkeeping if (ctx.add_work(static_cast(6 * ctx.words + R.size() + 4))) { return; } @@ -307,6 +318,10 @@ void bron_kerbosch(bk_bitset_context_t& ctx, // note that candidates will include pivot if it is in P for (auto v : candidates) { + if (ctx.over_call_limit()) { + ctx.call_limit_reached = true; + return; + } if (toc(ctx.start_time) >= ctx.time_limit) { return; } R.push_back(v); std::vector P_next(ctx.words, 0); @@ -317,6 +332,10 @@ void bron_kerbosch(bk_bitset_context_t& ctx, } bron_kerbosch(ctx, R, P_next, X_next, weight_R + ctx.weights[v]); if (ctx.over_work_limit()) { return; } + if (ctx.over_call_limit()) { + ctx.call_limit_reached = true; + return; + } R.pop_back(); bitset_clear(P, static_cast(v)); bitset_set(X, static_cast(v)); @@ -337,7 +356,7 @@ void extend_clique_vertices(std::vector& clique_vertices, { if (toc(start_time) >= time_limit) { return; } if (clique_vertices.empty()) { return; } -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS const size_t initial_clique_vertices = clique_vertices.size(); #endif CLIQUE_CUTS_DEBUG("extend_clique_vertices start size=%lld", @@ -420,7 +439,7 @@ void extend_clique_vertices(std::vector& clique_vertices, clique_members.insert(candidate); } } -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS CLIQUE_CUTS_DEBUG("extend_clique_vertices done start=%lld final=%lld added=%lld", static_cast(initial_clique_vertices), static_cast(clique_vertices.size()), @@ -1154,9 +1173,9 @@ bool cut_generation_t::generate_clique_cuts( const f_t bound_tol = settings.primal_tol; const f_t min_weight = 1.0 + min_violation; // TODO this can be problem dependent - const i_t max_calls = 1000000; + const i_t max_calls = 100000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 2e11; + const f_t max_work_estimate = 1e8; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); @@ -1276,18 +1295,21 @@ bool cut_generation_t::generate_clique_cuts( if (work_estimate > max_work_estimate) { return true; } bron_kerbosch(ctx, R, P, X, 0.0); CLIQUE_CUTS_DEBUG( - "generate_clique_cuts maximal cliques found=%lld bk_calls=%lld work=%g work_limit=%d", + "generate_clique_cuts maximal cliques found=%lld bk_calls=%lld work=%g work_limit=%d " + "call_limit=%d", static_cast(ctx.cliques.size()), static_cast(ctx.num_calls), static_cast(work_estimate), - ctx.over_work_limit() ? 1 : 0); + ctx.over_work_limit() ? 1 : 0, + ctx.over_call_limit() ? 1 : 0); + if (ctx.over_call_limit()) { return true; } if (ctx.over_work_limit()) { return true; } if (toc(start_time) >= settings.time_limit) { return true; } if (work_estimate > max_work_estimate) { return true; } sparse_vector_t cut(lp.num_cols, 0); f_t cut_rhs = 0.0; -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS size_t candidate_cliques = 0; size_t added_cuts = 0; size_t rejected_cliques = 0; @@ -1295,7 +1317,7 @@ bool cut_generation_t::generate_clique_cuts( #endif for (auto& clique_local : ctx.cliques) { if (toc(start_time) >= settings.time_limit) { return true; } -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS candidate_cliques++; #endif std::vector clique_vertices; @@ -1305,7 +1327,7 @@ bool cut_generation_t::generate_clique_cuts( } work_estimate += 3.0 * static_cast(clique_local.size()) + 1.0; if (work_estimate > max_work_estimate) { return true; } -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS const size_t size_before_extension = clique_vertices.size(); #endif extend_clique_vertices(clique_vertices, @@ -1318,7 +1340,7 @@ bool cut_generation_t::generate_clique_cuts( settings.time_limit, &work_estimate, max_work_estimate); -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS extension_gain += clique_vertices.size() - size_before_extension; #endif if (work_estimate > max_work_estimate) { return true; } @@ -1345,7 +1367,7 @@ bool cut_generation_t::generate_clique_cuts( } if (build_status == clique_cut_build_status_t::CUT_ADDED) { cut_pool_.add_cut(cut_type_t::CLIQUE, cut, cut_rhs); -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS added_cuts++; CLIQUE_CUTS_DEBUG("generate_clique_cuts added cut nz=%lld rhs=%g clique_size=%lld", static_cast(cut.i.size()), @@ -1353,13 +1375,13 @@ bool cut_generation_t::generate_clique_cuts( static_cast(clique_vertices.size())); #endif } -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS else { rejected_cliques++; } #endif } -#ifdef DEBUG_CLIQUE_CUTS +#if DEBUG_CLIQUE_CUTS CLIQUE_CUTS_DEBUG( "generate_clique_cuts done candidate_cliques=%lld added=%lld rejected=%lld extension_gain=%lld " "final_work=%g", From 3979c727efca1baf9ecd3b0d83f2ae8519d423bf Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 01:23:45 -0800 Subject: [PATCH 078/147] old presolve timer --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index d1d7ef4397..36e4b8bb33 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -112,7 +112,7 @@ solution_t mip_solver_t::run_solver() f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : timer_.remaining_time(); - double presolve_time_limit = std::min(0.04 * time_limit, 60.0); + double presolve_time_limit = std::min(0.1 * time_limit, 60.0); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; From 812322e8cc5c166a520351a10974180f9c1ccc88 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 01:24:08 -0800 Subject: [PATCH 079/147] no cliques --- .../diversity/diversity_manager.cu | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 86d256eca5..20a6992d8a 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,21 +207,21 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!context.settings.heuristics_only && !problem_ptr->empty) { - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - "host lower bound size mismatch"); - cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - "host upper bound size mismatch"); - std::vector all_var_indices(problem_ptr->n_variables); - std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - trivial_presolve(*problem_ptr, remap_cache_ids); - } + // if (!context.settings.heuristics_only && !problem_ptr->empty) { + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + // "host lower bound size mismatch"); + // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + // "host upper bound size mismatch"); + // std::vector all_var_indices(problem_ptr->n_variables); + // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From c2959303fd88281c2277eeb9eed9a303b6e44bb5 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 02:32:59 -0800 Subject: [PATCH 080/147] with corrected work units --- cpp/src/branch_and_bound/branch_and_bound.cpp | 3 --- cpp/src/cuts/cuts.cpp | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 3075360bc4..d3dc1c2c27 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -43,9 +43,6 @@ namespace cuopt::linear_programming::dual_simplex { -#define CHECK_CUTS_AGAINST_SAVED_SOLUTION 1 -#define PRINT_FRACTIONAL_INFO 1 - namespace { template diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index aa2b39338e..c8e0ba6e78 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -23,10 +23,8 @@ namespace cuopt::linear_programming::dual_simplex { namespace { -#ifndef DEBUG_CLIQUE_CUTS #define DEBUG_CLIQUE_CUTS 0 -#endif -#define CHECK_WORKSPACE 1 +#define CHECK_WORKSPACE 0 enum class clique_cut_build_status_t : int8_t { NO_CUT = 0, CUT_ADDED = 1, INFEASIBLE = 2 }; From a4b162e3a3fe394580394996fd9e7c5d674978d8 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 02:43:08 -0800 Subject: [PATCH 081/147] corrected obj rounding --- cpp/src/mip_heuristics/solver.cu | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 36e4b8bb33..82e8ce17c5 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -173,6 +173,12 @@ solution_t mip_solver_t::run_solver() namespace dual_simplex = cuopt::linear_programming::dual_simplex; std::future branch_and_bound_status_future; dual_simplex::user_problem_t branch_and_bound_problem(context.problem_ptr->handle_ptr); + context.problem_ptr->recompute_objective_integrality(); + if (context.problem_ptr->is_objective_integral()) { + CUOPT_LOG_INFO("Objective function is integral, scale %g", + context.problem_ptr->presolve_data.objective_scaling_factor); + } + branch_and_bound_problem.objective_is_integral = context.problem_ptr->is_objective_integral(); dual_simplex::simplex_solver_settings_t branch_and_bound_settings; std::unique_ptr> branch_and_bound; branch_and_bound_solution_helper_t solution_helper(&dm, branch_and_bound_settings); @@ -182,7 +188,6 @@ solution_t mip_solver_t::run_solver() if (run_bb) { // Convert the presolved problem to dual_simplex::user_problem_t op_problem_.get_host_user_problem(branch_and_bound_problem); - context.problem_ptr->set_constraints_from_host_user_problem(branch_and_bound_problem); // Resize the solution now that we know the number of columns/variables branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); From 6d91c07d42b8fcaad0aa8908273d2532329b8712 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 02:43:46 -0800 Subject: [PATCH 082/147] old presolve time limit --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 82e8ce17c5..64c7505407 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -115,7 +115,7 @@ solution_t mip_solver_t::run_solver() double presolve_time_limit = std::min(0.1 * time_limit, 60.0); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() - : presolve_time_limit; + : time_limit; bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); From 607b797c72d3949096cde6bd757daa298f8365a9 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 02:44:46 -0800 Subject: [PATCH 083/147] new presolve time limit and cliques --- .../diversity/diversity_manager.cu | 30 +++++++++---------- cpp/src/mip_heuristics/solver.cu | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 20a6992d8a..86d256eca5 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,21 +207,21 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // if (!context.settings.heuristics_only && !problem_ptr->empty) { - // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - // problem_ptr->get_host_user_problem(host_problem); - // std::shared_ptr> clique_table; - // find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); - // problem_ptr->set_constraints_from_host_user_problem(host_problem); - // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - // "host lower bound size mismatch"); - // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - // "host upper bound size mismatch"); - // std::vector all_var_indices(problem_ptr->n_variables); - // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - // trivial_presolve(*problem_ptr, remap_cache_ids); - // } + if (!context.settings.heuristics_only && !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + "host lower bound size mismatch"); + cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + "host upper bound size mismatch"); + std::vector all_var_indices(problem_ptr->n_variables); + std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + trivial_presolve(*problem_ptr, remap_cache_ids); + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 64c7505407..82e8ce17c5 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -115,7 +115,7 @@ solution_t mip_solver_t::run_solver() double presolve_time_limit = std::min(0.1 * time_limit, 60.0); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() - : time_limit; + : presolve_time_limit; bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); From 0d2a80dd6b8ba33ec2efe1b76eae716012fd88e4 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 02:54:40 -0800 Subject: [PATCH 084/147] without preprocessing --- cpp/src/branch_and_bound/branch_and_bound.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index d3dc1c2c27..12cca1e811 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2363,7 +2363,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_.num_cols, original_lp_.A.col_start[original_lp_.A.n]); } - exit(0); set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); From 72da24d75b43948a6f3481f04eaaa4970682c2ff Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 02:55:20 -0800 Subject: [PATCH 085/147] with preprocessing --- .../diversity/diversity_manager.cu | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 1ad85877b8..f88e4d0456 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -207,24 +207,24 @@ bool diversity_manager_t::run_presolve(f_t time_limit) const bool remap_cache_ids = true; trivial_presolve(*problem_ptr, remap_cache_ids); if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // if (!context.settings.heuristics_only && !problem_ptr->empty) { - // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - // problem_ptr->get_host_user_problem(host_problem); - // std::shared_ptr> clique_table; - // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - // find_initial_cliques( - // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - // problem_ptr->set_constraints_from_host_user_problem(host_problem); - // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - // "host lower bound size mismatch"); - // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - // "host upper bound size mismatch"); - // std::vector all_var_indices(problem_ptr->n_variables); - // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - // trivial_presolve(*problem_ptr, remap_cache_ids); - // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - // } + if (!context.settings.heuristics_only && !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + find_initial_cliques( + host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + "host lower bound size mismatch"); + cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + "host upper bound size mismatch"); + std::vector all_var_indices(problem_ptr->n_variables); + std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + trivial_presolve(*problem_ptr, remap_cache_ids); + if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 5e58a676483d458b70372eabede95b9ce073702f Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 07:46:20 -0800 Subject: [PATCH 086/147] fix row names bug --- cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 7e5cb98491..0a9fb46fb5 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -427,6 +427,7 @@ void insert_clique_into_problem(const std::vector& clique, A.append_row(new_row); problem.row_sense.push_back('L'); problem.rhs.push_back(rhs); + problem.row_names.push_back("Clique" + std::to_string(problem.row_names.size())); } template From d0c5a425ca1cc24f9b4cb984bc0090b6a4c38755 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Feb 2026 16:09:51 +0000 Subject: [PATCH 087/147] fix thrust build + more timer checks --- .../diversity/diversity_manager.cu | 26 ++++++++++--------- cpp/src/mip_heuristics/solve.cu | 5 ++++ cpp/src/mip_heuristics/solver.cu | 16 ++++++++++++ cpp/src/pdlp/swap_and_resize_helper.cuh | 1 + cpp/src/pdlp/utils.cuh | 1 + cpp/src/utilities/copy_helpers.hpp | 1 + cpp/src/utilities/cuda_helpers.cuh | 1 + 7 files changed, 39 insertions(+), 12 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 25b021ac35..4fa0d3f4ab 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -201,18 +201,20 @@ bool diversity_manager_t::run_presolve(f_t time_limit) compute_probing_cache(ls.constraint_prop.bounds_update, *problem_ptr, probing_timer); if (problem_is_infeasible) { return false; } } - const bool remap_cache_ids = true; - trivial_presolve(*problem_ptr, remap_cache_ids); - if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // May overconstrain if Papilo presolve has been run before - if (context.settings.presolver == presolver_t::None) { - if (!problem_ptr->empty) { - // do the resizing no-matter what, bounds presolve might not change the bounds but initial - // trivial presolve might have - ls.constraint_prop.bounds_update.resize(*problem_ptr); - ls.constraint_prop.conditional_bounds_update.update_constraint_bounds( - *problem_ptr, ls.constraint_prop.bounds_update); - if (!check_bounds_sanity(*problem_ptr)) { return false; } + if (!presolve_timer.check_time_limit()) { + const bool remap_cache_ids = true; + trivial_presolve(*problem_ptr, remap_cache_ids); + if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } + // May overconstrain if Papilo presolve has been run before + if (context.settings.presolver == presolver_t::None) { + if (!problem_ptr->empty) { + // do the resizing no-matter what, bounds presolve might not change the bounds but initial + // trivial presolve might have + ls.constraint_prop.bounds_update.resize(*problem_ptr); + ls.constraint_prop.conditional_bounds_update.update_constraint_bounds( + *problem_ptr, ls.constraint_prop.bounds_update); + if (!check_bounds_sanity(*problem_ptr)) { return false; } + } } } stats.presolve_time = presolve_timer.elapsed_time(); diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 41e2729927..3c6ee21a42 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -153,6 +153,11 @@ mip_solution_t run_mip(detail::problem_t& problem, detail::trivial_presolve(scaled_problem); detail::mip_solver_t solver(scaled_problem, settings, scaling, timer); + if (timer.check_time_limit()) { + CUOPT_LOG_INFO("Time limit reached before main solve"); + detail::solution_t sol(problem); + return sol.get_solution(false, solver.get_solver_stats(), false); + } auto scaled_sol = solver.run_solver(); bool is_feasible_before_scaling = scaled_sol.get_feasible(); scaled_sol.problem_ptr = &problem; diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 3e22511714..235d4500d2 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -134,6 +134,14 @@ solution_t mip_solver_t::run_solver() return sol; } + if (timer_.check_time_limit()) { + CUOPT_LOG_INFO("Time limit reached after presolve"); + solution_t sol(*context.problem_ptr); + context.stats.total_solve_time = timer_.elapsed_time(); + context.problem_ptr->post_process_solution(sol); + return sol; + } + // if the problem was reduced to a LP: run concurrent LP if (run_presolve && context.problem_ptr->n_integer_vars == 0) { CUOPT_LOG_INFO("Problem reduced to a LP, running concurrent LP"); @@ -285,6 +293,14 @@ solution_t mip_solver_t::run_solver() std::placeholders::_5, std::placeholders::_6); + if (timer_.check_time_limit()) { + CUOPT_LOG_INFO("Time limit reached during B&B setup"); + solution_t sol(*context.problem_ptr); + context.stats.total_solve_time = timer_.elapsed_time(); + context.problem_ptr->post_process_solution(sol); + return sol; + } + // Fork a thread for branch and bound // std::async and std::future allow us to get the return value of bb::solve() // without having to manually manage the thread diff --git a/cpp/src/pdlp/swap_and_resize_helper.cuh b/cpp/src/pdlp/swap_and_resize_helper.cuh index 0d4e2a6d01..6ed05df241 100644 --- a/cpp/src/pdlp/swap_and_resize_helper.cuh +++ b/cpp/src/pdlp/swap_and_resize_helper.cuh @@ -16,6 +16,7 @@ #include #include #include +#include #include #include diff --git a/cpp/src/pdlp/utils.cuh b/cpp/src/pdlp/utils.cuh index 33625f7680..138c9c2ab9 100644 --- a/cpp/src/pdlp/utils.cuh +++ b/cpp/src/pdlp/utils.cuh @@ -25,6 +25,7 @@ #include #include #include +#include namespace cuopt::linear_programming::detail { diff --git a/cpp/src/utilities/copy_helpers.hpp b/cpp/src/utilities/copy_helpers.hpp index 943ce463a7..36a4659059 100644 --- a/cpp/src/utilities/copy_helpers.hpp +++ b/cpp/src/utilities/copy_helpers.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include diff --git a/cpp/src/utilities/cuda_helpers.cuh b/cpp/src/utilities/cuda_helpers.cuh index ae50e9967b..946099648d 100644 --- a/cpp/src/utilities/cuda_helpers.cuh +++ b/cpp/src/utilities/cuda_helpers.cuh @@ -10,6 +10,7 @@ #include #include +#include #include #include #include From 67240f57035bde789b1e5512c116c00fd46c5baf Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Feb 2026 16:51:09 +0000 Subject: [PATCH 088/147] review comment --- cpp/src/mip_heuristics/solve.cu | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 3c6ee21a42..44f0e7468d 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -156,7 +156,9 @@ mip_solution_t run_mip(detail::problem_t& problem, if (timer.check_time_limit()) { CUOPT_LOG_INFO("Time limit reached before main solve"); detail::solution_t sol(problem); - return sol.get_solution(false, solver.get_solver_stats(), false); + auto stats = solver.get_solver_stats(); + stats.total_solve_time = timer.elapsed_time(); + return sol.get_solution(false, stats, false); } auto scaled_sol = solver.run_solver(); bool is_feasible_before_scaling = scaled_sol.get_feasible(); From a15424fc250b6e649b8b0b0015641d9771c9b426 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Tue, 24 Feb 2026 17:02:33 +0000 Subject: [PATCH 089/147] fix thrust solve --- cpp/src/routing/utilities/check_input.cu | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/routing/utilities/check_input.cu b/cpp/src/routing/utilities/check_input.cu index b2f035ee5c..e902f2d460 100644 --- a/cpp/src/routing/utilities/check_input.cu +++ b/cpp/src/routing/utilities/check_input.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -17,6 +17,7 @@ #include #include #include +#include #include #include From 02d4c09fbf348c653a59559ca9a397d54ffe31b5 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 23:15:40 -0800 Subject: [PATCH 090/147] fix timers on probing cache --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 7 +++---- cpp/src/mip_heuristics/diversity/diversity_manager.cuh | 2 +- .../mip_heuristics/presolve/conflict_graph/clique_table.cu | 1 - cpp/src/mip_heuristics/solver.cu | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 94c4c2cb26..b34563a6ff 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -175,7 +175,7 @@ void diversity_manager_t::add_user_given_solutions( } template -bool diversity_manager_t::run_presolve(f_t time_limit) +bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_timer) { raft::common::nvtx::range fun_scope("run_presolve"); CUOPT_LOG_INFO("Running presolve!"); @@ -196,8 +196,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit) // Run probing cache before trivial presolve to discover variable implications const f_t time_ratio_of_probing_cache = diversity_config.time_ratio_of_probing_cache; const f_t max_time_on_probing = diversity_config.max_time_on_probing; - f_t time_for_probing_cache = - std::min(max_time_on_probing, time_limit * time_ratio_of_probing_cache); + f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit * 0.8); timer_t probing_timer{time_for_probing_cache}; // this function computes probing cache, finds singletons, substitutions and changes the problem bool problem_is_infeasible = @@ -205,7 +204,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit) if (problem_is_infeasible) { return false; } } const bool remap_cache_ids = true; - if (!presolve_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } + if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && !problem_ptr->empty) { diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cuh b/cpp/src/mip_heuristics/diversity/diversity_manager.cuh index 91fc4049a6..d4e24bdeaf 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cuh +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cuh @@ -33,7 +33,7 @@ template class diversity_manager_t { public: diversity_manager_t(mip_solver_context_t& context); - bool run_presolve(f_t time_limit); + bool run_presolve(f_t time_limit, timer_t global_timer); solution_t run_solver(); void generate_solution(f_t time_limit, bool random_start = true); void run_fj_alone(solution_t& solution); diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 0a9fb46fb5..b2b34ff177 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -416,7 +416,6 @@ void insert_clique_into_problem(const std::vector& clique, new_vars.push_back(var_idx); new_coeffs.push_back(coeff); } - // For complemented literals (1 - x), expansion contributes a constant term on the left: // coeff_scale * (1 - x) = coeff_scale - coeff_scale * x // Move constants to the right, so rhs must decrease by rhs_offset. f_t rhs = coeff_scale - rhs_offset; diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index a4389a5da4..e6f6d50b62 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -116,7 +116,7 @@ solution_t mip_solver_t::run_solver() presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; - bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit) : true; + bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit, timer_) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); solution_t sol(*context.problem_ptr); From 18dd670f0fee68eb8f5acf3eb461aeb0e5e53c32 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 24 Feb 2026 23:17:26 -0800 Subject: [PATCH 091/147] without initial presolve --- .../diversity/diversity_manager.cu | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index c62f02fadc..6c36ea49b5 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -206,25 +206,25 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && - !problem_ptr->empty) { - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - find_initial_cliques( - host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - "host lower bound size mismatch"); - cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - "host upper bound size mismatch"); - std::vector all_var_indices(problem_ptr->n_variables); - std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - trivial_presolve(*problem_ptr, remap_cache_ids); - if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - } + // if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && + // !problem_ptr->empty) { + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + // find_initial_cliques( + // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + // "host lower bound size mismatch"); + // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + // "host upper bound size mismatch"); + // std::vector all_var_indices(problem_ptr->n_variables); + // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 0b9460ce0cbab20689134494bf944dfb6845435a Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 25 Feb 2026 01:49:20 -0800 Subject: [PATCH 092/147] without clique preprocessing --- .../diversity/diversity_manager.cu | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index b34563a6ff..9d403f83a3 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -206,22 +206,22 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && - !problem_ptr->empty) { - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - "host lower bound size mismatch"); - cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - "host upper bound size mismatch"); - std::vector all_var_indices(problem_ptr->n_variables); - std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - trivial_presolve(*problem_ptr, remap_cache_ids); - } + // if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && + // !problem_ptr->empty) { + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + // "host lower bound size mismatch"); + // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + // "host upper bound size mismatch"); + // std::vector all_var_indices(problem_ptr->n_variables); + // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From bdedb6c953da567171ba3703bc11f0671c13fcd7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 25 Feb 2026 03:20:49 -0800 Subject: [PATCH 093/147] restore timers --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index e6f6d50b62..0404441270 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -116,7 +116,7 @@ solution_t mip_solver_t::run_solver() presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; - bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit, timer_) : true; + bool presolve_success = run_presolve ? dm.run_presolve(time_limit, timer_) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); solution_t sol(*context.problem_ptr); From 31c6bab18baa27ea0ec646951782028ffaec790b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 25 Feb 2026 03:21:15 -0800 Subject: [PATCH 094/147] enable preprocessinG --- .../diversity/diversity_manager.cu | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 9d403f83a3..4326d91f1b 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -196,7 +196,8 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ // Run probing cache before trivial presolve to discover variable implications const f_t time_ratio_of_probing_cache = diversity_config.time_ratio_of_probing_cache; const f_t max_time_on_probing = diversity_config.max_time_on_probing; - f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit * 0.8); + f_t time_for_probing_cache = + std::min(max_time_on_probing, time_limit * time_ratio_of_probing_cache); timer_t probing_timer{time_for_probing_cache}; // this function computes probing cache, finds singletons, substitutions and changes the problem bool problem_is_infeasible = @@ -206,22 +207,22 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && - // !problem_ptr->empty) { - // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - // problem_ptr->get_host_user_problem(host_problem); - // std::shared_ptr> clique_table; - // find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); - // problem_ptr->set_constraints_from_host_user_problem(host_problem); - // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - // "host lower bound size mismatch"); - // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - // "host upper bound size mismatch"); - // std::vector all_var_indices(problem_ptr->n_variables); - // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - // trivial_presolve(*problem_ptr, remap_cache_ids); - // } + if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && + !problem_ptr->empty) { + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); + problem_ptr->set_constraints_from_host_user_problem(host_problem); + cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + "host lower bound size mismatch"); + cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + "host upper bound size mismatch"); + std::vector all_var_indices(problem_ptr->n_variables); + std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + trivial_presolve(*problem_ptr, remap_cache_ids); + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 02bc9b8b68ef0ab9c0997ced346dd5e854442013 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 25 Feb 2026 04:38:45 -0800 Subject: [PATCH 095/147] with clique timer --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 4326d91f1b..a50d0afd57 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -209,10 +209,11 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && !problem_ptr->empty) { + timer_t clique_timer(15.); dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); std::shared_ptr> clique_table; - find_initial_cliques(host_problem, context.settings.tolerances, presolve_timer); + find_initial_cliques(host_problem, context.settings.tolerances, clique_timer); problem_ptr->set_constraints_from_host_user_problem(host_problem); cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), "host lower bound size mismatch"); From fa2330c0e0f1738d5ec02c53cf6a37690fd058c0 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 25 Feb 2026 07:42:21 -0800 Subject: [PATCH 096/147] inside else --- .../presolve/conflict_graph/clique_table.cu | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index b2b34ff177..ef495e5029 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -833,12 +833,13 @@ void remove_dominated_cliques( sp.row_idx); fix_difference(curr_clique_vars, vars_sp); } + } else { + // knapsack cstr_idx may refer to virtual rows; only real model row indices can be + // removed from A. + if (sp.row_idx < 0 || sp.row_idx >= static_cast(removal_marker.size())) { continue; } + if (removal_marker[sp.row_idx]) { continue; } + removal_marker[sp.row_idx] = true; } - // knapsack cstr_idx may refer to virtual rows; only real model row indices can be - // removed from A. - if (sp.row_idx < 0 || sp.row_idx >= static_cast(removal_marker.size())) { continue; } - if (removal_marker[sp.row_idx]) { continue; } - removal_marker[sp.row_idx] = true; } if ((i % 128) == 0) { CUOPT_LOG_TRACE("Processed extended clique %d/%d", i + 1, n_extended_cliques); From b176767d8031238d02669d74661daa4ed154d0d4 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 25 Feb 2026 08:08:50 -0800 Subject: [PATCH 097/147] ranged rows --- .../presolve/conflict_graph/clique_table.cu | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index ef495e5029..c3c2d9e3c3 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -871,8 +871,15 @@ void remove_dominated_cliques( return removal_marker[n++]; }); problem.rhs.erase(new_end_rhs, problem.rhs.end()); + n = 0; + auto new_end_row_names = std::remove_if( + problem.row_names.begin(), + problem.row_names.end(), + [&removal_marker, &n](const std::string&) mutable { return removal_marker[n++]; }); + problem.row_names.erase(new_end_row_names, problem.row_names.end()); CUOPT_LOG_DEBUG("Number of removed constraints by clique covering: %d", n_of_removed_constraints); cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); + cuopt_assert(problem.row_names.size() == problem.rhs.size(), "row names and rhs size mismatch"); cuopt_assert(problem.A.m == problem.rhs.size(), "matrix and num rows mismatch after removal"); // Renumber the ranged row indices in problem.range_rows to ensure consistency after constraint // removals. Create a mapping from old indices to new indices. @@ -903,6 +910,7 @@ void remove_dominated_cliques( problem.range_rows = std::move(new_range_rows); problem.range_value = std::move(new_range_values); } + problem.num_range_rows = static_cast(problem.range_rows.size()); } template From 0d44fd1ef8abe569a024262abcb2bfb2b982c324 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 26 Feb 2026 08:31:25 -0800 Subject: [PATCH 098/147] original timer --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index da6c7c0b81..3b7b15936d 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -115,7 +115,7 @@ solution_t mip_solver_t::run_solver() double presolve_time_limit = std::min(0.1 * time_limit, 60.0); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() - : presolve_time_limit; + : time_limit; bool presolve_success = run_presolve ? dm.run_presolve(presolve_time_limit, timer_) : true; if (!presolve_success) { CUOPT_LOG_INFO("Problem proven infeasible in presolve"); From 36ac698878eed010e9361ed23c94cdfa5bf251a3 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 04:30:49 -0800 Subject: [PATCH 099/147] do one by one replacement and add only when it removes some --- .../presolve/conflict_graph/clique_table.cu | 487 ++++++++---------- 1 file changed, 228 insertions(+), 259 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index c3c2d9e3c3..e20f90d6d4 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -537,9 +537,9 @@ bool extend_clique(const std::vector& clique, remaining_nnz_budget < static_cast(new_clique.size())) { return true; } - // insert the new clique into the problem as a new constraint - insert_clique_into_problem(new_clique, problem, A, coeff_scale); - inserted_row_nnz = static_cast(new_clique.size()); + // Row insertion is now deferred until dominance is confirmed against model rows. + // This keeps extension and replacement sequential: detect dominance first, then replace. + inserted_row_nnz = 0; } } return new_clique.size() > clique.size(); @@ -550,13 +550,14 @@ bool extend_clique(const std::vector& clique, // TODO: consider a heuristic on how much of the cliques derived from knapsacks to include here template i_t extend_cliques(const std::vector>& knapsack_constraints, + const std::unordered_set& set_packing_constraints, clique_table_t& clique_table, dual_simplex::user_problem_t& problem, dual_simplex::csr_matrix_t& A, cuopt::timer_t& timer) { constexpr i_t min_extension_gain = 2; - constexpr i_t extension_yield_window = 64; + constexpr i_t extension_yield_window = 128; constexpr i_t min_successes_per_window = 1; i_t base_rows = A.m; @@ -573,6 +574,217 @@ i_t extend_cliques(const std::vector>& knapsack_ min_extension_gain, max_added_rows, max_added_nnz); + std::vector> cstr_vars(knapsack_constraints.size()); + struct clique_sig_t { + i_t knapsack_idx; + i_t size; + long long signature; + }; + std::vector sp_sigs; + sp_sigs.reserve(set_packing_constraints.size()); + for (const auto knapsack_idx : set_packing_constraints) { + if (knapsack_idx < 0 || knapsack_idx >= static_cast(knapsack_constraints.size())) { + continue; + } + const auto& vars = knapsack_constraints[knapsack_idx].entries; + cstr_vars[knapsack_idx].reserve(vars.size()); + for (const auto& entry : vars) { + cstr_vars[knapsack_idx].push_back(entry.col); + } + std::sort(cstr_vars[knapsack_idx].begin(), cstr_vars[knapsack_idx].end()); + cstr_vars[knapsack_idx].erase( + std::unique(cstr_vars[knapsack_idx].begin(), cstr_vars[knapsack_idx].end()), + cstr_vars[knapsack_idx].end()); + long long signature = 0; + for (auto v : cstr_vars[knapsack_idx]) { + signature += static_cast(v); + } + sp_sigs.push_back({knapsack_idx, static_cast(cstr_vars[knapsack_idx].size()), signature}); + } + std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { + if (a.signature != b.signature) { return a.signature < b.signature; } + return a.size < b.size; + }); + std::vector original_to_current_row_idx(problem.row_sense.size(), -1); + for (i_t row_idx = 0; row_idx < static_cast(original_to_current_row_idx.size()); row_idx++) { + original_to_current_row_idx[row_idx] = row_idx; + } + auto is_subset = [](const std::vector& a, const std::vector& b) { + size_t i = 0; + size_t j = 0; + while (i < a.size() && j < b.size()) { + if (a[i] == b[j]) { + i++; + j++; + } else if (a[i] > b[j]) { + j++; + } else { + return false; + } + } + return i == a.size(); + }; + auto fix_difference = [&](const std::vector& superset, const std::vector& subset) { + cuopt_assert(std::is_sorted(subset.begin(), subset.end()), + "subset vector passed to fix_difference is not sorted"); + for (auto var_idx : superset) { + if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } + if (var_idx >= problem.num_cols) { + i_t orig_idx = var_idx - problem.num_cols; + CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); + cuopt_assert(problem.lower[orig_idx] != 0 || problem.upper[orig_idx] != 0, + "Variable is fixed to other side"); + problem.lower[orig_idx] = 1; + problem.upper[orig_idx] = 1; + } else { + CUOPT_LOG_DEBUG("Fixing variable %d", var_idx); + cuopt_assert(problem.lower[var_idx] != 1 || problem.upper[var_idx] != 1, + "Variable is fixed to other side"); + problem.lower[var_idx] = 0; + problem.upper[var_idx] = 0; + } + } + }; + auto remove_dominated_cliques_in_problem_for_single_extended_clique = + [&](const std::vector& curr_clique, + f_t coeff_scale, + i_t remaining_rows_budget, + i_t remaining_nnz_budget, + i_t& inserted_row_nnz) { + inserted_row_nnz = 0; + if (curr_clique.empty() || sp_sigs.empty()) { return; } + std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); + std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); + curr_clique_vars.erase(std::unique(curr_clique_vars.begin(), curr_clique_vars.end()), + curr_clique_vars.end()); + long long signature = 0; + for (auto v : curr_clique_vars) { + signature += static_cast(v); + } + constexpr size_t dominance_window = 100; + auto end_it = std::upper_bound( + sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { + return value < a.signature; + }); + size_t end = static_cast(std::distance(sp_sigs.begin(), end_it)); + size_t start = (end > dominance_window) ? (end - dominance_window) : 0; + std::vector rows_to_remove; + for (size_t idx = end; idx > start; idx--) { + if (timer.check_time_limit()) { break; } + const auto& sp = sp_sigs[idx - 1]; + const auto& vars_sp = cstr_vars[sp.knapsack_idx]; + if (vars_sp.size() > curr_clique_vars.size()) { continue; } + cuopt_assert(std::is_sorted(vars_sp.begin(), vars_sp.end()), + "vars_sp vector passed to is_subset is not sorted"); + if (!is_subset(vars_sp, curr_clique_vars)) { continue; } + if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { + if (vars_sp.size() != curr_clique_vars.size()) { + fix_difference(curr_clique_vars, vars_sp); + } + continue; + } + i_t original_row_idx = knapsack_constraints[sp.knapsack_idx].cstr_idx; + if (original_row_idx < 0 || + original_row_idx >= static_cast(original_to_current_row_idx.size())) { + continue; + } + i_t current_row_idx = original_to_current_row_idx[original_row_idx]; + if (current_row_idx < 0 || current_row_idx >= static_cast(problem.row_sense.size())) { + continue; + } + rows_to_remove.push_back(current_row_idx); + } + if (rows_to_remove.empty()) { return; } + std::sort(rows_to_remove.begin(), rows_to_remove.end()); + rows_to_remove.erase(std::unique(rows_to_remove.begin(), rows_to_remove.end()), + rows_to_remove.end()); + if (remaining_rows_budget <= 0 || + remaining_nnz_budget < static_cast(curr_clique_vars.size())) { + return; + } + // Replace dominated rows with this stronger clique row. + insert_clique_into_problem(curr_clique_vars, problem, A, coeff_scale); + inserted_row_nnz = static_cast(curr_clique_vars.size()); + std::vector removal_marker(problem.row_sense.size(), 0); + for (auto row_idx : rows_to_remove) { + if (row_idx >= 0 && row_idx < static_cast(removal_marker.size())) { + CUOPT_LOG_DEBUG("Removing dominated row %d", row_idx); + removal_marker[row_idx] = true; + } + } + dual_simplex::csr_matrix_t A_removed(0, 0, 0); + A.remove_rows(removal_marker, A_removed); + A = std::move(A_removed); + problem.num_rows = A.m; + i_t n = 0; + auto new_end = std::remove_if( + problem.row_sense.begin(), problem.row_sense.end(), [&removal_marker, &n](char) mutable { + return removal_marker[n++]; + }); + problem.row_sense.erase(new_end, problem.row_sense.end()); + n = 0; + auto new_end_rhs = + std::remove_if(problem.rhs.begin(), problem.rhs.end(), [&removal_marker, &n](f_t) mutable { + return removal_marker[n++]; + }); + problem.rhs.erase(new_end_rhs, problem.rhs.end()); + n = 0; + auto new_end_row_names = std::remove_if( + problem.row_names.begin(), + problem.row_names.end(), + [&removal_marker, &n](const std::string&) mutable { return removal_marker[n++]; }); + problem.row_names.erase(new_end_row_names, problem.row_names.end()); + cuopt_assert(problem.rhs.size() == problem.row_sense.size(), + "rhs and row sense size mismatch"); + cuopt_assert(problem.row_names.size() == problem.rhs.size(), + "row names and rhs size mismatch"); + cuopt_assert(problem.num_rows == static_cast(problem.rhs.size()), + "matrix and num rows mismatch after removal"); + if (!problem.range_rows.empty()) { + std::vector old_to_new_indices; + old_to_new_indices.reserve(removal_marker.size()); + i_t new_idx = 0; + for (size_t i = 0; i < removal_marker.size(); ++i) { + if (!removal_marker[i]) { + old_to_new_indices.push_back(new_idx++); + } else { + old_to_new_indices.push_back(-1); + } + } + std::vector new_range_rows; + std::vector new_range_values; + for (size_t i = 0; i < problem.range_rows.size(); ++i) { + i_t old_row = problem.range_rows[i]; + if (old_row >= 0 && old_row < static_cast(removal_marker.size()) && + !removal_marker[old_row]) { + i_t new_row = old_to_new_indices[old_row]; + cuopt_assert(new_row != -1, "Invalid new row index for ranged row renumbering"); + new_range_rows.push_back(new_row); + new_range_values.push_back(problem.range_value[i]); + } + } + problem.range_rows = std::move(new_range_rows); + problem.range_value = std::move(new_range_values); + } + problem.num_range_rows = static_cast(problem.range_rows.size()); + std::vector removed_prefix(removal_marker.size() + 1, 0); + for (size_t row_idx = 0; row_idx < removal_marker.size(); row_idx++) { + removed_prefix[row_idx + 1] = + removed_prefix[row_idx] + static_cast(removal_marker[row_idx]); + } + for (i_t row_idx = 0; row_idx < static_cast(original_to_current_row_idx.size()); + row_idx++) { + i_t current_row_idx = original_to_current_row_idx[row_idx]; + if (current_row_idx < 0) { continue; } + cuopt_assert(current_row_idx < static_cast(removal_marker.size()), + "Row index map is out of bounds"); + if (removal_marker[current_row_idx]) { + original_to_current_row_idx[row_idx] = -1; + } else { + original_to_current_row_idx[row_idx] = current_row_idx - removed_prefix[current_row_idx]; + } + } + }; struct extension_candidate_t { i_t knapsack_idx; i_t estimated_gain; @@ -635,10 +847,16 @@ i_t extend_cliques(const std::vector>& knapsack_ inserted_row_nnz); if (extended_clique) { n_extended_cliques++; - window_successes++; - if (inserted_row_nnz > 0) { + i_t replacement_row_nnz = 0; + remove_dominated_cliques_in_problem_for_single_extended_clique(clique_table.first.back(), + coeff_scale, + max_added_rows - added_rows, + max_added_nnz - added_nnz, + replacement_row_nnz); + if (replacement_row_nnz > 0) { + window_successes++; added_rows++; - added_nnz += inserted_row_nnz; + added_nnz += replacement_row_nnz; } } if (window_attempts >= extension_yield_window) { @@ -651,9 +869,9 @@ i_t extend_cliques(const std::vector>& knapsack_ window_successes = 0; } } - // problem.A.check_matrix(); // copy modified matrix back to problem A.to_compressed_col(problem.A); + CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); return n_extended_cliques; } @@ -673,246 +891,6 @@ void fill_var_clique_maps(clique_table_t& clique_table) } } -// we want to remove constraints that are covered by extended cliques -// for set partitioning constraints, we will keep the constraint on original problem but fix -// extended vars to zero For a set partitioning constraint: v1+v2+...+vk = 1 and discovered: -// v1+v2+...+vk + vl1+vl2 +...+vli <= 1 -// Then substituting the first to the second you have: -// 1 + vl1+vl2 +...+vli <= 1 -// Which is simply: -// vl1+vl2 +...+vli <= 0 -// so we can fix them -template -void remove_dominated_cliques( - dual_simplex::user_problem_t& problem, - dual_simplex::csr_matrix_t& A, - clique_table_t& clique_table, - std::unordered_set& set_packing_constraints, - const std::vector>& knapsack_constraints, - i_t n_extended_cliques, - cuopt::timer_t& timer) -{ - // TODO check if we need to add the dominance for the table itself - i_t extended_clique_start_idx = clique_table.first.size() - n_extended_cliques; - CUOPT_LOG_DEBUG("Number of extended cliques: %d", n_extended_cliques); - std::vector removal_marker(problem.row_sense.size(), 0); - std::vector> cstr_vars(knapsack_constraints.size()); - bool time_limit_reached = timer.check_time_limit(); - for (const auto knapsack_idx : set_packing_constraints) { - if (timer.check_time_limit()) { - time_limit_reached = true; - break; - } - cuopt_assert(knapsack_constraints[knapsack_idx].is_set_packing, - "Set packing constraint is not a set packing constraint"); - const auto& vars = knapsack_constraints[knapsack_idx].entries; - cstr_vars[knapsack_idx].reserve(vars.size()); - for (const auto& entry : vars) { - cstr_vars[knapsack_idx].push_back(entry.col); - } - std::sort(cstr_vars[knapsack_idx].begin(), cstr_vars[knapsack_idx].end()); - } - if (!time_limit_reached) { - CUOPT_LOG_DEBUG("Constraint variable lists built: %zu", set_packing_constraints.size()); - constexpr size_t dominance_window = 100; - struct clique_sig_t { - i_t knapsack_idx; - i_t row_idx; - i_t size; - long long signature; - }; - std::vector sp_sigs; - sp_sigs.reserve(set_packing_constraints.size()); - CUOPT_LOG_DEBUG("Building set packing signatures"); - for (const auto knapsack_idx : set_packing_constraints) { - if (timer.check_time_limit()) { - time_limit_reached = true; - break; - } - const auto& vars = cstr_vars[knapsack_idx]; - if (vars.empty()) { continue; } - long long signature = 0; - for (auto v : vars) { - signature += static_cast(v); - } - sp_sigs.push_back({knapsack_idx, - knapsack_constraints[knapsack_idx].cstr_idx, - static_cast(vars.size()), - signature}); - } - CUOPT_LOG_DEBUG("Sorting signatures: %zu", sp_sigs.size()); - std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { - if (a.signature != b.signature) { return a.signature < b.signature; } - return a.size < b.size; - }); - auto is_subset = [](const std::vector& a, const std::vector& b) { - size_t i = 0; - size_t j = 0; - while (i < a.size() && j < b.size()) { - if (a[i] == b[j]) { - i++; - j++; - } else if (a[i] > b[j]) { - j++; - } else { - return false; - } - } - return i == a.size(); - }; - auto fix_difference = [&](const std::vector& superset, const std::vector& subset) { - cuopt_assert(std::is_sorted(subset.begin(), subset.end()), - "subset vector passed to fix_difference is not sorted"); - for (auto var_idx : superset) { - if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } - if (var_idx >= problem.num_cols) { - i_t orig_idx = var_idx - problem.num_cols; - CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); - cuopt_assert(problem.lower[orig_idx] != 0 || problem.upper[orig_idx] != 0, - "Variable is fixed to other side"); - problem.lower[orig_idx] = 1; - problem.upper[orig_idx] = 1; - } else { - CUOPT_LOG_DEBUG("Fixing variable %d", var_idx); - cuopt_assert(problem.lower[var_idx] != 1 || problem.upper[var_idx] != 1, - "Variable is fixed to other side"); - problem.lower[var_idx] = 0; - problem.upper[var_idx] = 0; - } - } - }; - auto find_window_end = [&](long long signature) { - auto it = std::upper_bound( - sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { - return value < a.signature; - }); - return static_cast(std::distance(sp_sigs.begin(), it)); - }; - CUOPT_LOG_DEBUG("Scanning extended cliques for dominance"); - for (i_t i = 0; i < n_extended_cliques; i++) { - // Break here so that the discovered dominance is applied - if (timer.check_time_limit()) { - time_limit_reached = true; - break; - } - i_t clique_idx = extended_clique_start_idx + i; - const auto& curr_clique = clique_table.first[clique_idx]; - if (curr_clique.empty()) { continue; } - std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); - std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); - cuopt_assert( - std::unique(curr_clique_vars.begin(), curr_clique_vars.end()) == curr_clique_vars.end(), - "Clique variables are not unique"); - long long signature = 0; - for (auto v : curr_clique_vars) { - signature += static_cast(v); - } - // Subsets must have signature <= current clique signature. Scan only that side. - size_t end = find_window_end(signature); - size_t start = (end > dominance_window) ? (end - dominance_window) : 0; - for (size_t idx = end; idx > start; idx--) { - size_t cand_idx = idx - 1; - const auto& sp = sp_sigs[cand_idx]; - const auto& vars_sp = cstr_vars[sp.knapsack_idx]; - if (vars_sp.size() > curr_clique_vars.size()) { continue; } - cuopt_assert(std::is_sorted(vars_sp.begin(), vars_sp.end()), - "vars_sp vector passed to is_subset is not sorted"); - if (!is_subset(vars_sp, curr_clique_vars)) { continue; } - // If this is a real model row and it is already marked for removal, it must not drive - // additional fixings/removals. - if (sp.row_idx >= 0 && sp.row_idx < static_cast(removal_marker.size()) && - removal_marker[sp.row_idx]) { - continue; - } - if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { - // note that we never deleter set partitioning constraints but it fixes some other - // variables - if (vars_sp.size() != curr_clique_vars.size()) { - CUOPT_LOG_DEBUG("Fixing difference between clique %d and set packing constraint %d", - clique_idx, - sp.row_idx); - fix_difference(curr_clique_vars, vars_sp); - } - } else { - // knapsack cstr_idx may refer to virtual rows; only real model row indices can be - // removed from A. - if (sp.row_idx < 0 || sp.row_idx >= static_cast(removal_marker.size())) { continue; } - if (removal_marker[sp.row_idx]) { continue; } - removal_marker[sp.row_idx] = true; - } - } - if ((i % 128) == 0) { - CUOPT_LOG_TRACE("Processed extended clique %d/%d", i + 1, n_extended_cliques); - } - } - CUOPT_LOG_DEBUG("Dominance scan complete"); - } - // TODO if more row removal is needed somewher else(e.g another presolve), standardize this - dual_simplex::csr_matrix_t A_removed(0, 0, 0); - CUOPT_LOG_DEBUG("Removing dominated rows"); - A.remove_rows(removal_marker, A_removed); - CUOPT_LOG_DEBUG("Rows removed, updating problem"); - A_removed.to_compressed_col(problem.A); - problem.num_rows = A_removed.m; - cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); - i_t n = 0; - // Remove problem.row_sense entries for which removal_marker is true, using remove_if - auto new_end = std::remove_if( - problem.row_sense.begin(), problem.row_sense.end(), [&removal_marker, &n](char) mutable { - return removal_marker[n++]; - }); - // Compute count before erase invalidates the iterator - size_t n_of_removed_constraints = std::distance(new_end, problem.row_sense.end()); - problem.row_sense.erase(new_end, problem.row_sense.end()); - n = 0; - // Remove problem.rhs entries for which removal_marker is true, using remove_if - auto new_end_rhs = - std::remove_if(problem.rhs.begin(), problem.rhs.end(), [&removal_marker, &n](f_t) mutable { - return removal_marker[n++]; - }); - problem.rhs.erase(new_end_rhs, problem.rhs.end()); - n = 0; - auto new_end_row_names = std::remove_if( - problem.row_names.begin(), - problem.row_names.end(), - [&removal_marker, &n](const std::string&) mutable { return removal_marker[n++]; }); - problem.row_names.erase(new_end_row_names, problem.row_names.end()); - CUOPT_LOG_DEBUG("Number of removed constraints by clique covering: %d", n_of_removed_constraints); - cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); - cuopt_assert(problem.row_names.size() == problem.rhs.size(), "row names and rhs size mismatch"); - cuopt_assert(problem.A.m == problem.rhs.size(), "matrix and num rows mismatch after removal"); - // Renumber the ranged row indices in problem.range_rows to ensure consistency after constraint - // removals. Create a mapping from old indices to new indices. - if (!problem.range_rows.empty()) { - std::vector old_to_new_indices; - old_to_new_indices.reserve(removal_marker.size()); - i_t new_idx = 0; - for (size_t i = 0; i < removal_marker.size(); ++i) { - if (!removal_marker[i]) { - old_to_new_indices.push_back(new_idx++); - } else { - old_to_new_indices.push_back(-1); // removed constraint - } - } - // Remove entries from range_rows and range_value where the underlying row has been removed. - std::vector new_range_rows; - std::vector new_range_values; - for (size_t i = 0; i < problem.range_rows.size(); ++i) { - i_t old_row = problem.range_rows[i]; - if (old_row >= 0 && old_row < (i_t)removal_marker.size() && !removal_marker[old_row]) { - i_t new_row = old_to_new_indices[old_row]; - cuopt_assert(new_row != -1, "Invalid new row index for ranged row renumbering"); - new_range_rows.push_back(new_row); - new_range_values.push_back(problem.range_value[i]); - } - // else: the ranged row was removed, so we skip it - } - problem.range_rows = std::move(new_range_rows); - problem.range_value = std::move(new_range_values); - } - problem.num_range_rows = static_cast(problem.range_rows.size()); -} - template void print_knapsack_constraints( const std::vector>& knapsack_constraints, @@ -1015,19 +993,10 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, #ifdef DEBUG_CLIQUE_TABLE t_maps = stage_timer.elapsed_time(); #endif - i_t n_extended_cliques = extend_cliques(knapsack_constraints, clique_table, problem, A, timer); + extend_cliques(knapsack_constraints, set_packing_constraints, clique_table, problem, A, timer); #ifdef DEBUG_CLIQUE_TABLE t_extend = stage_timer.elapsed_time(); -#endif - remove_dominated_cliques(problem, - A, - clique_table, - set_packing_constraints, - knapsack_constraints, - n_extended_cliques, - timer); -#ifdef DEBUG_CLIQUE_TABLE - t_remove = stage_timer.elapsed_time(); + t_remove = t_extend; CUOPT_LOG_DEBUG( "Clique table timing (s): fill=%.6f coeff=%.6f sort=%.6f find=%.6f small=%.6f maps=%.6f " "extend=%.6f remove=%.6f total=%.6f", From 2ec06d3163fd2d2b096be8a1855457536c89aaf7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 04:31:09 -0800 Subject: [PATCH 100/147] don't do fix difference --- cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index e20f90d6d4..988cdd7883 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -679,7 +679,7 @@ i_t extend_cliques(const std::vector>& knapsack_ if (!is_subset(vars_sp, curr_clique_vars)) { continue; } if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { if (vars_sp.size() != curr_clique_vars.size()) { - fix_difference(curr_clique_vars, vars_sp); + // fix_difference(curr_clique_vars, vars_sp); } continue; } From 68e0fad02db63fdb23b9cf6e0082ed20bbe06fef Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 04:35:45 -0800 Subject: [PATCH 101/147] odn't add covering clique when set partitioing --- .../presolve/conflict_graph/clique_table.cu | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 988cdd7883..971ad3b698 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -669,6 +669,7 @@ i_t extend_cliques(const std::vector>& knapsack_ size_t end = static_cast(std::distance(sp_sigs.begin(), end_it)); size_t start = (end > dominance_window) ? (end - dominance_window) : 0; std::vector rows_to_remove; + bool covering_clique_implied_by_partitioning = false; for (size_t idx = end; idx > start; idx--) { if (timer.check_time_limit()) { break; } const auto& sp = sp_sigs[idx - 1]; @@ -679,7 +680,8 @@ i_t extend_cliques(const std::vector>& knapsack_ if (!is_subset(vars_sp, curr_clique_vars)) { continue; } if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { if (vars_sp.size() != curr_clique_vars.size()) { - // fix_difference(curr_clique_vars, vars_sp); + fix_difference(curr_clique_vars, vars_sp); + covering_clique_implied_by_partitioning = true; } continue; } @@ -698,13 +700,15 @@ i_t extend_cliques(const std::vector>& knapsack_ std::sort(rows_to_remove.begin(), rows_to_remove.end()); rows_to_remove.erase(std::unique(rows_to_remove.begin(), rows_to_remove.end()), rows_to_remove.end()); - if (remaining_rows_budget <= 0 || - remaining_nnz_budget < static_cast(curr_clique_vars.size())) { - return; + if (!covering_clique_implied_by_partitioning) { + if (remaining_rows_budget <= 0 || + remaining_nnz_budget < static_cast(curr_clique_vars.size())) { + return; + } + // Replace dominated rows with this stronger clique row. + insert_clique_into_problem(curr_clique_vars, problem, A, coeff_scale); + inserted_row_nnz = static_cast(curr_clique_vars.size()); } - // Replace dominated rows with this stronger clique row. - insert_clique_into_problem(curr_clique_vars, problem, A, coeff_scale); - inserted_row_nnz = static_cast(curr_clique_vars.size()); std::vector removal_marker(problem.row_sense.size(), 0); for (auto row_idx : rows_to_remove) { if (row_idx >= 0 && row_idx < static_cast(removal_marker.size())) { From 46cb9e862e718def8ed1beb59952da899350a632 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 08:48:05 -0800 Subject: [PATCH 102/147] add asserts --- .../presolve/conflict_graph/clique_table.cu | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 971ad3b698..ce24401b6e 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -557,7 +557,7 @@ i_t extend_cliques(const std::vector>& knapsack_ cuopt::timer_t& timer) { constexpr i_t min_extension_gain = 2; - constexpr i_t extension_yield_window = 128; + constexpr i_t extension_yield_window = 64; constexpr i_t min_successes_per_window = 1; i_t base_rows = A.m; @@ -583,9 +583,8 @@ i_t extend_cliques(const std::vector>& knapsack_ std::vector sp_sigs; sp_sigs.reserve(set_packing_constraints.size()); for (const auto knapsack_idx : set_packing_constraints) { - if (knapsack_idx < 0 || knapsack_idx >= static_cast(knapsack_constraints.size())) { - continue; - } + cuopt_assert(knapsack_idx >= 0 && knapsack_idx < static_cast(knapsack_constraints.size()), + "Invalid set packing constraint index"); const auto& vars = knapsack_constraints[knapsack_idx].entries; cstr_vars[knapsack_idx].reserve(vars.size()); for (const auto& entry : vars) { @@ -686,14 +685,13 @@ i_t extend_cliques(const std::vector>& knapsack_ continue; } i_t original_row_idx = knapsack_constraints[sp.knapsack_idx].cstr_idx; - if (original_row_idx < 0 || - original_row_idx >= static_cast(original_to_current_row_idx.size())) { - continue; - } + if (original_row_idx < 0) { continue; } + cuopt_assert(original_row_idx < static_cast(original_to_current_row_idx.size()), + "Invalid original row index in knapsack constraint"); i_t current_row_idx = original_to_current_row_idx[original_row_idx]; - if (current_row_idx < 0 || current_row_idx >= static_cast(problem.row_sense.size())) { - continue; - } + if (current_row_idx < 0) { continue; } + cuopt_assert(current_row_idx < static_cast(problem.row_sense.size()), + "Invalid current row index in row mapping"); rows_to_remove.push_back(current_row_idx); } if (rows_to_remove.empty()) { return; } @@ -711,10 +709,10 @@ i_t extend_cliques(const std::vector>& knapsack_ } std::vector removal_marker(problem.row_sense.size(), 0); for (auto row_idx : rows_to_remove) { - if (row_idx >= 0 && row_idx < static_cast(removal_marker.size())) { - CUOPT_LOG_DEBUG("Removing dominated row %d", row_idx); - removal_marker[row_idx] = true; - } + cuopt_assert(row_idx >= 0 && row_idx < static_cast(removal_marker.size()), + "Invalid dominated row index"); + CUOPT_LOG_DEBUG("Removing dominated row %d", row_idx); + removal_marker[row_idx] = true; } dual_simplex::csr_matrix_t A_removed(0, 0, 0); A.remove_rows(removal_marker, A_removed); @@ -759,8 +757,9 @@ i_t extend_cliques(const std::vector>& knapsack_ std::vector new_range_values; for (size_t i = 0; i < problem.range_rows.size(); ++i) { i_t old_row = problem.range_rows[i]; - if (old_row >= 0 && old_row < static_cast(removal_marker.size()) && - !removal_marker[old_row]) { + cuopt_assert(old_row >= 0 && old_row < static_cast(removal_marker.size()), + "Invalid row index in range_rows"); + if (!removal_marker[old_row]) { i_t new_row = old_to_new_indices[old_row]; cuopt_assert(new_row != -1, "Invalid new row index for ranged row renumbering"); new_range_rows.push_back(new_row); From 4f248301e2f15df561f3cd4509aa9c494ad849e4 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 08:49:44 -0800 Subject: [PATCH 103/147] 500 window --- cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index ce24401b6e..a35f7db500 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -660,7 +660,7 @@ i_t extend_cliques(const std::vector>& knapsack_ for (auto v : curr_clique_vars) { signature += static_cast(v); } - constexpr size_t dominance_window = 100; + constexpr size_t dominance_window = 500; auto end_it = std::upper_bound( sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { return value < a.signature; From af4c502a04ddc547db463a9d05bee0537a849d14 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 08:50:02 -0800 Subject: [PATCH 104/147] 1000 window --- cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index a35f7db500..c50ac360d0 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -660,7 +660,7 @@ i_t extend_cliques(const std::vector>& knapsack_ for (auto v : curr_clique_vars) { signature += static_cast(v); } - constexpr size_t dominance_window = 500; + constexpr size_t dominance_window = 1000; auto end_it = std::upper_bound( sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { return value < a.signature; From a3a1bcdad89d427c372356aceffad0fe83b27e14 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 08:53:30 -0800 Subject: [PATCH 105/147] 3 s timer --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 4 +++- .../mip_heuristics/presolve/conflict_graph/clique_table.cu | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index a50d0afd57..6fec16621f 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -203,13 +203,15 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ bool problem_is_infeasible = compute_probing_cache(ls.constraint_prop.bounds_update, *problem_ptr, probing_timer); if (problem_is_infeasible) { return false; } + s } const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && !problem_ptr->empty) { - timer_t clique_timer(15.); + f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 2); + timer_t clique_timer(time_limit_for_clique_table); dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); std::shared_ptr> clique_table; diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index c50ac360d0..ce24401b6e 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -660,7 +660,7 @@ i_t extend_cliques(const std::vector>& knapsack_ for (auto v : curr_clique_vars) { signature += static_cast(v); } - constexpr size_t dominance_window = 1000; + constexpr size_t dominance_window = 100; auto end_it = std::upper_bound( sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { return value < a.signature; From 2f877195aa6a32d6278d739b29cf7bb30a4b3458 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 27 Feb 2026 08:57:47 -0800 Subject: [PATCH 106/147] 3s timer --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 6fec16621f..7d60bcf743 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -203,7 +203,6 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ bool problem_is_infeasible = compute_probing_cache(ls.constraint_prop.bounds_update, *problem_ptr, probing_timer); if (problem_is_infeasible) { return false; } - s } const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } From 4457d33adb87c14744eee25463d9afe2aa271daf Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Sat, 28 Feb 2026 01:05:48 -0800 Subject: [PATCH 107/147] longer dominance window, shorter runtime --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 2 +- cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 7d60bcf743..8c48ecdd95 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -209,7 +209,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && !problem_ptr->empty) { - f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 2); + f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); timer_t clique_timer(time_limit_for_clique_table); dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index ce24401b6e..ade8f70c7f 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -660,7 +660,7 @@ i_t extend_cliques(const std::vector>& knapsack_ for (auto v : curr_clique_vars) { signature += static_cast(v); } - constexpr size_t dominance_window = 100; + constexpr size_t dominance_window = 20000; auto end_it = std::upper_bound( sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { return value < a.signature; From d4d4d6dd26ece9327871c3961b39e51c39734803 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Sat, 28 Feb 2026 01:08:32 -0800 Subject: [PATCH 108/147] increase the limits --- .../mip_heuristics/presolve/conflict_graph/clique_table.cu | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index ade8f70c7f..53b599d4ac 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -557,13 +557,15 @@ i_t extend_cliques(const std::vector>& knapsack_ cuopt::timer_t& timer) { constexpr i_t min_extension_gain = 2; - constexpr i_t extension_yield_window = 64; + constexpr i_t extension_yield_window = 1024; constexpr i_t min_successes_per_window = 1; i_t base_rows = A.m; i_t base_nnz = A.row_start[A.m]; i_t max_added_rows = std::max(8, base_rows / 50); i_t max_added_nnz = std::max(8 * clique_table.max_clique_size_for_extension, base_nnz / 50); + max_added_rows = 1024; + max_added_nnz = std::numeric_limits::max(); i_t added_rows = 0; i_t added_nnz = 0; From 492b6f33f27a562aea94db0dbbf492ac7c95f81f Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 2 Mar 2026 01:08:30 -0800 Subject: [PATCH 109/147] cut gap measurement wip --- .../linear_programming/cuopt/run_mip.cpp | 4 + cpp/src/branch_and_bound/branch_and_bound.cpp | 48 +++ cpp/src/cuts/cuts.cpp | 11 + cpp/src/cuts/cuts.hpp | 43 +++ .../diversity/diversity_manager.cu | 27 ++ .../diversity/known_miplib_objectives.hpp | 334 ++++++++++++++++++ 6 files changed, 467 insertions(+) create mode 100644 cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 51d1b4a43d..35dce566f3 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -183,6 +183,10 @@ int run_single_file(std::string file_path, CUOPT_LOG_ERROR("Parsing MPS failed exiting!"); return -1; } + // Use the benchmark filename for downstream instance-level reporting. + // This keeps per-instance metrics aligned with the run list even if the MPS NAME card differs. + mps_data_model.set_problem_name(base_filename); + if (initial_solution_dir.has_value()) { auto initial_solutions = read_solution_from_dir( initial_solution_dir.value(), base_filename, mps_data_model.get_variable_names()); diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 12cca1e811..9c4486889a 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -2098,6 +2099,49 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t last_objective = root_objective_; f_t root_relax_objective = root_objective_; + auto report_cut_gap_closure_metric = [&]() { + const auto cut_configuration = classify_cut_configuration(settings_); + if (settings_.max_cut_passes <= 0 || cut_configuration == cut_configuration_t::NONE) { + settings_.log.debug("Cut gap closure skipped: max_cut_passes=%d cut_configuration=%s\n", + settings_.max_cut_passes, + cut_configuration_name(cut_configuration)); + return; + } + const std::string normalized_problem_name = + ::cuopt::linear_programming::detail::normalize_problem_name(original_problem_.problem_name); + const auto objective_reference = + ::cuopt::linear_programming::detail::lookup_known_objective_reference( + original_problem_.problem_name); + if (!objective_reference.has_value()) { + settings_.log.printf( + "Cut gap closure skipped: no objective reference for instance raw='%s' normalized='%s'\n", + original_problem_.problem_name.c_str(), + normalized_problem_name.c_str()); + return; + } + const f_t user_objective_before_cuts = + compute_user_objective(original_lp_, root_relax_objective); + const f_t user_objective_after_cuts = compute_user_objective(original_lp_, root_objective_); + const auto gap_closure = + compute_cut_gap_closure(static_cast(objective_reference->objective_value), + user_objective_before_cuts, + user_objective_after_cuts); + settings_.log.printf( + "Cut gap closure [%s] instance=%s known_%s=%.16e root_before=%.16e root_after=%.16e " + "gap_before=%.16e gap_after=%.16e gap_closed=%.16e gap_closed_ratio=%.2f%%\n", + cut_configuration_name(cut_configuration), + original_problem_.problem_name.c_str(), + ::cuopt::linear_programming::detail::objective_reference_status_name( + objective_reference->status), + static_cast(objective_reference->objective_value), + static_cast(user_objective_before_cuts), + static_cast(user_objective_after_cuts), + static_cast(gap_closure.initial_gap), + static_cast(gap_closure.final_gap), + static_cast(gap_closure.gap_closed), + static_cast(gap_closure.gap_closed_ratio * 100.0)); + }; + i_t cut_pool_size = 0; for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { if (num_fractional == 0) { @@ -2335,6 +2379,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); f_t abs_gap = upper_bound_.load() - root_objective_; if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { + report_cut_gap_closure_metric(); set_solution_at_root(solution, cut_info); set_final_solution(solution, root_objective_); return mip_status_t::OPTIMAL; @@ -2354,6 +2399,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } + report_cut_gap_closure_metric(); print_cut_info(settings_, cut_info); if (cut_info.has_cuts()) { @@ -2363,6 +2409,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_.num_cols, original_lp_.A.col_start[original_lp_.A.n]); } + exit(0); + set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); pc_.resize(original_lp_.num_cols); diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index c8e0ba6e78..59f7992649 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -21,6 +21,17 @@ namespace cuopt::linear_programming::dual_simplex { +const char* cut_configuration_name(cut_configuration_t cut_configuration) +{ + switch (cut_configuration) { + case cut_configuration_t::WITHOUT_CLIQUE: return "cuts_without_clique"; + case cut_configuration_t::WITH_CLIQUE: return "cuts_with_clique"; + case cut_configuration_t::CLIQUE_ONLY: return "clique_only"; + case cut_configuration_t::NONE: return "no_cuts_enabled"; + default: return "unknown"; + } +} + namespace { #define DEBUG_CLIQUE_CUTS 0 diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 3302f75111..5cb3253bbb 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -39,6 +39,49 @@ enum cut_type_t : int8_t { MAX_CUT_TYPE = 5 }; +enum class cut_configuration_t : int8_t { + NONE = 0, + WITHOUT_CLIQUE = 1, + WITH_CLIQUE = 2, + CLIQUE_ONLY = 3, +}; + +template +cut_configuration_t classify_cut_configuration(const simplex_solver_settings_t& settings) +{ + const bool clique_enabled = settings.clique_cuts != 0; + const bool non_clique_enabled = settings.mixed_integer_gomory_cuts != 0 || + settings.strong_chvatal_gomory_cuts != 0 || + settings.knapsack_cuts != 0 || settings.mir_cuts != 0; + if (clique_enabled && non_clique_enabled) { return cut_configuration_t::WITH_CLIQUE; } + if (clique_enabled) { return cut_configuration_t::CLIQUE_ONLY; } + if (non_clique_enabled) { return cut_configuration_t::WITHOUT_CLIQUE; } + return cut_configuration_t::NONE; +} + +const char* cut_configuration_name(cut_configuration_t cut_configuration); + +template +struct cut_gap_closure_t { + f_t initial_gap{0.0}; + f_t final_gap{0.0}; + f_t gap_closed{0.0}; + f_t gap_closed_ratio{0.0}; +}; + +template +cut_gap_closure_t compute_cut_gap_closure(f_t objective_reference, + f_t objective_before_cuts, + f_t objective_after_cuts) +{ + const f_t initial_gap = std::abs(objective_reference - objective_before_cuts); + const f_t final_gap = std::abs(objective_reference - objective_after_cuts); + const f_t gap_closed = initial_gap - final_gap; + constexpr f_t eps = static_cast(1e-12); + const f_t gap_closed_ratio = initial_gap > eps ? gap_closed / initial_gap : static_cast(0.0); + return {initial_gap, final_gap, gap_closed, gap_closed_ratio}; +} + template struct cut_info_t { bool has_cuts() const diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 6c36ea49b5..20bf9fed74 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -8,6 +8,7 @@ #include "cuda_profiler_api.h" #include "diversity_manager.cuh" +#include #include #include #include @@ -334,6 +335,32 @@ solution_t diversity_manager_t::run_solver() context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? "deterministic" : "opportunistic"); + if (problem_ptr->original_problem_ptr != nullptr) { + const auto problem_name = problem_ptr->original_problem_ptr->get_problem_name(); + const auto normalized_problem_name = + ::cuopt::linear_programming::detail::normalize_problem_name(problem_name); + CUOPT_LOG_DEBUG("Objective reference lookup raw='%s' normalized='%s'", + problem_name.c_str(), + normalized_problem_name.c_str()); + if (!problem_name.empty()) { + const auto objective_reference = + ::cuopt::linear_programming::detail::lookup_known_objective_reference(problem_name); + if (objective_reference.has_value()) { + CUOPT_LOG_INFO("Known objective reference for %s: %.17g (%s)", + problem_name.c_str(), + objective_reference->objective_value, + ::cuopt::linear_programming::detail::objective_reference_status_name( + objective_reference->status)); + } else { + CUOPT_LOG_DEBUG("No objective reference mapping found for %s", problem_name.c_str()); + } + } else { + CUOPT_LOG_DEBUG("Skipping objective reference lookup because problem_name is empty"); + } + } else { + CUOPT_LOG_DEBUG("Skipping objective reference lookup because original_problem_ptr is null"); + } + // to automatically compute the solving time on scope exit auto timer_raii_guard = cuopt::scope_guard([&]() { stats.total_solve_time = timer.elapsed_time(); }); diff --git a/cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp b/cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp new file mode 100644 index 0000000000..23c4b74b8b --- /dev/null +++ b/cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp @@ -0,0 +1,334 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::detail { + +enum class objective_reference_status_t : int8_t { OPTIMAL = 0, BEST_KNOWN = 1 }; + +struct objective_reference_t { + double objective_value; + objective_reference_status_t status; +}; + +inline const char* objective_reference_status_name(objective_reference_status_t status) +{ + return status == objective_reference_status_t::OPTIMAL ? "optimal" : "best_known"; +} + +inline std::string normalize_problem_name(std::string problem_name) +{ + auto trim_ascii_whitespace = [](std::string& s) { + auto is_ws = [](unsigned char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; + }; + size_t begin = 0; + while (begin < s.size() && is_ws(static_cast(s[begin]))) { + ++begin; + } + size_t end = s.size(); + while (end > begin && is_ws(static_cast(s[end - 1]))) { + --end; + } + s = s.substr(begin, end - begin); + }; + + trim_ascii_whitespace(problem_name); + if (!problem_name.empty()) { + const char first = problem_name.front(); + const char last = problem_name.back(); + const bool wrapped_in_quotes = (first == '"' && last == '"') || (first == '\'' && last == '\''); + if (wrapped_in_quotes && problem_name.size() >= 2) { + problem_name = problem_name.substr(1, problem_name.size() - 2); + trim_ascii_whitespace(problem_name); + } + } + + const auto slash_pos = problem_name.find_last_of("/\\"); + if (slash_pos != std::string::npos) { problem_name = problem_name.substr(slash_pos + 1); } + trim_ascii_whitespace(problem_name); + std::transform( + problem_name.begin(), problem_name.end(), problem_name.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + const std::array suffixes = {".mps.gz", ".mps.bz2", ".mps", ".gz", ".bz2"}; + auto ends_with = [](const std::string& s, const std::string& suffix) { + return s.size() >= suffix.size() && + s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; + }; + bool removed = true; + while (removed) { + removed = false; + for (const auto& suffix : suffixes) { + if (ends_with(problem_name, suffix)) { + problem_name.resize(problem_name.size() - suffix.size()); + removed = true; + break; + } + } + } + return problem_name; +} + +inline std::optional lookup_known_objective_reference( + std::string problem_name) +{ + // These benchmark names are intentionally absent because miplib2017-v36.solu does not provide + // a finite reference objective for them: + // bnatt500, cryptanalysiskb128n5obj14, fhnw-binpack4-4, neos-2075418-temuka, + // neos-3402454-bohle, neos-3988577-wolgan, neos859080. + static const std::unordered_map k_objective_map = { + {"30n20b8", {302, objective_reference_status_t::OPTIMAL}}, + {"50v-10", {3311.1799841000002, objective_reference_status_t::OPTIMAL}}, + {"academictimetablesmall", {0, objective_reference_status_t::OPTIMAL}}, + {"air05", {26374, objective_reference_status_t::OPTIMAL}}, + {"app1-1", {-3, objective_reference_status_t::OPTIMAL}}, + {"app1-2", {-41, objective_reference_status_t::OPTIMAL}}, + {"assign1-5-8", {211.99999999999801, objective_reference_status_t::OPTIMAL}}, + {"atlanta-ip", {90.009878614000002, objective_reference_status_t::OPTIMAL}}, + {"b1c1s1", {24544.25, objective_reference_status_t::OPTIMAL}}, + {"bab2", {-357544.31150000001, objective_reference_status_t::OPTIMAL}}, + {"bab6", {-284248.23070000007, objective_reference_status_t::OPTIMAL}}, + {"beasleyc3", {753.9999999999128, objective_reference_status_t::OPTIMAL}}, + {"binkar10_1", {6741.3800239397196, objective_reference_status_t::OPTIMAL}}, + {"blp-ar98", {6205.2147103999996, objective_reference_status_t::OPTIMAL}}, + {"blp-ic98", {4491.4475839500001, objective_reference_status_t::OPTIMAL}}, + {"bnatt400", {1, objective_reference_status_t::OPTIMAL}}, + {"bppc4-08", {53, objective_reference_status_t::OPTIMAL}}, + {"brazil3", {24, objective_reference_status_t::OPTIMAL}}, + {"buildingenergy", {33283.853236000003, objective_reference_status_t::OPTIMAL}}, + {"cbs-cta", {0, objective_reference_status_t::OPTIMAL}}, + {"chromaticindex1024-7", {4, objective_reference_status_t::OPTIMAL}}, + {"chromaticindex512-7", {4, objective_reference_status_t::OPTIMAL}}, + {"cmflsp50-24-8-8", {55789389.886, objective_reference_status_t::OPTIMAL}}, + {"cms750_4", {252, objective_reference_status_t::OPTIMAL}}, + {"co-100", {2639942.0600000001, objective_reference_status_t::OPTIMAL}}, + {"cod105", {-12, objective_reference_status_t::OPTIMAL}}, + {"comp07-2idx", {6, objective_reference_status_t::OPTIMAL}}, + {"comp21-2idx", {74, objective_reference_status_t::OPTIMAL}}, + {"cost266-uue", {25148940.55999998, objective_reference_status_t::OPTIMAL}}, + {"cryptanalysiskb128n5obj16", {0, objective_reference_status_t::OPTIMAL}}, + {"csched007", {350.99999999999551, objective_reference_status_t::OPTIMAL}}, + {"csched008", {173, objective_reference_status_t::OPTIMAL}}, + {"cvs16r128-89", {-97, objective_reference_status_t::OPTIMAL}}, + {"dano3_3", {576.34463302999995, objective_reference_status_t::OPTIMAL}}, + {"dano3_5", {576.9249159565619, objective_reference_status_t::OPTIMAL}}, + {"decomp2", {-160, objective_reference_status_t::OPTIMAL}}, + {"drayage-100-23", {103333.87407000001, objective_reference_status_t::OPTIMAL}}, + {"drayage-25-23", {101282.647018, objective_reference_status_t::OPTIMAL}}, + {"dws008-01", {37412.604587945083, objective_reference_status_t::OPTIMAL}}, + {"eil33-2", {934.007915999999, objective_reference_status_t::OPTIMAL}}, + {"eila101-2", {880.92010799999991, objective_reference_status_t::OPTIMAL}}, + {"enlight_hard", {37, objective_reference_status_t::OPTIMAL}}, + {"ex10", {100, objective_reference_status_t::OPTIMAL}}, + {"ex9", {81, objective_reference_status_t::OPTIMAL}}, + {"exp-1-500-5-5", {65887, objective_reference_status_t::OPTIMAL}}, + {"fast0507", {174, objective_reference_status_t::OPTIMAL}}, + {"fastxgemm-n2r6s0t2", {230, objective_reference_status_t::OPTIMAL}}, + {"fhnw-binpack4-48", {0, objective_reference_status_t::OPTIMAL}}, + {"fiball", {138, objective_reference_status_t::OPTIMAL}}, + {"gen-ip002", {-4783.7333920000001, objective_reference_status_t::OPTIMAL}}, + {"gen-ip054", {6840.9656417899996, objective_reference_status_t::OPTIMAL}}, + {"germanrr", {47095869.648999996, objective_reference_status_t::OPTIMAL}}, + {"gfd-schedulen180f7d50m30k18", {1, objective_reference_status_t::OPTIMAL}}, + {"glass-sc", {23, objective_reference_status_t::OPTIMAL}}, + {"glass4", {1200012599.972384, objective_reference_status_t::OPTIMAL}}, + {"gmu-35-40", {-2406733.3687999998, objective_reference_status_t::OPTIMAL}}, + {"gmu-35-50", {-2607958.3300000001, objective_reference_status_t::OPTIMAL}}, + {"graph20-20-1rand", {-9, objective_reference_status_t::OPTIMAL}}, + {"graphdraw-domain", {19685.999975500381, objective_reference_status_t::OPTIMAL}}, + {"h80x6320d", {6382.0990482459993, objective_reference_status_t::OPTIMAL}}, + {"highschool1-aigio", {0, objective_reference_status_t::OPTIMAL}}, + {"hypothyroid-k1", {-2851, objective_reference_status_t::OPTIMAL}}, + {"ic97_potential", {3941.9999309022501, objective_reference_status_t::OPTIMAL}}, + {"icir97_tension", {6375, objective_reference_status_t::OPTIMAL}}, + {"irish-electricity", {3723497.5913959998, objective_reference_status_t::OPTIMAL}}, + {"irp", {12159.492835396981, objective_reference_status_t::OPTIMAL}}, + {"istanbul-no-cutoff", {204.08170701, objective_reference_status_t::OPTIMAL}}, + {"k1mushroom", {-3288, objective_reference_status_t::OPTIMAL}}, + {"lectsched-5-obj", {24, objective_reference_status_t::OPTIMAL}}, + {"leo1", {404227536.16000003, objective_reference_status_t::OPTIMAL}}, + {"leo2", {404077441.12, objective_reference_status_t::OPTIMAL}}, + {"lotsize", {1480195, objective_reference_status_t::OPTIMAL}}, + {"mad", {0.026800000000000001, objective_reference_status_t::OPTIMAL}}, + {"map10", {-495, objective_reference_status_t::OPTIMAL}}, + {"map16715-04", {-111, objective_reference_status_t::OPTIMAL}}, + {"markshare2", {1, objective_reference_status_t::OPTIMAL}}, + {"markshare_4_0", {1, objective_reference_status_t::OPTIMAL}}, + {"mas74", {11801.185719999999, objective_reference_status_t::OPTIMAL}}, + {"mas76", {40005.053989999993, objective_reference_status_t::OPTIMAL}}, + {"mc11", {11688.99999999966, objective_reference_status_t::OPTIMAL}}, + {"mcsched", {211913, objective_reference_status_t::OPTIMAL}}, + {"mik-250-20-75-4", {-52301, objective_reference_status_t::OPTIMAL}}, + {"milo-v12-6-r2-40-1", {326481.14282799, objective_reference_status_t::OPTIMAL}}, + {"momentum1", {109143.4935, objective_reference_status_t::OPTIMAL}}, + {"mushroom-best", {0.055333761199999998, objective_reference_status_t::OPTIMAL}}, + {"mzzv11", {-21718, objective_reference_status_t::OPTIMAL}}, + {"mzzv42z", {-20540, objective_reference_status_t::OPTIMAL}}, + {"n2seq36q", {52200, objective_reference_status_t::OPTIMAL}}, + {"n3div36", {130800, objective_reference_status_t::OPTIMAL}}, + {"n5-3", {8104.9999999939992, objective_reference_status_t::OPTIMAL}}, + {"neos-1122047", {161, objective_reference_status_t::OPTIMAL}}, + {"neos-1171448", {-309, objective_reference_status_t::OPTIMAL}}, + {"neos-1171737", {-195, objective_reference_status_t::OPTIMAL}}, + {"neos-1354092", {46, objective_reference_status_t::OPTIMAL}}, + {"neos-1445765", {-17783, objective_reference_status_t::OPTIMAL}}, + {"neos-1456979", {176, objective_reference_status_t::OPTIMAL}}, + {"neos-1582420", {90.999999999999957, objective_reference_status_t::OPTIMAL}}, + {"neos-2657525-crna", {1.810748, objective_reference_status_t::OPTIMAL}}, + {"neos-2746589-doon", {2008.1999999999989, objective_reference_status_t::OPTIMAL}}, + {"neos-2978193-inde", {-2.3880616899999998, objective_reference_status_t::OPTIMAL}}, + {"neos-2987310-joes", {-607702988.29999995, objective_reference_status_t::OPTIMAL}}, + {"neos-3004026-krka", {0, objective_reference_status_t::OPTIMAL}}, + {"neos-3024952-loue", {26756, objective_reference_status_t::OPTIMAL}}, + {"neos-3046615-murg", {1600, objective_reference_status_t::OPTIMAL}}, + {"neos-3083819-nubu", {6307996, objective_reference_status_t::OPTIMAL}}, + {"neos-3216931-puriri", {71320, objective_reference_status_t::OPTIMAL}}, + {"neos-3381206-awhea", {453, objective_reference_status_t::OPTIMAL}}, + {"neos-3402294-bobin", {0.067249999999999491, objective_reference_status_t::OPTIMAL}}, + {"neos-3555904-turama", {-34.700000000000003, objective_reference_status_t::OPTIMAL}}, + {"neos-3627168-kasai", {988585.61999999976, objective_reference_status_t::OPTIMAL}}, + {"neos-3656078-kumeu", {-13172.200000000001, objective_reference_status_t::OPTIMAL}}, + {"neos-3754480-nidda", {12939.7540104743, objective_reference_status_t::OPTIMAL}}, + {"neos-4300652-rahue", {2.1415999999999999, objective_reference_status_t::OPTIMAL}}, + {"neos-4338804-snowy", {1471, objective_reference_status_t::OPTIMAL}}, + {"neos-4387871-tavua", {33.384729927000002, objective_reference_status_t::OPTIMAL}}, + {"neos-4413714-turia", {45.370167019999798, objective_reference_status_t::OPTIMAL}}, + {"neos-4532248-waihi", {61.599999999999987, objective_reference_status_t::OPTIMAL}}, + {"neos-4647030-tutaki", {27265.705999999958, objective_reference_status_t::OPTIMAL}}, + {"neos-4722843-widden", {25009.662227000001, objective_reference_status_t::OPTIMAL}}, + {"neos-4738912-atrato", {283627956.59500003, objective_reference_status_t::OPTIMAL}}, + {"neos-4763324-toguru", {1613.0388458499999, objective_reference_status_t::OPTIMAL}}, + {"neos-4954672-berkel", {2612710, objective_reference_status_t::OPTIMAL}}, + {"neos-5049753-cuanza", {561.99999716889999, objective_reference_status_t::OPTIMAL}}, + {"neos-5052403-cygnet", {182, objective_reference_status_t::OPTIMAL}}, + {"neos-5093327-huahum", {6259.9999971258949, objective_reference_status_t::OPTIMAL}}, + {"neos-5104907-jarama", {935, objective_reference_status_t::OPTIMAL}}, + {"neos-5107597-kakapo", {3644.9999999995198, objective_reference_status_t::OPTIMAL}}, + {"neos-5114902-kasavu", {655, objective_reference_status_t::OPTIMAL}}, + {"neos-5188808-nattai", {0.110283622999984, objective_reference_status_t::OPTIMAL}}, + {"neos-5195221-niemur", {0.0038354325999999999, objective_reference_status_t::OPTIMAL}}, + {"neos-631710", {203, objective_reference_status_t::OPTIMAL}}, + {"neos-662469", {184379.99999999991, objective_reference_status_t::OPTIMAL}}, + {"neos-787933", {30, objective_reference_status_t::OPTIMAL}}, + {"neos-827175", {112.00152, objective_reference_status_t::OPTIMAL}}, + {"neos-848589", {2351.40309999697, objective_reference_status_t::OPTIMAL}}, + {"neos-860300", {3200.9999999999982, objective_reference_status_t::OPTIMAL}}, + {"neos-873061", {113.6562385063, objective_reference_status_t::OPTIMAL}}, + {"neos-911970", {54.759999999999998, objective_reference_status_t::OPTIMAL}}, + {"neos-933966", {318, objective_reference_status_t::OPTIMAL}}, + {"neos-950242", {4, objective_reference_status_t::OPTIMAL}}, + {"neos-957323", {-237.75668150000001, objective_reference_status_t::OPTIMAL}}, + {"neos-960392", {-238, objective_reference_status_t::OPTIMAL}}, + {"neos17", {0.1500025774, objective_reference_status_t::OPTIMAL}}, + {"neos5", {15, objective_reference_status_t::OPTIMAL}}, + {"neos8", {-3719, objective_reference_status_t::OPTIMAL}}, + {"net12", {214, objective_reference_status_t::OPTIMAL}}, + {"netdiversion", {242, objective_reference_status_t::OPTIMAL}}, + {"nexp-150-20-8-5", {231, objective_reference_status_t::OPTIMAL}}, + {"ns1116954", {0, objective_reference_status_t::OPTIMAL}}, + {"ns1208400", {2, objective_reference_status_t::OPTIMAL}}, + {"ns1644855", {-1524.3333333333301, objective_reference_status_t::OPTIMAL}}, + {"ns1760995", {-549.21438505000003, objective_reference_status_t::OPTIMAL}}, + {"ns1830653", {20622, objective_reference_status_t::OPTIMAL}}, + {"ns1952667", {0, objective_reference_status_t::OPTIMAL}}, + {"nu25-pr12", {53904.999999999993, objective_reference_status_t::OPTIMAL}}, + {"nursesched-medium-hint03", {115, objective_reference_status_t::OPTIMAL}}, + {"nursesched-sprint02", {57.999999999999993, objective_reference_status_t::OPTIMAL}}, + {"nw04", {16862, objective_reference_status_t::OPTIMAL}}, + {"opm2-z10-s4", {-33269, objective_reference_status_t::OPTIMAL}}, + {"p200x1188c", {15078, objective_reference_status_t::OPTIMAL}}, + {"peg-solitaire-a3", {1, objective_reference_status_t::OPTIMAL}}, + {"pg", {-8674.3426071199992, objective_reference_status_t::OPTIMAL}}, + {"pg5_34", {-14339.353450000001, objective_reference_status_t::OPTIMAL}}, + {"physiciansched3-3", {2623271.3266670001, objective_reference_status_t::OPTIMAL}}, + {"physiciansched6-2", {49324, objective_reference_status_t::OPTIMAL}}, + {"piperout-08", {125054.9999999999, objective_reference_status_t::OPTIMAL}}, + {"piperout-27", {8123.9999999999727, objective_reference_status_t::OPTIMAL}}, + {"pk1", {11, objective_reference_status_t::OPTIMAL}}, + {"proteindesign121hz512p9", {1473, objective_reference_status_t::OPTIMAL}}, + {"proteindesign122trx11p8", {1747, objective_reference_status_t::OPTIMAL}}, + {"qap10", {339.99999999838712, objective_reference_status_t::OPTIMAL}}, + {"radiationm18-12-05", {17566, objective_reference_status_t::OPTIMAL}}, + {"radiationm40-10-02", {155328, objective_reference_status_t::OPTIMAL}}, + {"rail01", {-70.569964299999995, objective_reference_status_t::OPTIMAL}}, + {"rail02", {-200.44990770000001, objective_reference_status_t::OPTIMAL}}, + {"rail507", {174, objective_reference_status_t::OPTIMAL}}, + {"ran14x18-disj-8", {3712, objective_reference_status_t::OPTIMAL}}, + {"rd-rplusc-21", {165395.275295, objective_reference_status_t::OPTIMAL}}, + {"reblock115", {-36800603.233199999, objective_reference_status_t::OPTIMAL}}, + {"rmatr100-p10", {423, objective_reference_status_t::OPTIMAL}}, + {"rmatr200-p5", {4521, objective_reference_status_t::OPTIMAL}}, + {"roci-4-11", {-6020203, objective_reference_status_t::OPTIMAL}}, + {"rocii-5-11", {-6.6755047315380001, objective_reference_status_t::OPTIMAL}}, + {"rococob10-011000", {19449, objective_reference_status_t::OPTIMAL}}, + {"rocococ10-001000", {11460, objective_reference_status_t::OPTIMAL}}, + {"roi2alpha3n4", {-63.208495030000002, objective_reference_status_t::OPTIMAL}}, + {"roi5alpha10n8", {-52.322274350999997, objective_reference_status_t::OPTIMAL}}, + {"roll3000", {12889.999991999999, objective_reference_status_t::OPTIMAL}}, + {"s100", {-0.16972352705829999, objective_reference_status_t::OPTIMAL}}, + {"s250r10", {-0.17178048342319999, objective_reference_status_t::OPTIMAL}}, + {"satellites2-40", {-19, objective_reference_status_t::OPTIMAL}}, + {"satellites2-60-fs", {-19.000000000099998, objective_reference_status_t::OPTIMAL}}, + {"savsched1", {3217.6999999999998, objective_reference_status_t::OPTIMAL}}, + {"sct2", {-230.9891623, objective_reference_status_t::OPTIMAL}}, + {"seymour", {423, objective_reference_status_t::OPTIMAL}}, + {"seymour1", {410.76370138999999, objective_reference_status_t::OPTIMAL}}, + {"sing326", {7753674.8537600003, objective_reference_status_t::OPTIMAL}}, + {"sing44", {8128831.1771999998, objective_reference_status_t::OPTIMAL}}, + {"snp-02-004-104", {586803238.65672886, objective_reference_status_t::OPTIMAL}}, + {"sorrell3", {-16, objective_reference_status_t::OPTIMAL}}, + {"sp150x300d", {69, objective_reference_status_t::OPTIMAL}}, + {"sp97ar", {660705645.75899994, objective_reference_status_t::OPTIMAL}}, + {"sp98ar", {529740623.19999999, objective_reference_status_t::OPTIMAL}}, + {"splice1k1", {-394, objective_reference_status_t::OPTIMAL}}, + {"square41", {15, objective_reference_status_t::OPTIMAL}}, + {"square47", {15.9999999997877, objective_reference_status_t::OPTIMAL}}, + {"supportcase10", {7, objective_reference_status_t::OPTIMAL}}, + {"supportcase12", {-7559.5330538170001, objective_reference_status_t::OPTIMAL}}, + {"supportcase18", {48, objective_reference_status_t::OPTIMAL}}, + {"supportcase19", {12677205.999920519, objective_reference_status_t::OPTIMAL}}, + {"supportcase22", {110, objective_reference_status_t::BEST_KNOWN}}, + {"supportcase26", {1745.1238129999999, objective_reference_status_t::OPTIMAL}}, + {"supportcase33", {-345, objective_reference_status_t::OPTIMAL}}, + {"supportcase40", {24256.3122898, objective_reference_status_t::OPTIMAL}}, + {"supportcase42", {7.7586307222700004, objective_reference_status_t::OPTIMAL}}, + {"supportcase6", {51906.477370000001, objective_reference_status_t::OPTIMAL}}, + {"supportcase7", {-1132.2231770000001, objective_reference_status_t::OPTIMAL}}, + {"swath1", {379.07129574999999, objective_reference_status_t::OPTIMAL}}, + {"swath3", {397.76134365000001, objective_reference_status_t::OPTIMAL}}, + {"tbfp-network", {24.163194440000002, objective_reference_status_t::OPTIMAL}}, + {"thor50dday", {40417, objective_reference_status_t::OPTIMAL}}, + {"timtab1", {764771.99999977998, objective_reference_status_t::OPTIMAL}}, + {"tr12-30", {130595.9999999999, objective_reference_status_t::OPTIMAL}}, + {"traininstance2", {71820, objective_reference_status_t::OPTIMAL}}, + {"traininstance6", {28290, objective_reference_status_t::OPTIMAL}}, + {"trento1", {5189487, objective_reference_status_t::OPTIMAL}}, + {"triptim1", {22.868099999999899, objective_reference_status_t::OPTIMAL}}, + {"uccase12", {11507.4050616, objective_reference_status_t::OPTIMAL}}, + {"uccase9", {10993.131409, objective_reference_status_t::OPTIMAL}}, + {"uct-subprob", {314, objective_reference_status_t::OPTIMAL}}, + {"unitcal_7", {19635558.243999999, objective_reference_status_t::OPTIMAL}}, + {"var-smallemery-m6j6", {-149.37501, objective_reference_status_t::OPTIMAL}}, + {"wachplan", {-8, objective_reference_status_t::OPTIMAL}}, + }; + const auto normalized = normalize_problem_name(std::move(problem_name)); + const auto iter = k_objective_map.find(normalized); + if (iter == k_objective_map.end()) { return std::nullopt; } + return iter->second; +} + +} // namespace cuopt::linear_programming::detail From 358ee243b770e8b2d2f52fc6db50b41aa323b09d Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 2 Mar 2026 06:36:16 -0800 Subject: [PATCH 110/147] add clique config tests --- cpp/src/branch_and_bound/branch_and_bound.cpp | 13 +-- .../diversity/diversity_manager.cu | 88 ++++++++++++++++--- .../diversity/diversity_manager.cuh | 20 +++++ .../presolve/third_party_presolve.cpp | 1 + cpp/src/mip_heuristics/solver.cu | 1 + cpp/src/pdlp/solve.cu | 5 +- 6 files changed, 105 insertions(+), 23 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 9c4486889a..e0faf714bb 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2102,16 +2102,17 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut auto report_cut_gap_closure_metric = [&]() { const auto cut_configuration = classify_cut_configuration(settings_); if (settings_.max_cut_passes <= 0 || cut_configuration == cut_configuration_t::NONE) { - settings_.log.debug("Cut gap closure skipped: max_cut_passes=%d cut_configuration=%s\n", - settings_.max_cut_passes, - cut_configuration_name(cut_configuration)); + settings_.log.printf("Cut gap closure skipped: max_cut_passes=%d cut_configuration=%s\n", + settings_.max_cut_passes, + cut_configuration_name(cut_configuration)); return; } + const std::string& instance_name_for_lookup = original_problem_.problem_name; const std::string normalized_problem_name = - ::cuopt::linear_programming::detail::normalize_problem_name(original_problem_.problem_name); + ::cuopt::linear_programming::detail::normalize_problem_name(instance_name_for_lookup); const auto objective_reference = ::cuopt::linear_programming::detail::lookup_known_objective_reference( - original_problem_.problem_name); + instance_name_for_lookup); if (!objective_reference.has_value()) { settings_.log.printf( "Cut gap closure skipped: no objective reference for instance raw='%s' normalized='%s'\n", @@ -2130,7 +2131,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut "Cut gap closure [%s] instance=%s known_%s=%.16e root_before=%.16e root_after=%.16e " "gap_before=%.16e gap_after=%.16e gap_closed=%.16e gap_closed_ratio=%.2f%%\n", cut_configuration_name(cut_configuration), - original_problem_.problem_name.c_str(), + instance_name_for_lookup.c_str(), ::cuopt::linear_programming::detail::objective_reference_status_name( objective_reference->status), static_cast(objective_reference->objective_value), diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 20bf9fed74..dd2c83af36 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -8,6 +8,7 @@ #include "cuda_profiler_api.h" #include "diversity_manager.cuh" +#include #include #include #include @@ -25,6 +26,17 @@ constexpr bool fj_only_run = false; namespace cuopt::linear_programming::detail { +const char* env_cut_configuration_name(env_cut_configuration_t config) +{ + switch (config) { + case env_cut_configuration_t::WITHOUT_CLIQUE: return "cuts_without_clique"; + case env_cut_configuration_t::WITH_CLIQUE: return "cuts_with_clique"; + case env_cut_configuration_t::CLIQUE_ONLY: return "clique_only"; + case env_cut_configuration_t::NONE: return "default"; + default: return "unknown"; + } +} + size_t fp_recombiner_config_t::max_n_of_vars_from_other = fp_recombiner_config_t::initial_n_of_vars_from_other; size_t ls_recombiner_config_t::max_n_of_vars_from_other = @@ -79,9 +91,7 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_t::n_of_arms, cuopt::seed_generator::get_seed(), ls_alpha, "ls"), ls_hash_map(*context.problem_ptr) { - // Read configuration ID from environment variable - int max_config = -1; - // Read max configuration value from environment variable + int max_config = -1; const char* env_max_config = std::getenv("CUOPT_MAX_CONFIG"); if (env_max_config != nullptr) { try { @@ -91,18 +101,68 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_t 1) { - [[maybe_unused]] int config_id = -1; // Default value - const char* env_config_id = std::getenv("CUOPT_CONFIG_ID"); - if (env_config_id != nullptr) { - try { - config_id = std::stoi(env_config_id); - CUOPT_LOG_INFO("Using configuration ID from environment: %d", config_id); - } catch (const std::exception& e) { - CUOPT_LOG_WARN("Failed to parse CUOPT_CONFIG_ID environment variable: %s", e.what()); - } - } + + const char* env_config_id_raw = std::getenv("CUOPT_CONFIG_ID"); + if (env_config_id_raw == nullptr) { return; } + + try { + env_config_id = std::stoi(env_config_id_raw); + } catch (const std::exception& e) { + CUOPT_LOG_WARN("Failed to parse CUOPT_CONFIG_ID environment variable: %s", e.what()); + return; + } + + if (max_config > 0 && env_config_id >= max_config) { + CUOPT_LOG_WARN( + "CUOPT_CONFIG_ID=%d is outside [0, %d). Ignoring cut override.", env_config_id, max_config); + return; + } + + switch (env_config_id) { + case 0: env_cut_configuration = env_cut_configuration_t::WITHOUT_CLIQUE; break; + case 1: env_cut_configuration = env_cut_configuration_t::WITH_CLIQUE; break; + case 2: env_cut_configuration = env_cut_configuration_t::CLIQUE_ONLY; break; + default: + CUOPT_LOG_WARN( + "Unsupported CUOPT_CONFIG_ID=%d for cut configuration. " + "Expected 0 (without clique), 1 (with clique), or 2 (clique only).", + env_config_id); + env_cut_configuration = env_cut_configuration_t::NONE; + return; + } + + CUOPT_LOG_INFO("Using cut configuration from CUOPT_CONFIG_ID=%d (%s)", + env_config_id, + env_cut_configuration_name(env_cut_configuration)); +} + +template +void diversity_manager_t::apply_cut_configuration_from_env( + dual_simplex::simplex_solver_settings_t& settings) const +{ + if (env_cut_configuration == env_cut_configuration_t::NONE) { return; } + switch (env_cut_configuration) { + case env_cut_configuration_t::WITHOUT_CLIQUE: settings.clique_cuts = 0; break; + case env_cut_configuration_t::WITH_CLIQUE: settings.clique_cuts = 1; break; + case env_cut_configuration_t::CLIQUE_ONLY: + settings.clique_cuts = 1; + settings.mixed_integer_gomory_cuts = 0; + settings.mir_cuts = 0; + settings.knapsack_cuts = 0; + settings.strong_chvatal_gomory_cuts = 0; + break; + default: break; } + settings.log.printf( + "Applied cut configuration from CUOPT_CONFIG_ID=%d (%s): mir=%d gomory=%d knapsack=%d " + "strong_cg=%d clique=%d\n", + env_config_id, + env_cut_configuration_name(env_cut_configuration), + settings.mir_cuts, + settings.mixed_integer_gomory_cuts, + settings.knapsack_cuts, + settings.strong_chvatal_gomory_cuts, + settings.clique_cuts); } // this function is to specialize the local search with config from diversity manager diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cuh b/cpp/src/mip_heuristics/diversity/diversity_manager.cuh index d4e24bdeaf..06b7f06e0c 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cuh +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cuh @@ -27,8 +27,24 @@ #include #include +#include + +namespace cuopt::linear_programming::dual_simplex { +template +struct simplex_solver_settings_t; +} + namespace cuopt::linear_programming::detail { +enum class env_cut_configuration_t : int8_t { + NONE = -1, + WITHOUT_CLIQUE = 0, + WITH_CLIQUE = 1, + CLIQUE_ONLY = 2, +}; + +const char* env_cut_configuration_name(env_cut_configuration_t config); + template class diversity_manager_t { public: @@ -69,6 +85,8 @@ class diversity_manager_t { void set_simplex_solution(const std::vector& solution, const std::vector& dual_solution, f_t objective); + void apply_cut_configuration_from_env( + dual_simplex::simplex_solver_settings_t& settings) const; mip_solver_context_t& context; dual_simplex::branch_and_bound_t* branch_and_bound_ptr; @@ -103,6 +121,8 @@ class diversity_manager_t { bool run_only_bp_recombiner{false}; bool run_only_fp_recombiner{false}; bool run_only_sub_mip_recombiner{false}; + env_cut_configuration_t env_cut_configuration{env_cut_configuration_t::NONE}; + int env_config_id{-1}; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 5a89393a6a..7c38acdcbb 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -662,6 +662,7 @@ std::optional> third_party_presolve_t( papilo_problem, op_problem.get_handle_ptr(), category, maximize_); + opt_problem.set_problem_name(op_problem.get_problem_name()); auto col_flags = papilo_problem.getColFlags(); std::vector implied_integer_indices; for (size_t i = 0; i < col_flags.size(); i++) { diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 3b7b15936d..32d67fb550 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -223,6 +223,7 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.clique_cuts = context.settings.clique_cuts; branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; + dm.apply_cut_configuration_from_env(branch_and_bound_settings); branch_and_bound_settings.reduced_cost_strengthening = context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index fa0c79e391..ed77d11cf5 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -1398,9 +1398,8 @@ cuopt::linear_programming::optimization_problem_t mps_data_model_to_op if (data_model.get_objective_name().size() != 0) { op_problem.set_objective_name(data_model.get_objective_name()); } - if (data_model.get_problem_name().size() != 0) { - op_problem.set_problem_name(data_model.get_problem_name().data()); - } + auto problem_name = data_model.get_problem_name(); + op_problem.set_problem_name(problem_name); if (data_model.get_variable_names().size() != 0) { op_problem.set_variable_names(data_model.get_variable_names()); } From 878d18e0353a94aea85b865adac943fe1f0c8864 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 2 Mar 2026 06:39:22 -0800 Subject: [PATCH 111/147] larger work estimate --- cpp/src/cuts/cuts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 59f7992649..59c04568a0 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1184,7 +1184,7 @@ bool cut_generation_t::generate_clique_cuts( // TODO this can be problem dependent const i_t max_calls = 100000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 1e8; + const f_t max_work_estimate = 2e9; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); From 549d3c5b934830689cd79f7f8b1ed47fb6948a3c Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 2 Mar 2026 11:36:18 -0800 Subject: [PATCH 112/147] with initial clique --- cpp/src/branch_and_bound/branch_and_bound.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e0faf714bb..37782b613d 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2410,7 +2410,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut original_lp_.num_cols, original_lp_.A.col_start[original_lp_.A.n]); } - exit(0); set_uninitialized_steepest_edge_norms(original_lp_, basic_list, edge_norms_); From 550b1c057eb49e70f48179a117ab2f1e8e203c27 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 2 Mar 2026 11:38:00 -0800 Subject: [PATCH 113/147] without initial cliques --- .../diversity/diversity_manager.cu | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 511bbe4c2f..adab6d671f 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -268,27 +268,27 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && - !problem_ptr->empty) { - f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); - timer_t clique_timer(time_limit_for_clique_table); - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - find_initial_cliques( - host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - problem_ptr->set_constraints_from_host_user_problem(host_problem); - cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - "host lower bound size mismatch"); - cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - "host upper bound size mismatch"); - std::vector all_var_indices(problem_ptr->n_variables); - std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - trivial_presolve(*problem_ptr, remap_cache_ids); - if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - } + // if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && + // !problem_ptr->empty) { + // f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); + // timer_t clique_timer(time_limit_for_clique_table); + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + // find_initial_cliques( + // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + // "host lower bound size mismatch"); + // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + // "host upper bound size mismatch"); + // std::vector all_var_indices(problem_ptr->n_variables); + // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + // trivial_presolve(*problem_ptr, remap_cache_ids); + // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 9faf89ba3725c9131677222681a96d3f54f6fd2d Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Mar 2026 07:51:17 -0800 Subject: [PATCH 114/147] without initial clique and no infeasible on complement --- cpp/src/cuts/cuts.cpp | 50 ++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 59c04568a0..61b16f3642 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -110,6 +110,8 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic // that's why compelements have 1 as coeff and normal vars have -1 if (complement) { if (seen_original.count(var_idx) > 0) { + // FIXME: this is temporary, fix all the vars of all other vars in the clique + return clique_cut_build_status_t::NO_CUT; CLIQUE_CUTS_DEBUG("build_clique_cut infeasible var=%lld appears as variable and complement", static_cast(var_idx)); return clique_cut_build_status_t::INFEASIBLE; @@ -121,6 +123,8 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic cut.x.push_back(1.0); } else { if (seen_complement.count(var_idx) > 0) { + // FIXME: this is temporary, fix all the vars of all other vars in the clique + return clique_cut_build_status_t::NO_CUT; CLIQUE_CUTS_DEBUG("build_clique_cut infeasible var=%lld appears as variable and complement", static_cast(var_idx)); return clique_cut_build_status_t::INFEASIBLE; @@ -253,7 +257,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, if (!bitset_any(P) && !bitset_any(X)) { // if the weight is enough, add and exit if (weight_R >= ctx.min_weight) { - if (ctx.add_work(static_cast(R.size()))) { return; } + ctx.add_work(static_cast(R.size())); ctx.cliques.push_back(R); } return; @@ -269,11 +273,9 @@ void bron_kerbosch(bk_bitset_context_t& ctx, // pivoting rule according to the highest degree vertex // TODO try other pivoting strategies, we can also implement some online learning like MAB for (size_t w = 0; w < ctx.words; ++w) { - if (toc(ctx.start_time) >= ctx.time_limit) { return; } // union of P and X uint64_t word = P[w] | X[w]; while (word) { - if (toc(ctx.start_time) >= ctx.time_limit) { return; } pivot_vertices_examined++; // least significant set bit idnex const int bit = __builtin_ctzll(word); @@ -297,20 +299,15 @@ void bron_kerbosch(bk_bitset_context_t& ctx, } } // Coarse cost of pivot scan: for each visited vertex, scan one adjacency row over all words. - if (ctx.add_work(static_cast(pivot_vertices_examined) * - static_cast(2 * ctx.words + 4))) { - return; - } + ctx.add_work(static_cast(pivot_vertices_examined) * static_cast(2 * ctx.words + 4)); std::vector candidates; candidates.reserve(ctx.weights.size()); cuopt_assert(pivot >= 0, "Pivot must be valid when P or X is non-empty"); for (size_t w = 0; w < ctx.words; ++w) { - if (toc(ctx.start_time) >= ctx.time_limit) { return; } // P / N(pivot) uint64_t word = P[w] & ~ctx.adj[pivot][w]; while (word) { - if (toc(ctx.start_time) >= ctx.time_limit) { return; } const int bit = __builtin_ctzll(word); const i_t v = static_cast(w * 64 + static_cast(bit)); word &= (word - 1); @@ -319,12 +316,9 @@ void bron_kerbosch(bk_bitset_context_t& ctx, } const i_t num_candidates = static_cast(candidates.size()); // Coarse cost of candidate extraction from P \ N(pivot) - if (ctx.add_work(static_cast(ctx.words + 3 * num_candidates))) { return; } + ctx.add_work(static_cast(ctx.words + 3 * num_candidates)); // Coarse cost for all branch setups in this recursion frame. - if (ctx.add_work(static_cast(num_candidates) * static_cast(4 * ctx.words + 8))) { - return; - } - + ctx.add_work(static_cast(num_candidates) * static_cast(4 * ctx.words + 8)); // note that candidates will include pivot if it is in P for (auto v : candidates) { if (ctx.over_call_limit()) { @@ -332,6 +326,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, return; } if (toc(ctx.start_time) >= ctx.time_limit) { return; } + R.push_back(v); std::vector P_next(ctx.words, 0); std::vector X_next(ctx.words, 0); @@ -339,6 +334,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, P_next[k] = P[k] & ctx.adj[v][k]; X_next[k] = X[k] & ctx.adj[v][k]; } + bron_kerbosch(ctx, R, P_next, X_next, weight_R + ctx.weights[v]); if (ctx.over_work_limit()) { return; } if (ctx.over_call_limit()) { @@ -423,7 +419,8 @@ void extend_clique_vertices(std::vector& clique_vertices, // if it is disturbed too much, the cut might become non-binding auto reduced_cost = [&](i_t vertex_idx) -> f_t { i_t var_idx = vertex_idx % num_vars; - if (var_idx < 0 || var_idx >= static_cast(reduced_costs.size())) { return 0.0; } + cuopt_assert(var_idx >= 0 && var_idx < static_cast(reduced_costs.size()), + "Variable index out of range"); f_t rc = reduced_costs[var_idx]; if (!std::isfinite(rc)) { rc = 0.0; } return vertex_idx >= num_vars ? -rc : rc; @@ -434,10 +431,8 @@ void extend_clique_vertices(std::vector& clique_vertices, }); for (const auto candidate : candidates) { - if (toc(start_time) >= time_limit) { return; } bool add = true; for (const auto v : clique_vertices) { - if (toc(start_time) >= time_limit) { return; } if (!graph.check_adjacency(candidate, v)) { add = false; break; @@ -1143,7 +1138,7 @@ bool cut_generation_t::generate_clique_cuts( if (clique_table_ == nullptr) { CLIQUE_CUTS_DEBUG("generate_clique_cuts building clique table"); ::cuopt::linear_programming::detail::clique_config_t clique_config; - clique_config.min_clique_size = 1; + clique_config.min_clique_size = 2; clique_table_ = std::make_shared<::cuopt::linear_programming::detail::clique_table_t>( 2 * num_vars, clique_config.min_clique_size, clique_config.max_clique_size_for_extension); @@ -1184,7 +1179,7 @@ bool cut_generation_t::generate_clique_cuts( // TODO this can be problem dependent const i_t max_calls = 100000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 2e9; + const f_t max_work_estimate = 1e8; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); @@ -1196,7 +1191,6 @@ bool cut_generation_t::generate_clique_cuts( // create the sub graph induced by fractional binary variables for (i_t j = 0; j < num_vars; ++j) { - if (toc(start_time) >= settings.time_limit) { return true; } if (user_problem_.var_types[j] == variable_type_t::CONTINUOUS) { continue; } const f_t lower_bound = user_problem_.lower[j]; const f_t upper_bound = user_problem_.upper[j]; @@ -1236,10 +1230,11 @@ bool cut_generation_t::generate_clique_cuts( for (size_t idx = 0; idx < vertices.size(); ++idx) { if (toc(start_time) >= settings.time_limit) { return true; } i_t vertex_idx = vertices[idx]; - auto adj_set = clique_table_->get_adj_set_of_var(vertex_idx); + // returns the complement as well + auto adj_set = clique_table_->get_adj_set_of_var(vertex_idx); total_adj_entries += adj_set.size(); auto& adj = adj_local[idx]; - adj.reserve(adj_set.size() + 1); + adj.reserve(adj_set.size()); for (const auto neighbor : adj_set) { if (toc(start_time) >= settings.time_limit) { return true; } cuopt_assert(neighbor >= 0 && neighbor < 2 * num_vars, "Neighbor out of range"); @@ -1255,6 +1250,11 @@ bool cut_generation_t::generate_clique_cuts( adj_check.reserve(adj.size()); for (const auto neighbor : adj) { cuopt_assert(adj_check.insert(neighbor).second, "Duplicate neighbor in adjacency list"); + // Make sure no complementing variable is present in the adjacency list + // Given convention: if variable is j, its complement is j + num_vars (and vice versa) + i_t complement = (neighbor >= num_vars) ? (neighbor - num_vars) : (neighbor + num_vars); + cuopt_assert(adj_check.find(complement) == adj_check.end(), + "Adjacency list contains complementing variable"); } } #endif @@ -1270,13 +1270,9 @@ bool cut_generation_t::generate_clique_cuts( std::vector> adj_bitset(vertices.size(), std::vector(words, 0)); size_t local_adj_entries = 0; for (size_t v = 0; v < adj_local.size(); ++v) { - if (toc(start_time) >= settings.time_limit) { return true; } local_adj_entries += adj_local[v].size(); for (const auto neighbor : adj_local[v]) { - if (toc(start_time) >= settings.time_limit) { return true; } - if (neighbor >= 0 && static_cast(neighbor) < vertices.size()) { - bitset_set(adj_bitset[v], static_cast(neighbor)); - } + bitset_set(adj_bitset[v], static_cast(neighbor)); } } work_estimate += static_cast(adj_local.size()) + 3.0 * static_cast(local_adj_entries); From d1e202ecbba5a0c3c6d494545ae81e1e80baf5e2 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Mar 2026 08:02:23 -0800 Subject: [PATCH 115/147] with intiial cliques --- .../diversity/diversity_manager.cu | 42 +++++++++---------- .../presolve/conflict_graph/clique_table.cu | 1 + 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index adab6d671f..adf30343fb 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -268,27 +268,27 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && - // !problem_ptr->empty) { - // f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); - // timer_t clique_timer(time_limit_for_clique_table); - // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - // problem_ptr->get_host_user_problem(host_problem); - // std::shared_ptr> clique_table; - // auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - // find_initial_cliques( - // host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); - // problem_ptr->set_constraints_from_host_user_problem(host_problem); - // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - // "host lower bound size mismatch"); - // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - // "host upper bound size mismatch"); - // std::vector all_var_indices(problem_ptr->n_variables); - // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - // trivial_presolve(*problem_ptr, remap_cache_ids); - // if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - // } + if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && + !problem_ptr->empty) { + f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); + timer_t clique_timer(time_limit_for_clique_table); + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; + find_initial_cliques( + host_problem, context.settings.tolerances, clique_table_ptr, presolve_timer); + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + "host lower bound size mismatch"); + cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + "host upper bound size mismatch"); + std::vector all_var_indices(problem_ptr->n_variables); + std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + // trivial_presolve(*problem_ptr, remap_cache_ids); + if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 78fcb01cda..42d9e295fd 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -656,6 +656,7 @@ i_t extend_cliques(const std::vector>& knapsack_ i_t remaining_rows_budget, i_t remaining_nnz_budget, i_t& inserted_row_nnz) { + return; inserted_row_nnz = 0; if (curr_clique.empty() || sp_sigs.empty()) { return; } std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); From f9206de13350a4c8ebf4e9b16a7e9ee2a563561e Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 3 Mar 2026 08:04:56 -0800 Subject: [PATCH 116/147] only clique cuts --- .../cuopt/linear_programming/mip/solver_settings.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index a9e404d14e..6b6b48379a 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -90,11 +90,11 @@ class mip_solver_settings_t { i_t reliability_branching = -1; i_t num_cpu_threads = -1; // -1 means use default number of threads in branch and bound i_t max_cut_passes = 10; // number of cut passes to make - i_t mir_cuts = -1; - i_t mixed_integer_gomory_cuts = -1; - i_t knapsack_cuts = -1; + i_t mir_cuts = 0; + i_t mixed_integer_gomory_cuts = 0; + i_t knapsack_cuts = 0; i_t clique_cuts = -1; - i_t strong_chvatal_gomory_cuts = -1; + i_t strong_chvatal_gomory_cuts = 0; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; f_t cut_min_orthogonality = 0.5; From d6ee91b92ae37a3c2aca3b9b60485fc06d4f29ea Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 5 Mar 2026 22:08:53 -0800 Subject: [PATCH 117/147] test cliques with extended cliques --- .../mip_heuristics/presolve/conflict_graph/clique_table.cu | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 1ebdc82e34..3841f1d44d 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -1080,15 +1080,15 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, dual_simplex::user_problem_t & problem, \ typename mip_solver_settings_t::tolerances_t tolerances, \ std::shared_ptr> * clique_table_out, \ - cuopt::timer_t & timer); \ + cuopt::timer_t & timer, \ + bool modify_problem); \ template void build_clique_table( \ const dual_simplex::user_problem_t& problem, \ clique_table_t& clique_table, \ typename mip_solver_settings_t::tolerances_t tolerances, \ bool remove_small_cliques_flag, \ bool fill_var_clique_maps_flag, \ - cuopt::timer_t& timer, \ - bool modify_problem); \ + cuopt::timer_t& timer); \ template class clique_table_t; #if MIP_INSTANTIATE_FLOAT From c8301f57cae2ae36595def1d074b5791797493d0 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Thu, 5 Mar 2026 22:55:47 -0800 Subject: [PATCH 118/147] some refactor and reenable all cuts --- .../mip/solver_settings.hpp | 8 +- .../presolve/conflict_graph/clique_table.cu | 445 ++++++++++-------- 2 files changed, 243 insertions(+), 210 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 6b6b48379a..a9e404d14e 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -90,11 +90,11 @@ class mip_solver_settings_t { i_t reliability_branching = -1; i_t num_cpu_threads = -1; // -1 means use default number of threads in branch and bound i_t max_cut_passes = 10; // number of cut passes to make - i_t mir_cuts = 0; - i_t mixed_integer_gomory_cuts = 0; - i_t knapsack_cuts = 0; + i_t mir_cuts = -1; + i_t mixed_integer_gomory_cuts = -1; + i_t knapsack_cuts = -1; i_t clique_cuts = -1; - i_t strong_chvatal_gomory_cuts = 0; + i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; f_t cut_min_orthogonality = 0.5; diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 3841f1d44d..cf556aefee 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -552,6 +552,232 @@ bool extend_clique(const std::vector& clique, return new_clique.size() > clique.size(); } +template +struct clique_sig_t { + i_t knapsack_idx; + i_t size; + long long signature; +}; + +template +struct extension_candidate_t { + i_t knapsack_idx; + i_t estimated_gain; + i_t clique_size; +}; + +template +bool compare_clique_sig(const clique_sig_t& a, const clique_sig_t& b) +{ + if (a.signature != b.signature) { return a.signature < b.signature; } + return a.size < b.size; +} + +template +bool compare_signature_value(long long value, const clique_sig_t& a) +{ + return value < a.signature; +} + +template +bool compare_extension_candidate(const extension_candidate_t& a, + const extension_candidate_t& b) +{ + if (a.estimated_gain != b.estimated_gain) { return a.estimated_gain > b.estimated_gain; } + if (a.clique_size != b.clique_size) { return a.clique_size < b.clique_size; } + return a.knapsack_idx < b.knapsack_idx; +} + +template +bool is_sorted_subset(const std::vector& a, const std::vector& b) +{ + size_t i = 0; + size_t j = 0; + while (i < a.size() && j < b.size()) { + if (a[i] == b[j]) { + i++; + j++; + } else if (a[i] > b[j]) { + j++; + } else { + return false; + } + } + return i == a.size(); +} + +template +void fix_difference(const std::vector& superset, + const std::vector& subset, + dual_simplex::user_problem_t& problem) +{ + cuopt_assert(std::is_sorted(subset.begin(), subset.end()), + "subset vector passed to fix_difference is not sorted"); + for (auto var_idx : superset) { + if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } + if (var_idx >= problem.num_cols) { + i_t orig_idx = var_idx - problem.num_cols; + CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); + cuopt_assert(problem.lower[orig_idx] != 0 || problem.upper[orig_idx] != 0, + "Variable is fixed to other side"); + problem.lower[orig_idx] = 1; + problem.upper[orig_idx] = 1; + } else { + CUOPT_LOG_DEBUG("Fixing variable %d", var_idx); + cuopt_assert(problem.lower[var_idx] != 1 || problem.upper[var_idx] != 1, + "Variable is fixed to other side"); + problem.lower[var_idx] = 0; + problem.upper[var_idx] = 0; + } + } +} + +template +void remove_marked_elements(std::vector& vec, const std::vector& removal_marker) +{ + size_t write_idx = 0; + for (size_t i = 0; i < vec.size(); i++) { + if (!removal_marker[i]) { + if (write_idx != i) { vec[write_idx] = std::move(vec[i]); } + write_idx++; + } + } + vec.resize(write_idx); +} + +template +void remove_dominated_cliques_in_problem_for_single_extended_clique( + const std::vector& curr_clique, + f_t coeff_scale, + i_t remaining_rows_budget, + i_t remaining_nnz_budget, + i_t& inserted_row_nnz, + const std::vector>& sp_sigs, + const std::vector>& cstr_vars, + const std::vector>& knapsack_constraints, + std::vector& original_to_current_row_idx, + dual_simplex::user_problem_t& problem, + dual_simplex::csr_matrix_t& A, + cuopt::timer_t& timer) +{ + inserted_row_nnz = 0; + if (curr_clique.empty() || sp_sigs.empty()) { return; } + std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); + std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); + curr_clique_vars.erase(std::unique(curr_clique_vars.begin(), curr_clique_vars.end()), + curr_clique_vars.end()); + long long signature = 0; + for (auto v : curr_clique_vars) { + signature += static_cast(v); + } + constexpr size_t dominance_window = 20000; + auto end_it = + std::upper_bound(sp_sigs.begin(), sp_sigs.end(), signature, compare_signature_value); + size_t end = static_cast(std::distance(sp_sigs.begin(), end_it)); + size_t start = (end > dominance_window) ? (end - dominance_window) : 0; + std::vector rows_to_remove; + bool covering_clique_implied_by_partitioning = false; + for (size_t idx = end; idx > start; idx--) { + if (timer.check_time_limit()) { break; } + const auto& sp = sp_sigs[idx - 1]; + const auto& vars_sp = cstr_vars[sp.knapsack_idx]; + if (vars_sp.size() > curr_clique_vars.size()) { continue; } + cuopt_assert(std::is_sorted(vars_sp.begin(), vars_sp.end()), + "vars_sp vector passed to is_sorted_subset is not sorted"); + if (!is_sorted_subset(vars_sp, curr_clique_vars)) { continue; } + if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { + if (vars_sp.size() != curr_clique_vars.size()) { + fix_difference(curr_clique_vars, vars_sp, problem); + covering_clique_implied_by_partitioning = true; + } + continue; + } + i_t original_row_idx = knapsack_constraints[sp.knapsack_idx].cstr_idx; + if (original_row_idx < 0) { continue; } + cuopt_assert(original_row_idx < static_cast(original_to_current_row_idx.size()), + "Invalid original row index in knapsack constraint"); + i_t current_row_idx = original_to_current_row_idx[original_row_idx]; + if (current_row_idx < 0) { continue; } + cuopt_assert(current_row_idx < static_cast(problem.row_sense.size()), + "Invalid current row index in row mapping"); + rows_to_remove.push_back(current_row_idx); + } + if (rows_to_remove.empty()) { return; } + std::sort(rows_to_remove.begin(), rows_to_remove.end()); + rows_to_remove.erase(std::unique(rows_to_remove.begin(), rows_to_remove.end()), + rows_to_remove.end()); + if (!covering_clique_implied_by_partitioning) { + if (remaining_rows_budget <= 0 || + remaining_nnz_budget < static_cast(curr_clique_vars.size())) { + return; + } + insert_clique_into_problem(curr_clique_vars, problem, A, coeff_scale); + inserted_row_nnz = static_cast(curr_clique_vars.size()); + } + std::vector removal_marker(problem.row_sense.size(), 0); + for (auto row_idx : rows_to_remove) { + cuopt_assert(row_idx >= 0 && row_idx < static_cast(removal_marker.size()), + "Invalid dominated row index"); + CUOPT_LOG_DEBUG("Removing dominated row %d", row_idx); + removal_marker[row_idx] = true; + } + dual_simplex::csr_matrix_t A_removed(0, 0, 0); + A.remove_rows(removal_marker, A_removed); + A = std::move(A_removed); + problem.num_rows = A.m; + remove_marked_elements(problem.row_sense, removal_marker); + remove_marked_elements(problem.rhs, removal_marker); + remove_marked_elements(problem.row_names, removal_marker); + cuopt_assert(problem.rhs.size() == problem.row_sense.size(), "rhs and row sense size mismatch"); + cuopt_assert(problem.row_names.size() == problem.rhs.size(), "row names and rhs size mismatch"); + cuopt_assert(problem.num_rows == static_cast(problem.rhs.size()), + "matrix and num rows mismatch after removal"); + if (!problem.range_rows.empty()) { + std::vector old_to_new_indices; + old_to_new_indices.reserve(removal_marker.size()); + i_t new_idx = 0; + for (size_t i = 0; i < removal_marker.size(); ++i) { + if (!removal_marker[i]) { + old_to_new_indices.push_back(new_idx++); + } else { + old_to_new_indices.push_back(-1); + } + } + std::vector new_range_rows; + std::vector new_range_values; + for (size_t i = 0; i < problem.range_rows.size(); ++i) { + i_t old_row = problem.range_rows[i]; + cuopt_assert(old_row >= 0 && old_row < static_cast(removal_marker.size()), + "Invalid row index in range_rows"); + if (!removal_marker[old_row]) { + i_t new_row = old_to_new_indices[old_row]; + cuopt_assert(new_row != -1, "Invalid new row index for ranged row renumbering"); + new_range_rows.push_back(new_row); + new_range_values.push_back(problem.range_value[i]); + } + } + problem.range_rows = std::move(new_range_rows); + problem.range_value = std::move(new_range_values); + } + problem.num_range_rows = static_cast(problem.range_rows.size()); + std::vector removed_prefix(removal_marker.size() + 1, 0); + for (size_t row_idx = 0; row_idx < removal_marker.size(); row_idx++) { + removed_prefix[row_idx + 1] = + removed_prefix[row_idx] + static_cast(removal_marker[row_idx]); + } + for (i_t row_idx = 0; row_idx < static_cast(original_to_current_row_idx.size()); row_idx++) { + i_t current_row_idx = original_to_current_row_idx[row_idx]; + if (current_row_idx < 0) { continue; } + cuopt_assert(current_row_idx < static_cast(removal_marker.size()), + "Row index map is out of bounds"); + if (removal_marker[current_row_idx]) { + original_to_current_row_idx[row_idx] = -1; + } else { + original_to_current_row_idx[row_idx] = current_row_idx - removed_prefix[current_row_idx]; + } + } +} + // Also known as clique merging. Infer larger clique constraints which allows inclusion of vars from // other constraints. This only extends the original cliques in the formulation for now. // TODO: consider a heuristic on how much of the cliques derived from knapsacks to include here @@ -583,12 +809,7 @@ i_t extend_cliques(const std::vector>& knapsack_ max_added_rows, max_added_nnz); std::vector> cstr_vars(knapsack_constraints.size()); - struct clique_sig_t { - i_t knapsack_idx; - i_t size; - long long signature; - }; - std::vector sp_sigs; + std::vector> sp_sigs; sp_sigs.reserve(set_packing_constraints.size()); for (const auto knapsack_idx : set_packing_constraints) { cuopt_assert(knapsack_idx >= 0 && knapsack_idx < static_cast(knapsack_constraints.size()), @@ -608,200 +829,12 @@ i_t extend_cliques(const std::vector>& knapsack_ } sp_sigs.push_back({knapsack_idx, static_cast(cstr_vars[knapsack_idx].size()), signature}); } - std::sort(sp_sigs.begin(), sp_sigs.end(), [](const auto& a, const auto& b) { - if (a.signature != b.signature) { return a.signature < b.signature; } - return a.size < b.size; - }); + std::sort(sp_sigs.begin(), sp_sigs.end(), compare_clique_sig); std::vector original_to_current_row_idx(problem.row_sense.size(), -1); for (i_t row_idx = 0; row_idx < static_cast(original_to_current_row_idx.size()); row_idx++) { original_to_current_row_idx[row_idx] = row_idx; } - auto is_subset = [](const std::vector& a, const std::vector& b) { - size_t i = 0; - size_t j = 0; - while (i < a.size() && j < b.size()) { - if (a[i] == b[j]) { - i++; - j++; - } else if (a[i] > b[j]) { - j++; - } else { - return false; - } - } - return i == a.size(); - }; - auto fix_difference = [&](const std::vector& superset, const std::vector& subset) { - cuopt_assert(std::is_sorted(subset.begin(), subset.end()), - "subset vector passed to fix_difference is not sorted"); - for (auto var_idx : superset) { - if (std::binary_search(subset.begin(), subset.end(), var_idx)) { continue; } - if (var_idx >= problem.num_cols) { - i_t orig_idx = var_idx - problem.num_cols; - CUOPT_LOG_DEBUG("Fixing variable %d", orig_idx); - cuopt_assert(problem.lower[orig_idx] != 0 || problem.upper[orig_idx] != 0, - "Variable is fixed to other side"); - problem.lower[orig_idx] = 1; - problem.upper[orig_idx] = 1; - } else { - CUOPT_LOG_DEBUG("Fixing variable %d", var_idx); - cuopt_assert(problem.lower[var_idx] != 1 || problem.upper[var_idx] != 1, - "Variable is fixed to other side"); - problem.lower[var_idx] = 0; - problem.upper[var_idx] = 0; - } - } - }; - auto remove_dominated_cliques_in_problem_for_single_extended_clique = - [&](const std::vector& curr_clique, - f_t coeff_scale, - i_t remaining_rows_budget, - i_t remaining_nnz_budget, - i_t& inserted_row_nnz) { - inserted_row_nnz = 0; - if (curr_clique.empty() || sp_sigs.empty()) { return; } - std::vector curr_clique_vars(curr_clique.begin(), curr_clique.end()); - std::sort(curr_clique_vars.begin(), curr_clique_vars.end()); - curr_clique_vars.erase(std::unique(curr_clique_vars.begin(), curr_clique_vars.end()), - curr_clique_vars.end()); - long long signature = 0; - for (auto v : curr_clique_vars) { - signature += static_cast(v); - } - constexpr size_t dominance_window = 20000; - auto end_it = std::upper_bound( - sp_sigs.begin(), sp_sigs.end(), signature, [](long long value, const auto& a) { - return value < a.signature; - }); - size_t end = static_cast(std::distance(sp_sigs.begin(), end_it)); - size_t start = (end > dominance_window) ? (end - dominance_window) : 0; - std::vector rows_to_remove; - bool covering_clique_implied_by_partitioning = false; - for (size_t idx = end; idx > start; idx--) { - if (timer.check_time_limit()) { break; } - const auto& sp = sp_sigs[idx - 1]; - const auto& vars_sp = cstr_vars[sp.knapsack_idx]; - if (vars_sp.size() > curr_clique_vars.size()) { continue; } - cuopt_assert(std::is_sorted(vars_sp.begin(), vars_sp.end()), - "vars_sp vector passed to is_subset is not sorted"); - if (!is_subset(vars_sp, curr_clique_vars)) { continue; } - if (knapsack_constraints[sp.knapsack_idx].is_set_partitioning) { - if (vars_sp.size() != curr_clique_vars.size()) { - fix_difference(curr_clique_vars, vars_sp); - covering_clique_implied_by_partitioning = true; - } - continue; - } - i_t original_row_idx = knapsack_constraints[sp.knapsack_idx].cstr_idx; - if (original_row_idx < 0) { continue; } - cuopt_assert(original_row_idx < static_cast(original_to_current_row_idx.size()), - "Invalid original row index in knapsack constraint"); - i_t current_row_idx = original_to_current_row_idx[original_row_idx]; - if (current_row_idx < 0) { continue; } - cuopt_assert(current_row_idx < static_cast(problem.row_sense.size()), - "Invalid current row index in row mapping"); - rows_to_remove.push_back(current_row_idx); - } - if (rows_to_remove.empty()) { return; } - std::sort(rows_to_remove.begin(), rows_to_remove.end()); - rows_to_remove.erase(std::unique(rows_to_remove.begin(), rows_to_remove.end()), - rows_to_remove.end()); - if (!covering_clique_implied_by_partitioning) { - if (remaining_rows_budget <= 0 || - remaining_nnz_budget < static_cast(curr_clique_vars.size())) { - return; - } - // Replace dominated rows with this stronger clique row. - insert_clique_into_problem(curr_clique_vars, problem, A, coeff_scale); - inserted_row_nnz = static_cast(curr_clique_vars.size()); - } - std::vector removal_marker(problem.row_sense.size(), 0); - for (auto row_idx : rows_to_remove) { - cuopt_assert(row_idx >= 0 && row_idx < static_cast(removal_marker.size()), - "Invalid dominated row index"); - CUOPT_LOG_DEBUG("Removing dominated row %d", row_idx); - removal_marker[row_idx] = true; - } - dual_simplex::csr_matrix_t A_removed(0, 0, 0); - A.remove_rows(removal_marker, A_removed); - A = std::move(A_removed); - problem.num_rows = A.m; - i_t n = 0; - auto new_end = std::remove_if( - problem.row_sense.begin(), problem.row_sense.end(), [&removal_marker, &n](char) mutable { - return removal_marker[n++]; - }); - problem.row_sense.erase(new_end, problem.row_sense.end()); - n = 0; - auto new_end_rhs = - std::remove_if(problem.rhs.begin(), problem.rhs.end(), [&removal_marker, &n](f_t) mutable { - return removal_marker[n++]; - }); - problem.rhs.erase(new_end_rhs, problem.rhs.end()); - n = 0; - auto new_end_row_names = std::remove_if( - problem.row_names.begin(), - problem.row_names.end(), - [&removal_marker, &n](const std::string&) mutable { return removal_marker[n++]; }); - problem.row_names.erase(new_end_row_names, problem.row_names.end()); - cuopt_assert(problem.rhs.size() == problem.row_sense.size(), - "rhs and row sense size mismatch"); - cuopt_assert(problem.row_names.size() == problem.rhs.size(), - "row names and rhs size mismatch"); - cuopt_assert(problem.num_rows == static_cast(problem.rhs.size()), - "matrix and num rows mismatch after removal"); - if (!problem.range_rows.empty()) { - std::vector old_to_new_indices; - old_to_new_indices.reserve(removal_marker.size()); - i_t new_idx = 0; - for (size_t i = 0; i < removal_marker.size(); ++i) { - if (!removal_marker[i]) { - old_to_new_indices.push_back(new_idx++); - } else { - old_to_new_indices.push_back(-1); - } - } - std::vector new_range_rows; - std::vector new_range_values; - for (size_t i = 0; i < problem.range_rows.size(); ++i) { - i_t old_row = problem.range_rows[i]; - cuopt_assert(old_row >= 0 && old_row < static_cast(removal_marker.size()), - "Invalid row index in range_rows"); - if (!removal_marker[old_row]) { - i_t new_row = old_to_new_indices[old_row]; - cuopt_assert(new_row != -1, "Invalid new row index for ranged row renumbering"); - new_range_rows.push_back(new_row); - new_range_values.push_back(problem.range_value[i]); - } - } - problem.range_rows = std::move(new_range_rows); - problem.range_value = std::move(new_range_values); - } - problem.num_range_rows = static_cast(problem.range_rows.size()); - std::vector removed_prefix(removal_marker.size() + 1, 0); - for (size_t row_idx = 0; row_idx < removal_marker.size(); row_idx++) { - removed_prefix[row_idx + 1] = - removed_prefix[row_idx] + static_cast(removal_marker[row_idx]); - } - for (i_t row_idx = 0; row_idx < static_cast(original_to_current_row_idx.size()); - row_idx++) { - i_t current_row_idx = original_to_current_row_idx[row_idx]; - if (current_row_idx < 0) { continue; } - cuopt_assert(current_row_idx < static_cast(removal_marker.size()), - "Row index map is out of bounds"); - if (removal_marker[current_row_idx]) { - original_to_current_row_idx[row_idx] = -1; - } else { - original_to_current_row_idx[row_idx] = current_row_idx - removed_prefix[current_row_idx]; - } - } - }; - struct extension_candidate_t { - i_t knapsack_idx; - i_t estimated_gain; - i_t clique_size; - }; - std::vector extension_worklist; + std::vector> extension_worklist; extension_worklist.reserve(knapsack_constraints.size()); for (i_t knapsack_idx = 0; knapsack_idx < static_cast(knapsack_constraints.size()); knapsack_idx++) { @@ -819,15 +852,8 @@ i_t extend_cliques(const std::vector>& knapsack_ if (estimated_gain < min_extension_gain) { continue; } extension_worklist.push_back({knapsack_idx, estimated_gain, clique_size}); } - std::stable_sort(extension_worklist.begin(), - extension_worklist.end(), - [](const extension_candidate_t& a, const extension_candidate_t& b) { - if (a.estimated_gain != b.estimated_gain) { - return a.estimated_gain > b.estimated_gain; - } - if (a.clique_size != b.clique_size) { return a.clique_size < b.clique_size; } - return a.knapsack_idx < b.knapsack_idx; - }); + std::stable_sort( + extension_worklist.begin(), extension_worklist.end(), compare_extension_candidate); CUOPT_LOG_DEBUG("Clique extension candidates after scoring: %zu", extension_worklist.size()); i_t n_extended_cliques = 0; @@ -865,7 +891,14 @@ i_t extend_cliques(const std::vector>& knapsack_ coeff_scale, max_added_rows - added_rows, max_added_nnz - added_nnz, - replacement_row_nnz); + replacement_row_nnz, + sp_sigs, + cstr_vars, + knapsack_constraints, + original_to_current_row_idx, + problem, + A, + timer); } if (replacement_row_nnz > 0) { window_successes++; From a4a2a5c19edd4d77d3c0da64e6213a5dbcd56014 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 00:18:38 -0800 Subject: [PATCH 119/147] increase the work estimate --- cpp/src/cuts/cuts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index ecea10b7d7..17b2ada7a5 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1179,7 +1179,7 @@ bool cut_generation_t::generate_clique_cuts( // TODO this can be problem dependent const i_t max_calls = 100000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 1e8; + const f_t max_work_estimate = 2e9; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); From f9b5898974ec1ad9b03e8b88d9bd803083c30fe2 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 00:21:46 -0800 Subject: [PATCH 120/147] longer time for init cliques, more work estimate, only clique cuts --- .../cuopt/linear_programming/mip/solver_settings.hpp | 8 ++++---- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index a9e404d14e..6b6b48379a 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -90,11 +90,11 @@ class mip_solver_settings_t { i_t reliability_branching = -1; i_t num_cpu_threads = -1; // -1 means use default number of threads in branch and bound i_t max_cut_passes = 10; // number of cut passes to make - i_t mir_cuts = -1; - i_t mixed_integer_gomory_cuts = -1; - i_t knapsack_cuts = -1; + i_t mir_cuts = 0; + i_t mixed_integer_gomory_cuts = 0; + i_t knapsack_cuts = 0; i_t clique_cuts = -1; - i_t strong_chvatal_gomory_cuts = -1; + i_t strong_chvatal_gomory_cuts = 0; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; f_t cut_min_orthogonality = 0.5; diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 10ab7a9200..38ffacd88e 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -256,7 +256,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (run_probing_cache) { // Run probing cache before trivial presolve to discover variable implications const f_t max_time_on_probing = diversity_config.max_time_on_probing; - f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit); + f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit * 0.8); timer_t probing_timer{time_for_probing_cache}; // this function computes probing cache, finds singletons, substitutions and changes the problem bool problem_is_infeasible = @@ -268,7 +268,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && !problem_ptr->empty) { - f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); + f_t time_limit_for_clique_table = std::min(10., time_limit * 0.2); timer_t clique_timer(time_limit_for_clique_table); dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); From 787b824a9940dce9edb847ce2de5365e0b6a4c1e Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 00:22:40 -0800 Subject: [PATCH 121/147] old timer --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 38ffacd88e..10ab7a9200 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -256,7 +256,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (run_probing_cache) { // Run probing cache before trivial presolve to discover variable implications const f_t max_time_on_probing = diversity_config.max_time_on_probing; - f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit * 0.8); + f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit); timer_t probing_timer{time_for_probing_cache}; // this function computes probing cache, finds singletons, substitutions and changes the problem bool problem_is_infeasible = @@ -268,7 +268,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && !problem_ptr->empty) { - f_t time_limit_for_clique_table = std::min(10., time_limit * 0.2); + f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); timer_t clique_timer(time_limit_for_clique_table); dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); From 6f2115516dba863ef379bdc27d4496cd13b1b958 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 05:50:31 -0800 Subject: [PATCH 122/147] deterministic checks --- .../cuopt/linear_programming/mip/solver_settings.hpp | 8 ++++---- cpp/src/cuts/cuts.cpp | 2 +- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 6 ++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 6b6b48379a..a9e404d14e 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -90,11 +90,11 @@ class mip_solver_settings_t { i_t reliability_branching = -1; i_t num_cpu_threads = -1; // -1 means use default number of threads in branch and bound i_t max_cut_passes = 10; // number of cut passes to make - i_t mir_cuts = 0; - i_t mixed_integer_gomory_cuts = 0; - i_t knapsack_cuts = 0; + i_t mir_cuts = -1; + i_t mixed_integer_gomory_cuts = -1; + i_t knapsack_cuts = -1; i_t clique_cuts = -1; - i_t strong_chvatal_gomory_cuts = 0; + i_t strong_chvatal_gomory_cuts = -1; i_t reduced_cost_strengthening = -1; f_t cut_change_threshold = 1e-3; f_t cut_min_orthogonality = 0.5; diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 17b2ada7a5..474ca4045f 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -415,7 +415,7 @@ void extend_clique_vertices(std::vector& clique_vertices, // sort the candidates by reduced cost. // smaller reduce cost disturbs dual simplex less // less refactors and less iterations after resolve. - // it also increases the cut's effectiveness by keeping xstart not disturbed much + // it also increases the cut's effectiveness by keeping xstar not disturbed much // if it is disturbed too much, the cut might become non-binding auto reduced_cost = [&](i_t vertex_idx) -> f_t { i_t var_idx = vertex_idx % num_vars; diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 10ab7a9200..5ed3fff93b 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -268,7 +268,13 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && !problem_ptr->empty) { + // TODO this is just CPU time and blocking the GPU time. + // execute this in parallel with something else. f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); + // if it is deterministic run until the end + if (context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC) { + time_limit_for_clique_table = std::numeric_limits::infinity(); + } timer_t clique_timer(time_limit_for_clique_table); dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); From 38132a5fe1df76ec00eccedd75422a7e794bcfc5 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 06:24:14 -0800 Subject: [PATCH 123/147] handle ai reviews --- cpp/src/cuts/cuts.cpp | 13 ++-- .../diversity/diversity_manager.cu | 65 ++----------------- .../diversity/diversity_manager.cuh | 19 ------ .../presolve/conflict_graph/clique_table.cuh | 11 ++-- .../presolve/third_party_presolve.cpp | 1 + cpp/src/mip_heuristics/solver.cu | 1 - 6 files changed, 16 insertions(+), 94 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 474ca4045f..360bef4319 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1246,14 +1246,13 @@ bool cut_generation_t::generate_clique_cuts( kept_adj_entries += adj.size(); #ifdef ASSERT_MODE { - std::unordered_set adj_check; - adj_check.reserve(adj.size()); + std::unordered_set adj_global; + adj_global.reserve(adj.size()); for (const auto neighbor : adj) { - cuopt_assert(adj_check.insert(neighbor).second, "Duplicate neighbor in adjacency list"); - // Make sure no complementing variable is present in the adjacency list - // Given convention: if variable is j, its complement is j + num_vars (and vice versa) - i_t complement = (neighbor >= num_vars) ? (neighbor - num_vars) : (neighbor + num_vars); - cuopt_assert(adj_check.find(complement) == adj_check.end(), + i_t v = vertices[neighbor]; + cuopt_assert(adj_global.insert(v).second, "Duplicate neighbor in adjacency list"); + i_t complement = (v >= num_vars) ? (v - num_vars) : (v + num_vars); + cuopt_assert(adj_global.find(complement) == adj_global.end(), "Adjacency list contains complementing variable"); } } diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 5ed3fff93b..45d9bddb1c 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -8,7 +8,6 @@ #include "cuda_profiler_api.h" #include "diversity_manager.cuh" -#include #include #include #include @@ -26,17 +25,6 @@ constexpr bool fj_only_run = false; namespace cuopt::linear_programming::detail { -const char* env_cut_configuration_name(env_cut_configuration_t config) -{ - switch (config) { - case env_cut_configuration_t::WITHOUT_CLIQUE: return "cuts_without_clique"; - case env_cut_configuration_t::WITH_CLIQUE: return "cuts_with_clique"; - case env_cut_configuration_t::CLIQUE_ONLY: return "clique_only"; - case env_cut_configuration_t::NONE: return "default"; - default: return "unknown"; - } -} - size_t fp_recombiner_config_t::max_n_of_vars_from_other = fp_recombiner_config_t::initial_n_of_vars_from_other; size_t ls_recombiner_config_t::max_n_of_vars_from_other = @@ -92,6 +80,7 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_t::diversity_manager_t(mip_solver_context_t -void diversity_manager_t::apply_cut_configuration_from_env( - dual_simplex::simplex_solver_settings_t& settings) const -{ - if (env_cut_configuration == env_cut_configuration_t::NONE) { return; } - switch (env_cut_configuration) { - case env_cut_configuration_t::WITHOUT_CLIQUE: settings.clique_cuts = 0; break; - case env_cut_configuration_t::WITH_CLIQUE: settings.clique_cuts = 1; break; - case env_cut_configuration_t::CLIQUE_ONLY: - settings.clique_cuts = 1; - settings.mixed_integer_gomory_cuts = 0; - settings.mir_cuts = 0; - settings.knapsack_cuts = 0; - settings.strong_chvatal_gomory_cuts = 0; - break; - default: break; - } - settings.log.printf( - "Applied cut configuration from CUOPT_CONFIG_ID=%d (%s): mir=%d gomory=%d knapsack=%d " - "strong_cg=%d clique=%d\n", - env_config_id, - env_cut_configuration_name(env_cut_configuration), - settings.mir_cuts, - settings.mixed_integer_gomory_cuts, - settings.knapsack_cuts, - settings.strong_chvatal_gomory_cuts, - settings.clique_cuts); } // this function is to specialize the local search with config from diversity manager @@ -256,7 +199,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (run_probing_cache) { // Run probing cache before trivial presolve to discover variable implications const f_t max_time_on_probing = diversity_config.max_time_on_probing; - f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit); + f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit * 0.9); timer_t probing_timer{time_for_probing_cache}; // this function computes probing cache, finds singletons, substitutions and changes the problem bool problem_is_infeasible = @@ -270,7 +213,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ !problem_ptr->empty) { // TODO this is just CPU time and blocking the GPU time. // execute this in parallel with something else. - f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); + f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time()); // if it is deterministic run until the end if (context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC) { time_limit_for_clique_table = std::numeric_limits::infinity(); @@ -284,7 +227,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ find_initial_cliques(host_problem, context.settings.tolerances, clique_table_ptr, - presolve_timer, + clique_timer, modify_problem_with_cliques); if (modify_problem_with_cliques) { problem_ptr->set_constraints_from_host_user_problem(host_problem); diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cuh b/cpp/src/mip_heuristics/diversity/diversity_manager.cuh index 06b7f06e0c..4f86192db8 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cuh +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cuh @@ -29,22 +29,8 @@ #include -namespace cuopt::linear_programming::dual_simplex { -template -struct simplex_solver_settings_t; -} - namespace cuopt::linear_programming::detail { -enum class env_cut_configuration_t : int8_t { - NONE = -1, - WITHOUT_CLIQUE = 0, - WITH_CLIQUE = 1, - CLIQUE_ONLY = 2, -}; - -const char* env_cut_configuration_name(env_cut_configuration_t config); - template class diversity_manager_t { public: @@ -85,9 +71,6 @@ class diversity_manager_t { void set_simplex_solution(const std::vector& solution, const std::vector& dual_solution, f_t objective); - void apply_cut_configuration_from_env( - dual_simplex::simplex_solver_settings_t& settings) const; - mip_solver_context_t& context; dual_simplex::branch_and_bound_t* branch_and_bound_ptr; problem_t* problem_ptr; @@ -121,8 +104,6 @@ class diversity_manager_t { bool run_only_bp_recombiner{false}; bool run_only_fp_recombiner{false}; bool run_only_sub_mip_recombiner{false}; - env_cut_configuration_t env_cut_configuration{env_cut_configuration_t::NONE}; - int env_config_id{-1}; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh index 5c5f606260..a193242b7b 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh @@ -97,12 +97,11 @@ struct clique_table_t { }; template -void find_initial_cliques( - dual_simplex::user_problem_t& problem, - typename mip_solver_settings_t::tolerances_t tolerances, - std::shared_ptr>* clique_table_out = nullptr, - cuopt::timer_t& timer = cuopt::timer_t(std::numeric_limits::infinity()), - bool modify_problem = false); +void find_initial_cliques(dual_simplex::user_problem_t& problem, + typename mip_solver_settings_t::tolerances_t tolerances, + std::shared_ptr>* clique_table_out, + cuopt::timer_t& timer, + bool modify_problem); template void build_clique_table(const dual_simplex::user_problem_t& problem, diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index 7c38acdcbb..ef58fddcd7 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -588,6 +588,7 @@ std::optional> third_party_presolve_t( pslp_presolver_, op_problem.get_handle_ptr(), maximize_, original_obj_offset); + opt_problem.set_problem_name(op_problem.get_problem_name()); return std::make_optional(third_party_presolve_result_t{opt_problem, {}}); } diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index dbe3ce58e1..da6c7c0b81 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -223,7 +223,6 @@ solution_t mip_solver_t::run_solver() branch_and_bound_settings.clique_cuts = context.settings.clique_cuts; branch_and_bound_settings.strong_chvatal_gomory_cuts = context.settings.strong_chvatal_gomory_cuts; - dm.apply_cut_configuration_from_env(branch_and_bound_settings); branch_and_bound_settings.reduced_cost_strengthening = context.settings.reduced_cost_strengthening; branch_and_bound_settings.cut_change_threshold = context.settings.cut_change_threshold; From e9247069564c7413a184c49e983f5adb409a576b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 10:40:37 -0800 Subject: [PATCH 124/147] download neos via scriot --- datasets/mip/download_miplib_test_dataset.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/datasets/mip/download_miplib_test_dataset.sh b/datasets/mip/download_miplib_test_dataset.sh index dc2dd79662..3040f0f543 100755 --- a/datasets/mip/download_miplib_test_dataset.sh +++ b/datasets/mip/download_miplib_test_dataset.sh @@ -20,6 +20,7 @@ INSTANCES=( "thor50dday" "stein9inf" "neos5" + "neos8" "swath1" "enlight_hard" "enlight11" From 2f7e170fb76362c3d13eab3e44390980174259ca Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 11:07:00 -0800 Subject: [PATCH 125/147] better work units and clean up --- cpp/src/cuts/cuts.cpp | 52 ++++++++++++++----------------------------- cpp/src/cuts/cuts.hpp | 22 ------------------ 2 files changed, 17 insertions(+), 57 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 360bef4319..734442007f 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -21,17 +21,6 @@ namespace cuopt::linear_programming::dual_simplex { -const char* cut_configuration_name(cut_configuration_t cut_configuration) -{ - switch (cut_configuration) { - case cut_configuration_t::WITHOUT_CLIQUE: return "cuts_without_clique"; - case cut_configuration_t::WITH_CLIQUE: return "cuts_with_clique"; - case cut_configuration_t::CLIQUE_ONLY: return "clique_only"; - case cut_configuration_t::NONE: return "no_cuts_enabled"; - default: return "unknown"; - } -} - namespace { #define DEBUG_CLIQUE_CUTS 0 @@ -70,11 +59,9 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic const f_t clique_size = static_cast(clique_vertices.size()); CLIQUE_CUTS_DEBUG("build_clique_cut start clique_size=%lld", static_cast(clique_vertices.size())); - // Coarse function-level estimate: - // validate/transform vertices + duplicate checks + cut construction + sort + violation check - if (add_work_estimate(16.0 * clique_size + 4.0 * clique_size * std::log2(clique_size + 1.0), - work_estimate, - max_work_estimate)) { + const f_t sort_work = clique_size > 0.0 ? 2.0 * clique_size * std::log2(clique_size + 1.0) : 0.0; + const f_t estimated_work = 11.0 * clique_size + sort_work; + if (add_work_estimate(estimated_work, work_estimate, max_work_estimate)) { CLIQUE_CUTS_DEBUG("build_clique_cut skip work_limit clique_size=%lld work=%g limit=%g", static_cast(clique_vertices.size()), work_estimate == nullptr ? -1.0 : static_cast(*work_estimate), @@ -250,8 +237,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, ctx.call_limit_reached = true; return; } - // Coarse recursive cost: touch call state + frontiers + weight bookkeeping - if (ctx.add_work(static_cast(6 * ctx.words + R.size() + 4))) { return; } + if (ctx.add_work(static_cast(2 * ctx.words + R.size()))) { return; } // if P and X are empty, we are at maximal clique if (!bitset_any(P) && !bitset_any(X)) { @@ -298,8 +284,8 @@ void bron_kerbosch(bk_bitset_context_t& ctx, } } } - // Coarse cost of pivot scan: for each visited vertex, scan one adjacency row over all words. - ctx.add_work(static_cast(pivot_vertices_examined) * static_cast(2 * ctx.words + 4)); + ctx.add_work(static_cast(2 * ctx.words) + + static_cast(pivot_vertices_examined) * static_cast(2 * ctx.words)); std::vector candidates; candidates.reserve(ctx.weights.size()); @@ -315,10 +301,8 @@ void bron_kerbosch(bk_bitset_context_t& ctx, } } const i_t num_candidates = static_cast(candidates.size()); - // Coarse cost of candidate extraction from P \ N(pivot) - ctx.add_work(static_cast(ctx.words + 3 * num_candidates)); - // Coarse cost for all branch setups in this recursion frame. - ctx.add_work(static_cast(num_candidates) * static_cast(4 * ctx.words + 8)); + ctx.add_work(static_cast(2 * ctx.words + num_candidates)); + ctx.add_work(static_cast(num_candidates) * static_cast(7 * ctx.words + 6)); // note that candidates will include pivot if it is in P for (auto v : candidates) { if (ctx.over_call_limit()) { @@ -399,12 +383,10 @@ void extend_clique_vertices(std::vector& clique_vertices, static_cast(candidates.size())); const f_t candidate_size = static_cast(candidates.size()); const f_t sort_work = - candidate_size > 0.0 ? 6.0 * candidate_size * std::log2(candidate_size + 1.0) : 0.0; - // Coarse function-level estimate: - // degree scan + candidate filtering + sort + extension checks - const f_t estimated_extension_work = - 2.0 * initial_clique_size + 4.0 * static_cast(adj_set.size()) + sort_work + - 2.0 * candidate_size * initial_clique_size + 2.0 * candidate_size; + candidate_size > 0.0 ? 2.0 * candidate_size * std::log2(candidate_size + 1.0) : 0.0; + const f_t estimated_extension_work = 2.0 * initial_clique_size + + 3.0 * static_cast(adj_set.size()) + sort_work + + candidate_size * initial_clique_size + 2.0 * candidate_size; if (add_work_estimate(estimated_extension_work, work_estimate, max_work_estimate)) { CLIQUE_CUTS_DEBUG("extend_clique_vertices skip work_limit work=%g limit=%g", work_estimate == nullptr ? -1.0 : static_cast(*work_estimate), @@ -443,16 +425,15 @@ void extend_clique_vertices(std::vector& clique_vertices, clique_members.insert(candidate); } } -#if DEBUG_CLIQUE_CUTS CLIQUE_CUTS_DEBUG("extend_clique_vertices done start=%lld final=%lld added=%lld", static_cast(initial_clique_vertices), static_cast(clique_vertices.size()), static_cast(clique_vertices.size() - initial_clique_vertices)); -#endif } } // namespace +// This function is only used in tests std::vector> find_maximal_cliques_for_test( const std::vector>& adjacency_list, const std::vector& weights, @@ -1218,8 +1199,9 @@ bool cut_generation_t::generate_clique_cuts( std::vector in_subgraph(2 * num_vars, 0); for (size_t idx = 0; idx < vertices.size(); ++idx) { if (toc(start_time) >= settings.time_limit) { return true; } - vertex_to_local[vertices[idx]] = static_cast(idx); - in_subgraph[vertices[idx]] = 1; + const i_t vertex_idx = vertices[idx]; + vertex_to_local[vertex_idx] = static_cast(idx); + in_subgraph[vertex_idx] = 1; } work_estimate += 3.0 * static_cast(vertices.size()); if (work_estimate > max_work_estimate) { return true; } @@ -1329,7 +1311,7 @@ bool cut_generation_t::generate_clique_cuts( for (auto local_idx : clique_local) { clique_vertices.push_back(vertices[local_idx]); } - work_estimate += 3.0 * static_cast(clique_local.size()) + 1.0; + work_estimate += 3.0 * static_cast(clique_local.size()); if (work_estimate > max_work_estimate) { return true; } #if DEBUG_CLIQUE_CUTS const size_t size_before_extension = clique_vertices.size(); diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 5cb3253bbb..3ee73dd93c 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -39,28 +39,6 @@ enum cut_type_t : int8_t { MAX_CUT_TYPE = 5 }; -enum class cut_configuration_t : int8_t { - NONE = 0, - WITHOUT_CLIQUE = 1, - WITH_CLIQUE = 2, - CLIQUE_ONLY = 3, -}; - -template -cut_configuration_t classify_cut_configuration(const simplex_solver_settings_t& settings) -{ - const bool clique_enabled = settings.clique_cuts != 0; - const bool non_clique_enabled = settings.mixed_integer_gomory_cuts != 0 || - settings.strong_chvatal_gomory_cuts != 0 || - settings.knapsack_cuts != 0 || settings.mir_cuts != 0; - if (clique_enabled && non_clique_enabled) { return cut_configuration_t::WITH_CLIQUE; } - if (clique_enabled) { return cut_configuration_t::CLIQUE_ONLY; } - if (non_clique_enabled) { return cut_configuration_t::WITHOUT_CLIQUE; } - return cut_configuration_t::NONE; -} - -const char* cut_configuration_name(cut_configuration_t cut_configuration); - template struct cut_gap_closure_t { f_t initial_gap{0.0}; From d718b8b2b67da3e45ae494802d315f035a562be9 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 11:11:37 -0800 Subject: [PATCH 126/147] name space cleanup --- cpp/src/branch_and_bound/branch_and_bound.cpp | 24 +++++++++---------- cpp/src/branch_and_bound/branch_and_bound.hpp | 5 ++-- cpp/src/cuts/cuts.cpp | 8 +++---- cpp/src/cuts/cuts.hpp | 5 ++-- .../diversity/diversity_manager.cu | 11 ++++----- 5 files changed, 24 insertions(+), 29 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 37782b613d..746a2d1860 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -243,7 +243,7 @@ branch_and_bound_t::branch_and_bound_t( const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, f_t start_time, - std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> clique_table) + std::shared_ptr> clique_table) : original_problem_(user_problem), settings_(solver_settings), clique_table_(std::move(clique_table)), @@ -2100,19 +2100,21 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t root_relax_objective = root_objective_; auto report_cut_gap_closure_metric = [&]() { - const auto cut_configuration = classify_cut_configuration(settings_); - if (settings_.max_cut_passes <= 0 || cut_configuration == cut_configuration_t::NONE) { - settings_.log.printf("Cut gap closure skipped: max_cut_passes=%d cut_configuration=%s\n", + const bool cuts_enabled = settings_.clique_cuts != 0 || + settings_.mixed_integer_gomory_cuts != 0 || + settings_.strong_chvatal_gomory_cuts != 0 || + settings_.knapsack_cuts != 0 || settings_.mir_cuts != 0; + if (settings_.max_cut_passes <= 0 || !cuts_enabled) { + settings_.log.printf("Cut gap closure skipped: max_cut_passes=%d cuts_enabled=%d\n", settings_.max_cut_passes, - cut_configuration_name(cut_configuration)); + cuts_enabled ? 1 : 0); return; } const std::string& instance_name_for_lookup = original_problem_.problem_name; const std::string normalized_problem_name = - ::cuopt::linear_programming::detail::normalize_problem_name(instance_name_for_lookup); + detail::normalize_problem_name(instance_name_for_lookup); const auto objective_reference = - ::cuopt::linear_programming::detail::lookup_known_objective_reference( - instance_name_for_lookup); + detail::lookup_known_objective_reference(instance_name_for_lookup); if (!objective_reference.has_value()) { settings_.log.printf( "Cut gap closure skipped: no objective reference for instance raw='%s' normalized='%s'\n", @@ -2128,12 +2130,10 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut user_objective_before_cuts, user_objective_after_cuts); settings_.log.printf( - "Cut gap closure [%s] instance=%s known_%s=%.16e root_before=%.16e root_after=%.16e " + "Cut gap closure instance=%s known_%s=%.16e root_before=%.16e root_after=%.16e " "gap_before=%.16e gap_after=%.16e gap_closed=%.16e gap_closed_ratio=%.2f%%\n", - cut_configuration_name(cut_configuration), instance_name_for_lookup.c_str(), - ::cuopt::linear_programming::detail::objective_reference_status_name( - objective_reference->status), + detail::objective_reference_status_name(objective_reference->status), static_cast(objective_reference->objective_value), static_cast(user_objective_before_cuts), static_cast(user_objective_after_cuts), diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index f508fb145a..88ba23ca18 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -75,8 +75,7 @@ class branch_and_bound_t { branch_and_bound_t(const user_problem_t& user_problem, const simplex_solver_settings_t& solver_settings, f_t start_time, - std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> - clique_table = nullptr); + std::shared_ptr> clique_table = nullptr); // Set an initial guess based on the user_problem. This should be called before solve. void set_initial_guess(const std::vector& user_guess) { guess_ = user_guess; } @@ -147,7 +146,7 @@ class branch_and_bound_t { private: const user_problem_t& original_problem_; const simplex_solver_settings_t settings_; - std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> clique_table_; + std::shared_ptr> clique_table_; work_limit_context_t work_unit_context_{"B&B"}; diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 734442007f..59967d2e21 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -333,7 +333,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, template void extend_clique_vertices(std::vector& clique_vertices, - ::cuopt::linear_programming::detail::clique_table_t& graph, + detail::clique_table_t& graph, const std::vector& xstar, const std::vector& reduced_costs, i_t num_vars, @@ -1118,9 +1118,9 @@ bool cut_generation_t::generate_clique_cuts( if (clique_table_ == nullptr) { CLIQUE_CUTS_DEBUG("generate_clique_cuts building clique table"); - ::cuopt::linear_programming::detail::clique_config_t clique_config; + detail::clique_config_t clique_config; clique_config.min_clique_size = 2; - clique_table_ = std::make_shared<::cuopt::linear_programming::detail::clique_table_t>( + clique_table_ = std::make_shared>( 2 * num_vars, clique_config.min_clique_size, clique_config.max_clique_size_for_extension); typename ::cuopt::linear_programming::mip_solver_settings_t::tolerances_t tolerances; @@ -1134,7 +1134,7 @@ bool cut_generation_t::generate_clique_cuts( const f_t remaining_time = std::max(static_cast(0.), settings.time_limit - toc(start_time)); cuopt::timer_t clique_build_timer(static_cast(remaining_time)); - ::cuopt::linear_programming::detail::build_clique_table( + detail::build_clique_table( user_problem_, *clique_table_, tolerances, true, true, clique_build_timer); if (clique_build_timer.check_time_limit()) { return true; } CLIQUE_CUTS_DEBUG("generate_clique_cuts clique table built first=%lld addtl=%lld", diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 3ee73dd93c..840572f9b3 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -279,8 +279,7 @@ class cut_generation_t { const std::vector& new_slacks, const std::vector& var_types, const user_problem_t& user_problem, - std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> - clique_table = nullptr) + std::shared_ptr> clique_table = nullptr) : cut_pool_(cut_pool), knapsack_generation_(lp, settings, Arow, new_slacks, var_types), user_problem_(user_problem), @@ -339,7 +338,7 @@ class cut_generation_t { cut_pool_t& cut_pool_; knapsack_generation_t knapsack_generation_; const user_problem_t& user_problem_; - std::shared_ptr<::cuopt::linear_programming::detail::clique_table_t> clique_table_; + std::shared_ptr> clique_table_; }; template diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 45d9bddb1c..bf8b833c44 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -352,21 +352,18 @@ solution_t diversity_manager_t::run_solver() : "opportunistic"); if (problem_ptr->original_problem_ptr != nullptr) { - const auto problem_name = problem_ptr->original_problem_ptr->get_problem_name(); - const auto normalized_problem_name = - ::cuopt::linear_programming::detail::normalize_problem_name(problem_name); + const auto problem_name = problem_ptr->original_problem_ptr->get_problem_name(); + const auto normalized_problem_name = normalize_problem_name(problem_name); CUOPT_LOG_DEBUG("Objective reference lookup raw='%s' normalized='%s'", problem_name.c_str(), normalized_problem_name.c_str()); if (!problem_name.empty()) { - const auto objective_reference = - ::cuopt::linear_programming::detail::lookup_known_objective_reference(problem_name); + const auto objective_reference = lookup_known_objective_reference(problem_name); if (objective_reference.has_value()) { CUOPT_LOG_INFO("Known objective reference for %s: %.17g (%s)", problem_name.c_str(), objective_reference->objective_value, - ::cuopt::linear_programming::detail::objective_reference_status_name( - objective_reference->status)); + objective_reference_status_name(objective_reference->status)); } else { CUOPT_LOG_DEBUG("No objective reference mapping found for %s", problem_name.c_str()); } From ff6605a903e0beedc79c90d4f13e558483f7bd9d Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 22:50:02 -0800 Subject: [PATCH 127/147] fix adj list timer --- .../presolve/conflict_graph/clique_table.cu | 72 +++++++++---------- .../presolve/conflict_graph/clique_table.cuh | 2 + 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index cf556aefee..2b09ed8ed9 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -320,15 +320,16 @@ std::unordered_set clique_table_t::get_adj_set_of_var(i_t var_idx addtl_cliques[addtl_clique_idx].start_pos_on_clique, first[addtl_cliques[addtl_clique_idx].clique_idx].end()); } - // Memory-neutral reverse lookup for additional cliques: + // Reverse lookup for additional cliques using position map: // if var_idx is in first[clique_idx][start_pos_on_clique:], it is adjacent to vertex_idx. for (const auto& addtl : addtl_cliques) { if (addtl.vertex_idx == var_idx) { continue; } - const auto& clique = first[addtl.clique_idx]; - size_t start_pos = static_cast(addtl.start_pos_on_clique); - if (start_pos < clique.size() && - std::find(clique.begin() + start_pos, clique.end(), var_idx) != clique.end()) { - adj_set.insert(addtl.vertex_idx); + if (static_cast(addtl.clique_idx) < first_var_positions.size()) { + const auto& pos_map = first_var_positions[addtl.clique_idx]; + auto it = pos_map.find(var_idx); + if (it != pos_map.end() && it->second >= addtl.start_pos_on_clique) { + adj_set.insert(addtl.vertex_idx); + } } } @@ -353,44 +354,39 @@ i_t clique_table_t::get_degree_of_var(i_t var_idx) template bool clique_table_t::check_adjacency(i_t var_idx1, i_t var_idx2) { - // if passed same variable if (var_idx1 == var_idx2) { return false; } - // in case they are complements of each other if (var_idx1 % n_variables == var_idx2 % n_variables) { return true; } - if (adj_list_small_cliques[var_idx1].count(var_idx2) > 0) { return true; } - // Check first cliques: var_clique_map_first stores clique indices - for (const auto& clique_idx : var_clique_map_first[var_idx1]) { - const auto& clique = first[clique_idx]; - // TODO: we can also keep a set of the clique if the memory allows, instead of doing linear - // search - if (std::find(clique.begin(), clique.end(), var_idx2) != clique.end()) { return true; } + + { + auto it = adj_list_small_cliques.find(var_idx1); + if (it != adj_list_small_cliques.end() && it->second.count(var_idx2) > 0) { return true; } } - // Check additional cliques: var_clique_map_addtl stores indices into addtl_cliques - for (const auto& addtl_idx : var_clique_map_addtl[var_idx1]) { - const auto& addtl = addtl_cliques[addtl_idx]; - const auto& clique = first[addtl.clique_idx]; - // addtl clique is: vertex_idx + first[clique_idx][start_pos_on_clique:] - if (addtl.vertex_idx == var_idx2) { return true; } - if (addtl.start_pos_on_clique < static_cast(clique.size())) { - if (std::find(clique.begin() + addtl.start_pos_on_clique, clique.end(), var_idx2) != - clique.end()) { - return true; - } + // Iterate whichever variable belongs to fewer first-cliques + { + i_t probe_var = var_idx1; + i_t target_var = var_idx2; + if (var_clique_map_first[var_idx1].size() > var_clique_map_first[var_idx2].size()) { + probe_var = var_idx2; + target_var = var_idx1; + } + for (const auto& clique_idx : var_clique_map_first[probe_var]) { + if (first_var_positions[clique_idx].count(target_var) > 0) { return true; } } } - // var_clique_map_addtl is keyed by addtl.vertex_idx, so also check the reverse direction. + for (const auto& addtl_idx : var_clique_map_addtl[var_idx1]) { + const auto& addtl = addtl_cliques[addtl_idx]; + const auto& pos_map = first_var_positions[addtl.clique_idx]; + auto it = pos_map.find(var_idx2); + if (it != pos_map.end() && it->second >= addtl.start_pos_on_clique) { return true; } + } + for (const auto& addtl_idx : var_clique_map_addtl[var_idx2]) { - const auto& addtl = addtl_cliques[addtl_idx]; - const auto& clique = first[addtl.clique_idx]; - if (addtl.vertex_idx == var_idx1) { return true; } - if (addtl.start_pos_on_clique < static_cast(clique.size())) { - if (std::find(clique.begin() + addtl.start_pos_on_clique, clique.end(), var_idx1) != - clique.end()) { - return true; - } - } + const auto& addtl = addtl_cliques[addtl_idx]; + const auto& pos_map = first_var_positions[addtl.clique_idx]; + auto it = pos_map.find(var_idx1); + if (it != pos_map.end() && it->second >= addtl.start_pos_on_clique) { return true; } } return false; @@ -927,11 +923,15 @@ i_t extend_cliques(const std::vector>& knapsack_ template void fill_var_clique_maps(clique_table_t& clique_table) { + clique_table.first_var_positions.resize(clique_table.first.size()); for (size_t clique_idx = 0; clique_idx < clique_table.first.size(); clique_idx++) { const auto& clique = clique_table.first[clique_idx]; + auto& pos_map = clique_table.first_var_positions[clique_idx]; + pos_map.reserve(clique.size()); for (size_t idx = 0; idx < clique.size(); idx++) { i_t var_idx = clique[idx]; clique_table.var_clique_map_first[var_idx].insert(clique_idx); + pos_map[var_idx] = static_cast(idx); } } for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh index a193242b7b..a56218de4e 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh @@ -84,6 +84,8 @@ struct clique_table_t { std::vector> var_clique_map_first; // keeps the indices of additional cliques that contain variable x std::vector> var_clique_map_addtl; + // var_idx -> position mapping for each first clique, enabling O(1) membership/position checks + std::vector> first_var_positions; // adjacency list to keep small cliques, this basically keeps the vars share a small clique // constraint std::unordered_map> adj_list_small_cliques; From 43bf1eaa5df1406301b450fae95ac3ab47a1eeee Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 22:51:06 -0800 Subject: [PATCH 128/147] increase clique table timer --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index bf8b833c44..58b4f40b16 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -213,7 +213,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ !problem_ptr->empty) { // TODO this is just CPU time and blocking the GPU time. // execute this in parallel with something else. - f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time()); + f_t time_limit_for_clique_table = std::min(10., presolve_timer.remaining_time()); // if it is deterministic run until the end if (context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC) { time_limit_for_clique_table = std::numeric_limits::infinity(); From 349c7bff7ec0029961a005f79c82585d01f053a7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 6 Mar 2026 22:51:36 -0800 Subject: [PATCH 129/147] reduce work estimates --- cpp/src/cuts/cuts.cpp | 2 +- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 59967d2e21..033ab156c7 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1160,7 +1160,7 @@ bool cut_generation_t::generate_clique_cuts( // TODO this can be problem dependent const i_t max_calls = 100000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 2e9; + const f_t max_work_estimate = 2e8; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 58b4f40b16..bf8b833c44 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -213,7 +213,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ !problem_ptr->empty) { // TODO this is just CPU time and blocking the GPU time. // execute this in parallel with something else. - f_t time_limit_for_clique_table = std::min(10., presolve_timer.remaining_time()); + f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time()); // if it is deterministic run until the end if (context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC) { time_limit_for_clique_table = std::numeric_limits::infinity(); From e27e1424e27b7f132fb016e623b2d35fd5bf5485 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Sat, 7 Mar 2026 14:13:00 -0800 Subject: [PATCH 130/147] fix clique generation times --- .../presolve/conflict_graph/clique_table.cu | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 2b09ed8ed9..495d88822e 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -34,11 +34,27 @@ namespace cuopt::linear_programming::detail { // do constraints with only binary variables. template void find_cliques_from_constraint(const knapsack_constraint_t& kc, - clique_table_t& clique_table) + clique_table_t& clique_table, + cuopt::timer_t& timer) { i_t size = kc.entries.size(); cuopt_assert(size > 1, "Constraint has not enough variables"); if (kc.entries[size - 1].val + kc.entries[size - 2].val <= kc.rhs) { return; } + + // For set packing constraints all variables are pairwise conflicting. + // Materialize edges directly into adj_list to avoid a large entry in 'first' + // and the downstream overhead of clique maps / position lookups. + constexpr i_t max_set_packing_adj_list_size = 100; + if (kc.is_set_packing && size <= max_set_packing_adj_list_size) { + for (i_t i = 0; i < size; i++) { + for (i_t j = i + 1; j < size; j++) { + clique_table.adj_list_small_cliques[kc.entries[i].col].insert(kc.entries[j].col); + clique_table.adj_list_small_cliques[kc.entries[j].col].insert(kc.entries[i].col); + } + } + return; + } + std::vector clique; i_t k = size - 1; // find the first clique, which is the largest @@ -55,6 +71,7 @@ void find_cliques_from_constraint(const knapsack_constraint_t& kc, // find the additional cliques k--; while (k >= 0) { + if (timer.check_time_limit()) { return; } f_t curr_val = kc.entries[k].val; i_t curr_col = kc.entries[k].col; // do a binary search in the clique coefficients to find f, such that coeff_k + coeff_f > rhs @@ -963,7 +980,7 @@ void build_clique_table(const dual_simplex::user_problem_t& problem, clique_table.tolerances = tolerances; for (const auto& knapsack_constraint : knapsack_constraints) { if (timer.check_time_limit()) { return; } - find_cliques_from_constraint(knapsack_constraint, clique_table); + find_cliques_from_constraint(knapsack_constraint, clique_table, timer); } if (timer.check_time_limit()) { return; } if (remove_small_cliques_flag) { remove_small_cliques(clique_table, timer); } @@ -1064,9 +1081,8 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, clique_table_ptr->tolerances = tolerances; for (const auto& knapsack_constraint : knapsack_constraints) { if (timer.check_time_limit()) { break; } - find_cliques_from_constraint(knapsack_constraint, *clique_table_ptr); + find_cliques_from_constraint(knapsack_constraint, *clique_table_ptr, timer); } - if (timer.check_time_limit()) { return; } #ifdef DEBUG_CLIQUE_TABLE t_find = stage_timer.elapsed_time(); #endif From 8a34777086a3a2b8b3306b3f268f1a69810103e5 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Sun, 8 Mar 2026 05:04:38 -0700 Subject: [PATCH 131/147] with higher probing cache time --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index da6c7c0b81..7cc15ffc34 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -112,7 +112,7 @@ solution_t mip_solver_t::run_solver() f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : timer_.remaining_time(); - double presolve_time_limit = std::min(0.1 * time_limit, 60.0); + double presolve_time_limit = std::min(0.12 * time_limit, 70.0); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; From 7b3604fd92d7ef8e20fd144e2b044586f0e84dc9 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Sun, 8 Mar 2026 23:00:22 -0700 Subject: [PATCH 132/147] better work units and timers --- cpp/src/branch_and_bound/branch_and_bound.cpp | 6 ++++-- cpp/src/cuts/cuts.cpp | 17 +++++++++++------ .../presolve/conflict_graph/clique_table.cu | 6 +++++- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 746a2d1860..2c3f3d37c0 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2143,7 +2143,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut static_cast(gap_closure.gap_closed_ratio * 100.0)); }; - i_t cut_pool_size = 0; + f_t cut_generation_start_time = tic(); + i_t cut_pool_size = 0; for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { if (num_fractional == 0) { set_solution_at_root(solution, cut_info); @@ -2402,8 +2403,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut report_cut_gap_closure_metric(); print_cut_info(settings_, cut_info); - + f_t cut_generation_time = toc(cut_generation_start_time); if (cut_info.has_cuts()) { + settings_.log.printf("Cut generation time: %.2f seconds\n", cut_generation_time); settings_.log.printf("Cut pool size : %d\n", cut_pool_size); settings_.log.printf("Size with cuts : %d constraints, %d variables, %d nonzeros\n", original_lp_.num_rows, diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 033ab156c7..66fe014cc3 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -23,7 +23,7 @@ namespace cuopt::linear_programming::dual_simplex { namespace { -#define DEBUG_CLIQUE_CUTS 0 +#define DEBUG_CLIQUE_CUTS 1 #define CHECK_WORKSPACE 0 enum class clique_cut_build_status_t : int8_t { NO_CUT = 0, CUT_ADDED = 1, INFEASIBLE = 2 }; @@ -60,7 +60,8 @@ clique_cut_build_status_t build_clique_cut(const std::vector& clique_vertic CLIQUE_CUTS_DEBUG("build_clique_cut start clique_size=%lld", static_cast(clique_vertices.size())); const f_t sort_work = clique_size > 0.0 ? 2.0 * clique_size * std::log2(clique_size + 1.0) : 0.0; - const f_t estimated_work = 11.0 * clique_size + sort_work; + const f_t dot_work = 2.0 * clique_size; + const f_t estimated_work = 9.0 * clique_size + sort_work + dot_work; if (add_work_estimate(estimated_work, work_estimate, max_work_estimate)) { CLIQUE_CUTS_DEBUG("build_clique_cut skip work_limit clique_size=%lld work=%g limit=%g", static_cast(clique_vertices.size()), @@ -237,7 +238,7 @@ void bron_kerbosch(bk_bitset_context_t& ctx, ctx.call_limit_reached = true; return; } - if (ctx.add_work(static_cast(2 * ctx.words + R.size()))) { return; } + if (ctx.add_work(static_cast(4 * ctx.words))) { return; } // if P and X are empty, we are at maximal clique if (!bitset_any(P) && !bitset_any(X)) { @@ -384,9 +385,12 @@ void extend_clique_vertices(std::vector& clique_vertices, const f_t candidate_size = static_cast(candidates.size()); const f_t sort_work = candidate_size > 0.0 ? 2.0 * candidate_size * std::log2(candidate_size + 1.0) : 0.0; - const f_t estimated_extension_work = 2.0 * initial_clique_size + - 3.0 * static_cast(adj_set.size()) + sort_work + - candidate_size * initial_clique_size + 2.0 * candidate_size; + const f_t adj_set_build_cost = 2.0 * static_cast(adj_set.size()); + const f_t adj_check_cost = 3.0; + const f_t estimated_extension_work = + 2.0 * initial_clique_size + adj_set_build_cost + 3.0 * static_cast(adj_set.size()) + + sort_work + adj_check_cost * candidate_size * (initial_clique_size + 0.5 * candidate_size) + + 2.0 * candidate_size; if (add_work_estimate(estimated_extension_work, work_estimate, max_work_estimate)) { CLIQUE_CUTS_DEBUG("extend_clique_vertices skip work_limit work=%g limit=%g", work_estimate == nullptr ? -1.0 : static_cast(*work_estimate), @@ -1061,6 +1065,7 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, return false; } f_t cut_generation_time = toc(cut_start_time); + settings.log.printf("Clique cut generation time %.2f seconds\n", cut_generation_time); if (cut_generation_time > 1.0) { settings.log.debug("Clique cut generation time %.2f seconds\n", cut_generation_time); } diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 495d88822e..2b8b734c08 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -1079,9 +1079,13 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, clique_table_ptr = clique_table_shared.get(); } clique_table_ptr->tolerances = tolerances; + // spend maximum half of the time for additional cliques + // the other half for extending them + double time_limit_for_additional_cliques = timer.remaining_time() / 2; + cuopt::timer_t additional_cliques_timer(time_limit_for_additional_cliques); for (const auto& knapsack_constraint : knapsack_constraints) { if (timer.check_time_limit()) { break; } - find_cliques_from_constraint(knapsack_constraint, *clique_table_ptr, timer); + find_cliques_from_constraint(knapsack_constraint, *clique_table_ptr, additional_cliques_timer); } #ifdef DEBUG_CLIQUE_TABLE t_find = stage_timer.elapsed_time(); From f9b39f67c9098af8f1ede1434f98ebfcf5a2ec2b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Mar 2026 00:05:27 -0700 Subject: [PATCH 133/147] better work estimate in extend cliques --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 +- cpp/src/cuts/cuts.cpp | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 2c3f3d37c0..1964d5a207 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2313,7 +2313,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); f_t dual_phase2_time = toc(dual_phase2_start_time); if (dual_phase2_time > 1.0) { - settings_.log.debug("Dual phase2 time %.2f seconds\n", dual_phase2_time); + settings_.log.printf("Dual phase2 time %.2f seconds\n", dual_phase2_time); } if (cut_status == dual::status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 66fe014cc3..8930bc5970 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -385,13 +385,12 @@ void extend_clique_vertices(std::vector& clique_vertices, const f_t candidate_size = static_cast(candidates.size()); const f_t sort_work = candidate_size > 0.0 ? 2.0 * candidate_size * std::log2(candidate_size + 1.0) : 0.0; - const f_t adj_set_build_cost = 2.0 * static_cast(adj_set.size()); - const f_t adj_check_cost = 3.0; - const f_t estimated_extension_work = - 2.0 * initial_clique_size + adj_set_build_cost + 3.0 * static_cast(adj_set.size()) + - sort_work + adj_check_cost * candidate_size * (initial_clique_size + 0.5 * candidate_size) + - 2.0 * candidate_size; - if (add_work_estimate(estimated_extension_work, work_estimate, max_work_estimate)) { + const f_t adj_set_build_cost = 2.0 * static_cast(adj_set.size()); + const f_t adj_check_cost = 5.0; + const f_t estimated_preloop_work = 2.0 * initial_clique_size + adj_set_build_cost + + 3.0 * static_cast(adj_set.size()) + sort_work + + 2.0 * candidate_size; + if (add_work_estimate(estimated_preloop_work, work_estimate, max_work_estimate)) { CLIQUE_CUTS_DEBUG("extend_clique_vertices skip work_limit work=%g limit=%g", work_estimate == nullptr ? -1.0 : static_cast(*work_estimate), static_cast(max_work_estimate)); @@ -417,13 +416,19 @@ void extend_clique_vertices(std::vector& clique_vertices, }); for (const auto candidate : candidates) { - bool add = true; + bool add = true; + i_t checks = 0; for (const auto v : clique_vertices) { + checks++; if (!graph.check_adjacency(candidate, v)) { add = false; break; } } + if (add_work_estimate( + adj_check_cost * static_cast(checks), work_estimate, max_work_estimate)) { + break; + } if (add) { clique_vertices.push_back(candidate); clique_members.insert(candidate); From 395d5f0761b467d7a37918a4ad2dd4f29c9b027f Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Mar 2026 00:05:57 -0700 Subject: [PATCH 134/147] higher work unit --- cpp/src/cuts/cuts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 8930bc5970..a83141e3a3 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1170,7 +1170,7 @@ bool cut_generation_t::generate_clique_cuts( // TODO this can be problem dependent const i_t max_calls = 100000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 2e8; + const f_t max_work_estimate = 2e9; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); From 5fb1b5c2077f8dfe5b2df61d43eeaef3de22d4a7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Mar 2026 00:07:26 -0700 Subject: [PATCH 135/147] longer time for clique table gen --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index bf8b833c44..58b4f40b16 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -213,7 +213,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ !problem_ptr->empty) { // TODO this is just CPU time and blocking the GPU time. // execute this in parallel with something else. - f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time()); + f_t time_limit_for_clique_table = std::min(10., presolve_timer.remaining_time()); // if it is deterministic run until the end if (context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC) { time_limit_for_clique_table = std::numeric_limits::infinity(); From d20016958850a63d2ba5b5c68586dc47e7da0c09 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Mar 2026 04:55:41 -0700 Subject: [PATCH 136/147] clean up --- cpp/src/branch_and_bound/branch_and_bound.cpp | 47 +-- cpp/src/cuts/cuts.cpp | 3 +- .../diversity/diversity_manager.cu | 23 -- .../diversity/known_miplib_objectives.hpp | 334 ------------------ .../presolve/conflict_graph/clique_table.cu | 2 +- 5 files changed, 3 insertions(+), 406 deletions(-) delete mode 100644 cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 1964d5a207..2215d48128 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2099,50 +2099,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t last_objective = root_objective_; f_t root_relax_objective = root_objective_; - auto report_cut_gap_closure_metric = [&]() { - const bool cuts_enabled = settings_.clique_cuts != 0 || - settings_.mixed_integer_gomory_cuts != 0 || - settings_.strong_chvatal_gomory_cuts != 0 || - settings_.knapsack_cuts != 0 || settings_.mir_cuts != 0; - if (settings_.max_cut_passes <= 0 || !cuts_enabled) { - settings_.log.printf("Cut gap closure skipped: max_cut_passes=%d cuts_enabled=%d\n", - settings_.max_cut_passes, - cuts_enabled ? 1 : 0); - return; - } - const std::string& instance_name_for_lookup = original_problem_.problem_name; - const std::string normalized_problem_name = - detail::normalize_problem_name(instance_name_for_lookup); - const auto objective_reference = - detail::lookup_known_objective_reference(instance_name_for_lookup); - if (!objective_reference.has_value()) { - settings_.log.printf( - "Cut gap closure skipped: no objective reference for instance raw='%s' normalized='%s'\n", - original_problem_.problem_name.c_str(), - normalized_problem_name.c_str()); - return; - } - const f_t user_objective_before_cuts = - compute_user_objective(original_lp_, root_relax_objective); - const f_t user_objective_after_cuts = compute_user_objective(original_lp_, root_objective_); - const auto gap_closure = - compute_cut_gap_closure(static_cast(objective_reference->objective_value), - user_objective_before_cuts, - user_objective_after_cuts); - settings_.log.printf( - "Cut gap closure instance=%s known_%s=%.16e root_before=%.16e root_after=%.16e " - "gap_before=%.16e gap_after=%.16e gap_closed=%.16e gap_closed_ratio=%.2f%%\n", - instance_name_for_lookup.c_str(), - detail::objective_reference_status_name(objective_reference->status), - static_cast(objective_reference->objective_value), - static_cast(user_objective_before_cuts), - static_cast(user_objective_after_cuts), - static_cast(gap_closure.initial_gap), - static_cast(gap_closure.final_gap), - static_cast(gap_closure.gap_closed), - static_cast(gap_closure.gap_closed_ratio * 100.0)); - }; - f_t cut_generation_start_time = tic(); i_t cut_pool_size = 0; for (i_t cut_pass = 0; cut_pass < settings_.max_cut_passes; cut_pass++) { @@ -2313,7 +2269,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_objective_ = compute_objective(original_lp_, root_relax_soln_.x); f_t dual_phase2_time = toc(dual_phase2_start_time); if (dual_phase2_time > 1.0) { - settings_.log.printf("Dual phase2 time %.2f seconds\n", dual_phase2_time); + settings_.log.debug("Dual phase2 time %.2f seconds\n", dual_phase2_time); } if (cut_status == dual::status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; @@ -2401,7 +2357,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut } } - report_cut_gap_closure_metric(); print_cut_info(settings_, cut_info); f_t cut_generation_time = toc(cut_generation_start_time); if (cut_info.has_cuts()) { diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index a83141e3a3..bceb4ca205 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -23,7 +23,7 @@ namespace cuopt::linear_programming::dual_simplex { namespace { -#define DEBUG_CLIQUE_CUTS 1 +#define DEBUG_CLIQUE_CUTS 0 #define CHECK_WORKSPACE 0 enum class clique_cut_build_status_t : int8_t { NO_CUT = 0, CUT_ADDED = 1, INFEASIBLE = 2 }; @@ -1070,7 +1070,6 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, return false; } f_t cut_generation_time = toc(cut_start_time); - settings.log.printf("Clique cut generation time %.2f seconds\n", cut_generation_time); if (cut_generation_time > 1.0) { settings.log.debug("Clique cut generation time %.2f seconds\n", cut_generation_time); } diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 58b4f40b16..ba0e1ace6b 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -351,29 +351,6 @@ solution_t diversity_manager_t::run_solver() context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? "deterministic" : "opportunistic"); - if (problem_ptr->original_problem_ptr != nullptr) { - const auto problem_name = problem_ptr->original_problem_ptr->get_problem_name(); - const auto normalized_problem_name = normalize_problem_name(problem_name); - CUOPT_LOG_DEBUG("Objective reference lookup raw='%s' normalized='%s'", - problem_name.c_str(), - normalized_problem_name.c_str()); - if (!problem_name.empty()) { - const auto objective_reference = lookup_known_objective_reference(problem_name); - if (objective_reference.has_value()) { - CUOPT_LOG_INFO("Known objective reference for %s: %.17g (%s)", - problem_name.c_str(), - objective_reference->objective_value, - objective_reference_status_name(objective_reference->status)); - } else { - CUOPT_LOG_DEBUG("No objective reference mapping found for %s", problem_name.c_str()); - } - } else { - CUOPT_LOG_DEBUG("Skipping objective reference lookup because problem_name is empty"); - } - } else { - CUOPT_LOG_DEBUG("Skipping objective reference lookup because original_problem_ptr is null"); - } - // to automatically compute the solving time on scope exit auto timer_raii_guard = cuopt::scope_guard([&]() { stats.total_solve_time = timer.elapsed_time(); }); diff --git a/cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp b/cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp deleted file mode 100644 index 23c4b74b8b..0000000000 --- a/cpp/src/mip_heuristics/diversity/known_miplib_objectives.hpp +++ /dev/null @@ -1,334 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -namespace cuopt::linear_programming::detail { - -enum class objective_reference_status_t : int8_t { OPTIMAL = 0, BEST_KNOWN = 1 }; - -struct objective_reference_t { - double objective_value; - objective_reference_status_t status; -}; - -inline const char* objective_reference_status_name(objective_reference_status_t status) -{ - return status == objective_reference_status_t::OPTIMAL ? "optimal" : "best_known"; -} - -inline std::string normalize_problem_name(std::string problem_name) -{ - auto trim_ascii_whitespace = [](std::string& s) { - auto is_ws = [](unsigned char c) { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; - }; - size_t begin = 0; - while (begin < s.size() && is_ws(static_cast(s[begin]))) { - ++begin; - } - size_t end = s.size(); - while (end > begin && is_ws(static_cast(s[end - 1]))) { - --end; - } - s = s.substr(begin, end - begin); - }; - - trim_ascii_whitespace(problem_name); - if (!problem_name.empty()) { - const char first = problem_name.front(); - const char last = problem_name.back(); - const bool wrapped_in_quotes = (first == '"' && last == '"') || (first == '\'' && last == '\''); - if (wrapped_in_quotes && problem_name.size() >= 2) { - problem_name = problem_name.substr(1, problem_name.size() - 2); - trim_ascii_whitespace(problem_name); - } - } - - const auto slash_pos = problem_name.find_last_of("/\\"); - if (slash_pos != std::string::npos) { problem_name = problem_name.substr(slash_pos + 1); } - trim_ascii_whitespace(problem_name); - std::transform( - problem_name.begin(), problem_name.end(), problem_name.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); - const std::array suffixes = {".mps.gz", ".mps.bz2", ".mps", ".gz", ".bz2"}; - auto ends_with = [](const std::string& s, const std::string& suffix) { - return s.size() >= suffix.size() && - s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; - }; - bool removed = true; - while (removed) { - removed = false; - for (const auto& suffix : suffixes) { - if (ends_with(problem_name, suffix)) { - problem_name.resize(problem_name.size() - suffix.size()); - removed = true; - break; - } - } - } - return problem_name; -} - -inline std::optional lookup_known_objective_reference( - std::string problem_name) -{ - // These benchmark names are intentionally absent because miplib2017-v36.solu does not provide - // a finite reference objective for them: - // bnatt500, cryptanalysiskb128n5obj14, fhnw-binpack4-4, neos-2075418-temuka, - // neos-3402454-bohle, neos-3988577-wolgan, neos859080. - static const std::unordered_map k_objective_map = { - {"30n20b8", {302, objective_reference_status_t::OPTIMAL}}, - {"50v-10", {3311.1799841000002, objective_reference_status_t::OPTIMAL}}, - {"academictimetablesmall", {0, objective_reference_status_t::OPTIMAL}}, - {"air05", {26374, objective_reference_status_t::OPTIMAL}}, - {"app1-1", {-3, objective_reference_status_t::OPTIMAL}}, - {"app1-2", {-41, objective_reference_status_t::OPTIMAL}}, - {"assign1-5-8", {211.99999999999801, objective_reference_status_t::OPTIMAL}}, - {"atlanta-ip", {90.009878614000002, objective_reference_status_t::OPTIMAL}}, - {"b1c1s1", {24544.25, objective_reference_status_t::OPTIMAL}}, - {"bab2", {-357544.31150000001, objective_reference_status_t::OPTIMAL}}, - {"bab6", {-284248.23070000007, objective_reference_status_t::OPTIMAL}}, - {"beasleyc3", {753.9999999999128, objective_reference_status_t::OPTIMAL}}, - {"binkar10_1", {6741.3800239397196, objective_reference_status_t::OPTIMAL}}, - {"blp-ar98", {6205.2147103999996, objective_reference_status_t::OPTIMAL}}, - {"blp-ic98", {4491.4475839500001, objective_reference_status_t::OPTIMAL}}, - {"bnatt400", {1, objective_reference_status_t::OPTIMAL}}, - {"bppc4-08", {53, objective_reference_status_t::OPTIMAL}}, - {"brazil3", {24, objective_reference_status_t::OPTIMAL}}, - {"buildingenergy", {33283.853236000003, objective_reference_status_t::OPTIMAL}}, - {"cbs-cta", {0, objective_reference_status_t::OPTIMAL}}, - {"chromaticindex1024-7", {4, objective_reference_status_t::OPTIMAL}}, - {"chromaticindex512-7", {4, objective_reference_status_t::OPTIMAL}}, - {"cmflsp50-24-8-8", {55789389.886, objective_reference_status_t::OPTIMAL}}, - {"cms750_4", {252, objective_reference_status_t::OPTIMAL}}, - {"co-100", {2639942.0600000001, objective_reference_status_t::OPTIMAL}}, - {"cod105", {-12, objective_reference_status_t::OPTIMAL}}, - {"comp07-2idx", {6, objective_reference_status_t::OPTIMAL}}, - {"comp21-2idx", {74, objective_reference_status_t::OPTIMAL}}, - {"cost266-uue", {25148940.55999998, objective_reference_status_t::OPTIMAL}}, - {"cryptanalysiskb128n5obj16", {0, objective_reference_status_t::OPTIMAL}}, - {"csched007", {350.99999999999551, objective_reference_status_t::OPTIMAL}}, - {"csched008", {173, objective_reference_status_t::OPTIMAL}}, - {"cvs16r128-89", {-97, objective_reference_status_t::OPTIMAL}}, - {"dano3_3", {576.34463302999995, objective_reference_status_t::OPTIMAL}}, - {"dano3_5", {576.9249159565619, objective_reference_status_t::OPTIMAL}}, - {"decomp2", {-160, objective_reference_status_t::OPTIMAL}}, - {"drayage-100-23", {103333.87407000001, objective_reference_status_t::OPTIMAL}}, - {"drayage-25-23", {101282.647018, objective_reference_status_t::OPTIMAL}}, - {"dws008-01", {37412.604587945083, objective_reference_status_t::OPTIMAL}}, - {"eil33-2", {934.007915999999, objective_reference_status_t::OPTIMAL}}, - {"eila101-2", {880.92010799999991, objective_reference_status_t::OPTIMAL}}, - {"enlight_hard", {37, objective_reference_status_t::OPTIMAL}}, - {"ex10", {100, objective_reference_status_t::OPTIMAL}}, - {"ex9", {81, objective_reference_status_t::OPTIMAL}}, - {"exp-1-500-5-5", {65887, objective_reference_status_t::OPTIMAL}}, - {"fast0507", {174, objective_reference_status_t::OPTIMAL}}, - {"fastxgemm-n2r6s0t2", {230, objective_reference_status_t::OPTIMAL}}, - {"fhnw-binpack4-48", {0, objective_reference_status_t::OPTIMAL}}, - {"fiball", {138, objective_reference_status_t::OPTIMAL}}, - {"gen-ip002", {-4783.7333920000001, objective_reference_status_t::OPTIMAL}}, - {"gen-ip054", {6840.9656417899996, objective_reference_status_t::OPTIMAL}}, - {"germanrr", {47095869.648999996, objective_reference_status_t::OPTIMAL}}, - {"gfd-schedulen180f7d50m30k18", {1, objective_reference_status_t::OPTIMAL}}, - {"glass-sc", {23, objective_reference_status_t::OPTIMAL}}, - {"glass4", {1200012599.972384, objective_reference_status_t::OPTIMAL}}, - {"gmu-35-40", {-2406733.3687999998, objective_reference_status_t::OPTIMAL}}, - {"gmu-35-50", {-2607958.3300000001, objective_reference_status_t::OPTIMAL}}, - {"graph20-20-1rand", {-9, objective_reference_status_t::OPTIMAL}}, - {"graphdraw-domain", {19685.999975500381, objective_reference_status_t::OPTIMAL}}, - {"h80x6320d", {6382.0990482459993, objective_reference_status_t::OPTIMAL}}, - {"highschool1-aigio", {0, objective_reference_status_t::OPTIMAL}}, - {"hypothyroid-k1", {-2851, objective_reference_status_t::OPTIMAL}}, - {"ic97_potential", {3941.9999309022501, objective_reference_status_t::OPTIMAL}}, - {"icir97_tension", {6375, objective_reference_status_t::OPTIMAL}}, - {"irish-electricity", {3723497.5913959998, objective_reference_status_t::OPTIMAL}}, - {"irp", {12159.492835396981, objective_reference_status_t::OPTIMAL}}, - {"istanbul-no-cutoff", {204.08170701, objective_reference_status_t::OPTIMAL}}, - {"k1mushroom", {-3288, objective_reference_status_t::OPTIMAL}}, - {"lectsched-5-obj", {24, objective_reference_status_t::OPTIMAL}}, - {"leo1", {404227536.16000003, objective_reference_status_t::OPTIMAL}}, - {"leo2", {404077441.12, objective_reference_status_t::OPTIMAL}}, - {"lotsize", {1480195, objective_reference_status_t::OPTIMAL}}, - {"mad", {0.026800000000000001, objective_reference_status_t::OPTIMAL}}, - {"map10", {-495, objective_reference_status_t::OPTIMAL}}, - {"map16715-04", {-111, objective_reference_status_t::OPTIMAL}}, - {"markshare2", {1, objective_reference_status_t::OPTIMAL}}, - {"markshare_4_0", {1, objective_reference_status_t::OPTIMAL}}, - {"mas74", {11801.185719999999, objective_reference_status_t::OPTIMAL}}, - {"mas76", {40005.053989999993, objective_reference_status_t::OPTIMAL}}, - {"mc11", {11688.99999999966, objective_reference_status_t::OPTIMAL}}, - {"mcsched", {211913, objective_reference_status_t::OPTIMAL}}, - {"mik-250-20-75-4", {-52301, objective_reference_status_t::OPTIMAL}}, - {"milo-v12-6-r2-40-1", {326481.14282799, objective_reference_status_t::OPTIMAL}}, - {"momentum1", {109143.4935, objective_reference_status_t::OPTIMAL}}, - {"mushroom-best", {0.055333761199999998, objective_reference_status_t::OPTIMAL}}, - {"mzzv11", {-21718, objective_reference_status_t::OPTIMAL}}, - {"mzzv42z", {-20540, objective_reference_status_t::OPTIMAL}}, - {"n2seq36q", {52200, objective_reference_status_t::OPTIMAL}}, - {"n3div36", {130800, objective_reference_status_t::OPTIMAL}}, - {"n5-3", {8104.9999999939992, objective_reference_status_t::OPTIMAL}}, - {"neos-1122047", {161, objective_reference_status_t::OPTIMAL}}, - {"neos-1171448", {-309, objective_reference_status_t::OPTIMAL}}, - {"neos-1171737", {-195, objective_reference_status_t::OPTIMAL}}, - {"neos-1354092", {46, objective_reference_status_t::OPTIMAL}}, - {"neos-1445765", {-17783, objective_reference_status_t::OPTIMAL}}, - {"neos-1456979", {176, objective_reference_status_t::OPTIMAL}}, - {"neos-1582420", {90.999999999999957, objective_reference_status_t::OPTIMAL}}, - {"neos-2657525-crna", {1.810748, objective_reference_status_t::OPTIMAL}}, - {"neos-2746589-doon", {2008.1999999999989, objective_reference_status_t::OPTIMAL}}, - {"neos-2978193-inde", {-2.3880616899999998, objective_reference_status_t::OPTIMAL}}, - {"neos-2987310-joes", {-607702988.29999995, objective_reference_status_t::OPTIMAL}}, - {"neos-3004026-krka", {0, objective_reference_status_t::OPTIMAL}}, - {"neos-3024952-loue", {26756, objective_reference_status_t::OPTIMAL}}, - {"neos-3046615-murg", {1600, objective_reference_status_t::OPTIMAL}}, - {"neos-3083819-nubu", {6307996, objective_reference_status_t::OPTIMAL}}, - {"neos-3216931-puriri", {71320, objective_reference_status_t::OPTIMAL}}, - {"neos-3381206-awhea", {453, objective_reference_status_t::OPTIMAL}}, - {"neos-3402294-bobin", {0.067249999999999491, objective_reference_status_t::OPTIMAL}}, - {"neos-3555904-turama", {-34.700000000000003, objective_reference_status_t::OPTIMAL}}, - {"neos-3627168-kasai", {988585.61999999976, objective_reference_status_t::OPTIMAL}}, - {"neos-3656078-kumeu", {-13172.200000000001, objective_reference_status_t::OPTIMAL}}, - {"neos-3754480-nidda", {12939.7540104743, objective_reference_status_t::OPTIMAL}}, - {"neos-4300652-rahue", {2.1415999999999999, objective_reference_status_t::OPTIMAL}}, - {"neos-4338804-snowy", {1471, objective_reference_status_t::OPTIMAL}}, - {"neos-4387871-tavua", {33.384729927000002, objective_reference_status_t::OPTIMAL}}, - {"neos-4413714-turia", {45.370167019999798, objective_reference_status_t::OPTIMAL}}, - {"neos-4532248-waihi", {61.599999999999987, objective_reference_status_t::OPTIMAL}}, - {"neos-4647030-tutaki", {27265.705999999958, objective_reference_status_t::OPTIMAL}}, - {"neos-4722843-widden", {25009.662227000001, objective_reference_status_t::OPTIMAL}}, - {"neos-4738912-atrato", {283627956.59500003, objective_reference_status_t::OPTIMAL}}, - {"neos-4763324-toguru", {1613.0388458499999, objective_reference_status_t::OPTIMAL}}, - {"neos-4954672-berkel", {2612710, objective_reference_status_t::OPTIMAL}}, - {"neos-5049753-cuanza", {561.99999716889999, objective_reference_status_t::OPTIMAL}}, - {"neos-5052403-cygnet", {182, objective_reference_status_t::OPTIMAL}}, - {"neos-5093327-huahum", {6259.9999971258949, objective_reference_status_t::OPTIMAL}}, - {"neos-5104907-jarama", {935, objective_reference_status_t::OPTIMAL}}, - {"neos-5107597-kakapo", {3644.9999999995198, objective_reference_status_t::OPTIMAL}}, - {"neos-5114902-kasavu", {655, objective_reference_status_t::OPTIMAL}}, - {"neos-5188808-nattai", {0.110283622999984, objective_reference_status_t::OPTIMAL}}, - {"neos-5195221-niemur", {0.0038354325999999999, objective_reference_status_t::OPTIMAL}}, - {"neos-631710", {203, objective_reference_status_t::OPTIMAL}}, - {"neos-662469", {184379.99999999991, objective_reference_status_t::OPTIMAL}}, - {"neos-787933", {30, objective_reference_status_t::OPTIMAL}}, - {"neos-827175", {112.00152, objective_reference_status_t::OPTIMAL}}, - {"neos-848589", {2351.40309999697, objective_reference_status_t::OPTIMAL}}, - {"neos-860300", {3200.9999999999982, objective_reference_status_t::OPTIMAL}}, - {"neos-873061", {113.6562385063, objective_reference_status_t::OPTIMAL}}, - {"neos-911970", {54.759999999999998, objective_reference_status_t::OPTIMAL}}, - {"neos-933966", {318, objective_reference_status_t::OPTIMAL}}, - {"neos-950242", {4, objective_reference_status_t::OPTIMAL}}, - {"neos-957323", {-237.75668150000001, objective_reference_status_t::OPTIMAL}}, - {"neos-960392", {-238, objective_reference_status_t::OPTIMAL}}, - {"neos17", {0.1500025774, objective_reference_status_t::OPTIMAL}}, - {"neos5", {15, objective_reference_status_t::OPTIMAL}}, - {"neos8", {-3719, objective_reference_status_t::OPTIMAL}}, - {"net12", {214, objective_reference_status_t::OPTIMAL}}, - {"netdiversion", {242, objective_reference_status_t::OPTIMAL}}, - {"nexp-150-20-8-5", {231, objective_reference_status_t::OPTIMAL}}, - {"ns1116954", {0, objective_reference_status_t::OPTIMAL}}, - {"ns1208400", {2, objective_reference_status_t::OPTIMAL}}, - {"ns1644855", {-1524.3333333333301, objective_reference_status_t::OPTIMAL}}, - {"ns1760995", {-549.21438505000003, objective_reference_status_t::OPTIMAL}}, - {"ns1830653", {20622, objective_reference_status_t::OPTIMAL}}, - {"ns1952667", {0, objective_reference_status_t::OPTIMAL}}, - {"nu25-pr12", {53904.999999999993, objective_reference_status_t::OPTIMAL}}, - {"nursesched-medium-hint03", {115, objective_reference_status_t::OPTIMAL}}, - {"nursesched-sprint02", {57.999999999999993, objective_reference_status_t::OPTIMAL}}, - {"nw04", {16862, objective_reference_status_t::OPTIMAL}}, - {"opm2-z10-s4", {-33269, objective_reference_status_t::OPTIMAL}}, - {"p200x1188c", {15078, objective_reference_status_t::OPTIMAL}}, - {"peg-solitaire-a3", {1, objective_reference_status_t::OPTIMAL}}, - {"pg", {-8674.3426071199992, objective_reference_status_t::OPTIMAL}}, - {"pg5_34", {-14339.353450000001, objective_reference_status_t::OPTIMAL}}, - {"physiciansched3-3", {2623271.3266670001, objective_reference_status_t::OPTIMAL}}, - {"physiciansched6-2", {49324, objective_reference_status_t::OPTIMAL}}, - {"piperout-08", {125054.9999999999, objective_reference_status_t::OPTIMAL}}, - {"piperout-27", {8123.9999999999727, objective_reference_status_t::OPTIMAL}}, - {"pk1", {11, objective_reference_status_t::OPTIMAL}}, - {"proteindesign121hz512p9", {1473, objective_reference_status_t::OPTIMAL}}, - {"proteindesign122trx11p8", {1747, objective_reference_status_t::OPTIMAL}}, - {"qap10", {339.99999999838712, objective_reference_status_t::OPTIMAL}}, - {"radiationm18-12-05", {17566, objective_reference_status_t::OPTIMAL}}, - {"radiationm40-10-02", {155328, objective_reference_status_t::OPTIMAL}}, - {"rail01", {-70.569964299999995, objective_reference_status_t::OPTIMAL}}, - {"rail02", {-200.44990770000001, objective_reference_status_t::OPTIMAL}}, - {"rail507", {174, objective_reference_status_t::OPTIMAL}}, - {"ran14x18-disj-8", {3712, objective_reference_status_t::OPTIMAL}}, - {"rd-rplusc-21", {165395.275295, objective_reference_status_t::OPTIMAL}}, - {"reblock115", {-36800603.233199999, objective_reference_status_t::OPTIMAL}}, - {"rmatr100-p10", {423, objective_reference_status_t::OPTIMAL}}, - {"rmatr200-p5", {4521, objective_reference_status_t::OPTIMAL}}, - {"roci-4-11", {-6020203, objective_reference_status_t::OPTIMAL}}, - {"rocii-5-11", {-6.6755047315380001, objective_reference_status_t::OPTIMAL}}, - {"rococob10-011000", {19449, objective_reference_status_t::OPTIMAL}}, - {"rocococ10-001000", {11460, objective_reference_status_t::OPTIMAL}}, - {"roi2alpha3n4", {-63.208495030000002, objective_reference_status_t::OPTIMAL}}, - {"roi5alpha10n8", {-52.322274350999997, objective_reference_status_t::OPTIMAL}}, - {"roll3000", {12889.999991999999, objective_reference_status_t::OPTIMAL}}, - {"s100", {-0.16972352705829999, objective_reference_status_t::OPTIMAL}}, - {"s250r10", {-0.17178048342319999, objective_reference_status_t::OPTIMAL}}, - {"satellites2-40", {-19, objective_reference_status_t::OPTIMAL}}, - {"satellites2-60-fs", {-19.000000000099998, objective_reference_status_t::OPTIMAL}}, - {"savsched1", {3217.6999999999998, objective_reference_status_t::OPTIMAL}}, - {"sct2", {-230.9891623, objective_reference_status_t::OPTIMAL}}, - {"seymour", {423, objective_reference_status_t::OPTIMAL}}, - {"seymour1", {410.76370138999999, objective_reference_status_t::OPTIMAL}}, - {"sing326", {7753674.8537600003, objective_reference_status_t::OPTIMAL}}, - {"sing44", {8128831.1771999998, objective_reference_status_t::OPTIMAL}}, - {"snp-02-004-104", {586803238.65672886, objective_reference_status_t::OPTIMAL}}, - {"sorrell3", {-16, objective_reference_status_t::OPTIMAL}}, - {"sp150x300d", {69, objective_reference_status_t::OPTIMAL}}, - {"sp97ar", {660705645.75899994, objective_reference_status_t::OPTIMAL}}, - {"sp98ar", {529740623.19999999, objective_reference_status_t::OPTIMAL}}, - {"splice1k1", {-394, objective_reference_status_t::OPTIMAL}}, - {"square41", {15, objective_reference_status_t::OPTIMAL}}, - {"square47", {15.9999999997877, objective_reference_status_t::OPTIMAL}}, - {"supportcase10", {7, objective_reference_status_t::OPTIMAL}}, - {"supportcase12", {-7559.5330538170001, objective_reference_status_t::OPTIMAL}}, - {"supportcase18", {48, objective_reference_status_t::OPTIMAL}}, - {"supportcase19", {12677205.999920519, objective_reference_status_t::OPTIMAL}}, - {"supportcase22", {110, objective_reference_status_t::BEST_KNOWN}}, - {"supportcase26", {1745.1238129999999, objective_reference_status_t::OPTIMAL}}, - {"supportcase33", {-345, objective_reference_status_t::OPTIMAL}}, - {"supportcase40", {24256.3122898, objective_reference_status_t::OPTIMAL}}, - {"supportcase42", {7.7586307222700004, objective_reference_status_t::OPTIMAL}}, - {"supportcase6", {51906.477370000001, objective_reference_status_t::OPTIMAL}}, - {"supportcase7", {-1132.2231770000001, objective_reference_status_t::OPTIMAL}}, - {"swath1", {379.07129574999999, objective_reference_status_t::OPTIMAL}}, - {"swath3", {397.76134365000001, objective_reference_status_t::OPTIMAL}}, - {"tbfp-network", {24.163194440000002, objective_reference_status_t::OPTIMAL}}, - {"thor50dday", {40417, objective_reference_status_t::OPTIMAL}}, - {"timtab1", {764771.99999977998, objective_reference_status_t::OPTIMAL}}, - {"tr12-30", {130595.9999999999, objective_reference_status_t::OPTIMAL}}, - {"traininstance2", {71820, objective_reference_status_t::OPTIMAL}}, - {"traininstance6", {28290, objective_reference_status_t::OPTIMAL}}, - {"trento1", {5189487, objective_reference_status_t::OPTIMAL}}, - {"triptim1", {22.868099999999899, objective_reference_status_t::OPTIMAL}}, - {"uccase12", {11507.4050616, objective_reference_status_t::OPTIMAL}}, - {"uccase9", {10993.131409, objective_reference_status_t::OPTIMAL}}, - {"uct-subprob", {314, objective_reference_status_t::OPTIMAL}}, - {"unitcal_7", {19635558.243999999, objective_reference_status_t::OPTIMAL}}, - {"var-smallemery-m6j6", {-149.37501, objective_reference_status_t::OPTIMAL}}, - {"wachplan", {-8, objective_reference_status_t::OPTIMAL}}, - }; - const auto normalized = normalize_problem_name(std::move(problem_name)); - const auto iter = k_objective_map.find(normalized); - if (iter == k_objective_map.end()) { return std::nullopt; } - return iter->second; -} - -} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 2b8b734c08..2c7bce06cd 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -15,7 +15,7 @@ * limitations under the License. */ -#define DEBUG_KNAPSACK_CONSTRAINTS 1 +#define DEBUG_KNAPSACK_CONSTRAINTS 0 #include "clique_table.cuh" From ceba8eb430d1005e634b47af07647688d0f003ce Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Mar 2026 04:58:02 -0700 Subject: [PATCH 137/147] cleanup --- cpp/src/mip_heuristics/solver.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 7cc15ffc34..2a9ce489e2 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -112,7 +112,7 @@ solution_t mip_solver_t::run_solver() f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : timer_.remaining_time(); - double presolve_time_limit = std::min(0.12 * time_limit, 70.0); + double presolve_time_limit = std::min(0.11 * time_limit, 70.0); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; From d939d6a04d5d2003352feef7de5a157117d89cca Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Mar 2026 05:19:45 -0700 Subject: [PATCH 138/147] fix test --- cpp/src/branch_and_bound/branch_and_bound.cpp | 2 -- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 1 - cpp/tests/mip/cuts_test.cu | 3 +++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 2215d48128..e81efbcace 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -2337,7 +2336,6 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); f_t abs_gap = upper_bound_.load() - root_objective_; if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { - report_cut_gap_closure_metric(); set_solution_at_root(solution, cut_info); set_final_solution(solution, root_objective_); return mip_status_t::OPTIMAL; diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index ba0e1ace6b..dd9bd2b2ab 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -8,7 +8,6 @@ #include "cuda_profiler_api.h" #include "diversity_manager.cuh" -#include #include #include #include diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index aec6b0850b..67baab5f12 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -723,6 +723,7 @@ mps_parser::mps_data_model_t append_literal_cut_prefix_to_lp_model( std::vector matrix_offsets = base_lp_model.get_constraint_matrix_offsets(); std::vector constraint_lbs = base_lp_model.get_constraint_lower_bounds(); std::vector constraint_ubs = base_lp_model.get_constraint_upper_bounds(); + std::vector row_names = base_lp_model.get_row_names(); if (matrix_offsets.empty()) { matrix_offsets.push_back(0); } const size_t cuts_to_apply = std::min(prefix_end_exclusive, dumped_cuts.size()); @@ -769,6 +770,7 @@ mps_parser::mps_data_model_t append_literal_cut_prefix_to_lp_model( matrix_offsets.push_back(static_cast(matrix_indices.size())); constraint_lbs.push_back(static_cast(num_complements - 1)); constraint_ubs.push_back(std::numeric_limits::infinity()); + row_names.push_back("literal_cut_" + std::to_string(cut_idx)); } model_with_cuts.set_csr_constraint_matrix(matrix_values.data(), @@ -779,6 +781,7 @@ mps_parser::mps_data_model_t append_literal_cut_prefix_to_lp_model( matrix_offsets.size()); model_with_cuts.set_constraint_lower_bounds(constraint_lbs.data(), constraint_lbs.size()); model_with_cuts.set_constraint_upper_bounds(constraint_ubs.data(), constraint_ubs.size()); + model_with_cuts.set_row_names(row_names); return model_with_cuts; } From cfe2fdbb5dcee1950211c870559c773761a0849e Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 07:58:56 -0700 Subject: [PATCH 139/147] cuts in backgroun --- cpp/src/branch_and_bound/branch_and_bound.cpp | 50 ++++++++++++-- cpp/src/branch_and_bound/branch_and_bound.hpp | 4 ++ cpp/src/cuts/cuts.cpp | 67 ++++++++----------- cpp/src/cuts/cuts.hpp | 27 +++++--- .../diversity/diversity_manager.cu | 37 +--------- .../presolve/conflict_graph/clique_table.cu | 52 ++++++++------ .../presolve/conflict_graph/clique_table.cuh | 4 +- 7 files changed, 133 insertions(+), 108 deletions(-) diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e81efbcace..41d23bc0ff 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -1969,6 +1970,31 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut root_relax_soln_.resize(original_lp_.num_rows, original_lp_.num_cols); + if (settings_.clique_cuts != 0 && clique_table_ == nullptr) { + signal_extend_cliques_.store(false, std::memory_order_release); + typename ::cuopt::linear_programming::mip_solver_settings_t::tolerances_t + tolerances_for_clique{}; + tolerances_for_clique.presolve_absolute_tolerance = settings_.primal_tol; + tolerances_for_clique.absolute_tolerance = settings_.primal_tol; + tolerances_for_clique.relative_tolerance = settings_.zero_tol; + tolerances_for_clique.integrality_tolerance = settings_.integer_tol; + tolerances_for_clique.absolute_mip_gap = settings_.absolute_mip_gap_tol; + tolerances_for_clique.relative_mip_gap = settings_.relative_mip_gap_tol; + auto* signal_ptr = &signal_extend_cliques_; + clique_table_future_ = + std::async(std::launch::async, + [this, + tolerances_for_clique, + signal_ptr]() -> std::shared_ptr> { + user_problem_t problem_copy = original_problem_; + cuopt::timer_t timer(std::numeric_limits::infinity()); + std::shared_ptr> table; + detail::find_initial_cliques( + problem_copy, tolerances_for_clique, &table, timer, false, signal_ptr); + return table; + }); + } + i_t original_rows = original_lp_.num_rows; simplex_solver_settings_t lp_settings = settings_; lp_settings.inside_mip = 1; @@ -2004,14 +2030,16 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut exploration_stats_.total_lp_iters = root_relax_soln_.iterations; exploration_stats_.total_lp_solve_time = toc(exploration_stats_.start_time); + auto finish_clique_thread = [this]() { + if (clique_table_future_.valid()) { + signal_extend_cliques_.store(true, std::memory_order_release); + clique_table_ = clique_table_future_.get(); + } + }; + if (root_status == lp_status_t::INFEASIBLE) { settings_.log.printf("MIP Infeasible\n"); - // FIXME: rarely dual simplex detects infeasible whereas it is feasible. - // to add a small safety net, check if there is a primal solution already. - // Uncomment this if the issue with cost266-UUE is resolved - // if (settings.heuristic_preemption_callback != nullptr) { - // settings.heuristic_preemption_callback(); - // } + finish_clique_thread(); return mip_status_t::INFEASIBLE; } if (root_status == lp_status_t::UNBOUNDED) { @@ -2019,23 +2047,27 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } + finish_clique_thread(); return mip_status_t::UNBOUNDED; } if (root_status == lp_status_t::TIME_LIMIT) { solver_status_ = mip_status_t::TIME_LIMIT; set_final_solution(solution, -inf); + finish_clique_thread(); return solver_status_; } if (root_status == lp_status_t::WORK_LIMIT) { solver_status_ = mip_status_t::WORK_LIMIT; set_final_solution(solution, -inf); + finish_clique_thread(); return solver_status_; } if (root_status == lp_status_t::NUMERICAL_ISSUES) { solver_status_ = mip_status_t::NUMERICAL; set_final_solution(solution, -inf); + finish_clique_thread(); return solver_status_; } @@ -2066,6 +2098,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (num_fractional == 0) { set_solution_at_root(solution, cut_info); + finish_clique_thread(); return mip_status_t::OPTIMAL; } @@ -2087,7 +2120,9 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut new_slacks_, var_types_, original_problem_, - clique_table_); + clique_table_, + &clique_table_future_, + &signal_extend_cliques_); std::vector saved_solution; #ifdef CHECK_CUTS_AGAINST_SAVED_SOLUTION @@ -2134,6 +2169,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut if (settings_.heuristic_preemption_callback != nullptr) { settings_.heuristic_preemption_callback(); } + finish_clique_thread(); return mip_status_t::INFEASIBLE; } f_t cut_generation_time = toc(cut_start_time); diff --git a/cpp/src/branch_and_bound/branch_and_bound.hpp b/cpp/src/branch_and_bound/branch_and_bound.hpp index 88ba23ca18..f44142f37f 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.hpp +++ b/cpp/src/branch_and_bound/branch_and_bound.hpp @@ -32,7 +32,9 @@ #include +#include #include +#include #include #include @@ -147,6 +149,8 @@ class branch_and_bound_t { const user_problem_t& original_problem_; const simplex_solver_settings_t settings_; std::shared_ptr> clique_table_; + std::future>> clique_table_future_; + std::atomic signal_extend_cliques_{false}; work_limit_context_t work_unit_context_{"B&B"}; diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index bceb4ca205..4d9e07c9a6 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1061,7 +1061,17 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, } } - // Generate Clique cuts + // Generate MIR and CG cuts + if (settings.mir_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { + f_t cut_start_time = tic(); + generate_mir_cuts(lp, settings, Arow, new_slacks, var_types, xstar); + f_t cut_generation_time = toc(cut_start_time); + if (cut_generation_time > 1.0) { + settings.log.debug("MIR and CG cut generation time %.2f seconds\n", cut_generation_time); + } + } + + // Generate Clique cuts (last to give background clique table generation maximum time) if (settings.clique_cuts != 0) { f_t cut_start_time = tic(); bool feasible = generate_clique_cuts(lp, settings, var_types, xstar, reduced_costs, start_time); @@ -1074,16 +1084,6 @@ bool cut_generation_t::generate_cuts(const lp_problem_t& lp, settings.log.debug("Clique cut generation time %.2f seconds\n", cut_generation_time); } } - - // Generate MIR and CG cuts - if (settings.mir_cuts != 0 || settings.strong_chvatal_gomory_cuts != 0) { - f_t cut_start_time = tic(); - generate_mir_cuts(lp, settings, Arow, new_slacks, var_types, xstar); - f_t cut_generation_time = toc(cut_start_time); - if (cut_generation_time > 1.0) { - settings.log.debug("MIR and CG cut generation time %.2f seconds\n", cut_generation_time); - } - } return true; } @@ -1125,35 +1125,26 @@ bool cut_generation_t::generate_clique_cuts( static_cast(settings.time_limit), static_cast(toc(start_time))); + if (clique_table_ == nullptr && clique_table_future_ != nullptr && + clique_table_future_->valid()) { + CLIQUE_CUTS_DEBUG("generate_clique_cuts signaling background thread and waiting"); + if (signal_extend_) { signal_extend_->store(true, std::memory_order_release); } + clique_table_ = clique_table_future_->get(); + clique_table_future_ = nullptr; + if (clique_table_) { + CLIQUE_CUTS_DEBUG("generate_clique_cuts received clique table first=%lld addtl=%lld", + static_cast(clique_table_->first.size()), + static_cast(clique_table_->addtl_cliques.size())); + } + } + if (clique_table_ == nullptr) { - CLIQUE_CUTS_DEBUG("generate_clique_cuts building clique table"); - detail::clique_config_t clique_config; - clique_config.min_clique_size = 2; - clique_table_ = std::make_shared>( - 2 * num_vars, clique_config.min_clique_size, clique_config.max_clique_size_for_extension); - - typename ::cuopt::linear_programming::mip_solver_settings_t::tolerances_t tolerances; - tolerances.presolve_absolute_tolerance = settings.primal_tol; - tolerances.absolute_tolerance = settings.primal_tol; - tolerances.relative_tolerance = settings.zero_tol; - tolerances.integrality_tolerance = settings.integer_tol; - tolerances.absolute_mip_gap = settings.absolute_mip_gap_tol; - tolerances.relative_mip_gap = settings.relative_mip_gap_tol; - - const f_t remaining_time = - std::max(static_cast(0.), settings.time_limit - toc(start_time)); - cuopt::timer_t clique_build_timer(static_cast(remaining_time)); - detail::build_clique_table( - user_problem_, *clique_table_, tolerances, true, true, clique_build_timer); - if (clique_build_timer.check_time_limit()) { return true; } - CLIQUE_CUTS_DEBUG("generate_clique_cuts clique table built first=%lld addtl=%lld", - static_cast(clique_table_->first.size()), - static_cast(clique_table_->addtl_cliques.size())); - } else { - CLIQUE_CUTS_DEBUG("generate_clique_cuts reusing clique table first=%lld addtl=%lld", - static_cast(clique_table_->first.size()), - static_cast(clique_table_->addtl_cliques.size())); + CLIQUE_CUTS_DEBUG("generate_clique_cuts no clique table available, skipping"); + return true; } + CLIQUE_CUTS_DEBUG("generate_clique_cuts using clique table first=%lld addtl=%lld", + static_cast(clique_table_->first.size()), + static_cast(clique_table_->addtl_cliques.size())); if (clique_table_->first.empty() && clique_table_->addtl_cliques.empty()) { CLIQUE_CUTS_DEBUG("generate_clique_cuts empty clique table, nothing to separate"); diff --git a/cpp/src/cuts/cuts.hpp b/cpp/src/cuts/cuts.hpp index 840572f9b3..fffa4b10fe 100644 --- a/cpp/src/cuts/cuts.hpp +++ b/cpp/src/cuts/cuts.hpp @@ -15,6 +15,8 @@ #include #include +#include +#include #include #include #include @@ -272,18 +274,23 @@ class mixed_integer_rounding_cut_t; template class cut_generation_t { public: - cut_generation_t(cut_pool_t& cut_pool, - const lp_problem_t& lp, - const simplex_solver_settings_t& settings, - csr_matrix_t& Arow, - const std::vector& new_slacks, - const std::vector& var_types, - const user_problem_t& user_problem, - std::shared_ptr> clique_table = nullptr) + cut_generation_t( + cut_pool_t& cut_pool, + const lp_problem_t& lp, + const simplex_solver_settings_t& settings, + csr_matrix_t& Arow, + const std::vector& new_slacks, + const std::vector& var_types, + const user_problem_t& user_problem, + std::shared_ptr> clique_table = nullptr, + std::future>>* clique_table_future = nullptr, + std::atomic* signal_extend = nullptr) : cut_pool_(cut_pool), knapsack_generation_(lp, settings, Arow, new_slacks, var_types), user_problem_(user_problem), - clique_table_(std::move(clique_table)) + clique_table_(std::move(clique_table)), + clique_table_future_(clique_table_future), + signal_extend_(signal_extend) { } @@ -339,6 +346,8 @@ class cut_generation_t { knapsack_generation_t knapsack_generation_; const user_problem_t& user_problem_; std::shared_ptr> clique_table_; + std::future>>* clique_table_future_{nullptr}; + std::atomic* signal_extend_{nullptr}; }; template diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index dd9bd2b2ab..e07b70c881 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -9,7 +9,7 @@ #include "diversity_manager.cuh" #include -#include + #include #include #include @@ -208,39 +208,8 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && - !problem_ptr->empty) { - // TODO this is just CPU time and blocking the GPU time. - // execute this in parallel with something else. - f_t time_limit_for_clique_table = std::min(10., presolve_timer.remaining_time()); - // if it is deterministic run until the end - if (context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC) { - time_limit_for_clique_table = std::numeric_limits::infinity(); - } - timer_t clique_timer(time_limit_for_clique_table); - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - auto clique_table_ptr = context.settings.clique_cuts != 0 ? &clique_table : nullptr; - constexpr bool modify_problem_with_cliques = false; - find_initial_cliques(host_problem, - context.settings.tolerances, - clique_table_ptr, - clique_timer, - modify_problem_with_cliques); - if (modify_problem_with_cliques) { - problem_ptr->set_constraints_from_host_user_problem(host_problem); - cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - "host lower bound size mismatch"); - cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - "host upper bound size mismatch"); - std::vector all_var_indices(problem_ptr->n_variables); - std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - trivial_presolve(*problem_ptr, remap_cache_ids); - } - if (clique_table_ptr != nullptr) { problem_ptr->clique_table = std::move(clique_table); } - } + // Clique table generation is now launched asynchronously in branch_and_bound::solve() + // to run concurrently with the root LP relaxation. // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 2c7bce06cd..986e182b99 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -801,12 +801,17 @@ i_t extend_cliques(const std::vector>& knapsack_ dual_simplex::user_problem_t& problem, dual_simplex::csr_matrix_t& A, bool modify_problem, - cuopt::timer_t& timer) + cuopt::timer_t& timer, + double* work_estimate_out, + double max_work_estimate) { constexpr i_t min_extension_gain = 2; constexpr i_t extension_yield_window = 64; constexpr i_t min_successes_per_window = 1; + double local_work = 0.0; + double& work = work_estimate_out ? *work_estimate_out : local_work; + i_t base_rows = A.m; i_t base_nnz = A.row_start[A.m]; i_t max_added_rows = std::max(8, base_rows / 50); @@ -841,7 +846,9 @@ i_t extend_cliques(const std::vector>& knapsack_ signature += static_cast(v); } sp_sigs.push_back({knapsack_idx, static_cast(cstr_vars[knapsack_idx].size()), signature}); + work += cstr_vars[knapsack_idx].size(); } + if (work > max_work_estimate) { return 0; } std::sort(sp_sigs.begin(), sp_sigs.end(), compare_clique_sig); std::vector original_to_current_row_idx(problem.row_sense.size(), -1); for (i_t row_idx = 0; row_idx < static_cast(original_to_current_row_idx.size()); row_idx++) { @@ -852,6 +859,7 @@ i_t extend_cliques(const std::vector>& knapsack_ for (i_t knapsack_idx = 0; knapsack_idx < static_cast(knapsack_constraints.size()); knapsack_idx++) { if (timer.check_time_limit()) { break; } + if (work > max_work_estimate) { break; } const auto& knapsack_constraint = knapsack_constraints[knapsack_idx]; if (!knapsack_constraint.is_set_packing) { continue; } i_t clique_size = static_cast(knapsack_constraint.entries.size()); @@ -860,19 +868,19 @@ i_t extend_cliques(const std::vector>& knapsack_ for (const auto& entry : knapsack_constraint.entries) { smallest_degree = std::min(smallest_degree, clique_table.get_degree_of_var(entry.col)); } - // The smallest-degree vertex upper-bounds how many new literals can be added. i_t estimated_gain = std::max(0, smallest_degree - (clique_size - 1)); if (estimated_gain < min_extension_gain) { continue; } extension_worklist.push_back({knapsack_idx, estimated_gain, clique_size}); + work += knapsack_constraint.entries.size(); } std::stable_sort( extension_worklist.begin(), extension_worklist.end(), compare_extension_candidate); CUOPT_LOG_DEBUG("Clique extension candidates after scoring: %zu", extension_worklist.size()); i_t n_extended_cliques = 0; - // Try highest estimated gain candidates first so budget is spent on promising rows. for (const auto& candidate : extension_worklist) { if (timer.check_time_limit()) { break; } + if (work > max_work_estimate) { break; } if (added_rows >= max_added_rows || added_nnz >= max_added_nnz) { CUOPT_LOG_DEBUG( "Stopping clique extension: budget reached (rows=%d nnz=%d)", added_rows, added_nnz); @@ -896,6 +904,7 @@ i_t extend_cliques(const std::vector>& knapsack_ max_added_rows - added_rows, max_added_nnz - added_nnz, inserted_row_nnz); + work += clique.size() * clique.size(); if (extended_clique) { n_extended_cliques++; i_t replacement_row_nnz = 0; @@ -1033,7 +1042,8 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, std::shared_ptr>* clique_table_out, cuopt::timer_t& timer, - bool modify_problem) + bool modify_problem, + std::atomic* signal_extend) { cuopt::timer_t stage_timer(std::numeric_limits::infinity()); #ifdef DEBUG_CLIQUE_TABLE @@ -1063,8 +1073,6 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, #ifdef DEBUG_CLIQUE_TABLE t_sort = stage_timer.elapsed_time(); #endif - // print_knapsack_constraints(knapsack_constraints); - // TODO think about getting min_clique_size according to some problem property clique_config_t clique_config; std::shared_ptr> clique_table_shared; clique_table_t clique_table_local(2 * problem.num_cols, @@ -1078,45 +1086,48 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, clique_config.max_clique_size_for_extension); clique_table_ptr = clique_table_shared.get(); } - clique_table_ptr->tolerances = tolerances; - // spend maximum half of the time for additional cliques - // the other half for extending them + clique_table_ptr->tolerances = tolerances; double time_limit_for_additional_cliques = timer.remaining_time() / 2; cuopt::timer_t additional_cliques_timer(time_limit_for_additional_cliques); + double find_work_estimate = 0.0; for (const auto& knapsack_constraint : knapsack_constraints) { if (timer.check_time_limit()) { break; } + if (signal_extend && signal_extend->load(std::memory_order_acquire)) { break; } find_cliques_from_constraint(knapsack_constraint, *clique_table_ptr, additional_cliques_timer); + find_work_estimate += knapsack_constraint.entries.size(); } #ifdef DEBUG_CLIQUE_TABLE t_find = stage_timer.elapsed_time(); #endif - CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d", + CUOPT_LOG_DEBUG("Number of cliques: %d, additional cliques: %d, find_work=%.0f", clique_table_ptr->first.size(), - clique_table_ptr->addtl_cliques.size()); - // print_clique_table(clique_table); - // remove small cliques and add them to adj_list + clique_table_ptr->addtl_cliques.size(), + find_work_estimate); remove_small_cliques(*clique_table_ptr, timer); #ifdef DEBUG_CLIQUE_TABLE t_small = stage_timer.elapsed_time(); #endif - // fill var clique maps fill_var_clique_maps(*clique_table_ptr); #ifdef DEBUG_CLIQUE_TABLE t_maps = stage_timer.elapsed_time(); #endif if (clique_table_out != nullptr) { *clique_table_out = std::move(clique_table_shared); } - i_t n_extended_cliques = extend_cliques(knapsack_constraints, + double extend_work = 0.0; + constexpr double max_extend_work = 2e9; + i_t n_extended_cliques = extend_cliques(knapsack_constraints, set_packing_constraints, *clique_table_ptr, problem, A, modify_problem, - timer); + timer, + &extend_work, + max_extend_work); #ifdef DEBUG_CLIQUE_TABLE t_extend = stage_timer.elapsed_time(); CUOPT_LOG_DEBUG( "Clique table timing (s): fill=%.6f coeff=%.6f sort=%.6f find=%.6f small=%.6f maps=%.6f " - "extend=%.6f total=%.6f", + "extend=%.6f total=%.6f find_work=%.0f extend_work=%.0f", t_fill, t_coeff - t_fill, t_sort - t_coeff, @@ -1124,7 +1135,9 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, t_small - t_find, t_maps - t_small, t_extend - t_maps, - t_extend); + t_extend, + find_work_estimate, + extend_work); #endif } @@ -1134,7 +1147,8 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, \ std::shared_ptr> * clique_table_out, \ cuopt::timer_t & timer, \ - bool modify_problem); \ + bool modify_problem, \ + std::atomic* signal_extend); \ template void build_clique_table( \ const dual_simplex::user_problem_t& problem, \ clique_table_t& clique_table, \ diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh index a56218de4e..944241b4f0 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -103,7 +104,8 @@ void find_initial_cliques(dual_simplex::user_problem_t& problem, typename mip_solver_settings_t::tolerances_t tolerances, std::shared_ptr>* clique_table_out, cuopt::timer_t& timer, - bool modify_problem); + bool modify_problem, + std::atomic* signal_extend = nullptr); template void build_clique_table(const dual_simplex::user_problem_t& problem, From 5e4ee563d5e439ecef03f6547e460ea16cb926de Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 10:27:51 -0700 Subject: [PATCH 140/147] lower work estimates and reduce the probing cache time --- cpp/src/cuts/cuts.cpp | 2 +- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 2 +- cpp/src/mip_heuristics/solver.cu | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/cuts/cuts.cpp b/cpp/src/cuts/cuts.cpp index 4d9e07c9a6..f4b3106112 100644 --- a/cpp/src/cuts/cuts.cpp +++ b/cpp/src/cuts/cuts.cpp @@ -1160,7 +1160,7 @@ bool cut_generation_t::generate_clique_cuts( // TODO this can be problem dependent const i_t max_calls = 100000; f_t work_estimate = 0.0; - const f_t max_work_estimate = 2e9; + const f_t max_work_estimate = 1e8; cuopt_assert(user_problem_.var_types.size() == static_cast(num_vars), "User problem var_types size mismatch"); diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index e07b70c881..e1bb0bca9c 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -198,7 +198,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ if (run_probing_cache) { // Run probing cache before trivial presolve to discover variable implications const f_t max_time_on_probing = diversity_config.max_time_on_probing; - f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit * 0.9); + f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit); timer_t probing_timer{time_for_probing_cache}; // this function computes probing cache, finds singletons, substitutions and changes the problem bool problem_is_infeasible = diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index 2a9ce489e2..da6c7c0b81 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -112,7 +112,7 @@ solution_t mip_solver_t::run_solver() f_t time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : timer_.remaining_time(); - double presolve_time_limit = std::min(0.11 * time_limit, 70.0); + double presolve_time_limit = std::min(0.1 * time_limit, 60.0); presolve_time_limit = context.settings.determinism_mode == CUOPT_MODE_DETERMINISTIC ? std::numeric_limits::infinity() : presolve_time_limit; From 2472f335887e9b2126bbe417f202aa6991160c2e Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 12:37:10 -0700 Subject: [PATCH 141/147] remove cuts in submip and rins --- cpp/src/mip_heuristics/diversity/lns/rins.cu | 1 + .../diversity/recombiners/sub_mip.cuh | 1 + .../presolve/conflict_graph/clique_table.cu | 17 ----------------- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/lns/rins.cu b/cpp/src/mip_heuristics/diversity/lns/rins.cu index 2fbe79ba34..d7d7601014 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cu +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cu @@ -266,6 +266,7 @@ void rins_t::run_rins() branch_and_bound_settings.num_threads = 1; branch_and_bound_settings.reliability_branching = 0; branch_and_bound_settings.max_cut_passes = 0; + branch_and_bound_settings.clique_cuts = 0; branch_and_bound_settings.sub_mip = 1; branch_and_bound_settings.log.log = false; branch_and_bound_settings.log.log_prefix = "[RINS] "; diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index b2f7f80066..a867141d0a 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -108,6 +108,7 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_settings.num_threads = 1; branch_and_bound_settings.reliability_branching = 0; branch_and_bound_settings.max_cut_passes = 0; + branch_and_bound_settings.clique_cuts = 0; branch_and_bound_settings.sub_mip = 1; branch_and_bound_settings.solution_callback = [this](std::vector& solution, f_t objective) { diff --git a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu index 986e182b99..70855267df 100644 --- a/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu +++ b/cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu @@ -41,20 +41,6 @@ void find_cliques_from_constraint(const knapsack_constraint_t& kc, cuopt_assert(size > 1, "Constraint has not enough variables"); if (kc.entries[size - 1].val + kc.entries[size - 2].val <= kc.rhs) { return; } - // For set packing constraints all variables are pairwise conflicting. - // Materialize edges directly into adj_list to avoid a large entry in 'first' - // and the downstream overhead of clique maps / position lookups. - constexpr i_t max_set_packing_adj_list_size = 100; - if (kc.is_set_packing && size <= max_set_packing_adj_list_size) { - for (i_t i = 0; i < size; i++) { - for (i_t j = i + 1; j < size; j++) { - clique_table.adj_list_small_cliques[kc.entries[i].col].insert(kc.entries[j].col); - clique_table.adj_list_small_cliques[kc.entries[j].col].insert(kc.entries[i].col); - } - } - return; - } - std::vector clique; i_t k = size - 1; // find the first clique, which is the largest @@ -229,8 +215,6 @@ void fill_knapsack_constraints(const dual_simplex::user_problem_t& pro template void remove_small_cliques(clique_table_t& clique_table, cuopt::timer_t& timer) { - if (timer.check_time_limit()) { return; } - i_t num_removed_first = 0; i_t num_removed_addtl = 0; std::vector to_delete(clique_table.first.size(), false); @@ -250,7 +234,6 @@ void remove_small_cliques(clique_table_t& clique_table, cuopt::timer_t } } for (size_t addtl_c = 0; addtl_c < clique_table.addtl_cliques.size(); addtl_c++) { - if (timer.check_time_limit()) { return; } const auto& addtl_clique = clique_table.addtl_cliques[addtl_c]; const auto base_clique_idx = static_cast(addtl_clique.clique_idx); cuopt_assert(base_clique_idx < to_delete.size(), From dc9a762cc36f3bfb284188b638680f6a31814713 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 12:57:23 -0700 Subject: [PATCH 142/147] without hyper threading --- benchmarks/linear_programming/cuopt/run_mip.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index e11c57941f..e9aac7a817 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -417,7 +417,16 @@ int main(int argc, char* argv[]) int reliability_branching = program.get("--reliability-branching"); bool deterministic = program.get("--determinism"); - if (num_cpu_threads < 0) { num_cpu_threads = omp_get_max_threads() / n_gpus; } + if (num_cpu_threads < 0) { + num_cpu_threads = omp_get_max_threads() / n_gpus; + std::ifstream smt_file("/sys/devices/system/cpu/smt/active"); + if (smt_file.is_open()) { + int smt_active = 0; + smt_file >> smt_active; + if (smt_active) { num_cpu_threads /= 2; } + } + num_cpu_threads = std::max(num_cpu_threads, 1); + } if (program.is_used("--out-dir")) { out_dir = program.get("--out-dir"); From 6dd44b5eb6c6dec45b452f1b07f181232a3cfa98 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 13:59:15 -0700 Subject: [PATCH 143/147] without clique cuts and modify problem --- .../linear_programming/cuopt/run_mip.cpp | 13 +++++----- .../diversity/diversity_manager.cu | 24 +++++++++++++++++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index e9aac7a817..9b6a1f80b4 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -213,6 +213,7 @@ int run_single_file(std::string file_path, settings.tolerances.absolute_tolerance = 1e-6; settings.presolver = cuopt::linear_programming::presolver_t::Default; settings.reliability_branching = reliability_branching; + settings.clique_cuts = 0; settings.seed = 42; cuopt::linear_programming::benchmark_info_t benchmark_info; settings.benchmark_info_ptr = &benchmark_info; @@ -419,12 +420,12 @@ int main(int argc, char* argv[]) if (num_cpu_threads < 0) { num_cpu_threads = omp_get_max_threads() / n_gpus; - std::ifstream smt_file("/sys/devices/system/cpu/smt/active"); - if (smt_file.is_open()) { - int smt_active = 0; - smt_file >> smt_active; - if (smt_active) { num_cpu_threads /= 2; } - } + // std::ifstream smt_file("/sys/devices/system/cpu/smt/active"); + // if (smt_file.is_open()) { + // int smt_active = 0; + // smt_file >> smt_active; + // if (smt_active) { num_cpu_threads /= 2; } + // } num_cpu_threads = std::max(num_cpu_threads, 1); } diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index e1bb0bca9c..89c3437bd2 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -208,8 +208,28 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - // Clique table generation is now launched asynchronously in branch_and_bound::solve() - // to run concurrently with the root LP relaxation. + if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && + !problem_ptr->empty) { + f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); + timer_t clique_timer(time_limit_for_clique_table); + dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + problem_ptr->get_host_user_problem(host_problem); + std::shared_ptr> clique_table; + constexpr bool modify_problem_with_cliques = true; + find_initial_cliques( + host_problem, context.settings.tolerances, clique_timer, modify_problem_with_cliques); + if (modify_problem_with_cliques) { + problem_ptr->set_constraints_from_host_user_problem(host_problem); + cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + "host lower bound size mismatch"); + cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + "host upper bound size mismatch"); + std::vector all_var_indices(problem_ptr->n_variables); + std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); + trivial_presolve(*problem_ptr, remap_cache_ids); + } + } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) { From 64e78dbcb8ae8c46277c99ec60cc7150740f6ba8 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 14:36:02 -0700 Subject: [PATCH 144/147] fix initial cliques --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 89c3437bd2..d019c66c8b 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -10,6 +10,7 @@ #include +#include #include #include #include @@ -216,8 +217,12 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ problem_ptr->get_host_user_problem(host_problem); std::shared_ptr> clique_table; constexpr bool modify_problem_with_cliques = true; - find_initial_cliques( - host_problem, context.settings.tolerances, clique_timer, modify_problem_with_cliques); + find_initial_cliques(host_problem, + context.settings.tolerances, + &clique_table, + clique_timer, + modify_problem_with_cliques, + nullptr); if (modify_problem_with_cliques) { problem_ptr->set_constraints_from_host_user_problem(host_problem); cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), From 74678e5fea5d13bcc5bfcb8d72cb8782208d3fc6 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 14:36:58 -0700 Subject: [PATCH 145/147] with clique cuts --- benchmarks/linear_programming/cuopt/run_mip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 9b6a1f80b4..e01e533a65 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -213,7 +213,7 @@ int run_single_file(std::string file_path, settings.tolerances.absolute_tolerance = 1e-6; settings.presolver = cuopt::linear_programming::presolver_t::Default; settings.reliability_branching = reliability_branching; - settings.clique_cuts = 0; + settings.clique_cuts = -1; settings.seed = 42; cuopt::linear_programming::benchmark_info_t benchmark_info; settings.benchmark_info_ptr = &benchmark_info; From be217bd4c030a1160788eb536775d12dbbeee58b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 16:14:19 -0700 Subject: [PATCH 146/147] without modify problem --- cpp/src/mip_heuristics/diversity/diversity_manager.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index d019c66c8b..9ff837479d 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -216,7 +216,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); problem_ptr->get_host_user_problem(host_problem); std::shared_ptr> clique_table; - constexpr bool modify_problem_with_cliques = true; + constexpr bool modify_problem_with_cliques = false; find_initial_cliques(host_problem, context.settings.tolerances, &clique_table, From 32c7a7a3091a16498a7c508774e4a61980fdc569 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 10 Mar 2026 16:51:55 -0700 Subject: [PATCH 147/147] disable initial cliques --- .../diversity/diversity_manager.cu | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 9ff837479d..0ded8337d8 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -209,32 +209,32 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ const bool remap_cache_ids = true; if (!global_timer.check_time_limit()) { trivial_presolve(*problem_ptr, remap_cache_ids); } if (!problem_ptr->empty && !check_bounds_sanity(*problem_ptr)) { return false; } - if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && - !problem_ptr->empty) { - f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); - timer_t clique_timer(time_limit_for_clique_table); - dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); - problem_ptr->get_host_user_problem(host_problem); - std::shared_ptr> clique_table; - constexpr bool modify_problem_with_cliques = false; - find_initial_cliques(host_problem, - context.settings.tolerances, - &clique_table, - clique_timer, - modify_problem_with_cliques, - nullptr); - if (modify_problem_with_cliques) { - problem_ptr->set_constraints_from_host_user_problem(host_problem); - cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), - "host lower bound size mismatch"); - cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), - "host upper bound size mismatch"); - std::vector all_var_indices(problem_ptr->n_variables); - std::iota(all_var_indices.begin(), all_var_indices.end(), 0); - problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, host_problem.upper); - trivial_presolve(*problem_ptr, remap_cache_ids); - } - } + // if (!presolve_timer.check_time_limit() && !context.settings.heuristics_only && + // !problem_ptr->empty) { + // f_t time_limit_for_clique_table = std::min(3., presolve_timer.remaining_time() / 5); + // timer_t clique_timer(time_limit_for_clique_table); + // dual_simplex::user_problem_t host_problem(problem_ptr->handle_ptr); + // problem_ptr->get_host_user_problem(host_problem); + // std::shared_ptr> clique_table; + // constexpr bool modify_problem_with_cliques = false; + // find_initial_cliques(host_problem, + // context.settings.tolerances, + // &clique_table, + // clique_timer, + // modify_problem_with_cliques, + // nullptr); + // if (modify_problem_with_cliques) { + // problem_ptr->set_constraints_from_host_user_problem(host_problem); + // cuopt_assert(host_problem.lower.size() == static_cast(problem_ptr->n_variables), + // "host lower bound size mismatch"); + // cuopt_assert(host_problem.upper.size() == static_cast(problem_ptr->n_variables), + // "host upper bound size mismatch"); + // std::vector all_var_indices(problem_ptr->n_variables); + // std::iota(all_var_indices.begin(), all_var_indices.end(), 0); + // problem_ptr->update_variable_bounds(all_var_indices, host_problem.lower, + // host_problem.upper); trivial_presolve(*problem_ptr, remap_cache_ids); + // } + // } // May overconstrain if Papilo presolve has been run before if (context.settings.presolver == presolver_t::None) { if (!problem_ptr->empty) {