diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index e444abcdc5..f445c5d591 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -2507,6 +2507,7 @@ void branch_and_bound_t::solve_submip(diving_worker_t* worke f_t work_limit = 1.0; submip_fj_cpu_worker.create_worker(submip_bnb.original_lp_, submip_bnb.var_types_, + submip_bnb.original_problem_.num_cols, initial_guess, submip_bnb.settings_, std::format("{} [CPU FJ]", log_prefix), @@ -2984,6 +2985,7 @@ void branch_and_bound_t::recursive_submip( submip_fj_cpu_worker.create_worker( worker->leaf_problem, worker->var_types, + original_problem_.num_cols, worker->leaf_solution.x, settings_, std::format("{} [CPU FJ]", submip_settings.log.log_prefix), @@ -3063,7 +3065,7 @@ void branch_and_bound_t::launch_root_heuristics( set_solution_from_cpu_fj(obj, assignment, work_units); }; current_heuristic->fj_cpu_worker_.create_worker( - lp, var_types_, lp_solution.x, settings_, "[RootCut CPUFJ] "); + lp, var_types_, original_problem_.num_cols, lp_solution.x, settings_, "[RootCut CPUFJ] "); ++(*worker_count); ++current_heuristic->active_workers_; diff --git a/cpp/src/mip_heuristics/CMakeLists.txt b/cpp/src/mip_heuristics/CMakeLists.txt index 187017fb14..5771b69fe6 100644 --- a/cpp/src/mip_heuristics/CMakeLists.txt +++ b/cpp/src/mip_heuristics/CMakeLists.txt @@ -45,7 +45,14 @@ set(MIP_NON_LP_FILES ${CMAKE_CURRENT_SOURCE_DIR}/presolve/trivial_presolve.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 + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/cpu/audit.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/cpu/climber.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/cpu/loop.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/cpu/portfolio.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/cpu/search/escape.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/cpu/setup/lp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/cpu/setup/structure.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/fj_cpu_bridge.cu ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/fj_cpu_binary.cu ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/fj_cpu_binary_preprocess.cu ${CMAKE_CURRENT_SOURCE_DIR}/feasibility_jump/fj_cpu_binary_kernels.cpp diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 96165e6064..c8a1a09ea5 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -197,6 +198,7 @@ void diversity_manager_t::add_user_given_solutions( const bool has_papilo = problem_ptr->has_papilo_presolve_data(); const i_t papilo_orig_n = problem_ptr->get_papilo_original_num_variables(); for (size_t sol_idx = 0; sol_idx < context.settings.initial_solutions.size(); ++sol_idx) { + if (timer.check_time_limit()) { break; } const auto& init_sol = context.settings.initial_solutions[sol_idx]; solution_t sol(*problem_ptr); rmm::device_uvector init_sol_assignment(*init_sol, sol.handle_ptr->get_stream()); @@ -228,17 +230,18 @@ void diversity_manager_t::add_user_given_solutions( "reduced objective size must match crushed solution dimension"); // Map each solution to user space with its own problem's scale, so the comparison holds even // if the original and reduced objective scales ever diverge. - const double input_obj = + [[maybe_unused]] const double input_obj = (double)presolver_ptr->get_original_objective_scaling_factor() * std::inner_product(h_ori_obj.begin(), h_ori_obj.end(), h_original.begin(), (double)presolver_ptr->get_original_objective_offset()); - const double crushed_obj = (double)reduced_problem.get_objective_scaling_factor() * - std::inner_product(h_red_obj.begin(), - h_red_obj.end(), - h_crushed.begin(), - (double)reduced_problem.get_objective_offset()); + [[maybe_unused]] const double crushed_obj = + (double)reduced_problem.get_objective_scaling_factor() * + std::inner_product(h_red_obj.begin(), + h_red_obj.end(), + h_crushed.begin(), + (double)reduced_problem.get_objective_offset()); CUOPT_LOG_DEBUG( "Crushed initial solution %d through Papilo (%d -> %d vars), objective %g -> %g", sol_idx, diff --git a/cpp/src/mip_heuristics/early_heuristic.cuh b/cpp/src/mip_heuristics/early_heuristic.cuh index cb0be4200a..fbb5636a2f 100644 --- a/cpp/src/mip_heuristics/early_heuristic.cuh +++ b/cpp/src/mip_heuristics/early_heuristic.cuh @@ -7,18 +7,13 @@ #pragma once -#include -#include - #include - -#include - -#include +#include #include #include #include +#include #include namespace cuopt::mathematical_optimization::mip { @@ -34,25 +29,13 @@ template class early_heuristic_t { public: early_heuristic_t(const optimization_problem_t& op_problem, - const typename mip_solver_settings_t::tolerances_t& tolerances, early_incumbent_callback_t incumbent_callback) - : incumbent_callback_(std::move(incumbent_callback)) + : objective_scaling_factor_(op_problem.get_sense() ? -op_problem.get_objective_scaling_factor() + : op_problem.get_objective_scaling_factor()), + objective_offset_(op_problem.get_sense() ? -op_problem.get_objective_offset() + : op_problem.get_objective_offset()), + incumbent_callback_(std::move(incumbent_callback)) { - RAFT_CUDA_TRY(cudaGetDevice(&device_id_)); - - // Build and preprocess on the original handle, then copy onto our own handle - // so the derived solver can run on a dedicated stream (prevents graph capture conflicts). - problem_t temp_problem(op_problem, tolerances, false); - temp_problem.preprocess_problem(); - temp_problem.handle_ptr->sync_stream(); - problem_ptr_ = std::make_unique>(temp_problem, &handle_); - - solution_ptr_ = std::make_unique>(*problem_ptr_); - thrust::fill(handle_.get_thrust_policy(), - solution_ptr_->assignment.begin(), - solution_ptr_->assignment.end(), - f_t{0}); - solution_ptr_->clamp_within_bounds(); } bool solution_found() const { return solution_found_; } @@ -60,12 +43,12 @@ class early_heuristic_t { // Return the best objective converted to user-space (sense-aware, offset-aware). f_t get_best_user_objective() const { - return problem_ptr_->get_user_obj_from_solver_obj(best_objective_); + return objective_scaling_factor_ * (best_objective_ + objective_offset_); } // Set the incumbent threshold. `obj` must be in THIS heuristic's solver-space - // (i.e. the space of problem_ptr_). Callers that hold a value from a different - // problem representation (e.g., the original pre-presolve problem) must convert - // it first, otherwise try_update_best will reject valid solutions. + // (i.e. the space of its input problem). Callers that hold a value from a + // different problem representation (e.g., the original pre-presolve problem) + // must convert it first, otherwise try_update_best will reject valid solutions. void set_best_objective(f_t obj) { best_objective_ = obj; } const std::vector& get_best_assignment() const { return best_assignment_; } @@ -81,34 +64,20 @@ class early_heuristic_t { if (solver_obj >= best_objective_) { return; } best_objective_ = solver_obj; - RAFT_CUDA_TRY(cudaSetDevice(device_id_)); - auto stream = handle_.get_stream(); - rmm::device_uvector d_assignment(assignment.size(), stream); - raft::copy(d_assignment.data(), assignment.data(), assignment.size(), stream); - problem_ptr_->post_process_assignment(d_assignment, true, stream); - auto user_assignment = cuopt::host_copy(d_assignment, stream); - - best_assignment_ = user_assignment; + best_assignment_ = ((Derived*)this)->to_user_assignment(assignment); solution_found_ = true; - f_t user_obj = problem_ptr_->get_user_obj_from_solver_obj(solver_obj); + f_t user_obj = get_best_user_objective(); // Log and callback are deferred to the shared incumbent_callback_ which enforces // global monotonicity across all early heuristic instances. if (incumbent_callback_) { - incumbent_callback_(solver_obj, user_obj, user_assignment, heuristic_name); + incumbent_callback_(solver_obj, user_obj, best_assignment_, heuristic_name); } } - int device_id_{0}; - - // handle_ must be declared before problem_ptr_/solution_ptr_ so it outlives them - // (C++ destroys members in reverse declaration order) - raft::handle_t handle_; - - std::unique_ptr> problem_ptr_; - std::unique_ptr> solution_ptr_; - bool solution_found_{false}; f_t best_objective_{std::numeric_limits::infinity()}; + f_t objective_scaling_factor_; + f_t objective_offset_; std::vector best_assignment_; early_incumbent_callback_t incumbent_callback_; diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/audit.cpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/audit.cpp new file mode 100644 index 0000000000..397e4cebe4 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/audit.cpp @@ -0,0 +1,382 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "audit.hpp" +#include "internal.hpp" +#include "problem.hpp" + +namespace cuopt::mathematical_optimization::mip { + +namespace { +constexpr double fj_audit_rel_slack = 1e-9; +constexpr double fj_audit_abs_floor = 1e-6; +constexpr int32_t fj_audit_row_terms_printed = 64; +constexpr bool fj_audit_each_row_update = false; +constexpr bool fj_audit_each_objective_update = false; +} // namespace + +template +void audit_assignment_bounds(fj_cpu_climber_t& fj_cpu, const char* site) +{ + for (i_t var = 0; var < fj_cpu.problem->n_variables; ++var) { + const f_t val = fj_cpu.h_assignment[var]; + auto bounds = fj_cpu.h_var_bounds[var].get(); + const bool inbox = fj_cpu.check_variable_within_bounds(var, val); + const bool integral = + var_t::INTEGER != fj_cpu.problem->h_var_types[var] || fj_cpu.problem->is_integer(val); + if (inbox && integral) continue; + + CUOPT_LOG_DEBUG("%sCPUFJ %s left var %d at %.17g outside [%.17g, %.17g], integer %d", + fj_cpu.log_prefix.c_str(), + site, + (int)var, + val, + get_lower(bounds), + get_upper(bounds), + var_t::INTEGER == fj_cpu.problem->h_var_types[var]); + cuopt_assert(false, "assignment left the variable bounds"); + return; + } +} + +template +f_t fresh_row_slack(fj_cpu_climber_t& fj_cpu, i_t row, const f_t* assignment) +{ + const f_t activity = compensated_dot2_csr(fj_cpu.h_offsets.data(), + fj_cpu.h_variables.data(), + fj_cpu.h_coefficients.data(), + assignment, + row); + return (f_t)fj_cpu.h_bound[row] - activity; +} + +template +void report_row_divergence(fj_cpu_climber_t& fj_cpu, + i_t cstr_idx, + const f_t* assignment, + const char* site) +{ + auto [row_begin, row_end] = fj_cpu.range_for_row(cstr_idx); + const f_t sumcomp = fj_cpu.h_slack_sumcomp[cstr_idx]; + + CUOPT_LOG_DEBUG( + "%sCPUFJ %s row %d state: iteration %d, width %d, slack sumcomp " + "%.17g, bound %.17g, refresh period %d, recomputes total %lld periodic %lld bigval " + "%lld perturb %lld restart %lld", + fj_cpu.log_prefix.c_str(), + site, + (int)cstr_idx, + (int)fj_cpu.iterations, + (int)(row_end - row_begin), + sumcomp, + fj_cpu.h_bound[cstr_idx], + (int)fj_cpu.lhs_refresh_period_used, + (long long)fj_cpu.n_lhs_recompute_total, + (long long)fj_cpu.n_lhs_recompute_periodic, + (long long)fj_cpu.n_lhs_recompute_bigval, + (long long)fj_cpu.n_lhs_recompute_perturb, + (long long)fj_cpu.n_lhs_recompute_restart); + + i_t unreachable = 0; + i_t mismatched = 0; + for (i_t p = row_begin; p < row_end; ++p) { + const i_t var = fj_cpu.h_variables[p]; + const f_t coeff = fj_cpu.h_coefficients[p]; + const f_t val = assignment[var]; + + // apply_move reaches this row only through the variable's slice of the transpose. + const auto [rev_begin, rev_end] = fj_cpu.range_for_variable(var); + bool reachable = false; + f_t rev_coeff = 0; + for (i_t q = rev_begin; q < rev_end; ++q) { + if (fj_cpu.h_reverse_constraints[q] != cstr_idx) continue; + reachable = true; + rev_coeff = fj_cpu.h_reverse_coefficients[q]; + break; + } + + if (!reachable) { + ++unreachable; + } else if (rev_coeff != coeff) { + ++mismatched; + } + + if (p - row_begin >= (i_t)fj_audit_row_terms_printed) continue; + CUOPT_LOG_DEBUG( + "%sCPUFJ %s row %d term %d: var %d integer %d degree %d, coeff %.17g x %.17g " + "product %.17g, reachable %d transpose coeff %.17g", + fj_cpu.log_prefix.c_str(), + site, + (int)cstr_idx, + (int)(p - row_begin), + (int)var, + var_t::INTEGER == fj_cpu.problem->h_var_types[var], + (int)(rev_end - rev_begin), + coeff, + val, + coeff * val, + reachable, + rev_coeff); + } + + CUOPT_LOG_DEBUG( + "%sCPUFJ %s row %d structure: %d of %d variables cannot reach it through the " + "transpose, %d carry a different transpose coefficient%s", + fj_cpu.log_prefix.c_str(), + site, + (int)cstr_idx, + (int)unreachable, + (int)(row_end - row_begin), + (int)mismatched, + row_end - row_begin > (i_t)fj_audit_row_terms_printed ? " (terms truncated)" : ""); +} + +template +void audit_objective_update( + fj_cpu_climber_t& fj_cpu, i_t var_idx, f_t old_val, f_t delta, f_t obj_old, f_t obj_y) +{ + if (!fj_audit_each_objective_update) return; + const f_t* const assignment = fj_cpu.h_assignment.data(); + + const f_t fresh = + compensated_dot2(fj_cpu.problem->h_obj_coeffs.data(), assignment, fj_cpu.problem->n_variables); + const f_t gap = std::fabs(fj_cpu.h_incumbent_objective - fresh); + const f_t slack = (f_t)fj_audit_abs_floor + (f_t)fj_audit_rel_slack * std::fabs(fresh); + if (!(gap > slack)) return; + + const f_t coeff = fj_cpu.problem->h_obj_coeffs[var_idx]; + const f_t product = coeff * delta; + // Debug messages are flushed before the abort below. + CUOPT_LOG_DEBUG( + "%sCPUFJ objective update: carried %.17g vs c'x %.17g, gap %.17g over slack %.17g. " + "iteration %d, var %d moved %.17g -> %.17g by delta %.17g, objective coeff %.17g, " + "product %.17g whose ulp is %.17g, obj_old %.17g, obj_y %.17g, sumcomp %.17g", + fj_cpu.log_prefix.c_str(), + fj_cpu.h_incumbent_objective, + fresh, + gap, + slack, + (int)fj_cpu.iterations, + (int)var_idx, + old_val, + old_val + delta, + delta, + coeff, + product, + std::numeric_limits::epsilon() * std::fabs(product), + obj_old, + obj_y, + fj_cpu.h_objective_sumcomp); + cuopt_assert(false, "h_incumbent_objective disagrees with c'x after a move"); +} + +template +void audit_row_updates( + fj_cpu_climber_t& fj_cpu, i_t var_idx, f_t old_val, f_t delta, i_t begin, i_t end) +{ + if (!fj_audit_each_row_update) return; + const f_t* const assignment = fj_cpu.h_assignment.data(); + + for (i_t cstr_idx = 0; cstr_idx < fj_cpu.n_rows; ++cstr_idx) { + const f_t carried = fj_cpu.row_state()[cstr_idx].slack + fj_cpu.h_slack_sumcomp[cstr_idx]; + const f_t fresh = fresh_row_slack(fj_cpu, cstr_idx, assignment); + const f_t gap = std::fabs(carried - fresh); + // The verdict, not the gap. A row far from its bound may carry a value an ulp off the fresh one + // with no consequence, and above |slack| ~ 4e9 one ulp already exceeds the row tolerance, so + // any absolute threshold fires there on correct arithmetic. + if ((carried < -fj_cpu.row_tolerance) == (fresh < -fj_cpu.row_tolerance)) continue; + + f_t incidence_coeff = 0; + bool touched = false; + for (i_t i = begin; i < end; ++i) { + if (fj_cpu.h_reverse_constraints[i] != cstr_idx) continue; + touched = true; + incidence_coeff = fj_cpu.h_reverse_coefficients[i]; + break; + } + + // Debug messages are flushed before the abort below. + CUOPT_LOG_DEBUG( + "%sCPUFJ row update row %d: carried slack %.17g says violated %d, fresh %.17g says " + "%d, differ by %.17g against tol %.17g. iteration %d, var %d moved %.17g -> %.17g " + "by delta %.17g, row in this move's support %d with coeff %.17g, row bound %.17g, " + "slack sumcomp %.17g, width %d", + fj_cpu.log_prefix.c_str(), + (int)cstr_idx, + carried, + carried < -fj_cpu.row_tolerance, + fresh, + fresh < -fj_cpu.row_tolerance, + gap, + fj_cpu.row_tolerance, + (int)fj_cpu.iterations, + (int)var_idx, + old_val, + old_val + delta, + delta, + touched, + incidence_coeff, + fj_cpu.h_bound[cstr_idx], + fj_cpu.h_slack_sumcomp[cstr_idx], + (int)(fj_cpu.h_offsets[cstr_idx + 1] - fj_cpu.h_offsets[cstr_idx])); + report_row_divergence(fj_cpu, cstr_idx, assignment, "row update"); + cuopt_assert(false, "carried slack disagrees with a fresh sum after a move"); + return; + } +} + +template +void audit_incremental_state(fj_cpu_climber_t& fj_cpu, const char* site) +{ + const f_t* const assignment = fj_cpu.h_assignment.data(); + const f_t tol = fj_cpu.row_tolerance; + + f_t fresh_total = 0; + // The total re-derived from the carried slacks rather than from the model. Stored against this + // isolates the total's own accounting; this against fresh_total isolates the row slacks. + f_t carried_total = 0; + + for (i_t cstr_idx = 0; cstr_idx < fj_cpu.n_rows; ++cstr_idx) { + const f_t fresh = fresh_row_slack(fj_cpu, cstr_idx, assignment); + const f_t cost = fresh < f_t{0} ? fresh : f_t{0}; + const f_t carried = fj_cpu.row_state()[cstr_idx].slack + fj_cpu.h_slack_sumcomp[cstr_idx]; + + const bool truly_violated = fresh < -tol; + if (truly_violated) { fresh_total += cost; } + + const bool carried_violated = fj_cpu.violated_constraints.contains(cstr_idx); + if (carried_violated) { carried_total += carried; } + if (carried_violated == truly_violated) continue; + + // Debug messages are flushed before the abort below. + CUOPT_LOG_DEBUG( + "%sCPUFJ %s row %d: integral %d, carried violated %d actual %d, carried " + "slack %.17g vs fresh %.17g differ by %.17g, bound %.17g, tol %.17g", + fj_cpu.log_prefix.c_str(), + site, + (int)cstr_idx, + fj_cpu.h_row_is_integral[cstr_idx], + carried_violated, + truly_violated, + carried, + fresh, + std::fabs(carried - fresh), + fj_cpu.h_bound[cstr_idx], + tol); + report_row_divergence(fj_cpu, cstr_idx, assignment, site); + cuopt_assert(false, "violated set disagrees with a fresh slack"); + return; + } + + const f_t fresh_obj = + compensated_dot2(fj_cpu.problem->h_obj_coeffs.data(), assignment, fj_cpu.problem->n_variables); + const f_t obj_gap = std::fabs(fj_cpu.h_incumbent_objective - fresh_obj); + const f_t obj_slack = (f_t)fj_audit_abs_floor + (f_t)fj_audit_rel_slack * std::fabs(fresh_obj); + if (obj_gap > obj_slack) { + CUOPT_LOG_DEBUG( + "%sCPUFJ %s h_incumbent_objective %.17g vs c'x %.17g, gap %.17g over slack %.17g, " + "sumcomp %.17g", + fj_cpu.log_prefix.c_str(), + site, + fj_cpu.h_incumbent_objective, + fresh_obj, + obj_gap, + obj_slack, + fj_cpu.h_objective_sumcomp); + cuopt_assert(false, "h_incumbent_objective left c'x behind"); + } +} + +template +bool check_variable_feasibility(fj_cpu_climber_t& fj_cpu, bool check_integer) +{ + for (i_t var_idx = 0; var_idx < fj_cpu.problem->n_variables; var_idx += 1) { + auto val = fj_cpu.h_assignment[var_idx]; + bool feasible = check_variable_within_bounds(fj_cpu, var_idx, val); + + if (!feasible) return false; + if (check_integer && is_integer_var(fj_cpu, var_idx) && + !fj_cpu.problem->is_integer(fj_cpu.h_assignment[var_idx])) + return false; + } + return true; +} + +template +void sanity_checks(fj_cpu_climber_t& fj_cpu) +{ + cuopt_assert((i_t)fj_cpu.h_row_state.size() == fj_cpu.n_rows, + "row state does not cover the search rows"); + cuopt_assert((i_t)fj_cpu.h_slack_sumcomp.size() == fj_cpu.n_rows, + "slack compensation does not cover the search rows"); + + // Check that each variable is within its bounds + for (i_t var_idx = 0; var_idx < fj_cpu.problem->n_variables; ++var_idx) { + f_t val = fj_cpu.h_assignment[var_idx]; + cuopt_assert(fj_cpu.check_variable_within_bounds(var_idx, val), "Variable is out of bounds"); + } + + // Check that each violated constraint is actually violated and not present in + // satisfied_constraints + for (const auto& cstr_idx : fj_cpu.violated_constraints) { + cuopt_assert(!fj_cpu.satisfied_constraints.contains(cstr_idx), + "Violated constraint also in satisfied_constraints"); + cuopt_assert( + fj_cpu.row_state()[cstr_idx].slack + fj_cpu.h_slack_sumcomp[cstr_idx] < -fj_cpu.row_tolerance, + "Constraint in violated_constraints is not actually violated"); + } + + // Check that each satisfied constraint is actually satisfied and not present in + // violated_constraints + for (const auto& cstr_idx : fj_cpu.satisfied_constraints) { + cuopt_assert(!fj_cpu.violated_constraints.contains(cstr_idx), + "Satisfied constraint also in violated_constraints"); + cuopt_assert(!(fj_cpu.row_state()[cstr_idx].slack + fj_cpu.h_slack_sumcomp[cstr_idx] < + -fj_cpu.row_tolerance), + "Constraint in satisfied_constraints is actually violated"); + } + + // Check that each constraint is in exactly one of violated_constraints or satisfied_constraints + for (i_t cstr_idx = 0; cstr_idx < fj_cpu.n_rows; ++cstr_idx) { + bool in_viol = fj_cpu.violated_constraints.contains(cstr_idx); + bool in_sat = fj_cpu.satisfied_constraints.contains(cstr_idx); + cuopt_assert( + in_viol != in_sat, + "Constraint must be in exactly one of violated_constraints or satisfied_constraints"); + + cuopt_assert(fj_cpu.row_state()[cstr_idx].weight >= 0, "Weights should be positive or zero"); + } + cuopt_assert(fj_cpu.h_objective_weight >= 0, "Objective weight should be positive or zero"); + cuopt_assert(fj_cpu.seed_objective_weight >= 0, + "Objective weight floor should be positive or zero"); +} + +#if MIP_INSTANTIATE_FLOAT +template void audit_assignment_bounds(fj_cpu_climber_t&, const char*); +template float fresh_row_slack(fj_cpu_climber_t&, int, const float*); +template void audit_objective_update( + fj_cpu_climber_t&, int, float, float, float, float); +template void audit_row_updates( + fj_cpu_climber_t&, int, float, float, int, int); +template void audit_incremental_state(fj_cpu_climber_t&, const char*); +template bool check_variable_feasibility(fj_cpu_climber_t&, bool); +template void sanity_checks(fj_cpu_climber_t&); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template void audit_assignment_bounds(fj_cpu_climber_t&, const char*); +template double fresh_row_slack(fj_cpu_climber_t&, int, const double*); +template void audit_objective_update( + fj_cpu_climber_t&, int, double, double, double, double); +template void audit_row_updates( + fj_cpu_climber_t&, int, double, double, int, int); +template void audit_incremental_state(fj_cpu_climber_t&, const char*); +template bool check_variable_feasibility(fj_cpu_climber_t&, bool); +template void sanity_checks(fj_cpu_climber_t&); +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/audit.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/audit.hpp new file mode 100644 index 0000000000..137c4e284d --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/audit.hpp @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "state.hpp" + +namespace cuopt::mathematical_optimization::mip { + +inline constexpr bool fj_audit_every_iteration = false; + +template +void audit_assignment_bounds(fj_cpu_climber_t&, const char*); +template +f_t fresh_row_slack(fj_cpu_climber_t&, i_t, const f_t*); +template +void audit_objective_update(fj_cpu_climber_t&, i_t, f_t, f_t, f_t, f_t); +template +void audit_row_updates(fj_cpu_climber_t&, i_t, f_t, f_t, i_t, i_t); +template +void audit_incremental_state(fj_cpu_climber_t&, const char*); +template +bool check_variable_feasibility(fj_cpu_climber_t&, bool check_integer = true); +template +void sanity_checks(fj_cpu_climber_t&); +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/climber.cpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/climber.cpp new file mode 100644 index 0000000000..672696b8ae --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/climber.cpp @@ -0,0 +1,593 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "climber.hpp" +#include "internal.hpp" +#include "problem.hpp" +#include "search/api.hpp" +#include "setup/lp.hpp" +#include "setup/structure.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +void init_fj_cpu_from_template(fj_cpu_climber_t& fj_cpu, + const fj_cpu_climber_t& tmpl, + const std::vector& left_weights, + const std::vector& right_weights, + f_t objective_weight) +{ + const i_t n_variables = (i_t)tmpl.problem->reverse_offsets.size() - 1; + const i_t n_constraints = (i_t)tmpl.problem->offsets.size() - 1; + const i_t nnz = (i_t)tmpl.problem->coefficients.size(); + + cuopt_assert(n_variables == tmpl.problem->n_variables, "template variable count mismatch"); + cuopt_assert(n_constraints == tmpl.problem->n_constraints, "template constraint count mismatch"); + cuopt_assert(nnz == tmpl.problem->nnz, "template nnz mismatch"); + cuopt_assert(left_weights.size() == static_cast(n_constraints), + "left weight size mismatch"); + cuopt_assert(right_weights.size() == static_cast(n_constraints), + "right weight size mismatch"); + + // Shared, not copied: read-only for the whole solve. + fj_cpu.problem = tmpl.problem; + fj_cpu.hp = tmpl.hp; + + fj_cpu.h_initial_left_weights = left_weights; + fj_cpu.h_initial_right_weights = right_weights; + fj_cpu.max_weight = 1.0; + fj_cpu.h_objective_weight = objective_weight; + fj_cpu.h_assignment = tmpl.h_assignment; + fj_cpu.h_best_assignment = tmpl.h_assignment; + fj_cpu.h_var_bounds = tmpl.h_var_bounds; + fj_cpu.h_is_binary_variable = tmpl.h_is_binary_variable; + fj_cpu.h_binary_indices = tmpl.h_binary_indices; + fj_cpu.n_binary_vars = tmpl.n_binary_vars; + fj_cpu.n_integer_vars = tmpl.n_integer_vars; + fj_cpu.h_tabu_nodec_until.resize(n_variables, 0); + fj_cpu.h_tabu_noinc_until.resize(n_variables, 0); + fj_cpu.h_tabu_lastdec.resize(n_variables, 0); + fj_cpu.h_tabu_lastinc.resize(n_variables, 0); + fj_cpu.iterations = 0; + + finalize_fj_cpu_host_initialization_from_template( + fj_cpu, tmpl, n_variables, n_constraints, tmpl.n_integer_vars, nnz, tmpl.problem->tolerances); +} + +template +void set_host_data_view(fj_cpu_climber_t& fj_cpu, + i_t n_variables, + i_t n_constraints, + i_t n_integer_vars, + i_t nnz, + const typename mip_solver_settings_t::tolerances_t& tolerances) +{ + cuopt_assert(fj_cpu.problem->n_variables == n_variables, "problem variable count mismatch"); + cuopt_assert(fj_cpu.problem->n_constraints == n_constraints, "problem constraint count mismatch"); + cuopt_assert(fj_cpu.problem->nnz == nnz, "problem nonzero count mismatch"); + fj_cpu.row_tolerance = tolerances.absolute_tolerance * (f_t)0.9; + fj_cpu.n_integer_vars = n_integer_vars; +} + +template +void wire_fj_cpu_host_views( + fj_cpu_climber_t& fj_cpu, + i_t n_variables, + i_t n_constraints, + i_t n_integer_vars, + i_t nnz, + const typename mip_solver_settings_t::tolerances_t& tolerances) +{ + cuopt_assert(n_variables >= 0, "invalid variable count"); + cuopt_assert(n_constraints >= 0, "invalid constraint count"); + cuopt_assert(fj_cpu.problem->offsets.size() == static_cast(n_constraints + 1), + "invalid CSR offsets"); + cuopt_assert(fj_cpu.problem->reverse_offsets.size() == static_cast(n_variables + 1), + "invalid reverse offsets"); + cuopt_assert(fj_cpu.h_assignment.size() == static_cast(n_variables), + "start assignment size mismatch"); + + set_host_data_view(fj_cpu, n_variables, n_constraints, n_integer_vars, nnz, tolerances); + + fj_cpu.h_best_objective = +std::numeric_limits::infinity(); + + // cached_mtm_moves, cached_mtm_moves_version and h_cstr_version are indexed by search row and + // search nonzero, so build_one_sided_rows sizes them; nothing reads them before it runs. + + fj_cpu.flip_move_stamp.assign(n_variables, 0); + fj_cpu.flip_move_epoch = 1; + + certify_epigraph_variables(fj_cpu, n_variables); +} + +template +void finalize_fj_cpu_host_initialization( + fj_cpu_climber_t& fj_cpu, + fj_cpu_problem_t& problem, + i_t n_variables, + i_t n_constraints, + i_t n_integer_vars, + i_t nnz, + const typename mip_solver_settings_t::tolerances_t& tolerances) +{ + raft::common::nvtx::range scope("finalize_fj_cpu_host_initialization"); + cuopt_assert(fj_cpu.problem.get() == &problem, "mutable problem builder does not match climber"); + + detect_implied_integers(fj_cpu, problem); + wire_fj_cpu_host_views(fj_cpu, n_variables, n_constraints, n_integer_vars, nnz, tolerances); + + problem.h_objective_vars.resize(n_variables); + auto end = std::copy_if( + thrust::counting_iterator(0), + thrust::counting_iterator(n_variables), + problem.h_objective_vars.begin(), + [&problem](i_t idx) { return !problem.integer_equal(problem.h_obj_coeffs[idx], (f_t)0); }); + problem.h_objective_vars.resize(end - problem.h_objective_vars.begin()); + // get_breakthrough_move divides by the coefficient of every variable in here. + for ([[maybe_unused]] auto var_idx : problem.h_objective_vars) { + cuopt_assert(problem.h_obj_coeffs[var_idx] != f_t{0}, "null coefficient in the objective vars"); + cuopt_assert(std::isfinite((f_t)problem.h_obj_coeffs[var_idx]), + "non-finite objective coefficient"); + } + + f_t abs_obj_sum = 0; + for (auto var_idx : problem.h_objective_vars) { + const f_t coeff = problem.h_obj_coeffs[var_idx]; + abs_obj_sum += coeff < 0 ? -coeff : coeff; + } + problem.obj_magnitude = abs_obj_sum > 0 ? abs_obj_sum / problem.h_objective_vars.size() : f_t{1}; + cuopt_assert(std::isfinite(problem.obj_magnitude) && problem.obj_magnitude > 0, + "objective magnitude unit must be finite and positive"); + + // Must precede recompute_lhs, which is what first populates them. + fj_cpu.violated_constraints.resize(n_constraints); + fj_cpu.satisfied_constraints.resize(n_constraints); + + { + phase_timer_t timer(fj_cpu.t_init_lhs); + recompute_lhs(fj_cpu); + } +} + +template +static void initialize_climber_state( + fj_cpu_climber_t& fj_cpu, + fj_cpu_problem_t& problem, + std::vector assignment, + i_t n_integer_vars, + const typename mip_solver_settings_t::tolerances_t& tolerances) +{ + const i_t n_variables = problem.n_variables; + const i_t n_constraints = problem.n_constraints; + fj_cpu.h_initial_left_weights.resize(n_constraints, f_t{1}); + fj_cpu.h_initial_right_weights.resize(n_constraints, f_t{1}); + fj_cpu.max_weight = f_t{1}; + fj_cpu.h_objective_weight = f_t{0}; + fj_cpu.h_assignment = assignment; + fj_cpu.h_best_assignment = std::move(assignment); + fj_cpu.h_lhs.resize(n_constraints); + fj_cpu.h_lhs_sumcomp.resize(n_constraints, f_t{0}); + fj_cpu.h_tabu_nodec_until.resize(n_variables, 0); + fj_cpu.h_tabu_noinc_until.resize(n_variables, 0); + fj_cpu.h_tabu_lastdec.resize(n_variables, 0); + fj_cpu.h_tabu_lastinc.resize(n_variables, 0); + fj_cpu.iterations = 0; + + finalize_fj_cpu_host_initialization( + fj_cpu, problem, n_variables, n_constraints, n_integer_vars, problem.nnz, tolerances); +} + +template +void finalize_fj_cpu_host_initialization_from_template( + fj_cpu_climber_t& fj_cpu, + const fj_cpu_climber_t& tmpl, + i_t n_variables, + i_t n_constraints, + i_t n_integer_vars, + i_t nnz, + const typename mip_solver_settings_t::tolerances_t& tolerances) +{ + raft::common::nvtx::range scope("finalize_fj_cpu_host_initialization_from_template"); + + cuopt_assert(tmpl.h_lhs.size() == static_cast(n_constraints), "template lhs mismatch"); + cuopt_assert(tmpl.violated_constraints.max_size() == n_constraints, + "template violated set mismatch"); + cuopt_assert(tmpl.satisfied_constraints.max_size() == n_constraints, + "template satisfied set mismatch"); + + fj_cpu.h_lhs = tmpl.h_lhs; + fj_cpu.h_lhs_sumcomp = tmpl.h_lhs_sumcomp; + fj_cpu.violated_constraints = tmpl.violated_constraints; + fj_cpu.satisfied_constraints = tmpl.satisfied_constraints; + fj_cpu.total_violations = tmpl.total_violations; + fj_cpu.total_violations_sumcomp = tmpl.total_violations_sumcomp; + fj_cpu.h_incumbent_objective = tmpl.h_incumbent_objective; + fj_cpu.h_objective_sumcomp = tmpl.h_objective_sumcomp; + + fj_cpu.bin_eliminated_rows = tmpl.bin_eliminated_rows; + fj_cpu.bin_singletons = tmpl.bin_singletons; + fj_cpu.bin_ignore_row = tmpl.bin_ignore_row; + fj_cpu.bin_ignore_var = tmpl.bin_ignore_var; + fj_cpu.has_bin_elimination = tmpl.has_bin_elimination; + + wire_fj_cpu_host_views(fj_cpu, n_variables, n_constraints, n_integer_vars, nnz, tolerances); +} + +template +std::unique_ptr> init_fj_cpu_from_host_lp( + const lp_problem_t& problem, + const std::vector& variable_types, + i_t n_structural, + const std::vector& start_assignment, + const simplex_solver_settings_t& settings, + std::atomic& preemption_flag, + int64_t seed) +{ + using f_t2 = typename type_2::type; + + cuopt_assert(variable_types.size() >= static_cast(problem.num_cols), + "variable type size mismatch"); + + typename mip_solver_settings_t::tolerances_t tolerances{}; + tolerances.absolute_tolerance = settings.primal_tol; + tolerances.relative_tolerance = settings.zero_tol; + tolerances.integrality_tolerance = settings.integer_tol; + tolerances.absolute_mip_gap = settings.absolute_mip_gap_tol; + tolerances.relative_mip_gap = settings.relative_mip_gap_tol; + + const i_t n_constraints = problem.num_rows; + + csr_matrix_t csr_A(problem.num_rows, problem.num_cols, problem.A.nnz()); + problem.A.to_compressed_row(csr_A); + + std::vector constraint_lower_bounds; + std::vector constraint_upper_bounds; + i_t n_variables; + if (n_structural > 0 && n_structural < problem.num_cols) { + eliminate_slacks( + problem, n_structural, csr_A, constraint_lower_bounds, constraint_upper_bounds); + n_variables = n_structural; + } else { + n_variables = problem.num_cols; + // Standard form: every row is an equality. + constraint_lower_bounds = problem.rhs; + constraint_upper_bounds = problem.rhs; + } + + std::vector coefficients = csr_A.x; + std::vector variables = csr_A.j; + std::vector offsets = csr_A.row_start; + std::vector variable_bounds(n_variables); + std::vector cpufj_variable_types(n_variables); + std::vector is_binary_variable(n_variables, 0); + i_t n_integer_vars = 0; + + for (i_t j = 0; j < n_variables; ++j) { + variable_bounds[j] = f_t2{problem.lower[j], problem.upper[j]}; + const auto var_type = variable_types[j]; + cpufj_variable_types[j] = + var_type == variable_type_t::CONTINUOUS ? var_t::CONTINUOUS : var_t::INTEGER; + + const bool is_integer = cpufj_variable_types[j] == var_t::INTEGER; + const bool is_binary = is_integer && + std::abs(problem.lower[j] - f_t{0}) <= settings.integer_tol && + std::abs(problem.upper[j] - f_t{1}) <= settings.integer_tol; + if (is_integer) { ++n_integer_vars; } + if (is_binary) { is_binary_variable[j] = 1; } + } + + const i_t nnz = static_cast(variables.size()); + csc_matrix_t reverse_csc(n_constraints, n_variables, nnz); + csr_A.to_compressed_col(reverse_csc); + std::vector reverse_coefficients = std::move(reverse_csc.x); + std::vector reverse_constraints = std::move(reverse_csc.i); + std::vector reverse_offsets = std::move(reverse_csc.col_start); + + std::vector projected_start(n_variables, f_t{0}); + for (i_t j = 0; j < n_variables; ++j) { + f_t value = j < static_cast(start_assignment.size()) ? start_assignment[j] : f_t{0}; + value = std::clamp(value, problem.lower[j], problem.upper[j]); + if (variable_types[j] != variable_type_t::CONTINUOUS) { + value = std::clamp(std::round(value), problem.lower[j], problem.upper[j]); + } + projected_start[j] = value; + } + + fj_settings_t fj_settings; + fj_settings.mode = fj_mode_t::EXIT_NON_IMPROVING; + fj_settings.n_of_minimums_for_exit = std::numeric_limits::max(); + fj_settings.time_limit = std::numeric_limits::infinity(); + fj_settings.iteration_limit = std::numeric_limits::max(); + fj_settings.update_weights = true; + fj_settings.feasibility_run = false; + fj_settings.seed = seed >= 0 ? seed : cuopt::seed_generator::get_seed(); + + auto fj_cpu = std::make_unique>(preemption_flag); + fj_cpu->settings = fj_settings; + auto problem_data = std::make_shared>(); + fj_cpu->problem = problem_data; + problem_data->tolerances = tolerances; + problem_data->n_variables = n_variables; + problem_data->n_constraints = n_constraints; + problem_data->nnz = nnz; + problem_data->objective_scaling_factor = problem.obj_scale; + problem_data->objective_offset = problem.obj_constant; + + problem_data->reverse_coefficients = std::move(reverse_coefficients); + problem_data->reverse_constraints = std::move(reverse_constraints); + problem_data->reverse_offsets = std::move(reverse_offsets); + problem_data->coefficients = std::move(coefficients); + problem_data->offsets = std::move(offsets); + problem_data->variables = std::move(variables); + problem_data->h_obj_coeffs = + std::vector(problem.objective.begin(), problem.objective.begin() + n_variables); + fj_cpu->h_var_bounds = std::move(variable_bounds); + problem_data->cstr_lb = std::move(constraint_lower_bounds); + problem_data->cstr_ub = std::move(constraint_upper_bounds); + problem_data->h_var_types = std::move(cpufj_variable_types); + fj_cpu->h_is_binary_variable = std::move(is_binary_variable); + + initialize_climber_state( + *fj_cpu, *problem_data, std::move(projected_start), n_integer_vars, tolerances); + return fj_cpu; +} + +template +std::unique_ptr> init_fj_cpu_from_host_model( + i_t n_variables, + i_t n_constraints, + i_t nnz, + bool maximize, + f_t objective_scaling_factor, + f_t objective_offset, + std::vector coefficients, + std::vector variables, + std::vector offsets, + std::vector objective_coefficients, + std::vector variable_lower_bounds, + std::vector variable_upper_bounds, + std::vector constraint_lower_bounds, + std::vector constraint_upper_bounds, + std::vector constraint_bounds, + std::vector row_types, + std::vector variable_types, + const typename mip_solver_settings_t::tolerances_t& tolerances, + std::atomic& preemption_flag, + fj_settings_t settings) +{ + using f_t2 = typename type_2::type; + + raft::common::nvtx::range scope("init_fj_cpu_from_host_model"); + + cuopt_assert(coefficients.size() == (size_t)nnz, "coefficient size mismatch"); + cuopt_assert(variables.size() == (size_t)nnz, "variable index size mismatch"); + cuopt_assert(offsets.size() == (size_t)(n_constraints + 1), "constraint offset size mismatch"); + cuopt_assert(!offsets.empty() && offsets.front() == 0, "invalid first constraint offset"); + cuopt_assert(offsets.back() == nnz, "invalid final constraint offset"); + cuopt_assert(std::is_sorted(offsets.begin(), offsets.end()), "unsorted constraint offsets"); + cuopt_assert( + std::all_of(variables.begin(), + variables.end(), + [n_variables](i_t variable) { return variable >= 0 && variable < n_variables; }), + "variable index out of range"); + cuopt_assert(objective_coefficients.size() == (size_t)n_variables, "objective size mismatch"); + cuopt_assert(variable_lower_bounds.empty() || variable_lower_bounds.size() == (size_t)n_variables, + "variable lower bound size mismatch"); + cuopt_assert(variable_upper_bounds.empty() || variable_upper_bounds.size() == (size_t)n_variables, + "variable upper bound size mismatch"); + + if (constraint_lower_bounds.empty() && constraint_upper_bounds.empty()) { + cuopt_assert(row_types.size() == (size_t)n_constraints, "row type size mismatch"); + cuopt_assert(constraint_bounds.size() == (size_t)n_constraints, + "constraint bound size mismatch"); + constraint_lower_bounds.resize(n_constraints); + constraint_upper_bounds.resize(n_constraints); + for (i_t row = 0; row < n_constraints; ++row) { + const f_t bound = constraint_bounds[row]; + if (row_types[row] == 'E') { + constraint_lower_bounds[row] = bound; + constraint_upper_bounds[row] = bound; + } else if (row_types[row] == 'G') { + constraint_lower_bounds[row] = bound; + constraint_upper_bounds[row] = std::numeric_limits::infinity(); + } else { + cuopt_assert(row_types[row] == 'L', "invalid row type"); + constraint_lower_bounds[row] = -std::numeric_limits::infinity(); + constraint_upper_bounds[row] = bound; + } + } + } else { + cuopt_assert(constraint_lower_bounds.size() == (size_t)n_constraints, + "constraint lower bound size mismatch"); + cuopt_assert(constraint_upper_bounds.size() == (size_t)n_constraints, + "constraint upper bound size mismatch"); + } + + if (variable_lower_bounds.empty()) { variable_lower_bounds.assign(n_variables, f_t{0}); } + if (variable_upper_bounds.empty()) { + variable_upper_bounds.assign(n_variables, std::numeric_limits::infinity()); + } + if (variable_types.empty()) { variable_types.assign(n_variables, var_t::CONTINUOUS); } + cuopt_assert(variable_types.size() == (size_t)n_variables, "variable type size mismatch"); + + if (maximize) { + std::transform(objective_coefficients.begin(), + objective_coefficients.end(), + objective_coefficients.begin(), + std::negate{}); + } + + std::vector variable_bounds(n_variables); + std::vector is_binary_variable(n_variables, 0); + std::vector binary_indices; + binary_indices.reserve(n_variables); + i_t n_integer_vars = 0; + for (i_t variable = 0; variable < n_variables; ++variable) { + f_t lower = variable_lower_bounds[variable]; + f_t upper = variable_upper_bounds[variable]; + const bool is_integer = variable_types[variable] == var_t::INTEGER; + if (is_integer) { + lower = std::ceil(lower); + upper = std::floor(upper); + ++n_integer_vars; + } + cuopt_assert(lower <= upper, "crossing variable bounds"); + variable_bounds[variable] = f_t2{lower, upper}; + if (is_integer && lower == f_t{0} && upper == f_t{1}) { + is_binary_variable[variable] = 1; + binary_indices.push_back(variable); + } + } + + csr_matrix_t csr(n_constraints, n_variables, nnz); + csr.x = coefficients; + csr.j = variables; + csr.row_start = offsets; + csc_matrix_t csc(n_constraints, n_variables, nnz); + csr.to_compressed_col(csc); + + std::vector assignment(n_variables, f_t{0}); + for (i_t variable = 0; variable < n_variables; ++variable) { + f_t value = std::clamp( + f_t{0}, get_lower(variable_bounds[variable]), get_upper(variable_bounds[variable])); + if (variable_types[variable] == var_t::INTEGER) { value = std::round(value); } + assignment[variable] = value; + } + + auto fj_cpu = std::make_unique>(preemption_flag); + fj_cpu->settings = settings; + auto problem_data = std::make_shared>(); + fj_cpu->problem = problem_data; + problem_data->tolerances = tolerances; + problem_data->n_variables = n_variables; + problem_data->n_constraints = n_constraints; + problem_data->nnz = nnz; + problem_data->objective_scaling_factor = + maximize ? -objective_scaling_factor : objective_scaling_factor; + problem_data->objective_offset = maximize ? -objective_offset : objective_offset; + + problem_data->reverse_coefficients = std::move(csc.x); + problem_data->reverse_constraints = std::move(csc.i); + problem_data->reverse_offsets = std::move(csc.col_start); + problem_data->coefficients = std::move(coefficients); + problem_data->offsets = std::move(offsets); + problem_data->variables = std::move(variables); + problem_data->h_obj_coeffs = std::move(objective_coefficients); + fj_cpu->h_var_bounds = std::move(variable_bounds); + problem_data->cstr_lb = std::move(constraint_lower_bounds); + problem_data->cstr_ub = std::move(constraint_upper_bounds); + problem_data->h_var_types = std::move(variable_types); + fj_cpu->h_is_binary_variable = std::move(is_binary_variable); + fj_cpu->h_binary_indices = std::move(binary_indices); + + initialize_climber_state( + *fj_cpu, *problem_data, std::move(assignment), n_integer_vars, tolerances); + return fj_cpu; +} + +template +std::unique_ptr> init_fj_cpu_clone( + const fj_cpu_climber_t& tmpl, + std::atomic& preemption_flag, + fj_settings_t settings) +{ + raft::common::nvtx::range scope("init_fj_cpu_clone"); + + auto fj_cpu = std::make_unique>(preemption_flag); + + std::vector default_weights(tmpl.problem->n_constraints, 1.0); + init_fj_cpu_from_template(*fj_cpu, tmpl, default_weights, default_weights, f_t{0}); + fj_cpu->settings = settings; + + return fj_cpu; +} + +#if MIP_INSTANTIATE_FLOAT +template std::unique_ptr> init_fj_cpu_from_host_lp( + const lp_problem_t&, + const std::vector&, + int, + const std::vector&, + const simplex_solver_settings_t&, + std::atomic&, + int64_t); +template std::unique_ptr> init_fj_cpu_from_host_model( + int, + int, + int, + bool, + float, + float, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + const typename mip_solver_settings_t::tolerances_t&, + std::atomic&, + fj_settings_t); +template void finalize_fj_cpu_host_initialization( + fj_cpu_climber_t&, + fj_cpu_problem_t&, + int, + int, + int, + int, + const typename mip_solver_settings_t::tolerances_t&); +template std::unique_ptr> init_fj_cpu_clone( + const fj_cpu_climber_t&, std::atomic&, fj_settings_t); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template std::unique_ptr> init_fj_cpu_from_host_lp( + const lp_problem_t&, + const std::vector&, + int, + const std::vector&, + const simplex_solver_settings_t&, + std::atomic&, + int64_t); +template std::unique_ptr> init_fj_cpu_from_host_model( + int, + int, + int, + bool, + double, + double, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + std::vector, + const typename mip_solver_settings_t::tolerances_t&, + std::atomic&, + fj_settings_t); +template void finalize_fj_cpu_host_initialization( + fj_cpu_climber_t&, + fj_cpu_problem_t&, + int, + int, + int, + int, + const typename mip_solver_settings_t::tolerances_t&); +template std::unique_ptr> init_fj_cpu_clone( + const fj_cpu_climber_t&, std::atomic&, fj_settings_t); +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/climber.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/climber.hpp new file mode 100644 index 0000000000..8a252387db --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/climber.hpp @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "state.hpp" + +namespace cuopt::mathematical_optimization::simplex { + +template +struct lp_problem_t; + +template +struct simplex_solver_settings_t; + +enum class variable_type_t : int8_t; + +} // namespace cuopt::mathematical_optimization::simplex + +namespace cuopt::mathematical_optimization::mip { + +template +std::unique_ptr> init_fj_cpu_from_host_lp( + const simplex::lp_problem_t& problem, + const std::vector& variable_types, + i_t n_structural, + const std::vector& start_assignment, + const simplex::simplex_solver_settings_t& settings, + std::atomic& preemption_flag, + int64_t seed); + +template +std::unique_ptr> init_fj_cpu_from_host_model( + i_t n_variables, + i_t n_constraints, + i_t nnz, + bool maximize, + f_t objective_scaling_factor, + f_t objective_offset, + std::vector coefficients, + std::vector variables, + std::vector offsets, + std::vector objective_coefficients, + std::vector variable_lower_bounds, + std::vector variable_upper_bounds, + std::vector constraint_lower_bounds, + std::vector constraint_upper_bounds, + std::vector constraint_bounds, + std::vector row_types, + std::vector variable_types, + const typename mip_solver_settings_t::tolerances_t& tolerances, + std::atomic& preemption_flag, + fj_settings_t settings); + +template +void finalize_fj_cpu_host_initialization( + fj_cpu_climber_t&, + fj_cpu_problem_t&, + i_t, + i_t, + i_t, + i_t, + const typename mip_solver_settings_t::tolerances_t&); +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/internal.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/internal.hpp new file mode 100644 index 0000000000..2035a06893 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/internal.hpp @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#ifdef CPUFJ_NVTX_RANGES +#define CPUFJ_NVTX_RANGE(name) raft::common::nvtx::range CPUFJ_NVTX_UNIQUE_NAME(nvtx_scope_)(name) +#define CPUFJ_NVTX_UNIQUE_NAME(base) CPUFJ_NVTX_CONCAT(base, __LINE__) +#define CPUFJ_NVTX_CONCAT(a, b) CPUFJ_NVTX_CONCAT_INNER(a, b) +#define CPUFJ_NVTX_CONCAT_INNER(a, b) a##b +#else +#define CPUFJ_NVTX_RANGE(name) ((void)0) +#endif + +namespace cuopt::mathematical_optimization::mip { + +using simplex::lp_problem_t; +using simplex::simplex_solver_settings_t; +using simplex::variable_type_t; + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/loop.cpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/loop.cpp new file mode 100644 index 0000000000..bafb3bbf72 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/loop.cpp @@ -0,0 +1,261 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "audit.hpp" +#include "internal.hpp" +#include "problem.hpp" +#include "search/api.hpp" +#include "search/escape.hpp" +#include "search/moves.hpp" +#include "search/score.hpp" +#include "search/update.hpp" +#include "setup/structure.hpp" + +#include + +namespace cuopt::mathematical_optimization::mip { + +template +void cpufj_solve(fj_cpu_climber_t* fj_cpu, double time_limit, double work_unit_limit) +{ + if (try_cpufj_binary_solve(*fj_cpu, time_limit, work_unit_limit)) return; + + // Past this point the search runs on one-sided rows. Everything above reasons about the original + // model, which is why the rows are built here and not at construction. + build_one_sided_rows(*fj_cpu); + + // Publish a feasible structural start, then keep the lane active for objective improvement. + if (fj_cpu->violated_constraints.empty() && check_variable_feasibility(*fj_cpu)) { + fj_cpu->h_best_assignment = fj_cpu->h_assignment; + fj_cpu->h_best_objective = + fj_cpu->h_incumbent_objective - fj_cpu->settings.parameters.breakthrough_move_epsilon; + fj_cpu->feasible_found = true; + report_cpu_incumbent(*fj_cpu); + } + + [[maybe_unused]] i_t local_mins = 0; + const auto loop_start = std::chrono::steady_clock::now(); + + fj_cpu->rng.set_seed(fj_cpu->settings.seed); + + // Initialize feature tracking + fj_cpu->iterations_since_best = 0; + + // The recompute is O(nnz), so a fixed period costs a growing share of the budget. + cuopt_assert(fj_cpu->settings.parameters.lhs_refresh_period > 0, + "lhs_refresh_period should be positive"); + const i_t nnz_stretch = std::min(fj_cpu->problem->nnz / fj_cpu->hp.nnz_per_refresh_stretch, + fj_cpu->hp.max_refresh_stretch); + const i_t refresh_period = fj_cpu->settings.parameters.lhs_refresh_period * (1 + nnz_stretch); + // const i_t refresh_period = 5000 * (1 + nnz_stretch); + cuopt_assert(refresh_period > 0, "refresh period overflowed"); + fj_cpu->lhs_refresh_period_used = refresh_period; + + // Whatever the start left behind, these rows are satisfiable on their own, so the walk should not + // start with them in the violated set competing for the sampler's attention. + for (i_t var : fj_cpu->epigraph_vars) { + const f_t current = fj_cpu->h_assignment[var]; + const f_t delta = project_epigraph_variable(*fj_cpu, var) - current; + if (delta == f_t{0}) continue; + if (!fj_cpu->move_numerically_stable( + current, current + delta, fj_cpu->total_violations, fj_cpu->total_violations)) + continue; + apply_move(*fj_cpu, var, delta, false); + ++fj_cpu->n_epigraph_projections; + } + + while (!fj_cpu->halted && !fj_cpu->preemption_flag.load()) { + const double elapsed = + std::chrono::duration(std::chrono::steady_clock::now() - loop_start).count(); + if (elapsed > time_limit) { + CUOPT_LOG_TRACE("%sTime limit of %.4f seconds reached, breaking loop at iteration %d", + fj_cpu->log_prefix.c_str(), + time_limit, + fj_cpu->iterations); + break; + } + if (fj_cpu->iterations >= fj_cpu->settings.iteration_limit) { + CUOPT_LOG_TRACE("%sIteration limit of %d reached, breaking loop at iteration %d", + fj_cpu->log_prefix.c_str(), + fj_cpu->settings.iteration_limit, + fj_cpu->iterations); + break; + } + + // periodically recompute the slacks and violation scores + // to correct any accumulated numerical errors + if (fj_cpu->trigger_early_lhs_recomputation) { + ++fj_cpu->n_lhs_recompute_bigval; + recompute_slack(*fj_cpu); + fj_cpu->trigger_early_lhs_recomputation = false; + } else if (fj_cpu->iterations % refresh_period == 0) { + ++fj_cpu->n_lhs_recompute_periodic; + recompute_slack(*fj_cpu); + } + + fj_move_t move = fj_move_t{-1, 0}; + fj_staged_score_t score = fj_staged_score_t::invalid(); + bool is_lift = false; + bool is_mtm_viol = false; + bool is_mtm_sat = false; + + // Perform lift moves + fj_move_t lift_companion = fj_move_t{-1, 0}; + if (fj_cpu->violated_constraints.empty()) { + thrust::tie(move, score) = find_lift_move(*fj_cpu); + if (score > fj_staged_score_t::zero()) { + is_lift = true; + } else { + // Pairs are only reachable once no single improving flip preserves feasibility. + fj_move_t first, second; + fj_staged_score_t pair_score; + thrust::tie(first, second, pair_score) = find_lift_2opt_move(*fj_cpu); + if (pair_score > fj_staged_score_t::zero()) { + move = first; + lift_companion = second; + score = pair_score; + is_lift = true; + } + } + } + // Regular MTM + if (!(score > fj_staged_score_t::zero())) { + thrust::tie(move, score) = find_mtm_move_viol(*fj_cpu, fj_cpu->mtm_viol_samples); + if (score > fj_staged_score_t::zero()) is_mtm_viol = true; + } + // try with MTM in satisfied constraints + if (fj_cpu->feasible_found && !(score > fj_staged_score_t::zero())) { + thrust::tie(move, score) = find_mtm_move_sat(*fj_cpu, fj_cpu->mtm_sat_samples); + if (score > fj_staged_score_t::zero()) is_mtm_sat = true; + } + + // The scorers target one row at a time, so on an epigraph variable they climb toward the bound + // its rows already imply. The projection lands there in one move at the same O(degree) cost. + if (move.var_idx >= 0 && fj_cpu->epigraph_push[move.var_idx] != 0) { + const f_t projected = + project_epigraph_variable(*fj_cpu, move.var_idx) - (f_t)fj_cpu->h_assignment[move.var_idx]; + if (projected != f_t{0}) { + move.value = projected; + ++fj_cpu->n_epigraph_projections; + } + } + + // if we're in the feasible region but haven't found improvements in the last n iterations, + // perturb + bool should_perturb = false; + if (fj_cpu->violated_constraints.empty() && + fj_cpu->iterations_since_best > fj_cpu->perturb_interval) { + should_perturb = true; + // Without this the counter stays above the interval and every later iteration perturbs. + fj_cpu->iterations_since_best = 0; + } + + if (score > fj_staged_score_t::zero() && !should_perturb) { + apply_move(*fj_cpu, move.var_idx, move.value, false); + if (lift_companion.var_idx >= 0) { + apply_move(*fj_cpu, lift_companion.var_idx, lift_companion.value, false); + } + // Track move types + } else { + update_weights(*fj_cpu); + if (should_perturb) { + perturb(*fj_cpu); + invalidate_mtm_cache(*fj_cpu); + } + + if (!fj_cpu->violated_constraints.empty()) { + thrust::tie(move, score) = + find_mtm_move_viol(*fj_cpu, 1, true); // pick a single random violated constraint + i_t var_idx = move.var_idx >= 0 ? move.var_idx : 0; + f_t delta = move.var_idx >= 0 ? move.value : 0; + apply_move(*fj_cpu, var_idx, delta, true); + } else { + // Feasible and stuck with nothing violated to move against: find_mtm_move_viol above + // would sample an empty set and force a delta-0 no-op that still bumps every row version + // the fallback variable touches. A forced satisfied-row move is a real step instead, and + // when even that finds nothing the iteration is simply skipped rather than faked. + thrust::tie(move, score) = find_mtm_move_sat(*fj_cpu, fj_cpu->mtm_sat_samples, true); + if (move.var_idx >= 0) { apply_move(*fj_cpu, move.var_idx, move.value, true); } + } + ++local_mins; + } + + if (fj_cpu->log_interval && fj_cpu->iterations % fj_cpu->log_interval == 0) { + CUOPT_LOG_DEBUG( + "%sCPUFJ iteration: %d/%d, local mins: %d, best_objective: %g, viol: %zu, obj weight %g, " + "maxw %g", + fj_cpu->log_prefix.c_str(), + fj_cpu->iterations, + fj_cpu->settings.iteration_limit != std::numeric_limits::max() + ? fj_cpu->settings.iteration_limit + : -1, + local_mins, + fj_cpu->get_user_objective(fj_cpu->h_best_objective), + fj_cpu->violated_constraints.size(), + fj_cpu->h_objective_weight, + fj_cpu->max_weight); + } + + if (fj_cpu->iterations % 100 == 0 && fj_cpu->iterations > 0) { + // Use cumulative byte counts (collect() without flush). Each window's contribution to + // work_units_elapsed therefore grows roughly with the running total of bytes touched, + // i.e. quadratically in iterations rather than linearly. This is intentional: the + // memory_aggregator is calibrated for medium/large MIPs, and a strictly-linear scheme + // forces tiny instances (few KB per iteration) to run for tens of seconds before the + // accumulated bytes cross a 0.5 horizon, causing the deterministic producer_sync to + // stall and B&B to time out on instances that should solve in milliseconds. The + // accumulation is still deterministic across runs of the same problem, which is what + // the producer_sync contract actually requires. + auto [loads, stores] = fj_cpu->memory_aggregator.collect(); + double biased_work = (loads + stores) * fj_cpu->work_unit_bias / 1e10; + fj_cpu->work_units_elapsed += biased_work; + + if (fj_cpu->producer_sync != nullptr) { fj_cpu->producer_sync->notify_progress(); } + if (fj_cpu->work_units_elapsed >= work_unit_limit) { break; } + } + + cuopt_func_call(sanity_checks(*fj_cpu)); + if (fj_audit_every_iteration) { + cuopt_func_call(audit_incremental_state(*fj_cpu, "iteration")); + } + fj_cpu->iterations++; + fj_cpu->iterations_since_best++; + } + const double total_time = + std::chrono::duration(std::chrono::steady_clock::now() - loop_start).count(); + [[maybe_unused]] double avg_time_per_iter = + fj_cpu->iterations > 0 ? total_time / fj_cpu->iterations : 0; + CUOPT_LOG_TRACE("%sCPUFJ Average time per iteration: %.8fms", + fj_cpu->log_prefix.c_str(), + avg_time_per_iter * 1000.0); +} + +#if MIP_INSTANTIATE_FLOAT +template void cpufj_solve(fj_cpu_climber_t*, double, double); +template void report_cpu_incumbent(fj_cpu_climber_t&, + float, + const std::vector&, + double); +template void report_cpu_incumbent(fj_cpu_climber_t&); +template void recompute_lhs(fj_cpu_climber_t&); +template void recompute_slack(fj_cpu_climber_t&); +template void invalidate_mtm_cache(fj_cpu_climber_t&); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template void cpufj_solve(fj_cpu_climber_t*, double, double); +template void report_cpu_incumbent(fj_cpu_climber_t&, + double, + const std::vector&, + double); +template void report_cpu_incumbent(fj_cpu_climber_t&); +template void recompute_lhs(fj_cpu_climber_t&); +template void recompute_slack(fj_cpu_climber_t&); +template void invalidate_mtm_cache(fj_cpu_climber_t&); +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/portfolio.cpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/portfolio.cpp new file mode 100644 index 0000000000..b68d7460ec --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/portfolio.cpp @@ -0,0 +1,89 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "climber.hpp" +#include "internal.hpp" +#include "problem.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +void fj_cpu_worker_t::fj_cpu_deleter_t::operator()(fj_cpu_climber_t* ptr) const +{ + delete ptr; +} + +template +void fj_cpu_worker_t::create_worker( + const lp_problem_t& problem, + const std::vector& variable_types, + i_t n_structural, + const std::vector& start_assignment, + const simplex_solver_settings_t& settings, + std::string log_prefix, + int64_t seed) +{ + auto new_climber = init_fj_cpu_from_host_lp( + problem, variable_types, n_structural, start_assignment, settings, preemption_flag, seed); + fj_cpu.reset(new_climber.release()); + fj_cpu->log_prefix = std::move(log_prefix); + fj_cpu->improvement_callback = improvement_callback; + fj_cpu->halted = false; + preemption_flag = false; + is_initialized = true; +} + +template +void fj_cpu_worker_t::run_async(f_t time_limit, double work_unit_limit) +{ + if (!is_initialized) return; + + auto& fj_ptr = fj_cpu; +#pragma omp task shared(fj_cpu, is_initialized, fj_ptr) firstprivate(time_limit, work_unit_limit) \ + priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : fj_ptr) + { + if (is_initialized) { cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); } + } +} + +template +void fj_cpu_worker_t::run_sync(f_t time_limit, double work_unit_limit) +{ + if (!is_initialized) return; + cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); + is_initialized = false; + fj_cpu.reset(); +} + +template +void fj_cpu_worker_t::stop() +{ + if (!is_initialized) return; + + preemption_flag = true; + + auto& fj_ptr = fj_cpu; +#pragma omp taskwait depend(in : fj_ptr) + is_initialized = false; + fj_cpu.reset(); +} + +template +void fj_cpu_worker_t::send_stop_signal() +{ + preemption_flag = true; +} + +#if MIP_INSTANTIATE_FLOAT +template struct fj_cpu_worker_t; +#endif + +#if MIP_INSTANTIATE_DOUBLE +template struct fj_cpu_worker_t; +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/problem.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/problem.hpp new file mode 100644 index 0000000000..2c79e3a9e6 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/problem.hpp @@ -0,0 +1,46 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include "internal.hpp" +#include "state.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +inline std::pair model_range_for_var(fj_cpu_climber_t& fj_cpu, i_t var_idx) +{ + cuopt_assert(var_idx >= 0 && var_idx < fj_cpu.problem->n_variables, + "Variable should be within the range"); + return std::make_pair(fj_cpu.problem->reverse_offsets[var_idx], + fj_cpu.problem->reverse_offsets[var_idx + 1]); +} + +template +inline std::pair model_range_for_row(fj_cpu_climber_t& fj_cpu, i_t cstr_idx) +{ + cuopt_assert(cstr_idx >= 0 && cstr_idx < fj_cpu.problem->n_constraints, "row out of range"); + return std::make_pair(fj_cpu.problem->offsets[cstr_idx], fj_cpu.problem->offsets[cstr_idx + 1]); +} + +template +inline bool check_variable_within_bounds(fj_cpu_climber_t& fj_cpu, i_t var_idx, f_t val) +{ + const f_t int_tol = fj_cpu.problem->tolerances.integrality_tolerance; + auto bounds = fj_cpu.h_var_bounds[var_idx].get(); + bool within_bounds = val <= (get_upper(bounds) + int_tol) && val >= (get_lower(bounds) - int_tol); + return within_bounds; +} + +template +inline bool is_integer_var(fj_cpu_climber_t& fj_cpu, i_t var_idx) +{ + return var_t::INTEGER == fj_cpu.problem->h_var_types[var_idx]; +} + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/search/api.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/api.hpp new file mode 100644 index 0000000000..c4a0a0850d --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/api.hpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "../state.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +void report_cpu_incumbent(fj_cpu_climber_t& c, + f_t objective, + const std::vector& assignment, + double work_units); + +template +void report_cpu_incumbent(fj_cpu_climber_t& c); + +template +void recompute_lhs(fj_cpu_climber_t& fj_cpu); + +template +void recompute_slack(fj_cpu_climber_t& fj_cpu); + +template +void invalidate_mtm_cache(fj_cpu_climber_t& fj_cpu); + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/search/escape.cpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/escape.cpp new file mode 100644 index 0000000000..725f2a7679 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/escape.cpp @@ -0,0 +1,64 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "escape.hpp" +#include "../internal.hpp" +#include "../problem.hpp" +#include "api.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +void randomize_variable(fj_cpu_climber_t& fj_cpu, i_t var_idx, cuopt::pcgenerator_t& rng) +{ + f_t lb = std::max(get_lower(fj_cpu.h_var_bounds[var_idx].get()), -1e7); + f_t ub = std::min(get_upper(fj_cpu.h_var_bounds[var_idx].get()), 1e7); + f_t val = lb + (ub - lb) * rng.next_double(); + if (is_integer_var(fj_cpu, var_idx)) { + lb = std::ceil(lb); + ub = std::floor(ub); + val = std::round(val); + } + val = std::clamp(val, + get_lower(fj_cpu.h_var_bounds[var_idx].get()), + get_upper(fj_cpu.h_var_bounds[var_idx].get())); + + fj_cpu.h_assignment[var_idx] = val; +} + +template +void perturb(fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::perturb"); + if (fj_cpu.feasible_found) { + cuopt_assert(fj_cpu.h_assignment.size() == fj_cpu.h_best_assignment.size(), + "incumbent_assignment span would be invalidated"); + fj_cpu.h_assignment = fj_cpu.h_best_assignment; + } + + const i_t n_kick = std::max(1, fj_cpu.perturb_vars); + std::vector sampled_vars = fj_cpu.problem->h_objective_vars; + fj_cpu.rng.shuffle(sampled_vars); + sampled_vars.resize(std::min(sampled_vars.size(), (size_t)n_kick)); + cuopt::pcgenerator_t rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); + + for (auto var_idx : sampled_vars) + randomize_variable(fj_cpu, var_idx, rng); + + ++fj_cpu.n_lhs_recompute_perturb; + recompute_slack(fj_cpu); +} + +#if MIP_INSTANTIATE_FLOAT +template void perturb(fj_cpu_climber_t&); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template void perturb(fj_cpu_climber_t&); +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/search/escape.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/escape.hpp new file mode 100644 index 0000000000..7fb45ba1dd --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/escape.hpp @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "../state.hpp" + +namespace cuopt::mathematical_optimization::mip { +template +void perturb(fj_cpu_climber_t&); +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/search/moves.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/moves.hpp new file mode 100644 index 0000000000..915d7225b8 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/moves.hpp @@ -0,0 +1,547 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include "../internal.hpp" +#include "score.hpp" + +namespace cuopt::mathematical_optimization::mip { + +struct two_opt_move_t { + fj_move_t first{-1, 0}; + fj_move_t second{-1, 0}; + fj_staged_score_t score{fj_staged_score_t::invalid()}; + double objective_delta{0}; + int age{std::numeric_limits::max()}; + + bool operator>(const two_opt_move_t& other) const + { + if (score != other.score) return score > other.score; + if (objective_delta != other.objective_delta) return objective_delta < other.objective_delta; + if (age != other.age) return age < other.age; + if (first.var_idx != other.first.var_idx) return first.var_idx < other.first.var_idx; + return second.var_idx < other.second.var_idx; + } +}; + +template +static fj_staged_score_t two_opt_compute_pair_score( + fj_cpu_climber_t& fj_cpu, i_t first, f_t first_delta, i_t second, f_t second_delta) +{ + auto& row_deltas = fj_cpu.two_opt_row_deltas; + row_deltas.clear(); + const fj_move_t endpoints[2] = {{first, first_delta}, {second, second_delta}}; + for (const auto& [var_idx, delta] : endpoints) { + const auto [offset_begin, offset_end] = fj_cpu.range_for_variable(var_idx); + fj_cpu.nnz_processed_window += offset_end - offset_begin; + for (i_t i = offset_begin; i < offset_end; ++i) { + const i_t cstr_idx = fj_cpu.h_reverse_constraints[i]; + const f_t coeff = fj_cpu.h_reverse_coefficients[i]; + row_deltas.emplace_back(cstr_idx, coeff * delta); + } + } + // Brings the entries of a shared row next to each other + std::sort(row_deltas.begin(), row_deltas.end()); + + f_t base_feas_sum = 0; + f_t bonus_robust_sum = 0; + for (size_t pos = 0; pos < row_deltas.size();) { + const i_t cstr_idx = row_deltas[pos].first; + f_t lhs_delta = 0; + do { + lhs_delta += row_deltas[pos++].second; + } while (pos < row_deltas.size() && row_deltas[pos].first == cstr_idx); + + // The coefficients are already folded into lhs_delta, hence the unit coefficient + auto [cstr_base_feas, cstr_bonus_robust] = + feas_score_constraint(fj_cpu, + lhs_delta, + 1, + fj_cpu.row_state()[cstr_idx].slack, + fj_cpu.row_state()[cstr_idx].weight); + base_feas_sum += cstr_base_feas; + bonus_robust_sum += cstr_bonus_robust; + } + + const f_t obj_diff = fj_cpu.problem->h_obj_coeffs[first] * first_delta + + fj_cpu.problem->h_obj_coeffs[second] * second_delta; + f_t base_obj = 0; + if (obj_diff < 0) + base_obj = fj_cpu.h_objective_weight; + else if (obj_diff > 0) + base_obj = -fj_cpu.h_objective_weight; + + f_t bonus_breakthrough = 0; + bool old_obj_better = fj_cpu.h_incumbent_objective < fj_cpu.h_best_objective; + bool new_obj_better = fj_cpu.h_incumbent_objective + obj_diff < fj_cpu.h_best_objective; + if (!old_obj_better && new_obj_better) + bonus_breakthrough += fj_cpu.h_objective_weight; + else if (old_obj_better && !new_obj_better) + bonus_breakthrough -= fj_cpu.h_objective_weight; + + fj_staged_score_t score; + score.base = std::round(base_obj + base_feas_sum); + score.bonus = std::round(bonus_breakthrough + bonus_robust_sum); + return score; +} + +template +static thrust::tuple find_mtm_move( + fj_cpu_climber_t& fj_cpu, const std::vector& target_cstrs, bool localmin = false) +{ + CPUFJ_NVTX_RANGE("CPUFJ::find_mtm_move"); + + cuopt::pcgenerator_t rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); + + fj_move_t best_move = fj_move_t{-1, 0}; + fj_staged_score_t best_score = fj_staged_score_t::invalid(); + double best_objective_delta = std::numeric_limits::infinity(); + auto improves_best = [&](fj_staged_score_t score, i_t var, f_t delta) { + if (score > best_score) return true; + // Magnitude ordering paid on the dedicated objective trajectories but displaced useful neutral + // moves on feasibility-biased lanes. Keep it behind the same high-pressure persona gate. + if (fj_cpu.h_objective_weight < f_t{16} || !(score == best_score)) return false; + const double objective_delta = static_cast(fj_cpu.problem->h_obj_coeffs[var]) * delta; + return objective_delta < best_objective_delta; + }; + auto store_best = [&](fj_staged_score_t score, i_t var, f_t delta) { + best_score = score; + best_move = fj_move_t{var, delta}; + best_objective_delta = static_cast(fj_cpu.problem->h_obj_coeffs[var]) * delta; + }; + + ++fj_cpu.n_mtm_calls; + + // Each row contributes at most its share of the sampling budget. The gate below sits inside the + // walk, so an uncapped wide row is walked in full whatever the budget says. + const i_t per_row_cap = + std::max(1, fj_cpu.nnz_samples / std::max(1, (i_t)target_cstrs.size())); + + i_t entries = 0; + for (size_t cstr_idx : target_cstrs) { + auto [offset_begin, offset_end] = fj_cpu.range_for_row((i_t)cstr_idx); + const i_t width = offset_end - offset_begin; + entries += std::min(width, per_row_cap); + fj_cpu.mtm_entries_capped += (int64_t)std::max(0, width - per_row_cap); + } + fj_cpu.mtm_row_entries += (int64_t)entries; + + // The exact sum over the candidate variables costs one random offset read each to set a single + // sampling rate. The mean reverse degree estimates it in constant time. + const f_t mean_reverse_degree = + (f_t)fj_cpu.h_coefficients.size() / (f_t)std::max(1, fj_cpu.problem->n_variables); + const f_t nnz_sum = (f_t)entries * mean_reverse_degree; + + f_t nnz_pick_probability = 1; + if (nnz_sum > (f_t)fj_cpu.nnz_samples) nnz_pick_probability = (f_t)fj_cpu.nnz_samples / nnz_sum; + + for (size_t cstr_idx : target_cstrs) { + cuopt_assert((i_t)cstr_idx < fj_cpu.n_rows, "cstr_idx is out of bounds"); + auto [offset_begin, offset_end] = fj_cpu.range_for_row((i_t)cstr_idx); + const i_t width = offset_end - offset_begin; + const i_t visit = std::min(width, per_row_cap); + const i_t start = + visit == width ? offset_begin : offset_begin + (i_t)(rng.next_u32() % (uint32_t)width); + for (i_t q = 0, i = start; q < visit; ++q, i = (i + 1 == offset_end ? offset_begin : i + 1)) { + const i_t var_idx = fj_cpu.h_variables[i]; + // early cached check + cuopt_assert(fj_cpu.cached_mtm_moves_version[i] <= fj_cpu.h_cstr_version[cstr_idx], + "cached move newer than its constraint"); + if (auto& cached_move = fj_cpu.cached_mtm_moves[i]; + cached_move.first != 0 && + fj_cpu.cached_mtm_moves_version[i] == fj_cpu.h_cstr_version[cstr_idx]) { + if (improves_best(cached_move.second, var_idx, cached_move.first)) { + if (check_variable_within_bounds( + fj_cpu, var_idx, fj_cpu.h_assignment[var_idx] + cached_move.first)) { + store_best(cached_move.second, var_idx, cached_move.first); + } + // cuopt_assert(fj_cpu.check_variable_within_bounds(var_idx, + // fj_cpu.h_assignment[var_idx] + cached_move.first), "best move is not within bounds"); + } + fj_cpu.hit_count++; + continue; + } + + // random chance to skip this nnz if there are many to consider + if (nnz_pick_probability < 1) + if (rng.next_float() > nnz_pick_probability) continue; + + f_t val = fj_cpu.h_assignment[var_idx]; + f_t new_val = val; + f_t delta = 0; + + // Special case for binary variables + if (fj_cpu.h_is_binary_variable[var_idx]) { + if (fj_cpu.flip_move_stamp[var_idx] == fj_cpu.flip_move_epoch) continue; + fj_cpu.flip_move_stamp[var_idx] = fj_cpu.flip_move_epoch; + new_val = 1 - val; + } else { + const f_t cstr_coeff = fj_cpu.h_coefficients[i]; + + const f_t delta = get_mtm_for_constraint( + cstr_coeff, fj_cpu.row_state()[cstr_idx].slack, fj_cpu.row_tolerance); + if (is_integer_var(fj_cpu, var_idx)) { + // The sign the two-sided form applied here is already folded into the coefficient. + new_val = cstr_coeff > 0 + ? std::floor(val + delta + fj_cpu.problem->tolerances.integrality_tolerance) + : std::ceil(val + delta - fj_cpu.problem->tolerances.integrality_tolerance); + } else { + new_val = val + delta; + } + // fallback + if (new_val < get_lower(fj_cpu.h_var_bounds[var_idx].get()) || + new_val > get_upper(fj_cpu.h_var_bounds[var_idx].get())) { + new_val = cstr_coeff > 0 ? get_lower(fj_cpu.h_var_bounds[var_idx].get()) + : get_upper(fj_cpu.h_var_bounds[var_idx].get()); + } + } + if (!std::isfinite(new_val)) continue; + cuopt_assert(check_variable_within_bounds(fj_cpu, var_idx, new_val), + "new_val is not within bounds"); + delta = new_val - val; + // more permissive tabu in the case of local minima + if (tabu_check(fj_cpu, var_idx, delta, localmin)) continue; + if (std::fabs(delta) < fj_cpu.row_tolerance) continue; + + auto move = fj_move_t{var_idx, delta}; + cuopt_assert(move.var_idx < fj_cpu.h_assignment.size(), "move.var_idx is out of bounds"); + cuopt_assert(move.var_idx >= 0, "move.var_idx is not positive"); + + auto [score, infeasibility] = compute_score(fj_cpu, var_idx, delta); + fj_cpu.miss_count++; + // reject this move if it would increase the target variable to a numerically unstable value + if (!fj_cpu.move_numerically_stable(val, new_val, infeasibility, fj_cpu.total_violations)) + continue; + fj_cpu.cached_mtm_moves[i] = std::make_pair(delta, score); + fj_cpu.cached_mtm_moves_version[i] = fj_cpu.h_cstr_version[cstr_idx]; + if (improves_best(score, move.var_idx, move.value)) + store_best(score, move.var_idx, move.value); + } + } + + // also consider BM moves if we have found a feasible solution at least once + if (move_type == MTMMoveType::FJ_MTM_VIOLATED && + fj_cpu.h_best_objective < std::numeric_limits::infinity() && + fj_cpu.h_incumbent_objective >= + fj_cpu.h_best_objective + fj_cpu.settings.parameters.breakthrough_move_epsilon) { + for (auto var_idx : fj_cpu.problem->h_objective_vars) { + f_t old_val = fj_cpu.h_assignment[var_idx]; + f_t new_val = fj_cpu.breakthrough_value(var_idx); + + if (fj_cpu.problem->integer_equal(new_val, old_val) || !std::isfinite(new_val)) continue; + + f_t delta = new_val - old_val; + + // Check if we already have a move for this variable + auto move = fj_move_t{var_idx, delta}; + cuopt_assert(move.var_idx < fj_cpu.h_assignment.size(), "move.var_idx is out of bounds"); + cuopt_assert(move.var_idx >= 0, "move.var_idx is not positive"); + + if (tabu_check(fj_cpu, var_idx, delta)) continue; + + auto [score, infeasibility] = compute_score(fj_cpu, var_idx, delta); + + cuopt_assert(check_variable_within_bounds(fj_cpu, var_idx, new_val), ""); + cuopt_assert(std::isfinite(delta), ""); + + if (fj_cpu.move_numerically_stable( + old_val, new_val, infeasibility, fj_cpu.total_violations)) { + if (improves_best(score, move.var_idx, move.value)) + store_best(score, move.var_idx, move.value); + } + } + } + + return thrust::make_tuple(best_move, best_score); +} + +template +void sample_with_replacement(const host_contiguous_set_t& pool, + i_t sample_size, + uint64_t seed, + std::vector& out) +{ + cuopt_assert(sample_size > 0, "invalid sample size"); + out.clear(); + const i_t pool_size = pool.size(); + if (pool_size == 0) { return; } + if (pool_size <= sample_size) { + out.assign(pool.begin(), pool.end()); + return; + } + out.reserve(sample_size); + cuopt::pcgenerator_t rng(seed); + for (i_t i = 0; i < sample_size; ++i) { + out.push_back(pool.contents[rng.next_u32() % (uint32_t)pool_size]); + } +} + +template +static thrust::tuple find_mtm_move_viol( + fj_cpu_climber_t& fj_cpu, i_t sample_size = 100, bool localmin = false) +{ + CPUFJ_NVTX_RANGE("CPUFJ::find_mtm_move_viol"); + + std::vector sampled_cstrs; + sample_with_replacement(fj_cpu.violated_constraints, + sample_size, + fj_cpu.settings.seed + fj_cpu.iterations, + sampled_cstrs); + + return find_mtm_move(fj_cpu, sampled_cstrs, localmin); +} + +template +static thrust::tuple find_mtm_move_sat( + fj_cpu_climber_t& fj_cpu, i_t sample_size = 100, bool localmin = false) +{ + CPUFJ_NVTX_RANGE("CPUFJ::find_mtm_move_sat"); + + std::vector sampled_cstrs; + sample_with_replacement(fj_cpu.satisfied_constraints, + sample_size, + fj_cpu.settings.seed + fj_cpu.iterations, + sampled_cstrs); + + return find_mtm_move(fj_cpu, sampled_cstrs, localmin); +} + +template +bool paired_flip_keeps_feasible( + fj_cpu_climber_t& fj_cpu, i_t var1, f_t delta1, i_t var2, f_t delta2) +{ + const auto range1 = fj_cpu.range_for_variable(var1); + const auto range2 = fj_cpu.range_for_variable(var2); + i_t i = range1.first, ie = range1.second; + i_t j = range2.first, je = range2.second; + + while (i < ie || j < je) { + const i_t r1 = i < ie ? (i_t)fj_cpu.h_reverse_constraints[i] : std::numeric_limits::max(); + const i_t r2 = j < je ? (i_t)fj_cpu.h_reverse_constraints[j] : std::numeric_limits::max(); + const i_t r = r1 < r2 ? r1 : r2; + + f_t change = 0; + if (r1 == r) { + change += (f_t)fj_cpu.h_reverse_coefficients[i] * delta1; + ++i; + } + if (r2 == r) { + change += (f_t)fj_cpu.h_reverse_coefficients[j] * delta2; + ++j; + } + + const f_t new_slack = (fj_cpu.row_state()[r].slack + fj_cpu.h_slack_sumcomp[r]) - change; + if (new_slack < -fj_cpu.row_tolerance) return false; + } + return true; +} + +template +static thrust::tuple find_lift_2opt_move( + fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::find_lift_2opt_move"); + cuopt_assert(fj_cpu.violated_constraints.empty(), "lift moves require a feasible incumbent"); + + fj_move_t best_first = fj_move_t{-1, 0}; + fj_move_t best_second = fj_move_t{-1, 0}; + fj_staged_score_t best_score = fj_staged_score_t::zero(); + f_t best_improvement = 0; + + const i_t n_obj = (i_t)fj_cpu.problem->h_objective_vars.size(); + if (n_obj == 0) return thrust::make_tuple(best_first, best_second, best_score); + + cuopt::pcgenerator_t rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); + const i_t n_draws = n_obj < fj_cpu.hp.two_opt_candidates ? n_obj : fj_cpu.hp.two_opt_candidates; + + for (i_t t = 0; t < n_draws; ++t) { + const i_t var1 = fj_cpu.problem->h_objective_vars[rng.next_u32() % (uint32_t)n_obj]; + if (!fj_cpu.h_is_binary_variable[var1]) continue; + + const f_t coeff1 = fj_cpu.problem->h_obj_coeffs[var1]; + const f_t val1 = fj_cpu.h_assignment[var1]; + const f_t delta1 = std::round(1.0 - 2 * val1); + if (delta1 * coeff1 >= 0) continue; + if (tabu_check(fj_cpu, var1, delta1)) continue; + + // Breaking nothing is the single-flip lift's job; breaking several rows cannot be repaired by + // one companion. + const auto range1 = fj_cpu.range_for_variable(var1); + i_t broken = -1; + bool multiple = false; + for (i_t k = range1.first; k < range1.second && !multiple; ++k) { + const i_t r = fj_cpu.h_reverse_constraints[k]; + const f_t new_slack = (fj_cpu.row_state()[r].slack + fj_cpu.h_slack_sumcomp[r]) - + (f_t)fj_cpu.h_reverse_coefficients[k] * delta1; + if (new_slack < -fj_cpu.row_tolerance) { + if (broken >= 0) + multiple = true; + else + broken = r; + } + } + if (multiple || broken < 0) continue; + + const auto row = fj_cpu.range_for_row(broken); + for (i_t k = row.first; k < row.second; ++k) { + const i_t var2 = fj_cpu.h_variables[k]; + if (var2 == var1) continue; + if (!fj_cpu.h_is_binary_variable[var2]) continue; + + const f_t coeff2 = fj_cpu.problem->h_obj_coeffs[var2]; + const f_t val2 = fj_cpu.h_assignment[var2]; + const f_t delta2 = std::round(1.0 - 2 * val2); + const f_t combined = delta1 * coeff1 + delta2 * coeff2; + if (combined >= 0) continue; + if (tabu_check(fj_cpu, var2, delta2)) continue; + if (!paired_flip_keeps_feasible(fj_cpu, var1, delta1, var2, delta2)) continue; + + // Both lift operators rank on the objective gain in its own units: the score quantization + // used elsewhere counts weights, so rounding a gain below 0.5 into it discards the move. + const f_t improvement = -combined; + if (improvement > best_improvement) { + best_improvement = improvement; + best_score.base = 1; // sign only, never compared against another operator's score + best_first = fj_move_t{var1, delta1}; + best_second = fj_move_t{var2, delta2}; + } + } + } + cuopt_assert((best_first.var_idx < 0) == (best_improvement <= 0), + "pair and score must agree on whether a move was found"); + return thrust::make_tuple(best_first, best_second, best_score); +} + +template +static thrust::tuple find_lift_move( + fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::find_lift_move"); + + fj_move_t best_move = fj_move_t{-1, 0}; + fj_staged_score_t best_score = fj_staged_score_t::zero(); + f_t best_improvement = 0; + + for (auto var_idx : fj_cpu.problem->h_objective_vars) { + cuopt_assert(var_idx < fj_cpu.problem->h_obj_coeffs.size(), "var_idx is out of bounds"); + cuopt_assert(var_idx >= 0, "var_idx is out of bounds"); + + f_t obj_coeff = fj_cpu.problem->h_obj_coeffs[var_idx]; + f_t delta = -std::numeric_limits::infinity(); + f_t val = fj_cpu.h_assignment[var_idx]; + + // special path for binary variables + if (fj_cpu.h_is_binary_variable[var_idx]) { + cuopt_assert(fj_cpu.problem->is_integer(val), "binary variable is not integer"); + cuopt_assert(fj_cpu.problem->integer_equal(val, 0) || fj_cpu.problem->integer_equal(val, 1), + "Current assignment is not binary!"); + delta = std::round(1.0 - 2 * val); + // flip move wouldn't improve + if (delta * obj_coeff >= 0) continue; + + auto [offset_begin, offset_end] = fj_cpu.range_for_variable(var_idx); + + const i_t* const rev_cstr = fj_cpu.h_reverse_constraints.data(); + const f_t* const rev_coeff = fj_cpu.h_reverse_coefficients.data(); + const f_t* const row_sumcomp = fj_cpu.h_slack_sumcomp.data(); + const typename fj_cpu_climber_t::row_state_t* const state = fj_cpu.row_state(); + + bool breaks_a_row = false; + i_t scanned = 0; + for (i_t j = offset_begin; j < offset_end; ++j) { + ++scanned; + const i_t cstr_idx = rev_cstr[j]; + const f_t cstr_coeff = rev_coeff[j]; + const f_t new_slack = (state[cstr_idx].slack + row_sumcomp[cstr_idx]) - cstr_coeff * delta; + if (new_slack < -fj_cpu.row_tolerance) { + breaks_a_row = true; + break; + } + } + + const size_t nnz_scanned = (size_t)scanned; + fj_cpu.h_reverse_constraints.byte_loads += nnz_scanned * sizeof(i_t); + fj_cpu.h_reverse_coefficients.byte_loads += nnz_scanned * sizeof(f_t); + fj_cpu.h_row_state.byte_loads += + nnz_scanned * sizeof(typename fj_cpu_climber_t::row_state_t); + fj_cpu.h_slack_sumcomp.byte_loads += nnz_scanned * sizeof(f_t); + + if (breaks_a_row) continue; + } else { + f_t lfd_lb = get_lower(fj_cpu.h_var_bounds[var_idx].get()) - val; + f_t lfd_ub = get_upper(fj_cpu.h_var_bounds[var_idx].get()) - val; + auto [offset_begin, offset_end] = fj_cpu.range_for_variable(var_idx); + for (i_t j = offset_begin; j < offset_end; j += 1) { + const i_t cstr_idx = fj_cpu.h_reverse_constraints[j]; + const f_t cstr_coeff = fj_cpu.h_reverse_coefficients[j]; + if (cstr_coeff == f_t{0}) continue; + const f_t slack = fj_cpu.row_state()[cstr_idx].slack; + cuopt_assert(!(slack < -fj_cpu.row_tolerance), "cstr should be satisfied"); + + // One bound per row here, and the sign the two-sided form carried is in the coefficient. + f_t delta_j = slack / cstr_coeff; + if (is_integer_var(fj_cpu, var_idx)) + delta_j = cstr_coeff < 0 ? std::ceil(delta_j) : std::floor(delta_j); + + // skip this variable if there is no slack + if (std::fabs(slack) <= fj_cpu.row_tolerance) { + if (cstr_coeff > 0) { + lfd_ub = 0; + } else { + lfd_lb = 0; + } + } else if (!check_variable_within_bounds(fj_cpu, var_idx, val + delta_j)) { + continue; + } else { + if (cstr_coeff < 0) { + lfd_lb = std::max(lfd_lb, delta_j); + } else { + lfd_ub = std::min(lfd_ub, delta_j); + } + } + if (lfd_lb >= lfd_ub) break; + } + + // invalid crossing bounds + if (lfd_lb >= lfd_ub) { lfd_lb = lfd_ub = 0; } + + if (!check_variable_within_bounds(fj_cpu, var_idx, val + lfd_lb)) { lfd_lb = 0; } + if (!check_variable_within_bounds(fj_cpu, var_idx, val + lfd_ub)) { lfd_ub = 0; } + + // Now that the lift move domain is computed, compute the correct lift move + cuopt_assert(std::isfinite(val), "invalid assignment value"); + delta = obj_coeff < 0 ? lfd_ub : lfd_lb; + } + + if (!std::isfinite(delta)) delta = 0; + if (fj_cpu.problem->integer_equal(delta, (f_t)0)) continue; + if (tabu_check(fj_cpu, var_idx, delta)) continue; + // The continuous branch takes its step straight from a row residual, bounded only by the + // variable's own bounds, so an unbounded variable gets an unbounded step. This is the same bar + // find_mtm_move holds its candidates to; total_violations twice because nothing here scores the + // move, which leaves only the step and value clauses meaningful. + if (!fj_cpu.move_numerically_stable( + val, val + delta, fj_cpu.total_violations, fj_cpu.total_violations)) + continue; + + cuopt_assert(delta * obj_coeff < 0, "lift move doesn't improve the objective!"); + + const f_t improvement = -obj_coeff * delta; + if (improvement > best_improvement) { + best_improvement = improvement; + best_score.base = 1; + best_move = fj_move_t{var_idx, delta}; + } + } + + cuopt_assert((best_move.var_idx < 0) == (best_improvement <= 0), + "move and score must agree on whether a move was found"); + return thrust::make_tuple(best_move, best_score); +} + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/search/score.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/score.hpp new file mode 100644 index 0000000000..d24226e03f --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/score.hpp @@ -0,0 +1,210 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include "../internal.hpp" +#include "api.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +void fj_simd_score_rows(const int32_t* rows, + const f_t* coefficients, + const f_t* row_state, + int32_t begin, + int32_t end, + f_t delta, + f_t tolerance, + f_t excess_weight, + f_t& base, + f_t& bonus); + +template +f_t get_mtm_for_constraint(f_t cstr_coeff, f_t slack, f_t row_tolerance) +{ + cuopt_assert(cstr_coeff != f_t{0}, "zero coefficient moves no row"); + const bool violated = slack < -row_tolerance; + if (move_type == MTMMoveType::FJ_MTM_VIOLATED ? !violated : violated) return f_t{0}; + return slack / cstr_coeff; +} + +template +std::pair feas_score_constraint( + fj_cpu_climber_t& fj_cpu, f_t delta, f_t cstr_coeff, f_t old_slack, f_t cstr_weight) +{ + cuopt_assert(std::isfinite(delta), "invalid delta"); + // A model may store explicit zeros, and a zero coefficient contributes nothing to the row. + cuopt_assert(std::isfinite(cstr_coeff), "invalid coefficient"); + cuopt_assert(std::isfinite(cstr_weight), "invalid weight"); + cuopt_assert(cstr_weight >= 0, "invalid weight"); + + const f_t tol = fj_cpu.row_tolerance; + const f_t new_slack = old_slack - cstr_coeff * delta; + cuopt_assert(std::isfinite(old_slack) && std::isfinite(new_slack), ""); + + const bool old_sat = old_slack > -tol; + const bool new_sat = new_slack > -tol; + + f_t base_feas = 0; + if (!old_sat && new_sat) { + base_feas += cstr_weight; + } else if (old_sat && !new_sat) { + base_feas -= cstr_weight; + } else if (!old_sat && !new_sat && new_slack > old_slack) { + // Keep the fractional excess signal. Converting through i_t made the default + // 0.5 improvement weight vanish for unit-weight rows, leaving FJ blind to + // progress on a row until a move crossed its bound. + base_feas += cstr_weight * fj_cpu.settings.parameters.excess_improvement_weight; + } else if (!old_sat && !new_sat && new_slack < old_slack) { + base_feas -= cstr_weight * fj_cpu.settings.parameters.excess_improvement_weight; + } + + f_t bonus_robust = 0; + const bool old_stable = old_slack > tol; + const bool new_stable = new_slack > tol; + if (!old_stable && new_stable) { + bonus_robust += cstr_weight; + } else if (old_stable && !new_stable) { + bonus_robust -= cstr_weight; + } + + return {base_feas, bonus_robust}; +} + +template +inline bool tabu_check(fj_cpu_climber_t& fj_cpu, + i_t var_idx, + f_t delta, + bool localmin = false) +{ + if (localmin) { + return (delta < 0 && fj_cpu.iterations == fj_cpu.h_tabu_lastinc[var_idx] + 1) || + (delta >= 0 && fj_cpu.iterations == fj_cpu.h_tabu_lastdec[var_idx] + 1); + } else { + return (delta < 0 && fj_cpu.iterations < fj_cpu.h_tabu_nodec_until[var_idx]) || + (delta >= 0 && fj_cpu.iterations < fj_cpu.h_tabu_noinc_until[var_idx]); + } +} + +template +inline std::pair compute_score(fj_cpu_climber_t& fj_cpu, + i_t var_idx, + f_t delta) +{ + f_t obj_diff = fj_cpu.problem->h_obj_coeffs[var_idx] * delta; + + cuopt_assert(std::isfinite(delta), ""); + + cuopt_assert(var_idx < fj_cpu.problem->n_variables, "variable index out of bounds"); + + f_t base_feas_sum = 0; + f_t bonus_robust_sum = 0; + + auto [offset_begin, offset_end] = fj_cpu.range_for_variable(var_idx); + fj_cpu.nnz_processed_window += (offset_end - offset_begin); + + const size_t nnz_read = (size_t)(offset_end - offset_begin); + ++fj_cpu.n_compute_score_calls; + fj_cpu.compute_score_nnz += (int64_t)nnz_read; + fj_cpu.h_reverse_constraints.byte_loads += nnz_read * sizeof(i_t); + fj_cpu.h_reverse_coefficients.byte_loads += nnz_read * sizeof(f_t); + fj_cpu.h_row_state.byte_loads += + nnz_read * sizeof(typename fj_cpu_climber_t::row_state_t); + + const i_t* const rev_cstr = fj_cpu.h_reverse_constraints.data(); + const f_t* const rev_coeff = fj_cpu.h_reverse_coefficients.data(); + const typename fj_cpu_climber_t::row_state_t* const state = fj_cpu.row_state(); + + static_assert(std::is_same_v); + fj_simd_score_rows(rev_cstr, + rev_coeff, + reinterpret_cast(state), + offset_begin, + offset_end, + delta, + fj_cpu.row_tolerance, + (f_t)fj_cpu.settings.parameters.excess_improvement_weight, + base_feas_sum, + bonus_robust_sum); + + f_t base_obj = 0; + if (fj_cpu.h_objective_weight > 0 && obj_diff != 0) { + // Scaling base is only meaningful where there is feasibility impact to trade against. + f_t weighted = fj_cpu.h_objective_weight; + if (base_feas_sum != 0) { + cuopt_assert(fj_cpu.problem->obj_magnitude > 0, "objective magnitude unit must be positive"); + weighted *= std::min( + (f_t)fj_obj_mult_max, + std::max((f_t)fj_obj_mult_min, std::fabs(obj_diff) / fj_cpu.problem->obj_magnitude)); + } + base_obj = obj_diff < 0 ? weighted : -weighted; + } + + f_t bonus_breakthrough = 0; + + bool old_obj_better = fj_cpu.h_incumbent_objective < fj_cpu.h_best_objective; + bool new_obj_better = fj_cpu.h_incumbent_objective + obj_diff < fj_cpu.h_best_objective; + if (!old_obj_better && new_obj_better) + bonus_breakthrough += fj_cpu.h_objective_weight; + else if (old_obj_better && !new_obj_better) { + bonus_breakthrough -= fj_cpu.h_objective_weight; + } + + fj_staged_score_t score; + score.base = std::round(base_obj + base_feas_sum); + score.bonus = std::round(bonus_breakthrough + bonus_robust_sum); + return std::make_pair(score, base_feas_sum); +} + +template +void smooth_weights(fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::smooth_weights"); + for (i_t row = 0; row < fj_cpu.n_rows; ++row) { + if (fj_cpu.violated_constraints.contains(row)) continue; + f_t& weight = fj_cpu.row_state()[row].weight; + weight = std::max((f_t)0, weight - 1); + } + + if (fj_cpu.h_objective_weight > 0 && fj_cpu.h_incumbent_objective >= fj_cpu.h_best_objective) { + fj_cpu.h_objective_weight = std::max(f_t{0}, fj_cpu.h_objective_weight - 1); + } +} + +template +void update_weights(fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::update_weights"); + + cuopt::pcgenerator_t rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); + bool smoothing = rng.next_float() <= fj_cpu.settings.parameters.weight_smoothing_probability; + + if (smoothing) { + smooth_weights(fj_cpu); + return; + } + + for (auto cstr_idx : fj_cpu.violated_constraints) { + const f_t old_weight = fj_cpu.row_state()[cstr_idx].weight; + cuopt_assert(fj_cpu.row_state()[cstr_idx].slack < 0, "constraint not violated"); + + f_t new_weight = std::round(old_weight + f_t{1}); + new_weight = std::min(new_weight, (f_t)fj_cpu.hp.weight_cap); + + fj_cpu.row_state()[cstr_idx].weight = new_weight; + fj_cpu.max_weight = std::max(fj_cpu.max_weight, new_weight); + + // Invalidate related cached move scores + ++fj_cpu.n_version_bumps_weights; + fj_cpu.h_cstr_version[cstr_idx]++; + } + + if (fj_cpu.violated_constraints.empty()) { fj_cpu.h_objective_weight += 1; } +} + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/search/update.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/update.hpp new file mode 100644 index 0000000000..004fd56de3 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/search/update.hpp @@ -0,0 +1,359 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include "../audit.hpp" +#include "../internal.hpp" +#include "api.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +void report_cpu_incumbent(fj_cpu_climber_t& c, + f_t objective, + const std::vector& assignment, + double work_units) +{ + // Constructive starts and lifted solves report through this same point as ordinary moves. + const f_t last_reported = c.h_last_reported_objective; + if (!(last_reported - objective > f_t{1e-6} * std::max(f_t{1}, std::fabs(last_reported)))) return; + c.h_last_reported_objective = objective; + if (!c.suppress_incumbent_log) + CUOPT_LOG_DEBUG("%sCPUFJ new incumbent: objective %.17g", + c.log_prefix.c_str(), + c.get_user_objective(objective)); + if (!c.improvement_callback) return; + c.improvement_callback(objective, assignment, work_units); +} + +template +void report_cpu_incumbent(fj_cpu_climber_t& c) +{ + report_cpu_incumbent(c, + c.h_incumbent_objective, + c.h_assignment.underlying(), + c.work_units_elapsed.load(std::memory_order_acquire)); +} + +template +void apply_move(fj_cpu_climber_t& fj_cpu, i_t var_idx, f_t delta, bool localmin = false) +{ + CPUFJ_NVTX_RANGE("CPUFJ::apply_move"); + + cuopt::pcgenerator_t rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); + + cuopt_assert(var_idx < fj_cpu.problem->n_variables, "variable index out of bounds"); + f_t old_val = fj_cpu.h_assignment[var_idx]; + f_t new_val = old_val + delta; + if (is_integer_var(fj_cpu, var_idx)) { + cuopt_assert(fj_cpu.problem->integer_equal(new_val, std::round(new_val)), + "new_val is not integer"); + new_val = std::round(new_val); + } + // clamp to var bounds, then to the magnitude the whole assignment is held to. A per-move guard + // bounds the step and not the position, so without this a run of individually legal steps walks a + // variable to a magnitude where one ulp of its rows' slacks passes the row tolerance. Every + // generator is bounded here rather than each having to know; delta is recomputed below, so the + // slack bookkeeping stays exact whatever the clamp does. + const auto var_bounds = fj_cpu.h_var_bounds[var_idx].get(); + new_val = std::min(std::max(new_val, get_lower(var_bounds)), get_upper(var_bounds)); + const f_t floor_ = std::max(get_lower(var_bounds), (f_t)-fj_cpu.hp.start_magnitude_limit); + const f_t ceil_ = std::min(get_upper(var_bounds), (f_t)fj_cpu.hp.start_magnitude_limit); + if (floor_ <= ceil_) new_val = std::min(std::max(new_val, floor_), ceil_); + delta = new_val - old_val; + cuopt_assert(std::isfinite(new_val), "assignment is not finite"); + cuopt_assert(std::isfinite(delta), "applied delta is not finite"); + cuopt_assert(check_variable_within_bounds(fj_cpu, var_idx, new_val), + "assignment not within bounds"); + + // Update the slack of every search row the variable appears in. + auto [offset_begin, offset_end] = fj_cpu.range_for_variable(var_idx); + + fj_cpu.nnz_processed_window += (offset_end - offset_begin); + const size_t nnz_touched = (size_t)(offset_end - offset_begin); + using row_state_t = typename fj_cpu_climber_t::row_state_t; + ++fj_cpu.n_moves_applied; + fj_cpu.apply_move_nnz += (int64_t)nnz_touched; + fj_cpu.n_version_bumps_apply += (int64_t)nnz_touched; + fj_cpu.h_reverse_constraints.byte_loads += nnz_touched * sizeof(i_t); + fj_cpu.h_reverse_coefficients.byte_loads += nnz_touched * sizeof(f_t); + fj_cpu.h_row_state.byte_loads += nnz_touched * sizeof(row_state_t); + fj_cpu.h_row_state.byte_stores += nnz_touched * sizeof(row_state_t); + fj_cpu.h_slack_sumcomp.byte_loads += nnz_touched * sizeof(f_t); + fj_cpu.h_slack_sumcomp.byte_stores += nnz_touched * sizeof(f_t); + + const i_t* const rev_cstr = fj_cpu.h_reverse_constraints.data(); + const f_t* const rev_coeff = fj_cpu.h_reverse_coefficients.data(); + f_t* const row_sumcomp = fj_cpu.h_slack_sumcomp.data(); + row_state_t* const state = fj_cpu.row_state(); + const f_t cstr_tolerance = fj_cpu.row_tolerance; + + for (auto i = offset_begin; i < offset_end; i++) { + cuopt_assert(i < (i_t)fj_cpu.h_reverse_constraints.size(), ""); + + const i_t cstr_idx = rev_cstr[i]; + const f_t cstr_coeff = rev_coeff[i]; + + // The row is a'x <= b holding slack b - a'x, so the move lowers the slack by its own + // coefficient times the delta. Dot2, as the activity was: the carry holds the correction to add + // to the stored slack, covering the rounding of both the product and the addition. + const f_t old_slack = state[cstr_idx].slack; + const f_t old_sumcomp = row_sumcomp[cstr_idx]; + const f_t h = -cstr_coeff * delta; + f_t t = old_slack + h; + const f_t z = t - old_slack; + f_t new_sumcomp = + old_sumcomp + (((old_slack - (t - z)) + (h - z)) + std::fma(-cstr_coeff, delta, -h)); + + const f_t old_value = old_slack + old_sumcomp; + f_t new_value = t + new_sumcomp; + if (fj_cpu.h_row_is_integral[cstr_idx]) { + cuopt_assert(old_value == std::round(old_value), "integral row state is fractional"); + cuopt_assert(delta == std::round(delta), "integral row received a fractional move"); + new_value = std::round(new_value); + t = new_value; + new_sumcomp = 0; + } + row_sumcomp[cstr_idx] = new_sumcomp; + state[cstr_idx].slack = t; + + const f_t old_cost = old_value < f_t{0} ? old_value : f_t{0}; + const f_t new_cost = new_value < f_t{0} ? new_value : f_t{0}; + + // trigger early slack recomputation if the sumcomp term gets too large + // to avoid large numerical errors + if (std::fabs(new_sumcomp) > (f_t)fj_cpu.hp.bigval_threshold) + fj_cpu.trigger_early_lhs_recomputation = true; + + const bool was_violated = fj_cpu.violated_constraints.contains(cstr_idx); + const bool now_violated = new_value < -cstr_tolerance; + + // total_violations sums the excess over the violated set alone, so a row crossing the boundary + // contributes its whole cost rather than a difference. Kahan compensated, as the slack is: this + // is now the only place the total is maintained between refreshes. + const f_t viol_delta = (now_violated ? new_cost : f_t{0}) - (was_violated ? old_cost : f_t{0}); + if (viol_delta != f_t{0}) { + const f_t viol_old = fj_cpu.total_violations; + const f_t viol_y = viol_delta - fj_cpu.total_violations_sumcomp; + const f_t viol_t = viol_old + viol_y; + fj_cpu.total_violations_sumcomp = (viol_t - viol_old) - viol_y; + fj_cpu.total_violations = viol_t; + } + + if (now_violated && !was_violated) { + fj_cpu.violated_constraints.insert(cstr_idx); + cuopt_assert(fj_cpu.satisfied_constraints.contains(cstr_idx), ""); + fj_cpu.satisfied_constraints.remove(cstr_idx); + } else if (!now_violated && was_violated) { + cuopt_assert(!fj_cpu.satisfied_constraints.contains(cstr_idx), ""); + fj_cpu.violated_constraints.remove(cstr_idx); + fj_cpu.satisfied_constraints.insert(cstr_idx); + } + + cuopt_assert(std::isfinite(delta), "delta should be finite"); + cuopt_assert(std::isfinite(t), "assignment should be finite"); + + // Invalidate related cached move scores + fj_cpu.h_cstr_version[cstr_idx]++; + } + + // update the assignment and objective proper + fj_cpu.h_assignment[var_idx] = new_val; + // The clamp above passes a NaN straight through, and every comparison against one is false. + cuopt_assert(fj_cpu.check_variable_within_bounds(var_idx, new_val), + "apply_move left the variable bounds"); + // After the assignment write, which is what a fresh sum reads. + cuopt_func_call(audit_row_updates(fj_cpu, var_idx, old_val, delta, offset_begin, offset_end)); + + // Kahan compensated summation, as for the slacks. The incumbent objective is reported as-is, so + // it cannot carry the drift of a long uncompensated chain of deltas. + const f_t obj_old = fj_cpu.h_incumbent_objective; + const f_t obj_y = fj_cpu.problem->h_obj_coeffs[var_idx] * delta - fj_cpu.h_objective_sumcomp; + const f_t obj_t = obj_old + obj_y; + fj_cpu.h_objective_sumcomp = (obj_t - obj_old) - obj_y; + fj_cpu.h_incumbent_objective = obj_t; + // The result of this addition carries the ulp of its larger operand, not of itself, and the + // compensation cannot see it when the loss is in the product rather than the addition. Once that + // exceeds the granularity an incumbent has to beat, the objective comparison below is reading + // noise, so rebuild here rather than setting the deferred flag: the gate is in this same function + // and would otherwise latch on the value this move just made unreliable. The row loop and the + // assignment write are done, so the state a rebuild reads is consistent. + const f_t obj_resolution = + std::numeric_limits::epsilon() * std::max(std::fabs(obj_old), std::fabs(obj_y)); + if (obj_resolution > (f_t)fj_cpu.settings.parameters.breakthrough_move_epsilon) { + recompute_slack(fj_cpu); + } + cuopt_func_call(audit_objective_update(fj_cpu, var_idx, old_val, delta, obj_old, obj_y)); + + if (fj_cpu.h_incumbent_objective < fj_cpu.h_best_objective && + fj_cpu.violated_constraints.empty() && check_variable_feasibility(fj_cpu)) { + cuopt_assert((i_t)fj_cpu.satisfied_constraints.size() == fj_cpu.n_rows, ""); + cuopt_func_call(audit_incremental_state(fj_cpu, "incumbent gate")); + fj_cpu.h_best_objective = + fj_cpu.h_incumbent_objective - fj_cpu.settings.parameters.breakthrough_move_epsilon; + fj_cpu.h_best_assignment = fj_cpu.h_assignment; + fj_cpu.iterations_since_best = 0; + report_cpu_incumbent(fj_cpu); + fj_cpu.feasible_found = true; + } + + i_t tabu_tenure = fj_cpu.settings.parameters.tabu_tenure_min + + rng.next_u32() % (fj_cpu.settings.parameters.tabu_tenure_max - + fj_cpu.settings.parameters.tabu_tenure_min); + if (delta > 0) { + fj_cpu.h_tabu_lastinc[var_idx] = fj_cpu.iterations; + fj_cpu.h_tabu_nodec_until[var_idx] = fj_cpu.iterations + tabu_tenure; + fj_cpu.h_tabu_noinc_until[var_idx] = fj_cpu.iterations + tabu_tenure / 2; + // CUOPT_LOG_TRACE("CPU: tabu nodec_until: %d\n", fj_cpu.h_tabu_nodec_until[var_idx]); + } else { + fj_cpu.h_tabu_lastdec[var_idx] = fj_cpu.iterations; + fj_cpu.h_tabu_noinc_until[var_idx] = fj_cpu.iterations + tabu_tenure; + fj_cpu.h_tabu_nodec_until[var_idx] = fj_cpu.iterations + tabu_tenure / 2; + // CUOPT_LOG_TRACE("CPU: tabu noinc_until: %d\n", fj_cpu.h_tabu_noinc_until[var_idx]); + } + + ++fj_cpu.flip_move_epoch; +} + +template +f_t project_epigraph_variable(fj_cpu_climber_t& fj_cpu, i_t var_idx) +{ + cuopt_assert(fj_cpu.epigraph_push[var_idx] != 0, "variable is not a certified epigraph variable"); + const bool push_up = fj_cpu.epigraph_push[var_idx] > 0; + const f_t current = fj_cpu.h_assignment[var_idx]; + const auto bounds = fj_cpu.h_var_bounds[var_idx].get(); + f_t target = push_up ? get_lower(bounds) : get_upper(bounds); + + auto [offset_begin, offset_end] = fj_cpu.range_for_variable(var_idx); + const size_t nnz_read = (size_t)(offset_end - offset_begin); + fj_cpu.h_reverse_constraints.byte_loads += nnz_read * sizeof(i_t); + fj_cpu.h_reverse_coefficients.byte_loads += nnz_read * sizeof(f_t); + fj_cpu.h_row_state.byte_loads += + nnz_read * sizeof(typename fj_cpu_climber_t::row_state_t); + + const i_t* const rev_cstr = fj_cpu.h_reverse_constraints.data(); + const f_t* const rev_coeff = fj_cpu.h_reverse_coefficients.data(); + const typename fj_cpu_climber_t::row_state_t* const state = fj_cpu.row_state(); + + // The row is a'x <= b holding slack b - a'x, so the value it allows this variable is + // current + slack / coefficient. A certified epigraph variable has one sign throughout, so every + // incidence gives a limit on the same side and the tightest is the extreme one. + for (i_t p = offset_begin; p < offset_end; ++p) { + const f_t coeff = rev_coeff[p]; + if (coeff == f_t{0}) continue; + const f_t implied = current + state[rev_cstr[p]].slack / coeff; + if (!std::isfinite(implied)) continue; + target = push_up ? std::max(target, implied) : std::min(target, implied); + } + + target = std::min(std::max(target, get_lower(bounds)), get_upper(bounds)); + + // An epigraph variable is unbounded in the push direction by construction, so the value its rows + // imply is unbounded too, and landing on it puts every row it touches at a magnitude where one + // ulp of the slack exceeds the row tolerance. Held to the range the start is held to, which keeps + // the rows decidable at the cost of reaching the implied value over several moves instead of one. + const f_t floor_ = std::max(get_lower(bounds), (f_t)-fj_cpu.hp.start_magnitude_limit); + const f_t ceil_ = std::min(get_upper(bounds), (f_t)fj_cpu.hp.start_magnitude_limit); + if (floor_ <= ceil_) target = std::min(std::max(target, floor_), ceil_); + + cuopt_assert(std::isfinite(target), "epigraph projection is not finite"); + return target; +} + +template +static void prepare_full_recompute(fj_cpu_climber_t& fj_cpu) +{ + ++fj_cpu.n_lhs_recompute_total; + // clamp to var bounds - defensive; apply_move should already have clamped appropriately + for (i_t var_idx = 0; var_idx < fj_cpu.problem->n_variables; ++var_idx) { + fj_cpu.h_assignment[var_idx] = std::min( + std::max(fj_cpu.h_assignment[var_idx].get(), get_lower(fj_cpu.h_var_bounds[var_idx].get())), + get_upper(fj_cpu.h_var_bounds[var_idx].get())); + } + fj_cpu.violated_constraints.clear(); + fj_cpu.satisfied_constraints.clear(); + fj_cpu.total_violations = 0; + fj_cpu.total_violations_sumcomp = 0; +} + +template +static void finish_full_recompute(fj_cpu_climber_t& fj_cpu, + const f_t* objective_lhs, + const f_t* objective_rhs) +{ + fj_cpu.h_incumbent_objective = + compensated_dot2(objective_lhs, objective_rhs, fj_cpu.problem->n_variables); + fj_cpu.h_objective_sumcomp = 0; +} + +template +void recompute_lhs(fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::recompute_lhs"); + cuopt_assert(fj_cpu.h_lhs.size() == fj_cpu.problem->n_constraints, "h_lhs size mismatch"); + // Repopulates the membership sets over model rows and leaves row_state().slack untouched, so it + // is only correct before build_one_sided_rows. recompute_slack is the counterpart afterwards. + cuopt_assert(fj_cpu.violated_constraints.max_size() == fj_cpu.problem->n_constraints, + "recompute_lhs keys the sets over model rows"); + cuopt_assert(fj_cpu.satisfied_constraints.max_size() == fj_cpu.problem->n_constraints, + "recompute_lhs keys the sets over model rows"); + prepare_full_recompute(fj_cpu); + for (i_t cstr_idx = 0; cstr_idx < fj_cpu.problem->n_constraints; ++cstr_idx) { + fj_cpu.h_lhs[cstr_idx] = compensated_dot2_csr(*fj_cpu.problem, fj_cpu.h_assignment, cstr_idx); + fj_cpu.h_lhs_sumcomp[cstr_idx] = 0; + + f_t new_cost = fj_cpu.excess_score(cstr_idx, + fj_cpu.h_lhs[cstr_idx], + fj_cpu.problem->cstr_lb[cstr_idx], + fj_cpu.problem->cstr_ub[cstr_idx]); + if (new_cost < -fj_cpu.row_tolerance) { + fj_cpu.violated_constraints.insert(cstr_idx); + fj_cpu.total_violations += new_cost; + } else { + fj_cpu.satisfied_constraints.insert(cstr_idx); + } + } + + finish_full_recompute(fj_cpu, fj_cpu.h_assignment.data(), fj_cpu.problem->h_obj_coeffs.data()); +} + +template +void recompute_slack(fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::recompute_slack"); + const i_t n_rows = fj_cpu.n_rows; + cuopt_assert(n_rows > 0, "no rows"); + cuopt_assert((i_t)fj_cpu.h_row_state.size() == n_rows, "row state size mismatch"); + prepare_full_recompute(fj_cpu); + const f_t* const assignment = fj_cpu.h_assignment.data(); + + for (i_t r = 0; r < n_rows; ++r) { + f_t slack = fresh_row_slack(fj_cpu, r, assignment); + if (fj_cpu.h_row_is_integral[r]) slack = std::round(slack); + fj_cpu.row_state()[r].slack = slack; + fj_cpu.h_slack_sumcomp[r] = 0; + if (slack < -fj_cpu.row_tolerance) { + fj_cpu.violated_constraints.insert(r); + fj_cpu.total_violations += slack; + } else { + fj_cpu.satisfied_constraints.insert(r); + } + } + + finish_full_recompute(fj_cpu, fj_cpu.problem->h_obj_coeffs.data(), assignment); +} + +template +void invalidate_mtm_cache(fj_cpu_climber_t& fj_cpu) +{ + ++fj_cpu.n_mtm_cache_invalidations; + for (size_t c = 0; c < fj_cpu.h_cstr_version.size(); ++c) + fj_cpu.h_cstr_version[c]++; +} + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.cpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.cpp new file mode 100644 index 0000000000..37911cc9fc --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.cpp @@ -0,0 +1,90 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "lp.hpp" +#include "../audit.hpp" +#include "../internal.hpp" +#include "../problem.hpp" +#include "../search/api.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +void eliminate_slacks(const lp_problem_t& problem, + i_t n_structural, + csr_matrix_t& csr_A, + std::vector& row_lower, + std::vector& row_upper) +{ + cuopt_assert(csr_A.m == problem.num_rows, "row count mismatch"); + cuopt_assert(csr_A.n == problem.num_cols, "column count mismatch"); + cuopt_assert(n_structural > 0, "no structural columns"); + cuopt_assert(n_structural < problem.num_cols, "no slacks to eliminate"); + cuopt_assert(problem.num_cols - n_structural <= problem.num_rows, "more slacks than rows"); + + row_lower = problem.rhs; + row_upper = problem.rhs; + + std::vector row_has_slack(problem.num_rows, 0); + for (i_t j = n_structural; j < problem.num_cols; ++j) { + cuopt_assert(problem.A.col_length(j) == 1, "slack column is not a singleton"); + + const i_t entry = problem.A.col_start[j]; + const i_t row = problem.A.i[entry]; + const f_t alpha = problem.A.x[entry]; + cuopt_assert(std::abs(alpha) == f_t{1}, "slack coefficient is not +/-1"); + cuopt_assert(!row_has_slack[row], "row has more than one slack"); + row_has_slack[row] = 1; + + const f_t scaled_lower = alpha * problem.lower[j]; + const f_t scaled_upper = alpha * problem.upper[j]; + row_lower[row] = problem.rhs[row] - std::max(scaled_lower, scaled_upper); + row_upper[row] = problem.rhs[row] - std::min(scaled_lower, scaled_upper); + cuopt_assert(std::isfinite(row_lower[row]) || std::isfinite(row_upper[row]), + "eliminated row is free on both sides"); + cuopt_assert(row_lower[row] <= row_upper[row], "eliminated row has crossed bounds"); + } + + i_t out = 0; + for (i_t row = 0; row < csr_A.m; ++row) { + const i_t row_start = csr_A.row_start[row]; + const i_t row_end = csr_A.row_start[row + 1]; + csr_A.row_start[row] = out; + for (i_t p = row_start; p < row_end; ++p) { + if (csr_A.j[p] >= n_structural) { continue; } + csr_A.j[out] = csr_A.j[p]; + csr_A.x[out] = csr_A.x[p]; + ++out; + } + } + cuopt_assert(out == csr_A.row_start[csr_A.m] - static_cast(problem.num_cols - n_structural), + "slack elimination removed the wrong number of entries"); + + csr_A.row_start[csr_A.m] = out; + csr_A.j.resize(out); + csr_A.x.resize(out); + csr_A.nz_max = out; + csr_A.n = n_structural; +} + +#if MIP_INSTANTIATE_FLOAT +template void eliminate_slacks(const lp_problem_t&, + int, + csr_matrix_t&, + std::vector&, + std::vector&); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template void eliminate_slacks(const lp_problem_t&, + int, + csr_matrix_t&, + std::vector&, + std::vector&); +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.hpp new file mode 100644 index 0000000000..d2dedc08c2 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.hpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "../state.hpp" + +namespace cuopt::mathematical_optimization { + +template +class csr_matrix_t; + +namespace simplex { + +template +struct lp_problem_t; + +} // namespace simplex +} // namespace cuopt::mathematical_optimization + +namespace cuopt::mathematical_optimization::mip { + +template +void eliminate_slacks(const simplex::lp_problem_t&, + i_t, + csr_matrix_t&, + std::vector&, + std::vector&); + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/structure.cpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/structure.cpp new file mode 100644 index 0000000000..1ee1173118 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/structure.cpp @@ -0,0 +1,316 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "structure.hpp" +#include +#include +#include "../climber.hpp" +#include "../internal.hpp" +#include "../problem.hpp" +#include "../search/api.hpp" + +namespace cuopt::mathematical_optimization::mip { + +template +double implied_integer_row_scale(const fj_cpu_problem_t& problem, + i_t row_idx, + f_t pivot_coeff) +{ + const i_t begin = problem.offsets[row_idx], end = problem.offsets[row_idx + 1]; + const f_t rhs = problem.cstr_lb[row_idx]; + if (!scaling_bound_finite(rhs)) return 0; + const double scale = row_int_scale(problem.coefficients.data() + begin, + end - begin, + rhs, + rhs, + end - begin, + std::numeric_limits::max()); + if (scale == 0) return 0; + const int64_t divisor = std::llround(scale * pivot_coeff); + if (divisor == 0 || std::llround(scale * rhs) % divisor != 0) return 0; + for (i_t entry_idx = begin; entry_idx < end; ++entry_idx) + if (std::llround(scale * problem.coefficients[entry_idx]) % divisor != 0) return 0; + return scale; +} + +template +void detect_implied_integers(fj_cpu_climber_t& fj_cpu, + fj_cpu_problem_t& problem) +{ + const i_t n_variables = problem.n_variables, n_constraints = problem.n_constraints; + // Snapshot the original types for the initial equality counts. + std::vector was_continuous(n_variables); + bool has_continuous = false; + for (i_t var_idx = 0; var_idx < n_variables; ++var_idx) + has_continuous |= was_continuous[var_idx] = problem.h_var_types[var_idx] == var_t::CONTINUOUS; + if (!has_continuous) return; + // Equalities with one continuous variable can force it integer; newly integral + // variables may expose another such equality through the existing transpose. + std::vector n_continuous_in_row(n_constraints, 0), pending_rows; + for (i_t row_idx = 0; row_idx < n_constraints; ++row_idx) { + if (!std::isfinite(problem.cstr_lb[row_idx]) || + problem.cstr_lb[row_idx] != problem.cstr_ub[row_idx]) + continue; + for (i_t entry_idx = problem.offsets[row_idx]; entry_idx < problem.offsets[row_idx + 1]; + ++entry_idx) + n_continuous_in_row[row_idx] += was_continuous[problem.variables[entry_idx]]; + if (n_continuous_in_row[row_idx] == 1) pending_rows.push_back(row_idx); + } + i_t n_forced = 0, n_completable = 0; + std::vector changed_vars; + // A row reaches one unknown at most once. Each newly integral column updates + // its incident counts once, so even a long chain costs O(nnz + rows + columns). + for (size_t queue_idx = 0; queue_idx < pending_rows.size(); ++queue_idx) { + const i_t row_idx = pending_rows[queue_idx]; + if (n_continuous_in_row[row_idx] != 1) continue; + i_t var_idx = -1; + f_t pivot_coeff = 0; + for (i_t entry_idx = problem.offsets[row_idx]; entry_idx < problem.offsets[row_idx + 1]; + ++entry_idx) { + if (problem.h_var_types[problem.variables[entry_idx]] == var_t::CONTINUOUS) { + var_idx = problem.variables[entry_idx]; + pivot_coeff = problem.coefficients[entry_idx]; + break; + } + } + if (var_idx < 0 || !implied_integer_row_scale(problem, row_idx, pivot_coeff)) continue; + const auto bounds = fj_cpu.h_var_bounds[var_idx].get(); + bool valid = scaling_bound_finite(get_lower(bounds)) && + scaling_bound_finite(get_upper(bounds)) && + get_lower(bounds) == std::round(get_lower(bounds)) && + get_upper(bounds) == std::round(get_upper(bounds)); + if (!valid) continue; + problem.h_var_types[var_idx] = var_t::INTEGER; + changed_vars.push_back(var_idx); + ++n_forced; + for (i_t entry_idx = problem.reverse_offsets[var_idx]; + entry_idx < problem.reverse_offsets[var_idx + 1]; + ++entry_idx) { + const i_t incident_row = problem.reverse_constraints[entry_idx]; + if (n_continuous_in_row[incident_row] > 0 && --n_continuous_in_row[incident_row] == 1) + pending_rows.push_back(incident_row); + } + } + // Each pair variable occurs only in this equality, so changing the pair cannot + // affect other rows. After scaling, the equality fixes their difference to an + // integer. With zero lower bounds, integral upper bounds and equal positive + // costs, setting the smaller variable to zero gives an integral optimal completion. + // Fractional feasible pairs can still exist; integrality here preserves an optimum. + for (i_t row_idx = 0; row_idx < n_constraints; ++row_idx) { + if (n_continuous_in_row[row_idx] != 2) continue; + i_t first_var = -1, second_var = -1; + f_t pivot_coeff = 0, second_coeff = 0; + for (i_t entry_idx = problem.offsets[row_idx]; entry_idx < problem.offsets[row_idx + 1]; + ++entry_idx) { + const i_t var_idx = problem.variables[entry_idx]; + if (problem.h_var_types[var_idx] != var_t::CONTINUOUS) continue; + if (first_var < 0) { + first_var = var_idx; + pivot_coeff = problem.coefficients[entry_idx]; + } else { + second_var = var_idx; + second_coeff = problem.coefficients[entry_idx]; + } + } + if (first_var < 0 || second_var < 0 || first_var == second_var) continue; + const double scale = implied_integer_row_scale(problem, row_idx, pivot_coeff); + if (scale == 0 || std::llround(scale * second_coeff) != -std::llround(scale * pivot_coeff)) + continue; + bool valid = problem.h_obj_coeffs[first_var] > 0 && + std::isfinite(problem.h_obj_coeffs[first_var]) && + problem.h_obj_coeffs[first_var] == problem.h_obj_coeffs[second_var]; + for (i_t var_idx : {first_var, second_var}) { + const auto bounds = fj_cpu.h_var_bounds[var_idx].get(); + valid &= problem.reverse_offsets[var_idx + 1] - problem.reverse_offsets[var_idx] == 1 && + get_lower(bounds) == f_t{0} && get_upper(bounds) >= f_t{0} && + scaling_bound_finite(get_upper(bounds)) && + get_upper(bounds) == std::round(get_upper(bounds)); + } + if (!valid) continue; + problem.h_var_types[first_var] = problem.h_var_types[second_var] = var_t::INTEGER; + changed_vars.push_back(first_var); + changed_vars.push_back(second_var); + n_completable += 2; + } + + // update classification for the newly-implied-integer vars + for (i_t var_idx : changed_vars) { + fj_cpu.h_assignment[var_idx] = std::round((f_t)fj_cpu.h_assignment[var_idx]); + fj_cpu.h_best_assignment[var_idx] = std::round((f_t)fj_cpu.h_best_assignment[var_idx]); + const auto bounds = fj_cpu.h_var_bounds[var_idx].get(); + if (get_lower(bounds) == f_t{0} && get_upper(bounds) == f_t{1}) { + fj_cpu.h_is_binary_variable[var_idx] = 1; + fj_cpu.h_binary_indices.push_back(var_idx); + } + } + CUOPT_LOG_DEBUG( + "CPUFJ implied integrality: %d forced, %d integral-completable", n_forced, n_completable); +} + +template +void certify_epigraph_variables(fj_cpu_climber_t& fj_cpu, i_t n_variables) +{ + fj_cpu.epigraph_push.assign(n_variables, 0); + fj_cpu.epigraph_vars.clear(); + + for (i_t var = 0; var < n_variables; ++var) { + if (is_integer_var(fj_cpu, var)) continue; + const f_t obj_coeff = fj_cpu.problem->h_obj_coeffs[var]; + if (obj_coeff == f_t{0}) continue; + + const auto [begin, end] = model_range_for_var(fj_cpu, var); + if (begin == end) continue; + + // A positive coefficient is minimised by pushing the variable down, so its rows must be the + // only thing holding it up, and it must be free to rise as far as they demand. + const bool push_up = obj_coeff > f_t{0}; + const auto bounds = fj_cpu.h_var_bounds[var].get(); + if (std::isfinite(push_up ? get_upper(bounds) : get_lower(bounds))) continue; + + bool certified = true; + for (i_t p = begin; p < end && certified; ++p) { + const i_t row = fj_cpu.problem->reverse_constraints[p]; + const f_t coeff = fj_cpu.problem->reverse_coefficients[p]; + const bool has_lb = std::isfinite((f_t)fj_cpu.problem->cstr_lb[row]); + const bool has_ub = std::isfinite((f_t)fj_cpu.problem->cstr_ub[row]); + if (coeff == f_t{0}) continue; + certified = push_up ? ((coeff > 0 && has_lb && !has_ub) || (coeff < 0 && has_ub && !has_lb)) + : ((coeff > 0 && has_ub && !has_lb) || (coeff < 0 && has_lb && !has_ub)); + } + if (!certified) continue; + + fj_cpu.epigraph_push[var] = push_up ? 1 : -1; + fj_cpu.epigraph_vars.push_back(var); + } +} + +template +void build_one_sided_rows(fj_cpu_climber_t& fj_cpu) +{ + CPUFJ_NVTX_RANGE("CPUFJ::build_one_sided_rows"); + const i_t n_model_rows = fj_cpu.problem->n_constraints; + + i_t n_rows = 0; + i_t nnz = 0; + for (i_t row = 0; row < n_model_rows; ++row) { + const i_t sides = (i_t)std::isfinite((f_t)fj_cpu.problem->cstr_lb[row]) + + (i_t)std::isfinite((f_t)fj_cpu.problem->cstr_ub[row]); + n_rows += sides; + nnz += sides * (fj_cpu.problem->offsets[row + 1] - fj_cpu.problem->offsets[row]); + } + cuopt_assert(n_rows > 0, "model has no bounded rows"); + + auto& fwd_offsets = fj_cpu.h_offsets; + auto& fwd_variables = fj_cpu.h_variables; + auto& fwd_coefficients = fj_cpu.h_coefficients; + fwd_offsets.clear(); + fwd_variables.clear(); + fwd_coefficients.clear(); + fwd_offsets.reserve((size_t)n_rows + 1); + fwd_variables.reserve((size_t)nnz); + fwd_coefficients.reserve((size_t)nnz); + fwd_offsets.push_back(0); + + fj_cpu.h_bound.clear(); + fj_cpu.h_bound.reserve((size_t)n_rows); + fj_cpu.h_row_state.underlying().assign((size_t)n_rows, {}); + fj_cpu.h_row_is_integral.clear(); + fj_cpu.h_row_is_integral.reserve((size_t)n_rows); + + for (i_t row = 0; row < n_model_rows; ++row) { + const f_t lb = fj_cpu.problem->cstr_lb[row]; + const f_t ub = fj_cpu.problem->cstr_ub[row]; + const i_t begin = fj_cpu.problem->offsets[row]; + const i_t end = fj_cpu.problem->offsets[row + 1]; + bool integral_activity = true; + for (i_t k = begin; k < end; ++k) { + const i_t var = fj_cpu.problem->variables[k]; + const f_t coeff = fj_cpu.problem->coefficients[k]; + if (coeff != f_t{0} && + (fj_cpu.problem->h_var_types[var] != var_t::INTEGER || coeff != std::round(coeff))) { + integral_activity = false; + break; + } + } + + for (i_t side = 0; side < 2; ++side) { + const f_t bound = side == 0 ? lb : ub; + if (!std::isfinite(bound)) continue; + const f_t sign = side == 0 ? (f_t)-1 : (f_t)1; + + for (i_t k = begin; k < end; ++k) { + fwd_variables.push_back(fj_cpu.problem->variables[k]); + fwd_coefficients.push_back(sign * (f_t)fj_cpu.problem->coefficients[k]); + } + fwd_offsets.push_back((i_t)fwd_variables.size()); + + const i_t r = (i_t)fj_cpu.h_bound.size(); + fj_cpu.h_bound.push_back(sign * bound); + fj_cpu.h_row_is_integral.push_back(integral_activity && bound == std::round(bound)); + fj_cpu.row_state()[r].weight = + side == 0 ? fj_cpu.h_initial_left_weights[row] : fj_cpu.h_initial_right_weights[row]; + } + } + cuopt_assert((i_t)fj_cpu.h_bound.size() == n_rows, "row count mismatch"); + cuopt_assert((i_t)fj_cpu.h_row_is_integral.size() == n_rows, "row count mismatch"); + cuopt_assert((i_t)fwd_variables.size() == nnz, "nonzero count mismatch"); + + fj_cpu.n_rows = n_rows; + fj_cpu.h_slack_sumcomp.underlying().assign((size_t)n_rows, f_t{0}); + fj_cpu.h_cstr_version.assign((size_t)n_rows, 0); + fj_cpu.violated_constraints.resize(n_rows); + fj_cpu.satisfied_constraints.resize(n_rows); + // Indexed by forward nonzero, so they follow the search rows rather than the model. + fj_cpu.cached_mtm_moves.assign((size_t)nnz, std::make_pair(f_t{0}, fj_staged_score_t::zero())); + fj_cpu.cached_mtm_moves_version.assign((size_t)nnz, -1); + + // Counting-sort transpose. Rows are emitted in increasing order, so each variable's slice comes + // out row-ascending, which the 2-opt merges require. + const i_t n_variables = fj_cpu.problem->n_variables; + auto& rev_offsets = fj_cpu.h_reverse_offsets.underlying(); + rev_offsets.assign((size_t)n_variables + 1, 0); + for (i_t k = 0; k < nnz; ++k) { + const i_t var_idx = fwd_variables[k]; + rev_offsets[var_idx + 1]++; + } + for (i_t v = 0; v < n_variables; ++v) + rev_offsets[v + 1] += rev_offsets[v]; + fj_cpu.h_reverse_constraints.resize((size_t)nnz); + fj_cpu.h_reverse_coefficients.resize((size_t)nnz); + { + std::vector cursor(rev_offsets.begin(), rev_offsets.begin() + n_variables); + for (i_t r = 0; r < n_rows; ++r) { + const i_t row_begin = fwd_offsets[r]; + const i_t row_end = fwd_offsets[r + 1]; + for (i_t k = row_begin; k < row_end; ++k) { + const i_t var_idx = fwd_variables[k]; + const i_t slot = cursor[var_idx]++; + fj_cpu.h_reverse_constraints[slot] = r; + fj_cpu.h_reverse_coefficients[slot] = fwd_coefficients[k]; + } + } + } + + recompute_slack(fj_cpu); + fj_cpu.release_setup_structures(); +} + +#if MIP_INSTANTIATE_FLOAT +template void detect_implied_integers(fj_cpu_climber_t&, + fj_cpu_problem_t&); +template void certify_epigraph_variables(fj_cpu_climber_t&, int); +template void build_one_sided_rows(fj_cpu_climber_t&); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template void detect_implied_integers(fj_cpu_climber_t&, + fj_cpu_problem_t&); +template void certify_epigraph_variables(fj_cpu_climber_t&, int); +template void build_one_sided_rows(fj_cpu_climber_t&); +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/structure.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/structure.hpp new file mode 100644 index 0000000000..6870468cae --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/setup/structure.hpp @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "../state.hpp" +namespace cuopt::mathematical_optimization::mip { + +template +void detect_implied_integers(fj_cpu_climber_t&, fj_cpu_problem_t&); +template +void certify_epigraph_variables(fj_cpu_climber_t&, i_t); +template +void build_one_sided_rows(fj_cpu_climber_t&); +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/state.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/state.hpp new file mode 100644 index 0000000000..a6561e4f3c --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/state.hpp @@ -0,0 +1,573 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cuopt::mathematical_optimization::simplex { +template +struct user_problem_t; +} + +namespace cuopt::mathematical_optimization::mip { + +// The default for one search knob, overridable through the environment so switching it costs a +// rerun rather than a rebuild. Read when the constant it initialises is constructed, so an override +// exported after the process has started has no effect. A value that does not parse in full leaves +// the default in place. +template +class probing_cache_t; + +template +struct host_contiguous_set_t { + void resize(i_t max_size) + { + cuopt_assert(max_size >= 0, "invalid max size"); + contents.clear(); + contents.reserve(max_size); + index_map.assign(max_size, -1); + is_member.assign(max_size, 0); + } + + void clear() + { + for (i_t val : contents) { + index_map[val] = -1; + is_member[val] = 0; + } + contents.clear(); + } + + void insert(i_t val) + { + cuopt_assert(val >= 0 && val < max_size(), "Value is out of bounds"); + cuopt_assert(!contains(val), "Value already exists"); + index_map[val] = contents.size(); + is_member[val] = 1; + contents.push_back(val); + } + + void remove(i_t val) + { + cuopt_assert(val >= 0 && val < max_size(), "Value is out of bounds"); + cuopt_assert(contains(val), "Value not found"); + const i_t idx = index_map[val]; + const i_t last_val = contents.back(); + contents[idx] = last_val; + index_map[last_val] = idx; + contents.pop_back(); + index_map[val] = -1; + is_member[val] = 0; + } + + bool contains(i_t val) const + { + cuopt_assert(val >= 0 && val < max_size(), "Value is out of bounds"); + return is_member[val] != 0; + } + + auto begin() const { return contents.begin(); } + auto end() const { return contents.end(); } + i_t size() const { return contents.size(); } + i_t max_size() const { return index_map.size(); } + bool empty() const { return contents.empty(); } + + std::vector contents; + std::vector index_map; + std::vector is_member; +}; + +// Best feasible assignment found by any lane of one portfolio. A lane publishes its own +// improvements and adopts a better one when it perturbs, so a lane that has stalled resumes from +// the portfolio's progress instead of its own. Lanes run concurrently, so which lane observes +// which incumbent depends on scheduling: a portfolio that shares is not run-to-run reproducible. +template +struct fj_cpu_shared_incumbent_t { + // True when the candidate beat the shared best, in which case it was stored. + bool publish(f_t candidate_objective, + f_t candidate_user_objective, + const std::vector& candidate) + { + if (!(candidate_objective < objective.load(std::memory_order_relaxed))) return false; + std::lock_guard lock(guard); + if (!(candidate_objective < objective.load(std::memory_order_relaxed))) return false; + assignment = candidate; + objective.store(candidate_objective, std::memory_order_relaxed); + CUOPT_LOG_DEBUG("New portfolio best found: %.17g:", candidate_user_objective); + return true; + } + + // True when the shared best beat local_objective, in which case it was copied into destination. + bool adopt(f_t local_objective, std::vector& destination, f_t* adopted_objective = nullptr) + { + if (!(objective.load(std::memory_order_relaxed) < local_objective)) return false; + std::lock_guard lock(guard); + const f_t shared_objective = objective.load(std::memory_order_relaxed); + if (!(shared_objective < local_objective)) return false; + cuopt_assert(assignment.size() == destination.size(), "shared incumbent size mismatch"); + destination = assignment; + if (adopted_objective != nullptr) *adopted_objective = shared_objective; + return true; + } + + std::mutex guard; + std::vector assignment; + std::atomic objective{std::numeric_limits::infinity()}; +}; + +// The problem as given: two-sided rows, original column space. Written once during climber +// construction and read-only from then on, so every lane shares one copy rather than carrying its +// own. +template +struct fj_cpu_problem_t { + typename mip_solver_settings_t::tolerances_t tolerances; + i_t n_variables{0}; + i_t n_constraints{0}; + i_t nnz{0}; + f_t objective_scaling_factor{1}; + f_t objective_offset{0}; + f_t obj_magnitude{1}; + i_t max_var_degree{0}; + double avg_var_degree{0.0}; + double equality_fraction{0.0}; + + std::vector h_obj_coeffs; + std::vector h_var_types; + std::vector h_objective_vars; + std::vector h_related_variables; + std::vector h_related_variables_offsets; + const probing_cache_t* probing_cache{nullptr}; + std::vector h_original_ids; + std::vector h_reverse_original_ids; + + std::vector coefficients; + std::vector offsets; + std::vector variables; + std::vector reverse_coefficients; + std::vector reverse_constraints; + std::vector reverse_offsets; + std::vector cstr_lb; + std::vector cstr_ub; + + // Host snapshot used by the optional LP-based setup phases. Keeping it with the immutable CPU + // model prevents the host search engine from reaching back into the GPU-backed problem_t. + std::shared_ptr> host_lp; + + // Members of uniform-coefficient binary equality rows. Exchanging opposite-valued members + // preserves the defining equality and provides a structural neighbourhood at local minima. + std::vector card_row_offsets; + std::vector card_variables; + // Right-hand-side cardinality of each recognized group (sum(x) = k). + std::vector card_cardinalities; + // Unique cardinality group containing each variable; -1 means none and -2 means ambiguous. + std::vector card_group_of_variable; + + bool is_integer(f_t value) const + { + return std::abs(std::round(value) - value) <= tolerances.integrality_tolerance; + } + + bool integer_equal(f_t lhs, f_t rhs) const + { + return std::abs(lhs - rhs) <= tolerances.integrality_tolerance; + } +}; + +// Variable domains and every index derived from them are lane-local. Bound propagation narrows +// these during setup, while the structural problem shared by the portfolio remains immutable. +template +struct fj_domains_t { + i_t n_integer_vars{0}; + i_t n_binary_vars{0}; + ins_vector::type> h_var_bounds; + ins_vector h_is_binary_variable; + ins_vector h_binary_indices; +}; + +template +struct fj_tabu_t { + ins_vector h_tabu_nodec_until; + ins_vector h_tabu_noinc_until; + ins_vector h_tabu_lastdec; + ins_vector h_tabu_lastinc; +}; + +template +struct fj_weights_t { + ins_vector h_initial_left_weights; + ins_vector h_initial_right_weights; + f_t max_weight; + f_t h_objective_weight; +}; + +template +struct fj_move_cache_t { + std::vector flip_move_stamp; + int64_t flip_move_epoch{1}; + std::vector> cached_mtm_moves; + std::vector cached_mtm_moves_version; +}; + +template +struct fj_pair_scratch_t { + std::vector> two_opt_row_deltas; +}; + +template +struct fj_epigraph_t { + std::vector epigraph_push; + std::vector epigraph_vars; +}; + +template +struct fj_checkpoint_t { + ins_vector h_best_infeasible_assignment; + f_t best_infeasible_severity{std::numeric_limits::infinity()}; + f_t checkpoint_severity{std::numeric_limits::infinity()}; + i_t iters_since_infeasible_improve{0}; + i_t restores_since_improvement{0}; +}; + +template +struct fj_search_rows_t { + struct row_state_t { + f_t slack; + f_t weight; + }; + static_assert(sizeof(row_state_t) == 2 * sizeof(f_t)); + f_t row_tolerance{0}; + i_t n_rows{0}; + ins_vector h_row_state; + ins_vector h_row_is_integral; + ins_vector h_slack_sumcomp; + ins_vector h_bound; + ins_vector h_offsets; + ins_vector h_variables; + ins_vector h_coefficients; + ins_vector h_reverse_offsets; + ins_vector h_reverse_constraints; + ins_vector h_reverse_coefficients; + std::vector h_cstr_version; + + row_state_t* row_state() { return h_row_state.data(); } + const row_state_t* row_state() const { return h_row_state.data(); } + bool one_sided() const { return n_rows > 0; } + + std::pair range_for_variable(i_t var_idx) const + { + cuopt_assert(var_idx >= 0 && var_idx < static_cast(h_reverse_offsets.size()) - 1, + "Variable should be within the range"); + return std::make_pair(h_reverse_offsets[var_idx], h_reverse_offsets[var_idx + 1]); + } + + std::pair range_for_row(i_t row) const + { + cuopt_assert(row >= 0 && row < n_rows, "row out of range"); + return std::make_pair(h_offsets[row], h_offsets[row + 1]); + } +}; + +template +struct fj_search_state_t { + cuopt::pcgenerator_t rng; + ins_vector h_lhs; + ins_vector h_lhs_sumcomp; + ins_vector h_assignment; + ins_vector h_best_assignment; + f_t h_incumbent_objective; + f_t h_objective_sumcomp{0}; + f_t h_best_objective; + f_t h_last_reported_objective{std::numeric_limits::max()}; + i_t iterations{0}; + host_contiguous_set_t violated_constraints; + host_contiguous_set_t satisfied_constraints; + bool feasible_found{false}; + bool trigger_early_lhs_recomputation{false}; + f_t total_violations{0}; + f_t total_violations_sumcomp{0}; + i_t perturb_streak{0}; + i_t iterations_since_best{0}; +}; + +template +struct fj_batching_t { + i_t n_colors{0}; + std::vector h_var_color; + std::vector h_var_best_score; + std::vector h_var_best_delta; + std::vector h_var_best_stamp; + std::vector h_var_best_rowsum; + int64_t var_best_epoch{1}; + std::vector> h_color_candidates; + std::vector h_color_epoch; + std::vector h_var_bucket_stamp; +}; + +template +struct fj_bin_bridge_t { + struct bin_eliminated_row_t { + i_t row; + f_t rhs; + std::vector positive, negative, all; + std::vector positive_coeff, negative_coeff; + }; + std::vector bin_eliminated_rows; + // (row, column) substitutions that retain the singleton's bounds as row bounds. + std::vector> bin_singletons; + std::vector bin_ignore_row, bin_ignore_var; + bool has_bin_elimination{false}; +}; + +template +struct fj_lane_policy_t { + fj_settings_t settings; + fj_cpu_hyper_parameters_t hp; + f_t seed_objective_weight{0}; + bool use_move_batching{false}; + i_t mtm_viol_samples{25}; + i_t mtm_sat_samples{15}; + i_t nnz_samples{50000}; + i_t perturb_interval{100}; + i_t perturb_vars{2}; + bool use_lp_start{false}; + bool lp_start_feasibility_objective{false}; + bool use_deep_lp_pump{false}; + bool use_integer_bit_encoding{true}; + bool use_lp_polish{false}; + bool use_precedence_start{false}; + bool use_affine_equality_start{false}; + bool use_unit_commitment_start{false}; + bool use_fixed_charge_network_start{false}; + bool use_pmedian_start{false}; + bool use_bound_prop{false}; + bool low_latency{false}; + bool use_weight_donation{false}; + bool degree_balance_mtm{false}; + bool use_cardinality_exchange{false}; + bool use_directed_infeasible_kick{false}; + bool use_compound_repair{false}; + bool use_equality_substitution{false}; + bool suppress_incumbent_log{false}; + bool use_multiplicative_weights{false}; + f_t saps_multiplier{(f_t)1.3}; + i_t infeasible_kick_interval{0}; + i_t infeasible_kick_vars{4}; + i_t infeasible_restart_window{200}; + i_t infeasible_restart_max_streak{20}; + f_t infeasible_restart_degrade_ratio{1.15}; + f_t infeasible_checkpoint_refresh_ratio{0.99}; +}; + +template +struct fj_stats_t { + int64_t n_batch_attempts{0}; + int64_t n_batched_moves{0}; + std::vector batch_size_hist; + int64_t max_batch_size{0}; + double t_start{0}; + double t_bound_prop{0}; + double t_lp_start{0}; + double t_lp_relaxation{0}; + double t_coloring{0}; + double t_features{0}; + double t_init_lhs{0}; + fj_bin_setup_times_t bin_setup; + int64_t hit_count{0}; + int64_t miss_count{0}; + int64_t n_moves_applied{0}; + int64_t apply_move_nnz{0}; + int64_t n_mtm_calls{0}; + int64_t mtm_row_entries{0}; + int64_t mtm_entries_capped{0}; + int64_t n_compute_score_calls{0}; + int64_t compute_score_nnz{0}; + int64_t n_version_bumps_apply{0}; + int64_t n_version_bumps_weights{0}; + int64_t n_mtm_cache_invalidations{0}; + int64_t n_lhs_recompute_total{0}; + int64_t n_lhs_recompute_periodic{0}; + int64_t n_lhs_recompute_bigval{0}; + int64_t n_lhs_recompute_perturb{0}; + int64_t n_lhs_recompute_restart{0}; + i_t lhs_refresh_period_used{0}; + int64_t n_epigraph_projections{0}; + i_t max_restores_since_improvement{0}; + int64_t n_checkpoint_restores{0}; + int64_t n_checkpoint_snapshots{0}; + i_t nnz_processed_window{0}; +}; + +template +struct fj_runtime_t { + explicit fj_runtime_t(std::atomic& flag) : preemption_flag(flag) {} + i_t log_interval{0}; + i_t diversity_callback_interval{3000}; + std::function&, double)> improvement_callback{nullptr}; + std::function&)> diversity_callback{nullptr}; + std::string log_prefix; + std::shared_ptr> shared_incumbent; + std::atomic work_units_elapsed{0.0}; + double work_unit_bias{1.5}; + producer_sync_t* producer_sync{nullptr}; + std::atomic halted{false}; + instrumentation_aggregator_t memory_aggregator; + std::atomic& preemption_flag; +}; + +template +struct fj_cpu_climber_t : fj_tabu_t, + fj_weights_t, + fj_domains_t, + fj_move_cache_t, + fj_pair_scratch_t, + fj_epigraph_t, + fj_checkpoint_t, + fj_search_rows_t, + fj_search_state_t, + fj_batching_t, + fj_bin_bridge_t, + fj_lane_policy_t, + fj_stats_t, + fj_runtime_t { + fj_cpu_climber_t(std::atomic& preemption_flag) : fj_runtime_t(preemption_flag) + { +#define ADD_INSTRUMENTED(var) \ + std::make_pair(#var, std::ref(static_cast(this->var))) + + // Initialize memory aggregator with all ins_vector members + this->memory_aggregator = + instrumentation_aggregator_t{ADD_INSTRUMENTED(h_tabu_nodec_until), + ADD_INSTRUMENTED(h_tabu_noinc_until), + ADD_INSTRUMENTED(h_tabu_lastdec), + ADD_INSTRUMENTED(h_tabu_lastinc), + ADD_INSTRUMENTED(h_lhs), + ADD_INSTRUMENTED(h_lhs_sumcomp), + ADD_INSTRUMENTED(h_initial_left_weights), + ADD_INSTRUMENTED(h_initial_right_weights), + ADD_INSTRUMENTED(h_var_bounds), + ADD_INSTRUMENTED(h_is_binary_variable), + ADD_INSTRUMENTED(h_binary_indices), + ADD_INSTRUMENTED(h_assignment), + ADD_INSTRUMENTED(h_best_assignment), + ADD_INSTRUMENTED(h_best_infeasible_assignment), + ADD_INSTRUMENTED(h_row_state), + ADD_INSTRUMENTED(h_row_is_integral), + ADD_INSTRUMENTED(h_slack_sumcomp), + ADD_INSTRUMENTED(h_bound), + ADD_INSTRUMENTED(h_offsets), + ADD_INSTRUMENTED(h_variables), + ADD_INSTRUMENTED(h_coefficients), + ADD_INSTRUMENTED(h_reverse_offsets), + ADD_INSTRUMENTED(h_reverse_constraints), + ADD_INSTRUMENTED(h_reverse_coefficients)}; + +#undef ADD_INSTRUMENTED + } + fj_cpu_climber_t(const fj_cpu_climber_t& other) = delete; + fj_cpu_climber_t& operator=(const fj_cpu_climber_t& other) = delete; + + fj_cpu_climber_t(fj_cpu_climber_t&& other) = default; + fj_cpu_climber_t& operator=(fj_cpu_climber_t&& other) = default; + + void release_setup_structures() + { + this->h_initial_left_weights.clear(); + this->h_initial_left_weights.shrink_to_fit(); + this->h_initial_right_weights.clear(); + this->h_initial_right_weights.shrink_to_fit(); + } + + f_t get_user_objective(f_t solver_objective) const + { + cuopt_assert(std::isfinite(problem->objective_scaling_factor) && + problem->objective_scaling_factor != f_t{0}, + "invalid objective scaling factor"); + return problem->objective_scaling_factor * (solver_objective + problem->objective_offset); + } + + bool check_variable_within_bounds(i_t variable, f_t value) const + { + const auto bounds = this->h_var_bounds[variable]; + const f_t tol = problem->tolerances.integrality_tolerance; + return value <= get_upper(bounds) + tol && value >= get_lower(bounds) - tol; + } + + bool move_numerically_stable(f_t old_value, f_t new_value, f_t infeasibility, f_t total) const + { + return std::abs(new_value - old_value) < 1e6 && std::abs(new_value) < 1e20 && + std::abs(total - infeasibility) < 1e20; + } + + f_t excess_score(i_t row, f_t lhs, f_t lower, f_t upper) const + { + const f_t right = upper - lhs; + if (right < 0) return right; + const f_t left = lhs - lower; + return left < 0 ? left : f_t{0}; + } + + f_t breakthrough_value(i_t variable) const + { + const f_t coefficient = problem->h_obj_coeffs[variable]; + const auto bounds = this->h_var_bounds[variable]; + const f_t old_value = this->h_assignment[variable]; + const f_t excess = this->h_best_objective - this->h_incumbent_objective; + cuopt_assert(std::isfinite(excess) && excess < 0, "invalid breakthrough state"); + f_t value = old_value + excess / coefficient; + if (problem->h_var_types[variable] == var_t::INTEGER) { + value = coefficient > 0 ? std::floor(value + problem->tolerances.integrality_tolerance) + : std::ceil(value - problem->tolerances.integrality_tolerance); + } + if (!check_variable_within_bounds(variable, value)) + value = coefficient > 0 ? get_lower(bounds) : get_upper(bounds); + cuopt_assert(std::isfinite(value), "breakthrough move left the representable range"); + return value; + } + + // Shared across every lane and frozen before the first clone is created; see fj_cpu_problem_t. + std::shared_ptr> problem; +}; + +template +void cpufj_solve(fj_cpu_climber_t* fj_cpu, + double time_limit = std::numeric_limits::infinity(), + double work_unit_limit = std::numeric_limits::infinity()); + +// Copies a climber that has already paid the O(nnz) problem construction. Everything the engine +// reads is host-owned, so this needs neither a problem handle nor any GPU work. +template +std::unique_ptr> init_fj_cpu_clone( + const fj_cpu_climber_t& tmpl, + std::atomic& preemption_flag, + fj_settings_t settings = fj_settings_t{}); + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/cpu/tuning.hpp b/cpp/src/mip_heuristics/feasibility_jump/cpu/tuning.hpp new file mode 100644 index 0000000000..c9ca5b7181 --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/cpu/tuning.hpp @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace cuopt::mathematical_optimization::mip { + +struct fj_cpu_hyper_parameters_t { + double bigval_threshold = 1e20; + double start_magnitude_limit = 1e7; + double integer_domain_limit = 1e7; + + int32_t nnz_per_refresh_stretch = 100000; + int32_t max_refresh_stretch = 8; + + int32_t weight_escalate_after = 2000; + int32_t perturb_escalate_cap = 24; + int32_t weight_escalate_max = 100; + double weight_cap = 1e5; + int32_t weight_donor_samples = 4; + double weight_donation_floor = 1.0; + double obj_weight_incumbent_bump = 4.0; + double obj_weight_incumbent_cap = 64.0; + + int32_t restart_window_nnz_scale = 80000; + int32_t restart_window_scale_max = 4; + int32_t restart_window_multiple = 4; + + int32_t two_opt_candidates = 32; + + double batch_min_class_size = 2.0; + double batch_max_edges_per_nnz = 32.0; + int32_t batch_probe_attempts = 500; + double batch_min_yield = 0.05; + int32_t batch_hist_bins = 64; + + int32_t bound_prop_rounds = 10; + double bound_prop_commit_scale = 1e3; + + int32_t lp_start_nnz_limit = 8'000'000; + double lp_pump_max_budget_s = 2.0; + double lp_pump_budget_share = 0.00625; + int32_t lp_pump_projections = 1; + int32_t lp_polish_nnz_limit = 6'000'000; + double lp_polish_budget_share = 0.20; + double lp_polish_min_budget_s = 0.05; + + int32_t start_nnz_limit = 8'000'000; + double matching_budget_s = 0.45; + int32_t matching_max_row_width = 20000; + int32_t aggressive_passes = 6; + double aggressive_budget_s = 0.9; + double exact_k_tol = 1e-6; + int32_t exact_k_max_width = 20000; + double exact_k_budget_s = 0.5; + int32_t anchor_repair_violated_share = 5; + double anchor_repair_budget_s = 0.1; + double precedence_budget_s = 0.05; + int32_t precedence_passes = 24; + int32_t precedence_lower_num = 9; + int32_t precedence_lower_den = 10; + double covering_budget_s = 0.4; +}; + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu index a6c95b3297..0547a12354 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -9,6 +9,8 @@ #include +#include + namespace cuopt::mathematical_optimization::mip { template @@ -17,8 +19,9 @@ early_cpufj_t::early_cpufj_t( const typename mip_solver_settings_t::tolerances_t& tolerances, early_incumbent_callback_t incumbent_callback, uint64_t seed) - : early_heuristic_t>( - op_problem, tolerances, std::move(incumbent_callback)), + : early_heuristic_t>(op_problem, std::move(incumbent_callback)), + problem_ptr_(&op_problem), + tolerances_(tolerances), seed_(seed) { } @@ -30,44 +33,65 @@ early_cpufj_t::~early_cpufj_t() } template -void early_cpufj_t::start() +void early_cpufj_t::start(bool low_latency) { + const bool threaded = !omp_in_parallel(); // 1: presolve, 1: early GPU FJ, 1: early CPU FJ - if (fj_cpu_ || omp_get_num_threads() < CUOPT_MIP_EARLY_CPUFJ_REQUIRED_THREAD_COUNT) { return; } + if (climber_ || + (!threaded && omp_get_num_threads() < CUOPT_MIP_EARLY_CPUFJ_REQUIRED_THREAD_COUNT)) { + return; + } this->preemption_flag_.store(false); this->start_time_ = std::chrono::steady_clock::now(); - fj_cpu_ = - init_fj_cpu_standalone(*this->problem_ptr_, *this->solution_ptr_, preemption_flag_, seed_); - - fj_cpu_->log_prefix = "[Early CPUFJ] "; - - fj_cpu_->improvement_callback = [this](f_t solver_obj, - const std::vector& assignment, - double) { this->try_update_best(solver_obj, assignment); }; - - CUOPT_LOG_DEBUG("Launching early CPUFJ task"); -#pragma omp task shared(fj_cpu_) priority(CUOPT_DEFAULT_TASK_PRIORITY) \ - depend(out : *fj_cpu_) default(none) - cpufj_solve(fj_cpu_.get()); + auto report_incumbent = [this](f_t solver_obj, const std::vector& assignment, double) { + this->try_update_best(solver_obj, assignment); + }; + + fj_settings_t settings; + settings.seed = (int)seed_; + climber_ = init_fj_cpu_from_optimization_problem( + *this->problem_ptr_, tolerances_, preemption_flag_, settings); + climber_->low_latency = low_latency; + climber_->log_prefix = "[Early CPUFJ] "; + climber_->improvement_callback = report_incumbent; + + CUOPT_LOG_DEBUG("Launching early CPUFJ %s", threaded ? "thread" : "task"); + auto* climber = climber_.get(); + if (threaded) { + worker_ = std::thread([climber] { cpufj_solve(climber); }); + return; + } +#pragma omp task firstprivate(climber) priority(CUOPT_DEFAULT_TASK_PRIORITY) \ + depend(out : *climber) default(none) + cpufj_solve(climber); } template void early_cpufj_t::stop() { - if (!fj_cpu_) { return; } + if (!climber_) { return; } preemption_flag_.store(true); - - fj_cpu_->halted = true; -#pragma omp taskwait depend(in : *fj_cpu_) // Wait for the early CPUFJ task to finish + climber_->halted = true; + if (worker_.joinable()) { + worker_.join(); + } else { +#pragma omp taskwait depend(in : *climber_) // Wait for the early CPUFJ task to finish + } CUOPT_LOG_DEBUG("[Early CPUFJ] Stopped after %d iterations, solution_found=%d", - fj_cpu_ ? fj_cpu_->iterations : 0, + climber_->iterations, this->solution_found_); - fj_cpu_.reset(); + climber_.reset(); +} + +template +std::vector early_cpufj_t::to_user_assignment(const std::vector& assignment) +{ + return assignment; } #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh index 61ef3ff51f..531e2ea704 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -12,6 +12,8 @@ #include #include +#include +#include namespace cuopt::mathematical_optimization::mip { @@ -27,11 +29,18 @@ class early_cpufj_t : public early_heuristic_t static constexpr const char* name() { return "CPUFJ"; } - void start(); + void start(bool low_latency = false); void stop(); private: - std::unique_ptr> fj_cpu_; + friend class early_heuristic_t>; + + std::vector to_user_assignment(const std::vector& assignment); + + const optimization_problem_t* problem_ptr_{nullptr}; + typename mip_solver_settings_t::tolerances_t tolerances_; + std::unique_ptr> climber_; + std::thread worker_; std::atomic preemption_flag_{false}; // Explicit seed for this climber's FJ RNG, resolved once from the solve's base seed (see // mip_solver_context_t::base_seed) since this heuristic runs before that context exists. diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu index 66c97d9b7c..207c5c4dc7 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu @@ -10,9 +10,15 @@ #include #include #include +#include #include #include +#include +#include + +#include +#include #include @@ -22,11 +28,26 @@ template early_gpufj_t::early_gpufj_t(const optimization_problem_t& op_problem, const mip_solver_settings_t& settings, early_incumbent_callback_t incumbent_callback) - : early_heuristic_t>( - op_problem, settings.get_tolerances(), std::move(incumbent_callback)) + : early_heuristic_t>(op_problem, std::move(incumbent_callback)) { - context_ptr_ = std::make_unique>( - &this->handle_, this->problem_ptr_.get(), settings); + RAFT_CUDA_TRY(cudaGetDevice(&device_id_)); + + // Build and preprocess on the original handle, then copy onto our own handle + // so the derived solver can run on a dedicated stream (prevents graph capture conflicts). + problem_t temp_problem(op_problem, settings.get_tolerances(), false); + temp_problem.preprocess_problem(); + temp_problem.handle_ptr->sync_stream(); + problem_ptr_ = std::make_unique>(temp_problem, &handle_); + + solution_ptr_ = std::make_unique>(*problem_ptr_); + thrust::fill(handle_.get_thrust_policy(), + solution_ptr_->assignment.begin(), + solution_ptr_->assignment.end(), + f_t{0}); + solution_ptr_->clamp_within_bounds(); + + context_ptr_ = + std::make_unique>(&handle_, problem_ptr_.get(), settings); } template @@ -81,6 +102,18 @@ void early_gpufj_t::stop() fj_ptr_.reset(); } +template +std::vector early_gpufj_t::to_user_assignment(const std::vector& assignment) +{ + // Uses a private CUDA stream to avoid racing with the FJ solver's stream. + RAFT_CUDA_TRY(cudaSetDevice(device_id_)); + auto stream = handle_.get_stream(); + rmm::device_uvector d_assignment(assignment.size(), stream); + raft::copy(d_assignment.data(), assignment.data(), assignment.size(), stream); + problem_ptr_->post_process_assignment(d_assignment, true, stream); + return cuopt::host_copy(d_assignment, stream); +} + #if MIP_INSTANTIATE_FLOAT template class early_gpufj_t; #endif diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cuh b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cuh index 99e8579d31..ed8d17206e 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cuh @@ -8,8 +8,11 @@ #pragma once #include +#include +#include #include +#include namespace cuopt::mathematical_optimization::mip { @@ -34,6 +37,18 @@ class early_gpufj_t : public early_heuristic_t void stop(); private: + friend class early_heuristic_t>; + + std::vector to_user_assignment(const std::vector& assignment); + + int device_id_{0}; + + // handle_ must be declared before problem_ptr_/solution_ptr_ so it outlives them + // (C++ destroys members in reverse declaration order) + raft::handle_t handle_; + + std::unique_ptr> problem_ptr_; + std::unique_ptr> solution_ptr_; std::unique_ptr> context_ptr_; std::unique_ptr> fj_ptr_; }; diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu index 450019d415..7dc3c48754 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu @@ -139,7 +139,7 @@ void fj_t::reset_weights(cuda::stream_ref climber_stream, f_t weight) template void fj_t::randomize_weights(const raft::handle_t* handle_ptr) { - std::mt19937 host_rng(rng.next_i64()); + cuopt::pcgenerator_t host_rng(rng.next_i64()); constexpr f_t min_weight = 10.; constexpr f_t max_weight = 30.; // generate a range of weights between 10. and 30. diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh index b71892b39d..923a18d062 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh @@ -10,6 +10,8 @@ #include #include "utils.cuh" +#include "fj_types.hpp" + #include #include #include @@ -44,50 +46,6 @@ static constexpr int TPB_update_changed_constraints = raft::WarpSize * 4; static constexpr int TPB_liftmoves = raft::WarpSize * 4; static constexpr int TPB_loadbalance = raft::WarpSize * 4; -struct fj_hyper_parameters_t { - // The number of moves to evaluate, if there are many positive-score - // variables available. - int max_sampled_moves = raft::WarpSize * 16; - // The probability of choosing a random positive-score variable. - double random_var_probability = 0.04; - // The probability of choosing a variable using a random constraint's - // non-zero coefficient after updating weights. - double random_cstr_probability = 0.16; - // The period in iterations of each global move value update - // (all variables being updated vs. considering only the selected one) - int global_move_update_period = 10; - int heavy_move_update_period = 50; - int sync_period = 200; - int lhs_refresh_period = 500; - int allow_infeasibility_iterations = 200; - // The value added to the objective weight everytime a new best solution is - // found in order to move towards better solutions - double objective_weight_increment = 0.01; - int load_balancing_variable_threshold = 300; - int load_balancing_constraint_threshold = 5000; - int load_balancing_variable_split_size = 50; - - double breakthrough_move_epsilon = 1e-4; - int tabu_tenure_min = 3; - int tabu_tenure_max = 13; - double excess_improvement_weight = (1.0 / 2.0); - double weight_smoothing_probability = 0.0003; - - double fractional_score_multiplier = 100; - double rounding_second_stage_split = 0.1; - - double small_move_tabu_threshold = 1e-6; - int small_move_tabu_tenure = 4; - - int two_opt_max_rows = 4; - int two_opt_max_row_vars = 256; - int two_opt_max_pairs = 256; - - // load-balancing related settings - int old_codepath_total_var_to_relvar_ratio_threshold = 200; - int load_balancing_codepath_min_varcount = 3200; -}; - enum fj_move_type_t { FJ_MOVE_BEGIN = 0, FJ_MOVE_LIFT = FJ_MOVE_BEGIN, @@ -95,81 +53,6 @@ enum fj_move_type_t { FJ_MOVE_SIZE, }; -enum class fj_mode_t { - FIRST_FEASIBLE, // iterate until a feasible solution is found, then return - GREEDY_DESCENT, // single descent until no improving jumps can be made - TREE, // tree mode - ROUNDING, // FJ as rounding procedure for fractionals - EXIT_NON_IMPROVING // iterate until we are don't improve the best -}; - -enum class MTMMoveType { FJ_MTM_VIOLATED, FJ_MTM_SATISFIED, FJ_MTM_ALL }; - -enum class fj_load_balancing_mode_t { ALWAYS_ON, AUTO, ALWAYS_OFF }; - -enum class fj_candidate_selection_t { WEIGHTED_SCORE, FEASIBLE_FIRST }; - -struct fj_settings_t { - int seed{0}; - fj_mode_t mode{fj_mode_t::FIRST_FEASIBLE}; - fj_candidate_selection_t candidate_selection{fj_candidate_selection_t::WEIGHTED_SCORE}; - double time_limit{60.0}; - int iteration_limit{std::numeric_limits::max()}; - fj_hyper_parameters_t parameters{}; - int n_of_minimums_for_exit = 7000; - double infeasibility_weight = 1.0; - bool update_weights = true; - bool feasibility_run = true; - fj_load_balancing_mode_t load_balancing_mode{fj_load_balancing_mode_t::AUTO}; - double baseline_objective_for_longer_run{std::numeric_limits::lowest()}; -}; - -struct fj_move_t { - int var_idx; - double value; - - bool operator<(const fj_move_t& rhs) const - { - if (var_idx == rhs.var_idx) return value < rhs.value; - return var_idx < rhs.var_idx; - } - bool operator==(const fj_move_t& rhs) const - { - return var_idx == rhs.var_idx && value == rhs.value; - } - bool operator!=(const fj_move_t& rhs) const { return !(*this == rhs); } -}; - -// TODO: use 32bit integers instead, -// as we dont need them to be floating point per the FJ2 scoring scheme -// sizeof(fj_staged_score_t) <= 8 is needed to allow for atomic loads -struct fj_staged_score_t { - float base{-std::numeric_limits::infinity()}; - float bonus{-std::numeric_limits::infinity()}; - - HDI bool operator<(fj_staged_score_t other) const noexcept - { - return base == other.base ? bonus < other.bonus : base < other.base; - } - HDI bool operator>(fj_staged_score_t other) const noexcept - { - return base == other.base ? bonus > other.bonus : base > other.base; - } - HDI bool operator==(fj_staged_score_t other) const noexcept - { - return base == other.base && bonus == other.bonus; - } - HDI bool operator!=(fj_staged_score_t other) const noexcept { return !(*this == other); } - - HDI static fj_staged_score_t invalid() - { - return {-std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; - } - HDI static fj_staged_score_t zero() { return {0, 0}; } - - HDI bool valid() const { return *this != invalid(); } -}; - template struct fj_move_score_info_base_t { fj_staged_score_t score; diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu deleted file mode 100644 index 807ad8d729..0000000000 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ /dev/null @@ -1,2243 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include - -#include -#include - -#include "feasibility_jump.cuh" -#include "feasibility_jump_impl_common.cuh" -#include "fj_cpu.cuh" -#include "fj_cpu_worker.cuh" - -#include - -#include - -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define CPUFJ_TIMING_TRACE 0 - -// Define CPUFJ_NVTX_RANGES to enable detailed NVTX profiling ranges -#ifdef CPUFJ_NVTX_RANGES -#define CPUFJ_NVTX_RANGE(name) raft::common::nvtx::range CPUFJ_NVTX_UNIQUE_NAME(nvtx_scope_)(name) -#define CPUFJ_NVTX_UNIQUE_NAME(base) CPUFJ_NVTX_CONCAT(base, __LINE__) -#define CPUFJ_NVTX_CONCAT(a, b) CPUFJ_NVTX_CONCAT_INNER(a, b) -#define CPUFJ_NVTX_CONCAT_INNER(a, b) a##b -#else -#define CPUFJ_NVTX_RANGE(name) ((void)0) -#endif - -namespace cuopt::mathematical_optimization::mip { - -using simplex::lp_problem_t; -using simplex::simplex_solver_settings_t; -using simplex::variable_type_t; - -template -void finalize_fj_cpu_host_initialization( - fj_cpu_climber_t& fj_cpu, - i_t n_variables, - i_t n_constraints, - i_t n_integer_vars, - i_t nnz, - const typename mip_solver_settings_t::tolerances_t& tolerances); - -template -thrust::tuple get_mtm_for_bound(const typename fj_t::climber_data_t::view_t& fj, - i_t var_idx, - i_t cstr_idx, - f_t cstr_coeff, - f_t bound, - f_t sign, - const ArrayType& assignment, - const ArrayType& lhs_vector) -{ - f_t delta_ij = 0; - f_t slack = 0; - f_t old_val = assignment[var_idx]; - - f_t lhs = lhs_vector[cstr_idx] * sign; - f_t rhs = bound * sign; - slack = rhs - lhs; // bound might be infinite. let the caller handle this case - - delta_ij = slack / (cstr_coeff * sign); - - return {delta_ij, slack}; -} - -template -thrust::tuple get_mtm_for_constraint( - const typename fj_t::climber_data_t::view_t& fj, - i_t var_idx, - i_t cstr_idx, - f_t cstr_coeff, - f_t c_lb, - f_t c_ub, - const ArrayType& assignment, - const ArrayType& lhs_vector) -{ - f_t sign = -1; - f_t delta_ij = 0; - f_t slack = 0; - - f_t cstr_tolerance = fj.get_corrected_tolerance(cstr_idx, c_lb, c_ub); - - f_t old_val = assignment[var_idx]; - - // process each bound as two separate constraints - f_t bounds[2] = {c_lb, c_ub}; - cuopt_assert(isfinite(bounds[0]) || isfinite(bounds[1]), "bounds are not finite"); - - for (i_t bound_idx = 0; bound_idx < 2; ++bound_idx) { - if (!isfinite(bounds[bound_idx])) continue; - - // factor to correct the lhs/rhs to turn a lb <= lhs <= ub constraint into - // two virtual constraints lhs <= ub and -lhs <= -lb - sign = bound_idx == 0 ? -1 : 1; - f_t lhs = lhs_vector[cstr_idx] * sign; - f_t rhs = bounds[bound_idx] * sign; - slack = rhs - lhs; - - // skip constraints that are violated/satisfied based on the MTM move type - bool violated = slack < -cstr_tolerance; - if (move_type == MTMMoveType::FJ_MTM_VIOLATED ? !violated : violated) continue; - - f_t new_val = old_val; - - delta_ij = slack / (cstr_coeff * sign); - break; - } - - return {delta_ij, sign, slack, cstr_tolerance}; -} - -template -std::pair feas_score_constraint(const typename fj_t::climber_data_t::view_t& fj, - f_t delta, - i_t cstr_idx, - f_t cstr_coeff, - f_t c_lb, - f_t c_ub, - f_t current_lhs, - f_t left_weight, - f_t right_weight) -{ - cuopt_assert(isfinite(delta), "invalid delta"); - cuopt_assert(cstr_coeff != 0 && isfinite(cstr_coeff), "invalid coefficient"); - - f_t base_feas = 0; - f_t bonus_robust = 0; - - f_t bounds[2] = {c_lb, c_ub}; - cuopt_assert(isfinite(c_lb) || isfinite(c_ub), "no range"); - for (i_t bound_idx = 0; bound_idx < 2; ++bound_idx) { - if (!isfinite(bounds[bound_idx])) continue; - - // factor to correct the lhs/rhs to turn a lb <= lhs <= ub constraint into - // two virtual leq constraints "lhs <= ub" and "-lhs <= -lb" in order to match - // the convention of the paper - - // TODO: broadcast left/right weights to a csr_offset-indexed table? local minimums - // usually occur on a rarer basis (around 50 iteratiosn to 1 local minimum) - // likely unreasonable and overkill however - f_t cstr_weight = bound_idx == 0 ? left_weight : right_weight; - f_t sign = bound_idx == 0 ? -1 : 1; - f_t rhs = bounds[bound_idx] * sign; - f_t old_lhs = current_lhs * sign; - f_t new_lhs = (current_lhs + cstr_coeff * delta) * sign; - f_t old_slack = rhs - old_lhs; - f_t new_slack = rhs - new_lhs; - - cuopt_assert(isfinite(cstr_weight), "invalid weight"); - cuopt_assert(cstr_weight >= 0, "invalid weight"); - cuopt_assert(isfinite(old_lhs), ""); - cuopt_assert(isfinite(new_lhs), ""); - cuopt_assert(isfinite(old_slack) && isfinite(new_slack), ""); - - f_t cstr_tolerance = fj.get_corrected_tolerance(cstr_idx, c_lb, c_ub); - - bool old_viol = fj.excess_score(cstr_idx, current_lhs, c_lb, c_ub) < -cstr_tolerance; - bool new_viol = - fj.excess_score(cstr_idx, current_lhs + cstr_coeff * delta, c_lb, c_ub) < -cstr_tolerance; - - bool old_sat = old_lhs < rhs + cstr_tolerance; - bool new_sat = new_lhs < rhs + cstr_tolerance; - - // equality - if (fj.pb.integer_equal(c_lb, c_ub)) { - if (!old_viol) cuopt_assert(old_sat == !old_viol, ""); - if (!new_viol) cuopt_assert(new_sat == !new_viol, ""); - } - - // if it would feasibilize this constraint - if (!old_sat && new_sat) { - cuopt_assert(old_viol, ""); - base_feas += cstr_weight; - } - // would cause this constraint to be violated - else if (old_sat && !new_sat) { - cuopt_assert(new_viol, ""); - base_feas -= cstr_weight; - } - // simple improvement - else if (!old_sat && !new_sat && old_lhs > new_lhs) { - cuopt_assert(old_viol && new_viol, ""); - base_feas += (i_t)(cstr_weight * fj.settings->parameters.excess_improvement_weight); - } - // simple worsening - else if (!old_sat && !new_sat && old_lhs < new_lhs) { - cuopt_assert(old_viol && new_viol, ""); - base_feas -= (i_t)(cstr_weight * fj.settings->parameters.excess_improvement_weight); - } - - // robustness score bonus if this would leave some strick slack - bool old_stable = old_lhs < rhs - cstr_tolerance; - bool new_stable = new_lhs < rhs - cstr_tolerance; - if (!old_stable && new_stable) { - bonus_robust += cstr_weight; - } else if (old_stable && !new_stable) { - bonus_robust -= cstr_weight; - } - } - - return {base_feas, bonus_robust}; -} - -static constexpr double BIGVAL_THRESHOLD = 1e20; - -template -class timing_raii_t { - public: - timing_raii_t(std::vector& times_vec) - : times_vec_(times_vec), start_time_(std::chrono::high_resolution_clock::now()) - { - } - - ~timing_raii_t() - { - // vector::push_back can throw bad_alloc; the catch-all keeps the destructor - // exception-free. Losing one timing sample under OOM is acceptable. - // fprintf to stderr is allocation-free and cannot throw; using the project - // logger here would risk a secondary bad_alloc that would escape the - // destructor and re-introduce std::terminate. - try { - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = - std::chrono::duration_cast>(end_time - start_time_); - times_vec_.push_back(duration.count()); - } catch (const std::exception& e) { - std::fprintf(stderr, "timing_raii_t destructor: failed to record sample (%s).\n", e.what()); - } catch (...) { - std::fprintf(stderr, - "timing_raii_t destructor: failed to record sample (unknown exception).\n"); - } - } - - private: - std::vector& times_vec_; - std::chrono::high_resolution_clock::time_point start_time_; -}; - -template -static void print_timing_stats(fj_cpu_climber_t& fj_cpu) -{ - auto compute_avg_and_total = [](const std::vector& times) -> std::pair { - if (times.empty()) return {0.0, 0.0}; - double sum = 0.0; - for (double time : times) - sum += time; - return {sum / times.size(), sum}; - }; - - auto [lift_avg, lift_total] = compute_avg_and_total(fj_cpu.find_lift_move_times); - auto [viol_avg, viol_total] = compute_avg_and_total(fj_cpu.find_mtm_move_viol_times); - auto [sat_avg, sat_total] = compute_avg_and_total(fj_cpu.find_mtm_move_sat_times); - auto [apply_avg, apply_total] = compute_avg_and_total(fj_cpu.apply_move_times); - auto [weights_avg, weights_total] = compute_avg_and_total(fj_cpu.update_weights_times); - auto [compute_score_avg, compute_score_total] = compute_avg_and_total(fj_cpu.compute_score_times); - CUOPT_LOG_TRACE("=== Timing Statistics (Iteration %d) ===", fj_cpu.iterations); - CUOPT_LOG_TRACE("find_lift_move: avg=%.6f ms, total=%.6f ms, calls=%zu", - lift_avg * 1000.0, - lift_total * 1000.0, - fj_cpu.find_lift_move_times.size()); - CUOPT_LOG_TRACE("find_mtm_move_viol: avg=%.6f ms, total=%.6f ms, calls=%zu", - viol_avg * 1000.0, - viol_total * 1000.0, - fj_cpu.find_mtm_move_viol_times.size()); - CUOPT_LOG_TRACE("find_mtm_move_sat: avg=%.6f ms, total=%.6f ms, calls=%zu", - sat_avg * 1000.0, - sat_total * 1000.0, - fj_cpu.find_mtm_move_sat_times.size()); - CUOPT_LOG_TRACE("apply_move: avg=%.6f ms, total=%.6f ms, calls=%zu", - apply_avg * 1000.0, - apply_total * 1000.0, - fj_cpu.apply_move_times.size()); - CUOPT_LOG_TRACE("update_weights: avg=%.6f ms, total=%.6f ms, calls=%zu", - weights_avg * 1000.0, - weights_total * 1000.0, - fj_cpu.update_weights_times.size()); - CUOPT_LOG_TRACE("compute_score: avg=%.6f ms, total=%.6f ms, calls=%zu", - compute_score_avg * 1000.0, - compute_score_total * 1000.0, - fj_cpu.compute_score_times.size()); - CUOPT_LOG_TRACE("cache hit percentage: %.2f%%", - (double)fj_cpu.hit_count / (fj_cpu.hit_count + fj_cpu.miss_count) * 100.0); - CUOPT_LOG_TRACE("bin candidate move hit percentage: %.2f%%", - (double)fj_cpu.candidate_move_hits[0] / - (fj_cpu.candidate_move_hits[0] + fj_cpu.candidate_move_misses[0]) * 100.0); - CUOPT_LOG_TRACE("int candidate move hit percentage: %.2f%%", - (double)fj_cpu.candidate_move_hits[1] / - (fj_cpu.candidate_move_hits[1] + fj_cpu.candidate_move_misses[1]) * 100.0); - CUOPT_LOG_TRACE("cont candidate move hit percentage: %.2f%%", - (double)fj_cpu.candidate_move_hits[2] / - (fj_cpu.candidate_move_hits[2] + fj_cpu.candidate_move_misses[2]) * 100.0); - CUOPT_LOG_TRACE("========================================"); -} - -template -static void precompute_problem_features(fj_cpu_climber_t& fj_cpu) -{ - fj_cpu.n_binary_vars = 0; - fj_cpu.n_integer_vars = 0; - for (i_t i = 0; i < (i_t)fj_cpu.h_is_binary_variable.size(); i++) { - if (fj_cpu.h_is_binary_variable[i]) { - fj_cpu.n_binary_vars++; - } else if (fj_cpu.h_var_types[i] == var_t::INTEGER) { - fj_cpu.n_integer_vars++; - } - } - - i_t total_nnz = fj_cpu.h_reverse_offsets.back(); - i_t n_vars = fj_cpu.h_reverse_offsets.size() - 1; - i_t n_cstrs = fj_cpu.h_offsets.size() - 1; - - fj_cpu.avg_var_degree = (double)total_nnz / n_vars; - - fj_cpu.max_var_degree = 0; - std::vector var_degrees(n_vars); - for (i_t i = 0; i < n_vars; i++) { - i_t degree = fj_cpu.h_reverse_offsets[i + 1] - fj_cpu.h_reverse_offsets[i]; - var_degrees[i] = degree; - fj_cpu.max_var_degree = std::max(fj_cpu.max_var_degree, degree); - } - - double var_deg_variance = 0.0; - for (i_t i = 0; i < n_vars; i++) { - double diff = var_degrees[i] - fj_cpu.avg_var_degree; - var_deg_variance += diff * diff; - } - var_deg_variance /= n_vars; - double var_degree_std = std::sqrt(var_deg_variance); - fj_cpu.var_degree_cv = fj_cpu.avg_var_degree > 0 ? var_degree_std / fj_cpu.avg_var_degree : 0.0; - - fj_cpu.avg_cstr_degree = (double)total_nnz / n_cstrs; - - fj_cpu.max_cstr_degree = 0; - std::vector cstr_degrees(n_cstrs); - for (i_t i = 0; i < n_cstrs; i++) { - i_t degree = fj_cpu.h_offsets[i + 1] - fj_cpu.h_offsets[i]; - cstr_degrees[i] = degree; - fj_cpu.max_cstr_degree = std::max(fj_cpu.max_cstr_degree, degree); - } - - double cstr_deg_variance = 0.0; - for (i_t i = 0; i < n_cstrs; i++) { - double diff = cstr_degrees[i] - fj_cpu.avg_cstr_degree; - cstr_deg_variance += diff * diff; - } - cstr_deg_variance /= n_cstrs; - double cstr_degree_std = std::sqrt(cstr_deg_variance); - fj_cpu.cstr_degree_cv = - fj_cpu.avg_cstr_degree > 0 ? cstr_degree_std / fj_cpu.avg_cstr_degree : 0.0; - - fj_cpu.problem_density = (double)total_nnz / ((double)n_vars * n_cstrs); -} - -template -static void log_regression_features(fj_cpu_climber_t& fj_cpu, - double time_window_ms, - double total_time_ms, - size_t mem_loads_bytes, - size_t mem_stores_bytes) -{ - i_t total_nnz = fj_cpu.h_reverse_offsets.back(); - i_t n_vars = fj_cpu.h_reverse_offsets.size() - 1; - i_t n_cstrs = fj_cpu.h_offsets.size() - 1; - - // Dynamic runtime features - double violated_ratio = (double)fj_cpu.violated_constraints.size() / n_cstrs; - - // Compute per-iteration metrics - [[maybe_unused]] double nnz_per_move = 0.0; - i_t total_moves = - fj_cpu.n_lift_moves_window + fj_cpu.n_mtm_viol_moves_window + fj_cpu.n_mtm_sat_moves_window; - if (total_moves > 0) { nnz_per_move = (double)fj_cpu.nnz_processed_window / total_moves; } - - double eval_intensity = (double)fj_cpu.nnz_processed_window / 1000.0; - - // Cache and locality metrics - i_t cache_hits_window = fj_cpu.hit_count - fj_cpu.hit_count_window_start; - i_t cache_misses_window = fj_cpu.miss_count - fj_cpu.miss_count_window_start; - i_t total_cache_accesses = cache_hits_window + cache_misses_window; - double cache_hit_rate = - total_cache_accesses > 0 ? (double)cache_hits_window / total_cache_accesses : 0.0; - - i_t unique_cstrs = fj_cpu.unique_cstrs_accessed_window.size(); - i_t unique_vars = fj_cpu.unique_vars_accessed_window.size(); - - // Reuse ratios: how many times each constraint/variable was accessed on average - double cstr_reuse_ratio = - unique_cstrs > 0 ? (double)fj_cpu.nnz_processed_window / unique_cstrs : 0.0; - double var_reuse_ratio = - unique_vars > 0 ? (double)fj_cpu.n_variable_updates_window / unique_vars : 0.0; - - // Working set size estimation (KB) - // Each constraint: lhs (f_t) + 2 bounds (f_t) + sumcomp (f_t) = 4 * sizeof(f_t) - // Each variable: assignment (f_t) = 1 * sizeof(f_t) - i_t working_set_bytes = unique_cstrs * 4 * sizeof(f_t) + unique_vars * sizeof(f_t); - double working_set_kb = working_set_bytes / 1024.0; - - // Coverage: what fraction of problem is actively touched - double cstr_coverage = (double)unique_cstrs / n_cstrs; - double var_coverage = (double)unique_vars / n_vars; - - double loads_per_iter = 0.0; - double stores_per_iter = 0.0; - double l1_miss = -1.0; - double l3_miss = -1.0; - - // Compute memory statistics - double mem_loads_mb = mem_loads_bytes / 1e6; - double mem_stores_mb = mem_stores_bytes / 1e6; - double mem_total_mb = (mem_loads_bytes + mem_stores_bytes) / 1e6; - double mem_bandwidth_gb_per_sec = (mem_total_mb / 1000.0) / (time_window_ms / 1000.0); - - // Build per-wrapper memory statistics string - std::stringstream wrapper_stats; - auto per_wrapper_stats = fj_cpu.memory_aggregator.collect_per_wrapper(); - for (const auto& [name, loads, stores] : per_wrapper_stats) { - wrapper_stats << " " << name << "_loads=" << loads << " " << name << "_stores=" << stores; - } - - fj_cpu.memory_aggregator.flush(); - - // Print everything on a single line using precomputed features - CUOPT_LOG_DEBUG( - "%sCPUFJ_FEATURES iter=%d time_window=%.2f " - "n_vars=%d n_cstrs=%d n_bin=%d n_int=%d total_nnz=%d " - "avg_var_deg=%.2f max_var_deg=%d var_deg_cv=%.4f " - "avg_cstr_deg=%.2f max_cstr_deg=%d cstr_deg_cv=%.4f " - "density=%.6f " - "total_viol=%.4f obj_weight=%.4f max_weight=%.4f " - "n_locmin=%d iter_since_best=%d feas_found=%d " - "nnz_proc=%d n_lift=%d n_mtm_viol=%d n_mtm_sat=%d n_var_updates=%d " - "cache_hit_rate=%.4f unique_cstrs=%d unique_vars=%d " - "cstr_reuse=%.2f var_reuse=%.2f working_set_kb=%.1f " - "cstr_coverage=%.4f var_coverage=%.4f " - "L1_miss=%.2f L3_miss=%.2f loads_per_iter=%.0f stores_per_iter=%.0f " - "viol_ratio=%.4f nnz_per_move=%.2f eval_intensity=%.2f " - "mem_loads_mb=%.3f mem_stores_mb=%.3f mem_total_mb=%.3f mem_bandwidth_gb_s=%.3f%s", - fj_cpu.log_prefix.c_str(), - fj_cpu.iterations, - time_window_ms, - n_vars, - n_cstrs, - fj_cpu.n_binary_vars, - fj_cpu.n_integer_vars, - total_nnz, - fj_cpu.avg_var_degree, - fj_cpu.max_var_degree, - fj_cpu.var_degree_cv, - fj_cpu.avg_cstr_degree, - fj_cpu.max_cstr_degree, - fj_cpu.cstr_degree_cv, - fj_cpu.problem_density, - fj_cpu.total_violations, - fj_cpu.h_objective_weight, - fj_cpu.max_weight, - fj_cpu.n_local_minima_window, - fj_cpu.iterations_since_best, - fj_cpu.feasible_found ? 1 : 0, - fj_cpu.nnz_processed_window, - fj_cpu.n_lift_moves_window, - fj_cpu.n_mtm_viol_moves_window, - fj_cpu.n_mtm_sat_moves_window, - fj_cpu.n_variable_updates_window, - cache_hit_rate, - unique_cstrs, - unique_vars, - cstr_reuse_ratio, - var_reuse_ratio, - working_set_kb, - cstr_coverage, - var_coverage, - l1_miss, - l3_miss, - loads_per_iter, - stores_per_iter, - violated_ratio, - nnz_per_move, - eval_intensity, - mem_loads_mb, - mem_stores_mb, - mem_total_mb, - mem_bandwidth_gb_per_sec, - wrapper_stats.str().c_str()); - - // Reset window counters - fj_cpu.nnz_processed_window = 0; - fj_cpu.n_lift_moves_window = 0; - fj_cpu.n_mtm_viol_moves_window = 0; - fj_cpu.n_mtm_sat_moves_window = 0; - fj_cpu.n_variable_updates_window = 0; - fj_cpu.n_local_minima_window = 0; - fj_cpu.prev_best_objective = fj_cpu.h_best_objective; - - // Reset cache and locality tracking - fj_cpu.hit_count_window_start = fj_cpu.hit_count; - fj_cpu.miss_count_window_start = fj_cpu.miss_count; - fj_cpu.unique_cstrs_accessed_window.clear(); - fj_cpu.unique_vars_accessed_window.clear(); -} - -template -static inline std::pair reverse_range_for_var(fj_cpu_climber_t& fj_cpu, - i_t var_idx) -{ - cuopt_assert(var_idx >= 0 && var_idx < fj_cpu.view.pb.n_variables, - "Variable should be within the range"); - return std::make_pair(fj_cpu.h_reverse_offsets[var_idx], fj_cpu.h_reverse_offsets[var_idx + 1]); -} - -template -static inline std::pair range_for_constraint(fj_cpu_climber_t& fj_cpu, - i_t cstr_idx) -{ - return std::make_pair(fj_cpu.h_offsets[cstr_idx], fj_cpu.h_offsets[cstr_idx + 1]); -} - -template -static inline bool check_variable_within_bounds(fj_cpu_climber_t& fj_cpu, - i_t var_idx, - f_t val) -{ - const f_t int_tol = fj_cpu.view.pb.tolerances.integrality_tolerance; - auto bounds = fj_cpu.h_var_bounds[var_idx].get(); - bool within_bounds = val <= (get_upper(bounds) + int_tol) && val >= (get_lower(bounds) - int_tol); - return within_bounds; -} - -template -static inline bool is_integer_var(fj_cpu_climber_t& fj_cpu, i_t var_idx) -{ - return var_t::INTEGER == fj_cpu.h_var_types[var_idx]; -} - -template -static inline bool tabu_check(fj_cpu_climber_t& fj_cpu, - i_t var_idx, - f_t delta, - bool localmin = false) -{ - if (localmin) { - return (delta < 0 && fj_cpu.iterations == fj_cpu.h_tabu_lastinc[var_idx] + 1) || - (delta >= 0 && fj_cpu.iterations == fj_cpu.h_tabu_lastdec[var_idx] + 1); - } else { - return (delta < 0 && fj_cpu.iterations < fj_cpu.h_tabu_nodec_until[var_idx]) || - (delta >= 0 && fj_cpu.iterations < fj_cpu.h_tabu_noinc_until[var_idx]); - } -} - -template -static bool check_variable_feasibility(fj_cpu_climber_t& fj_cpu, - bool check_integer = true) -{ - for (i_t var_idx = 0; var_idx < fj_cpu.view.pb.n_variables; var_idx += 1) { - auto val = fj_cpu.h_assignment[var_idx]; - bool feasible = check_variable_within_bounds(fj_cpu, var_idx, val); - - if (!feasible) return false; - if (check_integer && is_integer_var(fj_cpu, var_idx) && - !fj_cpu.view.pb.is_integer(fj_cpu.h_assignment[var_idx])) - return false; - } - return true; -} - -template -static inline std::pair compute_score(fj_cpu_climber_t& fj_cpu, - i_t var_idx, - f_t delta) -{ - // timing_raii_t timer(fj_cpu.compute_score_times); - - f_t obj_diff = fj_cpu.h_obj_coeffs[var_idx] * delta; - - cuopt_assert(isfinite(delta), ""); - - cuopt_assert(var_idx < fj_cpu.view.pb.n_variables, "variable index out of bounds"); - - f_t base_feas_sum = 0; - f_t bonus_robust_sum = 0; - - auto [offset_begin, offset_end] = reverse_range_for_var(fj_cpu, var_idx); - fj_cpu.nnz_processed_window += (offset_end - offset_begin); - - for (i_t i = offset_begin; i < offset_end; i++) { - auto cstr_idx = fj_cpu.h_reverse_constraints[i]; - fj_cpu.unique_cstrs_accessed_window.insert(cstr_idx); - auto cstr_coeff = fj_cpu.h_reverse_coefficients[i]; - auto [c_lb, c_ub] = fj_cpu.cached_cstr_bounds[i].get(); - - cuopt_assert(c_lb <= c_ub, "invalid bounds"); - - auto [cstr_base_feas, cstr_bonus_robust] = - feas_score_constraint(fj_cpu.view, - delta, - cstr_idx, - cstr_coeff, - c_lb, - c_ub, - fj_cpu.h_lhs[cstr_idx], - fj_cpu.h_cstr_left_weights[cstr_idx], - fj_cpu.h_cstr_right_weights[cstr_idx]); - - base_feas_sum += cstr_base_feas; - bonus_robust_sum += cstr_bonus_robust; - } - - f_t base_obj = 0; - if (obj_diff < 0) // improving move wrt objective - base_obj = fj_cpu.h_objective_weight; - else if (obj_diff > 0) - base_obj = -fj_cpu.h_objective_weight; - - f_t bonus_breakthrough = 0; - - bool old_obj_better = fj_cpu.h_incumbent_objective < fj_cpu.h_best_objective; - bool new_obj_better = fj_cpu.h_incumbent_objective + obj_diff < fj_cpu.h_best_objective; - if (!old_obj_better && new_obj_better) - bonus_breakthrough += fj_cpu.h_objective_weight; - else if (old_obj_better && !new_obj_better) { - bonus_breakthrough -= fj_cpu.h_objective_weight; - } - - fj_staged_score_t score; - score.base = round(base_obj + base_feas_sum); - score.bonus = round(bonus_breakthrough + bonus_robust_sum); - return std::make_pair(score, base_feas_sum); -} - -struct two_opt_move_t { - fj_move_t first{-1, 0}; - fj_move_t second{-1, 0}; - fj_staged_score_t score{fj_staged_score_t::invalid()}; - int age{std::numeric_limits::max()}; - - bool operator>(const two_opt_move_t& other) const - { - if (score != other.score) return score > other.score; - if (age != other.age) return age < other.age; - if (first.var_idx != other.first.var_idx) return first.var_idx < other.first.var_idx; - return second.var_idx < other.second.var_idx; - } -}; - -// returns the combined score of a joint 2opt move -template -static fj_staged_score_t two_opt_compute_pair_score( - fj_cpu_climber_t& fj_cpu, i_t first, f_t first_delta, i_t second, f_t second_delta) -{ - auto& row_deltas = fj_cpu.two_opt_row_deltas; - row_deltas.clear(); - const fj_move_t endpoints[2] = {{first, first_delta}, {second, second_delta}}; - for (const auto& [var_idx, delta] : endpoints) { - const auto [offset_begin, offset_end] = reverse_range_for_var(fj_cpu, var_idx); - fj_cpu.nnz_processed_window += offset_end - offset_begin; - for (i_t i = offset_begin; i < offset_end; ++i) { - const i_t cstr_idx = fj_cpu.h_reverse_constraints[i]; - const f_t coeff = fj_cpu.h_reverse_coefficients[i]; - row_deltas.emplace_back(cstr_idx, coeff * delta); - } - } - // Brings the entries of a shared row next to each other - std::sort(row_deltas.begin(), row_deltas.end()); - - f_t base_feas_sum = 0; - f_t bonus_robust_sum = 0; - for (size_t pos = 0; pos < row_deltas.size();) { - const i_t cstr_idx = row_deltas[pos].first; - f_t lhs_delta = 0; - do { - lhs_delta += row_deltas[pos++].second; - } while (pos < row_deltas.size() && row_deltas[pos].first == cstr_idx); - - // The coefficients are already folded into lhs_delta, hence the unit coefficient - auto [cstr_base_feas, cstr_bonus_robust] = - feas_score_constraint(fj_cpu.view, - lhs_delta, - cstr_idx, - 1, - fj_cpu.h_cstr_lb[cstr_idx], - fj_cpu.h_cstr_ub[cstr_idx], - fj_cpu.h_lhs[cstr_idx], - fj_cpu.h_cstr_left_weights[cstr_idx], - fj_cpu.h_cstr_right_weights[cstr_idx]); - base_feas_sum += cstr_base_feas; - bonus_robust_sum += cstr_bonus_robust; - } - - const f_t obj_diff = - fj_cpu.h_obj_coeffs[first] * first_delta + fj_cpu.h_obj_coeffs[second] * second_delta; - f_t base_obj = 0; - if (obj_diff < 0) - base_obj = fj_cpu.h_objective_weight; - else if (obj_diff > 0) - base_obj = -fj_cpu.h_objective_weight; - - f_t bonus_breakthrough = 0; - bool old_obj_better = fj_cpu.h_incumbent_objective < fj_cpu.h_best_objective; - bool new_obj_better = fj_cpu.h_incumbent_objective + obj_diff < fj_cpu.h_best_objective; - if (!old_obj_better && new_obj_better) - bonus_breakthrough += fj_cpu.h_objective_weight; - else if (old_obj_better && !new_obj_better) - bonus_breakthrough -= fj_cpu.h_objective_weight; - - fj_staged_score_t score; - score.base = round(base_obj + base_feas_sum); - score.bonus = round(bonus_breakthrough + bonus_robust_sum); - return score; -} - -template -static void two_opt_add_partner(fj_cpu_climber_t& fj_cpu, - i_t first, - i_t var_idx, - f_t target) -{ - if (var_idx == first) return; - const f_t val = fj_cpu.h_assignment[var_idx].get(); - // A partner between two integers has no opposite value to swap to - if (!fj_cpu.view.pb.is_integer(val)) return; - const f_t delta = target - val; - // Already at the value we would move it to, so there is no compound move to make - if (fabs(delta) < 0.5) return; - if (!check_variable_within_bounds(fj_cpu, var_idx, target)) return; - if (tabu_check(fj_cpu, var_idx, delta, true)) return; - fj_cpu.two_opt_partners.emplace_back(var_idx, delta); -} - -/** - * @brief Fill fj_cpu.two_opt_partners with candidates to flip together with `first`. - * - * Preferred source is the probing cache: it recorded, for each probed variable and value, the - * bounds propagation implies on every other variable. An implied bound pinning a binary to a value - * names both the partner and the value it has to take once `first` moves, so a pair moving in the - * same direction is reached as naturally as a swap. The - * variables sharing a row with it are used as fallback. - */ -template -static void two_opt_collect_partners(fj_cpu_climber_t& fj_cpu, - i_t first, - f_t first_delta, - size_t max_partners) -{ - auto& partners = fj_cpu.two_opt_partners; - const i_t n_variables = fj_cpu.view.pb.n_variables; - partners.clear(); - cuopt_assert(fj_cpu.h_is_binary_variable[first], "2-opt is only defined for binaries"); - cuopt_assert( - fj_cpu.probing_cache == nullptr || fj_cpu.h_original_ids.size() == (size_t)n_variables, - "original id map does not cover every variable"); - cuopt_assert(fj_cpu.probing_cache == nullptr || - fj_cpu.h_reverse_original_ids.size() >= fj_cpu.h_original_ids.size(), - "reverse original id map smaller than the problem"); - - if (fj_cpu.probing_cache != nullptr) { - const auto& cache = fj_cpu.probing_cache->probing_cache; - const auto cached_probe = cache.find(fj_cpu.h_original_ids[first]); - if (cached_probe != cache.end()) { - const f_t new_val = fj_cpu.h_assignment[first].get() + first_delta; - i_t hit_interval = -1; - i_t unused_hit = -1; - for (i_t interval = 0; interval < 2; ++interval) { - const auto& entry = cached_probe->second[interval]; - if (entry.var_to_cached_bound_map.empty()) { continue; } - entry.val_interval.fill_cache_hits(interval, new_val, new_val, hit_interval, unused_hit); - } - if (hit_interval != -1) { - const auto& implications = cached_probe->second[hit_interval].var_to_cached_bound_map; - for (const auto& [probed_id, implied] : implications) { - if (partners.size() >= max_partners) break; - const i_t var_idx = fj_cpu.h_reverse_original_ids[probed_id]; - // -1 means presolve removed the variable after the probe recorded it - if (var_idx < 0) { continue; } - cuopt_assert(var_idx < n_variables, "implied variable out of range"); - if (!fj_cpu.h_is_binary_variable[var_idx]) { continue; } - if (!fj_cpu.view.pb.integer_equal(implied.lb, implied.ub)) { continue; } - two_opt_add_partner(fj_cpu, first, var_idx, round(implied.lb)); - } - } - } - } - - const auto& related = fj_cpu.h_related_variables; - const auto& related_offsets = fj_cpu.h_related_variables_offsets; - if (related_offsets.size() != (size_t)n_variables + 1) return; - const f_t swap_target = fj_cpu.h_assignment[first].get(); - const i_t related_begin = related_offsets[first]; - const i_t related_end = related_offsets[first + 1]; - for (i_t i = related_begin; i < related_end && partners.size() < max_partners; ++i) { - const i_t var_idx = related[i]; - if (fj_cpu.h_is_binary_variable[var_idx]) { - two_opt_add_partner(fj_cpu, first, var_idx, swap_target); - } - } -} - -// Look for binary 2opt moves at a local minimum. by definition no 1opt move can improve, but -// combined moves may especially in the case of set partitioning constraints / cliques. Use -// information from the probing cache to find potential good 2opt moves. -template -static two_opt_move_t find_two_opt_move(fj_cpu_climber_t& fj_cpu) -{ - CPUFJ_NVTX_RANGE("CPUFJ::find_two_opt_move"); - constexpr size_t max_obj_starts = 64; - constexpr size_t max_partners_per_var = 16; - - const auto& params = fj_cpu.settings.parameters; - const size_t max_target_rows = params.two_opt_max_rows; - const size_t max_first_vars = params.two_opt_max_row_vars; - const size_t max_pairs = params.two_opt_max_pairs; - - two_opt_move_t best; - - const bool partner_source_exists = - (fj_cpu.probing_cache != nullptr && !fj_cpu.probing_cache->probing_cache.empty()) || - (int64_t)fj_cpu.h_related_variables_offsets.size() == fj_cpu.view.pb.n_variables + 1; - - if (fj_cpu.n_binary_vars == 0 || !partner_source_exists) return best; - - auto& first_vars = fj_cpu.two_opt_first_vars; - first_vars.clear(); - - // target binvars in violated constraints for flips - if (!fj_cpu.violated_constraints.empty()) { - cuopt_assert(fj_cpu.h_binrow_offsets.size() == fj_cpu.view.pb.n_constraints + 1, - "binary row table missing"); - auto& target_cstrs = fj_cpu.two_opt_target_cstrs; - target_cstrs.clear(); - std::sample(fj_cpu.violated_constraints.begin(), - fj_cpu.violated_constraints.end(), - std::back_inserter(target_cstrs), - max_target_rows, - fj_cpu.rng); - for (i_t cstr_idx : target_cstrs) { - const i_t bin_begin = fj_cpu.h_binrow_offsets[cstr_idx]; - const i_t bin_end = fj_cpu.h_binrow_offsets[cstr_idx + 1]; - for (i_t i = bin_begin; i < bin_end && first_vars.size() < max_first_vars; ++i) { - first_vars.push_back(fj_cpu.h_binrow_vars[i].get()); - } - } - } else { - // target objective-bearing binary vars in satisfied constraints - std::sample(fj_cpu.h_objective_vars.underlying().begin(), - fj_cpu.h_objective_vars.underlying().end(), - std::back_inserter(first_vars), - max_obj_starts, - fj_cpu.rng); - first_vars.erase(std::remove_if(first_vars.begin(), - first_vars.end(), - [&](i_t var_idx) { - if (!fj_cpu.h_is_binary_variable[var_idx]) return true; - const f_t delta = - round(1 - 2 * fj_cpu.h_assignment[var_idx].get()); - return fj_cpu.h_obj_coeffs[var_idx] * delta >= 0; - }), - first_vars.end()); - } - std::shuffle(first_vars.begin(), first_vars.end(), fj_cpu.rng); - - const i_t nnz_at_entry = fj_cpu.nnz_processed_window; - size_t pairs_scored = 0; - // find a (first, second) pair for the 2opt - for (i_t first : first_vars) { - if (pairs_scored >= max_pairs) break; - if (fj_cpu.nnz_processed_window - nnz_at_entry > fj_cpu.nnz_samples) break; - const f_t first_val = fj_cpu.h_assignment[first].get(); - if (!fj_cpu.view.pb.is_integer(first_val)) continue; - const f_t first_delta = round(1 - 2 * first_val); - if (tabu_check(fj_cpu, first, first_delta, true)) continue; - if (!check_variable_within_bounds(fj_cpu, first, first_val + first_delta)) continue; - const i_t first_touch = std::max(fj_cpu.h_tabu_lastinc[first], fj_cpu.h_tabu_lastdec[first]); - - // look for potential other binary vars to flip alongside the first var - two_opt_collect_partners(fj_cpu, first, first_delta, max_partners_per_var); - for (const auto& [second, second_delta] : fj_cpu.two_opt_partners) { - const i_t second_touch = - std::max(fj_cpu.h_tabu_lastinc[second], fj_cpu.h_tabu_lastdec[second]); - two_opt_move_t cand; - cand.first = {first, first_delta}; - cand.second = {second, second_delta}; - cand.score = two_opt_compute_pair_score(fj_cpu, first, first_delta, second, second_delta); - cand.age = std::max(first_touch, second_touch); - if (cand > best) { best = cand; } - ++pairs_scored; - - if (pairs_scored >= max_pairs) return best; - if (fj_cpu.nnz_processed_window - nnz_at_entry > fj_cpu.nnz_samples) return best; - } - } - return best; -} - -template -static void smooth_weights(fj_cpu_climber_t& fj_cpu) -{ - CPUFJ_NVTX_RANGE("CPUFJ::smooth_weights"); - for (i_t cstr_idx = 0; cstr_idx < fj_cpu.view.pb.n_constraints; cstr_idx++) { - // consider only satisfied constraints - if (fj_cpu.violated_constraints.count(cstr_idx)) continue; - - f_t weight_l = std::max((f_t)0, fj_cpu.h_cstr_left_weights[cstr_idx] - 1); - f_t weight_r = std::max((f_t)0, fj_cpu.h_cstr_right_weights[cstr_idx] - 1); - - fj_cpu.h_cstr_left_weights[cstr_idx] = weight_l; - fj_cpu.h_cstr_right_weights[cstr_idx] = weight_r; - } - - if (fj_cpu.h_objective_weight > 0 && fj_cpu.h_incumbent_objective >= fj_cpu.h_best_objective) { - fj_cpu.h_objective_weight = std::max((f_t)0, fj_cpu.h_objective_weight - 1); - } -} - -template -static void update_weights(fj_cpu_climber_t& fj_cpu) -{ - timing_raii_t timer(fj_cpu.update_weights_times); - CPUFJ_NVTX_RANGE("CPUFJ::update_weights"); - - raft::random::PCGenerator rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); - bool smoothing = rng.next_float() <= fj_cpu.settings.parameters.weight_smoothing_probability; - - if (smoothing) { - smooth_weights(fj_cpu); - return; - } - - for (auto cstr_idx : fj_cpu.violated_constraints) { - f_t curr_incumbent_lhs = fj_cpu.h_lhs[cstr_idx]; - f_t curr_lower_excess = - fj_cpu.view.lower_excess_score(cstr_idx, curr_incumbent_lhs, fj_cpu.h_cstr_lb[cstr_idx]); - f_t curr_upper_excess = - fj_cpu.view.upper_excess_score(cstr_idx, curr_incumbent_lhs, fj_cpu.h_cstr_ub[cstr_idx]); - f_t curr_excess_score = curr_lower_excess + curr_upper_excess; - - f_t old_weight; - if (curr_lower_excess < 0.) { - old_weight = fj_cpu.h_cstr_left_weights[cstr_idx]; - } else { - old_weight = fj_cpu.h_cstr_right_weights[cstr_idx]; - } - - cuopt_assert(curr_excess_score < 0, "constraint not violated"); - - i_t int_delta = 1.0; - f_t delta = int_delta; - - f_t new_weight = old_weight + delta; - new_weight = round(new_weight); - - if (curr_lower_excess < 0.) { - fj_cpu.h_cstr_left_weights[cstr_idx] = new_weight; - fj_cpu.max_weight = std::max(fj_cpu.max_weight, new_weight); - } else { - fj_cpu.h_cstr_right_weights[cstr_idx] = new_weight; - fj_cpu.max_weight = std::max(fj_cpu.max_weight, new_weight); - } - - // Invalidate related cached move scores - auto [relvar_offset_begin, relvar_offset_end] = - range_for_constraint(fj_cpu, cstr_idx); - for (auto i = relvar_offset_begin; i < relvar_offset_end; i++) { - fj_cpu.cached_mtm_moves[i].first = 0; - } - } - - if (fj_cpu.violated_constraints.empty()) { fj_cpu.h_objective_weight += 1; } -} - -template -static void apply_move(fj_cpu_climber_t& fj_cpu, - i_t var_idx, - f_t delta, - bool localmin = false) -{ - timing_raii_t timer(fj_cpu.apply_move_times); - CPUFJ_NVTX_RANGE("CPUFJ::apply_move"); - - raft::random::PCGenerator rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); - - cuopt_assert(var_idx < fj_cpu.view.pb.n_variables, "variable index out of bounds"); - f_t old_val = fj_cpu.h_assignment[var_idx]; - f_t new_val = old_val + delta; - if (is_integer_var(fj_cpu, var_idx)) { - cuopt_assert(fj_cpu.view.pb.integer_equal(new_val, round(new_val)), "new_val is not integer"); - new_val = round(new_val); - } - // clamp to var bounds - new_val = std::min(std::max(new_val, get_lower(fj_cpu.h_var_bounds[var_idx].get())), - get_upper(fj_cpu.h_var_bounds[var_idx].get())); - delta = new_val - old_val; - cuopt_assert(isfinite(new_val), "assignment is not finite"); - cuopt_assert(isfinite(delta), "applied delta is not finite"); - cuopt_assert(check_variable_within_bounds(fj_cpu, var_idx, new_val), - "assignment not within bounds"); - - // Update the LHSs of all involved constraints. - auto [offset_begin, offset_end] = reverse_range_for_var(fj_cpu, var_idx); - - fj_cpu.nnz_processed_window += (offset_end - offset_begin); - fj_cpu.n_variable_updates_window++; - fj_cpu.unique_vars_accessed_window.insert(var_idx); - - i_t previous_viol = fj_cpu.violated_constraints.size(); - - for (auto i = offset_begin; i < offset_end; i++) { - cuopt_assert(i < (i_t)fj_cpu.h_reverse_constraints.size(), ""); - auto [c_lb, c_ub] = fj_cpu.cached_cstr_bounds[i].get(); - - auto cstr_idx = fj_cpu.h_reverse_constraints[i]; - fj_cpu.unique_cstrs_accessed_window.insert(cstr_idx); - auto cstr_coeff = fj_cpu.h_reverse_coefficients[i]; - - f_t old_lhs = fj_cpu.h_lhs[cstr_idx]; - // Kahan compensated summation - f_t y = cstr_coeff * delta - fj_cpu.h_lhs_sumcomp[cstr_idx]; - f_t t = old_lhs + y; - fj_cpu.h_lhs_sumcomp[cstr_idx] = (t - old_lhs) - y; - fj_cpu.h_lhs[cstr_idx] = t; - f_t new_lhs = fj_cpu.h_lhs[cstr_idx]; - f_t old_cost = fj_cpu.view.excess_score(cstr_idx, old_lhs, c_lb, c_ub); - f_t new_cost = fj_cpu.view.excess_score(cstr_idx, new_lhs, c_lb, c_ub); - f_t cstr_tolerance = fj_cpu.view.get_corrected_tolerance(cstr_idx, c_lb, c_ub); - - // trigger early lhs recomputation if the sumcomp term gets too large - // to avoid large numerical errors - if (fabs(fj_cpu.h_lhs_sumcomp[cstr_idx]) > BIGVAL_THRESHOLD) - fj_cpu.trigger_early_lhs_recomputation = true; - - if (new_cost < -cstr_tolerance && !fj_cpu.violated_constraints.count(cstr_idx)) { - fj_cpu.violated_constraints.insert(cstr_idx); - cuopt_assert(fj_cpu.satisfied_constraints.count(cstr_idx) == 1, ""); - fj_cpu.satisfied_constraints.erase(cstr_idx); - } else if (!(new_cost < -cstr_tolerance) && fj_cpu.violated_constraints.count(cstr_idx)) { - cuopt_assert(fj_cpu.satisfied_constraints.count(cstr_idx) == 0, ""); - fj_cpu.violated_constraints.erase(cstr_idx); - fj_cpu.satisfied_constraints.insert(cstr_idx); - } - - cuopt_assert(isfinite(delta), "delta should be finite"); - cuopt_assert(isfinite(fj_cpu.h_lhs[cstr_idx]), "assignment should be finite"); - - // Invalidate related cached move scores - auto [relvar_offset_begin, relvar_offset_end] = - range_for_constraint(fj_cpu, cstr_idx); - for (auto i = relvar_offset_begin; i < relvar_offset_end; i++) { - fj_cpu.cached_mtm_moves[i].first = 0; - } - } - - if (previous_viol > 0 && fj_cpu.violated_constraints.empty()) { - fj_cpu.last_feasible_entrance_iter = fj_cpu.iterations; - } - - // update the assignment and objective proper - fj_cpu.h_assignment[var_idx] = new_val; - fj_cpu.h_incumbent_objective += fj_cpu.h_obj_coeffs[var_idx] * delta; - if (fj_cpu.h_incumbent_objective < fj_cpu.h_best_objective && - fj_cpu.violated_constraints.empty()) { - // recompute the LHS values to cancel out accumulation errors, then check if feasibility remains - recompute_lhs(fj_cpu); - - if (fj_cpu.violated_constraints.empty() && check_variable_feasibility(fj_cpu)) { - cuopt_assert(fj_cpu.satisfied_constraints.size() == fj_cpu.view.pb.n_constraints, ""); - fj_cpu.h_best_objective = - fj_cpu.h_incumbent_objective - fj_cpu.settings.parameters.breakthrough_move_epsilon; - fj_cpu.h_best_assignment = fj_cpu.h_assignment; - fj_cpu.iterations_since_best = 0; - CUOPT_LOG_TRACE( - "%sCPUFJ: new best objective: %g", fj_cpu.log_prefix.c_str(), fj_cpu.h_incumbent_objective); - if (fj_cpu.improvement_callback) { - double current_work_units = fj_cpu.work_units_elapsed.load(std::memory_order_acquire); - fj_cpu.improvement_callback( - fj_cpu.h_incumbent_objective, fj_cpu.h_assignment, current_work_units); - } - fj_cpu.feasible_found = true; - } - } - - i_t tabu_tenure = fj_cpu.settings.parameters.tabu_tenure_min + - rng.next_u32() % (fj_cpu.settings.parameters.tabu_tenure_max - - fj_cpu.settings.parameters.tabu_tenure_min); - if (delta > 0) { - fj_cpu.h_tabu_lastinc[var_idx] = fj_cpu.iterations; - fj_cpu.h_tabu_nodec_until[var_idx] = fj_cpu.iterations + tabu_tenure; - fj_cpu.h_tabu_noinc_until[var_idx] = fj_cpu.iterations + tabu_tenure / 2; - // CUOPT_LOG_TRACE("CPU: tabu nodec_until: %d\n", fj_cpu.h_tabu_nodec_until[var_idx]); - } else { - fj_cpu.h_tabu_lastdec[var_idx] = fj_cpu.iterations; - fj_cpu.h_tabu_noinc_until[var_idx] = fj_cpu.iterations + tabu_tenure; - fj_cpu.h_tabu_nodec_until[var_idx] = fj_cpu.iterations + tabu_tenure / 2; - // CUOPT_LOG_TRACE("CPU: tabu noinc_until: %d\n", fj_cpu.h_tabu_noinc_until[var_idx]); - } - - std::fill(fj_cpu.flip_move_computed.begin(), fj_cpu.flip_move_computed.end(), false); - std::fill(fj_cpu.var_bitmap.begin(), fj_cpu.var_bitmap.end(), false); - fj_cpu.iter_mtm_vars.clear(); -} - -template -static thrust::tuple find_mtm_move( - fj_cpu_climber_t& fj_cpu, const std::vector& target_cstrs, bool localmin = false) -{ - CPUFJ_NVTX_RANGE("CPUFJ::find_mtm_move"); - - raft::random::PCGenerator rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); - - fj_move_t best_move = fj_move_t{-1, 0}; - fj_staged_score_t best_score = fj_staged_score_t::invalid(); - - // collect all the variables that are involved in the target constraints - for (size_t cstr_idx : target_cstrs) { - auto [offset_begin, offset_end] = range_for_constraint(fj_cpu, cstr_idx); - for (auto i = offset_begin; i < offset_end; i++) { - i_t var_idx = fj_cpu.h_variables[i]; - if (fj_cpu.var_bitmap[var_idx]) continue; - fj_cpu.iter_mtm_vars.push_back(var_idx); - fj_cpu.var_bitmap[var_idx] = true; - } - } - // estimate the amount of nnzs to consider - i_t nnz_sum = 0; - for (auto var_idx : fj_cpu.iter_mtm_vars) { - auto [offset_begin, offset_end] = reverse_range_for_var(fj_cpu, var_idx); - nnz_sum += offset_end - offset_begin; - } - - f_t nnz_pick_probability = 1; - if (nnz_sum > fj_cpu.nnz_samples) nnz_pick_probability = (f_t)fj_cpu.nnz_samples / nnz_sum; - - for (size_t cstr_idx : target_cstrs) { - auto c_lb = fj_cpu.h_cstr_lb[cstr_idx]; - auto c_ub = fj_cpu.h_cstr_ub[cstr_idx]; - f_t cstr_tol = fj_cpu.view.get_corrected_tolerance(cstr_idx, c_lb, c_ub); - - cuopt_assert(cstr_idx < fj_cpu.h_cstr_lb.size(), "cstr_idx is out of bounds"); - auto [offset_begin, offset_end] = range_for_constraint(fj_cpu, cstr_idx); - for (auto i = offset_begin; i < offset_end; i++) { - // early cached check - if (auto& cached_move = fj_cpu.cached_mtm_moves[i]; cached_move.first != 0) { - if (best_score < cached_move.second) { - auto var_idx = fj_cpu.h_variables[i]; - if (check_variable_within_bounds( - fj_cpu, var_idx, fj_cpu.h_assignment[var_idx] + cached_move.first)) { - best_score = cached_move.second; - best_move = fj_move_t{var_idx, cached_move.first}; - } - // cuopt_assert(fj_cpu.view.pb.check_variable_within_bounds(var_idx, - // fj_cpu.h_assignment[var_idx] + cached_move.first), "best move is not within bounds"); - } - fj_cpu.hit_count++; - continue; - } - - // random chance to skip this nnz if there are many to consider - if (nnz_pick_probability < 1) - if (rng.next_float() > nnz_pick_probability) continue; - - auto var_idx = fj_cpu.h_variables[i]; - - f_t val = fj_cpu.h_assignment[var_idx]; - f_t new_val = val; - f_t delta = 0; - - // Special case for binary variables - if (fj_cpu.h_is_binary_variable[var_idx]) { - if (fj_cpu.flip_move_computed[var_idx]) continue; - fj_cpu.flip_move_computed[var_idx] = true; - new_val = 1 - val; - } else { - auto cstr_coeff = fj_cpu.h_coefficients[i]; - - f_t c_lb = fj_cpu.h_cstr_lb[cstr_idx]; - f_t c_ub = fj_cpu.h_cstr_ub[cstr_idx]; - auto [delta, sign, slack, cstr_tolerance] = - get_mtm_for_constraint(fj_cpu.view, - var_idx, - cstr_idx, - cstr_coeff, - c_lb, - c_ub, - fj_cpu.h_assignment, - fj_cpu.h_lhs); - if (is_integer_var(fj_cpu, var_idx)) { - new_val = cstr_coeff * sign > 0 - ? floor(val + delta + fj_cpu.view.pb.tolerances.integrality_tolerance) - : ceil(val + delta - fj_cpu.view.pb.tolerances.integrality_tolerance); - } else { - new_val = val + delta; - } - // fallback - if (new_val < get_lower(fj_cpu.h_var_bounds[var_idx].get()) || - new_val > get_upper(fj_cpu.h_var_bounds[var_idx].get())) { - new_val = cstr_coeff * sign > 0 ? get_lower(fj_cpu.h_var_bounds[var_idx].get()) - : get_upper(fj_cpu.h_var_bounds[var_idx].get()); - } - } - if (!isfinite(new_val)) continue; - cuopt_assert(check_variable_within_bounds(fj_cpu, var_idx, new_val), - "new_val is not within bounds"); - delta = new_val - val; - // more permissive tabu in the case of local minima - if (tabu_check(fj_cpu, var_idx, delta, localmin)) continue; - if (fabs(delta) < cstr_tol) continue; - - auto move = fj_move_t{var_idx, delta}; - cuopt_assert(move.var_idx < fj_cpu.h_assignment.size(), "move.var_idx is out of bounds"); - cuopt_assert(move.var_idx >= 0, "move.var_idx is not positive"); - - auto [score, infeasibility] = compute_score(fj_cpu, var_idx, delta); - fj_cpu.cached_mtm_moves[i] = std::make_pair(delta, score); - fj_cpu.miss_count++; - // reject this move if it would increase the target variable to a numerically unstable value - if (fj_cpu.view.move_numerically_stable( - val, new_val, infeasibility, fj_cpu.total_violations)) { - if (best_score < score) { - best_score = score; - best_move = move; - } - } - } - } - - // also consider BM moves if we have found a feasible solution at least once - if (move_type == MTMMoveType::FJ_MTM_VIOLATED && - fj_cpu.h_best_objective < std::numeric_limits::infinity() && - fj_cpu.h_incumbent_objective >= - fj_cpu.h_best_objective + fj_cpu.settings.parameters.breakthrough_move_epsilon) { - for (auto var_idx : fj_cpu.h_objective_vars) { - f_t old_val = fj_cpu.h_assignment[var_idx]; - f_t new_val = get_breakthrough_move(fj_cpu.view, var_idx); - - if (fj_cpu.view.pb.integer_equal(new_val, old_val) || !isfinite(new_val)) continue; - - f_t delta = new_val - old_val; - - // Check if we already have a move for this variable - auto move = fj_move_t{var_idx, delta}; - cuopt_assert(move.var_idx < fj_cpu.h_assignment.size(), "move.var_idx is out of bounds"); - cuopt_assert(move.var_idx >= 0, "move.var_idx is not positive"); - - if (tabu_check(fj_cpu, var_idx, delta)) continue; - - auto [score, infeasibility] = compute_score(fj_cpu, var_idx, delta); - - cuopt_assert(check_variable_within_bounds(fj_cpu, var_idx, new_val), ""); - cuopt_assert(isfinite(delta), ""); - - if (fj_cpu.view.move_numerically_stable( - old_val, new_val, infeasibility, fj_cpu.total_violations)) { - if (best_score < score) { - best_score = score; - best_move = move; - } - } - } - } - - return thrust::make_tuple(best_move, best_score); -} - -template -static thrust::tuple find_mtm_move_viol( - fj_cpu_climber_t& fj_cpu, i_t sample_size = 100, bool localmin = false) -{ - timing_raii_t timer(fj_cpu.find_mtm_move_viol_times); - CPUFJ_NVTX_RANGE("CPUFJ::find_mtm_move_viol"); - - std::vector sampled_cstrs; - sampled_cstrs.reserve(sample_size); - std::sample(fj_cpu.violated_constraints.begin(), - fj_cpu.violated_constraints.end(), - std::back_inserter(sampled_cstrs), - sample_size, - fj_cpu.rng); - - return find_mtm_move(fj_cpu, sampled_cstrs, localmin); -} - -template -static thrust::tuple find_mtm_move_sat( - fj_cpu_climber_t& fj_cpu, i_t sample_size = 100) -{ - timing_raii_t timer(fj_cpu.find_mtm_move_sat_times); - CPUFJ_NVTX_RANGE("CPUFJ::find_mtm_move_sat"); - - std::vector sampled_cstrs; - sampled_cstrs.reserve(sample_size); - std::sample(fj_cpu.satisfied_constraints.begin(), - fj_cpu.satisfied_constraints.end(), - std::back_inserter(sampled_cstrs), - sample_size, - fj_cpu.rng); - - return find_mtm_move(fj_cpu, sampled_cstrs); -} - -template -static void recompute_lhs(fj_cpu_climber_t& fj_cpu) -{ - CPUFJ_NVTX_RANGE("CPUFJ::recompute_lhs"); - cuopt_assert(fj_cpu.h_lhs.size() == fj_cpu.view.pb.n_constraints, "h_lhs size mismatch"); - - // clamp to var bounds - defensive; apply_move should already have clamped appropriately - for (i_t var_idx = 0; var_idx < fj_cpu.view.pb.n_variables; ++var_idx) { - fj_cpu.h_assignment[var_idx] = std::min( - std::max(fj_cpu.h_assignment[var_idx].get(), get_lower(fj_cpu.h_var_bounds[var_idx].get())), - get_upper(fj_cpu.h_var_bounds[var_idx].get())); - } - - fj_cpu.violated_constraints.clear(); - fj_cpu.satisfied_constraints.clear(); - fj_cpu.total_violations = 0; - for (i_t cstr_idx = 0; cstr_idx < fj_cpu.view.pb.n_constraints; ++cstr_idx) { - auto [offset_begin, offset_end] = range_for_constraint(fj_cpu, cstr_idx); - auto c_lb = fj_cpu.h_cstr_lb[cstr_idx]; - auto c_ub = fj_cpu.h_cstr_ub[cstr_idx]; - auto delta_it = - thrust::make_transform_iterator(thrust::make_counting_iterator(0), [&fj_cpu](i_t j) { - return fj_cpu.h_coefficients[j] * fj_cpu.h_assignment[fj_cpu.h_variables[j]]; - }); - fj_cpu.h_lhs[cstr_idx] = - fj_kahan_babushka_neumaier_sum(delta_it + offset_begin, delta_it + offset_end); - fj_cpu.h_lhs_sumcomp[cstr_idx] = 0; - - f_t cstr_tolerance = fj_cpu.view.get_corrected_tolerance(cstr_idx, c_lb, c_ub); - f_t new_cost = fj_cpu.view.excess_score(cstr_idx, fj_cpu.h_lhs[cstr_idx]); - if (new_cost < -cstr_tolerance) { - fj_cpu.violated_constraints.insert(cstr_idx); - fj_cpu.total_violations += new_cost; - } else { - fj_cpu.satisfied_constraints.insert(cstr_idx); - } - } - - // compute incumbent objective - fj_cpu.h_incumbent_objective = thrust::inner_product( - fj_cpu.h_assignment.begin(), fj_cpu.h_assignment.end(), fj_cpu.h_obj_coeffs.begin(), 0.); -} - -template -static thrust::tuple find_lift_move( - fj_cpu_climber_t& fj_cpu) -{ - timing_raii_t timer(fj_cpu.find_lift_move_times); - CPUFJ_NVTX_RANGE("CPUFJ::find_lift_move"); - - fj_move_t best_move = fj_move_t{-1, 0}; - fj_staged_score_t best_score = fj_staged_score_t::zero(); - - for (auto var_idx : fj_cpu.h_objective_vars) { - cuopt_assert(var_idx < fj_cpu.h_obj_coeffs.size(), "var_idx is out of bounds"); - cuopt_assert(var_idx >= 0, "var_idx is out of bounds"); - - f_t obj_coeff = fj_cpu.h_obj_coeffs[var_idx]; - f_t delta = -std::numeric_limits::infinity(); - f_t val = fj_cpu.h_assignment[var_idx]; - - // special path for binary variables - if (fj_cpu.h_is_binary_variable[var_idx]) { - cuopt_assert(fj_cpu.view.pb.is_integer(val), "binary variable is not integer"); - cuopt_assert(fj_cpu.view.pb.integer_equal(val, 0) || fj_cpu.view.pb.integer_equal(val, 1), - "Current assignment is not binary!"); - delta = round(1.0 - 2 * val); - // flip move wouldn't improve - if (delta * obj_coeff >= 0) continue; - } else { - f_t lfd_lb = get_lower(fj_cpu.h_var_bounds[var_idx].get()) - val; - f_t lfd_ub = get_upper(fj_cpu.h_var_bounds[var_idx].get()) - val; - auto [offset_begin, offset_end] = reverse_range_for_var(fj_cpu, var_idx); - for (i_t j = offset_begin; j < offset_end; j += 1) { - auto cstr_idx = fj_cpu.h_reverse_constraints[j]; - auto cstr_coeff = fj_cpu.h_reverse_coefficients[j]; - f_t c_lb = fj_cpu.h_cstr_lb[cstr_idx]; - f_t c_ub = fj_cpu.h_cstr_ub[cstr_idx]; - f_t cstr_tolerance = fj_cpu.view.get_corrected_tolerance(cstr_idx, c_lb, c_ub); - cuopt_assert(c_lb <= c_ub, "invalid bounds"); - cuopt_assert(fj_cpu.view.cstr_satisfied(cstr_idx, fj_cpu.h_lhs[cstr_idx]), - "cstr should be satisfied"); - - // Process each bound separately, as both are satified and may both be finite - // otherwise range constraints aren't correctly handled - for (auto [bound, sign] : {std::make_tuple(c_lb, -1), std::make_tuple(c_ub, 1)}) { - auto [delta, slack] = get_mtm_for_bound(fj_cpu.view, - var_idx, - cstr_idx, - cstr_coeff, - bound, - sign, - fj_cpu.h_assignment, - fj_cpu.h_lhs); - - if (cstr_coeff * sign < 0) { - if (is_integer_var(fj_cpu, var_idx)) delta = ceil(delta); - } else { - if (is_integer_var(fj_cpu, var_idx)) delta = floor(delta); - } - - // skip this variable if there is no slack - if (fabs(slack) <= cstr_tolerance) { - if (cstr_coeff * sign > 0) { - lfd_ub = 0; - } else { - lfd_lb = 0; - } - } else if (!check_variable_within_bounds(fj_cpu, var_idx, val + delta)) { - continue; - } else { - if (cstr_coeff * sign < 0) { - lfd_lb = std::max(lfd_lb, delta); - } else { - lfd_ub = std::min(lfd_ub, delta); - } - } - } - if (lfd_lb >= lfd_ub) break; - } - - // invalid crossing bounds - if (lfd_lb >= lfd_ub) { lfd_lb = lfd_ub = 0; } - - if (!check_variable_within_bounds(fj_cpu, var_idx, val + lfd_lb)) { lfd_lb = 0; } - if (!check_variable_within_bounds(fj_cpu, var_idx, val + lfd_ub)) { lfd_ub = 0; } - - // Now that the lift move domain is computed, compute the correct lift move - cuopt_assert(isfinite(val), "invalid assignment value"); - delta = obj_coeff < 0 ? lfd_ub : lfd_lb; - } - - if (!isfinite(delta)) delta = 0; - if (fj_cpu.view.pb.integer_equal(delta, (f_t)0)) continue; - if (tabu_check(fj_cpu, var_idx, delta)) continue; - - cuopt_assert(delta * obj_coeff < 0, "lift move doesn't improve the objective!"); - - // get the score - auto move = fj_move_t{var_idx, delta}; - fj_staged_score_t score = fj_staged_score_t::zero(); - f_t obj_score = -1 * obj_coeff * delta; // negated to turn this into a positive score - score.base = round(obj_score); - - if (best_score < score) { - best_score = score; - best_move = move; - } - } - - return thrust::make_tuple(best_move, best_score); -} - -template -static void perturb(fj_cpu_climber_t& fj_cpu) -{ - CPUFJ_NVTX_RANGE("CPUFJ::perturb"); - // select N variables, assign them a random value between their bounds - std::vector sampled_vars; - std::sample(fj_cpu.h_objective_vars.begin(), - fj_cpu.h_objective_vars.end(), - std::back_inserter(sampled_vars), - 2, - fj_cpu.rng); - raft::random::PCGenerator rng(fj_cpu.settings.seed + fj_cpu.iterations, 0, 0); - - for (auto var_idx : sampled_vars) { - f_t lb = std::max(get_lower(fj_cpu.h_var_bounds[var_idx].get()), -1e7); - f_t ub = std::min(get_upper(fj_cpu.h_var_bounds[var_idx].get()), 1e7); - f_t val = lb + (ub - lb) * rng.next_double(); - if (is_integer_var(fj_cpu, var_idx)) { - lb = std::ceil(lb); - ub = std::floor(ub); - val = std::round(val); - val = std::min(std::max(val, lb), ub); - } - - cuopt_assert(check_variable_within_bounds(fj_cpu, var_idx, val), - "value is out of bounds"); - fj_cpu.h_assignment[var_idx] = val; - } - - recompute_lhs(fj_cpu); -} - -template -static void init_fj_cpu(fj_cpu_climber_t& fj_cpu, - solution_t& solution, - const std::vector& left_weights, - const std::vector& right_weights, - f_t objective_weight, - const probing_cache_t* probing_cache) -{ - auto& problem = *solution.problem_ptr; - auto handle_ptr = solution.handle_ptr; - - auto sol_copy = solution; - clamp_within_var_bounds(sol_copy.assignment, &problem, handle_ptr); - - // build a cpu-based fj_view_t - fj_cpu.view = typename fj_t::climber_data_t::view_t{}; - fj_cpu.view.pb = problem.view(); - fj_cpu.pb_ptr = &problem; - // Get host copies of device data - fj_cpu.h_reverse_coefficients = - cuopt::host_copy(problem.reverse_coefficients, handle_ptr->get_stream()); - fj_cpu.h_reverse_constraints = - cuopt::host_copy(problem.reverse_constraints, handle_ptr->get_stream()); - fj_cpu.h_reverse_offsets = cuopt::host_copy(problem.reverse_offsets, handle_ptr->get_stream()); - fj_cpu.h_coefficients = cuopt::host_copy(problem.coefficients, handle_ptr->get_stream()); - fj_cpu.h_offsets = cuopt::host_copy(problem.offsets, handle_ptr->get_stream()); - fj_cpu.h_variables = cuopt::host_copy(problem.variables, handle_ptr->get_stream()); - fj_cpu.h_obj_coeffs = cuopt::host_copy(problem.objective_coefficients, handle_ptr->get_stream()); - fj_cpu.h_var_bounds = cuopt::host_copy(problem.variable_bounds, handle_ptr->get_stream()); - fj_cpu.h_cstr_lb = cuopt::host_copy(problem.constraint_lower_bounds, handle_ptr->get_stream()); - fj_cpu.h_cstr_ub = cuopt::host_copy(problem.constraint_upper_bounds, handle_ptr->get_stream()); - fj_cpu.h_var_types = cuopt::host_copy(problem.variable_types, handle_ptr->get_stream()); - fj_cpu.h_is_binary_variable = - cuopt::host_copy(problem.is_binary_variable, handle_ptr->get_stream()); - fj_cpu.h_binary_indices = cuopt::host_copy(problem.binary_indices, handle_ptr->get_stream()); - fj_cpu.h_related_variables = - cuopt::host_copy(problem.related_variables, handle_ptr->get_stream()); - fj_cpu.h_related_variables_offsets = - cuopt::host_copy(problem.related_variables_offsets, handle_ptr->get_stream()); - fj_cpu.probing_cache = probing_cache; - fj_cpu.h_original_ids = problem.original_ids; - fj_cpu.h_reverse_original_ids = problem.reverse_original_ids; - - fj_cpu.h_cstr_left_weights = left_weights; - fj_cpu.h_cstr_right_weights = right_weights; - fj_cpu.max_weight = 1.0; - fj_cpu.h_objective_weight = objective_weight; - auto h_assignment = sol_copy.get_host_assignment(); - fj_cpu.h_assignment = h_assignment; - fj_cpu.h_best_assignment = std::move(h_assignment); - fj_cpu.h_lhs.resize(fj_cpu.pb_ptr->n_constraints); - fj_cpu.h_lhs_sumcomp.resize(fj_cpu.pb_ptr->n_constraints, 0); - fj_cpu.h_tabu_nodec_until.resize(fj_cpu.pb_ptr->n_variables, 0); - fj_cpu.h_tabu_noinc_until.resize(fj_cpu.pb_ptr->n_variables, 0); - fj_cpu.h_tabu_lastdec.resize(fj_cpu.pb_ptr->n_variables, 0); - fj_cpu.h_tabu_lastinc.resize(fj_cpu.pb_ptr->n_variables, 0); - fj_cpu.iterations = 0; - - finalize_fj_cpu_host_initialization(fj_cpu, - problem.n_variables, - problem.n_constraints, - problem.n_integer_vars, - problem.nnz, - problem.tolerances); -} - -template -static void set_host_data_view( - fj_cpu_climber_t& fj_cpu, - i_t n_variables, - i_t n_constraints, - i_t n_integer_vars, - i_t nnz, - const typename mip_solver_settings_t::tolerances_t& tolerances) -{ - fj_cpu.view.pb.tolerances = tolerances; - fj_cpu.view.pb.n_variables = n_variables; - fj_cpu.view.pb.n_integer_vars = n_integer_vars; - fj_cpu.view.pb.n_constraints = n_constraints; - fj_cpu.view.pb.nnz = nnz; - - fj_cpu.view.pb.constraint_lower_bounds = - raft::device_span(fj_cpu.h_cstr_lb.data(), fj_cpu.h_cstr_lb.size()); - fj_cpu.view.pb.constraint_upper_bounds = - raft::device_span(fj_cpu.h_cstr_ub.data(), fj_cpu.h_cstr_ub.size()); - fj_cpu.view.pb.variable_bounds = raft::device_span::type>( - fj_cpu.h_var_bounds.data(), fj_cpu.h_var_bounds.size()); - fj_cpu.view.pb.variable_types = - raft::device_span(fj_cpu.h_var_types.data(), fj_cpu.h_var_types.size()); - fj_cpu.view.pb.is_binary_variable = - raft::device_span(fj_cpu.h_is_binary_variable.data(), fj_cpu.h_is_binary_variable.size()); - fj_cpu.view.pb.binary_indices = - raft::device_span(fj_cpu.h_binary_indices.data(), fj_cpu.h_binary_indices.size()); - fj_cpu.view.pb.coefficients = - raft::device_span(fj_cpu.h_coefficients.data(), fj_cpu.h_coefficients.size()); - fj_cpu.view.pb.offsets = raft::device_span(fj_cpu.h_offsets.data(), fj_cpu.h_offsets.size()); - fj_cpu.view.pb.variables = - raft::device_span(fj_cpu.h_variables.data(), fj_cpu.h_variables.size()); - fj_cpu.view.pb.reverse_coefficients = raft::device_span( - fj_cpu.h_reverse_coefficients.data(), fj_cpu.h_reverse_coefficients.size()); - fj_cpu.view.pb.reverse_constraints = raft::device_span(fj_cpu.h_reverse_constraints.data(), - fj_cpu.h_reverse_constraints.size()); - fj_cpu.view.pb.reverse_offsets = - raft::device_span(fj_cpu.h_reverse_offsets.data(), fj_cpu.h_reverse_offsets.size()); - fj_cpu.view.pb.objective_coefficients = - raft::device_span(fj_cpu.h_obj_coeffs.data(), fj_cpu.h_obj_coeffs.size()); - - // Spans over the arrays wired above, so the model reads the same memory under one name. A - // host-LP climber carries no presolve scaling, which is what the identity default stands for. - auto model = std::make_shared>(); - model->offsets = raft::device_span(fj_cpu.h_offsets.data(), fj_cpu.h_offsets.size()); - model->variables = raft::device_span(fj_cpu.h_variables.data(), fj_cpu.h_variables.size()); - model->coefficients = - raft::device_span(fj_cpu.h_coefficients.data(), fj_cpu.h_coefficients.size()); - model->reverse_offsets = - raft::device_span(fj_cpu.h_reverse_offsets.data(), fj_cpu.h_reverse_offsets.size()); - model->reverse_constraints = raft::device_span(fj_cpu.h_reverse_constraints.data(), - fj_cpu.h_reverse_constraints.size()); - model->cstr_lb = raft::device_span(fj_cpu.h_cstr_lb.data(), fj_cpu.h_cstr_lb.size()); - model->cstr_ub = raft::device_span(fj_cpu.h_cstr_ub.data(), fj_cpu.h_cstr_ub.size()); - model->h_obj_coeffs = - raft::device_span(fj_cpu.h_obj_coeffs.data(), fj_cpu.h_obj_coeffs.size()); - model->h_var_types = - raft::device_span(fj_cpu.h_var_types.data(), fj_cpu.h_var_types.size()); - model->h_original_ids = - raft::device_span(fj_cpu.h_original_ids.data(), fj_cpu.h_original_ids.size()); - model->h_reverse_original_ids = raft::device_span(fj_cpu.h_reverse_original_ids.data(), - fj_cpu.h_reverse_original_ids.size()); - model->h_related_variables = - raft::device_span(fj_cpu.h_related_variables.data(), fj_cpu.h_related_variables.size()); - model->h_related_variables_offsets = raft::device_span( - fj_cpu.h_related_variables_offsets.data(), fj_cpu.h_related_variables_offsets.size()); - model->n_variables = n_variables; - model->n_constraints = n_constraints; - model->tolerances = tolerances; - model->probing_cache = fj_cpu.probing_cache; - if (fj_cpu.pb_ptr != nullptr) { - model->objective_scaling_factor = fj_cpu.pb_ptr->presolve_data.objective_scaling_factor; - model->objective_offset = fj_cpu.pb_ptr->presolve_data.objective_offset; - } - fj_cpu.problem = std::move(model); -} - -template -void finalize_fj_cpu_host_initialization( - fj_cpu_climber_t& fj_cpu, - i_t n_variables, - i_t n_constraints, - i_t n_integer_vars, - i_t nnz, - const typename mip_solver_settings_t::tolerances_t& tolerances) -{ - raft::common::nvtx::range scope("finalize_fj_cpu_host_initialization"); - - cuopt_assert(n_variables >= 0, "invalid variable count"); - cuopt_assert(n_constraints >= 0, "invalid constraint count"); - cuopt_assert(fj_cpu.h_offsets.size() == static_cast(n_constraints + 1), - "invalid CSR offsets"); - cuopt_assert(fj_cpu.h_reverse_offsets.size() == static_cast(n_variables + 1), - "invalid reverse offsets"); - cuopt_assert(fj_cpu.h_assignment.size() == static_cast(n_variables), - "seed assignment size mismatch"); - - set_host_data_view(fj_cpu, n_variables, n_constraints, n_integer_vars, nnz, tolerances); - - fj_cpu.view.cstr_left_weights = - raft::device_span(fj_cpu.h_cstr_left_weights.data(), fj_cpu.h_cstr_left_weights.size()); - fj_cpu.view.cstr_right_weights = - raft::device_span(fj_cpu.h_cstr_right_weights.data(), fj_cpu.h_cstr_right_weights.size()); - fj_cpu.view.objective_weight = &fj_cpu.h_objective_weight; - fj_cpu.view.incumbent_assignment = - raft::device_span(fj_cpu.h_assignment.data(), fj_cpu.h_assignment.size()); - fj_cpu.view.incumbent_lhs = raft::device_span(fj_cpu.h_lhs.data(), fj_cpu.h_lhs.size()); - fj_cpu.view.incumbent_lhs_sumcomp = - raft::device_span(fj_cpu.h_lhs_sumcomp.data(), fj_cpu.h_lhs_sumcomp.size()); - fj_cpu.view.tabu_nodec_until = - raft::device_span(fj_cpu.h_tabu_nodec_until.data(), fj_cpu.h_tabu_nodec_until.size()); - fj_cpu.view.tabu_noinc_until = - raft::device_span(fj_cpu.h_tabu_noinc_until.data(), fj_cpu.h_tabu_noinc_until.size()); - fj_cpu.view.tabu_lastdec = - raft::device_span(fj_cpu.h_tabu_lastdec.data(), fj_cpu.h_tabu_lastdec.size()); - fj_cpu.view.tabu_lastinc = - raft::device_span(fj_cpu.h_tabu_lastinc.data(), fj_cpu.h_tabu_lastinc.size()); - fj_cpu.view.incumbent_objective = &fj_cpu.h_incumbent_objective; - fj_cpu.view.best_objective = &fj_cpu.h_best_objective; - fj_cpu.view.settings = &fj_cpu.settings; - - fj_cpu.h_objective_vars.resize(n_variables); - auto end = std::copy_if( - thrust::counting_iterator(0), - thrust::counting_iterator(n_variables), - fj_cpu.h_objective_vars.begin(), - [&fj_cpu](i_t idx) { return !fj_cpu.view.pb.integer_equal(fj_cpu.h_obj_coeffs[idx], (f_t)0); }); - fj_cpu.h_objective_vars.resize(end - fj_cpu.h_objective_vars.begin()); - fj_cpu.view.objective_vars = - raft::device_span(fj_cpu.h_objective_vars.data(), fj_cpu.h_objective_vars.size()); - - fj_cpu.h_best_objective = +std::numeric_limits::infinity(); - - // nnz count - fj_cpu.cached_mtm_moves.resize(fj_cpu.h_coefficients.size(), - std::make_pair(0, fj_staged_score_t::zero())); - - fj_cpu.cached_cstr_bounds.resize(fj_cpu.h_reverse_coefficients.size()); - for (i_t var_idx = 0; var_idx < n_variables; ++var_idx) { - auto [offset_begin, offset_end] = reverse_range_for_var(fj_cpu, var_idx); - for (i_t i = offset_begin; i < offset_end; ++i) { - fj_cpu.cached_cstr_bounds[i] = - std::make_pair(fj_cpu.h_cstr_lb[fj_cpu.h_reverse_constraints[i]], - fj_cpu.h_cstr_ub[fj_cpu.h_reverse_constraints[i]]); - } - } - - // precompute the binvars-pre-row tables for 2opt - fj_cpu.h_binrow_offsets.resize(n_constraints + 1); - fj_cpu.h_binrow_vars.clear(); - for (i_t cstr_idx = 0; cstr_idx < n_constraints; ++cstr_idx) { - fj_cpu.h_binrow_offsets[cstr_idx] = fj_cpu.h_binrow_vars.size(); - auto [offset_begin, offset_end] = range_for_constraint(fj_cpu, cstr_idx); - for (i_t i = offset_begin; i < offset_end; ++i) { - const i_t var_idx = fj_cpu.h_variables[i]; - if (fj_cpu.h_is_binary_variable[var_idx]) { fj_cpu.h_binrow_vars.push_back(var_idx); } - } - } - fj_cpu.h_binrow_offsets[n_constraints] = fj_cpu.h_binrow_vars.size(); - - fj_cpu.flip_move_computed.resize(n_variables, false); - fj_cpu.var_bitmap.resize(n_variables, false); - fj_cpu.iter_mtm_vars.reserve(n_variables); - - recompute_lhs(fj_cpu); - - // Precompute static problem features for regression model - precompute_problem_features(fj_cpu); -} - -template -static std::unique_ptr> init_fj_cpu_from_host_lp( - const lp_problem_t& problem, - const std::vector& variable_types, - const std::vector& seed_assignment, - const simplex_solver_settings_t& settings, - std::atomic& preemption_flag, - int64_t seed) -{ - using f_t2 = typename type_2::type; - - cuopt_assert(variable_types.size() >= static_cast(problem.num_cols), - "variable type size mismatch"); - - typename mip_solver_settings_t::tolerances_t tolerances{}; - tolerances.absolute_tolerance = settings.primal_tol; - tolerances.relative_tolerance = settings.zero_tol; - tolerances.integrality_tolerance = settings.integer_tol; - tolerances.absolute_mip_gap = settings.absolute_mip_gap_tol; - tolerances.relative_mip_gap = settings.relative_mip_gap_tol; - - const i_t n_variables = problem.num_cols; - const i_t n_constraints = problem.num_rows; - - csr_matrix_t csr_A(problem.num_rows, problem.num_cols, problem.A.nnz()); - problem.A.to_compressed_row(csr_A); - std::vector coefficients = csr_A.x; - std::vector variables = csr_A.j; - std::vector offsets = csr_A.row_start; - std::vector constraint_lower_bounds = problem.rhs; - std::vector constraint_upper_bounds = problem.rhs; - std::vector variable_bounds(n_variables); - std::vector cpufj_variable_types(n_variables); - std::vector is_binary_variable(n_variables, 0); - i_t n_integer_vars = 0; - - for (i_t j = 0; j < n_variables; ++j) { - variable_bounds[j] = f_t2{problem.lower[j], problem.upper[j]}; - const auto var_type = variable_types[j]; - cpufj_variable_types[j] = - var_type == variable_type_t::CONTINUOUS ? var_t::CONTINUOUS : var_t::INTEGER; - - const bool is_integer = cpufj_variable_types[j] == var_t::INTEGER; - const bool is_binary = is_integer && - integer_equal(problem.lower[j], f_t{0}, settings.integer_tol) && - integer_equal(problem.upper[j], f_t{1}, settings.integer_tol); - if (is_integer) { ++n_integer_vars; } - if (is_binary) { is_binary_variable[j] = 1; } - } - - const i_t nnz = static_cast(variables.size()); - csc_matrix_t reverse_csc(n_constraints, n_variables, nnz); - csr_A.to_compressed_col(reverse_csc); - std::vector reverse_coefficients = std::move(reverse_csc.x); - std::vector reverse_constraints = std::move(reverse_csc.i); - std::vector reverse_offsets = std::move(reverse_csc.col_start); - - std::vector projected_seed(n_variables, f_t{0}); - for (i_t j = 0; j < n_variables; ++j) { - f_t value = j < static_cast(seed_assignment.size()) ? seed_assignment[j] : f_t{0}; - value = std::clamp(value, problem.lower[j], problem.upper[j]); - if (variable_types[j] != variable_type_t::CONTINUOUS) { - value = std::clamp(std::round(value), problem.lower[j], problem.upper[j]); - } - projected_seed[j] = value; - } - - fj_settings_t fj_settings; - fj_settings.mode = fj_mode_t::EXIT_NON_IMPROVING; - fj_settings.n_of_minimums_for_exit = std::numeric_limits::max(); - fj_settings.time_limit = std::numeric_limits::infinity(); - fj_settings.iteration_limit = std::numeric_limits::max(); - fj_settings.update_weights = true; - fj_settings.feasibility_run = false; - fj_settings.seed = seed >= 0 ? seed : cuopt::seed_generator::get_seed(); - - auto fj_cpu = std::make_unique>(preemption_flag); - fj_cpu->view = typename fj_t::climber_data_t::view_t{}; - fj_cpu->pb_ptr = nullptr; - fj_cpu->settings = fj_settings; - - fj_cpu->h_reverse_coefficients = std::move(reverse_coefficients); - fj_cpu->h_reverse_constraints = std::move(reverse_constraints); - fj_cpu->h_reverse_offsets = std::move(reverse_offsets); - fj_cpu->h_coefficients = std::move(coefficients); - fj_cpu->h_offsets = std::move(offsets); - fj_cpu->h_variables = std::move(variables); - fj_cpu->h_obj_coeffs = problem.objective; - fj_cpu->h_var_bounds = std::move(variable_bounds); - fj_cpu->h_cstr_lb = std::move(constraint_lower_bounds); - fj_cpu->h_cstr_ub = std::move(constraint_upper_bounds); - fj_cpu->h_var_types = std::move(cpufj_variable_types); - fj_cpu->h_is_binary_variable = std::move(is_binary_variable); - - fj_cpu->h_cstr_left_weights.resize(n_constraints, 1.0); - fj_cpu->h_cstr_right_weights.resize(n_constraints, 1.0); - fj_cpu->max_weight = 1.0; - fj_cpu->h_objective_weight = 0.0; - fj_cpu->h_assignment = projected_seed; - fj_cpu->h_best_assignment = std::move(projected_seed); - fj_cpu->h_lhs.resize(n_constraints); - fj_cpu->h_lhs_sumcomp.resize(n_constraints, 0); - fj_cpu->h_tabu_nodec_until.resize(n_variables, 0); - fj_cpu->h_tabu_noinc_until.resize(n_variables, 0); - fj_cpu->h_tabu_lastdec.resize(n_variables, 0); - fj_cpu->h_tabu_lastinc.resize(n_variables, 0); - fj_cpu->iterations = 0; - - finalize_fj_cpu_host_initialization( - *fj_cpu, n_variables, n_constraints, n_integer_vars, nnz, tolerances); - return fj_cpu; -} - -template -static void sanity_checks(fj_cpu_climber_t& fj_cpu) -{ - // Check that each variable is within its bounds - for (i_t var_idx = 0; var_idx < fj_cpu.view.pb.n_variables; ++var_idx) { - f_t val = fj_cpu.h_assignment[var_idx]; - cuopt_assert(fj_cpu.view.pb.check_variable_within_bounds(var_idx, val), - "Variable is out of bounds"); - } - - // Check that each violated constraint is actually violated and not present in - // satisfied_constraints - for (const auto& cstr_idx : fj_cpu.violated_constraints) { - cuopt_assert(fj_cpu.satisfied_constraints.count(cstr_idx) == 0, - "Violated constraint also in satisfied_constraints"); - f_t lhs = fj_cpu.h_lhs[cstr_idx]; - f_t tol = fj_cpu.view.get_corrected_tolerance(cstr_idx); - f_t excess = fj_cpu.view.excess_score(cstr_idx, lhs); - cuopt_assert(excess < -tol, "Constraint in violated_constraints is not actually violated"); - } - - // Check that each satisfied constraint is actually satisfied and not present in - // violated_constraints - for (const auto& cstr_idx : fj_cpu.satisfied_constraints) { - cuopt_assert(fj_cpu.violated_constraints.count(cstr_idx) == 0, - "Satisfied constraint also in violated_constraints"); - f_t lhs = fj_cpu.h_lhs[cstr_idx]; - f_t tol = fj_cpu.view.get_corrected_tolerance(cstr_idx); - f_t excess = fj_cpu.view.excess_score(cstr_idx, lhs); - cuopt_assert(!(excess < -tol), "Constraint in satisfied_constraints is actually violated"); - } - - // Check that each constraint is in exactly one of violated_constraints or satisfied_constraints - for (i_t cstr_idx = 0; cstr_idx < fj_cpu.view.pb.n_constraints; ++cstr_idx) { - bool in_viol = fj_cpu.violated_constraints.count(cstr_idx) > 0; - bool in_sat = fj_cpu.satisfied_constraints.count(cstr_idx) > 0; - cuopt_assert( - in_viol != in_sat, - "Constraint must be in exactly one of violated_constraints or satisfied_constraints"); - - cuopt_assert(fj_cpu.h_cstr_left_weights[cstr_idx] >= 0, "Weights should be positive or zero"); - cuopt_assert(fj_cpu.h_cstr_right_weights[cstr_idx] >= 0, "Weights should be positive or zero"); - } - cuopt_assert(fj_cpu.h_objective_weight >= 0, "Objective weight should be positive or zero"); -} - -template -std::unique_ptr> fj_t::create_cpu_climber( - solution_t& solution, - const std::vector& left_weights, - const std::vector& right_weights, - f_t objective_weight, - std::atomic& preemption_flag, - const probing_cache_t* probing_cache, - fj_settings_t settings, - bool randomize_params) -{ - raft::common::nvtx::range scope("fj_cpu_init"); - - auto fj_cpu = std::make_unique>(preemption_flag); - - // Initialize fj_cpu with all the data - init_fj_cpu(*fj_cpu, solution, left_weights, right_weights, objective_weight, probing_cache); - fj_cpu->settings = settings; - if (randomize_params) { - auto host_rng = std::mt19937(rng.next_i64()); - fj_cpu->mtm_viol_samples = std::uniform_int_distribution(15, 50)(host_rng); - fj_cpu->mtm_sat_samples = std::uniform_int_distribution(10, 30)(host_rng); - fj_cpu->nnz_samples = std::uniform_int_distribution(2000, 15000)(host_rng); - fj_cpu->perturb_interval = std::uniform_int_distribution(50, 500)(host_rng); - } - fj_cpu->settings.seed = rng.next_i64(); - return fj_cpu; // move -} - -template -void cpufj_solve(fj_cpu_climber_t* fj_cpu, f_t in_time_limit, double work_unit_limit) -{ - // A model whose columns are all binary and whose rows carry int8/int16 coefficients is searched - // by the specialized engine instead. It reports through the same callbacks and declines rather - // than approximating, so the general path below still covers everything else. - if (try_cpufj_binary_solve(*fj_cpu, in_time_limit, work_unit_limit)) return; - - i_t local_mins = 0; - auto loop_start = std::chrono::high_resolution_clock::now(); - const bool bounded_time = std::isfinite((double)in_time_limit); - const auto time_limit = bounded_time - ? std::chrono::milliseconds((int64_t)std::floor(in_time_limit * 1000.0)) - : std::chrono::milliseconds::zero(); - auto loop_time_start = std::chrono::high_resolution_clock::now(); - - fj_cpu->rng.seed(fj_cpu->settings.seed); - - // Initialize feature tracking - fj_cpu->last_feature_log_time = loop_start; - fj_cpu->prev_best_objective = fj_cpu->h_best_objective; - fj_cpu->iterations_since_best = 0; - - while (!fj_cpu->halted && !fj_cpu->preemption_flag.load()) { - // Check if 5 seconds have passed - auto now = std::chrono::high_resolution_clock::now(); - if (bounded_time && now - loop_time_start > time_limit) { - CUOPT_LOG_TRACE("%sTime limit of %.4f seconds reached, breaking loop at iteration %d", - fj_cpu->log_prefix.c_str(), - time_limit.count() / 1000.f, - fj_cpu->iterations); - break; - } - if (fj_cpu->iterations >= fj_cpu->settings.iteration_limit) { - CUOPT_LOG_TRACE("%sIteration limit of %d reached, breaking loop at iteration %d", - fj_cpu->log_prefix.c_str(), - fj_cpu->settings.iteration_limit, - fj_cpu->iterations); - break; - } - - // periodically recompute the LHS and violation scores - // to correct any accumulated numerical errors - cuopt_assert(fj_cpu->settings.parameters.lhs_refresh_period > 0, - "lhs_refresh_period should be positive"); - if (fj_cpu->iterations % fj_cpu->settings.parameters.lhs_refresh_period == 0 || - fj_cpu->trigger_early_lhs_recomputation) { - recompute_lhs(*fj_cpu); - fj_cpu->trigger_early_lhs_recomputation = false; - } - - fj_move_t move = fj_move_t{-1, 0}; - fj_staged_score_t score = fj_staged_score_t::invalid(); - bool is_lift = false; - bool is_mtm_viol = false; - bool is_mtm_sat = false; - - // Perform lift moves - if (fj_cpu->violated_constraints.empty()) { - thrust::tie(move, score) = find_lift_move(*fj_cpu); - if (score > fj_staged_score_t::zero()) is_lift = true; - } - // Regular MTM - if (!(score > fj_staged_score_t::zero())) { - thrust::tie(move, score) = find_mtm_move_viol(*fj_cpu, fj_cpu->mtm_viol_samples); - if (score > fj_staged_score_t::zero()) is_mtm_viol = true; - } - // try with MTM in satisfied constraints - if (fj_cpu->feasible_found && !(score > fj_staged_score_t::zero())) { - thrust::tie(move, score) = find_mtm_move_sat(*fj_cpu, fj_cpu->mtm_sat_samples); - if (score > fj_staged_score_t::zero()) is_mtm_sat = true; - } - // if we're in the feasible region but haven't found improvements in the last n iterations, - // perturb - bool should_perturb = false; - if (fj_cpu->violated_constraints.empty() && - fj_cpu->iterations - fj_cpu->last_feasible_entrance_iter > fj_cpu->perturb_interval) { - should_perturb = true; - fj_cpu->last_feasible_entrance_iter = fj_cpu->iterations; - } - - if (score > fj_staged_score_t::zero() && !should_perturb) { - apply_move(*fj_cpu, move.var_idx, move.value, false); - // Track move types - if (is_lift) fj_cpu->n_lift_moves_window++; - if (is_mtm_viol) fj_cpu->n_mtm_viol_moves_window++; - if (is_mtm_sat) fj_cpu->n_mtm_sat_moves_window++; - } else { - // Local Min - update_weights(*fj_cpu); - if (should_perturb) { - perturb(*fj_cpu); - for (size_t i = 0; i < fj_cpu->cached_mtm_moves.size(); i++) - fj_cpu->cached_mtm_moves[i].first = 0; - } - - two_opt_move_t two_opt_move; - if (!should_perturb) two_opt_move = find_two_opt_move(*fj_cpu); - if (two_opt_move.score > fj_staged_score_t::zero()) { - apply_move(*fj_cpu, two_opt_move.first.var_idx, two_opt_move.first.value, true); - apply_move(*fj_cpu, two_opt_move.second.var_idx, two_opt_move.second.value, true); - fj_cpu->n_mtm_viol_moves_window += 2; - } else { - thrust::tie(move, score) = - find_mtm_move_viol(*fj_cpu, 1, true); // pick a single random violated constraint - i_t var_idx = move.var_idx >= 0 ? move.var_idx : 0; - f_t delta = move.var_idx >= 0 ? move.value : 0; - apply_move(*fj_cpu, var_idx, delta, true); - } - ++local_mins; - ++fj_cpu->n_local_minima_window; - } - - // number of violated constraints is usually small (<100). recomputing from all LHSs is cheap - // and more numerically precise than just adding to the accumulator in apply_move - fj_cpu->total_violations = 0; - for (auto cstr_idx : fj_cpu->violated_constraints) { - fj_cpu->total_violations += fj_cpu->view.excess_score(cstr_idx, fj_cpu->h_lhs[cstr_idx]); - } - if (fj_cpu->iterations % fj_cpu->log_interval == 0) { - CUOPT_LOG_DEBUG( - "%sCPUFJ iteration: %d/%d, local mins: %d, best_objective: %g, viol: %zu, obj weight %g, " - "maxw %g", - fj_cpu->log_prefix.c_str(), - fj_cpu->iterations, - fj_cpu->settings.iteration_limit != std::numeric_limits::max() - ? fj_cpu->settings.iteration_limit - : -1, - local_mins, - fj_cpu->h_best_objective, - fj_cpu->violated_constraints.size(), - fj_cpu->h_objective_weight, - fj_cpu->max_weight); - } - // send current solution to callback every 3000 steps for diversity - if (fj_cpu->iterations % fj_cpu->diversity_callback_interval == 0) { - if (fj_cpu->diversity_callback) { - fj_cpu->diversity_callback(fj_cpu->h_incumbent_objective, fj_cpu->h_assignment); - } - } - - // Print timing statistics every N iterations -#if CPUFJ_TIMING_TRACE - if (fj_cpu->iterations % fj_cpu->timing_stats_interval == 0 && fj_cpu->iterations > 0) { - print_timing_stats(*fj_cpu); - } -#endif - - if (fj_cpu->iterations % 100 == 0 && fj_cpu->iterations > 0) { - // Use cumulative byte counts (collect() without flush). Each window's contribution to - // work_units_elapsed therefore grows roughly with the running total of bytes touched, - // i.e. quadratically in iterations rather than linearly. This is intentional: the - // memory_aggregator is calibrated for medium/large MIPs, and a strictly-linear scheme - // forces tiny instances (few KB per iteration) to run for tens of seconds before the - // accumulated bytes cross a 0.5 horizon, causing the deterministic producer_sync to - // stall and B&B to time out on instances that should solve in milliseconds. The - // accumulation is still deterministic across runs of the same problem, which is what - // the producer_sync contract actually requires. - auto [loads, stores] = fj_cpu->memory_aggregator.collect(); - double biased_work = (loads + stores) * fj_cpu->work_unit_bias / 1e10; - fj_cpu->work_units_elapsed += biased_work; - - if (fj_cpu->producer_sync != nullptr) { fj_cpu->producer_sync->notify_progress(); } - if (fj_cpu->work_units_elapsed >= work_unit_limit) { break; } - } - - cuopt_func_call(sanity_checks(*fj_cpu)); - fj_cpu->iterations++; - fj_cpu->iterations_since_best++; - } - auto loop_end = std::chrono::high_resolution_clock::now(); - double total_time = - std::chrono::duration_cast>(loop_end - loop_start).count(); - double avg_time_per_iter = fj_cpu->iterations > 0 ? total_time / fj_cpu->iterations : 0; - CUOPT_LOG_TRACE("%sCPUFJ Average time per iteration: %.8fms", - fj_cpu->log_prefix.c_str(), - avg_time_per_iter * 1000.0); - -#if CPUFJ_TIMING_TRACE - // Print final timing statistics - CUOPT_LOG_TRACE("=== Final Timing Statistics ==="); - print_timing_stats(*fj_cpu); -#endif -} - -template -std::unique_ptr> init_fj_cpu_standalone( - problem_t& problem, - solution_t& solution, - std::atomic& preemption_flag, - uint64_t seed, - fj_settings_t settings) -{ - raft::common::nvtx::range scope("init_fj_cpu_standalone"); - - auto fj_cpu = std::make_unique>(preemption_flag); - - std::vector default_weights(problem.n_constraints, 1.0); - // Early CPUFJ runs while presolve is still probing, so there are no implications to hand it - const probing_cache_t* no_implications = nullptr; - init_fj_cpu(*fj_cpu, solution, default_weights, default_weights, 0.0, no_implications); - fj_cpu->settings = settings; - fj_cpu->settings.seed = seed; - - return fj_cpu; -} - -template -void fj_cpu_worker_t::fj_cpu_deleter_t::operator()(fj_cpu_climber_t* ptr) const -{ - delete ptr; -} - -template -void fj_cpu_worker_t::create_worker( - const lp_problem_t& problem, - const std::vector& variable_types, - const std::vector& seed_assignment, - const simplex_solver_settings_t& settings, - std::string log_prefix, - int64_t seed) -{ - auto new_climber = init_fj_cpu_from_host_lp( - problem, variable_types, seed_assignment, settings, preemption_flag, seed); - fj_cpu.reset(new_climber.release()); - fj_cpu->log_prefix = std::move(log_prefix); - fj_cpu->improvement_callback = improvement_callback; - fj_cpu->halted = false; - preemption_flag = false; - is_initialized = true; -} - -template -void fj_cpu_worker_t::run_async(f_t time_limit, double work_unit_limit) -{ - if (!is_initialized) return; - - auto& fj_ptr = fj_cpu; -#pragma omp task shared(fj_cpu, is_initialized, fj_ptr) firstprivate(time_limit, work_unit_limit) \ - priority(CUOPT_DEFAULT_TASK_PRIORITY) default(none) depend(out : fj_ptr) - { - if (is_initialized) { cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); } - } -} - -template -void fj_cpu_worker_t::run_sync(f_t time_limit, double work_unit_limit) -{ - if (!is_initialized) return; - cpufj_solve(fj_cpu.get(), time_limit, work_unit_limit); - is_initialized = false; - fj_cpu.reset(); -} - -template -void fj_cpu_worker_t::stop() -{ - if (!is_initialized) return; - - preemption_flag = true; - - auto& fj_ptr = fj_cpu; -#pragma omp taskwait depend(in : fj_ptr) - is_initialized = false; - fj_cpu.reset(); -} - -template -void fj_cpu_worker_t::send_stop_signal() -{ - preemption_flag = true; -} - -#if MIP_INSTANTIATE_FLOAT -template class fj_t; -template struct fj_cpu_worker_t; -template void cpufj_solve(fj_cpu_climber_t* fj_cpu, - float in_time_limit, - double work_unit_limit); -template std::unique_ptr> init_fj_cpu_standalone( - problem_t& problem, - solution_t& solution, - std::atomic& preemption_flag, - uint64_t seed, - fj_settings_t settings); -template void finalize_fj_cpu_host_initialization( - fj_cpu_climber_t& fj_cpu, - int n_variables, - int n_constraints, - int n_integer_vars, - int nnz, - const typename mip_solver_settings_t::tolerances_t& tolerances); -#endif - -#if MIP_INSTANTIATE_DOUBLE -template class fj_t; -template struct fj_cpu_worker_t; -template void cpufj_solve(fj_cpu_climber_t* fj_cpu, - double in_time_limit, - double work_unit_limit); -template std::unique_ptr> init_fj_cpu_standalone( - problem_t& problem, - solution_t& solution, - std::atomic& preemption_flag, - uint64_t seed, - fj_settings_t settings); -template void finalize_fj_cpu_host_initialization( - fj_cpu_climber_t& fj_cpu, - int n_variables, - int n_constraints, - int n_integer_vars, - int nnz, - const typename mip_solver_settings_t::tolerances_t& tolerances); -#endif - -} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh index b9efd193d1..b803191642 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh @@ -1,341 +1,28 @@ -/* clang-format off */ /* * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ -/* clang-format on */ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace cuopt::mathematical_optimization::mip { - -// Best feasible assignment found by any lane of one portfolio. Lanes run concurrently, so which -// lane observes which incumbent depends on scheduling: a portfolio that shares is not run-to-run -// reproducible. Null until a caller opts a set of climbers into sharing. -template -struct fj_cpu_shared_incumbent_t { - // True when the candidate beat the shared best, in which case it was stored. - bool publish(f_t candidate_objective, - f_t candidate_user_objective, - const std::vector& candidate) - { - // Unlocked reject first: the publish sites are hot on instances that improve in tiny steps. - if (!(candidate_objective < objective.load(std::memory_order_relaxed))) return false; - std::lock_guard lock(guard); - if (!(candidate_objective < objective.load(std::memory_order_relaxed))) return false; - assignment = candidate; - objective.store(candidate_objective, std::memory_order_relaxed); - CUOPT_LOG_DEBUG("New portfolio best found: %.17g:", candidate_user_objective); - return true; - } - - // True when the shared best beat local_objective, in which case it was copied into destination. - bool adopt(f_t local_objective, std::vector& destination, f_t* adopted_objective = nullptr) - { - if (!(objective.load(std::memory_order_relaxed) < local_objective)) return false; - std::lock_guard lock(guard); - const f_t shared_objective = objective.load(std::memory_order_relaxed); - if (!(shared_objective < local_objective)) return false; - cuopt_assert(assignment.size() == destination.size(), "shared incumbent size mismatch"); - destination = assignment; - if (adopted_objective != nullptr) *adopted_objective = shared_objective; - return true; - } - - std::mutex guard; - std::vector assignment; - std::atomic objective{std::numeric_limits::infinity()}; -}; - -// The model as given, in the original column space, addressed by non-owning spans over the -// climber's host arrays. Written once during construction and read-only from then on. -template -struct fj_cpu_problem_t { - raft::device_span offsets; - raft::device_span variables; - raft::device_span coefficients; - raft::device_span reverse_offsets; - raft::device_span reverse_constraints; - raft::device_span cstr_lb; - raft::device_span cstr_ub; - raft::device_span h_obj_coeffs; - raft::device_span h_var_types; - raft::device_span h_original_ids; - raft::device_span h_reverse_original_ids; - raft::device_span h_related_variables; - raft::device_span h_related_variables_offsets; - - i_t n_variables{0}; - i_t n_constraints{0}; - f_t objective_scaling_factor{1}; - f_t objective_offset{0}; - typename mip_solver_settings_t::tolerances_t tolerances; - const probing_cache_t* probing_cache{nullptr}; - - bool integer_equal(f_t a, f_t b) const - { - return std::abs(a - b) <= tolerances.integrality_tolerance; - } -}; +// Stable external include. CPU implementation details and state ownership live below cpu/. +#include +namespace cuopt::mathematical_optimization { template -class probing_cache_t; - -// NOTE: this seems an easy pick for reflection/xmacros once this is available (C++26?) -// Maintaining a single source of truth for all members would be nice -template -struct fj_cpu_climber_t { - fj_cpu_climber_t(std::atomic& preemption_flag) : preemption_flag(preemption_flag) - { -#define ADD_INSTRUMENTED(var) \ - std::make_pair(#var, std::ref(static_cast(var))) - - // Initialize memory aggregator with all ins_vector members - memory_aggregator = instrumentation_aggregator_t{ADD_INSTRUMENTED(h_reverse_coefficients), - ADD_INSTRUMENTED(h_reverse_constraints), - ADD_INSTRUMENTED(h_reverse_offsets), - ADD_INSTRUMENTED(h_coefficients), - ADD_INSTRUMENTED(h_offsets), - ADD_INSTRUMENTED(h_variables), - ADD_INSTRUMENTED(h_obj_coeffs), - ADD_INSTRUMENTED(h_var_bounds), - ADD_INSTRUMENTED(h_cstr_lb), - ADD_INSTRUMENTED(h_cstr_ub), - ADD_INSTRUMENTED(h_var_types), - ADD_INSTRUMENTED(h_is_binary_variable), - ADD_INSTRUMENTED(h_objective_vars), - ADD_INSTRUMENTED(h_binary_indices), - ADD_INSTRUMENTED(h_related_variables), - ADD_INSTRUMENTED(h_related_variables_offsets), - ADD_INSTRUMENTED(h_binrow_offsets), - ADD_INSTRUMENTED(h_binrow_vars), - ADD_INSTRUMENTED(h_original_ids), - ADD_INSTRUMENTED(h_reverse_original_ids), - ADD_INSTRUMENTED(h_tabu_nodec_until), - ADD_INSTRUMENTED(h_tabu_noinc_until), - ADD_INSTRUMENTED(h_tabu_lastdec), - ADD_INSTRUMENTED(h_tabu_lastinc), - ADD_INSTRUMENTED(h_lhs), - ADD_INSTRUMENTED(h_lhs_sumcomp), - ADD_INSTRUMENTED(h_cstr_left_weights), - ADD_INSTRUMENTED(h_cstr_right_weights), - ADD_INSTRUMENTED(h_assignment), - ADD_INSTRUMENTED(h_best_assignment), - ADD_INSTRUMENTED(cached_cstr_bounds), - ADD_INSTRUMENTED(iter_mtm_vars)}; - -#undef ADD_INSTRUMENTED - } - fj_cpu_climber_t(const fj_cpu_climber_t& other) = delete; - fj_cpu_climber_t& operator=(const fj_cpu_climber_t& other) = delete; - - fj_cpu_climber_t(fj_cpu_climber_t&& other) = default; - fj_cpu_climber_t& operator=(fj_cpu_climber_t&& other) = default; - - problem_t* pb_ptr; - fj_settings_t settings; - std::mt19937 rng; - typename fj_t::climber_data_t::view_t view; - // Host copies of device data as struct members - ins_vector h_reverse_coefficients; - ins_vector h_reverse_constraints; - ins_vector h_reverse_offsets; - ins_vector h_coefficients; - ins_vector h_offsets; - ins_vector h_variables; - ins_vector h_obj_coeffs; - ins_vector::type> h_var_bounds; - ins_vector h_cstr_lb; - ins_vector h_cstr_ub; - ins_vector h_var_types; - ins_vector h_is_binary_variable; - ins_vector h_objective_vars; - ins_vector h_binary_indices; - ins_vector h_related_variables; - ins_vector h_related_variables_offsets; - - // precompute the binary variables per row for bin 2opt - ins_vector h_binrow_offsets; - ins_vector h_binrow_vars; - const probing_cache_t* probing_cache{nullptr}; - // Probing cache keys are pre-trivial-presolve variable ids; these translate to and from them - ins_vector h_original_ids; - ins_vector h_reverse_original_ids; - - ins_vector h_tabu_nodec_until; - ins_vector h_tabu_noinc_until; - ins_vector h_tabu_lastdec; - ins_vector h_tabu_lastinc; - - ins_vector h_lhs; - ins_vector h_lhs_sumcomp; - ins_vector h_cstr_left_weights; - ins_vector h_cstr_right_weights; - f_t max_weight; - ins_vector h_assignment; - ins_vector h_best_assignment; - f_t h_objective_weight; - f_t h_incumbent_objective; - f_t h_best_objective; - i_t last_feasible_entrance_iter{0}; - i_t iterations; - std::unordered_set violated_constraints; - std::unordered_set satisfied_constraints; - bool feasible_found{false}; - bool trigger_early_lhs_recomputation{false}; - f_t total_violations{0}; - - // Timing data structures - std::vector find_lift_move_times; - std::vector find_mtm_move_viol_times; - std::vector find_mtm_move_sat_times; - std::vector apply_move_times; - std::vector update_weights_times; - std::vector compute_score_times; +class optimization_problem_t; +} - i_t hit_count{0}; - i_t miss_count{0}; - - i_t candidate_move_hits[3] = {0}; - i_t candidate_move_misses[3] = {0}; - - // vector is actually likely beneficial here since we're memory bound - std::vector flip_move_computed; - - // CSR nnz offset -> (delta, score) - std::vector> cached_mtm_moves; - - // CSC (transposed!) nnz-offset-indexed constraint bounds (lb, ub) - // std::pair better compile down to 16 bytes!! GCC do your job! - ins_vector> cached_cstr_bounds; - - std::vector var_bitmap; - ins_vector iter_mtm_vars; - - // Scratch reused by the binary 2-opt search, which runs at every local minimum - std::vector two_opt_target_cstrs; - std::vector two_opt_first_vars; - std::vector> two_opt_partners; - std::vector> two_opt_row_deltas; - - i_t mtm_viol_samples{25}; - i_t mtm_sat_samples{15}; - i_t nnz_samples{50000}; - i_t perturb_interval{100}; - - i_t log_interval{1000}; - i_t diversity_callback_interval{3000}; - i_t timing_stats_interval{5000}; - - // Callback with work unit timestamp for deterministic mode - // Parameters: objective, solution, work_units - std::function&, double)> improvement_callback{nullptr}; - std::function&)> diversity_callback{nullptr}; - std::string log_prefix{""}; - - // Work unit tracking for deterministic synchronization - std::atomic work_units_elapsed{0.0}; - double work_unit_bias{1.5}; // Bias factor to keep CPUFJ ahead of B&B - producer_sync_t* producer_sync{nullptr}; // Optional sync utility for notifying progress - - std::atomic halted{false}; - - // Feature tracking for regression model (last 1000 iterations) - i_t nnz_processed_window{0}; - i_t n_lift_moves_window{0}; - i_t n_mtm_viol_moves_window{0}; - i_t n_mtm_sat_moves_window{0}; - i_t n_variable_updates_window{0}; - i_t n_local_minima_window{0}; - std::chrono::high_resolution_clock::time_point last_feature_log_time; - f_t prev_best_objective{std::numeric_limits::infinity()}; - i_t iterations_since_best{0}; - - // Cache and locality tracking - i_t hit_count_window_start{0}; - i_t miss_count_window_start{0}; - std::unordered_set unique_cstrs_accessed_window; - std::unordered_set unique_vars_accessed_window; - - // Precomputed static problem features - i_t n_binary_vars{0}; - i_t n_integer_vars{0}; - i_t max_var_degree{0}; - i_t max_cstr_degree{0}; - double avg_var_degree{0.0}; - double avg_cstr_degree{0.0}; - double var_degree_cv{0.0}; - double cstr_degree_cv{0.0}; - double problem_density{0.0}; - - // Shared across every lane and frozen before the first clone is created. - std::shared_ptr> problem; - - f_t get_user_objective(f_t solver_objective) const - { - cuopt_assert(std::isfinite(problem->objective_scaling_factor) && - problem->objective_scaling_factor != f_t{0}, - "invalid objective scaling factor"); - return problem->objective_scaling_factor * (solver_objective + problem->objective_offset); - } - - // Equality rows backed by private free continuous variables may be omitted by the binary engine - // and reconstructed before publication. - struct bin_eliminated_row_t { - i_t row; - f_t rhs; - std::vector positive, negative, all; - std::vector positive_coeff, negative_coeff; - }; - std::vector bin_eliminated_rows; - std::vector bin_ignore_row, bin_ignore_var; - bool has_bin_elimination{false}; - fj_bin_setup_times_t bin_setup; - - // Held with the other lanes of the same portfolio. Null when the climber runs alone, which is - // what keeps a solo climber reproducible. - std::shared_ptr> shared_incumbent; - - // Per-lane search policy. Uniform across lanes until a caller diversifies them. - bool low_latency{false}; - bool use_integer_bit_encoding{true}; - // Lower bound h_objective_weight decays to, so a lane seeded with objective pressure keeps it. - f_t seed_objective_weight{0}; - - // Memory instrumentation aggregator - instrumentation_aggregator_t memory_aggregator; - // TODO atomic ref? c++20 - std::atomic& preemption_flag; -}; +namespace cuopt::mathematical_optimization::mip { template -void cpufj_solve(fj_cpu_climber_t* fj_cpu, - f_t in_time_limit = std::numeric_limits::infinity(), - double work_unit_limit = std::numeric_limits::infinity()); +class problem_t; -// Standalone CPUFJ init for running without full fj_t infrastructure (avoids GPU allocations). -// Used for early CPUFJ during presolve. template -std::unique_ptr> init_fj_cpu_standalone( - problem_t& problem, - solution_t& solution, +std::unique_ptr> init_fj_cpu_from_optimization_problem( + const optimization_problem_t& problem, + const typename mip_solver_settings_t::tolerances_t& tolerances, std::atomic& preemption_flag, - uint64_t seed, fj_settings_t settings = fj_settings_t{}); } // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary.cu index a0ec2c509f..93581e55c8 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary.cu @@ -1006,7 +1006,7 @@ struct fj_bin_engine_t { apply_move(var, (int8_t)(1 - 2 * assign[var]), climber); } - if (iters % climber.log_interval == 0) { + if (climber.log_interval && iters % climber.log_interval == 0) { CUOPT_LOG_DEBUG("%sCPUFJ[bin%d] iteration: %d, viol: %zu, best: %g, maxw: %d", climber.log_prefix.c_str(), coefficient_bits(), @@ -1082,6 +1082,7 @@ bool try_cpufj_binary_solve(fj_cpu_climber_t& climber, climber.log_prefix.c_str(), engine8.pb.n_variables, engine8.pb.n_constraints); + climber.release_setup_structures(); engine8.solve(climber, time_limit, work_unit_limit); return true; } @@ -1090,6 +1091,7 @@ bool try_cpufj_binary_solve(fj_cpu_climber_t& climber, climber.log_prefix.c_str(), probe.pb.n_variables, probe.pb.n_constraints); + climber.release_setup_structures(); probe.solve(climber, time_limit, work_unit_limit); return true; } @@ -1110,6 +1112,7 @@ bool try_cpufj_binary_solve(fj_cpu_climber_t& climber, climber.log_prefix.c_str(), scan.coefficient_bits, scan.n_split_constraints); + climber.release_setup_structures(); engine.solve(climber, time_limit, work_unit_limit); return true; }; diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary_preprocess.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary_preprocess.cu index ffed26751a..c44a93d119 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary_preprocess.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_binary_preprocess.cu @@ -248,8 +248,8 @@ void fj_bin_narrow(const fj_cpu_climber_t& c, const auto& coeffs = c.problem->coefficients; const auto& cstr_lb = c.problem->cstr_lb; const auto& cstr_ub = c.problem->cstr_ub; - const auto& left_w = c.h_cstr_left_weights; - const auto& right_w = c.h_cstr_right_weights; + const auto& left_w = c.h_initial_left_weights; + const auto& right_w = c.h_initial_right_weights; const auto& obj = c.problem->h_obj_coeffs; // Explicit stamps rather than scoped timers, so narrow and transpose can be timed separately. @@ -396,8 +396,8 @@ bool fj_bin_encode(const fj_cpu_climber_t& c, const auto& coeffs = c.problem->coefficients; const auto& cstr_lb = c.problem->cstr_lb; const auto& cstr_ub = c.problem->cstr_ub; - const auto& left_w = c.h_cstr_left_weights; - const auto& right_w = c.h_cstr_right_weights; + const auto& left_w = c.h_initial_left_weights; + const auto& right_w = c.h_initial_right_weights; const auto& obj = c.problem->h_obj_coeffs; std::vector lower(n_cols); diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_bridge.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_bridge.cu new file mode 100644 index 0000000000..6570d268ae --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_bridge.cu @@ -0,0 +1,229 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include "feasibility_jump.cuh" + +#include "cpu/climber.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace cuopt::mathematical_optimization::mip { + +template +void init_fj_cpu_from_problem(fj_cpu_climber_t& fj_cpu, + problem_t& problem, + const raft::handle_t* handle_ptr, + std::vector start_assignment, + const std::vector& left_weights, + const std::vector& right_weights, + f_t objective_weight, + const probing_cache_t* probing_cache) +{ + auto problem_data = std::make_shared>(); + fj_cpu.problem = problem_data; + problem_data->tolerances = problem.tolerances; + problem_data->n_variables = problem.n_variables; + problem_data->n_constraints = problem.n_constraints; + problem_data->nnz = problem.nnz; + const auto problem_view = problem.view(); + problem_data->objective_scaling_factor = problem_view.objective_scaling_factor; + problem_data->objective_offset = problem_view.objective_offset; + + // Queue the device-to-host copies together and synchronize once before constructing host state. + auto stream = handle_ptr->get_stream(); + const double download_start = tic(); + problem_data->reverse_coefficients = cuopt::host_copy_async(problem.reverse_coefficients, stream); + problem_data->reverse_constraints = cuopt::host_copy_async(problem.reverse_constraints, stream); + problem_data->reverse_offsets = cuopt::host_copy_async(problem.reverse_offsets, stream); + problem_data->coefficients = cuopt::host_copy_async(problem.coefficients, stream); + problem_data->offsets = cuopt::host_copy_async(problem.offsets, stream); + problem_data->variables = cuopt::host_copy_async(problem.variables, stream); + problem_data->h_obj_coeffs = cuopt::host_copy_async(problem.objective_coefficients, stream); + fj_cpu.h_var_bounds = cuopt::host_copy_async(problem.variable_bounds, stream); + problem_data->cstr_lb = cuopt::host_copy_async(problem.constraint_lower_bounds, stream); + problem_data->cstr_ub = cuopt::host_copy_async(problem.constraint_upper_bounds, stream); + problem_data->h_var_types = cuopt::host_copy_async(problem.variable_types, stream); + fj_cpu.h_is_binary_variable = cuopt::host_copy_async(problem.is_binary_variable, stream); + fj_cpu.h_binary_indices = cuopt::host_copy_async(problem.binary_indices, stream); + problem_data->h_related_variables = cuopt::host_copy_async(problem.related_variables, stream); + problem_data->h_related_variables_offsets = + cuopt::host_copy_async(problem.related_variables_offsets, stream); + handle_ptr->sync_stream(); + CUOPT_LOG_DEBUG( + "CPUFJ model download from device: %.4fs for %d nnz", toc(download_start), problem.nnz); + + auto host_lp = std::make_shared>(handle_ptr); + problem.get_host_user_problem(*host_lp); + problem_data->host_lp = std::move(host_lp); + problem_data->probing_cache = probing_cache; + problem_data->h_original_ids = problem.original_ids; + problem_data->h_reverse_original_ids = problem.reverse_original_ids; + + fj_cpu.h_initial_left_weights = left_weights; + fj_cpu.h_initial_right_weights = right_weights; + fj_cpu.max_weight = f_t{1}; + fj_cpu.h_objective_weight = objective_weight; + if (start_assignment.empty()) { + start_assignment.resize(problem.n_variables); + for (i_t variable = 0; variable < problem.n_variables; ++variable) { + const auto bounds = fj_cpu.h_var_bounds[variable].get(); + f_t value = std::clamp(f_t{0}, get_lower(bounds), get_upper(bounds)); + if (fj_cpu.problem->h_var_types[variable] == var_t::INTEGER) { value = std::round(value); } + start_assignment[variable] = value; + } + } + cuopt_assert(start_assignment.size() == static_cast(problem.n_variables), + "start assignment must cover every variable"); + fj_cpu.h_assignment = start_assignment; + fj_cpu.h_best_assignment = std::move(start_assignment); + fj_cpu.h_lhs.resize(problem.n_constraints); + fj_cpu.h_lhs_sumcomp.resize(problem.n_constraints, f_t{0}); + fj_cpu.h_tabu_nodec_until.resize(problem.n_variables, 0); + fj_cpu.h_tabu_noinc_until.resize(problem.n_variables, 0); + fj_cpu.h_tabu_lastdec.resize(problem.n_variables, 0); + fj_cpu.h_tabu_lastinc.resize(problem.n_variables, 0); + fj_cpu.iterations = 0; + + finalize_fj_cpu_host_initialization(fj_cpu, + *problem_data, + problem.n_variables, + problem.n_constraints, + problem.n_integer_vars, + problem.nnz, + problem.tolerances); +} + +template +std::unique_ptr> init_fj_cpu_from_optimization_problem( + const optimization_problem_t& problem, + const typename mip_solver_settings_t::tolerances_t& tolerances, + std::atomic& preemption_flag, + fj_settings_t settings) +{ + raft::common::nvtx::range scope("init_fj_cpu_from_optimization_problem"); + auto stream = problem.get_handle_ptr()->get_stream(); + + const double download_start = tic(); + auto coefficients = cuopt::host_copy_async(problem.get_constraint_matrix_values(), stream); + auto variables = cuopt::host_copy_async(problem.get_constraint_matrix_indices(), stream); + auto offsets = cuopt::host_copy_async(problem.get_constraint_matrix_offsets(), stream); + auto objective_coefficients = + cuopt::host_copy_async(problem.get_objective_coefficients(), stream); + auto variable_lower_bounds = cuopt::host_copy_async(problem.get_variable_lower_bounds(), stream); + auto variable_upper_bounds = cuopt::host_copy_async(problem.get_variable_upper_bounds(), stream); + auto constraint_lower_bounds = + cuopt::host_copy_async(problem.get_constraint_lower_bounds(), stream); + auto constraint_upper_bounds = + cuopt::host_copy_async(problem.get_constraint_upper_bounds(), stream); + auto constraint_bounds = cuopt::host_copy_async(problem.get_constraint_bounds(), stream); + auto row_types = cuopt::host_copy_async(problem.get_row_types(), stream); + auto variable_types = cuopt::host_copy_async(problem.get_variable_types(), stream); + problem.get_handle_ptr()->sync_stream(); + CUOPT_LOG_DEBUG( + "CPUFJ model download from device: %.4fs for %d nnz", toc(download_start), problem.get_nnz()); + + return init_fj_cpu_from_host_model(problem.get_n_variables(), + problem.get_n_constraints(), + problem.get_nnz(), + problem.get_sense(), + problem.get_objective_scaling_factor(), + problem.get_objective_offset(), + std::move(coefficients), + std::move(variables), + std::move(offsets), + std::move(objective_coefficients), + std::move(variable_lower_bounds), + std::move(variable_upper_bounds), + std::move(constraint_lower_bounds), + std::move(constraint_upper_bounds), + std::move(constraint_bounds), + std::move(row_types), + std::move(variable_types), + tolerances, + preemption_flag, + settings); +} + +template +std::unique_ptr> fj_t::create_cpu_climber( + solution_t& solution, + const std::vector& left_weights, + const std::vector& right_weights, + f_t objective_weight, + std::atomic& preemption_flag, + const probing_cache_t* probing_cache, + fj_settings_t settings, + bool randomize_params) +{ + raft::common::nvtx::range scope("fj_cpu_init"); + + auto fj_cpu = std::make_unique>(preemption_flag); + auto sol_copy = solution; + clamp_within_var_bounds(sol_copy.assignment, solution.problem_ptr, solution.handle_ptr); + + init_fj_cpu_from_problem(*fj_cpu, + *solution.problem_ptr, + solution.handle_ptr, + sol_copy.get_host_assignment(), + left_weights, + right_weights, + objective_weight, + probing_cache); + fj_cpu->settings = settings; + if (randomize_params) { + cuopt::pcgenerator_t rng(cuopt::seed_generator::get_seed()); + fj_cpu->mtm_viol_samples = rng.uniform(15, 51); + fj_cpu->mtm_sat_samples = rng.uniform(10, 31); + fj_cpu->nnz_samples = rng.uniform(2000, 15001); + fj_cpu->perturb_interval = rng.uniform(50, 501); + } + return fj_cpu; +} + +#if MIP_INSTANTIATE_FLOAT +template std::unique_ptr> init_fj_cpu_from_optimization_problem( + const optimization_problem_t&, + const typename mip_solver_settings_t::tolerances_t&, + std::atomic&, + fj_settings_t); +template std::unique_ptr> fj_t::create_cpu_climber( + solution_t&, + const std::vector&, + const std::vector&, + float, + std::atomic&, + const probing_cache_t*, + fj_settings_t, + bool); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template std::unique_ptr> init_fj_cpu_from_optimization_problem( + const optimization_problem_t&, + const typename mip_solver_settings_t::tolerances_t&, + std::atomic&, + fj_settings_t); +template std::unique_ptr> fj_t::create_cpu_climber( + solution_t&, + const std::vector&, + const std::vector&, + double, + std::atomic&, + const probing_cache_t*, + fj_settings_t, + bool); +#endif + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh index bb2c69f81c..521df10b8b 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -38,13 +38,16 @@ struct fj_cpu_worker_t { ~fj_cpu_worker_t() { stop(); } + // `n_structural` is where `problem`'s slack block starts; those columns fold into two-sided row + // bounds, so the climber and the assignment it reports span only the ones below. -1 keeps them. // `seed` selects the FJ RNG seed: pass a non-negative value for a deterministic seed, // or -1 to draw from the global cuopt::seed_generator (the historical behavior). // In deterministic mode the caller MUST pass an explicit seed, otherwise the underlying // seed_generator::get_seed() racing with concurrent callers breaks reproducibility. void create_worker(const simplex::lp_problem_t& problem, const std::vector& variable_types, - const std::vector& seed_assignment, + i_t n_structural, + const std::vector& start_assignment, const simplex::simplex_solver_settings_t& settings, std::string log_prefix, int64_t seed = -1); diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_types.hpp b/cpp/src/mip_heuristics/feasibility_jump/fj_types.hpp new file mode 100644 index 0000000000..f3ef2e7d8c --- /dev/null +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_types.hpp @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace cuopt::mathematical_optimization::mip { + +struct fj_hyper_parameters_t { + int max_sampled_moves = 32 * 16; + double random_var_probability = 0.04; + double random_cstr_probability = 0.16; + int global_move_update_period = 10; + int heavy_move_update_period = 50; + int sync_period = 200; + int lhs_refresh_period = 500; + int allow_infeasibility_iterations = 200; + double objective_weight_increment = 0.01; + int load_balancing_variable_threshold = 300; + int load_balancing_constraint_threshold = 5000; + int load_balancing_variable_split_size = 50; + + double breakthrough_move_epsilon = 1e-4; + int tabu_tenure_min = 3; + int tabu_tenure_max = 13; + double excess_improvement_weight = (1.0 / 2.0); + double weight_smoothing_probability = 0.0003; + + double fractional_score_multiplier = 100; + double rounding_second_stage_split = 0.1; + + double small_move_tabu_threshold = 1e-6; + int small_move_tabu_tenure = 4; + + int two_opt_max_rows = 4; + int two_opt_max_row_vars = 256; + int two_opt_max_pairs = 256; + + int old_codepath_total_var_to_relvar_ratio_threshold = 200; + int load_balancing_codepath_min_varcount = 3200; +}; + +enum class fj_mode_t { FIRST_FEASIBLE, GREEDY_DESCENT, TREE, ROUNDING, EXIT_NON_IMPROVING }; + +enum class MTMMoveType { FJ_MTM_VIOLATED, FJ_MTM_SATISFIED, FJ_MTM_ALL }; + +enum class fj_load_balancing_mode_t { ALWAYS_ON, AUTO, ALWAYS_OFF }; + +enum class fj_candidate_selection_t { WEIGHTED_SCORE, FEASIBLE_FIRST }; + +struct fj_settings_t { + int seed{0}; + fj_mode_t mode{fj_mode_t::FIRST_FEASIBLE}; + fj_candidate_selection_t candidate_selection{fj_candidate_selection_t::WEIGHTED_SCORE}; + double time_limit{60.0}; + int iteration_limit{std::numeric_limits::max()}; + fj_hyper_parameters_t parameters{}; + int n_of_minimums_for_exit = 7000; + double infeasibility_weight = 1.0; + bool update_weights = true; + bool feasibility_run = true; + fj_load_balancing_mode_t load_balancing_mode{fj_load_balancing_mode_t::AUTO}; + double baseline_objective_for_longer_run{std::numeric_limits::lowest()}; +}; + +struct fj_move_t { + int var_idx; + double value; + + bool operator<(const fj_move_t& rhs) const + { + if (var_idx == rhs.var_idx) return value < rhs.value; + return var_idx < rhs.var_idx; + } + bool operator==(const fj_move_t& rhs) const + { + return var_idx == rhs.var_idx && value == rhs.value; + } + bool operator!=(const fj_move_t& rhs) const { return !(*this == rhs); } +}; + +struct fj_staged_score_t { + float base{-std::numeric_limits::infinity()}; + float bonus{-std::numeric_limits::infinity()}; + +#if defined(__CUDACC__) +#define CUOPT_FJ_HOST_DEVICE inline __host__ __device__ +#else +#define CUOPT_FJ_HOST_DEVICE inline +#endif + + CUOPT_FJ_HOST_DEVICE bool operator<(fj_staged_score_t other) const noexcept + { + return base == other.base ? bonus < other.bonus : base < other.base; + } + CUOPT_FJ_HOST_DEVICE bool operator>(fj_staged_score_t other) const noexcept + { + return base == other.base ? bonus > other.bonus : base > other.base; + } + CUOPT_FJ_HOST_DEVICE bool operator==(fj_staged_score_t other) const noexcept + { + return base == other.base && bonus == other.bonus; + } + CUOPT_FJ_HOST_DEVICE bool operator!=(fj_staged_score_t other) const noexcept + { + return !(*this == other); + } + + CUOPT_FJ_HOST_DEVICE static fj_staged_score_t invalid() + { + return {-std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; + } + CUOPT_FJ_HOST_DEVICE static fj_staged_score_t zero() { return {0, 0}; } + + CUOPT_FJ_HOST_DEVICE bool valid() const { return *this != invalid(); } + +#undef CUOPT_FJ_HOST_DEVICE +}; + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/presolve/probing_cache.cuh b/cpp/src/mip_heuristics/presolve/probing_cache.cuh index 94d8af98ca..2d3b968023 100644 --- a/cpp/src/mip_heuristics/presolve/probing_cache.cuh +++ b/cpp/src/mip_heuristics/presolve/probing_cache.cuh @@ -8,6 +8,7 @@ #pragma once #include "bounds_presolve.cuh" +#include "probing_cache.hpp" #include @@ -36,110 +37,6 @@ class bound_presolve_t; rounding. To save from memory, we will keep the the results in host map. */ -enum interval_type_t { EQUALS = 0, LEQ, GEQ }; - -template -struct val_interval_t { - void fill_cache_hits(i_t interval, - f_t first_probe, - f_t second_probe, - i_t& hit_interval_for_first_probe, - i_t& hit_interval_for_second_probe) const - { - if (interval_type == interval_type_t::EQUALS) { - if (val == first_probe) { hit_interval_for_first_probe = interval; } - if (val == second_probe) { hit_interval_for_second_probe = interval; } - } else if (interval_type == interval_type_t::LEQ) { - if (val >= first_probe) { hit_interval_for_first_probe = interval; } - if (val >= second_probe) { hit_interval_for_second_probe = interval; } - } else if (interval_type == interval_type_t::GEQ) { - if (val <= first_probe) { hit_interval_for_first_probe = interval; } - if (val <= second_probe) { hit_interval_for_second_probe = interval; } - } - } - f_t val; - interval_type_t interval_type; -}; - -template -struct cached_bound_t { - f_t lb; - f_t ub; -}; - -template -struct cache_entry_t { - val_interval_t val_interval; - std::unordered_map> var_to_cached_bound_map; -}; - -// A forcing read off an exactly projected block: var == value implies forced_var == forced_value. -template -struct probe_forcing_t { - i_t var; - i_t forced_var; - bool value; - bool forced_value; -}; - -template -struct probe_findings_t { - std::vector> forcings; - std::vector> fixings; // var forced to value by its block alone -}; - -template -class probing_cache_t { - public: - bool contains(problem_t& problem, i_t var_id); - void update_bounds_with_selected(std::vector& host_lb, - std::vector& host_ub, - const cache_entry_t& cache_entry, - const std::vector& reverse_original_ids); - i_t check_number_of_conflicting_vars(const std::vector& host_lb, - const std::vector& host_ub, - const cache_entry_t& cache_entry, - f_t integrality_tolerance, - const std::vector& reverse_original_ids); - // check if there are any conflicting bounds - f_t get_least_conflicting_rounding(problem_t& problem, - std::vector& host_lb, - std::vector& host_ub, - i_t var_id_on_problem, - f_t first_probe, - f_t second_probe, - f_t integrality_tolerance); - void merge_forcings(const std::vector>& forcings, - std::vector>& fixings); - // 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; -}; - -template -class lb_probing_cache_t { - public: - bool contains(problem_t& problem, i_t var_id); - void update_bounds_with_selected(std::vector& host_bounds, - const cache_entry_t& cache_entry, - const std::vector& reverse_original_ids); - i_t check_number_of_conflicting_vars(const std::vector& host_bounds, - const cache_entry_t& cache_entry, - f_t integrality_tolerance, - const std::vector& reverse_original_ids); - // check if there are any conflicting bounds - f_t get_least_conflicting_rounding(problem_t& problem, - std::vector& host_bounds, - i_t var_id_on_problem, - f_t first_probe, - f_t second_probe, - f_t integrality_tolerance); - - std::unordered_map, 2>> probing_cache; -}; - template presolve_features_t probing_presolve_features(problem_t const& problem) { diff --git a/cpp/src/mip_heuristics/presolve/probing_cache.hpp b/cpp/src/mip_heuristics/presolve/probing_cache.hpp new file mode 100644 index 0000000000..19f8376930 --- /dev/null +++ b/cpp/src/mip_heuristics/presolve/probing_cache.hpp @@ -0,0 +1,121 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace cuopt::mathematical_optimization::mip { + +template +class problem_t; + +enum interval_type_t { EQUALS = 0, LEQ, GEQ }; + +template +struct val_interval_t { + void fill_cache_hits(i_t interval, + f_t first_probe, + f_t second_probe, + i_t& hit_interval_for_first_probe, + i_t& hit_interval_for_second_probe) const + { + if (interval_type == interval_type_t::EQUALS) { + if (val == first_probe) { hit_interval_for_first_probe = interval; } + if (val == second_probe) { hit_interval_for_second_probe = interval; } + } else if (interval_type == interval_type_t::LEQ) { + if (val >= first_probe) { hit_interval_for_first_probe = interval; } + if (val >= second_probe) { hit_interval_for_second_probe = interval; } + } else if (interval_type == interval_type_t::GEQ) { + if (val <= first_probe) { hit_interval_for_first_probe = interval; } + if (val <= second_probe) { hit_interval_for_second_probe = interval; } + } + } + + f_t val; + interval_type_t interval_type; +}; + +template +struct cached_bound_t { + f_t lb; + f_t ub; +}; + +template +struct cache_entry_t { + val_interval_t val_interval; + std::unordered_map> var_to_cached_bound_map; +}; + +template +struct probe_forcing_t { + i_t var; + i_t forced_var; + bool value; + bool forced_value; +}; + +template +struct probe_findings_t { + std::vector> forcings; + std::vector> fixings; +}; + +template +class probing_cache_t { + public: + bool contains(problem_t& problem, i_t var_id); + void update_bounds_with_selected(std::vector& host_lb, + std::vector& host_ub, + const cache_entry_t& cache_entry, + const std::vector& reverse_original_ids); + i_t check_number_of_conflicting_vars(const std::vector& host_lb, + const std::vector& host_ub, + const cache_entry_t& cache_entry, + f_t integrality_tolerance, + const std::vector& reverse_original_ids); + f_t get_least_conflicting_rounding(problem_t& problem, + std::vector& host_lb, + std::vector& host_ub, + i_t var_id_on_problem, + f_t first_probe, + f_t second_probe, + f_t integrality_tolerance); + void merge_forcings(const std::vector>& forcings, + std::vector>& fixings); + + std::unordered_map, 2>> probing_cache; + std::mutex probing_cache_mutex; +}; + +template +class lb_probing_cache_t { + public: + bool contains(problem_t& problem, i_t var_id); + void update_bounds_with_selected(std::vector& host_bounds, + const cache_entry_t& cache_entry, + const std::vector& reverse_original_ids); + i_t check_number_of_conflicting_vars(const std::vector& host_bounds, + const cache_entry_t& cache_entry, + f_t integrality_tolerance, + const std::vector& reverse_original_ids); + f_t get_least_conflicting_rounding(problem_t& problem, + std::vector& host_bounds, + i_t var_id_on_problem, + f_t first_probe, + f_t second_probe, + f_t integrality_tolerance); + + std::unordered_map, 2>> probing_cache; +}; + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 8ec6700ca9..88a5c7f885 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -268,7 +268,6 @@ mip_solution_t run_mip_solver( settings.determinism_mode != CUOPT_MODE_DETERMINISTIC && problem.original_problem_ptr->get_n_integers() > 0; if (run_early_cpufj) { - auto early_fj_start = std::chrono::steady_clock::now(); auto* presolver_ptr = problem.presolve_data.papilo_presolve_ptr; auto mip_callbacks = settings.get_mip_callbacks(); f_t no_bound = problem.presolve_data.objective_scaling_factor >= 0 ? (f_t)-1e20 : (f_t)1e20; @@ -286,10 +285,10 @@ mip_solution_t run_mip_solver( papilo_num_original_vars = problem.get_papilo_original_num_variables(), &papilo_callback_mutex, &papilo_best_solver_obj, - early_fj_start](f_t solver_obj, - f_t user_obj, - const std::vector& assignment, - const char* heuristic_name) { + &timer](f_t solver_obj, + f_t user_obj, + const std::vector& assignment, + const char* heuristic_name) { std::lock_guard lock(papilo_callback_mutex); if (solver_obj >= papilo_best_solver_obj) { return; } papilo_best_solver_obj = solver_obj; @@ -299,14 +298,11 @@ mip_solution_t run_mip_solver( cuopt_assert(user_assignment.size() == (size_t)papilo_num_original_vars, "Size mismatch"); ctx_ptr->initial_incumbent_assignment = user_assignment; ctx_ptr->initial_upper_bound = user_obj; - double elapsed = - std::chrono::duration(std::chrono::steady_clock::now() - early_fj_start) - .count(); CUOPT_LOG_INFO( - "New solution from early primal heuristics (%s). Objective %+.6e. Time %.2f", + "New solution from early primal heuristics (%s). Objective %+.6e. Time %.3f", heuristic_name, user_obj, - elapsed); + timer.elapsed_time()); invoke_solution_callbacks(mip_callbacks, has_semi_continuous_callback_translation, semi_continuous_original_num_variables, @@ -413,21 +409,6 @@ mip_solution_t solve_mip_helper( raft::common::nvtx::range fun_scope("Running solver"); auto timer = timer_t(time_limit); - problem_checking_t::check_problem_representation(op_problem); - problem_checking_t::check_initial_solution_representation(op_problem, settings); - - if (!settings.initial_solutions.empty()) { - CUOPT_LOG_INFO("Using %zu user-provided initial MIP solution(s)", - settings.initial_solutions.size()); - } - - CUOPT_LOG_INFO( - "Solving a problem with %d constraints, %d variables (%d integers), and %d nonzeros", - op_problem.get_n_constraints(), - op_problem.get_n_variables(), - op_problem.get_n_integers(), - op_problem.get_nnz()); - // Reformulate semi-continuous variables (x = 0 OR L <= x <= U) before Papilo presolve. // Uses deterministic CPU bounds strengthening to derive tight upper bounds for SC vars with // infinite UB. @@ -449,15 +430,6 @@ mip_solution_t solve_mip_helper( settings, n_orig_before_sc, semi_continuous_binary_to_original_indices); } - op_problem.print_scaling_information(); - - // Check for crossing bounds. Return infeasible if there are any - if (problem_checking_t::has_crossing_bounds(op_problem)) { - return mip_solution_t(mip_termination_status_t::Infeasible, - solver_stats_t{}, - op_problem.get_handle_ptr()->get_stream()); - } - for (auto callback : settings.get_mip_callbacks()) { auto callback_num_variables = op_problem.get_n_variables(); if (mip_solver_settings_accessor::has_semi_continuous_callback_translation( @@ -486,16 +458,6 @@ mip_solution_t solve_mip_helper( } #endif - if (settings.mip_scaling != CUOPT_MIP_SCALING_OFF) { - mip::mip_scaling_strategy_t scaling(op_problem); - scaling.scale_problem(settings.mip_scaling != CUOPT_MIP_SCALING_NO_OBJECTIVE); - } - double presolve_time = 0.0; - std::unique_ptr> presolver; - std::optional> presolve_result_opt; - mip::problem_t problem( - op_problem, settings.get_tolerances(), settings.determinism_mode == CUOPT_MODE_DETERMINISTIC); - auto run_presolve = settings.presolver != presolver_t::None; bool has_set_solution_callback = false; for (auto callback : settings.get_mip_callbacks()) { @@ -523,8 +485,8 @@ mip_solution_t solve_mip_helper( std::vector early_incumbent_pool; // Track best incumbent found during presolve (shared across CPU and GPU FJ). - // early_best_objective is in the original problem's solver-space (always minimization), - // used for fast comparison in the callback. + // The CPU and GPU heuristics can use differently scaled solver spaces, so compare their + // objectives in a common minimization-oriented user space. // early_best_user_obj is the corresponding user-space objective, // passed to run_mip for correct cross-space conversion. // We attempt to crush early-heuristics solutions into the presolved space. @@ -533,67 +495,67 @@ mip_solution_t solve_mip_helper( // but is dropped due to these dual reductions, and we lose a good solution. // This is why we still keep the solution around in original-space // and later extract it at the end of the solve. - std::atomic early_best_objective{std::numeric_limits::infinity()}; + std::atomic early_best_user_score{std::numeric_limits::infinity()}; f_t early_best_user_obj{std::numeric_limits::infinity()}; std::vector early_best_user_assignment; std::mutex early_callback_mutex; - if (pre_solve_heuristics && pre_solve_heuristics->solution_found()) { - early_best_user_obj = pre_solve_heuristics->get_best_user_objective(); - early_best_user_assignment = pre_solve_heuristics->get_best_assignment(); - early_best_objective.store(problem.get_solver_obj_from_user_obj(early_best_user_obj)); - } - std::unique_ptr> early_cpufj; std::unique_ptr> early_gpufj; std::unique_ptr> early_structural; bool run_early_fj = run_presolve && settings.determinism_mode != CUOPT_MODE_DETERMINISTIC && - op_problem.get_n_integers() > 0 && op_problem.get_n_constraints() > 0; - f_t no_bound = problem.presolve_data.objective_scaling_factor >= 0 ? (f_t)-1e20 : (f_t)1e20; - if (run_early_fj) { - auto early_fj_start = std::chrono::steady_clock::now(); - auto early_fj_callback = - [&early_best_objective, - &early_best_user_obj, - &early_best_user_assignment, - &early_incumbent_pool, - &early_callback_mutex, - early_fj_start, - mip_callbacks = settings.get_mip_callbacks(), - has_semi_continuous_callback_translation = - mip_solver_settings_accessor::has_semi_continuous_callback_translation( - settings), - semi_continuous_original_num_variables = - mip_solver_settings_accessor::get_semi_continuous_original_num_variables( - settings), - no_bound](f_t solver_obj, - f_t user_obj, - const std::vector& assignment, - const char* heuristic_name) { - std::lock_guard lock(early_callback_mutex); - if (solver_obj >= early_best_objective.load()) { return; } - early_best_objective.store(solver_obj); - early_best_user_obj = user_obj; - early_best_user_assignment = assignment; - early_incumbent_pool.push_back({user_obj, assignment}); - double elapsed = - std::chrono::duration(std::chrono::steady_clock::now() - early_fj_start) - .count(); - CUOPT_LOG_INFO( - "New solution from early primal heuristics (%s). Objective %+.6e. Time %.2f", - heuristic_name, - user_obj, - elapsed); - auto user_assignment = assignment; - invoke_solution_callbacks(mip_callbacks, - has_semi_continuous_callback_translation, - semi_continuous_original_num_variables, - user_obj, - user_assignment, - no_bound); - }; + op_problem.get_problem_category() != problem_category_t::LP && + op_problem.get_n_constraints() > 0; + const f_t objective_sense = op_problem.get_sense() ? f_t{-1} : f_t{1}; + f_t no_bound = objective_sense > f_t{0} ? (f_t)-1e20 : (f_t)1e20; + + // The probe published its incumbent already; adopt it as the early-heuristic best so the + // lanes started below do not republish worse points, and so it reaches the initial bound, + // the population pool and the end-of-solve fallback. + if (pre_solve_heuristics && pre_solve_heuristics->solution_found()) { + early_best_user_obj = pre_solve_heuristics->get_best_user_objective(); + early_best_user_assignment = pre_solve_heuristics->get_best_assignment(); + early_best_user_score.store(objective_sense * early_best_user_obj); + early_incumbent_pool.push_back({early_best_user_obj, early_best_user_assignment}); + } + auto early_fj_callback = + [&early_best_user_score, + &early_best_user_obj, + &early_best_user_assignment, + &early_incumbent_pool, + &early_callback_mutex, + &timer, + objective_sense, + mip_callbacks = settings.get_mip_callbacks(), + has_semi_continuous_callback_translation = + mip_solver_settings_accessor::has_semi_continuous_callback_translation(settings), + semi_continuous_original_num_variables = + mip_solver_settings_accessor::get_semi_continuous_original_num_variables( + settings), + no_bound]( + f_t, f_t user_obj, const std::vector& assignment, const char* heuristic_name) { + std::lock_guard lock(early_callback_mutex); + const f_t objective = objective_sense * user_obj; + if (objective >= early_best_user_score.load()) { return; } + early_best_user_score.store(objective); + early_best_user_obj = user_obj; + early_best_user_assignment = assignment; + early_incumbent_pool.push_back({user_obj, assignment}); + CUOPT_LOG_INFO("New solution from early primal heuristics (%s). Objective %+.6e. Time %.3f", + heuristic_name, + user_obj, + timer.elapsed_time()); + auto user_assignment = assignment; + invoke_solution_callbacks(mip_callbacks, + has_semi_continuous_callback_translation, + semi_continuous_original_num_variables, + user_obj, + user_assignment, + no_bound); + }; + if (run_early_fj) { // Start early CPUFJ on original problem (will restart on presolved problem after Papilo) const uint64_t early_fj_base_seed = mip::get_base_seed(settings.seed); early_cpufj = std::make_unique>( @@ -601,9 +563,56 @@ mip_solution_t solve_mip_helper( settings.get_tolerances(), early_fj_callback, mip::derive_seed(early_fj_base_seed, mip::rng_id_t::early_cpufj)); + // Both are built from the same op_problem, so the probe's threshold needs no conversion. + if (pre_solve_heuristics && pre_solve_heuristics->solution_found()) { + early_cpufj->set_best_objective(pre_solve_heuristics->get_best_objective()); + } early_cpufj->start(); CUOPT_LOG_DEBUG("Started early CPUFJ on original problem"); + } + + auto early_cpufj_guard = cuopt::scope_guard([&]() { + if (early_cpufj) { + early_cpufj->stop(); + early_cpufj.reset(); + } + }); + + problem_checking_t::check_problem_representation(op_problem); + problem_checking_t::check_initial_solution_representation(op_problem, settings); + + if (!settings.initial_solutions.empty()) { + CUOPT_LOG_INFO("Using %zu user-provided initial MIP solution(s)", + settings.initial_solutions.size()); + } + + CUOPT_LOG_INFO( + "Solving a problem with %d constraints, %d variables (%d integers), and %d nonzeros", + op_problem.get_n_constraints(), + op_problem.get_n_variables(), + op_problem.get_n_integers(), + op_problem.get_nnz()); + + op_problem.print_scaling_information(); + // Check for crossing bounds. Return infeasible if there are any + if (problem_checking_t::has_crossing_bounds(op_problem)) { + return mip_solution_t(mip_termination_status_t::Infeasible, + solver_stats_t{}, + op_problem.get_handle_ptr()->get_stream()); + } + + if (settings.mip_scaling != CUOPT_MIP_SCALING_OFF) { + mip::mip_scaling_strategy_t scaling(op_problem); + scaling.scale_problem(settings.mip_scaling != CUOPT_MIP_SCALING_NO_OBJECTIVE); + } + double presolve_time = 0.0; + std::unique_ptr> presolver; + std::optional> presolve_result_opt; + mip::problem_t problem( + op_problem, settings.get_tolerances(), settings.determinism_mode == CUOPT_MODE_DETERMINISTIC); + + if (run_early_fj) { // Start early GPU FJ (uses GPU while CPU is busy with Papilo) early_gpufj = std::make_unique>(op_problem, settings, early_fj_callback); @@ -720,13 +729,18 @@ mip_solution_t solve_mip_helper( // Add early-heuristic incumbents (original-space) to initial_solutions. // PaPILO crushing + validation happens downstream in add_user_given_solutions(). if (!early_incumbent_pool.empty()) { - auto stream = op_problem.get_handle_ptr()->get_stream(); - for (const auto& inc : early_incumbent_pool) { - auto d = std::make_shared>(device_copy(inc.assignment, stream)); + auto stream = op_problem.get_handle_ptr()->get_stream(); + constexpr size_t max_early_incumbents = 5; + const size_t first_kept = early_incumbent_pool.size() > max_early_incumbents + ? early_incumbent_pool.size() - max_early_incumbents + : 0; + for (size_t i = first_kept; i < early_incumbent_pool.size(); ++i) { + auto d = std::make_shared>( + device_copy(early_incumbent_pool[i].assignment, stream)); settings.initial_solutions.emplace_back(std::move(d)); } CUOPT_LOG_DEBUG("Added %zu early-heuristic incumbents to initial solutions", - early_incumbent_pool.size()); + early_incumbent_pool.size() - first_kept); } if (settings.user_problem_file != "") { @@ -940,6 +954,8 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, f_t, f_t user_obj, const std::vector& assignment, const char* heuristic_name) { std::vector user_assignment = assignment; invoke_solution_callbacks(mip_callbacks, false, 0, user_obj, user_assignment, no_bound); + // try_update_best is the monotonicity gate and the single probe lane serialises on + // early_cpufj_t::incumbent_mutex_, so there is nothing left to guard here. CUOPT_LOG_INFO( "New solution from early primal heuristics (%s). Objective %+.6e. Time %.3f", heuristic_name, @@ -947,7 +963,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, std::chrono::duration(std::chrono::steady_clock::now() - probe_start).count()); }, mip::derive_seed(settings_const.seed, mip::rng_id_t::early_cpufj)); - pre_solve_heuristics->start(); + pre_solve_heuristics->start(/*low_latency=*/true); } cuopt::scope_guard release_probe([&pre_solve_heuristics] { if (pre_solve_heuristics) { pre_solve_heuristics->stop(); } @@ -994,6 +1010,8 @@ mip_solution_t solve_mip(raft::handle_t const* handle_ptr, const io::mps_data_model_t& mps_data_model, mip_solver_settings_t const& settings) { + // launch trivial heuristics on a separate thread while the OMP and the GPU problem are being + // initialized. auto op_problem = mps_data_model_to_optimization_problem(handle_ptr, mps_data_model); return solve_mip(op_problem, settings); } diff --git a/cpp/src/mip_heuristics/structural/early_structural.cu b/cpp/src/mip_heuristics/structural/early_structural.cu index 0f64e49eff..e91e31f6c6 100644 --- a/cpp/src/mip_heuristics/structural/early_structural.cu +++ b/cpp/src/mip_heuristics/structural/early_structural.cu @@ -8,12 +8,17 @@ #include "early_structural.cuh" #include +#include #include #include +#include #include +#include + #include +#include #include @@ -63,13 +68,21 @@ early_structural_t::early_structural_t( const typename mip_solver_settings_t::tolerances_t& tolerances, early_incumbent_callback_t incumbent_callback, std::unique_ptr> active) - : early_heuristic_t>( - op_problem, tolerances, std::move(incumbent_callback)), + : early_heuristic_t>(op_problem, + std::move(incumbent_callback)), op_problem_(op_problem), tolerances_(tolerances), active_(std::move(active)) { cuopt_assert(active_ != nullptr, "missing structural heuristic"); + + RAFT_CUDA_TRY(cudaGetDevice(&device_id_)); + + problem_t temp_problem(op_problem, tolerances, false); + temp_problem.preprocess_problem(); + temp_problem.handle_ptr->sync_stream(); + problem_ = std::make_unique>(temp_problem, &handle_); + CUOPT_LOG_DEBUG("[Early Structural] %s recognized the model", active_->name()); } @@ -112,11 +125,9 @@ template bool early_structural_t::preprocessing_is_identity() const { // Recognition produces assignments in the source problem's column space. - const auto& presolve_data = this->problem_ptr_->presolve_data; - if (this->problem_ptr_->n_variables != op_problem_.get_n_variables()) { return false; } - if ((i_t)presolve_data.variable_offsets.size() != this->problem_ptr_->n_variables) { - return false; - } + const auto& presolve_data = problem_->presolve_data; + if (problem_->n_variables != op_problem_.get_n_variables()) { return false; } + if ((i_t)presolve_data.variable_offsets.size() != problem_->n_variables) { return false; } for (const f_t offset : presolve_data.variable_offsets) { if (offset != f_t{0}) { return false; } } @@ -145,8 +156,9 @@ void early_structural_t::run() return; } + RAFT_CUDA_TRY(cudaSetDevice(device_id_)); f_t objective{0}; - if (!validate(*this->problem_ptr_, assignment, objective)) { + if (!validate(*problem_, assignment, objective)) { CUOPT_LOG_DEBUG("[Early Structural] %s constructed a point that failed validation, discarding", active_->name()); return; @@ -154,6 +166,18 @@ void early_structural_t::run() this->try_update_best(objective, assignment, active_->name()); } +template +std::vector early_structural_t::to_user_assignment( + const std::vector& assignment) +{ + RAFT_CUDA_TRY(cudaSetDevice(device_id_)); + auto stream = handle_.get_stream(); + rmm::device_uvector d_assignment(assignment.size(), stream); + raft::copy(d_assignment.data(), assignment.data(), assignment.size(), stream); + problem_->post_process_assignment(d_assignment, true, stream); + return cuopt::host_copy(d_assignment, stream); +} + template root_structural_t::root_structural_t( problem_t& problem, diff --git a/cpp/src/mip_heuristics/structural/early_structural.cuh b/cpp/src/mip_heuristics/structural/early_structural.cuh index 883ec2e9fe..0179563b3d 100644 --- a/cpp/src/mip_heuristics/structural/early_structural.cuh +++ b/cpp/src/mip_heuristics/structural/early_structural.cuh @@ -8,10 +8,14 @@ #pragma once #include +#include + +#include #include #include #include +#include namespace cuopt::mathematical_optimization::mip { @@ -53,6 +57,8 @@ class early_structural_t : public early_heuristic_t>; + early_structural_t(const optimization_problem_t& op_problem, const typename mip_solver_settings_t::tolerances_t& tolerances, early_incumbent_callback_t incumbent_callback, @@ -62,8 +68,13 @@ class early_structural_t : public early_heuristic_t to_user_assignment(const std::vector& assignment); + const optimization_problem_t& op_problem_; typename mip_solver_settings_t::tolerances_t tolerances_; + int device_id_{0}; + raft::handle_t handle_; + std::unique_ptr> problem_; std::unique_ptr> active_; std::atomic preemption_flag_{false}; bool task_launched_{false}; diff --git a/cpp/src/mip_heuristics/utils.cuh b/cpp/src/mip_heuristics/utils.cuh index 24a0d6ac00..faf2b9f080 100644 --- a/cpp/src/mip_heuristics/utils.cuh +++ b/cpp/src/mip_heuristics/utils.cuh @@ -18,6 +18,7 @@ #include #include #include +#include #include @@ -142,6 +143,21 @@ inline std::vector get_random_uniform_vector(i_t size, return vec; } +template +inline std::vector get_random_uniform_vector(i_t size, + cuopt::pcgenerator_t& rng, + f_t range_start = -1., + f_t range_end = 1.) +{ + std::vector vec; + vec.reserve(size); + for (i_t i = 0; i < size; ++i) { + f_t random_val = rng.uniform(range_start, range_end); + vec.push_back(random_val); + } + return vec; +} + template inline void elementwise_square_root(rmm::device_uvector& vals, const raft::handle_t* handle_ptr) diff --git a/cpp/src/mip_heuristics/utils.hpp b/cpp/src/mip_heuristics/utils.hpp index 7b0c9f3f78..0e5f177378 100644 --- a/cpp/src/mip_heuristics/utils.hpp +++ b/cpp/src/mip_heuristics/utils.hpp @@ -7,6 +7,8 @@ #pragma once +#include + #include #include #include @@ -59,6 +61,31 @@ __attribute__((optimize("no-fast-math"))) CUOPT_MIP_HOST_DEVICE auto compensated return p + s; } +template +CUOPT_MIP_HOST_DEVICE auto compensated_dot2_csr(CoeffIt coefficients, + IndexIt columns, + ValueIt values, + size_t nnz) +{ + return compensated_dot2(coefficients, thrust::make_permutation_iterator(values, columns), nnz); +} + +template +CUOPT_MIP_HOST_DEVICE auto compensated_dot2_csr( + OffsetIt offsets, IndexIt columns, CoeffIt coefficients, ValueIt values, i_t row) +{ + const auto begin = offsets[row]; + const auto end = offsets[row + 1]; + return compensated_dot2_csr(coefficients + begin, columns + begin, values, end - begin); +} + +template +inline auto compensated_dot2_csr(const CsrLike& csr, const Values& values, i_t row) +{ + return compensated_dot2_csr( + csr.offsets.data(), csr.variables.data(), csr.coefficients.data(), values.data(), row); +} + } // namespace cuopt::mathematical_optimization::mip #undef CUOPT_MIP_HOST_DEVICE diff --git a/cpp/src/utilities/copy_helpers.hpp b/cpp/src/utilities/copy_helpers.hpp index 4c228f0393..c1dd8cb0ce 100644 --- a/cpp/src/utilities/copy_helpers.hpp +++ b/cpp/src/utilities/copy_helpers.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -24,61 +25,6 @@ namespace cuopt { -template -struct type_2 { - using type = void; -}; - -template <> -struct type_2 { - using type = int2; -}; - -template <> -struct type_2 { - using type = float2; -}; - -template <> -struct type_2 { - using type = double2; -}; - -template -struct scalar_type { - using type = void; -}; - -template <> -struct scalar_type { - using type = int; -}; - -template <> -struct scalar_type { - using type = float; -}; - -template <> -struct scalar_type { - using type = double; -}; - -template <> -struct scalar_type { - using type = const int; -}; - -template <> -struct scalar_type { - using type = const float; -}; - -template <> -struct scalar_type { - using type = const double; -}; - template raft::device_span::type> make_span_2(rmm::device_uvector& container) { @@ -98,18 +44,6 @@ raft::device_span::type> make_span_2( sizeof(T) * container.size() / sizeof(T2)); } -template -__host__ __device__ inline typename scalar_type::type& get_lower(f_t2& val) -{ - return val.x; -} - -template -__host__ __device__ inline typename scalar_type::type& get_upper(f_t2& val) -{ - return val.y; -} - /** * @brief Simple utility function to copy device ptr to host * @@ -129,6 +63,14 @@ auto host_copy(T const* device_ptr, size_t size, cuda::stream_ref stream_view) return host_vec; } +template +auto host_copy_async(rmm::device_uvector const& device_vec, cuda::stream_ref stream_view) +{ + std::vector host_vec(device_vec.size()); + raft::copy(host_vec.data(), device_vec.data(), device_vec.size(), stream_view); + return host_vec; +} + /** * @brief Simple utility function to copy bool device ptr to host * diff --git a/cpp/src/utilities/seed_generator.cuh b/cpp/src/utilities/seed_generator.cuh index b57752336d..3a37d896b9 100644 --- a/cpp/src/utilities/seed_generator.cuh +++ b/cpp/src/utilities/seed_generator.cuh @@ -8,8 +8,6 @@ #pragma once #include -#include -#include namespace cuopt { diff --git a/cpp/src/utilities/type_2.hpp b/cpp/src/utilities/type_2.hpp new file mode 100644 index 0000000000..cbab136eaf --- /dev/null +++ b/cpp/src/utilities/type_2.hpp @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace cuopt { + +template +struct type_2 { + using type = void; +}; + +template <> +struct type_2 { + using type = int2; +}; + +template <> +struct type_2 { + using type = float2; +}; + +template <> +struct type_2 { + using type = double2; +}; + +#if defined(__CUDACC__) +#define CUOPT_TYPE_2_HOST_DEVICE inline __host__ __device__ +#else +#define CUOPT_TYPE_2_HOST_DEVICE inline +#endif + +template +CUOPT_TYPE_2_HOST_DEVICE auto& get_lower(f_t2& value) +{ + return value.x; +} + +template +CUOPT_TYPE_2_HOST_DEVICE auto& get_upper(f_t2& value) +{ + return value.y; +} + +#undef CUOPT_TYPE_2_HOST_DEVICE + +} // namespace cuopt diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 70d0d6d2e3..8696be791d 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -118,6 +118,7 @@ function(ConfigureTest CMAKE_TEST_NAME) GTest::gtest GTest::gtest_main ${CUOPT_PRIVATE_CUDA_LIBS} + OpenMP::OpenMP_CUDA ) if(NOT DEFINED INSTALL_TARGET OR "${INSTALL_TARGET}" STREQUAL "") target_link_options(${CMAKE_TEST_NAME} PRIVATE -Wl,--enable-new-dtags) diff --git a/cpp/tests/linear_programming/grpc/CMakeLists.txt b/cpp/tests/linear_programming/grpc/CMakeLists.txt index eaa787cc0d..7bcac7634f 100644 --- a/cpp/tests/linear_programming/grpc/CMakeLists.txt +++ b/cpp/tests/linear_programming/grpc/CMakeLists.txt @@ -126,6 +126,10 @@ target_link_libraries(GRPC_INTEGRATION_TEST protobuf::libprotobuf ) +if(CUOPT_ENABLE_GRPC_ROUTING) + target_compile_definitions(GRPC_INTEGRATION_TEST PRIVATE CUOPT_ENABLE_GRPC_ROUTING) +endif() + if(NOT DEFINED INSTALL_TARGET OR "${INSTALL_TARGET}" STREQUAL "") target_link_options(GRPC_INTEGRATION_TEST PRIVATE -Wl,--enable-new-dtags) endif() diff --git a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp index a98bb1d574..5e04a85dd7 100644 --- a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp @@ -43,8 +43,10 @@ #include #include #include +#ifdef CUOPT_ENABLE_GRPC_ROUTING #include #include +#endif #include #include "grpc_client.hpp" @@ -825,6 +827,7 @@ TEST_F(DefaultServerTests, SolveMIPBlocking) // -- Explicit Async LP Flow (submit/poll/get/delete) -- +#ifdef CUOPT_ENABLE_GRPC_ROUTING // VRP over gRPC, in the same shape as the LP and MIP cases above: submit a problem, // poll to completion, fetch the solution and check it. This is the only routing case in // this suite, and it is the end-to-end exercise of the routing mappers -- the problem and @@ -889,6 +892,7 @@ TEST_F(DefaultServerTests, SolveVRP) EXPECT_TRUE(solution.unserviced_nodes.empty()) << solution.unserviced_nodes.size() << " orders left unserved"; } +#endif TEST_F(DefaultServerTests, ExplicitAsyncLPFlow) {