From b148a3b7a4c893edb159e86e7dbe841111eeb8fb Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Wed, 3 Jun 2026 19:02:56 -0700 Subject: [PATCH 1/5] Add a guard for huge bounds in bounds propagation --- .../presolve/bounds_update_helpers.cuh | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh b/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh index b3f2ac16c0..94f3d097b8 100644 --- a/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh +++ b/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh @@ -40,6 +40,13 @@ inline __device__ f_t update_ub(f_t curr_ub, f_t coeff, f_t delta_min_act, f_t d return min(curr_ub, comp_bnd); } +template +inline __device__ bool accept_candidate_bound_update(f_t bound, f_t abs_tol) +{ + constexpr f_t candidate_bound_scale = f_t{1e-14}; + return abs(bound) * candidate_bound_scale <= abs_tol; +} + template __global__ void calc_activity_kernel(typename problem_t::view_t pb, typename bounds_update_data_t::view_t upd_0, @@ -185,10 +192,17 @@ inline __device__ thrust::pair update_bounds_per_cnst( } min_a -= (coeff < 0) ? coeff * thrust::get<1>(old_bnd) : coeff * thrust::get<0>(old_bnd); max_a -= (coeff > 0) ? coeff * thrust::get<1>(old_bnd) : coeff * thrust::get<0>(old_bnd); - auto delta_min_act = cnst_ub - min_a; - auto delta_max_act = cnst_lb - max_a; - thrust::get<0>(bnd) = update_lb(thrust::get<0>(bnd), coeff, delta_min_act, delta_max_act); - thrust::get<1>(bnd) = update_ub(thrust::get<1>(bnd), coeff, delta_min_act, delta_max_act); + auto delta_min_act = cnst_ub - min_a; + auto delta_max_act = cnst_lb - max_a; + auto new_lb = update_lb(thrust::get<0>(bnd), coeff, delta_min_act, delta_max_act); + auto new_ub = update_ub(thrust::get<1>(bnd), coeff, delta_min_act, delta_max_act); + + if (accept_candidate_bound_update(new_lb, pb.tolerances.absolute_tolerance)) { + thrust::get<0>(bnd) = new_lb; + } + if (accept_candidate_bound_update(new_ub, pb.tolerances.absolute_tolerance)) { + thrust::get<1>(bnd) = new_ub; + } return bnd; } From b7a719c3c73025239c4f5d296e32d2df6533b506 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Wed, 10 Jun 2026 00:48:22 +0000 Subject: [PATCH 2/5] MAke bound acceptance a function of abs tol and big-m --- cpp/src/mip_heuristics/presolve/bounds_presolve.cu | 2 ++ cpp/src/mip_heuristics/presolve/bounds_update_data.cu | 4 +++- .../mip_heuristics/presolve/bounds_update_data.cuh | 2 ++ .../mip_heuristics/presolve/bounds_update_helpers.cuh | 11 +++++++---- cpp/src/mip_heuristics/presolve/multi_probe.cu | 4 ++++ 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/bounds_presolve.cu b/cpp/src/mip_heuristics/presolve/bounds_presolve.cu index 0a7c9de41a..81717dbecf 100644 --- a/cpp/src/mip_heuristics/presolve/bounds_presolve.cu +++ b/cpp/src/mip_heuristics/presolve/bounds_presolve.cu @@ -118,6 +118,8 @@ bool bound_presolve_t::calculate_bounds_update(problem_t& pb constexpr i_t zero = 0; constexpr auto n_threads = 256; + upd.candidate_bound_scale = + pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; upd.bounds_changed.set_value_async(zero, pb.handle_ptr->get_stream()); update_bounds_kernel <<get_stream()>>>(pb.view(), upd.view()); diff --git a/cpp/src/mip_heuristics/presolve/bounds_update_data.cu b/cpp/src/mip_heuristics/presolve/bounds_update_data.cu index 487549aa4a..7314cd5c23 100644 --- a/cpp/src/mip_heuristics/presolve/bounds_update_data.cu +++ b/cpp/src/mip_heuristics/presolve/bounds_update_data.cu @@ -21,7 +21,8 @@ bounds_update_data_t::bounds_update_data_t(problem_t& proble ub(problem.n_variables, problem.handle_ptr->get_stream()), changed_constraints(problem.n_constraints, problem.handle_ptr->get_stream()), next_changed_constraints(problem.n_constraints, problem.handle_ptr->get_stream()), - changed_variables(problem.n_variables, problem.handle_ptr->get_stream()) + changed_variables(problem.n_variables, problem.handle_ptr->get_stream()), + candidate_bound_scale(f_t(0)) { } @@ -49,6 +50,7 @@ typename bounds_update_data_t::view_t bounds_update_data_t:: v.changed_constraints = make_span(changed_constraints); v.next_changed_constraints = make_span(next_changed_constraints); v.changed_variables = make_span(changed_variables); + v.candidate_bound_scale = candidate_bound_scale; return v; } diff --git a/cpp/src/mip_heuristics/presolve/bounds_update_data.cuh b/cpp/src/mip_heuristics/presolve/bounds_update_data.cuh index e8b5e85864..8d1eac478b 100644 --- a/cpp/src/mip_heuristics/presolve/bounds_update_data.cuh +++ b/cpp/src/mip_heuristics/presolve/bounds_update_data.cuh @@ -24,6 +24,7 @@ struct bounds_update_data_t { rmm::device_uvector changed_constraints; rmm::device_uvector next_changed_constraints; rmm::device_uvector changed_variables; + f_t candidate_bound_scale; struct view_t { i_t* bounds_changed; @@ -34,6 +35,7 @@ struct bounds_update_data_t { raft::device_span changed_constraints; raft::device_span next_changed_constraints; raft::device_span changed_variables; + f_t candidate_bound_scale; }; bounds_update_data_t(problem_t& pb); diff --git a/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh b/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh index 94f3d097b8..074cd17a60 100644 --- a/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh +++ b/cpp/src/mip_heuristics/presolve/bounds_update_helpers.cuh @@ -41,9 +41,10 @@ inline __device__ f_t update_ub(f_t curr_ub, f_t coeff, f_t delta_min_act, f_t d } template -inline __device__ bool accept_candidate_bound_update(f_t bound, f_t abs_tol) +inline __device__ bool accept_candidate_bound_update(f_t bound, + f_t abs_tol, + f_t candidate_bound_scale) { - constexpr f_t candidate_bound_scale = f_t{1e-14}; return abs(bound) * candidate_bound_scale <= abs_tol; } @@ -197,10 +198,12 @@ inline __device__ thrust::pair update_bounds_per_cnst( auto new_lb = update_lb(thrust::get<0>(bnd), coeff, delta_min_act, delta_max_act); auto new_ub = update_ub(thrust::get<1>(bnd), coeff, delta_min_act, delta_max_act); - if (accept_candidate_bound_update(new_lb, pb.tolerances.absolute_tolerance)) { + if (accept_candidate_bound_update( + new_lb, pb.tolerances.absolute_tolerance, upd.candidate_bound_scale)) { thrust::get<0>(bnd) = new_lb; } - if (accept_candidate_bound_update(new_ub, pb.tolerances.absolute_tolerance)) { + if (accept_candidate_bound_update( + new_ub, pb.tolerances.absolute_tolerance, upd.candidate_bound_scale)) { thrust::get<1>(bnd) = new_ub; } return bnd; diff --git a/cpp/src/mip_heuristics/presolve/multi_probe.cu b/cpp/src/mip_heuristics/presolve/multi_probe.cu index f798957e1c..9ffed054eb 100644 --- a/cpp/src/mip_heuristics/presolve/multi_probe.cu +++ b/cpp/src/mip_heuristics/presolve/multi_probe.cu @@ -141,6 +141,10 @@ bool multi_probe_t::calculate_bounds_update(problem_t& pb, constexpr i_t zero = 0; constexpr auto n_threads = 256; + upd_0.candidate_bound_scale = + pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; + upd_1.candidate_bound_scale = + pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; if (skip_0 && skip_1) { return false; } else if (skip_0) { From 2d0e0867a6f9fbfc3788882132533b206959a0d6 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Wed, 10 Jun 2026 05:21:16 +0000 Subject: [PATCH 3/5] Guard division by zero in scale computation --- cpp/src/mip_heuristics/presolve/bounds_presolve.cu | 5 +++-- cpp/src/mip_heuristics/presolve/multi_probe.cu | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/bounds_presolve.cu b/cpp/src/mip_heuristics/presolve/bounds_presolve.cu index 81717dbecf..6d674fb9e6 100644 --- a/cpp/src/mip_heuristics/presolve/bounds_presolve.cu +++ b/cpp/src/mip_heuristics/presolve/bounds_presolve.cu @@ -118,8 +118,9 @@ bool bound_presolve_t::calculate_bounds_update(problem_t& pb constexpr i_t zero = 0; constexpr auto n_threads = 256; - upd.candidate_bound_scale = - pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; + const f_t scale_divisor = + std::max(context.settings.semi_continuous_big_m, pb.tolerances.absolute_tolerance); + upd.candidate_bound_scale = pb.tolerances.absolute_tolerance / scale_divisor; upd.bounds_changed.set_value_async(zero, pb.handle_ptr->get_stream()); update_bounds_kernel <<get_stream()>>>(pb.view(), upd.view()); diff --git a/cpp/src/mip_heuristics/presolve/multi_probe.cu b/cpp/src/mip_heuristics/presolve/multi_probe.cu index 9ffed054eb..8333581904 100644 --- a/cpp/src/mip_heuristics/presolve/multi_probe.cu +++ b/cpp/src/mip_heuristics/presolve/multi_probe.cu @@ -141,10 +141,10 @@ bool multi_probe_t::calculate_bounds_update(problem_t& pb, constexpr i_t zero = 0; constexpr auto n_threads = 256; - upd_0.candidate_bound_scale = - pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; - upd_1.candidate_bound_scale = - pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; + const f_t scale_divisor = + std::max(context.settings.semi_continuous_big_m, pb.tolerances.absolute_tolerance); + upd_0.candidate_bound_scale = pb.tolerances.absolute_tolerance / scale_divisor; + upd_1.candidate_bound_scale = pb.tolerances.absolute_tolerance / scale_divisor; if (skip_0 && skip_1) { return false; } else if (skip_0) { From 6d7a94164467ef1804ec86942e78cc382cca083c Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Wed, 10 Jun 2026 14:21:16 +0000 Subject: [PATCH 4/5] Add a big-m and abs tol health runtime check --- cpp/src/mip_heuristics/presolve/bounds_presolve.cu | 5 ++--- cpp/src/mip_heuristics/presolve/multi_probe.cu | 8 ++++---- cpp/src/mip_heuristics/solve.cu | 8 ++++++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cpp/src/mip_heuristics/presolve/bounds_presolve.cu b/cpp/src/mip_heuristics/presolve/bounds_presolve.cu index 6d674fb9e6..81717dbecf 100644 --- a/cpp/src/mip_heuristics/presolve/bounds_presolve.cu +++ b/cpp/src/mip_heuristics/presolve/bounds_presolve.cu @@ -118,9 +118,8 @@ bool bound_presolve_t::calculate_bounds_update(problem_t& pb constexpr i_t zero = 0; constexpr auto n_threads = 256; - const f_t scale_divisor = - std::max(context.settings.semi_continuous_big_m, pb.tolerances.absolute_tolerance); - upd.candidate_bound_scale = pb.tolerances.absolute_tolerance / scale_divisor; + upd.candidate_bound_scale = + pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; upd.bounds_changed.set_value_async(zero, pb.handle_ptr->get_stream()); update_bounds_kernel <<get_stream()>>>(pb.view(), upd.view()); diff --git a/cpp/src/mip_heuristics/presolve/multi_probe.cu b/cpp/src/mip_heuristics/presolve/multi_probe.cu index 8333581904..9ffed054eb 100644 --- a/cpp/src/mip_heuristics/presolve/multi_probe.cu +++ b/cpp/src/mip_heuristics/presolve/multi_probe.cu @@ -141,10 +141,10 @@ bool multi_probe_t::calculate_bounds_update(problem_t& pb, constexpr i_t zero = 0; constexpr auto n_threads = 256; - const f_t scale_divisor = - std::max(context.settings.semi_continuous_big_m, pb.tolerances.absolute_tolerance); - upd_0.candidate_bound_scale = pb.tolerances.absolute_tolerance / scale_divisor; - upd_1.candidate_bound_scale = pb.tolerances.absolute_tolerance / scale_divisor; + upd_0.candidate_bound_scale = + pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; + upd_1.candidate_bound_scale = + pb.tolerances.absolute_tolerance / context.settings.semi_continuous_big_m; if (skip_0 && skip_1) { return false; } else if (skip_0) { diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 2b64a7c681..79c4a56715 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -336,6 +336,14 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p { try { mip_solver_settings_t settings(settings_const); + cuopt_expects(std::isfinite(settings.tolerances.absolute_tolerance) && + settings.tolerances.absolute_tolerance >= f_t(0) && + std::isfinite(settings.semi_continuous_big_m) && + settings.semi_continuous_big_m > settings.tolerances.absolute_tolerance, + error_type_t::ValidationError, + "mip_absolute_tolerance must be finite and non-negative, and " + "mip_semi_continuous_big_m must be finite and greater than " + "mip_absolute_tolerance"); if (settings.presolver == presolver_t::Default || settings.presolver == presolver_t::PSLP) { if (settings.presolver == presolver_t::PSLP) { CUOPT_LOG_INFO( From 65f9e35f2e38d62782101b24c6b4a566850a33d6 Mon Sep 17 00:00:00 2001 From: Hugo Linsenmaier Date: Wed, 10 Jun 2026 17:01:35 +0000 Subject: [PATCH 5/5] Disallow zero mip_absolute_tolerance to keep huge-bound guard effective --- cpp/src/mip_heuristics/solve.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 79c4a56715..ef474e7c12 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -337,11 +337,11 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p try { mip_solver_settings_t settings(settings_const); cuopt_expects(std::isfinite(settings.tolerances.absolute_tolerance) && - settings.tolerances.absolute_tolerance >= f_t(0) && + settings.tolerances.absolute_tolerance > f_t(0) && std::isfinite(settings.semi_continuous_big_m) && settings.semi_continuous_big_m > settings.tolerances.absolute_tolerance, error_type_t::ValidationError, - "mip_absolute_tolerance must be finite and non-negative, and " + "mip_absolute_tolerance must be finite and strictly positive, and " "mip_semi_continuous_big_m must be finite and greater than " "mip_absolute_tolerance"); if (settings.presolver == presolver_t::Default || settings.presolver == presolver_t::PSLP) {