Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cu
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ void find_cliques_from_constraint(const knapsack_constraint_t<i_t, f_t>& kc,
{
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; }
const f_t rhs_with_tolerance = kc.rhs + clique_table.tolerances.absolute_tolerance;
if (kc.entries[size - 1].val + kc.entries[size - 2].val <= rhs_with_tolerance) { return; }
Comment thread
coderabbitai[bot] marked this conversation as resolved.

std::vector<i_t> clique;
i_t k = size - 1;
// 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 && kc.entries[k].val + kc.entries[k - 1].val > kc.rhs) {
while (k >= 1 && kc.entries[k].val + kc.entries[k - 1].val > rhs_with_tolerance) {
k--;
}
for (i_t idx = k; idx < size; idx++) {
Expand All @@ -51,16 +52,18 @@ void find_cliques_from_constraint(const knapsack_constraint_t<i_t, f_t>& kc,
if (timer.check_time_limit()) { return; }
f_t curr_val = kc.entries[k].val;
i_t curr_col = kc.entries[k].col;
// do a binary search in the clique coefficients to find f, such that coeff_k + coeff_f > rhs
// this means that we get a subset of the original clique and extend it with a variable
// do a binary search in the clique coefficients to find f, such that
// coeff_k + coeff_f > rhs_with_tolerance
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);
auto it = std::upper_bound(kc.entries.begin() + original_clique_start_idx,
kc.entries.end(),
val_to_find,
[](f_t value, const auto& entry) { return value < entry.val; });
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");
cuopt_assert(it->val + curr_val > rhs_with_tolerance, "RHS mismatch");
#if DEBUG_KNAPSACK_CONSTRAINTS
CUOPT_LOG_DEBUG("Found additional clique: %d, %d, %d",
curr_col,
Expand Down
43 changes: 26 additions & 17 deletions cpp/src/mip_heuristics/presolve/conflict_graph/clique_table.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

#include <algorithm>
#include <atomic>
#include <cstdint>
#include <span>
#include <unordered_map>
#include <unordered_set>
Expand All @@ -22,7 +23,12 @@
namespace cuopt::mathematical_optimization::mip {

struct clique_config_t {
int min_clique_size = 512;
// Cliques with size <= min_clique_size are demoted by remove_small_cliques into
// explicit pairwise edges (O(size^2) each); larger cliques stay structured in
// `first`. Both representations encode the same conflict edges, so this only
// trades memory for adjacency-query speed. Keep the threshold small so mixed-row
// sub-cliques (which are frequently mid-sized) do not blow up the edge list.
int min_clique_size = 64;
int max_clique_size_for_extension = 128;
// extend_cliques work budget; one unit ≈ one hash/scan op in extend_clique.
// Soft floor before honoring cut-gen signal; hard ceiling.
Expand Down Expand Up @@ -59,20 +65,21 @@ struct addtl_clique_t {
// pairs and call `finalize_from_unsorted_pairs`.
template <typename i_t>
struct csr_var_map_t {
std::vector<i_t> offsets; // size: n_vertices + 1; offsets[v] is the start in `indices`
std::vector<i_t> indices; // sorted within each [offsets[v], offsets[v+1]) slice
std::vector<int64_t> offsets; // size: n_vertices + 1; offsets[v] is the start in `indices`
std::vector<i_t> indices; // sorted within each [offsets[v], offsets[v+1]) slice

void clear_and_resize(i_t n_vertices)
{
offsets.assign(n_vertices + 1, 0);
offsets.assign(static_cast<size_t>(n_vertices) + 1, 0);
indices.clear();
}
i_t n_keys() const { return offsets.empty() ? 0 : static_cast<i_t>(offsets.size() - 1); }
i_t slice_size(i_t v) const { return offsets[v + 1] - offsets[v]; }
int64_t slice_size(i_t v) const { return offsets[v + 1] - offsets[v]; }
// Range-for friendly view of the sorted slice for vertex v.
std::span<const i_t> slice(i_t v) const
{
return {indices.data() + offsets[v], (size_t)(offsets[v + 1] - offsets[v])};
return {indices.data() + static_cast<size_t>(offsets[v]),
static_cast<size_t>(offsets[v + 1] - offsets[v])};
}
// O(1) summary used by cut/extension cost-budget heuristics.
double avg_slice_size() const
Expand All @@ -90,36 +97,38 @@ struct csr_var_map_t {
// and deduplicated. Caller must keep p.first in [0, n_vertices).
void finalize_from_unsorted_pairs(i_t n_vertices, std::vector<std::pair<i_t, i_t>>& pairs)
{
offsets.assign(n_vertices + 1, 0);
offsets.assign(static_cast<size_t>(n_vertices) + 1, 0);
for (const auto& p : pairs) {
offsets[p.first + 1]++;
++offsets[p.first + 1];
}
for (i_t v = 1; v <= n_vertices; ++v) {
offsets[v] += offsets[v - 1];
}
indices.assign(static_cast<size_t>(offsets.back()), i_t{0});
std::vector<i_t> head(n_vertices, 0);
std::vector<int64_t> head(static_cast<size_t>(n_vertices), 0);
for (const auto& p : pairs) {
indices[offsets[p.first] + head[p.first]++] = p.second;
const size_t pos =
static_cast<size_t>(offsets[p.first]) + static_cast<size_t>(head[p.first]++);
indices[pos] = p.second;
}
for (i_t v = 0; v < n_vertices; ++v) {
auto* b = indices.data() + offsets[v];
auto* e = indices.data() + offsets[v] + head[v];
auto* b = indices.data() + static_cast<size_t>(offsets[v]);
auto* e = indices.data() + static_cast<size_t>(offsets[v] + head[v]);
std::sort(b, e);
auto* new_end = std::unique(b, e);
head[v] = static_cast<i_t>(new_end - b);
head[v] = static_cast<int64_t>(new_end - b);
}
// Compact away dedupe holes.
std::vector<i_t> new_offsets(n_vertices + 1, 0);
std::vector<int64_t> new_offsets(static_cast<size_t>(n_vertices) + 1, 0);
for (i_t v = 0; v < n_vertices; ++v) {
new_offsets[v + 1] = new_offsets[v] + head[v];
}
if (new_offsets.back() != offsets.back()) {
std::vector<i_t> new_indices(static_cast<size_t>(new_offsets.back()));
for (i_t v = 0; v < n_vertices; ++v) {
std::copy(indices.data() + offsets[v],
indices.data() + offsets[v] + head[v],
new_indices.data() + new_offsets[v]);
std::copy(indices.data() + static_cast<size_t>(offsets[v]),
indices.data() + static_cast<size_t>(offsets[v] + head[v]),
new_indices.data() + static_cast<size_t>(new_offsets[v]));
}
offsets = std::move(new_offsets);
indices = std::move(new_indices);
Expand Down
55 changes: 55 additions & 0 deletions cpp/tests/mip/cuts_test.cu
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,23 @@ End
)LP");
}

io::mps_data_model_t<int, double> create_mixed_row_roundoff_non_conflict_problem()
{
return cuopt::test::parse_inline_lp(R"LP(
Minimize
obj: 0 x0 + 0 x1 + 0 y0 + 0 y1
Subject To
c1: -0.08 x0 - 0.08 x1 + 0.1 y0 + 0.2 y1 <= 0.3
Bounds
1 <= y0 <= 1
1 <= y1 <= 1
Binaries
x0
x1
End
)LP");
}

// x0 + x1 <= 1 but x1 has upper bound 0.9999999, so this row should not be
// treated as a binary conflict row.
io::mps_data_model_t<int, double> create_near_binary_bound_conflict_problem()
Expand Down Expand Up @@ -179,6 +196,22 @@ End
)LP");
}

io::mps_data_model_t<int, double> create_addtl_clique_tolerance_boundary_problem()
{
return cuopt::test::parse_inline_lp(R"LP(
Minimize
obj: 0 x0 + 0 x1 + 0 x2 + 0 x3
Subject To
c1: x0 + 2 x1 + 3.000001 x2 + 4 x3 <= 5
Binaries
x0
x1
x2
x3
End
)LP");
}

mip::clique_table_t<int, double> build_clique_table_for_model_with_min_size(
const raft::handle_t& handle, const io::mps_data_model_t<int, double>& model, int min_clique_size)
{
Expand Down Expand Up @@ -1019,6 +1052,18 @@ TEST(cuts, clique_phase1_addtl_conflict_symmetry_and_reverse_lookup)
EXPECT_TRUE(adj_of_3.count(1) > 0);
}

TEST(cuts, clique_phase1_addtl_conflict_rejects_tolerance_boundary)
{
const raft::handle_t handle{};
auto problem = create_addtl_clique_tolerance_boundary_problem();
auto clique_table = build_clique_table_for_model_with_min_size(handle, problem, 1);

ASSERT_FALSE(clique_table.addtl_cliques.empty());
EXPECT_TRUE(clique_table.check_adjacency(2, 3));
EXPECT_TRUE(clique_table.check_adjacency(1, 3));
EXPECT_FALSE(clique_table.check_adjacency(1, 2));
}

TEST(cuts, clique_phase1_remove_small_cliques_preserves_addtl_conflicts)
{
const raft::handle_t handle{};
Expand Down Expand Up @@ -1161,6 +1206,16 @@ TEST(cuts, clique_phase5_extracts_binary_subclique_from_mixed_row)
EXPECT_FALSE(clique_table.check_adjacency(0, 2));
}

TEST(cuts, clique_phase5_mixed_row_roundoff_does_not_create_conflict)
{
const raft::handle_t handle{};
auto problem = create_mixed_row_roundoff_non_conflict_problem();
auto clique_table = build_clique_table_for_model(handle, problem);
const int num_vars = problem.get_n_variables();

EXPECT_FALSE(clique_table.check_adjacency(num_vars, num_vars + 1));
}

TEST(cuts, clique_phase5_ignores_fractional_binary_bounds)
{
const raft::handle_t handle{};
Expand Down
Loading