From 703287408bd4015787826772d79b5d1239e8e279 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Fri, 2 Jan 2026 19:59:22 +0000 Subject: [PATCH 1/4] initial impl --- .../cuopt/linear_programming/constants.h | 3 +- cpp/src/dual_simplex/branch_and_bound.cpp | 21 ++- .../dual_simplex/simplex_solver_settings.hpp | 7 +- cpp/src/linear_programming/solve.cu | 15 ++- .../linear_programming/solver_termination.hpp | 105 +++++++++++++++ .../user_interrupt_handler.hpp | 121 ++++++++++++++++++ cpp/src/mip/diversity/diversity_manager.cu | 15 ++- cpp/src/mip/diversity/lns/rins.cu | 10 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 9 +- .../mip/feasibility_jump/feasibility_jump.cu | 4 +- cpp/src/mip/local_search/local_search.cu | 16 +-- cpp/src/mip/solver.cu | 8 +- cpp/src/mip/solver_context.cuh | 14 +- 13 files changed, 314 insertions(+), 34 deletions(-) create mode 100644 cpp/src/linear_programming/solver_termination.hpp create mode 100644 cpp/src/linear_programming/user_interrupt_handler.hpp diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index b512944a6..d1dca2cf4 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -73,6 +73,7 @@ #define CUOPT_TERIMINATION_STATUS_PRIMAL_FEASIBLE 7 #define CUOPT_TERIMINATION_STATUS_FEASIBLE_FOUND 8 #define CUOPT_TERIMINATION_STATUS_CONCURRENT_LIMIT 9 +#define CUOPT_TERIMINATION_STATUS_USER_INTERRUPT 10 /* @brief The objective sense constants */ #define CUOPT_MINIMIZE 1 diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 6161f4d3f..a7473d3fc 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -505,6 +506,10 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t::exploration_ramp_up(mip_node_t* nod } } - if (now > settings_.time_limit) { + if (now > settings_.time_limit || + (settings_.termination != nullptr && settings_.termination->should_terminate())) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } @@ -957,7 +963,8 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } } - if (now > settings_.time_limit) { + if (now > settings_.time_limit || + (settings_.termination != nullptr && settings_.termination->should_terminate())) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } @@ -1151,7 +1158,10 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A continue; } - if (toc(exploration_stats_.start_time) > settings_.time_limit) { return; } + if (toc(exploration_stats_.start_time) > settings_.time_limit || + (settings_.termination != nullptr && settings_.termination->should_terminate())) { + return; + } if (nodes_explored >= 1000) { break; } @@ -1420,7 +1430,8 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut edge_norms_, pc_); - if (toc(exploration_stats_.start_time) > settings_.time_limit) { + if (toc(exploration_stats_.start_time) > settings_.time_limit || + (settings_.termination != nullptr && settings_.termination->should_terminate())) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return set_final_solution(solution, root_objective_); } diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index a1cc049e7..12be770b9 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -17,6 +17,10 @@ #include #include +namespace cuopt::linear_programming { +class solver_termination_t; +} + namespace cuopt::linear_programming::dual_simplex { template @@ -147,6 +151,7 @@ struct simplex_solver_settings_t { mutable logger_t log; std::atomic* concurrent_halt; // if nullptr ignored, if !nullptr, 0 if solver should // continue, 1 if solver should halt + solver_termination_t* termination{nullptr}; // if not nullptr, check for user interrupt }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/linear_programming/solve.cu b/cpp/src/linear_programming/solve.cu index d038ade72..cdacf025f 100644 --- a/cpp/src/linear_programming/solve.cu +++ b/cpp/src/linear_programming/solve.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -863,6 +864,18 @@ optimization_problem_solution_t solve_lp( } auto lp_timer = cuopt::timer_t(settings.time_limit); + + // Create termination control (auto-registers for Ctrl-C) + solver_termination_t termination(settings.time_limit); + // Also set global_concurrent_halt on user interrupt for LP concurrent solver compatibility + auto halt_callback_id = user_interrupt_handler_t::instance().register_callback( + []() { global_concurrent_halt.store(1); }); + // Ensure cleanup on exit + struct callback_guard_t { + size_t id; + ~callback_guard_t() { user_interrupt_handler_t::instance().unregister_callback(id); } + } halt_callback_guard{halt_callback_id}; + detail::problem_t problem(op_problem); double presolve_time = 0.0; diff --git a/cpp/src/linear_programming/solver_termination.hpp b/cpp/src/linear_programming/solver_termination.hpp new file mode 100644 index 000000000..5517bd8b4 --- /dev/null +++ b/cpp/src/linear_programming/solver_termination.hpp @@ -0,0 +1,105 @@ +/* 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 "user_interrupt_handler.hpp" + +#include +#include + +namespace cuopt::linear_programming { + +/** + * @brief Controls solver termination based on time limit, user interrupt, and parent termination. + * + * This class owns its own timer and automatically registers with the global interrupt handler. + * It can optionally be linked to a parent termination object (for sub-MIPs) to inherit + * termination conditions. + * + * Usage: + * // Root termination (main solver) + * solver_termination_t termination(60.0); + * + * // Slave termination (sub-MIP, linked to parent) + * solver_termination_t sub_termination(10.0, &parent_termination); + * + * // Or inline with context + * solver_termination_t sub_termination(10.0, context.termination); + */ +class solver_termination_t { + public: + /** + * @brief Construct a termination object. + * @param time_limit Time limit in seconds. + * @param parent Optional parent termination object to check for termination. + */ + explicit solver_termination_t(double time_limit, solver_termination_t* parent = nullptr) + : timer_(time_limit), parent_(parent) + { + callback_id_ = user_interrupt_handler_t::instance().register_callback( + [this]() { request_user_termination(); }); + } + + ~solver_termination_t() + { + user_interrupt_handler_t::instance().unregister_callback(callback_id_); + } + + // Non-copyable, non-movable (due to registered callback with 'this' pointer) + solver_termination_t(const solver_termination_t&) = delete; + solver_termination_t& operator=(const solver_termination_t&) = delete; + solver_termination_t(solver_termination_t&&) = delete; + solver_termination_t& operator=(solver_termination_t&&) = delete; + + /** + * @brief Check if the solver should terminate. + * @return true if user interrupt requested, time limit reached, or parent terminated. + */ + bool should_terminate() const + { + if (user_interrupt_requested_.load(std::memory_order_relaxed)) { return true; } + if (timer_.check_time_limit()) { return true; } + if (parent_ != nullptr && parent_->should_terminate()) { return true; } + return false; + } + + /** + * @brief Check if termination was due to user interrupt. + * @return true if user requested termination via Ctrl-C. + */ + bool user_interrupt_requested() const + { + return user_interrupt_requested_.load(std::memory_order_relaxed); + } + + /** + * @brief Request termination due to user interrupt (e.g., Ctrl-C). + */ + void request_user_termination() + { + user_interrupt_requested_.store(true, std::memory_order_relaxed); + } + + /** + * @brief Get remaining time in seconds. + */ + double remaining_time() const { return timer_.remaining_time(); } + + /** + * @brief Get elapsed time in seconds. + */ + double elapsed_time() const { return timer_.elapsed_time(); } + + private: + timer_t timer_; + solver_termination_t* parent_; + std::atomic user_interrupt_requested_{false}; + size_t callback_id_; +}; + +} // namespace cuopt::linear_programming diff --git a/cpp/src/linear_programming/user_interrupt_handler.hpp b/cpp/src/linear_programming/user_interrupt_handler.hpp new file mode 100644 index 000000000..88443c8c0 --- /dev/null +++ b/cpp/src/linear_programming/user_interrupt_handler.hpp @@ -0,0 +1,121 @@ +/* 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 + +namespace cuopt::linear_programming { + +/** + * @brief Global singleton that handles SIGINT (Ctrl-C) and invokes registered callbacks. + * + * Components that want to respond to user interrupts register a callback via + * register_callback() and unregister via unregister_callback(). + * + * Safety feature: If the user presses Ctrl-C 5 times within 5 seconds, + * the process is forcefully terminated. + */ +class user_interrupt_handler_t { + public: + static user_interrupt_handler_t& instance() + { + static user_interrupt_handler_t instance; + return instance; + } + + /** + * @brief Register a callback to be invoked on SIGINT. + * @param callback Function to call when interrupt is received. + * @return Registration ID for later removal. + */ + size_t register_callback(std::function callback) + { + std::lock_guard lock(mutex_); + size_t id = next_id_++; + callbacks_[id] = std::move(callback); + return id; + } + + /** + * @brief Unregister a previously registered callback. + * @param id Registration ID returned by register_callback(). + */ + void unregister_callback(size_t id) + { + std::lock_guard lock(mutex_); + callbacks_.erase(id); + } + + // Non-copyable, non-movable + user_interrupt_handler_t(const user_interrupt_handler_t&) = delete; + user_interrupt_handler_t& operator=(const user_interrupt_handler_t&) = delete; + user_interrupt_handler_t(user_interrupt_handler_t&&) = delete; + user_interrupt_handler_t& operator=(user_interrupt_handler_t&&) = delete; + + private: + static constexpr int force_quit_threshold = 5; + static constexpr int force_quit_window_seconds = 5; + + using time_point = std::chrono::steady_clock::time_point; + + user_interrupt_handler_t() { previous_handler_ = std::signal(SIGINT, &handle_signal); } + + ~user_interrupt_handler_t() + { + if (previous_handler_ != SIG_ERR) { std::signal(SIGINT, previous_handler_); } + } + + static void handle_signal(int /*sig*/) + { + auto& self = instance(); + std::lock_guard lock(self.mutex_); + + auto now = std::chrono::steady_clock::now(); + auto cutoff = now - std::chrono::seconds(force_quit_window_seconds); + + // Remove timestamps older than the window + while (!self.interrupt_times_.empty() && self.interrupt_times_.front() < cutoff) { + self.interrupt_times_.pop_front(); + } + self.interrupt_times_.push_back(now); + + // Force quit if too many interrupts in the window + if (static_cast(self.interrupt_times_.size()) >= force_quit_threshold) { + CUOPT_LOG_INFO("Force quit: %d interrupts in %d seconds.", + force_quit_threshold, + force_quit_window_seconds); + std::_Exit(128 + SIGINT); + } + + // Invoke all registered callbacks + for (const auto& [id, callback] : self.callbacks_) { + callback(); + } + + auto remaining = force_quit_threshold - static_cast(self.interrupt_times_.size()); + CUOPT_LOG_INFO( + "Interrupt received. Stopping solver cleanly... (press Ctrl-C %d more time(s) to force quit)", + remaining); + } + + std::mutex mutex_; + std::unordered_map> callbacks_; + size_t next_id_{0}; + std::deque interrupt_times_; + void (*previous_handler_)(int) = SIG_ERR; +}; + +} // namespace cuopt::linear_programming diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index 483ffeb68..833ee0fc0 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.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 */ @@ -8,6 +8,7 @@ #include "cuda_profiler_api.h" #include "diversity_manager.cuh" +#include #include #include #include @@ -243,7 +244,7 @@ void diversity_manager_t::generate_quick_feasible_solution() template bool diversity_manager_t::check_b_b_preemption() { - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { if (population.current_size() == 0) { population.allocate_solutions(); } population.add_external_solutions_to_population(); return true; @@ -473,7 +474,7 @@ solution_t diversity_manager_t::run_solver() rins.enable(); generate_solution(timer.remaining_time(), false); - if (timer.check_time_limit()) { + if (context.termination.should_terminate()) { population.add_external_solutions_to_population(); return population.best_feasible(); } @@ -501,7 +502,7 @@ void diversity_manager_t::diversity_step(i_t max_iterations_without_im CUOPT_LOG_DEBUG("Population degenerated in diversity step"); return; } - if (timer.check_time_limit()) return; + if (context.termination.should_terminate()) { return; } constexpr bool tournament = true; auto [sol1, sol2] = population.get_two_random(tournament); cuopt_assert(population.test_invariant(), ""); @@ -554,7 +555,7 @@ void diversity_manager_t::recombine_and_ls_with_all(solution_t::recombine_and_ls_with_all( population.add_solution(std::move(solution_t(sol))); } for (auto& sol : solutions) { - if (timer.check_time_limit()) { return; } + if (context.termination.should_terminate()) { return; } solution_t ls_solution(sol); ls_config_t ls_config; run_local_search(ls_solution, population.weights, timer, ls_config); - if (timer.check_time_limit()) { return; } + if (context.termination.should_terminate()) { return; } // TODO try if running LP with integers fixed makes it feasible if (ls_solution.get_feasible()) { CUOPT_LOG_DEBUG("LS searched solution feasible, running recombiners!"); diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index b7e3a5331..954fc3d25 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights * reserved. SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include +#include #include #include #include @@ -218,7 +219,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); + &rins_handle, &fixed_problem, context.settings, context.scaling, time_limit); fj_t fj(fj_context); solution_t fj_solution(fixed_problem); fj_solution.copy_new_assignment(cuopt::host_copy(fixed_assignment)); @@ -248,6 +249,11 @@ void rins_t::run_rins() dual_simplex::mip_status_t branch_and_bound_status = dual_simplex::mip_status_t::UNSET; fixed_problem.get_host_user_problem(branch_and_bound_problem); branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); + + // Termination control (linked to parent, handles Ctrl-C) + solver_termination_t rins_termination(time_limit, &context.termination); + branch_and_bound_settings.termination = &rins_termination; + // Fill in the settings for branch and bound branch_and_bound_settings.time_limit = time_limit; // branch_and_bound_settings.node_limit = 5000 + node_count / 100; // try harder as time goes diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 5be807372..30f9dcbf7 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -13,6 +13,7 @@ #include #include #include +#include namespace cuopt::linear_programming::detail { @@ -96,6 +97,12 @@ class sub_mip_recombiner_t : public recombiner_t { dual_simplex::simplex_solver_settings_t branch_and_bound_settings; fixed_problem.get_host_user_problem(branch_and_bound_problem); branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); + + // Termination control (linked to parent, handles Ctrl-C) + solver_termination_t sub_mip_termination(sub_mip_recombiner_config_t::sub_mip_time_limit, + &context.termination); + branch_and_bound_settings.termination = &sub_mip_termination; + // Fill in the settings for branch and bound branch_and_bound_settings.time_limit = sub_mip_recombiner_config_t::sub_mip_time_limit; branch_and_bound_settings.print_presolve_stats = false; diff --git a/cpp/src/mip/feasibility_jump/feasibility_jump.cu b/cpp/src/mip/feasibility_jump/feasibility_jump.cu index 6dcd768c9..5f68dbdd5 100644 --- a/cpp/src/mip/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip/feasibility_jump/feasibility_jump.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 */ @@ -869,7 +869,7 @@ i_t fj_t::host_loop(solution_t& solution, i_t climber_idx) // to actualize time limit handle_ptr->sync_stream(); if (timer.check_time_limit() || steps >= settings.iteration_limit || - context.preempt_heuristic_solver_.load()) { + context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { limit_reached = true; } diff --git a/cpp/src/mip/local_search/local_search.cu b/cpp/src/mip/local_search/local_search.cu index a3353e72f..5225fccd4 100644 --- a/cpp/src/mip/local_search/local_search.cu +++ b/cpp/src/mip/local_search/local_search.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 */ @@ -460,7 +460,7 @@ bool local_search_t::run_staged_fp(solution_t& solution, fp.resize_vectors(*solution.problem_ptr, solution.handle_ptr); for (i_t i = 0; i < n_fp_iterations && !timer.check_time_limit(); ++i) { population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return false; } @@ -470,7 +470,7 @@ bool local_search_t::run_staged_fp(solution_t& solution, i_t binary_it_counter = 0; for (; binary_it_counter < 100; ++binary_it_counter) { population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return false; } @@ -668,14 +668,14 @@ bool local_search_t::run_fp(solution_t& solution, } CUOPT_LOG_DEBUG("fp_loop it %d last_improved_iteration %d", i, last_improved_iteration); population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); break; } is_feasible = fp.run_single_fp_descent(solution); population_ptr->add_external_solutions_to_population(); CUOPT_LOG_DEBUG("Population size at iteration %d: %d", i, population_ptr->current_size()); - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); break; } @@ -699,7 +699,7 @@ bool local_search_t::run_fp(solution_t& solution, } is_feasible = fp.restart_fp(solution); population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); break; } @@ -754,7 +754,7 @@ bool local_search_t::generate_solution(solution_t& solution, return true; } population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return is_feasible; } @@ -775,7 +775,7 @@ bool local_search_t::generate_solution(solution_t& solution, solution.handle_ptr->get_stream()); } population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load()) { + if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return is_feasible; } diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index 0da4c6398..fab2eecf7 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.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 */ @@ -20,6 +20,8 @@ #include #include +#include + #include #include @@ -49,7 +51,8 @@ mip_solver_t::mip_solver_t(const problem_t& op_problem, context(op_problem.handle_ptr, const_cast*>(&op_problem), solver_settings, - scaling), + scaling, + timer.remaining_time()), timer_(timer) { init_handler(op_problem.handle_ptr); @@ -179,6 +182,7 @@ solution_t mip_solver_t::run_solver() i_t num_diving_threads = std::max(1, num_threads - num_bfs_threads); branch_and_bound_settings.num_bfs_threads = num_bfs_threads; branch_and_bound_settings.num_diving_threads = num_diving_threads; + branch_and_bound_settings.termination = &context.termination; // Set the branch and bound -> primal heuristics callback branch_and_bound_settings.solution_callback = diff --git a/cpp/src/mip/solver_context.cuh b/cpp/src/mip/solver_context.cuh index 59ada5feb..73fae0efc 100644 --- a/cpp/src/mip/solver_context.cuh +++ b/cpp/src/mip/solver_context.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -8,12 +8,12 @@ #include #include +#include #include #include #pragma once -// Forward declare namespace cuopt::linear_programming::dual_simplex { template class branch_and_bound_t; @@ -28,8 +28,13 @@ 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) + pdlp_initial_scaling_strategy_t& scaling, + double time_limit) + : handle_ptr(handle_ptr_), + problem_ptr(problem_ptr_), + settings(settings_), + scaling(scaling), + termination(time_limit) { cuopt_assert(problem_ptr != nullptr, "problem_ptr is nullptr"); stats.solution_bound = problem_ptr->maximize ? std::numeric_limits::infinity() @@ -43,6 +48,7 @@ struct mip_solver_context_t { const mip_solver_settings_t settings; pdlp_initial_scaling_strategy_t& scaling; solver_stats_t stats; + solver_termination_t termination; }; } // namespace cuopt::linear_programming::detail From 59af63688606b8ac15f68b804d5c05859da08f89 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Mon, 5 Jan 2026 17:17:53 +0000 Subject: [PATCH 2/4] overhaul termination checks in the solver --- .../linear_programming/cuopt/run_mip.cpp | 13 ++- cpp/cuopt_cli.cpp | 16 ++- .../pdlp/solver_settings.hpp | 15 ++- .../linear_programming/solver_settings.hpp | 4 +- .../utilities/callbacks_implems.hpp | 15 ++- .../utilities/internals.hpp | 12 +- .../utilities}/user_interrupt_handler.hpp | 26 +++-- cpp/src/dual_simplex/barrier.cu | 12 +- cpp/src/dual_simplex/branch_and_bound.cpp | 10 +- cpp/src/dual_simplex/crossover.cpp | 12 +- cpp/src/dual_simplex/phase2.cpp | 16 +-- cpp/src/dual_simplex/pseudo_costs.cpp | 10 +- cpp/src/dual_simplex/right_looking_lu.cpp | 4 +- .../dual_simplex/simplex_solver_settings.hpp | 16 ++- cpp/src/linear_programming/pdlp.cu | 16 +-- cpp/src/linear_programming/pdlp.cuh | 15 ++- cpp/src/linear_programming/solve.cu | 64 ++++++----- cpp/src/linear_programming/solve.cuh | 4 +- .../linear_programming/solver_termination.hpp | 105 ------------------ cpp/src/math_optimization/solver_settings.cu | 15 ++- cpp/src/mip/diversity/diversity_manager.cu | 28 ++--- cpp/src/mip/diversity/diversity_manager.cuh | 7 +- cpp/src/mip/diversity/lns/rins.cu | 6 +- cpp/src/mip/diversity/population.cu | 6 +- cpp/src/mip/diversity/population.cuh | 6 +- .../recombiners/bound_prop_recombiner.cuh | 8 +- .../diversity/recombiners/fp_recombiner.cuh | 11 +- .../recombiners/line_segment_recombiner.cuh | 5 +- cpp/src/mip/diversity/recombiners/sub_mip.cuh | 6 +- .../mip/feasibility_jump/feasibility_jump.cu | 8 +- .../feasibility_pump/feasibility_pump.cu | 14 ++- .../feasibility_pump/feasibility_pump.cuh | 6 +- .../line_segment_search.cu | 10 +- .../line_segment_search.cuh | 6 +- cpp/src/mip/local_search/local_search.cu | 77 +++++++------ cpp/src/mip/local_search/local_search.cuh | 22 ++-- .../local_search/rounding/bounds_repair.cu | 9 +- .../local_search/rounding/bounds_repair.cuh | 6 +- .../local_search/rounding/constraint_prop.cu | 27 +++-- .../local_search/rounding/constraint_prop.cuh | 10 +- .../local_search/rounding/lb_bounds_repair.cu | 6 +- .../rounding/lb_bounds_repair.cuh | 6 +- .../rounding/lb_constraint_prop.cu | 23 ++-- .../rounding/lb_constraint_prop.cuh | 10 +- cpp/src/mip/presolve/bounds_presolve.cu | 14 +-- cpp/src/mip/presolve/bounds_presolve.cuh | 5 +- cpp/src/mip/presolve/lb_probing_cache.cu | 8 +- .../presolve/load_balanced_bounds_presolve.cu | 12 +- .../load_balanced_bounds_presolve.cuh | 5 +- cpp/src/mip/presolve/multi_probe.cu | 13 +-- cpp/src/mip/presolve/multi_probe.cuh | 4 +- cpp/src/mip/presolve/probing_cache.cu | 8 +- cpp/src/mip/presolve/probing_cache.cuh | 8 +- cpp/src/mip/relaxed_lp/relaxed_lp.cu | 26 +++-- cpp/src/mip/relaxed_lp/relaxed_lp.cuh | 9 +- cpp/src/mip/solve.cu | 46 ++++++-- cpp/src/mip/solver.cu | 8 +- cpp/src/mip/solver.cuh | 6 +- cpp/src/mip/solver_context.cuh | 8 +- cpp/src/utilities/termination_checker.hpp | 76 +++++++++++++ cpp/src/utilities/timer.hpp | 12 +- cpp/tests/linear_programming/pdlp_test.cu | 68 ++++++++---- cpp/tests/mip/bounds_standardization_test.cu | 7 +- cpp/tests/mip/elim_var_remap_test.cu | 10 +- cpp/tests/mip/feasibility_jump_tests.cu | 4 +- cpp/tests/mip/load_balancing_test.cu | 5 +- cpp/tests/mip/multi_probe_test.cu | 6 +- .../linear_programming/internals/__init__.py | 3 +- .../internals/internals.pyx | 17 ++- .../linear_programming/solver/solver.pxd | 7 +- .../solver/solver_wrapper.pyx | 10 +- .../solver_settings/solver_settings.py | 54 ++++++++- 72 files changed, 708 insertions(+), 474 deletions(-) rename cpp/{src/linear_programming => include/cuopt/utilities}/user_interrupt_handler.hpp (83%) delete mode 100644 cpp/src/linear_programming/solver_termination.hpp create mode 100644 cpp/src/utilities/termination_checker.hpp diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 6013dcaf5..bd21d0156 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -40,6 +41,14 @@ #include "initial_problem_check.hpp" +class check_termination_callback_t : public cuopt::internals::check_termination_callback_t { + public: + virtual bool check_termination() override + { + return cuopt::user_interrupt_handler_t::instance().termination_requested(); + } +}; + void merge_result_files(const std::string& out_dir, const std::string& final_result_file, int n_gpus, @@ -197,6 +206,8 @@ int run_single_file(std::string file_path, } } + check_termination_callback_t termination_callback; + settings.set_mip_callback(&termination_callback); settings.time_limit = time_limit; settings.heuristics_only = heuristics_only; settings.num_cpu_threads = num_cpu_threads; diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 5023cefc6..6c7da7a67 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,14 @@ #include +class check_termination_callback_t : public cuopt::internals::check_termination_callback_t { + public: + virtual bool check_termination() override + { + return cuopt::user_interrupt_handler_t::instance().termination_requested(); + } +}; + static char cuda_module_loading_env[] = "CUDA_MODULE_LOADING=EAGER"; /** @@ -92,6 +101,11 @@ int run_single_file(const std::string& file_path, const raft::handle_t handle_{}; cuopt::linear_programming::solver_settings_t settings; + // Ctrl-C handler + check_termination_callback_t termination_callback; + settings.set_mip_callback(&termination_callback); + settings.set_lp_callback(&termination_callback); + try { for (auto& [key, val] : settings_strings) { settings.set_parameter_from_string(key, val); diff --git a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp index 76388504e..f0135853a 100644 --- a/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/pdlp/solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -170,6 +171,16 @@ class pdlp_solver_settings_t { bool has_initial_primal_solution() const; bool has_initial_dual_solution() const; + const std::vector get_lp_callbacks() const + { + return lp_callbacks_; + } + + void set_lp_callback(internals::base_solution_callback_t* callback) + { + lp_callbacks_.push_back(callback); + } + struct tolerances_t { f_t absolute_dual_tolerance = 1.0e-4; f_t relative_dual_tolerance = 1.0e-4; @@ -215,6 +226,8 @@ class pdlp_solver_settings_t { std::atomic* concurrent_halt{nullptr}; static constexpr f_t minimal_absolute_tolerance = 1.0e-12; + std::vector lp_callbacks_; + private: /** Initial primal solution */ std::shared_ptr> initial_primal_solution_; diff --git a/cpp/include/cuopt/linear_programming/solver_settings.hpp b/cpp/include/cuopt/linear_programming/solver_settings.hpp index 180293254..d5c9c2512 100644 --- a/cpp/include/cuopt/linear_programming/solver_settings.hpp +++ b/cpp/include/cuopt/linear_programming/solver_settings.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -82,6 +82,8 @@ class solver_settings_t { i_t size, rmm::cuda_stream_view stream = rmm::cuda_stream_default); void set_mip_callback(internals::base_solution_callback_t* callback = nullptr); + void set_lp_callback(internals::base_solution_callback_t* callback = nullptr); + const std::vector get_lp_callbacks() const; const pdlp_warm_start_data_view_t& get_pdlp_warm_start_data_view() const noexcept; const std::vector get_mip_callbacks() const; diff --git a/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp b/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp index f0cd74c24..7437ad23f 100644 --- a/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/callbacks_implems.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -89,5 +89,18 @@ class default_set_solution_callback_t : public set_solution_callback_t { PyObject* pyCallbackClass; }; +class default_check_termination_callback_t : public check_termination_callback_t { + public: + bool check_termination() override + { + PyObject* res = PyObject_CallMethod(this->pyCallbackClass, "check_termination", nullptr); + bool should_terminate = PyObject_IsTrue(res); + Py_DECREF(res); + return should_terminate; + } + + PyObject* pyCallbackClass; +}; + } // namespace internals } // namespace cuopt diff --git a/cpp/include/cuopt/linear_programming/utilities/internals.hpp b/cpp/include/cuopt/linear_programming/utilities/internals.hpp index 84c96a716..92424bc24 100644 --- a/cpp/include/cuopt/linear_programming/utilities/internals.hpp +++ b/cpp/include/cuopt/linear_programming/utilities/internals.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -20,7 +20,7 @@ class Callback { virtual ~Callback() {} }; -enum class base_solution_callback_type { GET_SOLUTION, SET_SOLUTION }; +enum class base_solution_callback_type { GET_SOLUTION, SET_SOLUTION, CHECK_TERMINATION }; class base_solution_callback_t : public Callback { public: @@ -56,6 +56,14 @@ class set_solution_callback_t : public base_solution_callback_t { } }; +class check_termination_callback_t : public base_solution_callback_t { + public: + virtual bool check_termination() = 0; + base_solution_callback_type get_type() const override + { + return base_solution_callback_type::CHECK_TERMINATION; + } +}; } // namespace internals namespace linear_programming { diff --git a/cpp/src/linear_programming/user_interrupt_handler.hpp b/cpp/include/cuopt/utilities/user_interrupt_handler.hpp similarity index 83% rename from cpp/src/linear_programming/user_interrupt_handler.hpp rename to cpp/include/cuopt/utilities/user_interrupt_handler.hpp index 88443c8c0..b896f2982 100644 --- a/cpp/src/linear_programming/user_interrupt_handler.hpp +++ b/cpp/include/cuopt/utilities/user_interrupt_handler.hpp @@ -1,14 +1,12 @@ /* 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 */ #pragma once -#include - #include #include #include @@ -17,7 +15,7 @@ #include #include -namespace cuopt::linear_programming { +namespace cuopt { /** * @brief Global singleton that handles SIGINT (Ctrl-C) and invokes registered callbacks. @@ -36,6 +34,8 @@ class user_interrupt_handler_t { return instance; } + bool termination_requested() const { return terminate_signal_received_.load(); } + /** * @brief Register a callback to be invoked on SIGINT. * @param callback Function to call when interrupt is received. @@ -83,6 +83,8 @@ class user_interrupt_handler_t { auto& self = instance(); std::lock_guard lock(self.mutex_); + self.terminate_signal_received_ = true; + auto now = std::chrono::steady_clock::now(); auto cutoff = now - std::chrono::seconds(force_quit_window_seconds); @@ -94,9 +96,10 @@ class user_interrupt_handler_t { // Force quit if too many interrupts in the window if (static_cast(self.interrupt_times_.size()) >= force_quit_threshold) { - CUOPT_LOG_INFO("Force quit: %d interrupts in %d seconds.", - force_quit_threshold, - force_quit_window_seconds); + fprintf(stderr, + "Force quit: %d interrupts in %d seconds.", + force_quit_threshold, + force_quit_window_seconds); std::_Exit(128 + SIGINT); } @@ -106,16 +109,17 @@ class user_interrupt_handler_t { } auto remaining = force_quit_threshold - static_cast(self.interrupt_times_.size()); - CUOPT_LOG_INFO( - "Interrupt received. Stopping solver cleanly... (press Ctrl-C %d more time(s) to force quit)", - remaining); + fprintf(stderr, + "Interrupt received. Stopping solver... (press Ctrl-C %d more time(s) to force quit)", + remaining); } std::mutex mutex_; std::unordered_map> callbacks_; + std::atomic terminate_signal_received_{false}; size_t next_id_{0}; std::deque interrupt_times_; void (*previous_handler_)(int) = SIG_ERR; }; -} // namespace cuopt::linear_programming +} // namespace cuopt diff --git a/cpp/src/dual_simplex/barrier.cu b/cpp/src/dual_simplex/barrier.cu index 8c0a32fad..5d965c2fb 100644 --- a/cpp/src/dual_simplex/barrier.cu +++ b/cpp/src/dual_simplex/barrier.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -3688,13 +3688,13 @@ lp_status_t barrier_solver_t::solve(f_t start_time, data.cusparse_y_residual_ = data.cusparse_view_.create_vector(data.d_y_residual_); data.restrict_u_.resize(num_upper_bounds); - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Barrier time limit exceeded\n"); return lp_status_t::TIME_LIMIT; } i_t initial_status = initial_point(data); - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Barrier time limit exceeded\n"); return lp_status_t::TIME_LIMIT; } @@ -3793,7 +3793,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, while (iter < iteration_limit) { raft::common::nvtx::range fun_scope("Barrier: iteration"); - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Barrier time limit exceeded\n"); return lp_status_t::TIME_LIMIT; } @@ -3829,7 +3829,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, relative_complementarity_residual, solution); } - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Barrier time limit exceeded\n"); return lp_status_t::TIME_LIMIT; } @@ -3869,7 +3869,7 @@ lp_status_t barrier_solver_t::solve(f_t start_time, } data.has_factorization = false; data.has_solve_info = false; - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Barrier time limit exceeded\n"); return lp_status_t::TIME_LIMIT; } diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index a7473d3fc..b8cfc2aff 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include @@ -834,7 +834,7 @@ void branch_and_bound_t::exploration_ramp_up(mip_node_t* nod } if (now > settings_.time_limit || - (settings_.termination != nullptr && settings_.termination->should_terminate())) { + (settings_.termination != nullptr && settings_.termination->check())) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } @@ -964,7 +964,7 @@ void branch_and_bound_t::explore_subtree(i_t task_id, } if (now > settings_.time_limit || - (settings_.termination != nullptr && settings_.termination->should_terminate())) { + (settings_.termination != nullptr && settings_.termination->check())) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return; } @@ -1159,7 +1159,7 @@ void branch_and_bound_t::diving_thread(const csr_matrix_t& A } if (toc(exploration_stats_.start_time) > settings_.time_limit || - (settings_.termination != nullptr && settings_.termination->should_terminate())) { + (settings_.termination != nullptr && settings_.termination->check())) { return; } @@ -1431,7 +1431,7 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut pc_); if (toc(exploration_stats_.start_time) > settings_.time_limit || - (settings_.termination != nullptr && settings_.termination->should_terminate())) { + (settings_.termination != nullptr && settings_.termination->check())) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return set_final_solution(solution, root_objective_); } diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index 23d9a0e8e..df270d6ef 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -529,7 +529,7 @@ i_t dual_push(const lp_problem_t& lp, settings.log.printf( "%d of %d dual pushes in %.2fs\n", num_pushes, total_superbasics, toc(start_time)); } - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Crossover time exceeded\n"); return -1; } @@ -820,7 +820,7 @@ i_t primal_push(const lp_problem_t& lp, last_print_time = tic(); } - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Crossover time limit exceeded\n"); return -1; } @@ -1143,7 +1143,7 @@ crossover_status_t crossover(const lp_problem_t& lp, } reorder_basic_list(q, basic_list); - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Time limit exceeded\n"); return crossover_status_t::TIME_LIMIT; } @@ -1230,7 +1230,7 @@ crossover_status_t crossover(const lp_problem_t& lp, std::vector edge_norms; dual::status_t status = dual_phase2(2, 0, start_time, lp, settings, vstatus, solution, dual_iter, edge_norms); - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Time limit exceeded\n"); return crossover_status_t::TIME_LIMIT; } @@ -1358,7 +1358,7 @@ crossover_status_t crossover(const lp_problem_t& lp, std::vector edge_norms; dual::status_t status = dual_phase2( 2, iter == 0 ? 1 : 0, start_time, lp, settings, vstatus, solution, iter, edge_norms); - if (toc(start_time) > settings.time_limit) { + if (settings.check_termination(start_time)) { settings.log.printf("Time limit exceeded\n"); return crossover_status_t::TIME_LIMIT; } diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 56298ef4d..86b879e38 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -1227,7 +1227,7 @@ i_t initialize_steepest_edge_norms(const lp_problem_t& lp, last_log = tic(); settings.log.printf("Initialized %d of %d steepest edge norms in %.2fs\n", k, m, now); } - if (toc(start_time) > settings.time_limit) { return -1; } + if (settings.check_termination(start_time)) { return -1; } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return -1; } } return 0; @@ -2245,7 +2245,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, return dual::status_t::NUMERICAL; } - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } } std::vector c_basic(m); @@ -2256,7 +2256,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, // Solve B'*y = cB ft.b_transpose_solve(c_basic, y); - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } constexpr bool print_norms = false; if constexpr (print_norms) { settings.log.printf( @@ -2301,7 +2301,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, phase2::compute_primal_variables( ft, lp.rhs, lp.A, basic_list, nonbasic_list, settings.tight_tol, x); - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } if (print_norms) { settings.log.printf("|| x || %e\n", vector_norm2(x)); } #ifdef COMPUTE_PRIMAL_RESIDUAL @@ -2886,7 +2886,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } i_t count = 0; i_t deficient_size; while ((deficient_size = @@ -2895,7 +2895,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, iter, static_cast(deficient_size)); - if (toc(start_time) > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } settings.threshold_partial_pivoting_tol = 1.0; count++; @@ -2961,7 +2961,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, return dual::status_t::CUTOFF; } - if (now > settings.time_limit) { return dual::status_t::TIME_LIMIT; } + if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { return dual::status_t::CONCURRENT_LIMIT; diff --git a/cpp/src/dual_simplex/pseudo_costs.cpp b/cpp/src/dual_simplex/pseudo_costs.cpp index 9f84e108d..28a007923 100644 --- a/cpp/src/dual_simplex/pseudo_costs.cpp +++ b/cpp/src/dual_simplex/pseudo_costs.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -53,7 +53,7 @@ void strong_branch_helper(i_t start, child_settings.set_log(false); f_t lp_start_time = tic(); f_t elapsed_time = toc(start_time); - if (elapsed_time > settings.time_limit) { break; } + if (settings.check_termination(start_time)) { break; } child_settings.time_limit = std::max(0.0, settings.time_limit - elapsed_time); child_settings.iteration_limit = 200; lp_solution_t solution(original_lp.num_rows, original_lp.num_cols); @@ -112,9 +112,9 @@ void strong_branch_helper(i_t start, toc(start_time)); } } - if (toc(start_time) > settings.time_limit) { break; } + if (settings.check_termination(start_time)) { break; } } - if (toc(start_time) > settings.time_limit) { break; } + if (settings.check_termination(start_time)) { break; } const i_t completed = pc.num_strong_branches_completed++; @@ -129,7 +129,7 @@ void strong_branch_helper(i_t start, child_problem.lower[j] = original_lp.lower[j]; child_problem.upper[j] = original_lp.upper[j]; - if (toc(start_time) > settings.time_limit) { break; } + if (settings.check_termination(start_time)) { break; } } } diff --git a/cpp/src/dual_simplex/right_looking_lu.cpp b/cpp/src/dual_simplex/right_looking_lu.cpp index a63c1181f..52d40795a 100644 --- a/cpp/src/dual_simplex/right_looking_lu.cpp +++ b/cpp/src/dual_simplex/right_looking_lu.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -1108,7 +1108,7 @@ i_t right_looking_lu_row_permutation_only(const csc_matrix_t& A, toc(factorization_start_time)); last_print = tic(); } - if (toc(factorization_start_time) > settings.time_limit) { + if (settings.check_termination(factorization_start_time)) { settings.log.printf("Right-looking LU factorization time exceeded\n"); return -1; } diff --git a/cpp/src/dual_simplex/simplex_solver_settings.hpp b/cpp/src/dual_simplex/simplex_solver_settings.hpp index 12be770b9..4718378df 100644 --- a/cpp/src/dual_simplex/simplex_solver_settings.hpp +++ b/cpp/src/dual_simplex/simplex_solver_settings.hpp @@ -8,8 +8,11 @@ #pragma once #include +#include #include +#include + #include #include #include @@ -17,10 +20,6 @@ #include #include -namespace cuopt::linear_programming { -class solver_termination_t; -} - namespace cuopt::linear_programming::dual_simplex { template @@ -88,6 +87,13 @@ struct simplex_solver_settings_t { void enable_log_to_file() { log.enable_log_to_file(); } void set_log_filename(const std::string& log_filename) { log.set_log_file(log_filename); } void close_log_file() { log.close_log_file(); } + bool check_termination(double start_time) const + { + bool terminate = + toc(start_time) > time_limit || (termination != nullptr && termination->check()); + if (terminate && concurrent_halt != nullptr) *concurrent_halt = 1; + return terminate; + } i_t iteration_limit; i_t node_limit; f_t time_limit; @@ -151,7 +157,7 @@ struct simplex_solver_settings_t { mutable logger_t log; std::atomic* concurrent_halt; // if nullptr ignored, if !nullptr, 0 if solver should // continue, 1 if solver should halt - solver_termination_t* termination{nullptr}; // if not nullptr, check for user interrupt + termination_checker_t* termination{nullptr}; // if not nullptr, check for user interrupt }; } // namespace cuopt::linear_programming::dual_simplex diff --git a/cpp/src/linear_programming/pdlp.cu b/cpp/src/linear_programming/pdlp.cu index 076af6ee3..1e2b38944 100644 --- a/cpp/src/linear_programming/pdlp.cu +++ b/cpp/src/linear_programming/pdlp.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -252,11 +252,11 @@ void pdlp_solver_t::set_initial_dual_solution( initial_dual_.data(), initial_dual_solution.data(), initial_dual_solution.size(), stream_view_); } -static bool time_limit_reached(const timer_t& timer) { return timer.check_time_limit(); } +static bool time_limit_reached(const termination_checker_t& timer) { return timer.check(); } template std::optional> pdlp_solver_t::check_limits( - const timer_t& timer) + const termination_checker_t& timer) { // Check for time limit if (time_limit_reached(timer)) { @@ -494,7 +494,8 @@ pdlp_warm_start_data_t pdlp_solver_t::get_filled_warmed_star } template -void pdlp_solver_t::print_termination_criteria(const timer_t& timer, bool is_average) +void pdlp_solver_t::print_termination_criteria(const termination_checker_t& timer, + bool is_average) { if (!inside_mip_) { auto elapsed = timer.elapsed_time(); @@ -508,7 +509,7 @@ void pdlp_solver_t::print_termination_criteria(const timer_t& timer, b template void pdlp_solver_t::print_final_termination_criteria( - const timer_t& timer, + const termination_checker_t& timer, const convergence_information_t& convergence_information, const pdlp_termination_status_t& termination_status, bool is_average) @@ -537,7 +538,7 @@ void pdlp_solver_t::print_final_termination_criteria( template std::optional> pdlp_solver_t::check_termination( - const timer_t& timer) + const termination_checker_t& timer) { raft::common::nvtx::range fun_scope("Check termination"); @@ -1076,7 +1077,8 @@ void pdlp_solver_t::compute_fixed_error(bool& has_restarted) } template -optimization_problem_solution_t pdlp_solver_t::run_solver(const timer_t& timer) +optimization_problem_solution_t pdlp_solver_t::run_solver( + const termination_checker_t& timer) { bool verbose; #ifdef PDLP_VERBOSE_MODE diff --git a/cpp/src/linear_programming/pdlp.cuh b/cpp/src/linear_programming/pdlp.cuh index 663c617d4..0f763de62 100644 --- a/cpp/src/linear_programming/pdlp.cuh +++ b/cpp/src/linear_programming/pdlp.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -60,7 +61,7 @@ class pdlp_solver_t { pdlp_solver_settings_t const& settings = pdlp_solver_settings_t{}, bool is_batch_mode = false); - optimization_problem_solution_t run_solver(const timer_t& timer); + optimization_problem_solution_t run_solver(const termination_checker_t& timer); f_t get_primal_weight_h() const; f_t get_step_size_h() const; @@ -93,14 +94,16 @@ class pdlp_solver_t { void compute_initial_primal_weight(); private: - void print_termination_criteria(const timer_t& timer, bool is_average = false); + void print_termination_criteria(const termination_checker_t& timer, bool is_average = false); void print_final_termination_criteria( - const timer_t& timer, + const termination_checker_t& timer, const convergence_information_t& convergence_information, const pdlp_termination_status_t& termination_status, bool is_average = false); - std::optional> check_termination(const timer_t& timer); - std::optional> check_limits(const timer_t& timer); + std::optional> check_termination( + const termination_checker_t& timer); + std::optional> check_limits( + const termination_checker_t& timer); void record_best_primal_so_far(const detail::pdlp_termination_strategy_t& current, const detail::pdlp_termination_strategy_t& average, const pdlp_termination_status_t& termination_current, diff --git a/cpp/src/linear_programming/solve.cu b/cpp/src/linear_programming/solve.cu index cdacf025f..3722afa11 100644 --- a/cpp/src/linear_programming/solve.cu +++ b/cpp/src/linear_programming/solve.cu @@ -32,8 +32,8 @@ #include #include #include -#include #include +#include #include #include @@ -387,7 +387,7 @@ template std::tuple, dual_simplex::lp_status_t, f_t, f_t, f_t> run_barrier(dual_simplex::user_problem_t& user_problem, pdlp_solver_settings_t const& settings, - const timer_t& timer) + const termination_checker_t& timer) { f_t norm_user_objective = dual_simplex::vector_norm2(user_problem.objective); f_t norm_rhs = dual_simplex::vector_norm2(user_problem.rhs); @@ -434,7 +434,7 @@ template optimization_problem_solution_t run_barrier( detail::problem_t& problem, pdlp_solver_settings_t const& settings, - const timer_t& timer) + const termination_checker_t& timer) { // Convert data structures to dual simplex format and back dual_simplex::user_problem_t dual_simplex_problem = @@ -456,7 +456,7 @@ void run_barrier_thread( std::unique_ptr< std::tuple, dual_simplex::lp_status_t, f_t, f_t, f_t>>& sol_ptr, - const timer_t& timer) + const termination_checker_t& timer) { // We will return the solution from the thread as a unique_ptr sol_ptr = std::make_unique< @@ -471,9 +471,9 @@ template std::tuple, dual_simplex::lp_status_t, f_t, f_t, f_t> run_dual_simplex(dual_simplex::user_problem_t& user_problem, pdlp_solver_settings_t const& settings, - const timer_t& timer) + const termination_checker_t& timer) { - timer_t timer_dual_simplex(timer.remaining_time()); + termination_checker_t timer_dual_simplex(timer.remaining_time(), timer); f_t norm_user_objective = dual_simplex::vector_norm2(user_problem.objective); f_t norm_rhs = dual_simplex::vector_norm2(user_problem.rhs); @@ -508,7 +508,7 @@ template optimization_problem_solution_t run_dual_simplex( detail::problem_t& problem, pdlp_solver_settings_t const& settings, - const timer_t& timer) + const termination_checker_t& timer) { // Convert data structures to dual simplex format and back dual_simplex::user_problem_t dual_simplex_problem = @@ -527,7 +527,7 @@ template static optimization_problem_solution_t run_pdlp_solver( detail::problem_t& problem, pdlp_solver_settings_t const& settings, - const timer_t& timer, + const termination_checker_t& timer, bool is_batch_mode) { if (problem.n_constraints == 0) { @@ -543,12 +543,12 @@ static optimization_problem_solution_t run_pdlp_solver( template optimization_problem_solution_t run_pdlp(detail::problem_t& problem, pdlp_solver_settings_t const& settings, - const timer_t& timer, + const termination_checker_t& timer, bool is_batch_mode) { auto start_solver = std::chrono::high_resolution_clock::now(); f_t start_time = dual_simplex::tic(); - timer_t timer_pdlp(timer.remaining_time()); + termination_checker_t timer_pdlp(timer.remaining_time(), timer); auto sol = run_pdlp_solver(problem, settings, timer, is_batch_mode); auto pdlp_solve_time = timer_pdlp.elapsed_time(); sol.set_solve_time(timer.elapsed_time()); @@ -639,7 +639,7 @@ void run_dual_simplex_thread( std::unique_ptr< std::tuple, dual_simplex::lp_status_t, f_t, f_t, f_t>>& sol_ptr, - const timer_t& timer) + const termination_checker_t& timer) { // We will return the solution from the thread as a unique_ptr sol_ptr = std::make_unique< @@ -651,11 +651,11 @@ template optimization_problem_solution_t run_concurrent( detail::problem_t& problem, pdlp_solver_settings_t const& settings, - const timer_t& timer, + const termination_checker_t& timer, bool is_batch_mode) { CUOPT_LOG_INFO("Running concurrent\n"); - timer_t timer_concurrent(timer.remaining_time()); + termination_checker_t timer_concurrent(timer.remaining_time(), timer); // Copy the settings so that we can set the concurrent halt pointer pdlp_solver_settings_t settings_pdlp(settings); @@ -796,7 +796,7 @@ template optimization_problem_solution_t solve_lp_with_method( detail::problem_t& problem, pdlp_solver_settings_t const& settings, - const timer_t& timer, + const termination_checker_t& timer, bool is_batch_mode) { if (settings.method == method_t::DualSimplex) { @@ -863,18 +863,24 @@ optimization_problem_solution_t solve_lp( op_problem.get_handle_ptr()->get_stream()); } - auto lp_timer = cuopt::timer_t(settings.time_limit); - // Create termination control (auto-registers for Ctrl-C) - solver_termination_t termination(settings.time_limit); - // Also set global_concurrent_halt on user interrupt for LP concurrent solver compatibility - auto halt_callback_id = user_interrupt_handler_t::instance().register_callback( - []() { global_concurrent_halt.store(1); }); - // Ensure cleanup on exit - struct callback_guard_t { - size_t id; - ~callback_guard_t() { user_interrupt_handler_t::instance().unregister_callback(id); } - } halt_callback_guard{halt_callback_id}; + termination_checker_t termination(settings.time_limit, termination_checker_t::root_tag_t{}); + // TODO: potential for virtual function calls in a loop here... eh, very unlikely to be a + // bottleneck + termination.set_termination_callback( + [](void* termination_callback_data) { + auto settings = static_cast*>(termination_callback_data); + for (auto callback : settings->get_lp_callbacks()) { + if (callback->get_type() != internals::base_solution_callback_type::CHECK_TERMINATION) { + continue; + } + auto check_termination_callback = + static_cast(callback); + if (check_termination_callback->check_termination()) { return true; } + } + return false; + }, + &settings); detail::problem_t problem(op_problem); @@ -890,7 +896,7 @@ optimization_problem_solution_t solve_lp( // Note that this is not the presolve time, but the time limit for presolve. // But no less than 1 second, to avoid early timeout triggering known crashes const double presolve_time_limit = - std::max(1.0, std::min(0.1 * lp_timer.remaining_time(), 60.0)); + std::max(1.0, std::min(0.1 * termination.remaining_time(), 60.0)); presolver = std::make_unique>(); auto result = presolver->apply(op_problem, cuopt::linear_programming::problem_category_t::LP, @@ -903,7 +909,7 @@ optimization_problem_solution_t solve_lp( pdlp_termination_status_t::PrimalInfeasible, op_problem.get_handle_ptr()->get_stream()); } problem = detail::problem_t(result->reduced_problem); - presolve_time = lp_timer.elapsed_time(); + presolve_time = termination.elapsed_time(); CUOPT_LOG_INFO("Papilo presolve time: %f", presolve_time); } @@ -921,7 +927,7 @@ optimization_problem_solution_t solve_lp( setup_device_symbols(op_problem.get_handle_ptr()->get_stream()); - auto solution = solve_lp_with_method(problem, settings, lp_timer, is_batch_mode); + auto solution = solve_lp_with_method(problem, settings, termination, is_batch_mode); if (run_presolve) { auto primal_solution = cuopt::device_copy(solution.get_primal_solution(), @@ -1083,7 +1089,7 @@ optimization_problem_solution_t solve_lp( template optimization_problem_solution_t solve_lp_with_method( \ detail::problem_t& problem, \ pdlp_solver_settings_t const& settings, \ - const timer_t& timer, \ + const termination_checker_t& timer, \ bool is_batch_mode); \ \ template optimization_problem_t mps_data_model_to_optimization_problem( \ diff --git a/cpp/src/linear_programming/solve.cuh b/cpp/src/linear_programming/solve.cuh index a3c3240f4..698ee2445 100644 --- a/cpp/src/linear_programming/solve.cuh +++ b/cpp/src/linear_programming/solve.cuh @@ -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 */ @@ -24,7 +24,7 @@ template cuopt::linear_programming::optimization_problem_solution_t solve_lp_with_method( detail::problem_t& problem, pdlp_solver_settings_t const& settings, - const timer_t& timer, + const termination_checker_t& timer, bool is_batch_mode = false); template diff --git a/cpp/src/linear_programming/solver_termination.hpp b/cpp/src/linear_programming/solver_termination.hpp deleted file mode 100644 index 5517bd8b4..000000000 --- a/cpp/src/linear_programming/solver_termination.hpp +++ /dev/null @@ -1,105 +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 */ - -#pragma once - -#include "user_interrupt_handler.hpp" - -#include -#include - -namespace cuopt::linear_programming { - -/** - * @brief Controls solver termination based on time limit, user interrupt, and parent termination. - * - * This class owns its own timer and automatically registers with the global interrupt handler. - * It can optionally be linked to a parent termination object (for sub-MIPs) to inherit - * termination conditions. - * - * Usage: - * // Root termination (main solver) - * solver_termination_t termination(60.0); - * - * // Slave termination (sub-MIP, linked to parent) - * solver_termination_t sub_termination(10.0, &parent_termination); - * - * // Or inline with context - * solver_termination_t sub_termination(10.0, context.termination); - */ -class solver_termination_t { - public: - /** - * @brief Construct a termination object. - * @param time_limit Time limit in seconds. - * @param parent Optional parent termination object to check for termination. - */ - explicit solver_termination_t(double time_limit, solver_termination_t* parent = nullptr) - : timer_(time_limit), parent_(parent) - { - callback_id_ = user_interrupt_handler_t::instance().register_callback( - [this]() { request_user_termination(); }); - } - - ~solver_termination_t() - { - user_interrupt_handler_t::instance().unregister_callback(callback_id_); - } - - // Non-copyable, non-movable (due to registered callback with 'this' pointer) - solver_termination_t(const solver_termination_t&) = delete; - solver_termination_t& operator=(const solver_termination_t&) = delete; - solver_termination_t(solver_termination_t&&) = delete; - solver_termination_t& operator=(solver_termination_t&&) = delete; - - /** - * @brief Check if the solver should terminate. - * @return true if user interrupt requested, time limit reached, or parent terminated. - */ - bool should_terminate() const - { - if (user_interrupt_requested_.load(std::memory_order_relaxed)) { return true; } - if (timer_.check_time_limit()) { return true; } - if (parent_ != nullptr && parent_->should_terminate()) { return true; } - return false; - } - - /** - * @brief Check if termination was due to user interrupt. - * @return true if user requested termination via Ctrl-C. - */ - bool user_interrupt_requested() const - { - return user_interrupt_requested_.load(std::memory_order_relaxed); - } - - /** - * @brief Request termination due to user interrupt (e.g., Ctrl-C). - */ - void request_user_termination() - { - user_interrupt_requested_.store(true, std::memory_order_relaxed); - } - - /** - * @brief Get remaining time in seconds. - */ - double remaining_time() const { return timer_.remaining_time(); } - - /** - * @brief Get elapsed time in seconds. - */ - double elapsed_time() const { return timer_.elapsed_time(); } - - private: - timer_t timer_; - solver_termination_t* parent_; - std::atomic user_interrupt_requested_{false}; - size_t callback_id_; -}; - -} // namespace cuopt::linear_programming diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cu index 4e3dc6465..521d64102 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -394,6 +394,19 @@ solver_settings_t::get_mip_callbacks() const return mip_settings.get_mip_callbacks(); } +template +void solver_settings_t::set_lp_callback(internals::base_solution_callback_t* callback) +{ + pdlp_settings.set_lp_callback(callback); +} + +template +const std::vector +solver_settings_t::get_lp_callbacks() const +{ + return pdlp_settings.get_lp_callbacks(); +} + template pdlp_solver_settings_t& solver_settings_t::get_pdlp_settings() { diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index 833ee0fc0..4fdd71335 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -8,11 +8,11 @@ #include "cuda_profiler_api.h" #include "diversity_manager.cuh" -#include #include #include #include #include +#include #include @@ -52,7 +52,7 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_thandle_ptr->get_stream()), ls(context, lp_optimal_solution), rins(context, *this), - timer(diversity_config.default_time_limit), + timer(diversity_config.default_time_limit, context.termination), bound_prop_recombiner(context, context.problem_ptr->n_variables, ls.constraint_prop, @@ -106,7 +106,7 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_t bool diversity_manager_t::run_local_search(solution_t& solution, const weight_t& weights, - timer_t& timer, + termination_checker_t& timer, ls_config_t& ls_config) { raft::common::nvtx::range fun_scope("run_local_search"); @@ -148,6 +148,7 @@ void diversity_manager_t::add_user_given_solutions( sol, problem_ptr->integer_indices, lp_settings, + timer, static_cast*>(nullptr)); raft::copy(sol.assignment.data(), init_sol_assignment.data(), @@ -177,7 +178,7 @@ bool diversity_manager_t::run_presolve(f_t time_limit) { raft::common::nvtx::range fun_scope("run_presolve"); CUOPT_LOG_INFO("Running presolve!"); - timer_t presolve_timer(time_limit); + termination_checker_t presolve_timer(time_limit, timer); 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(); @@ -219,7 +220,7 @@ void diversity_manager_t::generate_quick_feasible_solution() // min 1 second, max 10 seconds const f_t generate_fast_solution_time = std::min(diversity_config.max_fast_sol_time, std::max(1., timer.remaining_time() / 20.)); - timer_t sol_timer(generate_fast_solution_time); + termination_checker_t sol_timer(generate_fast_solution_time, timer); // do very short LP run to get somewhere close to the optimal point ls.generate_fast_solution(solution, sol_timer); if (solution.get_feasible()) { @@ -244,7 +245,7 @@ void diversity_manager_t::generate_quick_feasible_solution() template bool diversity_manager_t::check_b_b_preemption() { - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { if (population.current_size() == 0) { population.allocate_solutions(); } population.add_external_solutions_to_population(); return true; @@ -326,7 +327,7 @@ solution_t diversity_manager_t::run_solver() const f_t max_time_on_probing = diversity_config.max_time_on_probing; f_t time_for_probing_cache = std::min(max_time_on_probing, time_limit * time_ratio_of_probing_cache); - timer_t probing_timer{time_for_probing_cache}; + termination_checker_t probing_timer{time_for_probing_cache, timer}; if (check_b_b_preemption()) { return population.best_feasible(); } if (!fj_only_run) { compute_probing_cache(ls.constraint_prop.bounds_update, *problem_ptr, probing_timer); @@ -358,7 +359,7 @@ solution_t diversity_manager_t::run_solver() pdlp_settings.pdlp_solver_mode = pdlp_solver_mode_t::Stable2; pdlp_settings.num_gpus = context.settings.num_gpus; - timer_t lp_timer(lp_time_limit); + termination_checker_t lp_timer(lp_time_limit, timer); auto lp_result = solve_lp_with_method(*problem_ptr, pdlp_settings, lp_timer); { @@ -474,7 +475,7 @@ solution_t diversity_manager_t::run_solver() rins.enable(); generate_solution(timer.remaining_time(), false); - if (context.termination.should_terminate()) { + if (context.termination.check()) { population.add_external_solutions_to_population(); return population.best_feasible(); } @@ -502,7 +503,7 @@ void diversity_manager_t::diversity_step(i_t max_iterations_without_im CUOPT_LOG_DEBUG("Population degenerated in diversity step"); return; } - if (context.termination.should_terminate()) { return; } + if (context.termination.check()) { return; } constexpr bool tournament = true; auto [sol1, sol2] = population.get_two_random(tournament); cuopt_assert(population.test_invariant(), ""); @@ -555,7 +556,7 @@ void diversity_manager_t::recombine_and_ls_with_all(solution_t::recombine_and_ls_with_all( population.add_solution(std::move(solution_t(sol))); } for (auto& sol : solutions) { - if (context.termination.should_terminate()) { return; } + if (context.termination.check()) { return; } solution_t ls_solution(sol); ls_config_t ls_config; run_local_search(ls_solution, population.weights, timer, ls_config); - if (context.termination.should_terminate()) { return; } + if (context.termination.check()) { return; } // TODO try if running LP with integers fixed makes it feasible if (ls_solution.get_feasible()) { CUOPT_LOG_DEBUG("LS searched solution feasible, running recombiners!"); @@ -682,6 +683,7 @@ diversity_manager_t::recombine_and_local_search(solution_t& lp_offspring, lp_offspring.problem_ptr->integer_indices, lp_settings, + timer, &ls.constraint_prop.bounds_update, true /* check fixed assignment is feasible */, true /* use integer fixed problem */); diff --git a/cpp/src/mip/diversity/diversity_manager.cuh b/cpp/src/mip/diversity/diversity_manager.cuh index 9f3b4c90f..56e31ea58 100644 --- a/cpp/src/mip/diversity/diversity_manager.cuh +++ b/cpp/src/mip/diversity/diversity_manager.cuh @@ -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 */ @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace cuopt::linear_programming::detail { @@ -63,7 +64,7 @@ class diversity_manager_t { solution_t& sol2); bool run_local_search(solution_t& solution, const weight_t& weights, - timer_t& timer, + termination_checker_t& timer, ls_config_t& ls_config); void set_simplex_solution(const std::vector& solution, @@ -79,7 +80,7 @@ class diversity_manager_t { rmm::device_uvector lp_dual_optimal_solution; std::atomic simplex_solution_exists{false}; local_search_t ls; - cuopt::timer_t timer; + termination_checker_t timer; bound_prop_recombiner_t bound_prop_recombiner; fp_recombiner_t fp_recombiner; line_segment_recombiner_t line_segment_recombiner; diff --git a/cpp/src/mip/diversity/lns/rins.cu b/cpp/src/mip/diversity/lns/rins.cu index 954fc3d25..e9a54b3e5 100644 --- a/cpp/src/mip/diversity/lns/rins.cu +++ b/cpp/src/mip/diversity/lns/rins.cu @@ -17,11 +17,11 @@ #include -#include #include #include #include #include +#include namespace cuopt::linear_programming::detail { template @@ -218,8 +218,9 @@ void rins_t::run_rins() std::vector> rins_solution_queue; + termination_checker_t rins_termination(time_limit, context.termination); mip_solver_context_t fj_context( - &rins_handle, &fixed_problem, context.settings, context.scaling, time_limit); + &rins_handle, &fixed_problem, context.settings, context.scaling, rins_termination); fj_t fj(fj_context); solution_t fj_solution(fixed_problem); fj_solution.copy_new_assignment(cuopt::host_copy(fixed_assignment)); @@ -251,7 +252,6 @@ void rins_t::run_rins() branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); // Termination control (linked to parent, handles Ctrl-C) - solver_termination_t rins_termination(time_limit, &context.termination); branch_and_bound_settings.termination = &rins_termination; // Fill in the settings for branch and bound diff --git a/cpp/src/mip/diversity/population.cu b/cpp/src/mip/diversity/population.cu index 766ed09cb..20ed231a8 100644 --- a/cpp/src/mip/diversity/population.cu +++ b/cpp/src/mip/diversity/population.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 */ @@ -44,7 +44,7 @@ population_t::population_t(std::string const& name_, rng(cuopt::seed_generator::get_seed()), early_exit_primal_generation(false), population_hash_map(*problem_ptr), - timer(0) + timer(0, context.termination) { best_feasible_objective = std::numeric_limits::max(); } @@ -724,7 +724,7 @@ void population_t::start_threshold_adjustment() } template -void population_t::adjust_threshold(cuopt::timer_t timer) +void population_t::adjust_threshold(const termination_checker_t& timer) { double time_ratio = (timer.elapsed_time() - population_start_time) / (timer.get_time_limit() - population_start_time); diff --git a/cpp/src/mip/diversity/population.cuh b/cpp/src/mip/diversity/population.cuh index 05f22b623..eba63b8f3 100644 --- a/cpp/src/mip/diversity/population.cuh +++ b/cpp/src/mip/diversity/population.cuh @@ -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 */ @@ -122,7 +122,7 @@ class population_t { // updates qualities of each solution void update_qualities(); // adjusts the threshold of the population - void adjust_threshold(cuopt::timer_t timer); + void adjust_threshold(const termination_checker_t& timer); /*! \param sol { Input solution } * \return { Index of the best solution similar to sol. If no similar is found we return * max_solutions. }*/ @@ -206,7 +206,7 @@ class population_t { std::atomic solutions_in_external_queue_ = false; f_t best_feasible_objective = std::numeric_limits::max(); assignment_hash_map_t population_hash_map; - cuopt::timer_t timer; + termination_checker_t timer; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/diversity/recombiners/bound_prop_recombiner.cuh b/cpp/src/mip/diversity/recombiners/bound_prop_recombiner.cuh index 051d51483..4a94786d1 100644 --- a/cpp/src/mip/diversity/recombiners/bound_prop_recombiner.cuh +++ b/cpp/src/mip/diversity/recombiners/bound_prop_recombiner.cuh @@ -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 */ @@ -177,7 +177,8 @@ class bound_prop_recombiner_t : public recombiner_t { if (guiding_solution.get_feasible() && !a.problem_ptr->expensive_to_fix_vars) { this->compute_vars_to_fix(offspring, vars_to_fix, n_vars_from_other, n_vars_from_guiding); auto [fixed_problem, fixed_assignment, variable_map] = offspring.fix_variables(vars_to_fix); - timer_t timer(bp_recombiner_config_t::bounds_prop_time_limit); + termination_checker_t timer(bp_recombiner_config_t::bounds_prop_time_limit, + this->context.termination); rmm::device_uvector old_assignment(offspring.assignment, offspring.handle_ptr->get_stream()); offspring.handle_ptr->sync_stream(); @@ -211,7 +212,8 @@ class bound_prop_recombiner_t : public recombiner_t { // "Feasible after unfix should be same as feasible after bounds prop!"); a.handle_ptr->sync_stream(); } else { - timer_t timer(bp_recombiner_config_t::bounds_prop_time_limit); + termination_checker_t timer(bp_recombiner_config_t::bounds_prop_time_limit, + this->context.termination); get_probing_values_for_infeasible( guiding_solution, other_solution, offspring, probing_values, n_vars_from_other); probing_config.probing_values = host_copy(probing_values); diff --git a/cpp/src/mip/diversity/recombiners/fp_recombiner.cuh b/cpp/src/mip/diversity/recombiners/fp_recombiner.cuh index 1daaf3e51..ef3bd3fcd 100644 --- a/cpp/src/mip/diversity/recombiners/fp_recombiner.cuh +++ b/cpp/src/mip/diversity/recombiners/fp_recombiner.cuh @@ -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 */ @@ -77,8 +77,11 @@ class fp_recombiner_t : public recombiner_t { lp_settings.save_state = true; lp_settings.check_infeasibility = true; // run lp with infeasibility detection on - auto lp_response = - get_relaxed_lp_solution(fixed_problem, fixed_assignment, offspring.lp_state, lp_settings); + auto lp_response = get_relaxed_lp_solution(fixed_problem, + fixed_assignment, + offspring.lp_state, + lp_settings, + this->context.termination); if (lp_response.get_termination_status() == pdlp_termination_status_t::PrimalInfeasible || lp_response.get_termination_status() == pdlp_termination_status_t::DualInfeasible || lp_response.get_termination_status() == pdlp_termination_status_t::TimeLimit) { @@ -96,7 +99,7 @@ class fp_recombiner_t : public recombiner_t { offspring.handle_ptr->sync_stream(); offspring.assignment = std::move(fixed_assignment); cuopt_func_call(offspring.test_variable_bounds(false)); - timer_t timer(fp_recombiner_config_t::fp_time_limit); + termination_checker_t timer(fp_recombiner_config_t::fp_time_limit, this->context.termination); fp.timer = timer; fp.cycle_queue.reset(offspring); fp.reset(); diff --git a/cpp/src/mip/diversity/recombiners/line_segment_recombiner.cuh b/cpp/src/mip/diversity/recombiners/line_segment_recombiner.cuh index b39ee8542..638674abd 100644 --- a/cpp/src/mip/diversity/recombiners/line_segment_recombiner.cuh +++ b/cpp/src/mip/diversity/recombiners/line_segment_recombiner.cuh @@ -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 */ @@ -75,7 +75,8 @@ class line_segment_recombiner_t : public recombiner_t { auto& other_solution = a.get_feasible() ? b : a; // copy the solution from A solution_t offspring(guiding_solution); - timer_t line_segment_timer{ls_recombiner_config_t::time_limit}; + termination_checker_t line_segment_timer{ls_recombiner_config_t::time_limit, + this->context.termination}; // TODO after we have the conic combination, detect the lambda change // (i.e. the integral variables flip on line segment) i_t n_points_to_search = ls_recombiner_config_t::n_points_to_search; diff --git a/cpp/src/mip/diversity/recombiners/sub_mip.cuh b/cpp/src/mip/diversity/recombiners/sub_mip.cuh index 30f9dcbf7..01bd48873 100644 --- a/cpp/src/mip/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip/diversity/recombiners/sub_mip.cuh @@ -13,7 +13,7 @@ #include #include #include -#include +#include namespace cuopt::linear_programming::detail { @@ -99,8 +99,8 @@ class sub_mip_recombiner_t : public recombiner_t { branch_and_bound_solution.resize(branch_and_bound_problem.num_cols); // Termination control (linked to parent, handles Ctrl-C) - solver_termination_t sub_mip_termination(sub_mip_recombiner_config_t::sub_mip_time_limit, - &context.termination); + termination_checker_t sub_mip_termination(sub_mip_recombiner_config_t::sub_mip_time_limit, + context.termination); branch_and_bound_settings.termination = &sub_mip_termination; // Fill in the settings for branch and bound diff --git a/cpp/src/mip/feasibility_jump/feasibility_jump.cu b/cpp/src/mip/feasibility_jump/feasibility_jump.cu index 5f68dbdd5..2c41f00a2 100644 --- a/cpp/src/mip/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip/feasibility_jump/feasibility_jump.cu @@ -862,14 +862,14 @@ i_t fj_t::host_loop(solution_t& solution, i_t climber_idx) data.incumbent_quality.set_value_async(obj, handle_ptr->get_stream()); - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit, context.termination); i_t steps; bool limit_reached = false; for (steps = 0; steps < std::numeric_limits::max(); steps += iterations_per_graph) { // to actualize time limit handle_ptr->sync_stream(); - if (timer.check_time_limit() || steps >= settings.iteration_limit || - context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (timer.check() || steps >= settings.iteration_limit || + context.preempt_heuristic_solver_.load() || context.termination.check()) { limit_reached = true; } @@ -1047,7 +1047,7 @@ template i_t fj_t::solve(solution_t& solution) { raft::common::nvtx::range scope("fj_solve"); - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit, context.termination); handle_ptr = const_cast(solution.handle_ptr); pb_ptr = solution.problem_ptr; if (settings.mode != fj_mode_t::ROUNDING) { diff --git a/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.cu b/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.cu index 76d930b45..0637c87d1 100644 --- a/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.cu +++ b/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.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 */ @@ -51,7 +51,7 @@ feasibility_pump_t::feasibility_pump_t( context.problem_ptr->handle_ptr->get_stream()), lp_optimal_solution(lp_optimal_solution_), rng(cuopt::seed_generator::get_seed()), - timer(20.) + timer(20., context.termination) { } @@ -217,7 +217,7 @@ bool feasibility_pump_t::linear_project_onto_polytope(solution_t::round(solution_t& solution) { bool result; CUOPT_LOG_DEBUG("Rounding the point"); - timer_t bounds_prop_timer(std::max(0.05, std::min(0.5, timer.remaining_time() / 10.))); + termination_checker_t bounds_prop_timer( + std::max(0.05, std::min(0.5, timer.remaining_time() / 10.)), context.termination); const f_t lp_run_time_after_feasible = 0.; bool old_var = constraint_prop.round_all_vars; f_t old_time = constraint_prop.max_time_for_bounds_prop; @@ -477,7 +478,7 @@ bool feasibility_pump_t::run_single_fp_descent(solution_t& s solution.assignment.size(), solution.handle_ptr->get_stream()); while (true) { - if (timer.check_time_limit()) { + if (timer.check()) { CUOPT_LOG_DEBUG("FP time limit reached!"); round(solution); return false; @@ -530,6 +531,7 @@ bool feasibility_pump_t::run_single_fp_descent(solution_t& s solution, solution.problem_ptr->integer_indices, lp_settings, + timer, &constraint_prop.bounds_update); is_feasible = solution.get_feasible(); n_integers = solution.compute_number_of_integers(); @@ -547,7 +549,7 @@ bool feasibility_pump_t::run_single_fp_descent(solution_t& s const f_t time_ratio = 0.2; is_feasible = test_fj_feasible(solution, time_ratio * proj_and_round_time); } - if (timer.check_time_limit()) { + if (timer.check()) { CUOPT_LOG_DEBUG("FP time limit reached!"); return false; } diff --git a/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.cuh b/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.cuh index 1a771da3f..ff3efe6df 100644 --- a/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.cuh +++ b/cpp/src/mip/local_search/feasibility_pump/feasibility_pump.cuh @@ -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 */ @@ -127,7 +127,7 @@ class feasibility_pump_t { bool check_distance_cycle(solution_t& solution); void reset(); void resize_vectors(problem_t& problem, const raft::handle_t* handle_ptr); - bool random_round_with_fj(solution_t& solution, timer_t& round_timer); + bool random_round_with_fj(solution_t& solution, termination_checker_t& round_timer); bool round_multiple_points(solution_t& solution); void relax_general_integers(solution_t& solution); void revert_relaxation(solution_t& solution); @@ -155,7 +155,7 @@ class feasibility_pump_t { f_t proj_begin; i_t n_fj_single_descents; i_t max_n_of_integers = 0; - cuopt::timer_t timer; + termination_checker_t timer; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/local_search/line_segment_search/line_segment_search.cu b/cpp/src/mip/local_search/line_segment_search/line_segment_search.cu index a1477276a..19e4e374b 100644 --- a/cpp/src/mip/local_search/line_segment_search/line_segment_search.cu +++ b/cpp/src/mip/local_search/line_segment_search/line_segment_search.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 */ @@ -128,7 +128,7 @@ bool line_segment_search_t::search_line_segment( const rmm::device_uvector& point_2, const rmm::device_uvector& delta_vector, bool is_feasibility_run, - cuopt::timer_t& timer) + const termination_checker_t& timer) { CUOPT_LOG_DEBUG("Running line segment search with a given delta vector"); cuopt_assert(point_1.size() == point_2.size(), "size mismatch"); @@ -186,7 +186,7 @@ bool line_segment_search_t::search_line_segment( best_feasible_cost, curr_cost); } - if (timer.check_time_limit()) { break; } + if (timer.check()) { break; } i_t number_of_integer_var_diff = compute_number_of_integer_var_diff( solution.problem_ptr->integer_indices, solution.assignment, @@ -224,7 +224,7 @@ bool line_segment_search_t::search_line_segment( best_feasible_cost, curr_cost); } - if (timer.check_time_limit()) { break; } + if (timer.check()) { break; } } // if not recombiner mode but local search mode if (!settings.recombiner_mode) { @@ -263,7 +263,7 @@ bool line_segment_search_t::search_line_segment(solution_t& const rmm::device_uvector& point_1, const rmm::device_uvector& point_2, bool is_feasibility_run, - cuopt::timer_t& timer) + const termination_checker_t& timer) { CUOPT_LOG_DEBUG("Running line segment search"); cuopt_assert(point_1.size() == point_2.size(), "size mismatch"); diff --git a/cpp/src/mip/local_search/line_segment_search/line_segment_search.cuh b/cpp/src/mip/local_search/line_segment_search/line_segment_search.cuh index 6e81ef938..c327c081a 100644 --- a/cpp/src/mip/local_search/line_segment_search/line_segment_search.cuh +++ b/cpp/src/mip/local_search/line_segment_search/line_segment_search.cuh @@ -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 */ @@ -31,14 +31,14 @@ class line_segment_search_t { const rmm::device_uvector& point_1, const rmm::device_uvector& point_2, bool is_feasibility_run, - cuopt::timer_t& timer); + const termination_checker_t& timer); bool search_line_segment(solution_t& solution, const rmm::device_uvector& point_1, const rmm::device_uvector& point_2, const rmm::device_uvector& delta_vector, bool is_feasibility_run, - cuopt::timer_t& timer); + const termination_checker_t& timer); void save_solution_if_better(solution_t& solution, const rmm::device_uvector& point_1, diff --git a/cpp/src/mip/local_search/local_search.cu b/cpp/src/mip/local_search/local_search.cu index 5225fccd4..2603fc3ff 100644 --- a/cpp/src/mip/local_search/local_search.cu +++ b/cpp/src/mip/local_search/local_search.cu @@ -150,7 +150,7 @@ bool local_search_t::do_fj_solve(solution_t& solution, { if (time_limit == 0.) return solution.get_feasible(); - timer_t timer(time_limit); + termination_checker_t timer(time_limit, context.termination); auto h_weights = cuopt::host_copy(in_fj.cstr_weights, solution.handle_ptr->get_stream()); auto h_objective_weight = in_fj.objective_weight.value(solution.handle_ptr->get_stream()); @@ -233,7 +233,8 @@ bool local_search_t::do_fj_solve(solution_t& solution, } template -void local_search_t::generate_fast_solution(solution_t& solution, timer_t timer) +void local_search_t::generate_fast_solution(solution_t& solution, + const termination_checker_t& timer) { thrust::fill(solution.handle_ptr->get_thrust_policy(), solution.assignment.begin(), @@ -245,12 +246,13 @@ void local_search_t::generate_fast_solution(solution_t& solu fj.settings.update_weights = true; fj.settings.feasibility_run = true; fj.settings.time_limit = std::min(30., timer.remaining_time()); - while (!timer.check_time_limit()) { - timer_t constr_prop_timer = timer_t(std::min(timer.remaining_time(), 2.)); + while (!timer.check()) { + termination_checker_t constr_prop_timer = + termination_checker_t(std::min(timer.remaining_time(), 2.), context.termination); // do constraint prop on lp optimal solution constraint_prop.apply_round(solution, 1., constr_prop_timer); if (solution.compute_feasibility()) { return; } - if (timer.check_time_limit()) { return; }; + if (timer.check()) { return; }; f_t time_limit = std::min(3., timer.remaining_time()); // run fj on the solution do_fj_solve(solution, fj, time_limit, "fast"); @@ -263,20 +265,21 @@ void local_search_t::generate_fast_solution(solution_t& solu template bool local_search_t::run_local_search(solution_t& solution, const weight_t& weights, - timer_t timer, + const termination_checker_t& in_timer, const ls_config_t& ls_config) { raft::common::nvtx::range fun_scope("local search"); fj_settings_t fj_settings; - if (timer.check_time_limit()) return false; + auto timer = in_timer; + if (timer.check()) return false; // adjust these time limits if (!solution.get_feasible()) { if (ls_config.at_least_one_parent_feasible) { fj_settings.time_limit = 0.5; - timer = timer_t(fj_settings.time_limit); + timer = termination_checker_t(fj_settings.time_limit, context.termination); } else { fj_settings.time_limit = 0.25; - timer = timer_t(fj_settings.time_limit); + timer = termination_checker_t(fj_settings.time_limit, context.termination); } } else { fj_settings.time_limit = std::min(1., timer.remaining_time()); @@ -307,7 +310,7 @@ bool local_search_t::run_local_search(solution_t& solution, template bool local_search_t::run_fj_until_timer(solution_t& solution, const weight_t& weights, - timer_t timer) + const termination_checker_t& timer) { bool is_feasible; fj.settings.n_of_minimums_for_exit = 1e6; @@ -319,13 +322,13 @@ bool local_search_t::run_fj_until_timer(solution_t& solution do_fj_solve(solution, fj, time_limit, "until_timer"); CUOPT_LOG_DEBUG("Initial FJ feasibility done"); is_feasible = solution.compute_feasibility(); - if (fj.settings.feasibility_run || timer.check_time_limit()) { return is_feasible; } + if (fj.settings.feasibility_run || timer.check()) { return is_feasible; } return is_feasible; } template bool local_search_t::run_fj_annealing(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, const ls_config_t& ls_config) { raft::common::nvtx::range fun_scope("run_fj_annealing"); @@ -355,7 +358,7 @@ bool local_search_t::run_fj_annealing(solution_t& solution, template bool local_search_t::run_fj_line_segment(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, const ls_config_t& ls_config) { raft::common::nvtx::range fun_scope("run_fj_line_segment"); @@ -378,7 +381,7 @@ bool local_search_t::run_fj_line_segment(solution_t& solutio template bool local_search_t::check_fj_on_lp_optimal(solution_t& solution, bool perturb, - timer_t timer) + const termination_checker_t& timer) { raft::common::nvtx::range fun_scope("check_fj_on_lp_optimal"); if (lp_optimal_exists) { @@ -395,7 +398,8 @@ bool local_search_t::check_fj_on_lp_optimal(solution_t& solu } cuopt_func_call(solution.test_variable_bounds(false)); f_t lp_run_time_after_feasible = std::min(1., timer.remaining_time()); - timer_t bounds_prop_timer = timer_t(std::min(timer.remaining_time(), 10.)); + termination_checker_t bounds_prop_timer = + termination_checker_t(std::min(timer.remaining_time(), 10.), context.termination); bool is_feasible = constraint_prop.apply_round(solution, lp_run_time_after_feasible, bounds_prop_timer); if (!is_feasible) { @@ -404,7 +408,7 @@ bool local_search_t::check_fj_on_lp_optimal(solution_t& solu lp_settings.time_limit = std::min(lp_run_time, timer.remaining_time()); lp_settings.tolerance = solution.problem_ptr->tolerances.absolute_tolerance; run_lp_with_vars_fixed( - *solution.problem_ptr, solution, solution.problem_ptr->integer_indices, lp_settings); + *solution.problem_ptr, solution, solution.problem_ptr->integer_indices, lp_settings, timer); } else { return is_feasible; } @@ -419,7 +423,8 @@ bool local_search_t::check_fj_on_lp_optimal(solution_t& solu } template -bool local_search_t::run_fj_on_zero(solution_t& solution, timer_t timer) +bool local_search_t::run_fj_on_zero(solution_t& solution, + const termination_checker_t& timer) { raft::common::nvtx::range fun_scope("run_fj_on_zero"); thrust::fill(solution.handle_ptr->get_thrust_policy(), @@ -438,7 +443,7 @@ bool local_search_t::run_fj_on_zero(solution_t& solution, ti template bool local_search_t::run_staged_fp(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, population_t* population_ptr) { raft::common::nvtx::range fun_scope("run_staged_fp"); @@ -458,19 +463,19 @@ bool local_search_t::run_staged_fp(solution_t& solution, fp.cycle_queue.reset(solution); fp.reset(); fp.resize_vectors(*solution.problem_ptr, solution.handle_ptr); - for (i_t i = 0; i < n_fp_iterations && !timer.check_time_limit(); ++i) { + for (i_t i = 0; i < n_fp_iterations && !timer.check(); ++i) { population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return false; } CUOPT_LOG_DEBUG("Running staged FP from beginning it %d", i); fp.relax_general_integers(solution); - timer_t binary_timer(timer.remaining_time() / 3); + termination_checker_t binary_timer(timer.remaining_time() / 3, context.termination); i_t binary_it_counter = 0; for (; binary_it_counter < 100; ++binary_it_counter) { population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return false; } @@ -478,7 +483,7 @@ bool local_search_t::run_staged_fp(solution_t& solution, "Running binary problem from it %d large_restart_it %d", binary_it_counter, i); is_feasible = fp.run_single_fp_descent(solution); if (is_feasible) { break; } - if (timer.check_time_limit()) { + if (timer.check()) { fp.revert_relaxation(solution); solution.round_nearest(); CUOPT_LOG_DEBUG("Time limit reached during binary stage!"); @@ -487,7 +492,7 @@ bool local_search_t::run_staged_fp(solution_t& solution, is_feasible = fp.restart_fp(solution); if (is_feasible) { break; } // give the integer FP some chance - if (binary_timer.check_time_limit()) { + if (binary_timer.check()) { CUOPT_LOG_DEBUG("Binary FP time limit reached during binary stage!"); break; } @@ -505,7 +510,7 @@ bool local_search_t::run_staged_fp(solution_t& solution, "Running integer problem from it %d large_restart_it %d", integer_it_counter, i); is_feasible = fp.run_single_fp_descent(solution); if (is_feasible) { return true; } - if (timer.check_time_limit()) { + if (timer.check()) { CUOPT_LOG_DEBUG("FP time limit reached during integer stage!"); solution.round_nearest(); return false; @@ -631,7 +636,7 @@ void local_search_t::reset_alpha_and_run_recombiners( template bool local_search_t::run_fp(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, population_t* population_ptr) { raft::common::nvtx::range fun_scope("run_fp"); @@ -643,7 +648,7 @@ bool local_search_t::run_fp(solution_t& solution, is_feasible ? solution.get_objective() : std::numeric_limits::max(); rmm::device_uvector best_solution(solution.assignment, solution.handle_ptr->get_stream()); problem_t* old_problem_ptr = solution.problem_ptr; - fp.timer = timer_t(timer.remaining_time()); + fp.timer = termination_checker_t(timer.remaining_time(), context.termination); // if it has not been initialized yet, create a new problem and move it to the cut problem if (!problem_with_objective_cut.cutting_plane_added) { problem_with_objective_cut = std::move(problem_t(*old_problem_ptr)); @@ -661,21 +666,21 @@ bool local_search_t::run_fp(solution_t& solution, resize_to_new_problem(); } i_t last_improved_iteration = 0; - for (i_t i = 0; i < n_fp_iterations && !timer.check_time_limit(); ++i) { - if (timer.check_time_limit()) { + for (i_t i = 0; i < n_fp_iterations && !timer.check(); ++i) { + if (timer.check()) { is_feasible = false; break; } CUOPT_LOG_DEBUG("fp_loop it %d last_improved_iteration %d", i, last_improved_iteration); population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); break; } is_feasible = fp.run_single_fp_descent(solution); population_ptr->add_external_solutions_to_population(); CUOPT_LOG_DEBUG("Population size at iteration %d: %d", i, population_ptr->current_size()); - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); break; } @@ -693,13 +698,13 @@ bool local_search_t::run_fp(solution_t& solution, } // if not feasible, it means it is a cycle else { - if (timer.check_time_limit()) { + if (timer.check()) { is_feasible = false; break; } is_feasible = fp.restart_fp(solution); population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); break; } @@ -744,7 +749,7 @@ bool local_search_t::generate_solution(solution_t& solution, { raft::common::nvtx::range fun_scope("generate_solution"); cuopt_assert(population_ptr != nullptr, "Population pointer must not be null"); - timer_t timer(time_limit); + termination_checker_t timer(time_limit, context.termination); auto n_vars = solution.problem_ptr->n_variables; auto n_binary_vars = solution.problem_ptr->get_n_binary_variables(); auto n_integer_vars = solution.problem_ptr->n_integer_vars; @@ -754,7 +759,7 @@ bool local_search_t::generate_solution(solution_t& solution, return true; } population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return is_feasible; } @@ -775,7 +780,7 @@ bool local_search_t::generate_solution(solution_t& solution, solution.handle_ptr->get_stream()); } population_ptr->add_external_solutions_to_population(); - if (context.preempt_heuristic_solver_.load() || context.termination.should_terminate()) { + if (context.preempt_heuristic_solver_.load() || context.termination.check()) { CUOPT_LOG_DEBUG("Preempting heuristic solver!"); return is_feasible; } diff --git a/cpp/src/mip/local_search/local_search.cuh b/cpp/src/mip/local_search/local_search.cuh index 6fdf4ac72..34520aa6a 100644 --- a/cpp/src/mip/local_search/local_search.cuh +++ b/cpp/src/mip/local_search/local_search.cuh @@ -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 */ @@ -53,31 +53,33 @@ class local_search_t { void start_cpufj_scratch_threads(population_t& population); void start_cpufj_lptopt_scratch_threads(population_t& population); void stop_cpufj_scratch_threads(); - void generate_fast_solution(solution_t& solution, timer_t timer); + void generate_fast_solution(solution_t& solution, const termination_checker_t& timer); bool generate_solution(solution_t& solution, bool perturb, population_t* population_ptr, f_t time_limit = 300.); bool run_fj_until_timer(solution_t& solution, const weight_t& weights, - timer_t timer); + const termination_checker_t& timer); bool run_local_search(solution_t& solution, const weight_t& weights, - timer_t timer, + const termination_checker_t& timer, const ls_config_t& ls_config); bool run_fj_annealing(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, const ls_config_t& ls_config); bool run_fj_line_segment(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, const ls_config_t& ls_config); - bool run_fj_on_zero(solution_t& solution, timer_t timer); - bool check_fj_on_lp_optimal(solution_t& solution, bool perturb, timer_t timer); + bool run_fj_on_zero(solution_t& solution, const termination_checker_t& timer); + bool check_fj_on_lp_optimal(solution_t& solution, + bool perturb, + const termination_checker_t& timer); bool run_staged_fp(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, population_t* population_ptr); bool run_fp(solution_t& solution, - timer_t timer, + const termination_checker_t& timer, population_t* population_ptr = nullptr); void resize_vectors(problem_t& problem, const raft::handle_t* handle_ptr); diff --git a/cpp/src/mip/local_search/rounding/bounds_repair.cu b/cpp/src/mip/local_search/rounding/bounds_repair.cu index 4679bc2a5..2009b1a25 100644 --- a/cpp/src/mip/local_search/rounding/bounds_repair.cu +++ b/cpp/src/mip/local_search/rounding/bounds_repair.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -30,7 +30,8 @@ bounds_repair_t::bounds_repair_t(const problem_t& pb, violated_cstr_map(0, pb.handle_ptr->get_stream()), total_vio(pb.handle_ptr->get_stream()), gen(cuopt::seed_generator::get_seed()), - cycle_vector(MAX_CYCLE_SEQUENCE, -1) + cycle_vector(MAX_CYCLE_SEQUENCE, -1), + timer(0, bound_presolve.context.termination) { } @@ -377,7 +378,7 @@ void bounds_repair_t::apply_move(problem_t& problem, template bool bounds_repair_t::repair_problem(problem_t& problem, problem_t& original_problem, - timer_t timer_, + termination_checker_t timer_, const raft::handle_t* handle_ptr_) { CUOPT_LOG_DEBUG("Running bounds repair"); @@ -394,7 +395,7 @@ bool bounds_repair_t::repair_problem(problem_t& problem, h_n_violated_cstr, best_violation, curr_violation); - if (timer.check_time_limit()) { break; } + if (timer.check()) { break; } i_t curr_cstr = get_random_cstr(); // best way would be to check a variable cycle, but this is easier and more performant bool is_cycle = detect_cycle(curr_cstr); diff --git a/cpp/src/mip/local_search/rounding/bounds_repair.cuh b/cpp/src/mip/local_search/rounding/bounds_repair.cuh index b5d39ee1e..b096b02c5 100644 --- a/cpp/src/mip/local_search/rounding/bounds_repair.cuh +++ b/cpp/src/mip/local_search/rounding/bounds_repair.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -120,7 +120,7 @@ class bounds_repair_t { void compute_damages(problem_t& problem, i_t n_candidates); bool repair_problem(problem_t& problem, problem_t& original_problem, - timer_t timer_, + termination_checker_t timer_, const raft::handle_t* handle_ptr_); void apply_move(problem_t& problem, problem_t& original_problem, @@ -144,7 +144,7 @@ class bounds_repair_t { i_t h_n_violated_cstr; const raft::handle_t* handle_ptr; std::mt19937 gen; - timer_t timer{0.}; + termination_checker_t timer; std::vector cycle_vector; i_t cycle_write_pos = 0; }; diff --git a/cpp/src/mip/local_search/rounding/constraint_prop.cu b/cpp/src/mip/local_search/rounding/constraint_prop.cu index 99dc3c71d..86e9d6eac 100644 --- a/cpp/src/mip/local_search/rounding/constraint_prop.cu +++ b/cpp/src/mip/local_search/rounding/constraint_prop.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 */ @@ -39,7 +39,8 @@ constraint_prop_t::constraint_prop_t(mip_solver_context_t& c ub_restore(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), assignment_restore(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), - rng(cuopt::seed_generator::get_seed(), 0, 0) + rng(cuopt::seed_generator::get_seed(), 0, 0), + max_timer(0, context.termination) { } @@ -755,7 +756,7 @@ void constraint_prop_t::restore_original_bounds_on_unfixed( template bool constraint_prop_t::run_repair_procedure(problem_t& problem, problem_t& original_problem, - timer_t& timer, + termination_checker_t& timer, const raft::handle_t* handle_ptr) { // select the first probing value @@ -767,7 +768,7 @@ bool constraint_prop_t::run_repair_procedure(problem_t& prob i_t n_of_repairs_needed_for_feasible = 0; do { n_of_repairs_needed_for_feasible++; - if (timer.check_time_limit()) { + if (timer.check()) { CUOPT_LOG_DEBUG("Time limit is reached in repair loop!"); f_t repair_end_time = timer.remaining_time(); repair_stats.total_time_spent_on_repair += repair_start_time - repair_end_time; @@ -789,7 +790,7 @@ bool constraint_prop_t::run_repair_procedure(problem_t& prob bounds_update.settings.time_limit = timer.remaining_time(); auto term_crit = bounds_update.solve(problem); bounds_update.settings.iteration_limit = 50; - if (timer.check_time_limit()) { + if (timer.check()) { CUOPT_LOG_DEBUG("Time limit is reached in repair loop!"); f_t repair_end_time = timer.remaining_time(); repair_stats.total_time_spent_on_repair += repair_start_time - repair_end_time; @@ -841,7 +842,7 @@ bool constraint_prop_t::find_integer( solution_t& sol, solution_t& orig_sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_config) { using crit_t = termination_criterion_t; @@ -859,7 +860,7 @@ bool constraint_prop_t::find_integer( multi_probe.settings.iteration_limit = 50; multi_probe.settings.time_limit = max_timer.remaining_time(); multi_probe.resize(*sol.problem_ptr); - if (max_timer.check_time_limit()) { + if (max_timer.check()) { CUOPT_LOG_DEBUG("Time limit is reached before bounds prop rounding!"); sol.round_nearest(); expand_device_copy(orig_sol.assignment, sol.assignment, sol.handle_ptr->get_stream()); @@ -931,14 +932,14 @@ bool constraint_prop_t::find_integer( while (set_count < unset_integer_vars.size()) { CUOPT_LOG_TRACE("n_set_vars %d vars to set %lu", set_count, unset_integer_vars.size()); update_host_assignment(sol); - if (max_timer.check_time_limit()) { + if (max_timer.check()) { CUOPT_LOG_DEBUG("Second time limit is reached returning nearest rounding!"); collapse_crossing_bounds(*sol.problem_ptr, *orig_sol.problem_ptr, sol.handle_ptr); sol.round_nearest(); timeout_happened = true; break; } - if (!rounding_ii && timer.check_time_limit()) { + if (!rounding_ii && timer.check()) { CUOPT_LOG_DEBUG("First time limit is reached! Continuing without backtracking and repair!"); rounding_ii = true; // this is to not try the repair procedure again @@ -971,7 +972,8 @@ bool constraint_prop_t::find_integer( sol, orig_sol.problem_ptr, var_probe_vals, &set_count, unset_integer_vars, probing_config); if (!(n_failed_repair_iterations >= max_n_failed_repair_iterations) && rounding_ii && !timeout_happened) { - timer_t repair_timer{std::min(timer.remaining_time() / 5, timer.elapsed_time() / 3)}; + termination_checker_t repair_timer( + std::min(timer.remaining_time() / 5, timer.elapsed_time() / 3), context.termination); save_bounds(sol); // update bounds and run repair procedure bool bounds_repaired = @@ -1046,6 +1048,7 @@ bool constraint_prop_t::find_integer( orig_sol, orig_sol.problem_ptr->integer_indices, lp_settings, + timer, static_cast*>(nullptr)); } bool res_feasible = orig_sol.compute_feasibility(); @@ -1057,11 +1060,11 @@ template bool constraint_prop_t::apply_round( solution_t& sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_config) { raft::common::nvtx::range fun_scope("constraint prop round"); - max_timer = timer_t{max_time_for_bounds_prop}; + max_timer = termination_checker_t(max_time_for_bounds_prop, context.termination); if (check_brute_force_rounding(sol)) { return true; } recovery_mode = false; rounding_ii = false; diff --git a/cpp/src/mip/local_search/rounding/constraint_prop.cuh b/cpp/src/mip/local_search/rounding/constraint_prop.cuh index a300a8576..f5f34c752 100644 --- a/cpp/src/mip/local_search/rounding/constraint_prop.cuh +++ b/cpp/src/mip/local_search/rounding/constraint_prop.cuh @@ -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 */ @@ -43,7 +43,7 @@ struct constraint_prop_t { constraint_prop_t(mip_solver_context_t& context); bool apply_round(solution_t& sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_config = std::nullopt); void sort_by_implied_slack_consumption(solution_t& sol, @@ -56,7 +56,7 @@ struct constraint_prop_t { bool find_integer(solution_t& sol, solution_t& orig_sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_config = std::nullopt); void find_set_integer_vars(solution_t& sol, rmm::device_uvector& set_vars); @@ -121,7 +121,7 @@ struct constraint_prop_t { const raft::handle_t* handle_ptr); bool run_repair_procedure(problem_t& problem, problem_t& original_problem, - timer_t& timer, + termination_checker_t& timer, const raft::handle_t* handle_ptr); bool handle_fixed_vars( solution_t& sol, @@ -149,7 +149,7 @@ struct constraint_prop_t { i_t bounds_prop_interval = 1; i_t n_iter_in_recovery = 0; i_t max_n_failed_repair_iterations = 1; - timer_t max_timer{0.}; + termination_checker_t max_timer; bool use_probing_cache = true; static repair_stats_t repair_stats; bool single_rounding_only = false; diff --git a/cpp/src/mip/local_search/rounding/lb_bounds_repair.cu b/cpp/src/mip/local_search/rounding/lb_bounds_repair.cu index 04ae200b9..d5e06598d 100644 --- a/cpp/src/mip/local_search/rounding/lb_bounds_repair.cu +++ b/cpp/src/mip/local_search/rounding/lb_bounds_repair.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -397,7 +397,7 @@ bool lb_bounds_repair_t::repair_problem( load_balanced_problem_t* problem, load_balanced_bounds_presolve_t& lb_bound_presolve, problem_t& original_problem, - timer_t timer_, + termination_checker_t timer_, const raft::handle_t* handle_ptr_) { CUOPT_LOG_DEBUG("Running bounds repair"); @@ -414,7 +414,7 @@ bool lb_bounds_repair_t::repair_problem( h_n_violated_cstr, best_violation, curr_violation); - if (timer.check_time_limit()) { break; } + if (timer.check()) { break; } i_t curr_cstr = get_random_cstr(); // best way would be to check a variable cycle, but this is easier and more performant bool is_cycle = detect_cycle(curr_cstr); diff --git a/cpp/src/mip/local_search/rounding/lb_bounds_repair.cuh b/cpp/src/mip/local_search/rounding/lb_bounds_repair.cuh index 0f980d6e6..cda4edbe9 100644 --- a/cpp/src/mip/local_search/rounding/lb_bounds_repair.cuh +++ b/cpp/src/mip/local_search/rounding/lb_bounds_repair.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -58,7 +58,7 @@ class lb_bounds_repair_t { bool repair_problem(load_balanced_problem_t* problem, load_balanced_bounds_presolve_t& lb_bound_presolve, problem_t& original_problem, - timer_t timer_, + termination_checker_t timer_, const raft::handle_t* handle_ptr_); void apply_move(load_balanced_problem_t* problem, problem_t& original_problem, @@ -82,7 +82,7 @@ class lb_bounds_repair_t { i_t h_n_violated_cstr; const raft::handle_t* handle_ptr; std::mt19937 gen; - timer_t timer{0.}; + termination_checker_t timer{0.}; std::vector cycle_vector; i_t cycle_write_pos = 0; }; diff --git a/cpp/src/mip/local_search/rounding/lb_constraint_prop.cu b/cpp/src/mip/local_search/rounding/lb_constraint_prop.cu index 5789ade26..b6ddcd4c9 100644 --- a/cpp/src/mip/local_search/rounding/lb_constraint_prop.cu +++ b/cpp/src/mip/local_search/rounding/lb_constraint_prop.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 */ @@ -453,7 +453,7 @@ bool lb_constraint_prop_t::run_repair_procedure( load_balanced_problem_t* problem, load_balanced_bounds_presolve_t& lb_bounds_update, problem_t& original_problem, - timer_t& timer, + termination_checker_t& timer, const raft::handle_t* handle_ptr) { lb_bounds_update.set_updated_bounds(problem); @@ -462,7 +462,7 @@ bool lb_constraint_prop_t::run_repair_procedure( i_t n_of_repairs_needed_for_feasible = 0; do { n_of_repairs_needed_for_feasible++; - if (timer.check_time_limit()) { + if (timer.check()) { CUOPT_LOG_DEBUG("Time limit is reached in repair loop!"); f_t repair_end_time = timer.remaining_time(); total_time_spent_on_repair += repair_start_time - repair_end_time; @@ -487,7 +487,7 @@ bool lb_constraint_prop_t::run_repair_procedure( lb_bounds_update.settings.iteration_limit = 20; // if time limit is reached, this is needed sometimes we reach time limit and decide that it is // not ii but propagation eventually will make it ii - if (timer.check_time_limit()) { + if (timer.check()) { CUOPT_LOG_DEBUG("Time limit is reached in repair loop!"); f_t repair_end_time = timer.remaining_time(); total_time_spent_on_repair += repair_start_time - repair_end_time; @@ -700,14 +700,14 @@ template bool lb_constraint_prop_t::apply_round( solution_t& sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_candidates) { raft::common::nvtx::range fun_scope("constraint prop round"); // this is second timer that can continue but without recovery mode const f_t max_time_for_bounds_prop = 5.; - max_timer = timer_t{max_time_for_bounds_prop}; + max_timer = termination_checker_t{max_time_for_bounds_prop}; if (check_brute_force_rounding(sol)) { return true; } recovery_mode = false; rounding_ii = false; @@ -754,7 +754,7 @@ bool lb_constraint_prop_t::find_integer( load_balanced_bounds_presolve_t& lb_bounds_update, solution_t& orig_sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_candidates) { RAFT_CHECK_CUDA(problem.handle_ptr->get_stream()); @@ -778,7 +778,7 @@ bool lb_constraint_prop_t::find_integer( lb_bounds_update.settings.iteration_limit = 20; RAFT_CHECK_CUDA(problem.handle_ptr->get_stream()); - if (max_timer.check_time_limit()) { + if (max_timer.check()) { CUOPT_LOG_DEBUG("Time limit is reached before bounds prop rounding!"); orig_sol.round_nearest(); cuopt_func_call(orig_sol.test_variable_bounds()); @@ -825,13 +825,13 @@ bool lb_constraint_prop_t::find_integer( while (set_count < unset_integer_vars.size()) { update_host_assignment(assignment, orig_sol.handle_ptr); - if (max_timer.check_time_limit()) { + if (max_timer.check()) { CUOPT_LOG_DEBUG("Second time limit is reached returning nearest rounding!"); // sol.round_nearest(); timeout_happened = true; break; } - if (!rounding_ii && timer.check_time_limit()) { + if (!rounding_ii && timer.check()) { CUOPT_LOG_DEBUG("First time limit is reached! Continuing without backtracking!"); rounding_ii = true; } @@ -868,7 +868,8 @@ bool lb_constraint_prop_t::find_integer( &set_count, unset_integer_vars); if (!repair_tried && rounding_ii && !timeout_happened) { - timer_t repair_timer{std::min(timer.remaining_time() / 5, timer.elapsed_time() / 3)}; + termination_checker_t repair_timer{ + std::min(timer.remaining_time() / 5, timer.elapsed_time() / 3)}; save_bounds(problem, assignment, orig_sol.handle_ptr); // update bounds and run repair procedure // infeasible cnst_slack invalid diff --git a/cpp/src/mip/local_search/rounding/lb_constraint_prop.cuh b/cpp/src/mip/local_search/rounding/lb_constraint_prop.cuh index 8230df984..d14b1b1e6 100644 --- a/cpp/src/mip/local_search/rounding/lb_constraint_prop.cuh +++ b/cpp/src/mip/local_search/rounding/lb_constraint_prop.cuh @@ -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 */ @@ -23,7 +23,7 @@ struct lb_constraint_prop_t { bool apply_round( solution_t& sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_candidates = std::nullopt); void sort_by_implied_slack_consumption( problem_t& original_problem, @@ -40,7 +40,7 @@ struct lb_constraint_prop_t { load_balanced_bounds_presolve_t& lb_bounds_update, solution_t& orig_sol, f_t lp_run_time_after_feasible, - timer_t& timer, + termination_checker_t& timer, std::optional>> probing_candidates); std::tuple probing_values( load_balanced_bounds_presolve_t& lb_bounds_update, @@ -83,7 +83,7 @@ struct lb_constraint_prop_t { bool run_repair_procedure(load_balanced_problem_t* problem, load_balanced_bounds_presolve_t& lb_bounds_update, problem_t& original_problem, - timer_t& timer, + termination_checker_t& timer, const raft::handle_t* handle_ptr); mip_solver_context_t& context; @@ -100,7 +100,7 @@ struct lb_constraint_prop_t { bool rounding_ii = false; i_t bounds_prop_interval = 1; i_t n_iter_in_recovery = 0; - timer_t max_timer{0.}; + termination_checker_t max_timer{0.}; bool use_probing_cache = true; size_t repair_attempts = 0; diff --git a/cpp/src/mip/presolve/bounds_presolve.cu b/cpp/src/mip/presolve/bounds_presolve.cu index 05cc26bca..7d08b53b1 100644 --- a/cpp/src/mip/presolve/bounds_presolve.cu +++ b/cpp/src/mip/presolve/bounds_presolve.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -166,8 +166,8 @@ void bound_presolve_t::set_bounds( } template -termination_criterion_t bound_presolve_t::bound_update_loop(problem_t& pb, - timer_t timer) +termination_criterion_t bound_presolve_t::bound_update_loop( + problem_t& pb, const termination_checker_t& timer) { termination_criterion_t criteria = termination_criterion_t::ITERATION_LIMIT; @@ -175,7 +175,7 @@ termination_criterion_t bound_presolve_t::bound_update_loop(problem_t< upd.init_changed_constraints(pb.handle_ptr); for (iter = 0; iter < settings.iteration_limit; ++iter) { calculate_activity(pb); - if (timer.check_time_limit()) { + if (timer.check()) { criteria = termination_criterion_t::TIME_LIMIT; CUOPT_LOG_TRACE("Exiting bounds prop because of time limit at iter %d", iter); break; @@ -229,7 +229,7 @@ termination_criterion_t bound_presolve_t::solve(problem_t& p i_t var_idx) { auto& handle_ptr = pb.handle_ptr; - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit, context.termination); copy_input_bounds(pb); upd.lb.set_element_async(var_idx, var_lb, handle_ptr->get_stream()); upd.ub.set_element_async(var_idx, var_ub, handle_ptr->get_stream()); @@ -242,7 +242,7 @@ termination_criterion_t bound_presolve_t::solve( const std::vector>& var_probe_val_pairs, bool use_host_bounds) { - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit, context.termination); auto& handle_ptr = pb.handle_ptr; if (use_host_bounds) { update_device_bounds(handle_ptr); @@ -257,7 +257,7 @@ termination_criterion_t bound_presolve_t::solve( template termination_criterion_t bound_presolve_t::solve(problem_t& pb) { - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit, context.termination); auto& handle_ptr = pb.handle_ptr; copy_input_bounds(pb); return bound_update_loop(pb, timer); diff --git a/cpp/src/mip/presolve/bounds_presolve.cuh b/cpp/src/mip/presolve/bounds_presolve.cuh index 54194b059..2327cd969 100644 --- a/cpp/src/mip/presolve/bounds_presolve.cuh +++ b/cpp/src/mip/presolve/bounds_presolve.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -57,7 +57,8 @@ class bound_presolve_t { void set_updated_bounds(const raft::handle_t* handle_ptr, raft::device_span output_lb, raft::device_span output_ub); - termination_criterion_t bound_update_loop(problem_t& pb, timer_t timer); + termination_criterion_t bound_update_loop(problem_t& pb, + const termination_checker_t& timer); void set_bounds(raft::device_span var_lb, raft::device_span var_ub, const std::vector>& var_probe_vals, diff --git a/cpp/src/mip/presolve/lb_probing_cache.cu b/cpp/src/mip/presolve/lb_probing_cache.cu index 4a03a86fd..6d57531a0 100644 --- a/cpp/src/mip/presolve/lb_probing_cache.cu +++ b/cpp/src/mip/presolve/lb_probing_cache.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -309,7 +309,7 @@ inline std::vector compute_prioritized_integer_indices( template void compute_probing_cache(load_balanced_bounds_presolve_t& bound_presolve, load_balanced_problem_t& problem, - timer_t timer) + const termination_checker_t& timer) { // we dont want to compute the probing cache for all variables for time and computation resources auto priority_indices = compute_prioritized_integer_indices(bound_presolve, problem); @@ -326,7 +326,7 @@ void compute_probing_cache(load_balanced_bounds_presolve_t& bound_pres i_t n_of_implied_singletons = 0; // for each integer var, loop around two possible values/intervals and save the implied bounds for (auto var_idx : priority_indices) { - if (timer.check_time_limit()) { break; } + if (timer.check()) { break; } f_t lb = h_var_lower_bounds[var_idx]; f_t ub = h_var_upper_bounds[var_idx]; // TODO make this more efficient, we can do double probing @@ -399,7 +399,7 @@ void compute_probing_cache(load_balanced_bounds_presolve_t& bound_pres template void compute_probing_cache( \ load_balanced_bounds_presolve_t & bound_presolve, \ load_balanced_problem_t & problem, \ - timer_t timer); \ + const termination_checker_t& timer); \ template class lb_probing_cache_t; #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/mip/presolve/load_balanced_bounds_presolve.cu b/cpp/src/mip/presolve/load_balanced_bounds_presolve.cu index 2c9cd15aa..d0cc1484b 100644 --- a/cpp/src/mip/presolve/load_balanced_bounds_presolve.cu +++ b/cpp/src/mip/presolve/load_balanced_bounds_presolve.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -526,7 +526,7 @@ bool load_balanced_bounds_presolve_t::update_bounds_from_slack( template termination_criterion_t load_balanced_bounds_presolve_t::bound_update_loop( - const raft::handle_t* handle_ptr, timer_t timer) + const raft::handle_t* handle_ptr, const termination_checker_t& timer) { termination_criterion_t criteria = termination_criterion_t::ITERATION_LIMIT; @@ -541,7 +541,7 @@ termination_criterion_t load_balanced_bounds_presolve_t::bound_update_ } break; } - if (timer.check_time_limit()) { + if (timer.check()) { criteria = termination_criterion_t::TIME_LIMIT; CUOPT_LOG_DEBUG("Exiting bounds prop because of time limit at iter %d", iter); break; @@ -626,7 +626,7 @@ termination_criterion_t load_balanced_bounds_presolve_t::solve(f_t var f_t var_ub, i_t var_idx) { - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit); auto& handle_ptr = pb->handle_ptr; copy_input_bounds(*pb); vars_bnd.set_element_async(2 * var_idx, var_lb, handle_ptr->get_stream()); @@ -638,7 +638,7 @@ template termination_criterion_t load_balanced_bounds_presolve_t::solve( raft::device_span input_bounds) { - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit); auto& handle_ptr = pb->handle_ptr; if (input_bounds.size() != 0) { raft::copy(vars_bnd.data(), input_bounds.data(), input_bounds.size(), handle_ptr->get_stream()); @@ -667,7 +667,7 @@ template termination_criterion_t load_balanced_bounds_presolve_t::solve( const std::vector>& var_probe_val_pairs, bool use_host_bounds) { - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit); auto& handle_ptr = pb->handle_ptr; if (use_host_bounds) { update_device_bounds(handle_ptr); diff --git a/cpp/src/mip/presolve/load_balanced_bounds_presolve.cuh b/cpp/src/mip/presolve/load_balanced_bounds_presolve.cuh index befce7704..5d716442e 100644 --- a/cpp/src/mip/presolve/load_balanced_bounds_presolve.cuh +++ b/cpp/src/mip/presolve/load_balanced_bounds_presolve.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -159,7 +159,8 @@ class load_balanced_bounds_presolve_t { void calculate_constraint_slack_iter(const raft::handle_t* handle_ptr); bool update_bounds_from_slack(const raft::handle_t* handle_ptr); - termination_criterion_t bound_update_loop(const raft::handle_t* handle_ptr, timer_t timer); + termination_criterion_t bound_update_loop(const raft::handle_t* handle_ptr, + const termination_checker_t& timer); bool calculate_infeasible_redundant_constraints(const raft::handle_t* handle_ptr); // void calculate_constraint_slack_on_problem_bounds(); diff --git a/cpp/src/mip/presolve/multi_probe.cu b/cpp/src/mip/presolve/multi_probe.cu index 5a11164e4..e863216c8 100644 --- a/cpp/src/mip/presolve/multi_probe.cu +++ b/cpp/src/mip/presolve/multi_probe.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -261,9 +261,8 @@ void multi_probe_t::set_bounds( } template -termination_criterion_t multi_probe_t::bound_update_loop(problem_t& pb, - const raft::handle_t* handle_ptr, - timer_t timer) +termination_criterion_t multi_probe_t::bound_update_loop( + problem_t& pb, const raft::handle_t* handle_ptr, const termination_checker_t& timer) { termination_criterion_t criteria = termination_criterion_t::ITERATION_LIMIT; skip_0 = false; @@ -280,7 +279,7 @@ termination_criterion_t multi_probe_t::bound_update_loop(problem_t::solve_for_interval( const std::tuple, std::pair>& var_interval_vals, const raft::handle_t* handle_ptr) { - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit, context.termination); copy_problem_into_probing_buffers(pb, handle_ptr); set_interval_bounds(var_interval_vals, pb, handle_ptr); @@ -384,7 +383,7 @@ termination_criterion_t multi_probe_t::solve( const std::tuple, std::vector, std::vector>& var_probe_vals, bool use_host_bounds) { - timer_t timer(settings.time_limit); + termination_checker_t timer(settings.time_limit, context.termination); auto& handle_ptr = pb.handle_ptr; if (use_host_bounds) { update_device_bounds(handle_ptr); diff --git a/cpp/src/mip/presolve/multi_probe.cuh b/cpp/src/mip/presolve/multi_probe.cuh index 90cc3066e..4624be914 100644 --- a/cpp/src/mip/presolve/multi_probe.cuh +++ b/cpp/src/mip/presolve/multi_probe.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -54,7 +54,7 @@ class multi_probe_t { i_t select_update); termination_criterion_t bound_update_loop(problem_t& pb, const raft::handle_t* handle_ptr, - timer_t timer); + const termination_checker_t& timer); void set_interval_bounds( const std::tuple, std::pair>& var_interval_vals, problem_t& pb, diff --git a/cpp/src/mip/presolve/probing_cache.cu b/cpp/src/mip/presolve/probing_cache.cu index 18620dc51..3f77a9ffd 100644 --- a/cpp/src/mip/presolve/probing_cache.cu +++ b/cpp/src/mip/presolve/probing_cache.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -455,7 +455,7 @@ void compute_cache_for_var(i_t var_idx, template void compute_probing_cache(bound_presolve_t& bound_presolve, problem_t& problem, - timer_t timer) + const termination_checker_t& timer) { raft::common::nvtx::range fun_scope("compute_probing_cache"); // we dont want to compute the probing cache for all variables for time and computation resources @@ -491,7 +491,7 @@ void compute_probing_cache(bound_presolve_t& bound_presolve, { #pragma omp for schedule(static, 4) for (auto var_idx : priority_indices) { - if (timer.check_time_limit()) { continue; } + if (timer.check()) { continue; } int thread_idx = omp_get_thread_num(); CUOPT_LOG_TRACE("Computing probing cache for var %d on thread %d", var_idx, thread_idx); @@ -520,7 +520,7 @@ void compute_probing_cache(bound_presolve_t& bound_presolve, #define INSTANTIATE(F_TYPE) \ template void compute_probing_cache(bound_presolve_t & bound_presolve, \ problem_t & problem, \ - timer_t timer); \ + const termination_checker_t& timer); \ template class probing_cache_t; #if MIP_INSTANTIATE_FLOAT diff --git a/cpp/src/mip/presolve/probing_cache.cuh b/cpp/src/mip/presolve/probing_cache.cuh index 908ec1de9..8ae7c3a96 100644 --- a/cpp/src/mip/presolve/probing_cache.cuh +++ b/cpp/src/mip/presolve/probing_cache.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -13,6 +13,10 @@ #include +namespace cuopt::linear_programming { +class termination_checker_t; +} + namespace cuopt::linear_programming::detail { template @@ -117,6 +121,6 @@ class lb_probing_cache_t { template void compute_probing_cache(bound_presolve_t& bound_presolve, problem_t& problem, - timer_t timer); + const termination_checker_t& timer); } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/relaxed_lp/relaxed_lp.cu b/cpp/src/mip/relaxed_lp/relaxed_lp.cu index 4842a986a..231c6d835 100644 --- a/cpp/src/mip/relaxed_lp/relaxed_lp.cu +++ b/cpp/src/mip/relaxed_lp/relaxed_lp.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 */ @@ -27,9 +27,11 @@ template optimization_problem_solution_t get_relaxed_lp_solution( problem_t& op_problem, solution_t& solution, - const relaxed_lp_settings_t& settings) + const relaxed_lp_settings_t& settings, + const termination_checker_t& termination) { - return get_relaxed_lp_solution(op_problem, solution.assignment, solution.lp_state, settings); + return get_relaxed_lp_solution( + op_problem, solution.assignment, solution.lp_state, settings, termination); } template @@ -37,7 +39,8 @@ optimization_problem_solution_t get_relaxed_lp_solution( problem_t& op_problem, rmm::device_uvector& assignment, lp_state_t& lp_state, - const relaxed_lp_settings_t& settings) + const relaxed_lp_settings_t& settings, + const termination_checker_t& termination) { raft::common::nvtx::range fun_scope("get_relaxed_lp_solution"); pdlp_solver_settings_t pdlp_settings{}; @@ -84,7 +87,7 @@ optimization_problem_solution_t get_relaxed_lp_solution( // before LP flush the logs as it takes quite some time cuopt::default_logger().flush(); // temporarily add timer - auto start_time = timer_t(pdlp_settings.time_limit); + auto start_time = termination_checker_t(pdlp_settings.time_limit, termination); lp_solver.set_inside_mip(true); auto solver_response = lp_solver.run_solver(start_time); @@ -118,6 +121,7 @@ bool run_lp_with_vars_fixed(problem_t& op_problem, rmm::device_uvector& fixed_assignment, rmm::device_uvector& variable_map, relaxed_lp_settings_t& settings, + const termination_checker_t& termination, bound_presolve_t* bound_presolve, bool check_fixed_assignment_feasibility) { @@ -155,7 +159,7 @@ bool run_lp_with_vars_fixed(problem_t& op_problem, CUOPT_LOG_TRACE("save_state %d", settings.save_state); auto& lp_state = fixed_problem.lp_state; auto solver_response = - get_relaxed_lp_solution(fixed_problem, fixed_assignment, lp_state, settings); + get_relaxed_lp_solution(fixed_problem, fixed_assignment, lp_state, settings, termination); // unfix the assignment on given result no matter if it is feasible solution.unfix_variables(fixed_assignment, variable_map); if (bound_presolve != nullptr) { bound_presolve->resize(op_problem); } @@ -168,6 +172,7 @@ bool run_lp_with_vars_fixed(problem_t& op_problem, solution_t& solution, const rmm::device_uvector& variables_to_fix, relaxed_lp_settings_t& settings, + const termination_checker_t& termination, bound_presolve_t* bound_presolve, bool check_fixed_assignment_feasibility, bool use_integer_fixed_problem) @@ -184,6 +189,7 @@ bool run_lp_with_vars_fixed(problem_t& op_problem, fixed_assignment, op_problem.integer_fixed_variable_map, settings, + termination, bound_presolve, check_fixed_assignment_feasibility); } else { @@ -194,6 +200,7 @@ bool run_lp_with_vars_fixed(problem_t& op_problem, fixed_assignment, variable_map, settings, + termination, bound_presolve, check_fixed_assignment_feasibility); } @@ -203,17 +210,20 @@ bool run_lp_with_vars_fixed(problem_t& op_problem, template optimization_problem_solution_t get_relaxed_lp_solution( \ problem_t & op_problem, \ solution_t & solution, \ - const relaxed_lp_settings_t& settings); \ + const relaxed_lp_settings_t& settings, \ + const termination_checker_t& termination); \ template optimization_problem_solution_t get_relaxed_lp_solution( \ problem_t & op_problem, \ rmm::device_uvector & assignment, \ lp_state_t & lp_state, \ - const relaxed_lp_settings_t& settings); \ + const relaxed_lp_settings_t& settings, \ + const termination_checker_t& termination); \ template bool run_lp_with_vars_fixed( \ problem_t & op_problem, \ solution_t & solution, \ const rmm::device_uvector& variables_to_fix, \ relaxed_lp_settings_t& settings, \ + const termination_checker_t& termination, \ bound_presolve_t* bound_presolve, \ bool check_fixed_assignment_feasibility, \ bool use_integer_fixed_problem); diff --git a/cpp/src/mip/relaxed_lp/relaxed_lp.cuh b/cpp/src/mip/relaxed_lp/relaxed_lp.cuh index 0094f5982..777f5db5f 100644 --- a/cpp/src/mip/relaxed_lp/relaxed_lp.cuh +++ b/cpp/src/mip/relaxed_lp/relaxed_lp.cuh @@ -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 */ @@ -31,20 +31,23 @@ template optimization_problem_solution_t get_relaxed_lp_solution( problem_t& op_problem, solution_t& solution, - const relaxed_lp_settings_t& settings); + const relaxed_lp_settings_t& settings, + const termination_checker_t& termination); template optimization_problem_solution_t get_relaxed_lp_solution( problem_t& op_problem, rmm::device_uvector& assignment, lp_state_t& lp_state, - const relaxed_lp_settings_t& settings); + const relaxed_lp_settings_t& settings, + const termination_checker_t& termination); template bool run_lp_with_vars_fixed(problem_t& op_problem, solution_t& solution, const rmm::device_uvector& variables_to_fix, relaxed_lp_settings_t& settings, + const termination_checker_t& termination, bound_presolve_t* bound_presolve = nullptr, bool check_fixed_assignment_feasibility = false, bool use_integer_fixed_problem = false); diff --git a/cpp/src/mip/solve.cu b/cpp/src/mip/solve.cu index e5161882e..d72b73380 100644 --- a/cpp/src/mip/solve.cu +++ b/cpp/src/mip/solve.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -61,7 +61,7 @@ static void setup_device_symbols(rmm::cuda_stream_view stream_view) template mip_solution_t run_mip(detail::problem_t& problem, mip_solver_settings_t const& settings, - cuopt::timer_t& timer) + termination_checker_t& timer) { raft::common::nvtx::range fun_scope("run_mip"); auto constexpr const running_mip = true; @@ -183,15 +183,47 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, op_problem.get_handle_ptr()->get_stream()); } - auto timer = cuopt::timer_t(time_limit); + auto termination = termination_checker_t(time_limit, termination_checker_t::root_tag_t{}); + termination.set_termination_callback( + [](void* termination_callback_data) { + auto settings = static_cast*>(termination_callback_data); + for (auto callback : settings->get_mip_callbacks()) { + if (callback->get_type() != internals::base_solution_callback_type::CHECK_TERMINATION) { + continue; + } + auto check_termination_callback = + static_cast(callback); + if (check_termination_callback->check_termination()) { return true; } + } + return false; + }, + (void*)&settings); double presolve_time = 0.0; std::unique_ptr> presolver; detail::problem_t problem(op_problem, settings.get_tolerances()); auto run_presolve = settings.presolve; - run_presolve = run_presolve && settings.get_mip_callbacks().empty(); - run_presolve = run_presolve && settings.initial_solutions.size() == 0; + + // Check for get_solution or set_solution callbacks + bool has_solution_callbacks = false; + for (auto callback : settings.get_mip_callbacks()) { + auto type = callback->get_type(); + if (type == internals::base_solution_callback_type::GET_SOLUTION || + type == internals::base_solution_callback_type::SET_SOLUTION) { + has_solution_callbacks = true; + break; + } + } + if (has_solution_callbacks) { + CUOPT_LOG_WARN("Presolve is not yet supported with solution callbacks"); + run_presolve = false; + } + + if (settings.initial_solutions.size() > 0) { + CUOPT_LOG_WARN("Presolve is not yet supported with initial solutions"); + run_presolve = false; + } if (!run_presolve) { CUOPT_LOG_INFO("Presolve is disabled, skipping"); } @@ -217,7 +249,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, problem = detail::problem_t(result->reduced_problem); problem.set_implied_integers(result->implied_integer_indices); - presolve_time = timer.elapsed_time(); + presolve_time = termination.elapsed_time(); if (result->implied_integer_indices.size() > 0) { CUOPT_LOG_INFO("%d implied integers", result->implied_integer_indices.size()); } @@ -232,7 +264,7 @@ mip_solution_t solve_mip(optimization_problem_t& op_problem, // this is for PDLP, i think this should be part of pdlp solver setup_device_symbols(op_problem.get_handle_ptr()->get_stream()); - auto sol = run_mip(problem, settings, timer); + auto sol = run_mip(problem, settings, termination); if (run_presolve) { auto status_to_skip = sol.get_termination_status() == mip_termination_status_t::TimeLimit || diff --git a/cpp/src/mip/solver.cu b/cpp/src/mip/solver.cu index fab2eecf7..f97bd0df0 100644 --- a/cpp/src/mip/solver.cu +++ b/cpp/src/mip/solver.cu @@ -20,7 +20,7 @@ #include #include -#include +#include #include #include @@ -45,14 +45,14 @@ 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) + termination_checker_t& timer) : op_problem_(op_problem), solver_settings_(solver_settings), context(op_problem.handle_ptr, const_cast*>(&op_problem), solver_settings, scaling, - timer.remaining_time()), + timer), timer_(timer) { init_handler(op_problem.handle_ptr); @@ -133,7 +133,7 @@ solution_t mip_solver_t::run_solver() CUOPT_LOG_INFO("Problem reduced to a LP, running concurrent LP"); pdlp_solver_settings_t settings{}; settings.time_limit = timer_.remaining_time(); - auto lp_timer = timer_t(settings.time_limit); + auto lp_timer = termination_checker_t(settings.time_limit, context.termination); settings.method = method_t::Concurrent; auto opt_sol = solve_lp_with_method(*context.problem_ptr, settings, lp_timer); diff --git a/cpp/src/mip/solver.cuh b/cpp/src/mip/solver.cuh index b6d16fa2d..e607f6be1 100644 --- a/cpp/src/mip/solver.cuh +++ b/cpp/src/mip/solver.cuh @@ -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 */ @@ -21,7 +21,7 @@ class mip_solver_t { 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); + termination_checker_t& timer); solution_t run_solver(); solver_stats_t& get_solver_stats() { return context.stats; } @@ -30,7 +30,7 @@ class mip_solver_t { // reference to the original problem const problem_t& op_problem_; const mip_solver_settings_t& solver_settings_; - timer_t timer_; + termination_checker_t& timer_; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/mip/solver_context.cuh b/cpp/src/mip/solver_context.cuh index 73fae0efc..66bc52d35 100644 --- a/cpp/src/mip/solver_context.cuh +++ b/cpp/src/mip/solver_context.cuh @@ -8,9 +8,9 @@ #include #include -#include #include #include +#include #pragma once @@ -29,12 +29,12 @@ struct mip_solver_context_t { problem_t* problem_ptr_, mip_solver_settings_t settings_, pdlp_initial_scaling_strategy_t& scaling, - double time_limit) + termination_checker_t& termination) : handle_ptr(handle_ptr_), problem_ptr(problem_ptr_), settings(settings_), scaling(scaling), - termination(time_limit) + termination(termination) { cuopt_assert(problem_ptr != nullptr, "problem_ptr is nullptr"); stats.solution_bound = problem_ptr->maximize ? std::numeric_limits::infinity() @@ -48,7 +48,7 @@ struct mip_solver_context_t { const mip_solver_settings_t settings; pdlp_initial_scaling_strategy_t& scaling; solver_stats_t stats; - solver_termination_t termination; + termination_checker_t& termination; }; } // namespace cuopt::linear_programming::detail diff --git a/cpp/src/utilities/termination_checker.hpp b/cpp/src/utilities/termination_checker.hpp new file mode 100644 index 000000000..bbf09be04 --- /dev/null +++ b/cpp/src/utilities/termination_checker.hpp @@ -0,0 +1,76 @@ +/* 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 + +namespace cuopt::linear_programming { + +/** + * @brief Controls solver termination based on time limit, user interrupt, and parent termination. + * + * This class owns its own timer and automatically registers with the global interrupt handler. + * It can be linked to a parent termination object to inherit + * termination conditions. + * + * Usage: + * // Root termination (main solver) + * termination_checker_t termination(60.0, termination_checker_t::root_tag_t{}); + * + * // Slave termination (sub-MIP, linked to parent) + * termination_checker_t sub_termination(10.0, parent_termination); + * + */ +class termination_checker_t { + public: + // Separate tag to force any declaration of a root termination checker to be explicit + struct root_tag_t {}; + /** + * @brief Construct a termination object. + * @param time_limit Time limit in seconds. + * @param parent Parent termination object to check for termination. + */ + explicit termination_checker_t(double time_limit, const termination_checker_t& parent) + : timer_(time_limit), parent_(&parent) + { + } + explicit termination_checker_t(double time_limit, root_tag_t) + : timer_(time_limit), parent_(nullptr) + { + } + + void set_termination_callback(bool (*termination_callback)(void*), + void* termination_callback_data) + { + termination_callback_ = termination_callback; + termination_callback_data_ = termination_callback_data; + } + + bool check() const + { + if (termination_callback_ != nullptr && termination_callback_(termination_callback_data_)) { + return true; + } + if (timer_.check_time_limit()) { return true; } + if (parent_ != nullptr && parent_->check()) { return true; } + return false; + } + + double get_time_limit() const { return timer_.get_time_limit(); } + double remaining_time() const { return timer_.remaining_time(); } + double elapsed_time() const { return timer_.elapsed_time(); } + + private: + timer_t timer_; + const termination_checker_t* parent_{nullptr}; + // avoid including which is heavy. this is a top-level header + bool (*termination_callback_)(void*) = nullptr; + void* termination_callback_data_ = nullptr; +}; + +} // namespace cuopt::linear_programming diff --git a/cpp/src/utilities/timer.hpp b/cpp/src/utilities/timer.hpp index 1d1a4881e..950659d37 100644 --- a/cpp/src/utilities/timer.hpp +++ b/cpp/src/utilities/timer.hpp @@ -1,13 +1,12 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ #pragma once #include -#include namespace cuopt { @@ -25,15 +24,6 @@ class timer_t { begin = steady_clock::now(); } - void print_debug(std::string msg) const - { - printf("%s time_limit: %f remaining_time: %f elapsed_time: %f \n", - msg.c_str(), - time_limit, - remaining_time(), - elapsed_time()); - } - bool check_time_limit() const noexcept { return elapsed_time() >= time_limit; } bool check_half_time() const noexcept { return elapsed_time() >= time_limit / 2; } diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index 01e10ca61..08f64db5e 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -274,7 +274,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); solver.run_solver(pdlp_timer); RAFT_CUDA_TRY(cudaStreamSynchronize(handle_.get_stream())); EXPECT_NEAR(initial_step_size_afiro, solver.get_step_size_h(), factor_tolerance); @@ -285,7 +286,8 @@ TEST(pdlp_class, initial_solution_test) // scale on initial option is not toggled { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -296,7 +298,8 @@ TEST(pdlp_class, initial_solution_test) } { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_dual(op_problem.get_n_constraints(), 1); auto d_initial_dual = device_copy(initial_dual, handle_.get_stream()); solver.set_initial_dual_solution(d_initial_dual); @@ -307,7 +310,8 @@ TEST(pdlp_class, initial_solution_test) } { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -323,7 +327,8 @@ TEST(pdlp_class, initial_solution_test) // Toggle the scale on initial solution while not providing should yield the same { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; solver.run_solver(pdlp_timer); RAFT_CUDA_TRY(cudaStreamSynchronize(handle_.get_stream())); @@ -333,7 +338,8 @@ TEST(pdlp_class, initial_solution_test) } { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; solver.run_solver(pdlp_timer); RAFT_CUDA_TRY(cudaStreamSynchronize(handle_.get_stream())); @@ -343,7 +349,8 @@ TEST(pdlp_class, initial_solution_test) } { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; solver.run_solver(pdlp_timer); @@ -359,7 +366,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -372,7 +380,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_dual(op_problem.get_n_constraints(), 1); auto d_initial_dual = device_copy(initial_dual, handle_.get_stream()); solver.set_initial_dual_solution(d_initial_dual); @@ -388,7 +397,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -400,7 +410,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_dual(op_problem.get_n_constraints(), 1); auto d_initial_dual = device_copy(initial_dual, handle_.get_stream()); solver.set_initial_dual_solution(d_initial_dual); @@ -415,7 +426,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 0); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -433,7 +445,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 0); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -445,7 +458,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_dual(op_problem.get_n_constraints(), 0); auto d_initial_dual = device_copy(initial_dual, handle_.get_stream()); solver.set_initial_dual_solution(d_initial_dual); @@ -457,7 +471,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 0); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -475,7 +490,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -490,7 +506,8 @@ TEST(pdlp_class, initial_solution_test) { cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -506,7 +523,8 @@ TEST(pdlp_class, initial_solution_test) cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -546,7 +564,8 @@ TEST(pdlp_class, initial_primal_weight_step_size_test) // Check setting an initial primal weight and step size { cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); constexpr double test_initial_step_size = 1.0; constexpr double test_initial_primal_weight = 2.0; solver.set_initial_primal_weight(test_initial_primal_weight); @@ -564,7 +583,8 @@ TEST(pdlp_class, initial_primal_weight_step_size_test) cuopt::linear_programming::pdlp_hyper_params::update_primal_weight_on_initial_solution = true; cuopt::linear_programming::pdlp_hyper_params::update_step_size_on_initial_solution = true; cuopt::linear_programming::detail::pdlp_solver_t solver(problem, solver_settings); - auto pdlp_timer = timer_t(solver_settings.time_limit); + auto pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); std::vector initial_primal(op_problem.get_n_variables(), 1); auto d_initial_primal = device_copy(initial_primal, handle_.get_stream()); solver.set_initial_primal_solution(d_initial_primal); @@ -577,7 +597,8 @@ TEST(pdlp_class, initial_primal_weight_step_size_test) // Start again but with an initial and check the impact cuopt::linear_programming::detail::pdlp_solver_t solver2(problem, solver_settings); - pdlp_timer = timer_t(solver_settings.time_limit); + pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); constexpr double test_initial_step_size = 1.0; constexpr double test_initial_primal_weight = 2.0; solver2.set_initial_primal_weight(test_initial_primal_weight); @@ -593,7 +614,8 @@ TEST(pdlp_class, initial_primal_weight_step_size_test) // Again but with an initial k which should change the step size only, not the primal weight cuopt::linear_programming::detail::pdlp_solver_t solver3(problem, solver_settings); - pdlp_timer = timer_t(solver_settings.time_limit); + pdlp_timer = + termination_checker_t(solver_settings.time_limit, termination_checker_t::root_tag_t{}); solver3.set_initial_primal_weight(test_initial_primal_weight); solver3.set_initial_step_size(test_initial_step_size); solver3.set_initial_primal_solution(d_initial_primal); diff --git a/cpp/tests/mip/bounds_standardization_test.cu b/cpp/tests/mip/bounds_standardization_test.cu index d369e1469..11edb3edc 100644 --- a/cpp/tests/mip/bounds_standardization_test.cu +++ b/cpp/tests/mip/bounds_standardization_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 */ @@ -67,13 +67,16 @@ void test_bounds_standardization_test(std::string test_instance) detail::solution_t solution_1(standardized_problem); mip_solver_settings_t default_settings{}; + auto timer = termination_checker_t(std::numeric_limits::infinity(), + termination_checker_t::root_tag_t{}); detail::relaxed_lp_settings_t lp_settings; lp_settings.time_limit = 120.; lp_settings.tolerance = default_settings.tolerances.absolute_tolerance; lp_settings.per_constraint_residual = false; // run the problem through pdlp - auto result_1 = detail::get_relaxed_lp_solution(standardized_problem, solution_1, lp_settings); + auto result_1 = + detail::get_relaxed_lp_solution(standardized_problem, solution_1, lp_settings, timer); solution_1.compute_feasibility(); bool sol_1_feasible = (int)result_1.get_termination_status() == CUOPT_TERIMINATION_STATUS_OPTIMAL; // the problem might not be feasible in terms of per constraint residual diff --git a/cpp/tests/mip/elim_var_remap_test.cu b/cpp/tests/mip/elim_var_remap_test.cu index 2b2f3f576..9b919bb1f 100644 --- a/cpp/tests/mip/elim_var_remap_test.cu +++ b/cpp/tests/mip/elim_var_remap_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 */ @@ -150,14 +150,16 @@ void test_elim_var_solution(std::string test_instance) detail::problem_t sub_problem(standardized_problem); mip_solver_settings_t default_settings{}; - + auto timer = termination_checker_t(std::numeric_limits::infinity(), + termination_checker_t::root_tag_t{}); detail::solution_t solution_1(standardized_problem); detail::relaxed_lp_settings_t lp_settings; lp_settings.time_limit = 120.; lp_settings.tolerance = default_settings.tolerances.absolute_tolerance; lp_settings.per_constraint_residual = false; // run the problem through pdlp - auto result_1 = detail::get_relaxed_lp_solution(standardized_problem, solution_1, lp_settings); + auto result_1 = + detail::get_relaxed_lp_solution(standardized_problem, solution_1, lp_settings, timer); solution_1.compute_feasibility(); // the solution might not be feasible per row as we are getting the result of pdlp bool sol_1_feasible = (int)result_1.get_termination_status() == CUOPT_TERIMINATION_STATUS_OPTIMAL; @@ -187,7 +189,7 @@ void test_elim_var_solution(std::string test_instance) lp_settings_2.tolerance = default_settings.tolerances.absolute_tolerance; lp_settings_2.per_constraint_residual = false; // run the problem through pdlp - auto result_2 = detail::get_relaxed_lp_solution(sub_problem, solution_2, lp_settings_2); + auto result_2 = detail::get_relaxed_lp_solution(sub_problem, solution_2, lp_settings_2, timer); solution_2.compute_feasibility(); bool sol_2_feasible = (int)result_2.get_termination_status() == CUOPT_TERIMINATION_STATUS_OPTIMAL; EXPECT_EQ((int)result_2.get_termination_status(), CUOPT_TERIMINATION_STATUS_OPTIMAL); diff --git a/cpp/tests/mip/feasibility_jump_tests.cu b/cpp/tests/mip/feasibility_jump_tests.cu index 36410599c..389bc066b 100644 --- a/cpp/tests/mip/feasibility_jump_tests.cu +++ b/cpp/tests/mip/feasibility_jump_tests.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -90,7 +90,7 @@ static fj_state_t run_fj(std::string test_instance, auto settings = mip_solver_settings_t{}; settings.time_limit = 30.; - auto timer = cuopt::timer_t(30); + auto timer = termination_checker_t(30); detail::mip_solver_t solver(problem, settings, scaling, timer); detail::solution_t solution(*solver.context.problem_ptr); diff --git a/cpp/tests/mip/load_balancing_test.cu b/cpp/tests/mip/load_balancing_test.cu index 20f359fcb..bc44256ed 100644 --- a/cpp/tests/mip/load_balancing_test.cu +++ b/cpp/tests/mip/load_balancing_test.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -136,7 +136,8 @@ void test_multi_probe(std::string path) problem.reverse_constraints, nullptr, true); - detail::mip_solver_t solver(problem, default_settings, scaling, cuopt::timer_t(0)); + detail::mip_solver_t solver( + problem, default_settings, scaling, termination_checker_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 cb960425f..29103c92e 100644 --- a/cpp/tests/mip/multi_probe_test.cu +++ b/cpp/tests/mip/multi_probe_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 */ @@ -148,6 +148,8 @@ 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{}; + auto timer = termination_checker_t(std::numeric_limits::infinity(), + termination_checker_t::root_tag_t{}); detail::pdlp_initial_scaling_strategy_t scaling(&handle_, problem, 10, @@ -157,7 +159,7 @@ void test_multi_probe(std::string path) problem.reverse_constraints, nullptr, true); - detail::mip_solver_t solver(problem, default_settings, scaling, cuopt::timer_t(0)); + detail::mip_solver_t solver(problem, default_settings, scaling, timer); 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/python/cuopt/cuopt/linear_programming/internals/__init__.py b/python/cuopt/cuopt/linear_programming/internals/__init__.py index e935f5da7..989e2ff3d 100644 --- a/python/cuopt/cuopt/linear_programming/internals/__init__.py +++ b/python/cuopt/cuopt/linear_programming/internals/__init__.py @@ -1,7 +1,8 @@ -# SPDX-FileCopyrightText: Copyright (c) 2020-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from cuopt.linear_programming.internals.internals import ( + CheckTerminationCallback, GetSolutionCallback, SetSolutionCallback, ) diff --git a/python/cuopt/cuopt/linear_programming/internals/internals.pyx b/python/cuopt/cuopt/linear_programming/internals/internals.pyx index 0e4342fe1..dbfd58adf 100644 --- a/python/cuopt/cuopt/linear_programming/internals/internals.pyx +++ b/python/cuopt/cuopt/linear_programming/internals/internals.pyx @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 # cython: profile=False @@ -31,6 +31,10 @@ cdef extern from "cuopt/linear_programming/utilities/callbacks_implems.hpp" name void set_solution(void* data, void* objective_value) except + PyObject* pyCallbackClass + cdef cppclass default_check_termination_callback_t(Callback): + bint check_termination() except + + PyObject* pyCallbackClass + cdef class PyCallback: @@ -83,3 +87,14 @@ cdef class SetSolutionCallback(PyCallback): def get_native_callback(self): return &(self.native_callback) + + +cdef class CheckTerminationCallback(PyCallback): + + cdef default_check_termination_callback_t native_callback + + def __init__(self): + self.native_callback.pyCallbackClass = self + + def get_native_callback(self): + return &(self.native_callback) diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.pxd b/python/cuopt/cuopt/linear_programming/solver/solver.pxd index c140e3d0c..53894d9d0 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.pxd +++ b/python/cuopt/cuopt/linear_programming/solver/solver.pxd @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 @@ -81,6 +81,11 @@ cdef extern from "cuopt/linear_programming/solver_settings.hpp" namespace "cuopt base_solution_callback_t* callback ) except + + # LP settings (callbacks) + void set_lp_callback( + base_solution_callback_t* callback + ) except + + cdef extern from "cuopt/linear_programming/optimization_problem.hpp" namespace "cuopt::linear_programming": # noqa ctypedef enum problem_category_t "cuopt::linear_programming::problem_category_t": # noqa diff --git a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx index 1991af0d6..cfc24dd60 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/solver/solver_wrapper.pyx @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # noqa # SPDX-License-Identifier: Apache-2.0 @@ -208,6 +208,14 @@ cdef set_solver_setting( str(value).encode('utf-8') ) + lp_callbacks = settings.get_lp_callbacks() + for callback in lp_callbacks: + if callback: + callback_ptr = callback.get_native_callback() + + c_solver_settings.set_lp_callback( + callback_ptr + ) if settings.get_pdlp_warm_start_data() is not None: # noqa if len(data_model_obj.get_objective_coefficients()) != len( diff --git a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py index 85a944e2e..1d313c2ec 100644 --- a/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py +++ b/python/cuopt/cuopt/linear_programming/solver_settings/solver_settings.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from enum import IntEnum, auto @@ -110,6 +110,7 @@ def __init__(self): self.settings_dict = {} self.pdlp_warm_start_data = None self.mip_callbacks = [] + self.lp_callbacks = [] def to_base_type(self, value): """Convert a string to a base type. @@ -249,13 +250,13 @@ def set_mip_callback(self, callback): """ Note: Only supported for MILP - Set the callback to receive incumbent solution. + Set the callback to receive incumbent solution or check termination. Parameters ---------- callback : class for function callback - Callback class that inherits from GetSolutionCallback - or SetSolutionCallback. + Callback class that inherits from GetSolutionCallback, + SetSolutionCallback, or CheckTerminationCallback. Examples -------- @@ -297,6 +298,18 @@ def set_mip_callback(self, callback): >>> set_callback = CustomSetSolutionCallback(get_callback) >>> settings.set_mip_callback(get_callback) >>> settings.set_mip_callback(set_callback) + >>> + >>> # Callback for termination check + >>> class CustomCheckTerminationCallback(CheckTerminationCallback): + >>> def __init__(self): + >>> super().__init__() + >>> self.should_terminate = False + >>> + >>> def check_termination(self): + >>> return self.should_terminate + >>> + >>> termination_callback = CustomCheckTerminationCallback() + >>> settings.set_mip_callback(termination_callback) """ self.mip_callbacks.append(callback) @@ -306,6 +319,39 @@ def get_mip_callbacks(self): """ return self.mip_callbacks + def set_lp_callback(self, callback): + """ + Note: Only supported for LP + + Set the callback for LP solving, e.g., to check termination. + + Parameters + ---------- + callback : class for function callback + Callback class that inherits from CheckTerminationCallback. + + Examples + -------- + >>> # Callback for termination check + >>> class CustomCheckTerminationCallback(CheckTerminationCallback): + >>> def __init__(self): + >>> super().__init__() + >>> self.should_terminate = False + >>> + >>> def check_termination(self): + >>> return self.should_terminate + >>> + >>> termination_callback = CustomCheckTerminationCallback() + >>> settings.set_lp_callback(termination_callback) + """ + self.lp_callbacks.append(callback) + + def get_lp_callbacks(self): + """ + Return LP callback class objects + """ + return self.lp_callbacks + def get_pdlp_warm_start_data(self): """ Returns the warm start data. See `set_pdlp_warm_start_data` for more From 1bac257b95c266dd8a45518e3f25867c4fd1566d Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Mon, 5 Jan 2026 17:35:09 +0000 Subject: [PATCH 3/4] cleanup --- cpp/include/cuopt/linear_programming/constants.h | 3 +-- cpp/src/dual_simplex/branch_and_bound.cpp | 4 ---- cpp/tests/mip/feasibility_jump_tests.cu | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/constants.h b/cpp/include/cuopt/linear_programming/constants.h index d1dca2cf4..b512944a6 100644 --- a/cpp/include/cuopt/linear_programming/constants.h +++ b/cpp/include/cuopt/linear_programming/constants.h @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -73,7 +73,6 @@ #define CUOPT_TERIMINATION_STATUS_PRIMAL_FEASIBLE 7 #define CUOPT_TERIMINATION_STATUS_FEASIBLE_FOUND 8 #define CUOPT_TERIMINATION_STATUS_CONCURRENT_LIMIT 9 -#define CUOPT_TERIMINATION_STATUS_USER_INTERRUPT 10 /* @brief The objective sense constants */ #define CUOPT_MINIMIZE 1 diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index b8cfc2aff..4eb6f6fb4 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -506,10 +506,6 @@ mip_status_t branch_and_bound_t::set_final_solution(mip_solution_t{}; settings.time_limit = 30.; - auto timer = termination_checker_t(30); + auto timer = termination_checker_t(30, termination_checker_t::root_tag_t{}); detail::mip_solver_t solver(problem, settings, scaling, timer); detail::solution_t solution(*solver.context.problem_ptr); From 606a5f880104ba893cef415d7b35d31bcb2fe7b3 Mon Sep 17 00:00:00 2001 From: Alice Boucher Date: Thu, 8 Jan 2026 12:59:43 +0000 Subject: [PATCH 4/4] fix incorrect termination code when user termination is requested and incorrect PDLP objective problem space when passed to B&B from diversity_manager --- cpp/src/dual_simplex/basis_solves.cpp | 6 +-- cpp/src/dual_simplex/basis_updates.cpp | 49 ++++++++++++---------- cpp/src/dual_simplex/branch_and_bound.cpp | 5 +++ cpp/src/dual_simplex/crossover.cpp | 21 +++++++--- cpp/src/dual_simplex/phase2.cpp | 11 +++-- cpp/src/dual_simplex/primal.cpp | 9 ++-- cpp/src/dual_simplex/primal.hpp | 5 ++- cpp/src/mip/diversity/diversity_manager.cu | 15 ++++--- cpp/src/mip/problem/problem.cu | 6 +++ cpp/src/mip/problem/problem.cuh | 3 +- cpp/tests/mip/mip_utils.cuh | 10 +++-- 11 files changed, 89 insertions(+), 51 deletions(-) diff --git a/cpp/src/dual_simplex/basis_solves.cpp b/cpp/src/dual_simplex/basis_solves.cpp index db24f55a2..d6992bf84 100644 --- a/cpp/src/dual_simplex/basis_solves.cpp +++ b/cpp/src/dual_simplex/basis_solves.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -363,7 +363,7 @@ i_t factorize_basis(const csc_matrix_t& A, S_perm_inv); if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Concurrent halt\n"); - return -1; + return -2; // Use -2 to distinguish from rank deficiency (-1) } if (Srank != Sdim) { // Get the rank deficient columns @@ -582,7 +582,7 @@ i_t factorize_basis(const csc_matrix_t& A, } if (settings.concurrent_halt != nullptr && *settings.concurrent_halt == 1) { settings.log.printf("Concurrent halt\n"); - return -1; + return -2; // Use -2 to distinguish from rank deficiency (-1) } if (verbose) { printf("Right Lnz+Unz %d t %.3f\n", L.col_start[m] + U.col_start[m], toc(fact_start)); diff --git a/cpp/src/dual_simplex/basis_updates.cpp b/cpp/src/dual_simplex/basis_updates.cpp index 6b79f3c86..66937061c 100644 --- a/cpp/src/dual_simplex/basis_updates.cpp +++ b/cpp/src/dual_simplex/basis_updates.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -2055,16 +2055,21 @@ int basis_update_mpf_t::refactor_basis( if (L0_.m != A.m) { resize(A.m); } std::vector q; - if (factorize_basis(A, - settings, - basic_list, - L0_, - U0_, - row_permutation_, - inverse_row_permutation_, - q, - deficient, - slacks_needed) == -1) { + i_t factorize_result = factorize_basis(A, + settings, + basic_list, + L0_, + U0_, + row_permutation_, + inverse_row_permutation_, + q, + deficient, + slacks_needed); + if (factorize_result == -2) { + // Concurrent halt requested, return early + return -2; + } + if (factorize_result == -1) { settings.log.debug("Initial factorization failed\n"); basis_repair(A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); @@ -2085,16 +2090,18 @@ int basis_update_mpf_t::refactor_basis( } #endif - if (factorize_basis(A, - settings, - basic_list, - L0_, - U0_, - row_permutation_, - inverse_row_permutation_, - q, - deficient, - slacks_needed) == -1) { + factorize_result = factorize_basis(A, + settings, + basic_list, + L0_, + U0_, + row_permutation_, + inverse_row_permutation_, + q, + deficient, + slacks_needed); + if (factorize_result == -2) { return -2; } + if (factorize_result == -1) { #ifdef CHECK_L_FACTOR if (L0_.check_matrix() == -1) { settings.log.printf("Bad L after basis repair\n"); } #endif diff --git a/cpp/src/dual_simplex/branch_and_bound.cpp b/cpp/src/dual_simplex/branch_and_bound.cpp index 4eb6f6fb4..9408bde5e 100644 --- a/cpp/src/dual_simplex/branch_and_bound.cpp +++ b/cpp/src/dual_simplex/branch_and_bound.cpp @@ -1360,6 +1360,11 @@ mip_status_t branch_and_bound_t::solve(mip_solution_t& solut return mip_status_t::UNBOUNDED; } + if (root_status == lp_status_t::CONCURRENT_LIMIT && + settings_.check_termination(exploration_stats_.start_time)) { + solver_status_ = mip_exploration_status_t::TIME_LIMIT; + return set_final_solution(solution, root_objective_); + } if (root_status == lp_status_t::TIME_LIMIT) { solver_status_ = mip_exploration_status_t::TIME_LIMIT; return set_final_solution(solution, -inf); diff --git a/cpp/src/dual_simplex/crossover.cpp b/cpp/src/dual_simplex/crossover.cpp index df270d6ef..456f721d6 100644 --- a/cpp/src/dual_simplex/crossover.cpp +++ b/cpp/src/dual_simplex/crossover.cpp @@ -783,12 +783,15 @@ i_t primal_push(const lp_problem_t& lp, std::vector slacks_needed; i_t rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (rank == -2) { return -2; } // Concurrent halt if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair( lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); - if (factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { + i_t repair_rank = + factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (repair_rank == -2) { return -2; } // Concurrent halt + if (repair_rank == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); return -1; } else { @@ -1130,11 +1133,14 @@ crossover_status_t crossover(const lp_problem_t& lp, std::vector slacks_needed; rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (rank == -2) { return crossover_status_t::CONCURRENT_LIMIT; } // Concurrent halt if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); - if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == - -1) { + i_t repair_rank = + factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (repair_rank == -2) { return crossover_status_t::CONCURRENT_LIMIT; } // Concurrent halt + if (repair_rank == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); return crossover_status_t::NUMERICAL_ISSUES; } else { @@ -1321,11 +1327,14 @@ crossover_status_t crossover(const lp_problem_t& lp, get_basis_from_vstatus(m, vstatus, basic_list, nonbasic_list, superbasic_list); rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (rank == -2) { return crossover_status_t::CONCURRENT_LIMIT; } // Concurrent halt if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); - if (factorize_basis( - lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == -1) { + i_t repair_rank = + factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (repair_rank == -2) { return crossover_status_t::CONCURRENT_LIMIT; } // Concurrent halt + if (repair_rank == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); return crossover_status_t::NUMERICAL_ISSUES; } else { diff --git a/cpp/src/dual_simplex/phase2.cpp b/cpp/src/dual_simplex/phase2.cpp index 86b879e38..2a3c1684f 100644 --- a/cpp/src/dual_simplex/phase2.cpp +++ b/cpp/src/dual_simplex/phase2.cpp @@ -2241,9 +2241,9 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, assert(superbasic_list.size() == 0); assert(nonbasic_list.size() == n - m); - if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { - return dual::status_t::NUMERICAL; - } + i_t refactor_result = ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus); + if (refactor_result == -2) { return dual::status_t::CONCURRENT_LIMIT; } + if (refactor_result > 0) { return dual::status_t::NUMERICAL; } if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } } @@ -2883,7 +2883,9 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, #endif if (should_refactor) { bool should_recompute_x = false; - if (ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus) > 0) { + i_t refactor_result = ft.refactor_basis(lp.A, settings, basic_list, nonbasic_list, vstatus); + if (refactor_result == -2) { return dual::status_t::CONCURRENT_LIMIT; } + if (refactor_result > 0) { should_recompute_x = true; settings.log.printf("Failed to factorize basis. Iteration %d\n", iter); if (settings.check_termination(start_time)) { return dual::status_t::TIME_LIMIT; } @@ -2901,6 +2903,7 @@ dual::status_t dual_phase2_with_advanced_basis(i_t phase, count++; if (count > 10) { return dual::status_t::NUMERICAL; } } + if (deficient_size == -2) { return dual::status_t::CONCURRENT_LIMIT; } settings.log.printf("Successfully repaired basis. Iteration %d\n", iter); } diff --git a/cpp/src/dual_simplex/primal.cpp b/cpp/src/dual_simplex/primal.cpp index 80406dcf0..f9ca2c5e5 100644 --- a/cpp/src/dual_simplex/primal.cpp +++ b/cpp/src/dual_simplex/primal.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -296,11 +296,14 @@ primal::status_t primal_phase2(i_t phase, std::vector slacks_needed; i_t rank = factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (rank == -2) { return primal::status_t::CONCURRENT_HALT; } if (rank != m) { settings.log.debug("Failed to factorize basis. rank %d m %d\n", rank, m); basis_repair(lp.A, settings, deficient, slacks_needed, basic_list, nonbasic_list, vstatus); - if (factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed) == - -1) { + i_t repair_rank = + factorize_basis(lp.A, settings, basic_list, L, U, p, pinv, q, deficient, slacks_needed); + if (repair_rank == -2) { return primal::status_t::CONCURRENT_HALT; } + if (repair_rank == -1) { settings.log.printf("Failed to factorize basis after repair. rank %d m %d\n", rank, m); return primal::status_t::NUMERICAL; } else { diff --git a/cpp/src/dual_simplex/primal.hpp b/cpp/src/dual_simplex/primal.hpp index a5d356fdb..cd4b0cce4 100644 --- a/cpp/src/dual_simplex/primal.hpp +++ b/cpp/src/dual_simplex/primal.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -23,7 +23,8 @@ enum class status_t { PRIMAL_UNBOUNDED = 1, NUMERICAL = 2, NOT_LOADED = 3, - ITERATION_LIMIT = 4 + ITERATION_LIMIT = 4, + CONCURRENT_HALT = 5 }; } diff --git a/cpp/src/mip/diversity/diversity_manager.cu b/cpp/src/mip/diversity/diversity_manager.cu index 4fdd71335..05c4e60a7 100644 --- a/cpp/src/mip/diversity/diversity_manager.cu +++ b/cpp/src/mip/diversity/diversity_manager.cu @@ -434,15 +434,14 @@ solution_t diversity_manager_t::run_solver() problem_ptr->handle_ptr->get_stream()); problem_ptr->handle_ptr->sync_stream(); - auto user_obj = problem_ptr->get_user_obj_from_solver_obj(lp_result.get_objective_value()); + // PDLP returns user-space objective (it applies objective_scaling_factor internally) + auto user_obj = lp_result.get_objective_value(); + auto solver_obj = problem_ptr->get_solver_obj_from_user_obj(user_obj); auto iterations = lp_result.get_additional_termination_information().number_of_steps_taken; - // Set for the B&B - problem_ptr->set_root_relaxation_solution_callback(host_primal, - host_dual, - host_reduced_costs, - lp_result.get_objective_value(), - user_obj, - iterations); + + // Set for the B&B (param4 expects solver space, param5 expects user space) + problem_ptr->set_root_relaxation_solution_callback( + host_primal, host_dual, host_reduced_costs, solver_obj, user_obj, iterations); } // in case the pdlp returned var boudns that are out of bounds diff --git a/cpp/src/mip/problem/problem.cu b/cpp/src/mip/problem/problem.cu index 815ef5aa0..635796cc9 100644 --- a/cpp/src/mip/problem/problem.cu +++ b/cpp/src/mip/problem/problem.cu @@ -1780,6 +1780,12 @@ f_t problem_t::get_user_obj_from_solver_obj(f_t solver_obj) const return presolve_data.objective_scaling_factor * (solver_obj + presolve_data.objective_offset); } +template +f_t problem_t::get_solver_obj_from_user_obj(f_t user_obj) const +{ + return (user_obj / presolve_data.objective_scaling_factor) - presolve_data.objective_offset; +} + template void problem_t::compute_vars_with_objective_coeffs() { diff --git a/cpp/src/mip/problem/problem.cuh b/cpp/src/mip/problem/problem.cuh index ed0adb971..3e81fc610 100644 --- a/cpp/src/mip/problem/problem.cuh +++ b/cpp/src/mip/problem/problem.cuh @@ -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,6 +91,7 @@ class problem_t { void post_process_solution(solution_t& solution); void compute_transpose_of_problem(); f_t get_user_obj_from_solver_obj(f_t solver_obj) const; + f_t get_solver_obj_from_user_obj(f_t user_obj) const; bool is_objective_integral() const { return objective_is_integral; } void compute_integer_fixed_problem(); void fill_integer_fixed_problem(rmm::device_uvector& assignment, diff --git a/cpp/tests/mip/mip_utils.cuh b/cpp/tests/mip/mip_utils.cuh index 19c44b2fd..14aec8eab 100644 --- a/cpp/tests/mip/mip_utils.cuh +++ b/cpp/tests/mip/mip_utils.cuh @@ -103,9 +103,10 @@ static void test_constraint_sanity_per_row( static std::tuple test_mps_file( std::string test_instance, - double time_limit = 1, - bool heuristics_only = true, - bool presolve = true) + double time_limit = 1, + bool heuristics_only = true, + bool presolve = true, + std::vector callbacks = {}) { const raft::handle_t handle_{}; @@ -114,6 +115,9 @@ static std::tuple test_mps_file( cuopt::mps_parser::parse_mps(path, false); handle_.sync_stream(); mip_solver_settings_t settings; + for (auto callback : callbacks) { + settings.set_mip_callback(callback); + } settings.time_limit = time_limit; settings.heuristics_only = heuristics_only; settings.presolve = presolve;