From 594ec3f42556c76bf6e3af0e959cda18febe061b Mon Sep 17 00:00:00 2001 From: akifcorduk Date: Fri, 31 Oct 2025 04:40:56 -0700 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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