From 594ec3f42556c76bf6e3af0e959cda18febe061b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 31 Oct 2025 04:40:56 -0700 Subject: [PATCH 01/54] 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 000000000..c02d64d82 --- /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 000000000..73c4e704d --- /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 02/54] 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 c02d64d82..cd4d12e78 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 73c4e704d..e3b4f0015 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 755c18b0b..dfae17eb3 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 03/54] 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 e3b4f0015..e9a3abff2 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 04/54] 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 70c6ea0cc..b7493af5d 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 427f1049d..45471d5db 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 a3db2f8eb..40ca7aaf2 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 2d11a8dcb..aeeeeb243 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 000000000..09e5c2b0e --- /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 e9a3abff2..a1e7036b1 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 cd4d12e78..000000000 --- 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 0114882b0..7e0b10637 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 05/54] 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 09e5c2b0e..cd8dddb2d 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 a1e7036b1..3523f9896 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 91836dd10..c239f39a4 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 7e0b10637..f25e8cd98 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 06/54] 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 cd8dddb2d..cdce28690 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 07/54] 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 cdce28690..73b5c21df 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 3523f9896..6c7be483b 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 08/54] 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 b4fda11f5..395b0f995 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 4397e7068..e8b440a12 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 9823279f2..6b3b4553a 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 73b5c21df..9b3090775 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 6c7be483b..22625b208 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 09/54] 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 6b3b4553a..a56966224 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 9b3090775..a189a47ac 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 10/54] 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 395b0f995..1bafac04e 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 a189a47ac..9920ec779 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 11/54] 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 9920ec779..76cbc4570 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 12/54] 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 76cbc4570..f6b205509 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 fc2d974e3..cdd31147d 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 3acd16f31..4420e9392 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 9719f0b54..0e3b7a627 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 2a082d242..d325e0e37 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 13/54] 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 f6b205509..8c00c6fb7 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 14/54] 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 cfe9876de..26452ea15 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 d325e0e37..367041c78 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 15/54] 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 7cd04f8ed..8578ffae7 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 8c00c6fb7..9cadfd279 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 16/54] 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 9cadfd279..d69802c9a 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 17/54] 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 c48db84e6..924065ba1 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 a52dd176e..9e011ddd7 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 22625b208..a867bd833 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 12bebcd03..1dede408c 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 18/54] 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 d69802c9a..d31845c71 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 19/54] 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 d31845c71..0882d9a45 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 a867bd833..2c40d51eb 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 20/54] 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 0882d9a45..28cf10efa 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 21/54] 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 28cf10efa..de86276f6 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 22/54] 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 de86276f6..50ec07700 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 23/54] 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 26452ea15..4b9f16414 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 ecd277065..13f701249 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 50ec07700..06fd71eda 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 2c40d51eb..5bc8a6638 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 24/54] 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 13f701249..76e852f00 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 06fd71eda..85b81b8a8 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 5bc8a6638..cead4c5e8 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 25/54] 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 85b81b8a8..ea04f6e59 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 26/54] 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 d9f2195c4..1a11fec8e 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 ea04f6e59..0d536e694 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 27/54] 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 1a11fec8e..d9f2195c4 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 cb1099291cfbce91d8a2e8f6accee2436cd2d381 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Feb 2026 02:32:58 -0800 Subject: [PATCH 28/54] 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 d9f2195c4..ca756ebcc 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 0d536e694..e2c12b31c 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 cead4c5e8..3beb1b880 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 9ee22c47f..555da8504 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 933beb0814725b011d9c16d5cbc81f1f255af200 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Mon, 9 Feb 2026 06:22:40 -0800 Subject: [PATCH 29/54] 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 e2c12b31c..398ee0df0 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 30/54] 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 398ee0df0..d2e7b6edb 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 31/54] 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 d2e7b6edb..0dd24cc7f 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 32/54] 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 0dd24cc7f..912d61f6c 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 b758b9f3424e6599987b93a140e2513c9ea76a9b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Wed, 11 Feb 2026 04:12:07 -0800 Subject: [PATCH 33/54] 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 e74f69d13..176ff719d 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 0bb4e1744..ced10be89 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 a4a36d75b..17f5e032a 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 de8994e80..40949391e 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 21fa93bfa..201cf3b62 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 3fd05d97f..77a26cff7 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 0392a6247a4402e0fca3ce3c82e97bff9f6456ae Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Wed, 11 Feb 2026 15:30:43 +0000 Subject: [PATCH 34/54] 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 b5c35f510..f790ae16d 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 af7b7c1c3..aa377b315 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 35/54] disable jobserver unless explicitely requested --- build.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build.sh b/build.sh index f790ae16d..927acd664 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 36/54] better workaround fix build --- build.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/build.sh b/build.sh index 927acd664..b5c35f510 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 7de08d2dfd124bf5fba7faa3ea391c3cf08c85f8 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 02:45:54 -0800 Subject: [PATCH 37/54] 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 842845b0d..bd06dd3a0 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 aae893b22..2d98badb8 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 17f997f4a..9a8cfc143 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 59b4725e4..13227a832 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 71dce2e39..5f8ef4934 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 d9783053f..6fd7b13f3 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 597628e73..65da8d45c 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 1e057a16b..25d1c5fbe 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 8d4a533e9..f4602ac56 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 98f5f4193..2d0944542 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 cb9834705..4c55e1f19 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 179fff01b..3f83da42f 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 1e825b5e4..42f99a629 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 9de33ed3b..ea46a1f67 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 38/54] 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 bd06dd3a0..33a67acbf 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 2d98badb8..724233b1f 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 17f5e032a..96df6240b 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 39/54] 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 33a67acbf..6624e42e8 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 84ee986c4..a13d5cedc 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 40/54] 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 dfce00558..55013e3b9 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 dc01133c8..dcc1bf183 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 912d61f6c..2c421fc51 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 da8289b36..b3d452b86 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 b1675f259..159de75a8 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 69fbb17bb..97dfe0957 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 f3956725d9a30a9a29156f0d46c2887b2b0e4ff0 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 06:30:27 -0800 Subject: [PATCH 41/54] 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 2c421fc51..5d93ceac4 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 bc12fb360..31916e2c3 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 71b7f2f41247b6b0bcec382457f7cfeb694cc6b7 Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Tue, 17 Feb 2026 08:01:00 -0800 Subject: [PATCH 42/54] 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 bc12fb360..31916e2c3 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 43/54] 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 6624e42e8..8c32f5494 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 724233b1f..fab6297de 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 65da8d45c..626b15035 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 44/54] 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 5d93ceac4..ed1e82cd3 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 33712635e..ffadc1f51 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 45/54] 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 8c32f5494..30b913e6e 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 fab6297de..bbb4d1f15 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 96df6240b..4f55e96e4 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 9a8cfc143..1c17fc557 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 4c55e1f19..0f3a9bc2b 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 3f83da42f..02883ef75 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 46/54] 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 30b913e6e..054c7e23a 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 bbb4d1f15..ad1d34f7a 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 1c17fc557..2189dd57b 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 13227a832..3050e791e 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 5f8ef4934..7814d57e7 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 6fd7b13f3..8c4c6da29 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 626b15035..b044a8678 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 25d1c5fbe..5de12cc8c 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 2d0944542..628fadcbe 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 47/54] 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 931f515e7..988c9c50a 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 b50e674c2..d4c6743dc 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 48/54] 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 dcc1bf183..709e2554e 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 3247ae356..118b7181a 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 ed1e82cd3..432fb56e5 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 9b3e7a229..bc93a9d98 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 49/54] 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 aa377b315..6a684be70 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 50/54] 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 432fb56e5..7dcadc767 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 51/54] 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 7dcadc767..b65c72d7c 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 52/54] 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 709e2554e..86d256eca 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 53/54] 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 b65c72d7c..4bd915058 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 54/54] 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 6484498ba..8ccccd57c 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 237d4c606..b6d3ee9aa 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 4bd915058..7e5cb9849 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); }