-
Notifications
You must be signed in to change notification settings - Fork 132
Run FJ heuristics before and during presolve #899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
49030ad
5881d35
8a3684c
e019653
9c89092
d5f7dde
62a53ef
a4a195f
ca34f77
f349b5b
506d046
33230b6
71e8c9a
9d7055e
7b9451b
d0c5a42
d559b34
c5d0bd5
67240f5
a15424f
dee5d4d
7b3d40c
a5122ad
68e07bb
352cd15
e4b8166
a437adf
7b4b8fe
e1b0682
5703faf
c9db074
4f108b5
c77ea05
4c8e6f0
0e30474
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -108,6 +108,20 @@ class branch_and_bound_t { | |||||
|
|
||||||
| bool stop_for_time_limit(mip_solution_t<i_t, f_t>& solution); | ||||||
|
|
||||||
| // Set a cutoff bound from an external source (e.g., early FJ during presolve). | ||||||
| // Used for node pruning and reduced cost strengthening but NOT for gap computation. | ||||||
| // Unlike upper_bound_, this does not imply a verified incumbent solution exists. | ||||||
| // | ||||||
| // IMPORTANT: `bound` must be in B&B's internal objective space, i.e. the space of | ||||||
| // original_lp_ where: user_obj = obj_scale * (internal_obj + obj_constant). | ||||||
| // The caller (solver.cu) converts from user-space via | ||||||
| // problem_ptr->get_solver_obj_from_user_obj(user_cutoff) | ||||||
| // which accounts for both the presolve objective offset and maximization. | ||||||
| void set_initial_cutoff(f_t bound) { initial_cutoff_ = bound; } | ||||||
|
|
||||||
| // Effective cutoff for node pruning: min of verified incumbent and external cutoff. | ||||||
| f_t get_cutoff() const { return std::min(upper_bound_.load(), initial_cutoff_); } | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
cutoff is a bit generic/vague. Wdyt?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose "cutoff" because the early heuristics only provide a bound, not an actualy incumbent (because we lack crushing), and "cutoff" was already the terminology employed in the dual simplex code for such a bound 🙂 I wanted to make it clear that this is not necessarily the objective of the current best incumbent, only a proof that a solution with an objective <= to this one exists. |
||||||
|
|
||||||
| // Repair a low-quality solution from the heuristics. | ||||||
| bool repair_solution(const std::vector<f_t>& leaf_edge_norms, | ||||||
| const std::vector<f_t>& potential_solution, | ||||||
|
|
@@ -169,9 +183,13 @@ class branch_and_bound_t { | |||||
| // Mutex for upper bound | ||||||
| omp_mutex_t mutex_upper_; | ||||||
|
|
||||||
| // Global variable for upper bound | ||||||
| // Verified incumbent bound (only set when B&B has an actual integer-feasible solution). | ||||||
| omp_atomic_t<f_t> upper_bound_; | ||||||
|
|
||||||
| // External cutoff from early heuristics (for pruning only, no verified solution). | ||||||
| // Must be in B&B internal objective space (see set_initial_cutoff). | ||||||
| f_t initial_cutoff_{std::numeric_limits<f_t>::infinity()}; | ||||||
|
|
||||||
| // Global variable for incumbent. The incumbent should be updated with the upper bound | ||||||
| mip_solution_t<i_t, f_t> incumbent_; | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /* 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 <mip_heuristics/problem/problem.cuh> | ||
| #include <mip_heuristics/solution/solution.cuh> | ||
|
|
||
| #include <cuopt/linear_programming/mip/solver_settings.hpp> | ||
|
|
||
| #include <utilities/logger.hpp> | ||
|
|
||
| #include <thrust/fill.h> | ||
|
|
||
| #include <chrono> | ||
| #include <functional> | ||
| #include <limits> | ||
| #include <vector> | ||
|
|
||
| namespace cuopt::linear_programming::detail { | ||
|
|
||
| template <typename f_t> | ||
| using early_incumbent_callback_t = | ||
| std::function<void(f_t solver_obj, f_t user_obj, const std::vector<f_t>& assignment)>; | ||
|
|
||
| // CRTP base for early heuristics that run on the original (or papilo-presolved) problem | ||
| // during presolve to find incumbents as early as possible. | ||
| // Derived classes implement start() and stop(). | ||
| template <typename i_t, typename f_t, typename Derived> | ||
| class early_heuristic_t { | ||
| public: | ||
| early_heuristic_t(const optimization_problem_t<i_t, f_t>& op_problem, | ||
| const typename mip_solver_settings_t<i_t, f_t>::tolerances_t& tolerances, | ||
| early_incumbent_callback_t<f_t> incumbent_callback) | ||
| : incumbent_callback_(std::move(incumbent_callback)) | ||
| { | ||
| RAFT_CUDA_TRY(cudaGetDevice(&device_id_)); | ||
|
|
||
| // Build and preprocess on the original handle, then copy onto our own handle | ||
| // so the derived solver can run on a dedicated stream (prevents graph capture conflicts). | ||
| problem_t<i_t, f_t> temp_problem(op_problem, tolerances, false); | ||
| temp_problem.preprocess_problem(); | ||
| temp_problem.handle_ptr->sync_stream(); | ||
| problem_ptr_ = std::make_unique<problem_t<i_t, f_t>>(temp_problem, &handle_); | ||
|
|
||
| solution_ptr_ = std::make_unique<solution_t<i_t, f_t>>(*problem_ptr_); | ||
| thrust::fill(handle_.get_thrust_policy(), | ||
| solution_ptr_->assignment.begin(), | ||
| solution_ptr_->assignment.end(), | ||
| f_t{0}); | ||
| solution_ptr_->clamp_within_bounds(); | ||
| } | ||
|
|
||
| bool solution_found() const { return solution_found_; } | ||
| f_t get_best_objective() const { return best_objective_; } | ||
| // Return the best objective converted to user-space (sense-aware, offset-aware). | ||
| f_t get_best_user_objective() const | ||
| { | ||
| return problem_ptr_->get_user_obj_from_solver_obj(best_objective_); | ||
| } | ||
| // Set the incumbent threshold. `obj` must be in THIS heuristic's solver-space | ||
| // (i.e. the space of problem_ptr_). Callers that hold a value from a different | ||
| // problem representation (e.g., the original pre-presolve problem) must convert | ||
| // it first, otherwise try_update_best will reject valid solutions. | ||
| void set_best_objective(f_t obj) { best_objective_ = obj; } | ||
| const std::vector<f_t>& get_best_assignment() const { return best_assignment_; } | ||
|
|
||
| protected: | ||
| ~early_heuristic_t() = default; | ||
|
|
||
| // NOT thread-safe. solver_obj is in solver-space (always minimization). | ||
| // Uses a private CUDA stream to avoid racing with the FJ solver's stream. | ||
| void try_update_best(f_t solver_obj, const std::vector<f_t>& assignment) | ||
| { | ||
| if (solver_obj >= best_objective_) { return; } | ||
| best_objective_ = solver_obj; | ||
|
|
||
| RAFT_CUDA_TRY(cudaSetDevice(device_id_)); | ||
| auto stream = handle_.get_stream(); | ||
| rmm::device_uvector<f_t> d_assignment(assignment.size(), stream); | ||
| raft::copy(d_assignment.data(), assignment.data(), assignment.size(), stream); | ||
| problem_ptr_->post_process_assignment(d_assignment, true, stream); | ||
| auto user_assignment = cuopt::host_copy(d_assignment, stream); | ||
|
|
||
| best_assignment_ = user_assignment; | ||
| solution_found_ = true; | ||
| f_t user_obj = problem_ptr_->get_user_obj_from_solver_obj(solver_obj); | ||
| double elapsed = | ||
| std::chrono::duration<double>(std::chrono::steady_clock::now() - start_time_).count(); | ||
| CUOPT_LOG_INFO("Early heuristics (%s) lowered the primal bound. Objective %g. Time %.2f", | ||
| Derived::name(), | ||
| user_obj, | ||
| elapsed); | ||
| if (incumbent_callback_) { incumbent_callback_(solver_obj, user_obj, user_assignment); } | ||
| } | ||
|
|
||
| int device_id_{0}; | ||
|
|
||
| // handle_ must be declared before problem_ptr_/solution_ptr_ so it outlives them | ||
| // (C++ destroys members in reverse declaration order) | ||
| raft::handle_t handle_; | ||
|
|
||
| std::unique_ptr<problem_t<i_t, f_t>> problem_ptr_; | ||
| std::unique_ptr<solution_t<i_t, f_t>> solution_ptr_; | ||
|
|
||
| bool solution_found_{false}; | ||
| f_t best_objective_{std::numeric_limits<f_t>::infinity()}; | ||
| std::vector<f_t> best_assignment_; | ||
|
|
||
| early_incumbent_callback_t<f_t> incumbent_callback_; | ||
| std::chrono::steady_clock::time_point start_time_; | ||
| }; | ||
|
|
||
| } // namespace cuopt::linear_programming::detail |
Uh oh!
There was an error while loading. Please reload this page.