diff --git a/ci/validate_wheel.sh b/ci/validate_wheel.sh index 420f89c801..79188cacc3 100755 --- a/ci/validate_wheel.sh +++ b/ci/validate_wheel.sh @@ -22,11 +22,11 @@ PYDISTCHECK_ARGS=( if [[ "${package_dir}" == "python/libcuopt" ]]; then if [[ "${RAPIDS_CUDA_MAJOR}" == "12" ]]; then PYDISTCHECK_ARGS+=( - --max-allowed-size-compressed '645Mi' + --max-allowed-size-compressed '650Mi' ) else PYDISTCHECK_ARGS+=( - --max-allowed-size-compressed '490Mi' + --max-allowed-size-compressed '495Mi' ) fi elif [[ "${package_dir}" != "python/cuopt" ]] && \ diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index e8141eef5b..9249b53171 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -138,7 +138,8 @@ endif(BUILD_MSAN) # infrastructure files. Those files include abseil headers, and abseil's shared library # on conda-forge doesn't export Mutex::Dtor() in NDEBUG builds (abseil-cpp#1624). # Keeping NDEBUG defined for gRPC files makes the header inline an empty Dtor(), -# avoiding the missing symbol at runtime. +# avoiding the missing symbol at runtime. Additionally, gRPC files are always +# compiled with -DNDEBUG (see below) so Debug builds also avoid the missing symbol. if(DEFINE_ASSERT) add_definitions(-DASSERT_MODE) list(APPEND CUOPT_CUDA_FLAGS -UNDEBUG) @@ -390,7 +391,7 @@ if(DEFINE_ASSERT) endif() # Add gRPC mapper files and generated protobuf sources -list(APPEND CUOPT_SRC_FILES +set(GRPC_INFRA_FILES ${PROTO_SRCS} ${GRPC_PROTO_SRCS} ${GRPC_SERVICE_SRCS} @@ -401,6 +402,15 @@ list(APPEND CUOPT_SRC_FILES src/grpc/client/grpc_client.cpp src/grpc/client/solve_remote.cpp ) +list(APPEND CUOPT_SRC_FILES ${GRPC_INFRA_FILES}) + +# Always keep NDEBUG defined for gRPC infrastructure files so that abseil +# headers inline Mutex::Dtor() instead of emitting an external call. +# The conda-forge abseil shared library is built with NDEBUG and does not +# export that symbol (abseil-cpp#1624). Without this, Debug builds fail +# at runtime with "undefined symbol: absl::…::Mutex::Dtor". +set_property(SOURCE ${GRPC_INFRA_FILES} DIRECTORY ${CMAKE_SOURCE_DIR} + APPEND PROPERTY COMPILE_OPTIONS "-DNDEBUG") add_library(cuopt SHARED ${CUOPT_SRC_FILES} diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index 9d13e87105..06eacb3408 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -180,4 +180,9 @@ #define CUOPT_PRESOLVE_PAPILO 1 #define CUOPT_PRESOLVE_PSLP 2 +/* @brief MIP scaling mode constants */ +#define CUOPT_MIP_SCALING_OFF 0 +#define CUOPT_MIP_SCALING_ON 1 +#define CUOPT_MIP_SCALING_NO_OBJECTIVE 2 + #endif // CUOPT_CONSTANTS_H diff --git a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp index 77472b6eae..14c4d227bc 100644 --- a/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/mip/solver_settings.hpp @@ -115,7 +115,7 @@ class mip_solver_settings_t { /** Initial primal solutions */ std::vector>> initial_solutions; - bool mip_scaling = false; + int mip_scaling = CUOPT_MIP_SCALING_NO_OBJECTIVE; presolver_t presolver{presolver_t::Default}; /** * @brief Determinism mode for MIP solver. diff --git a/cpp/src/branch_and_bound/branch_and_bound.cpp b/cpp/src/branch_and_bound/branch_and_bound.cpp index 7f9c4665bc..e94e8c93e1 100644 --- a/cpp/src/branch_and_bound/branch_and_bound.cpp +++ b/cpp/src/branch_and_bound/branch_and_bound.cpp @@ -174,14 +174,14 @@ f_t sgn(f_t x) return x < 0 ? -1 : 1; } -template -f_t relative_gap(f_t obj_value, f_t lower_bound) +template +f_t compute_user_abs_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) { - f_t user_mip_gap = obj_value == 0.0 - ? (lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) - : std::abs(obj_value - lower_bound) / std::abs(obj_value); - if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } - return user_mip_gap; + // abs_gap = |user_obj - user_lower| = |obj_scale| * |obj_value - lower_bound| + // obj_constant cancels out in the subtraction; obj_scale sign must be removed via abs + f_t gap = std::abs(lp.obj_scale) * (obj_value - lower_bound); + if (gap < -1e-4) { CUOPT_LOG_ERROR("Gap is negative %e", gap); } + return gap; } template @@ -191,15 +191,15 @@ f_t user_relative_gap(const lp_problem_t& lp, f_t obj_value, f_t lower f_t user_lower_bound = compute_user_objective(lp, lower_bound); f_t user_mip_gap = user_obj == 0.0 ? (user_lower_bound == 0.0 ? 0.0 : std::numeric_limits::infinity()) - : std::abs(user_obj - user_lower_bound) / std::abs(user_obj); + : compute_user_abs_gap(lp, obj_value, lower_bound) / std::abs(user_obj); if (std::isnan(user_mip_gap)) { return std::numeric_limits::infinity(); } return user_mip_gap; } -template -std::string user_mip_gap(f_t obj_value, f_t lower_bound) +template +std::string user_mip_gap(const lp_problem_t& lp, f_t obj_value, f_t lower_bound) { - const f_t user_mip_gap = relative_gap(obj_value, lower_bound); + const f_t user_mip_gap = user_relative_gap(lp, obj_value, lower_bound); if (user_mip_gap == std::numeric_limits::infinity()) { return " - "; } else { @@ -319,7 +319,7 @@ void branch_and_bound_t::report_heuristic(f_t obj) if (is_running_) { f_t user_obj = compute_user_objective(original_lp_, obj); f_t user_lower = compute_user_objective(original_lp_, get_lower_bound()); - std::string user_gap = user_mip_gap(user_obj, user_lower); + std::string user_gap = user_mip_gap(original_lp_, obj, get_lower_bound()); settings_.log.printf( "H %+13.6e %+10.6e %s %9.2f\n", @@ -329,9 +329,9 @@ void branch_and_bound_t::report_heuristic(f_t obj) toc(exploration_stats_.start_time)); } else { if (solving_root_relaxation_.load()) { - f_t user_obj = compute_user_objective(original_lp_, obj); - f_t user_lower = root_lp_current_lower_bound_.load(); - std::string user_gap = user_mip_gap(user_obj, user_lower); + f_t user_obj = compute_user_objective(original_lp_, obj); + std::string user_gap = + user_mip_gap(original_lp_, obj, root_lp_current_lower_bound_.load()); settings_.log.printf( "New solution from primal heuristics. Objective %+.6e. Gap %s. Time %.2f\n", user_obj, @@ -356,7 +356,7 @@ void branch_and_bound_t::report( const f_t user_lower = compute_user_objective(original_lp_, lower_bound); const f_t iters = static_cast(exploration_stats_.total_lp_iters); const f_t iter_node = nodes_explored > 0 ? iters / nodes_explored : iters; - const std::string user_gap = user_mip_gap(user_obj, user_lower); + const std::string user_gap = user_mip_gap(original_lp_, obj, lower_bound); if (work_time >= 0) { settings_.log.printf( "%c %10d %10lu %+13.6e %+10.6e %6d %6d %7.1e %s %9.2f %9.2f\n", @@ -717,9 +717,9 @@ void branch_and_bound_t::set_final_solution(mip_solution_t& settings_.heuristic_preemption_callback(); } - f_t gap = upper_bound_ - lower_bound; f_t obj = compute_user_objective(original_lp_, upper_bound_.load()); f_t user_bound = compute_user_objective(original_lp_, lower_bound); + f_t gap = std::abs(obj - user_bound); f_t gap_rel = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); bool is_maximization = original_lp_.obj_scale < 0.0; @@ -1437,7 +1437,7 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_t 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { @@ -1528,13 +1528,13 @@ void branch_and_bound_t::plunge_with(branch_and_bound_worker_t 0 && (rel_gap <= settings_.relative_mip_gap_tol || abs_gap <= settings_.absolute_mip_gap_tol)) { @@ -1581,7 +1581,7 @@ void branch_and_bound_t::dive_with(branch_and_bound_worker_t f_t lower_bound = get_lower_bound(); f_t upper_bound = upper_bound_; f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); - f_t abs_gap = upper_bound - lower_bound; + f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); while (stack.size() > 0 && (solver_status_ == mip_status_t::UNSET && is_running_) && rel_gap > settings_.relative_mip_gap_tol && abs_gap > settings_.absolute_mip_gap_tol) { @@ -1636,7 +1636,7 @@ void branch_and_bound_t::dive_with(branch_and_bound_worker_t lower_bound = get_lower_bound(); upper_bound = upper_bound_; rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); - abs_gap = upper_bound - lower_bound; + abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); } worker_pool_.return_worker_to_pool(worker); @@ -1667,7 +1667,7 @@ void branch_and_bound_t::run_scheduler() #endif f_t lower_bound = get_lower_bound(); - f_t abs_gap = upper_bound_ - lower_bound; + f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), lower_bound); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); i_t last_node_depth = 0; i_t last_int_infeas = 0; @@ -1777,7 +1777,7 @@ void branch_and_bound_t::run_scheduler() } lower_bound = get_lower_bound(); - abs_gap = upper_bound_ - lower_bound; + abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), lower_bound); rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); if (abs_gap <= settings_.absolute_mip_gap_tol || rel_gap <= settings_.relative_mip_gap_tol) { @@ -1799,7 +1799,7 @@ void branch_and_bound_t::single_threaded_solve() branch_and_bound_worker_t worker(0, original_lp_, Arow_, var_types_, settings_); f_t lower_bound = get_lower_bound(); - f_t abs_gap = upper_bound_ - lower_bound; + f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), lower_bound); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); while (solver_status_ == mip_status_t::UNSET && abs_gap > settings_.absolute_mip_gap_tol && @@ -1844,7 +1844,7 @@ void branch_and_bound_t::single_threaded_solve() plunge_with(&worker); lower_bound = get_lower_bound(); - abs_gap = upper_bound_ - lower_bound; + abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), lower_bound); rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), lower_bound); if (abs_gap <= settings_.absolute_mip_gap_tol || rel_gap <= settings_.relative_mip_gap_tol) { @@ -2466,7 +2466,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut report(' ', obj, root_objective_, 0, num_fractional); f_t rel_gap = user_relative_gap(original_lp_, upper_bound_.load(), root_objective_); - f_t abs_gap = upper_bound_.load() - root_objective_; + f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound_.load(), root_objective_); if (rel_gap < settings_.relative_mip_gap_tol || abs_gap < settings_.absolute_mip_gap_tol) { set_solution_at_root(solution, cut_info); set_final_solution(solution, root_objective_); @@ -2961,7 +2961,6 @@ void branch_and_bound_t::run_deterministic_bfs_loop( worker.current_node = node; f_t upper_bound = worker.local_upper_bound; - f_t rel_gap = user_relative_gap(original_lp_, upper_bound, node->lower_bound); if (node->lower_bound > upper_bound) { worker.current_node = nullptr; worker.record_fathomed(node, node->lower_bound); @@ -3073,7 +3072,7 @@ void branch_and_bound_t::deterministic_sync_callback() f_t lower_bound = deterministic_compute_lower_bound(); f_t upper_bound = upper_bound_.load(); - f_t abs_gap = upper_bound - lower_bound; + f_t abs_gap = compute_user_abs_gap(original_lp_, upper_bound, lower_bound); f_t rel_gap = user_relative_gap(original_lp_, upper_bound, lower_bound); if (abs_gap <= settings_.absolute_mip_gap_tol || rel_gap <= settings_.relative_mip_gap_tol) { @@ -3112,7 +3111,7 @@ void branch_and_bound_t::deterministic_sync_callback() f_t obj = compute_user_objective(original_lp_, upper_bound); f_t user_lower = compute_user_objective(original_lp_, lower_bound); - std::string gap_user = user_mip_gap(obj, user_lower); + std::string gap_user = user_mip_gap(original_lp_, upper_bound, lower_bound); std::string idle_workers; i_t idle_count = 0; @@ -3750,7 +3749,6 @@ void branch_and_bound_t::deterministic_dive( stack.pop_front(); // Prune check using snapshot upper bound - f_t rel_gap = user_relative_gap(original_lp_, worker.local_upper_bound, node_ptr->lower_bound); if (node_ptr->lower_bound > worker.local_upper_bound) { worker.recompute_bounds_and_basis = true; continue; diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 34bf0376b7..5b1130796e 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -3571,7 +3571,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, sum_perturb, now); if (phase == 2 && settings.inside_mip == 1 && settings.dual_simplex_objective_callback) { - settings.dual_simplex_objective_callback(user_obj); + settings.dual_simplex_objective_callback(obj); } } diff --git a/cpp/src/dual_simplex/user_problem.hpp b/cpp/src/dual_simplex/user_problem.hpp index f50a6d33a5..73c4c391be 100644 --- a/cpp/src/dual_simplex/user_problem.hpp +++ b/cpp/src/dual_simplex/user_problem.hpp @@ -46,7 +46,7 @@ struct user_problem_t { std::vector row_names; std::vector col_names; f_t obj_constant; - f_t obj_scale; // 1.0 for min, -1.0 for max + f_t obj_scale; // positive for min, netagive for max bool objective_is_integral{false}; std::vector var_types; std::vector Q_offsets; diff --git a/cpp/src/grpc/cuopt_remote.proto b/cpp/src/grpc/cuopt_remote.proto index f638100c13..cc7af2a1f7 100644 --- a/cpp/src/grpc/cuopt_remote.proto +++ b/cpp/src/grpc/cuopt_remote.proto @@ -162,7 +162,7 @@ message MIPSolverSettings { int32 num_cpu_threads = 12; int32 num_gpus = 13; int32 presolver = 14; - bool mip_scaling = 15; + int32 mip_scaling = 15; } // LP solve request diff --git a/cpp/src/grpc/grpc_settings_mapper.cpp b/cpp/src/grpc/grpc_settings_mapper.cpp index 8885b2e358..0c52d766b0 100644 --- a/cpp/src/grpc/grpc_settings_mapper.cpp +++ b/cpp/src/grpc/grpc_settings_mapper.cpp @@ -230,7 +230,12 @@ void map_proto_to_mip_settings(const cuopt::remote::MIPSolverSettings& pb_settin ? static_cast(pv) : presolver_t::Default; } - settings.mip_scaling = pb_settings.mip_scaling(); + { + auto sv = pb_settings.mip_scaling(); + settings.mip_scaling = (sv >= CUOPT_MIP_SCALING_OFF && sv <= CUOPT_MIP_SCALING_NO_OBJECTIVE) + ? sv + : CUOPT_MIP_SCALING_ON; + } } // Explicit template instantiations diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 425d789b6c..c23b1d27ca 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -146,6 +146,7 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_RANDOM_SEED, &mip_settings.seed, -1, std::numeric_limits::max(), -1}, {CUOPT_MIP_RELIABILITY_BRANCHING, &mip_settings.reliability_branching, -1, std::numeric_limits::max(), -1}, {CUOPT_PDLP_PRECISION, reinterpret_cast(&pdlp_settings.pdlp_precision), CUOPT_PDLP_DEFAULT_PRECISION, CUOPT_PDLP_MIXED_PRECISION, CUOPT_PDLP_DEFAULT_PRECISION}, + {CUOPT_MIP_SCALING, &mip_settings.mip_scaling, CUOPT_MIP_SCALING_OFF, CUOPT_MIP_SCALING_NO_OBJECTIVE, CUOPT_MIP_SCALING_ON}, // MIP heuristic hyper-parameters (hidden from default --help: name contains "hyper_") {CUOPT_MIP_HYPER_HEURISTIC_POPULATION_SIZE, &mip_settings.heuristic_params.population_size, 1, std::numeric_limits::max(), 32, "max solutions in pool"}, {CUOPT_MIP_HYPER_HEURISTIC_NUM_CPUFJ_THREADS, &mip_settings.heuristic_params.num_cpufj_threads, 0, std::numeric_limits::max(), 8, "parallel CPU FJ climbers"}, @@ -163,7 +164,6 @@ solver_settings_t::solver_settings_t() : pdlp_settings(), mip_settings {CUOPT_PER_CONSTRAINT_RESIDUAL, &pdlp_settings.per_constraint_residual, false}, {CUOPT_SAVE_BEST_PRIMAL_SO_FAR, &pdlp_settings.save_best_primal_so_far, false}, {CUOPT_FIRST_PRIMAL_FEASIBLE, &pdlp_settings.first_primal_feasible, false}, - {CUOPT_MIP_SCALING, &mip_settings.mip_scaling, true}, {CUOPT_MIP_HEURISTICS_ONLY, &mip_settings.heuristics_only, false}, {CUOPT_LOG_TO_CONSOLE, &pdlp_settings.log_to_console, true}, {CUOPT_LOG_TO_CONSOLE, &mip_settings.log_to_console, true}, diff --git a/cpp/src/mip_heuristics/CMakeLists.txt b/cpp/src/mip_heuristics/CMakeLists.txt index 8bc07fc02e..13649682a6 100644 --- a/cpp/src/mip_heuristics/CMakeLists.txt +++ b/cpp/src/mip_heuristics/CMakeLists.txt @@ -19,6 +19,7 @@ set(MIP_LP_NECESSARY_FILES # Files that are MIP-specific and not needed for pure LP set(MIP_NON_LP_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/mip_scaling_strategy.cu ${CMAKE_CURRENT_SOURCE_DIR}/solve.cu ${CMAKE_CURRENT_SOURCE_DIR}/solver.cu ${CMAKE_CURRENT_SOURCE_DIR}/diversity/assignment_hash_map.cu diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 174d910c1f..e821c016c2 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -225,6 +225,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit, timer_t global_ raft::common::nvtx::range fun_scope("run_presolve"); CUOPT_LOG_INFO("Running presolve!"); timer_t presolve_timer(time_limit); + auto term_crit = ls.constraint_prop.bounds_update.solve(*problem_ptr); if (ls.constraint_prop.bounds_update.infeas_constraints_count > 0) { stats.presolve_time = timer.elapsed_time(); @@ -464,23 +465,25 @@ solution_t diversity_manager_t::run_solver() } else if (!fj_only_run) { convert_greater_to_less(*problem_ptr); - f_t tolerance_divisor = - problem_ptr->tolerances.absolute_tolerance / problem_ptr->tolerances.relative_tolerance; - if (tolerance_divisor == 0) { tolerance_divisor = 1; } f_t absolute_tolerance = context.settings.tolerances.absolute_tolerance; pdlp_solver_settings_t pdlp_settings{}; - pdlp_settings.tolerances.relative_primal_tolerance = absolute_tolerance / tolerance_divisor; - pdlp_settings.tolerances.relative_dual_tolerance = absolute_tolerance / tolerance_divisor; - pdlp_settings.time_limit = lp_time_limit; - pdlp_settings.first_primal_feasible = false; - pdlp_settings.concurrent_halt = &global_concurrent_halt; - pdlp_settings.method = method_t::Concurrent; - pdlp_settings.inside_mip = true; - pdlp_settings.pdlp_solver_mode = pdlp_solver_mode_t::Stable2; - pdlp_settings.num_gpus = context.settings.num_gpus; - pdlp_settings.presolver = presolver_t::None; - + pdlp_settings.tolerances.absolute_dual_tolerance = absolute_tolerance; + pdlp_settings.tolerances.relative_dual_tolerance = + context.settings.tolerances.relative_tolerance; + pdlp_settings.tolerances.absolute_primal_tolerance = absolute_tolerance; + pdlp_settings.tolerances.relative_primal_tolerance = + context.settings.tolerances.relative_tolerance; + pdlp_settings.time_limit = lp_time_limit; + pdlp_settings.first_primal_feasible = false; + pdlp_settings.concurrent_halt = &global_concurrent_halt; + pdlp_settings.method = method_t::Concurrent; + pdlp_settings.inside_mip = true; + pdlp_settings.pdlp_solver_mode = pdlp_solver_mode_t::Stable2; + pdlp_settings.num_gpus = context.settings.num_gpus; + pdlp_settings.presolver = presolver_t::None; + pdlp_settings.per_constraint_residual = true; + set_pdlp_solver_mode(pdlp_settings); timer_t lp_timer(lp_time_limit); auto lp_result = solve_lp_with_method(*problem_ptr, pdlp_settings, lp_timer); @@ -512,7 +515,11 @@ solution_t diversity_manager_t::run_solver() ls.lp_optimal_exists = true; if (!use_staged_simplex_solution) { if (lp_result.get_termination_status() == pdlp_termination_status_t::Optimal) { - set_new_user_bound(lp_result.get_objective_value()); + solution_t lp_sol(*problem_ptr); + lp_sol.copy_new_assignment(lp_optimal_solution); + const bool consider_integrality = false; + lp_sol.compute_feasibility(consider_integrality); + if (lp_sol.get_feasible()) { set_new_user_bound(lp_result.get_objective_value()); } } else if (lp_result.get_termination_status() == pdlp_termination_status_t::PrimalInfeasible) { CUOPT_LOG_ERROR("Problem is primal infeasible, continuing anyway!"); diff --git a/cpp/src/mip_heuristics/diversity/lns/rins.cu b/cpp/src/mip_heuristics/diversity/lns/rins.cu index 0112a9c669..c4331343de 100644 --- a/cpp/src/mip_heuristics/diversity/lns/rins.cu +++ b/cpp/src/mip_heuristics/diversity/lns/rins.cu @@ -224,8 +224,7 @@ void rins_t::run_rins() std::vector> rins_solution_queue; - mip_solver_context_t fj_context( - &rins_handle, &fixed_problem, context.settings, context.scaling); + mip_solver_context_t fj_context(&rins_handle, &fixed_problem, context.settings); fj_t fj(fj_context); solution_t fj_solution(fixed_problem); fj_solution.copy_new_assignment(cuopt::host_copy(fixed_assignment, rins_handle.get_stream())); diff --git a/cpp/src/mip_heuristics/diversity/population.cu b/cpp/src/mip_heuristics/diversity/population.cu index 7fa0df5486..bb0fdd6d11 100644 --- a/cpp/src/mip_heuristics/diversity/population.cu +++ b/cpp/src/mip_heuristics/diversity/population.cu @@ -265,11 +265,6 @@ void population_t::invoke_get_solution_callback( f_t user_bound = context.stats.get_solution_bound(); solution_t temp_sol(sol); problem_ptr->post_process_assignment(temp_sol.assignment); - if (context.settings.mip_scaling) { - cuopt_assert(context.scaling != nullptr, ""); - rmm::device_uvector dummy(0, temp_sol.handle_ptr->get_stream()); - context.scaling->unscale_solutions(temp_sol.assignment, dummy); - } if (problem_ptr->has_papilo_presolve_data()) { problem_ptr->papilo_uncrush_assignment(temp_sol.assignment); } @@ -309,10 +304,8 @@ void population_t::run_solution_callbacks(solution_t& sol) invoke_get_solution_callback(sol, get_sol_callback); } } - // save the best objective here, because we might not have been able to return the solution to - // the user because of the unscaling that causes infeasibility. - // This prevents an issue of repaired, or a fully feasible solution being reported in the call - // back in next run. + // Save the best objective here even if callback handling later exits early. + // This prevents older solutions from being reported as "new best" in subsequent callbacks. best_feasible_objective = sol.get_objective(); } @@ -345,10 +338,6 @@ void population_t::run_solution_callbacks(solution_t& sol) incumbent_assignment.size(), sol.handle_ptr->get_stream()); - if (context.settings.mip_scaling) { - cuopt_assert(context.scaling != nullptr, ""); - context.scaling->scale_solutions(incumbent_assignment); - } bool is_valid = problem_ptr->pre_process_assignment(incumbent_assignment); if (!is_valid) { return; } cuopt_assert(outside_sol.assignment.size() == incumbent_assignment.size(), diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index 8175293b98..5a637aae8e 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -14,6 +14,7 @@ #include #include #include +#include namespace cuopt::linear_programming::detail { diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu index 55726421d7..3f77427d87 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu @@ -26,7 +26,7 @@ early_gpufj_t::early_gpufj_t(const optimization_problem_t& o op_problem, settings.get_tolerances(), std::move(incumbent_callback)) { context_ptr_ = std::make_unique>( - &this->handle_, this->problem_ptr_.get(), settings, nullptr); + &this->handle_, this->problem_ptr_.get(), settings); } template diff --git a/cpp/src/mip_heuristics/mip_scaling_strategy.cu b/cpp/src/mip_heuristics/mip_scaling_strategy.cu new file mode 100644 index 0000000000..0aaa606aea --- /dev/null +++ b/cpp/src/mip_heuristics/mip_scaling_strategy.cu @@ -0,0 +1,883 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#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::linear_programming::detail { + +constexpr int row_scaling_max_iterations = 8; +constexpr double row_scaling_min_initial_log2_spread = 12.0; +constexpr int row_scaling_factor_exponent = 5; +constexpr int row_scaling_big_m_soft_factor_exponent = 4; +constexpr double row_scaling_min_factor = + 1.0 / static_cast(std::uint64_t{1} << row_scaling_factor_exponent); +constexpr double row_scaling_max_factor = + static_cast(std::uint64_t{1} << row_scaling_factor_exponent); +constexpr double row_scaling_big_m_soft_min_factor = + 1.0 / static_cast(std::uint64_t{1} << row_scaling_big_m_soft_factor_exponent); +constexpr double row_scaling_big_m_soft_max_factor = 1.0; +constexpr double row_scaling_spread_rel_tol = 1.0e-2; +constexpr double integer_coefficient_rel_tol = 1.0e-6; +constexpr double integer_multiplier_rounding_tolerance = 1.0e-6; +constexpr double min_abs_objective_coefficient_threshold = 1.0e-2; +constexpr double max_obj_scaling_coefficient = 1.0e3; + +constexpr int cumulative_row_scaling_exponent = 8; +constexpr double cumulative_row_scaling_min = + 1.0 / static_cast(std::uint64_t{1} << cumulative_row_scaling_exponent); +constexpr double cumulative_row_scaling_max = + static_cast(std::uint64_t{1} << cumulative_row_scaling_exponent); + +constexpr double post_scaling_max_ratio_warn = 1.0e15; + +constexpr double big_m_abs_threshold = 1.0e4; +constexpr double big_m_ratio_threshold = 1.0e4; + +template +struct abs_value_transform_t { + __device__ f_t operator()(f_t value) const { return raft::abs(value); } +}; + +template +struct nonzero_abs_or_inf_transform_t { + __device__ f_t operator()(f_t value) const + { + const f_t abs_value = raft::abs(value); + return abs_value > f_t(0) ? abs_value : std::numeric_limits::infinity(); + } +}; + +template +struct nonzero_count_transform_t { + __device__ i_t operator()(f_t value) const { return raft::abs(value) > f_t(0) ? i_t(1) : i_t(0); } +}; + +template +struct max_op_t { + __host__ __device__ item_t operator()(const item_t& lhs, const item_t& rhs) const + { + return lhs > rhs ? lhs : rhs; + } +}; + +template +struct min_op_t { + __host__ __device__ item_t operator()(const item_t& lhs, const item_t& rhs) const + { + return lhs < rhs ? lhs : rhs; + } +}; + +struct gcd_op_t { + __host__ __device__ std::int64_t operator()(std::int64_t lhs, std::int64_t rhs) const + { + lhs = lhs < 0 ? -lhs : lhs; + rhs = rhs < 0 ? -rhs : rhs; + if (lhs == 0) { return rhs; } + if (rhs == 0) { return lhs; } + while (rhs != 0) { + const std::int64_t remainder = lhs % rhs; + lhs = rhs; + rhs = remainder; + } + return lhs; + } +}; + +template +struct integer_coeff_for_integer_var_transform_t { + __device__ std::int64_t operator()(thrust::tuple coeff_with_type) const + { + const f_t coefficient = thrust::get<0>(coeff_with_type); + const var_t var_type = thrust::get<1>(coeff_with_type); + if (var_type != var_t::INTEGER) { return std::int64_t{0}; } + + const f_t abs_coefficient = raft::abs(coefficient); + if (!isfinite(abs_coefficient) || abs_coefficient <= f_t(0)) { return std::int64_t{0}; } + + const f_t rounded_abs_coefficient = round(abs_coefficient); + const f_t tolerance_scale = abs_coefficient > f_t(1) ? abs_coefficient : f_t(1); + const f_t integrality_tolerance = + static_cast(integer_coefficient_rel_tol) * tolerance_scale; + if (raft::abs(abs_coefficient - rounded_abs_coefficient) > integrality_tolerance) { + return std::int64_t{0}; + } + if (rounded_abs_coefficient <= f_t(0) || + rounded_abs_coefficient > static_cast(std::numeric_limits::max())) { + return std::int64_t{0}; + } + return static_cast(rounded_abs_coefficient); + } +}; + +template +void compute_row_inf_norm( + const cuopt::linear_programming::optimization_problem_t& op_problem, + rmm::device_uvector& temp_storage, + size_t temp_storage_bytes, + rmm::device_uvector& row_inf_norm, + rmm::cuda_stream_view stream_view) +{ + const auto& matrix_values = op_problem.get_constraint_matrix_values(); + const auto& matrix_offsets = op_problem.get_constraint_matrix_offsets(); + auto coeff_abs_iter = + thrust::make_transform_iterator(matrix_values.data(), abs_value_transform_t{}); + size_t current_bytes = temp_storage_bytes; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(temp_storage.data(), + current_bytes, + coeff_abs_iter, + row_inf_norm.data(), + op_problem.get_n_constraints(), + matrix_offsets.data(), + matrix_offsets.data() + 1, + max_op_t{}, + f_t(0), + stream_view)); +} + +template +void compute_row_integer_gcd( + const cuopt::linear_programming::optimization_problem_t& op_problem, + rmm::device_uvector& temp_storage, + size_t temp_storage_bytes, + rmm::device_uvector& row_integer_gcd, + rmm::cuda_stream_view stream_view) +{ + const auto& matrix_values = op_problem.get_constraint_matrix_values(); + const auto& matrix_indices = op_problem.get_constraint_matrix_indices(); + const auto& matrix_offsets = op_problem.get_constraint_matrix_offsets(); + const auto& variable_types = op_problem.get_variable_types(); + if (variable_types.size() != static_cast(op_problem.get_n_variables())) { + thrust::fill(op_problem.get_handle_ptr()->get_thrust_policy(), + row_integer_gcd.begin(), + row_integer_gcd.end(), + std::int64_t{0}); + return; + } + auto variable_type_per_nnz = + thrust::make_permutation_iterator(variable_types.data(), matrix_indices.data()); + auto coeff_and_type_iter = + thrust::make_zip_iterator(thrust::make_tuple(matrix_values.data(), variable_type_per_nnz)); + auto integer_coeff_iter = thrust::make_transform_iterator( + coeff_and_type_iter, integer_coeff_for_integer_var_transform_t{}); + size_t current_bytes = temp_storage_bytes; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(temp_storage.data(), + current_bytes, + integer_coeff_iter, + row_integer_gcd.data(), + op_problem.get_n_constraints(), + matrix_offsets.data(), + matrix_offsets.data() + 1, + gcd_op_t{}, + std::int64_t{0}, + stream_view)); +} + +template +void compute_big_m_skip_rows( + const cuopt::linear_programming::optimization_problem_t& op_problem, + rmm::device_uvector& temp_storage, + size_t temp_storage_bytes, + rmm::device_uvector& row_inf_norm, + rmm::device_uvector& row_min_nonzero, + rmm::device_uvector& row_nonzero_count, + rmm::device_uvector& row_skip_scaling) +{ + const auto& matrix_values = op_problem.get_constraint_matrix_values(); + const auto& matrix_offsets = op_problem.get_constraint_matrix_offsets(); + const auto stream_view = op_problem.get_handle_ptr()->get_stream(); + auto coeff_abs_iter = + thrust::make_transform_iterator(matrix_values.data(), abs_value_transform_t{}); + auto coeff_nonzero_min_iter = + thrust::make_transform_iterator(matrix_values.data(), nonzero_abs_or_inf_transform_t{}); + auto coeff_nonzero_count_iter = + thrust::make_transform_iterator(matrix_values.data(), nonzero_count_transform_t{}); + + size_t max_bytes = temp_storage_bytes; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(temp_storage.data(), + max_bytes, + coeff_abs_iter, + row_inf_norm.data(), + op_problem.get_n_constraints(), + matrix_offsets.data(), + matrix_offsets.data() + 1, + max_op_t{}, + f_t(0), + stream_view)); + size_t min_bytes = temp_storage_bytes; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(temp_storage.data(), + min_bytes, + coeff_nonzero_min_iter, + row_min_nonzero.data(), + op_problem.get_n_constraints(), + matrix_offsets.data(), + matrix_offsets.data() + 1, + min_op_t{}, + std::numeric_limits::infinity(), + stream_view)); + size_t count_bytes = temp_storage_bytes; + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(temp_storage.data(), + count_bytes, + coeff_nonzero_count_iter, + row_nonzero_count.data(), + op_problem.get_n_constraints(), + matrix_offsets.data(), + matrix_offsets.data() + 1, + thrust::plus{}, + i_t(0), + stream_view)); + + auto row_begin = thrust::make_zip_iterator( + thrust::make_tuple(row_inf_norm.begin(), row_min_nonzero.begin(), row_nonzero_count.begin())); + auto row_end = thrust::make_zip_iterator( + thrust::make_tuple(row_inf_norm.end(), row_min_nonzero.end(), row_nonzero_count.end())); + thrust::transform( + op_problem.get_handle_ptr()->get_thrust_policy(), + row_begin, + row_end, + row_skip_scaling.begin(), + [] __device__(auto row_info) -> i_t { + const f_t row_norm = thrust::get<0>(row_info); + const f_t row_min_non_zero = thrust::get<1>(row_info); + const i_t row_non_zero_size = thrust::get<2>(row_info); + if (row_non_zero_size < i_t(2) || row_min_non_zero >= std::numeric_limits::infinity()) { + return i_t(0); + } + + const f_t row_ratio = row_norm / row_min_non_zero; + return row_norm >= static_cast(big_m_abs_threshold) && + row_ratio >= static_cast(big_m_ratio_threshold) + ? i_t(1) + : i_t(0); + }); +} + +template +void scale_objective(cuopt::linear_programming::optimization_problem_t& op_problem) +{ + auto& obj_coefficients = op_problem.get_objective_coefficients(); + const i_t n_cols = op_problem.get_n_variables(); + if (n_cols == 0) { return; } + + const auto* handle_ptr = op_problem.get_handle_ptr(); + + f_t min_abs_obj = thrust::transform_reduce(handle_ptr->get_thrust_policy(), + obj_coefficients.begin(), + obj_coefficients.end(), + nonzero_abs_or_inf_transform_t{}, + std::numeric_limits::infinity(), + min_op_t{}); + + f_t max_abs_obj = thrust::transform_reduce(handle_ptr->get_thrust_policy(), + obj_coefficients.begin(), + obj_coefficients.end(), + abs_value_transform_t{}, + f_t(0), + max_op_t{}); + + if (!std::isfinite(static_cast(min_abs_obj)) || min_abs_obj <= f_t(0) || + max_abs_obj <= f_t(0)) { + CUOPT_LOG_INFO("MIP_OBJ_SCALING skipped: no finite nonzero objective coefficients"); + return; + } + + if (static_cast(min_abs_obj) >= min_abs_objective_coefficient_threshold) { + CUOPT_LOG_INFO("MIP_OBJ_SCALING skipped: min_abs_coeff=%g already above threshold=%g", + static_cast(min_abs_obj), + min_abs_objective_coefficient_threshold); + return; + } + + double raw_scale = min_abs_objective_coefficient_threshold / static_cast(min_abs_obj); + double scale = std::min(raw_scale, max_obj_scaling_coefficient); + + double post_max = static_cast(max_abs_obj) * scale; + if (post_max > 1.0e6) { + CUOPT_LOG_INFO("MIP_OBJ_SCALING skipped: would push max_coeff from %g to %g (limit 1e6)", + static_cast(max_abs_obj), + post_max); + return; + } + + f_t scale_f = static_cast(scale); + thrust::transform(handle_ptr->get_thrust_policy(), + obj_coefficients.begin(), + obj_coefficients.end(), + obj_coefficients.begin(), + [scale_f] __device__(f_t c) -> f_t { return c * scale_f; }); + + f_t old_sf = op_problem.get_objective_scaling_factor(); + f_t old_off = op_problem.get_objective_offset(); + op_problem.set_objective_scaling_factor(old_sf / scale_f); + op_problem.set_objective_offset(old_off * scale_f); + + CUOPT_LOG_INFO( + "MIP_OBJ_SCALING applied: min_abs_coeff=%g max_abs_coeff=%g scale=%g new_scaling_factor=%g", + static_cast(min_abs_obj), + static_cast(max_abs_obj), + scale, + static_cast(old_sf / scale_f)); +} + +template +rmm::device_uvector capture_pre_scaling_integer_gcd( + const cuopt::linear_programming::optimization_problem_t& op_problem, + rmm::device_uvector& temp_storage, + size_t temp_storage_bytes, + rmm::cuda_stream_view stream_view) +{ + const i_t n_rows = op_problem.get_n_constraints(); + rmm::device_uvector gcd(static_cast(n_rows), stream_view); + compute_row_integer_gcd(op_problem, temp_storage, temp_storage_bytes, gcd, stream_view); + return gcd; +} + +template +void assert_integer_coefficient_integrality( + const cuopt::linear_programming::optimization_problem_t& op_problem, + rmm::device_uvector& temp_storage, + size_t temp_storage_bytes, + const rmm::device_uvector& pre_scaling_gcd, + rmm::cuda_stream_view stream_view) +{ + const auto* handle_ptr = op_problem.get_handle_ptr(); + const i_t n_rows = op_problem.get_n_constraints(); + rmm::device_uvector post_scaling_gcd(static_cast(n_rows), stream_view); + compute_row_integer_gcd( + op_problem, temp_storage, temp_storage_bytes, post_scaling_gcd, stream_view); + + i_t broken_rows = thrust::inner_product( + handle_ptr->get_thrust_policy(), + pre_scaling_gcd.begin(), + pre_scaling_gcd.end(), + post_scaling_gcd.begin(), + i_t(0), + thrust::plus{}, + [] __device__(std::int64_t pre_gcd, std::int64_t post_gcd) -> i_t { + return (pre_gcd > std::int64_t{0} && post_gcd == std::int64_t{0}) ? i_t(1) : i_t(0); + }); + + if (broken_rows > 0) { + CUOPT_LOG_WARN("MIP row scaling: %d rows lost integer coefficient integrality after scaling", + broken_rows); + } + cuopt_assert(broken_rows == 0, + "MIP scaling must preserve integer coefficients for integer variables"); +} + +template +mip_scaling_strategy_t::mip_scaling_strategy_t( + typename mip_scaling_strategy_t::optimization_problem_type_t& op_problem_scaled) + : handle_ptr_(op_problem_scaled.get_handle_ptr()), + stream_view_(handle_ptr_->get_stream()), + op_problem_scaled_(op_problem_scaled) +{ +} + +template +size_t dry_run_cub(const cuopt::linear_programming::optimization_problem_t& op_problem, + i_t n_rows, + rmm::device_uvector& row_inf_norm, + rmm::device_uvector& row_min_nonzero, + rmm::device_uvector& row_nonzero_count, + rmm::device_uvector& row_integer_gcd, + rmm::cuda_stream_view stream_view) +{ + const auto& matrix_values = op_problem.get_constraint_matrix_values(); + const auto& matrix_indices = op_problem.get_constraint_matrix_indices(); + const auto& matrix_offsets = op_problem.get_constraint_matrix_offsets(); + const auto& variable_types = op_problem.get_variable_types(); + size_t temp_storage_bytes = 0; + size_t current_required_bytes = 0; + + auto coeff_abs_iter = + thrust::make_transform_iterator(matrix_values.data(), abs_value_transform_t{}); + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(nullptr, + current_required_bytes, + coeff_abs_iter, + row_inf_norm.data(), + n_rows, + matrix_offsets.data(), + matrix_offsets.data() + 1, + max_op_t{}, + f_t(0), + stream_view)); + temp_storage_bytes = std::max(temp_storage_bytes, current_required_bytes); + + auto coeff_nonzero_min_iter = + thrust::make_transform_iterator(matrix_values.data(), nonzero_abs_or_inf_transform_t{}); + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(nullptr, + current_required_bytes, + coeff_nonzero_min_iter, + row_min_nonzero.data(), + n_rows, + matrix_offsets.data(), + matrix_offsets.data() + 1, + min_op_t{}, + std::numeric_limits::infinity(), + stream_view)); + temp_storage_bytes = std::max(temp_storage_bytes, current_required_bytes); + + auto coeff_nonzero_count_iter = + thrust::make_transform_iterator(matrix_values.data(), nonzero_count_transform_t{}); + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(nullptr, + current_required_bytes, + coeff_nonzero_count_iter, + row_nonzero_count.data(), + n_rows, + matrix_offsets.data(), + matrix_offsets.data() + 1, + thrust::plus{}, + i_t(0), + stream_view)); + temp_storage_bytes = std::max(temp_storage_bytes, current_required_bytes); + + if (variable_types.size() == static_cast(op_problem.get_n_variables())) { + auto variable_type_per_nnz = + thrust::make_permutation_iterator(variable_types.data(), matrix_indices.data()); + auto coeff_and_type_iter = + thrust::make_zip_iterator(thrust::make_tuple(matrix_values.data(), variable_type_per_nnz)); + auto integer_coeff_iter = thrust::make_transform_iterator( + coeff_and_type_iter, integer_coeff_for_integer_var_transform_t{}); + RAFT_CUDA_TRY(cub::DeviceSegmentedReduce::Reduce(nullptr, + current_required_bytes, + integer_coeff_iter, + row_integer_gcd.data(), + n_rows, + matrix_offsets.data(), + matrix_offsets.data() + 1, + gcd_op_t{}, + std::int64_t{0}, + stream_view)); + temp_storage_bytes = std::max(temp_storage_bytes, current_required_bytes); + } + + return temp_storage_bytes; +} + +template +void mip_scaling_strategy_t::scale_problem(bool do_objective_scaling) +{ + raft::common::nvtx::range fun_scope("mip_scale_problem"); + + auto& matrix_values = op_problem_scaled_.get_constraint_matrix_values(); + auto& matrix_offsets = op_problem_scaled_.get_constraint_matrix_offsets(); + auto& constraint_bounds = op_problem_scaled_.get_constraint_bounds(); + auto& constraint_lower_bounds = op_problem_scaled_.get_constraint_lower_bounds(); + auto& constraint_upper_bounds = op_problem_scaled_.get_constraint_upper_bounds(); + const i_t n_rows = op_problem_scaled_.get_n_constraints(); + const i_t n_cols = op_problem_scaled_.get_n_variables(); + const i_t nnz = op_problem_scaled_.get_nnz(); + + if (do_objective_scaling) { + scale_objective(op_problem_scaled_); + } else { + CUOPT_LOG_INFO("MIP_OBJ_SCALING skipped: disabled by user setting"); + } + + if (n_rows == 0 || nnz <= 0) { return; } + cuopt_assert(constraint_bounds.size() == size_t{0} || + constraint_bounds.size() == static_cast(n_rows), + "constraint_bounds must be empty or have one value per constraint"); + + rmm::device_uvector row_inf_norm(static_cast(n_rows), stream_view_); + rmm::device_uvector row_min_nonzero(static_cast(n_rows), stream_view_); + rmm::device_uvector row_nonzero_count(static_cast(n_rows), stream_view_); + rmm::device_uvector row_integer_gcd(static_cast(n_rows), stream_view_); + rmm::device_uvector row_rhs_magnitude(static_cast(n_rows), stream_view_); + rmm::device_uvector row_skip_scaling(static_cast(n_rows), stream_view_); + thrust::fill( + handle_ptr_->get_thrust_policy(), row_skip_scaling.begin(), row_skip_scaling.end(), i_t(0)); + rmm::device_uvector iteration_scaling(static_cast(n_rows), stream_view_); + rmm::device_uvector cumulative_scaling(static_cast(n_rows), stream_view_); + thrust::fill( + handle_ptr_->get_thrust_policy(), cumulative_scaling.begin(), cumulative_scaling.end(), f_t(1)); + rmm::device_uvector coefficient_row_index(static_cast(nnz), stream_view_); + rmm::device_uvector ref_log2_values(static_cast(n_rows), stream_view_); + + thrust::upper_bound(handle_ptr_->get_thrust_policy(), + matrix_offsets.begin(), + matrix_offsets.end(), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(nnz), + coefficient_row_index.begin()); + thrust::transform( + handle_ptr_->get_thrust_policy(), + coefficient_row_index.begin(), + coefficient_row_index.end(), + coefficient_row_index.begin(), + [] __device__(i_t row_upper_bound_idx) -> i_t { return row_upper_bound_idx - 1; }); + + size_t temp_storage_bytes = dry_run_cub(op_problem_scaled_, + n_rows, + row_inf_norm, + row_min_nonzero, + row_nonzero_count, + row_integer_gcd, + stream_view_); + + rmm::device_uvector temp_storage(temp_storage_bytes, stream_view_); + + cuopt_func_call(auto pre_scaling_gcd = capture_pre_scaling_integer_gcd( + op_problem_scaled_, temp_storage, temp_storage_bytes, stream_view_)); + + compute_big_m_skip_rows(op_problem_scaled_, + temp_storage, + temp_storage_bytes, + row_inf_norm, + row_min_nonzero, + row_nonzero_count, + row_skip_scaling); + + i_t big_m_rows = thrust::count( + handle_ptr_->get_thrust_policy(), row_skip_scaling.begin(), row_skip_scaling.end(), i_t(1)); + + CUOPT_LOG_INFO("MIP row scaling start: rows=%d cols=%d max_iterations=%d soft_big_m_rows=%d", + n_rows, + n_cols, + row_scaling_max_iterations, + big_m_rows); + + f_t original_max_coeff = thrust::transform_reduce(handle_ptr_->get_thrust_policy(), + matrix_values.begin(), + matrix_values.end(), + abs_value_transform_t{}, + f_t(0), + max_op_t{}); + + double previous_row_log2_spread = std::numeric_limits::infinity(); + for (int iteration = 0; iteration < row_scaling_max_iterations; ++iteration) { + compute_row_inf_norm( + op_problem_scaled_, temp_storage, temp_storage_bytes, row_inf_norm, stream_view_); + compute_row_integer_gcd( + op_problem_scaled_, temp_storage, temp_storage_bytes, row_integer_gcd, stream_view_); + + using row_stats_t = thrust::tuple; + auto row_norm_log2_stats = thrust::transform_reduce( + handle_ptr_->get_thrust_policy(), + row_inf_norm.begin(), + row_inf_norm.end(), + [] __device__(f_t row_norm) -> row_stats_t { + if (row_norm == f_t(0)) { + return {0.0, + 0.0, + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + } + const double row_log2 = log2(static_cast(row_norm)); + return {row_log2, 1.0, row_log2, row_log2}; + }, + row_stats_t{0.0, + 0.0, + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}, + [] __device__(row_stats_t a, row_stats_t b) -> row_stats_t { + return {thrust::get<0>(a) + thrust::get<0>(b), + thrust::get<1>(a) + thrust::get<1>(b), + min_op_t{}(thrust::get<2>(a), thrust::get<2>(b)), + max_op_t{}(thrust::get<3>(a), thrust::get<3>(b))}; + }); + const i_t active_row_count = static_cast(thrust::get<1>(row_norm_log2_stats)); + if (active_row_count == 0) { break; } + const double row_log2_spread = + thrust::get<3>(row_norm_log2_stats) - thrust::get<2>(row_norm_log2_stats); + if (iteration == 0 && row_log2_spread <= row_scaling_min_initial_log2_spread) { + CUOPT_LOG_INFO("MIP row scaling skipped: initial_log2_spread=%g threshold=%g", + row_log2_spread, + row_scaling_min_initial_log2_spread); + break; + } + if (std::isfinite(previous_row_log2_spread)) { + const double spread_improvement = previous_row_log2_spread - row_log2_spread; + if (spread_improvement <= + row_scaling_spread_rel_tol * std::max(1.0, previous_row_log2_spread)) { + break; + } + } + previous_row_log2_spread = row_log2_spread; + + thrust::transform(handle_ptr_->get_thrust_policy(), + thrust::make_zip_iterator(thrust::make_tuple( + constraint_lower_bounds.begin(), constraint_upper_bounds.begin())), + thrust::make_zip_iterator(thrust::make_tuple(constraint_lower_bounds.end(), + constraint_upper_bounds.end())), + row_rhs_magnitude.begin(), + [] __device__(auto row_bounds) -> f_t { + const f_t lower_bound = thrust::get<0>(row_bounds); + const f_t upper_bound = thrust::get<1>(row_bounds); + f_t rhs_norm = f_t(0); + if (isfinite(lower_bound)) { rhs_norm = raft::abs(lower_bound); } + if (isfinite(upper_bound)) { + const f_t upper_abs = raft::abs(upper_bound); + rhs_norm = upper_abs > rhs_norm ? upper_abs : rhs_norm; + } + return rhs_norm; + }); + + constexpr double neg_inf_sentinel = -1.0e300; + thrust::transform(handle_ptr_->get_thrust_policy(), + thrust::make_zip_iterator(thrust::make_tuple( + row_inf_norm.begin(), row_rhs_magnitude.begin(), row_skip_scaling.begin())), + thrust::make_zip_iterator(thrust::make_tuple( + row_inf_norm.end(), row_rhs_magnitude.end(), row_skip_scaling.end())), + ref_log2_values.begin(), + [] __device__(auto row_info) -> double { + const f_t row_norm = thrust::get<0>(row_info); + const f_t rhs_norm = thrust::get<1>(row_info); + const i_t is_big_m = thrust::get<2>(row_info); + if (is_big_m) { return -std::numeric_limits::infinity(); } + if (rhs_norm == f_t(0)) { return -std::numeric_limits::infinity(); } + if (row_norm <= f_t(0)) { return -std::numeric_limits::infinity(); } + return log2(static_cast(row_norm)); + }); + thrust::sort(handle_ptr_->get_thrust_policy(), ref_log2_values.begin(), ref_log2_values.end()); + auto valid_begin_iter = thrust::lower_bound(handle_ptr_->get_thrust_policy(), + ref_log2_values.begin(), + ref_log2_values.end(), + neg_inf_sentinel); + i_t n_invalid = static_cast(valid_begin_iter - ref_log2_values.begin()); + i_t valid_count = n_rows - n_invalid; + if (valid_count == 0) { break; } + i_t median_idx = n_invalid + valid_count / 2; + double h_median_log2; + RAFT_CUDA_TRY(cudaMemcpyAsync(&h_median_log2, + ref_log2_values.data() + median_idx, + sizeof(double), + cudaMemcpyDeviceToHost, + stream_view_)); + handle_ptr_->sync_stream(); + f_t target_norm = static_cast(exp2(h_median_log2)); + cuopt_assert(std::isfinite(static_cast(target_norm)), "target_norm must be finite"); + cuopt_assert(target_norm > f_t(0), "target_norm must be positive"); + + thrust::transform( + handle_ptr_->get_thrust_policy(), + thrust::make_zip_iterator(thrust::make_tuple(row_inf_norm.begin(), + row_skip_scaling.begin(), + row_integer_gcd.begin(), + cumulative_scaling.begin(), + row_rhs_magnitude.begin())), + thrust::make_zip_iterator(thrust::make_tuple(row_inf_norm.end(), + row_skip_scaling.end(), + row_integer_gcd.end(), + cumulative_scaling.end(), + row_rhs_magnitude.end())), + iteration_scaling.begin(), + [target_norm] __device__(auto row_info) -> f_t { + const f_t row_norm = thrust::get<0>(row_info); + const i_t is_big_m = thrust::get<1>(row_info); + const std::int64_t row_coeff_gcd = thrust::get<2>(row_info); + const f_t cum_scale = thrust::get<3>(row_info); + const f_t rhs_norm = thrust::get<4>(row_info); + if (row_norm == f_t(0)) { return f_t(1); } + if (rhs_norm == f_t(0)) { return f_t(1); } + + const f_t desired_scaling = target_norm / row_norm; + if (!isfinite(desired_scaling) || desired_scaling <= f_t(0)) { return f_t(1); } + + f_t min_scaling = is_big_m ? static_cast(row_scaling_big_m_soft_min_factor) + : static_cast(row_scaling_min_factor); + f_t max_scaling = is_big_m ? static_cast(row_scaling_big_m_soft_max_factor) + : static_cast(row_scaling_max_factor); + + if (!is_big_m && row_norm >= static_cast(big_m_abs_threshold)) { + if (max_scaling > f_t(1)) { max_scaling = f_t(1); } + } + + const f_t cum_lower = static_cast(cumulative_row_scaling_min) / cum_scale; + const f_t cum_upper = static_cast(cumulative_row_scaling_max) / cum_scale; + if (cum_lower > min_scaling) { min_scaling = cum_lower; } + if (cum_upper < max_scaling) { max_scaling = cum_upper; } + if (min_scaling > max_scaling) { return f_t(1); } + + f_t row_scaling = desired_scaling; + if (row_scaling < min_scaling) { row_scaling = min_scaling; } + if (row_scaling > max_scaling) { row_scaling = max_scaling; } + + // Fix E: prefer power-of-two scaling for integer rows (exact in IEEE 754) + if (row_coeff_gcd > std::int64_t{0}) { + const f_t gcd_value = static_cast(row_coeff_gcd); + if (isfinite(gcd_value) && gcd_value > f_t(0)) { + const double log2_scaling = log2(static_cast(row_scaling)); + int k_candidates[3] = {static_cast(round(log2_scaling)), + static_cast(floor(log2_scaling)), + static_cast(ceil(log2_scaling))}; + bool found_pow2 = false; + for (int ci = 0; ci < 3 && !found_pow2; ++ci) { + int k = k_candidates[ci]; + f_t pow2 = static_cast(exp2(static_cast(k))); + if (pow2 < min_scaling || pow2 > max_scaling) { continue; } + bool preserves = + (k >= 0) || (-k < 63 && (row_coeff_gcd % (std::int64_t{1} << (-k))) == 0); + if (preserves) { + row_scaling = pow2; + found_pow2 = true; + } + } + if (!found_pow2) { + std::int64_t min_mult = static_cast( + ceil(static_cast(min_scaling * gcd_value - + static_cast(integer_multiplier_rounding_tolerance)))); + std::int64_t max_mult = static_cast(floor( + static_cast(max_scaling * gcd_value + + static_cast(integer_multiplier_rounding_tolerance)))); + if (min_mult < std::int64_t{1}) { min_mult = std::int64_t{1}; } + if (max_mult < min_mult) { max_mult = min_mult; } + std::int64_t proj_mult = static_cast(round(row_scaling * gcd_value)); + if (proj_mult < min_mult) { proj_mult = min_mult; } + if (proj_mult > max_mult) { proj_mult = max_mult; } + row_scaling = static_cast(proj_mult) / gcd_value; + } + } + } + return row_scaling; + }); + + i_t scaled_rows = + thrust::count_if(handle_ptr_->get_thrust_policy(), + iteration_scaling.begin(), + iteration_scaling.end(), + [] __device__(f_t row_scale) -> bool { return row_scale != f_t(1); }); + CUOPT_LOG_INFO( + "MIP_SCALING_METRICS iteration=%d log2_spread=%g target_norm=%g scaled_rows=%d " + "valid_rows=%d", + iteration, + row_log2_spread, + static_cast(target_norm), + scaled_rows, + valid_count); + if (scaled_rows == 0) { break; } + + f_t predicted_max = thrust::inner_product(handle_ptr_->get_thrust_policy(), + row_inf_norm.begin(), + row_inf_norm.end(), + iteration_scaling.begin(), + f_t(0), + max_op_t{}, + thrust::multiplies{}); + if (predicted_max > original_max_coeff) { + CUOPT_LOG_INFO("MIP_SCALING magnitude guard: predicted_max=%g > original_max=%g, stopping", + static_cast(predicted_max), + static_cast(original_max_coeff)); + break; + } + + thrust::transform( + handle_ptr_->get_thrust_policy(), + matrix_values.begin(), + matrix_values.end(), + thrust::make_permutation_iterator(iteration_scaling.begin(), coefficient_row_index.begin()), + matrix_values.begin(), + thrust::multiplies{}); + + thrust::transform(handle_ptr_->get_thrust_policy(), + cumulative_scaling.begin(), + cumulative_scaling.end(), + iteration_scaling.begin(), + cumulative_scaling.begin(), + thrust::multiplies{}); + + thrust::transform(handle_ptr_->get_thrust_policy(), + constraint_lower_bounds.begin(), + constraint_lower_bounds.end(), + iteration_scaling.begin(), + constraint_lower_bounds.begin(), + thrust::multiplies{}); + thrust::transform(handle_ptr_->get_thrust_policy(), + constraint_upper_bounds.begin(), + constraint_upper_bounds.end(), + iteration_scaling.begin(), + constraint_upper_bounds.begin(), + thrust::multiplies{}); + if (constraint_bounds.size() == static_cast(n_rows)) { + thrust::transform(handle_ptr_->get_thrust_policy(), + constraint_bounds.begin(), + constraint_bounds.end(), + iteration_scaling.begin(), + constraint_bounds.begin(), + thrust::multiplies{}); + } + } + + CUOPT_LOG_INFO("MIP_SCALING_SUMMARY rows=%d bigm_rows=%d final_spread=%g", + n_rows, + big_m_rows, + previous_row_log2_spread); + + cuopt_func_call(assert_integer_coefficient_integrality( + op_problem_scaled_, temp_storage, temp_storage_bytes, pre_scaling_gcd, stream_view_)); + + const f_t post_max_coeff = thrust::transform_reduce(handle_ptr_->get_thrust_policy(), + matrix_values.begin(), + matrix_values.end(), + abs_value_transform_t{}, + f_t(0), + max_op_t{}); + const f_t post_min_nonzero_coeff = thrust::transform_reduce(handle_ptr_->get_thrust_policy(), + matrix_values.begin(), + matrix_values.end(), + nonzero_abs_or_inf_transform_t{}, + std::numeric_limits::infinity(), + min_op_t{}); + if (std::isfinite(static_cast(post_max_coeff)) && + std::isfinite(static_cast(post_min_nonzero_coeff)) && + post_min_nonzero_coeff > f_t(0)) { + const double post_ratio = + static_cast(post_max_coeff) / static_cast(post_min_nonzero_coeff); + if (post_ratio > post_scaling_max_ratio_warn) { + CUOPT_LOG_WARN( + "MIP row scaling: extreme coefficient ratio after scaling: max=%g min_nz=%g ratio=%g", + static_cast(post_max_coeff), + static_cast(post_min_nonzero_coeff), + post_ratio); + } + } + + CUOPT_LOG_INFO("MIP row scaling completed"); + op_problem_scaled_.print_scaling_information(); +} + +#define INSTANTIATE(F_TYPE) template class mip_scaling_strategy_t; + +#if MIP_INSTANTIATE_FLOAT +INSTANTIATE(float) +#endif + +#if MIP_INSTANTIATE_DOUBLE +INSTANTIATE(double) +#endif + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip_heuristics/mip_scaling_strategy.cuh b/cpp/src/mip_heuristics/mip_scaling_strategy.cuh new file mode 100644 index 0000000000..63d88dbec6 --- /dev/null +++ b/cpp/src/mip_heuristics/mip_scaling_strategy.cuh @@ -0,0 +1,32 @@ +/* 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 + +namespace cuopt::linear_programming::detail { + +template +class mip_scaling_strategy_t { + public: + using optimization_problem_type_t = cuopt::linear_programming::optimization_problem_t; + explicit mip_scaling_strategy_t(optimization_problem_type_t& op_problem_scaled); + + void scale_problem(bool scale_objective = true); + + private: + raft::handle_t const* handle_ptr_{nullptr}; + rmm::cuda_stream_view stream_view_; + optimization_problem_type_t& op_problem_scaled_; +}; + +} // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp index bf93b54e26..d94cf5aa67 100644 --- a/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp +++ b/cpp/src/mip_heuristics/presolve/third_party_presolve.cpp @@ -739,7 +739,10 @@ third_party_presolve_result_t third_party_presolve_t::apply( auto opt_problem = build_optimization_problem( papilo_problem, op_problem.get_handle_ptr(), category, maximize_); + // metadata from original optimization problem that is not filled opt_problem.set_problem_name(op_problem.get_problem_name()); + opt_problem.set_objective_scaling_factor(op_problem.get_objective_scaling_factor()); + // when an objective offset outside (e.g. from mps file), handle accordingly auto col_flags = papilo_problem.getColFlags(); std::vector implied_integer_indices; for (size_t i = 0; i < col_flags.size(); i++) { diff --git a/cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu b/cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu index e2bbc8feb1..84415f5372 100644 --- a/cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu +++ b/cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu @@ -8,7 +8,6 @@ #include "relaxed_lp.cuh" #include -#include #include #include #include diff --git a/cpp/src/mip_heuristics/solution/solution.cu b/cpp/src/mip_heuristics/solution/solution.cu index 531d54372c..daa12b4f7b 100644 --- a/cpp/src/mip_heuristics/solution/solution.cu +++ b/cpp/src/mip_heuristics/solution/solution.cu @@ -322,7 +322,7 @@ f_t solution_t::compute_l2_residual() } template -bool solution_t::compute_feasibility() +bool solution_t::compute_feasibility(bool consider_integrality) { n_feasible_constraints.set_value_to_zero_async(handle_ptr->get_stream()); compute_constraints(); @@ -330,7 +330,8 @@ bool solution_t::compute_feasibility() compute_infeasibility(); compute_number_of_integers(); i_t h_n_feas_constraints = n_feasible_constraints.value(handle_ptr->get_stream()); - is_feasible = h_n_feas_constraints == problem_ptr->n_constraints && test_number_all_integer(); + is_feasible = h_n_feas_constraints == problem_ptr->n_constraints; + if (consider_integrality) { is_feasible = is_feasible && test_number_all_integer(); } CUOPT_LOG_TRACE("is_feasible %d n_feasible_cstr %d all_cstr %d", is_feasible, h_n_feas_constraints, diff --git a/cpp/src/mip_heuristics/solution/solution.cuh b/cpp/src/mip_heuristics/solution/solution.cuh index f6c2c2f802..9de10ed980 100644 --- a/cpp/src/mip_heuristics/solution/solution.cuh +++ b/cpp/src/mip_heuristics/solution/solution.cuh @@ -58,7 +58,7 @@ class solution_t { // makes the approximate integer values up to INTEGRALITY TOLERANCE whole integers void correct_integer_precision(); // does a reduction and returns if the current solution is feasible - bool compute_feasibility(); + bool compute_feasibility(bool consider_integrality = true); // sets the is_feasible flag to 1 void set_feasible(); // sets the is_feasible flag to 0 diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index cd7c822ef1..4e9cd6a2a5 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -11,13 +11,13 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include #include @@ -87,12 +87,6 @@ mip_solution_t run_mip(detail::problem_t& problem, { try { raft::common::nvtx::range fun_scope("run_mip"); - auto constexpr const running_mip = true; - - // TODO ask Akif and Alice how was this passed down? - auto hyper_params = settings.hyper_params; - hyper_params.update_primal_weight_on_initial_solution = false; - hyper_params.update_step_size_on_initial_solution = true; if (settings.get_mip_callbacks().size() > 0) { auto callback_num_variables = problem.original_problem_ptr->get_n_variables(); if (problem.has_papilo_presolve_data()) { @@ -147,7 +141,7 @@ mip_solution_t run_mip(detail::problem_t& problem, } // problem contains unpreprocessed data detail::problem_t scaled_problem(problem); - + cuopt_func_call(auto saved_problem = scaled_problem); CUOPT_LOG_INFO("Objective offset %f scaling_factor %f", problem.presolve_data.objective_offset, problem.presolve_data.objective_scaling_factor); @@ -156,34 +150,12 @@ mip_solution_t run_mip(detail::problem_t& problem, "Size mismatch"); cuopt_assert(problem.original_problem_ptr->get_n_constraints() == scaled_problem.n_constraints, "Size mismatch"); - detail::pdlp_initial_scaling_strategy_t scaling( - scaled_problem.handle_ptr, - scaled_problem, - hyper_params.default_l_inf_ruiz_iterations, - (f_t)hyper_params.default_alpha_pock_chambolle_rescaling, - scaled_problem.reverse_coefficients, - scaled_problem.reverse_offsets, - scaled_problem.reverse_constraints, - nullptr, - hyper_params, - running_mip); - - cuopt_func_call(auto saved_problem = scaled_problem); - if (settings.mip_scaling) { - scaling.scale_problem(); - if (settings.initial_solutions.size() > 0) { - for (const auto& initial_solution : settings.initial_solutions) { - scaling.scale_primal(*initial_solution); - } - } - } // only call preprocess on scaled problem, so we can compute feasibility on the original problem scaled_problem.preprocess_problem(); - // cuopt_func_call((check_scaled_problem(scaled_problem, saved_problem))); scaled_problem.related_vars_time_limit = settings.heuristic_params.related_vars_time_limit; detail::trivial_presolve(scaled_problem); - detail::mip_solver_t solver(scaled_problem, settings, scaling, timer); + detail::mip_solver_t solver(scaled_problem, settings, timer); // initial_cutoff is in user-space (representation-invariant). // It will be converted to the target solver-space at each consumption point. solver.context.initial_cutoff = initial_cutoff; @@ -229,22 +201,21 @@ mip_solution_t run_mip(detail::problem_t& problem, CUOPT_LOG_DEBUG("Started early CPUFJ on papilo-presolved problem during cuOpt presolve"); } - auto scaled_sol = solver.run_solver(); - bool is_feasible_before_scaling = scaled_sol.get_feasible(); - scaled_sol.problem_ptr = &problem; - - if (settings.mip_scaling) { scaling.unscale_solutions(scaled_sol); } + auto presolved_sol = solver.run_solver(); + bool is_feasible_on_presolved = presolved_sol.get_feasible(); + presolved_sol.problem_ptr = &problem; // at this point we need to compute the feasibility on the original problem not the presolved // one - bool is_feasible_after_unscaling = scaled_sol.compute_feasibility(); - if (!scaled_problem.empty && is_feasible_before_scaling != is_feasible_after_unscaling) { + bool is_feasible_on_original = presolved_sol.compute_feasibility(); + if (!scaled_problem.empty && is_feasible_on_presolved != is_feasible_on_original) { CUOPT_LOG_WARN( - "The feasibility does not match on scaled and unscaled problems. To overcome this issue, " + "The feasibility does not match on presolved and original problems. To overcome this " + "issue, " "please provide a more numerically stable problem."); } - auto sol = scaled_sol.get_solution( - is_feasible_before_scaling || is_feasible_after_unscaling, solver.get_solver_stats(), false); + auto sol = presolved_sol.get_solution( + is_feasible_on_presolved || is_feasible_on_original, solver.get_solver_stats(), false); int hidesol = std::getenv("CUOPT_MIP_HIDE_SOLUTION") ? atoi(std::getenv("CUOPT_MIP_HIDE_SOLUTION")) : 0; @@ -313,7 +284,10 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, } auto timer = timer_t(time_limit); - + if (settings.mip_scaling != CUOPT_MIP_SCALING_OFF) { + detail::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; diff --git a/cpp/src/mip_heuristics/solver.cu b/cpp/src/mip_heuristics/solver.cu index a953ad6b7d..0bbf48d95e 100644 --- a/cpp/src/mip_heuristics/solver.cu +++ b/cpp/src/mip_heuristics/solver.cu @@ -42,14 +42,10 @@ static void init_handler(const raft::handle_t* handle_ptr) template mip_solver_t::mip_solver_t(const problem_t& op_problem, const mip_solver_settings_t& solver_settings, - pdlp_initial_scaling_strategy_t& scaling, timer_t timer) : op_problem_(op_problem), solver_settings_(solver_settings), - context(op_problem.handle_ptr, - const_cast*>(&op_problem), - solver_settings, - &scaling), + context(op_problem.handle_ptr, const_cast*>(&op_problem), solver_settings), timer_(timer) { init_handler(op_problem.handle_ptr); diff --git a/cpp/src/mip_heuristics/solver.cuh b/cpp/src/mip_heuristics/solver.cuh index 1b5fe17244..9b9024a1dc 100644 --- a/cpp/src/mip_heuristics/solver.cuh +++ b/cpp/src/mip_heuristics/solver.cuh @@ -20,7 +20,6 @@ class mip_solver_t { public: explicit mip_solver_t(const problem_t& op_problem, const mip_solver_settings_t& solver_settings, - pdlp_initial_scaling_strategy_t& scaling, timer_t timer); solution_t run_solver(); diff --git a/cpp/src/mip_heuristics/solver_context.cuh b/cpp/src/mip_heuristics/solver_context.cuh index 8fa852609b..3ea7377e15 100644 --- a/cpp/src/mip_heuristics/solver_context.cuh +++ b/cpp/src/mip_heuristics/solver_context.cuh @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -37,9 +36,8 @@ template struct mip_solver_context_t { explicit mip_solver_context_t(raft::handle_t const* handle_ptr_, problem_t* problem_ptr_, - mip_solver_settings_t settings_, - pdlp_initial_scaling_strategy_t* scaling) - : handle_ptr(handle_ptr_), problem_ptr(problem_ptr_), settings(settings_), scaling(scaling) + mip_solver_settings_t settings_) + : handle_ptr(handle_ptr_), problem_ptr(problem_ptr_), settings(settings_) { cuopt_assert(problem_ptr != nullptr, "problem_ptr is nullptr"); stats.set_solution_bound(problem_ptr->maximize ? std::numeric_limits::infinity() @@ -56,7 +54,6 @@ struct mip_solver_context_t { diversity_manager_t* diversity_manager_ptr{nullptr}; std::atomic preempt_heuristic_solver_ = false; const mip_solver_settings_t settings; - pdlp_initial_scaling_strategy_t* scaling; // nullptr when not available (early FJ) solver_stats_t stats; // Work limit context for tracking work units in deterministic mode (shared across all timers in // GPU heuristic loop) diff --git a/cpp/tests/mip/feasibility_jump_tests.cu b/cpp/tests/mip/feasibility_jump_tests.cu index baa3e9b803..4e8a518522 100644 --- a/cpp/tests/mip/feasibility_jump_tests.cu +++ b/cpp/tests/mip/feasibility_jump_tests.cu @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -77,16 +78,7 @@ static fj_state_t run_fj(std::string test_instance, // run the problem constructor of MIP, so that we do bounds standardization detail::problem_t problem(op_problem); problem.preprocess_problem(); - detail::pdhg_solver_t pdhg_solver(problem.handle_ptr, problem); - detail::pdlp_initial_scaling_strategy_t scaling(&handle_, - problem, - 10, - 1.0, - pdhg_solver, - problem.reverse_coefficients, - problem.reverse_offsets, - problem.reverse_constraints, - true); + detail::mip_scaling_strategy_t scaling(problem); auto settings = mip_solver_settings_t{}; settings.time_limit = 30.; diff --git a/cpp/tests/mip/load_balancing_test.cu b/cpp/tests/mip/load_balancing_test.cu index 5e2f08007d..1f825a26f7 100644 --- a/cpp/tests/mip/load_balancing_test.cu +++ b/cpp/tests/mip/load_balancing_test.cu @@ -9,11 +9,11 @@ #include "mip_utils.cuh" #include +#include #include #include #include #include -#include #include #include #include @@ -128,16 +128,7 @@ void test_multi_probe(std::string path) problem_checking_t::check_problem_representation(op_problem); detail::problem_t problem(op_problem); mip_solver_settings_t default_settings{}; - detail::pdhg_solver_t pdhg_solver(problem.handle_ptr, problem); - detail::pdlp_initial_scaling_strategy_t scaling(&handle_, - problem, - 10, - 1.0, - problem.reverse_coefficients, - problem.reverse_offsets, - problem.reverse_constraints, - nullptr, - true); + detail::mip_scaling_strategy_t scaling(problem); detail::mip_solver_t solver(problem, default_settings, scaling, cuopt::timer_t(0)); detail::load_balanced_problem_t lb_problem(problem); detail::load_balanced_bounds_presolve_t lb_prs(lb_problem, solver.context); diff --git a/cpp/tests/mip/multi_probe_test.cu b/cpp/tests/mip/multi_probe_test.cu index 073c153486..003220de9b 100644 --- a/cpp/tests/mip/multi_probe_test.cu +++ b/cpp/tests/mip/multi_probe_test.cu @@ -9,10 +9,10 @@ #include "mip_utils.cuh" #include +#include #include #include #include -#include #include #include #include @@ -150,18 +150,7 @@ void test_multi_probe(std::string path) problem_checking_t::check_problem_representation(op_problem); detail::problem_t problem(op_problem); mip_solver_settings_t default_settings{}; - pdlp_hyper_params::pdlp_hyper_params_t hyper_params{}; - detail::pdlp_initial_scaling_strategy_t scaling(&handle_, - problem, - 10, - 1.0, - problem.reverse_coefficients, - problem.reverse_offsets, - problem.reverse_constraints, - nullptr, - hyper_params, - true); - detail::mip_solver_t solver(problem, default_settings, scaling, cuopt::timer_t(0)); + detail::mip_solver_t solver(problem, default_settings, cuopt::timer_t(0)); detail::bound_presolve_t bnd_prb_0(solver.context); detail::bound_presolve_t bnd_prb_1(solver.context); detail::multi_probe_t multi_probe_prs(solver.context); diff --git a/cpp/tests/mip/server_test.cu b/cpp/tests/mip/server_test.cu index 2c47191b95..b027be897f 100644 --- a/cpp/tests/mip/server_test.cu +++ b/cpp/tests/mip/server_test.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -91,12 +91,12 @@ TEST(ServerTest, TestSampleLP) class MILPTestParams : public testing::TestWithParam< - std::tuple> {}; + std::tuple> {}; TEST_P(MILPTestParams, TestSampleMILP) { bool maximize = std::get<0>(GetParam()); - bool scaling = std::get<1>(GetParam()); + int scaling = std::get<1>(GetParam()); bool heuristics_only = std::get<2>(GetParam()); auto expected_termination_status = std::get<3>(GetParam()); @@ -104,9 +104,9 @@ TEST_P(MILPTestParams, TestSampleMILP) auto problem = create_std_milp_problem(maximize); cuopt::linear_programming::mip_solver_settings_t settings{}; - settings.set_time_limit(5); - settings.set_mip_scaling(scaling); - settings.set_heuristics_only(heuristics_only); + settings.time_limit = 5; + settings.mip_scaling = scaling; + settings.heuristics_only = heuristics_only; auto result = cuopt::linear_programming::solve_mip(&handle, problem, settings); @@ -117,13 +117,21 @@ INSTANTIATE_TEST_SUITE_P( MILPTests, MILPTestParams, testing::Values( - std::make_tuple( - true, true, true, cuopt::linear_programming::mip_termination_status_t::FeasibleFound), - std::make_tuple( - false, true, false, cuopt::linear_programming::mip_termination_status_t::Optimal), - std::make_tuple( - true, false, true, cuopt::linear_programming::mip_termination_status_t::FeasibleFound), - std::make_tuple( - false, false, false, cuopt::linear_programming::mip_termination_status_t::Optimal))); + std::make_tuple(true, + CUOPT_MIP_SCALING_ON, + true, + cuopt::linear_programming::mip_termination_status_t::FeasibleFound), + std::make_tuple(false, + CUOPT_MIP_SCALING_ON, + false, + cuopt::linear_programming::mip_termination_status_t::Optimal), + std::make_tuple(true, + CUOPT_MIP_SCALING_OFF, + true, + cuopt::linear_programming::mip_termination_status_t::FeasibleFound), + std::make_tuple(false, + CUOPT_MIP_SCALING_OFF, + false, + cuopt::linear_programming::mip_termination_status_t::Optimal))); } // namespace cuopt::linear_programming::test diff --git a/cpp/tests/mip/unit_test.cu b/cpp/tests/mip/unit_test.cu index 29b58b736b..68de599f0c 100644 --- a/cpp/tests/mip/unit_test.cu +++ b/cpp/tests/mip/unit_test.cu @@ -9,7 +9,9 @@ #include "mip_utils.cuh" #include +#include #include +#include #include #include #include @@ -226,12 +228,12 @@ TEST(ErrorTest, TestError) class MILPTestParams : public testing::TestWithParam< - std::tuple> {}; + std::tuple> {}; TEST_P(MILPTestParams, TestSampleMILP) { bool maximize = std::get<0>(GetParam()); - bool scaling = std::get<1>(GetParam()); + int scaling = std::get<1>(GetParam()); bool heuristics_only = std::get<2>(GetParam()); auto expected_termination_status = std::get<3>(GetParam()); @@ -252,7 +254,7 @@ TEST_P(MILPTestParams, TestSampleMILP) TEST_P(MILPTestParams, TestSingleVarMILP) { bool maximize = std::get<0>(GetParam()); - bool scaling = std::get<1>(GetParam()); + int scaling = std::get<1>(GetParam()); bool heuristics_only = std::get<2>(GetParam()); auto expected_termination_status = std::get<3>(GetParam()); @@ -274,13 +276,165 @@ TEST_P(MILPTestParams, TestSingleVarMILP) INSTANTIATE_TEST_SUITE_P( MILPTests, MILPTestParams, - testing::Values( - std::make_tuple(true, true, true, cuopt::linear_programming::mip_termination_status_t::Optimal), - std::make_tuple( - false, true, false, cuopt::linear_programming::mip_termination_status_t::Optimal), - std::make_tuple( - true, false, true, cuopt::linear_programming::mip_termination_status_t::Optimal), - std::make_tuple( - false, false, false, cuopt::linear_programming::mip_termination_status_t::Optimal))); + testing::Values(std::make_tuple(true, + CUOPT_MIP_SCALING_ON, + true, + cuopt::linear_programming::mip_termination_status_t::Optimal), + std::make_tuple(false, + CUOPT_MIP_SCALING_ON, + false, + cuopt::linear_programming::mip_termination_status_t::Optimal), + std::make_tuple(true, + CUOPT_MIP_SCALING_OFF, + true, + cuopt::linear_programming::mip_termination_status_t::Optimal), + std::make_tuple(false, + CUOPT_MIP_SCALING_OFF, + false, + cuopt::linear_programming::mip_termination_status_t::Optimal))); + +// --------------------------------------------------------------------------- +// Scaling integrality preservation test +// --------------------------------------------------------------------------- + +static mps_parser::mps_data_model_t create_wide_spread_milp() +{ + mps_parser::mps_data_model_t problem; + + // 6 rows, 4 variables (x0=INT, x1=INT, x2=INT, x3=CONT) + // Coefficient spread: ~log2(100000/1) ≈ 17, well above the 12-threshold. + // clang-format off + std::vector values = { + 3.0, 7.0, 2.0, 1.5, // row 0: small ints + cont + 100000.0, 50000.0, 25000.0, 999.9, // row 1: large ints + cont + 5.0, 11.0, 13.0, 0.3, // row 2: small primes + cont + 60000.0, 30000.0, 9000.0, 42.42, // row 3: large + cont + 1.0, 1.0, 1.0, 0.0, // row 4: unit row (no cont) + 8.0, 4.0, 6.0, 3.14 // row 5: small ints + cont + }; + // clang-format on + std::vector indices = {0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, + 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3}; + std::vector offsets = {0, 4, 8, 12, 16, 20, 24}; + problem.set_csr_constraint_matrix( + values.data(), values.size(), indices.data(), indices.size(), offsets.data(), offsets.size()); + + std::vector cl = {0, 0, 0, 0, 0, 0}; + std::vector cu = {1e6, 1e8, 1e4, 1e8, 100, 1e4}; + problem.set_constraint_lower_bounds(cl.data(), cl.size()); + problem.set_constraint_upper_bounds(cu.data(), cu.size()); + + std::vector vl = {0, 0, 0, 0}; + std::vector vu = {1000, 1000, 1000, 1e6}; + problem.set_variable_lower_bounds(vl.data(), vl.size()); + problem.set_variable_upper_bounds(vu.data(), vu.size()); + + std::vector obj = {1.0, 2.0, 3.0, 0.5}; + problem.set_objective_coefficients(obj.data(), obj.size()); + problem.set_maximize(false); + + std::vector var_types = {'I', 'I', 'I', 'C'}; + problem.set_variable_types(var_types); + + return problem; +} + +TEST(ScalingIntegrity, IntegerCoefficientsPreservedAfterScaling) +{ + raft::handle_t handle; + auto mps_problem = create_wide_spread_milp(); + auto op_problem = mps_data_model_to_optimization_problem(&handle, mps_problem); + problem_checking_t::check_problem_representation(op_problem); + + const int nnz = op_problem.get_nnz(); + + auto pre_values = + cuopt::host_copy(op_problem.get_constraint_matrix_values(), handle.get_stream()); + auto col_indices = + cuopt::host_copy(op_problem.get_constraint_matrix_indices(), handle.get_stream()); + auto var_types = cuopt::host_copy(op_problem.get_variable_types(), handle.get_stream()); + handle.sync_stream(); + + std::vector was_integer(nnz, false); + for (int k = 0; k < nnz; ++k) { + int col = col_indices[k]; + if (var_types[col] == var_t::INTEGER) { + double abs_val = std::abs(pre_values[k]); + if (abs_val > 0.0 && + std::abs(abs_val - std::round(abs_val)) <= 1e-6 * std::max(1.0, abs_val)) { + was_integer[k] = true; + } + } + } + + detail::mip_scaling_strategy_t scaling(op_problem); + scaling.scale_problem(); + + auto post_values = + cuopt::host_copy(op_problem.get_constraint_matrix_values(), handle.get_stream()); + handle.sync_stream(); + + int violations = 0; + for (int k = 0; k < nnz; ++k) { + if (!was_integer[k]) { continue; } + double abs_val = std::abs(post_values[k]); + double frac_err = std::abs(abs_val - std::round(abs_val)); + double rel_tol = 1e-6 * std::max(1.0, abs_val); + if (frac_err > rel_tol) { + ++violations; + ADD_FAILURE() << "Coefficient [" << k << "] col=" << col_indices[k] << " was integer (" + << pre_values[k] << ") but after scaling is " << post_values[k] + << " (frac_err=" << frac_err << ")"; + } + } + EXPECT_EQ(violations, 0) << violations << " integer coefficients lost integrality after scaling"; +} + +TEST(ScalingIntegrity, NoObjectiveScalingPreservesIntegerCoefficients) +{ + raft::handle_t handle; + auto mps_problem = create_wide_spread_milp(); + auto op_problem = mps_data_model_to_optimization_problem(&handle, mps_problem); + problem_checking_t::check_problem_representation(op_problem); + + const int nnz = op_problem.get_nnz(); + + auto pre_values = + cuopt::host_copy(op_problem.get_constraint_matrix_values(), handle.get_stream()); + auto col_indices = + cuopt::host_copy(op_problem.get_constraint_matrix_indices(), handle.get_stream()); + auto var_types = cuopt::host_copy(op_problem.get_variable_types(), handle.get_stream()); + handle.sync_stream(); + + std::vector was_integer(nnz, false); + for (int k = 0; k < nnz; ++k) { + int col = col_indices[k]; + if (var_types[col] == var_t::INTEGER) { + double abs_val = std::abs(pre_values[k]); + if (abs_val > 0.0 && + std::abs(abs_val - std::round(abs_val)) <= 1e-6 * std::max(1.0, abs_val)) { + was_integer[k] = true; + } + } + } + + detail::mip_scaling_strategy_t scaling(op_problem); + scaling.scale_problem(/*scale_objective=*/false); + + auto post_values = + cuopt::host_copy(op_problem.get_constraint_matrix_values(), handle.get_stream()); + handle.sync_stream(); + + int violations = 0; + for (int k = 0; k < nnz; ++k) { + if (!was_integer[k]) { continue; } + double abs_val = std::abs(post_values[k]); + double frac_err = std::abs(abs_val - std::round(abs_val)); + double rel_tol = 1e-6 * std::max(1.0, abs_val); + if (frac_err > rel_tol) { ++violations; } + } + EXPECT_EQ(violations, 0) << violations + << " integer coefficients lost integrality after scaling (no-obj mode)"; +} } // namespace cuopt::linear_programming::test diff --git a/python/cuopt_server/cuopt_server/tests/test_lp.py b/python/cuopt_server/cuopt_server/tests/test_lp.py index 7b85899350..e3a683f8de 100644 --- a/python/cuopt_server/cuopt_server/tests/test_lp.py +++ b/python/cuopt_server/cuopt_server/tests/test_lp.py @@ -88,7 +88,7 @@ def get_std_data_for_milp(): data = get_std_data_for_lp() data["variable_types"] = ["I", "C"] data["maximize"] = True - data["solver_config"]["mip_scaling"] = False + data["solver_config"]["mip_scaling"] = 0 return data @@ -107,10 +107,10 @@ def test_sample_lp(cuoptproc): # noqa @pytest.mark.parametrize( "maximize, scaling, expected_status, heuristics_only", [ - (True, True, MILPTerminationStatus.Optimal.name, True), - (False, True, MILPTerminationStatus.Optimal.name, False), - (True, False, MILPTerminationStatus.Optimal.name, True), - (False, False, MILPTerminationStatus.Optimal.name, False), + (True, 1, MILPTerminationStatus.Optimal.name, True), + (False, 1, MILPTerminationStatus.Optimal.name, False), + (True, 0, MILPTerminationStatus.Optimal.name, True), + (False, 0, MILPTerminationStatus.Optimal.name, False), ], ) def test_sample_milp( diff --git a/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py b/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py index f532b6691a..78f3068014 100644 --- a/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py +++ b/python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py @@ -441,9 +441,15 @@ class SolverConfig(BaseModel): "
" "Note: Not supported for MILP. ", ) - mip_scaling: Optional[bool] = Field( - default=True, - description="Set True to enable MIP scaling, False to disable.", + mip_scaling: Optional[int] = Field( + default=1, + description="MIP scaling mode:" + "
" + "- 0: No scaling" + "
" + "- 1: Full scaling (objective + row), default" + "
" + "- 2: Row scaling only (no objective scaling)", ) mip_heuristics_only: Optional[bool] = Field( default=False,