-
Notifications
You must be signed in to change notification settings - Fork 236
[CPUFJ PR4] Structure-aware starts and repairs for CPUFJ #1951
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
ccf8c91
5b06036
346865f
154b989
d7c900d
607f6cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,16 +13,165 @@ | |||||||||||||||||||||||||
| #include "search/moves.hpp" | ||||||||||||||||||||||||||
| #include "search/score.hpp" | ||||||||||||||||||||||||||
| #include "search/update.hpp" | ||||||||||||||||||||||||||
| #include "setup/bounds.hpp" | ||||||||||||||||||||||||||
| #include "setup/lp.hpp" | ||||||||||||||||||||||||||
| #include "setup/structure.hpp" | ||||||||||||||||||||||||||
| #include "starts/starts.hpp" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| #include <mip_heuristics/feasibility_jump/fj_cpu_binary.cuh> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| namespace cuopt::mathematical_optimization::mip { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| template <typename i_t, typename f_t> | ||||||||||||||||||||||||||
| static std::vector<f_t> lift_equality_substituted_assignment( | ||||||||||||||||||||||||||
| fj_cpu_climber_t<i_t, f_t>& c, | ||||||||||||||||||||||||||
| const std::vector<f_t>& assignment, | ||||||||||||||||||||||||||
| const std::vector<i_t>& retained, | ||||||||||||||||||||||||||
| const std::vector<fj_equality_substitution_t<i_t, f_t>>& substitutions) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| std::vector<f_t> lifted(c.problem->n_variables, 0); | ||||||||||||||||||||||||||
| for (size_t j = 0; j < retained.size(); ++j) | ||||||||||||||||||||||||||
| lifted[retained[j]] = assignment[j]; | ||||||||||||||||||||||||||
| for (auto it = substitutions.rbegin(); it != substitutions.rend(); ++it) { | ||||||||||||||||||||||||||
| const auto coefficients = thrust::make_transform_iterator( | ||||||||||||||||||||||||||
| it->terms.begin(), [](const auto& term) { return term.second; }); | ||||||||||||||||||||||||||
| const auto values = thrust::make_transform_iterator( | ||||||||||||||||||||||||||
| it->terms.begin(), [&lifted](const auto& term) { return lifted[term.first]; }); | ||||||||||||||||||||||||||
| lifted[it->variable] = it->constant + compensated_dot2(coefficients, values, it->terms.size()); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| for (const auto& sub : substitutions) { | ||||||||||||||||||||||||||
| const auto bounds = c.h_var_bounds[sub.variable].get(); | ||||||||||||||||||||||||||
| f_t value = std::clamp(lifted[sub.variable], get_lower(bounds), get_upper(bounds)); | ||||||||||||||||||||||||||
| if (is_integer_var(c, sub.variable)) value = std::round(value); | ||||||||||||||||||||||||||
| lifted[sub.variable] = value; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // Substitution and the final bound/integrality repair must both survive a check in the unchanged | ||||||||||||||||||||||||||
| // parent model | ||||||||||||||||||||||||||
| const auto& p = *c.problem; | ||||||||||||||||||||||||||
| for (i_t v = 0; v < p.n_variables; ++v) { | ||||||||||||||||||||||||||
| if (!std::isfinite(lifted[v]) || !check_variable_within_bounds(c, v, lifted[v]) || | ||||||||||||||||||||||||||
| (is_integer_var(c, v) && !p.is_integer(lifted[v]))) | ||||||||||||||||||||||||||
| return {}; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| for (i_t r = 0; r < p.n_constraints; ++r) { | ||||||||||||||||||||||||||
| const f_t activity = compensated_dot2_csr(p, lifted, r); | ||||||||||||||||||||||||||
| const f_t tol = p.tolerances.absolute_tolerance; | ||||||||||||||||||||||||||
| if (!std::isfinite(activity) || activity < p.cstr_lb[r] - tol || activity > p.cstr_ub[r] + tol) | ||||||||||||||||||||||||||
| return {}; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return lifted; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Solve a lane in equality-reduced coordinates, then lift every candidate back into the unchanged | ||||||||||||||||||||||||||
| // parent model before reporting it. | ||||||||||||||||||||||||||
| template <typename i_t, typename f_t> | ||||||||||||||||||||||||||
| bool try_equality_substituted_solve(fj_cpu_climber_t<i_t, f_t>& c, | ||||||||||||||||||||||||||
| double time_limit, | ||||||||||||||||||||||||||
| double work_unit_limit) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| if (!c.use_equality_substitution || c.feasible_found || c.producer_sync || time_limit <= 0) | ||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||
| const auto started = std::chrono::steady_clock::now(); | ||||||||||||||||||||||||||
| std::vector<fj_equality_substitution_t<i_t, f_t>> substitutions; | ||||||||||||||||||||||||||
| std::vector<i_t> retained; | ||||||||||||||||||||||||||
| std::unique_ptr<fj_cpu_climber_t<i_t, f_t>> child; | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| phase_timer_t timer(c.t_start); | ||||||||||||||||||||||||||
| child = | ||||||||||||||||||||||||||
| make_equality_reduced_climber(c, std::min(0.75, 0.15 * time_limit), substitutions, retained); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if (!child) return false; | ||||||||||||||||||||||||||
| const double remaining = | ||||||||||||||||||||||||||
| time_limit - std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count(); | ||||||||||||||||||||||||||
| if (remaining <= 0) return false; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| bool rejected_lift = false; | ||||||||||||||||||||||||||
| child->work_unit_bias = c.work_unit_bias; | ||||||||||||||||||||||||||
| child->improvement_callback = | ||||||||||||||||||||||||||
| [&](f_t child_objective, const std::vector<f_t>& assignment, double work) { | ||||||||||||||||||||||||||
| if (assignment.size() != retained.size()) { | ||||||||||||||||||||||||||
| rejected_lift = true; | ||||||||||||||||||||||||||
| child->halted = true; | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const std::vector<f_t> lifted = | ||||||||||||||||||||||||||
| lift_equality_substituted_assignment(c, assignment, retained, substitutions); | ||||||||||||||||||||||||||
| if (lifted.empty()) { | ||||||||||||||||||||||||||
| rejected_lift = true; | ||||||||||||||||||||||||||
| child->halted = true; | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const f_t objective = child_objective + child->problem->objective_offset; | ||||||||||||||||||||||||||
| if (!c.feasible_found || objective < c.h_best_objective) { | ||||||||||||||||||||||||||
| c.h_best_assignment = lifted; | ||||||||||||||||||||||||||
| c.h_best_objective = objective; | ||||||||||||||||||||||||||
| c.feasible_found = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| report_cpu_incumbent(c, objective, lifted, work); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const auto setup_stats = static_cast<const fj_stats_t<i_t>&>(c); | ||||||||||||||||||||||||||
| cpufj_solve(child.get(), remaining, work_unit_limit); | ||||||||||||||||||||||||||
| static_cast<fj_stats_t<i_t>&>(c) = static_cast<const fj_stats_t<i_t>&>(*child); | ||||||||||||||||||||||||||
|
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. NITPICK: can you replace these static casts with a simpler expression? |
||||||||||||||||||||||||||
| c.t_start += setup_stats.t_start; | ||||||||||||||||||||||||||
| c.t_bound_prop += setup_stats.t_bound_prop; | ||||||||||||||||||||||||||
| c.t_lp_start += setup_stats.t_lp_start; | ||||||||||||||||||||||||||
| c.t_features += setup_stats.t_features; | ||||||||||||||||||||||||||
| c.t_init_lhs += setup_stats.t_init_lhs; | ||||||||||||||||||||||||||
| c.iterations = child->iterations; | ||||||||||||||||||||||||||
| c.work_units_elapsed.store(child->work_units_elapsed.load(std::memory_order_relaxed), | ||||||||||||||||||||||||||
| std::memory_order_relaxed); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (child->feasible_found && child->h_best_assignment.size() == retained.size()) { | ||||||||||||||||||||||||||
| std::vector<f_t> lifted = lift_equality_substituted_assignment( | ||||||||||||||||||||||||||
| c, child->h_best_assignment.underlying(), retained, substitutions); | ||||||||||||||||||||||||||
| if (lifted.empty()) return c.feasible_found; | ||||||||||||||||||||||||||
| const f_t objective = compensated_dot2( | ||||||||||||||||||||||||||
| thrust::make_permutation_iterator(c.problem->h_obj_coeffs.data(), | ||||||||||||||||||||||||||
| c.problem->h_objective_vars.data()), | ||||||||||||||||||||||||||
| thrust::make_permutation_iterator(lifted.data(), c.problem->h_objective_vars.data()), | ||||||||||||||||||||||||||
| c.problem->h_objective_vars.size()); | ||||||||||||||||||||||||||
| if (!c.feasible_found || objective < c.h_best_objective) { | ||||||||||||||||||||||||||
| c.h_best_assignment = std::move(lifted); | ||||||||||||||||||||||||||
| c.h_best_objective = objective; | ||||||||||||||||||||||||||
| c.feasible_found = true; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return !rejected_lift || c.feasible_found; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| template <typename i_t, typename f_t> | ||||||||||||||||||||||||||
| void cpufj_solve(fj_cpu_climber_t<i_t, f_t>* fj_cpu, double time_limit, double work_unit_limit) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| if (try_cpufj_binary_solve(*fj_cpu, time_limit, work_unit_limit)) return; | ||||||||||||||||||||||||||
| const auto solve_start = std::chrono::steady_clock::now(); | ||||||||||||||||||||||||||
| if (fj_cpu->use_precedence_start) apply_precedence_completion_start(*fj_cpu); | ||||||||||||||||||||||||||
| apply_bound_propagation(*fj_cpu); | ||||||||||||||||||||||||||
| if (fj_cpu->use_equality_substitution) { | ||||||||||||||||||||||||||
| const double elapsed = | ||||||||||||||||||||||||||
| std::chrono::duration<double>(std::chrono::steady_clock::now() - solve_start).count(); | ||||||||||||||||||||||||||
| if (try_equality_substituted_solve(*fj_cpu, time_limit - elapsed, work_unit_limit)) return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const double setup_time_left = | ||||||||||||||||||||||||||
| fj_cpu->use_equality_substitution | ||||||||||||||||||||||||||
| ? std::max( | ||||||||||||||||||||||||||
| 0.0, | ||||||||||||||||||||||||||
| time_limit - | ||||||||||||||||||||||||||
| std::chrono::duration<double>(std::chrono::steady_clock::now() - solve_start).count()) | ||||||||||||||||||||||||||
| : time_limit; | ||||||||||||||||||||||||||
| if (!fj_cpu->feasible_found) { apply_lp_rounded_start(*fj_cpu, setup_time_left); } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const bool paid_setup = fj_cpu->use_bound_prop || fj_cpu->use_lp_start || | ||||||||||||||||||||||||||
| fj_cpu->use_precedence_start || fj_cpu->use_equality_substitution; | ||||||||||||||||||||||||||
| const double setup_seconds = | ||||||||||||||||||||||||||
| paid_setup | ||||||||||||||||||||||||||
| ? std::chrono::duration<double>(std::chrono::steady_clock::now() - solve_start).count() | ||||||||||||||||||||||||||
| : 0.0; | ||||||||||||||||||||||||||
| const double remaining = std::max(0.0, time_limit - setup_seconds); | ||||||||||||||||||||||||||
| if (remaining <= 0.0) return; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (try_cpufj_binary_solve(*fj_cpu, remaining, work_unit_limit)) return; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| clamp_start_magnitude(*fj_cpu, fj_cpu->problem->n_variables); | ||||||||||||||||||||||||||
|
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Check the magnitude limit value and other writers of h_best_assignment before the loop.
rg -nP -C3 'start_magnitude_limit' cpp/src/mip_heuristics/feasibility_jump
rg -nP -C2 'h_best_assignment' cpp/src/mip_heuristics/feasibility_jump/cpu/setup/bounds.cpp cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.cppRepository: NVIDIA/cuopt Length of output: 10648 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- loop.cpp relevant ranges ---'
sed -n '120,205p' cpp/src/mip_heuristics/feasibility_jump/cpu/loop.cpp
printf '%s\n' '--- loop.cpp feasible_found and clamp references ---'
rg -n -C3 'feasible_found|clamp_start_magnitude|apply_lp_rounded_start|apply_lp_polish|structural' cpp/src/mip_heuristics/feasibility_jump/cpu/loop.cpp cpp/src/mip_heuristics/feasibility_jump/cpu
printf '%s\n' '--- bounds.cpp relevant definitions ---'
sed -n '1,70p' cpp/src/mip_heuristics/feasibility_jump/cpu/setup/bounds.cpp
printf '%s\n' '--- lp.cpp rounded-start and polish ranges ---'
sed -n '300,435p' cpp/src/mip_heuristics/feasibility_jump/cpu/setup/lp.cppRepository: NVIDIA/cuopt Length of output: 42433 Preserve
Keep the clamp on 🐛 Suggested fix fj_cpu.h_assignment[var] = std::clamp((f_t)fj_cpu.h_assignment[var], lower, upper);
- fj_cpu.h_best_assignment[var] = std::clamp((f_t)fj_cpu.h_best_assignment[var], lower, upper);
+ if (!fj_cpu.feasible_found) {
+ fj_cpu.h_best_assignment[var] =
+ std::clamp((f_t)fj_cpu.h_best_assignment[var], lower, upper);
+ }🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Past this point the search runs on one-sided rows. Everything above reasons about the original | ||||||||||||||||||||||||||
| // model, which is why the rows are built here and not at construction. | ||||||||||||||||||||||||||
|
|
@@ -38,7 +187,8 @@ void cpufj_solve(fj_cpu_climber_t<i_t, f_t>* fj_cpu, double time_limit, double w | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| [[maybe_unused]] i_t local_mins = 0; | ||||||||||||||||||||||||||
| const auto loop_start = std::chrono::steady_clock::now(); | ||||||||||||||||||||||||||
| const auto loop_start = paid_setup ? solve_start : std::chrono::steady_clock::now(); | ||||||||||||||||||||||||||
| bool first_cross_needs_polish = fj_cpu->use_lp_polish; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| fj_cpu->rng.set_seed(fj_cpu->settings.seed); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -86,6 +236,13 @@ void cpufj_solve(fj_cpu_climber_t<i_t, f_t>* fj_cpu, double time_limit, double w | |||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (first_cross_needs_polish && fj_cpu->feasible_found) { | ||||||||||||||||||||||||||
| first_cross_needs_polish = false; | ||||||||||||||||||||||||||
| const double elapsed = | ||||||||||||||||||||||||||
| std::chrono::duration<double>(std::chrono::steady_clock::now() - loop_start).count(); | ||||||||||||||||||||||||||
| apply_lp_polish(*fj_cpu, fj_cpu->hp.lp_polish_budget_share * (time_limit - elapsed)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // periodically recompute the slacks and violation scores | ||||||||||||||||||||||||||
| // to correct any accumulated numerical errors | ||||||||||||||||||||||||||
| if (fj_cpu->trigger_early_lhs_recomputation) { | ||||||||||||||||||||||||||
|
|
@@ -152,6 +309,11 @@ void cpufj_solve(fj_cpu_climber_t<i_t, f_t>* fj_cpu, double time_limit, double w | |||||||||||||||||||||||||
| should_perturb = true; | ||||||||||||||||||||||||||
| // Without this the counter stays above the interval and every later iteration perturbs. | ||||||||||||||||||||||||||
| fj_cpu->iterations_since_best = 0; | ||||||||||||||||||||||||||
| if (fj_cpu->use_lp_polish && fj_cpu->feasible_found) { | ||||||||||||||||||||||||||
| const double elapsed = | ||||||||||||||||||||||||||
| std::chrono::duration<double>(std::chrono::steady_clock::now() - loop_start).count(); | ||||||||||||||||||||||||||
| apply_lp_polish(*fj_cpu, fj_cpu->hp.lp_polish_budget_share * (time_limit - elapsed)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+312
to
+316
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. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win Skip LP polish when the incumbent has not changed since the last polish. In the feasible region, each perturbation event now runs
If The failure path also copies Record the objective that was last polished, and polish only when ⚡ Proposed fixNear Line 191: f_t last_polished_objective = std::numeric_limits<f_t>::quiet_NaN();At Line 239-244, after the first-cross polish: apply_lp_polish(*fj_cpu, fj_cpu->hp.lp_polish_budget_share * (time_limit - elapsed));
last_polished_objective = fj_cpu->h_best_objective;- if (fj_cpu->use_lp_polish && fj_cpu->feasible_found) {
+ if (fj_cpu->use_lp_polish && fj_cpu->feasible_found &&
+ fj_cpu->h_best_objective != last_polished_objective) {
const double elapsed =
std::chrono::duration<double>(std::chrono::steady_clock::now() - loop_start).count();
apply_lp_polish(*fj_cpu, fj_cpu->hp.lp_polish_budget_share * (time_limit - elapsed));
+ last_polished_objective = fj_cpu->h_best_objective;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (score > fj_staged_score_t::zero() && !should_perturb) { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Recompute the parent objective for lifted candidates in the callback.
The callback derives the reported objective from the reduced problem:
child_objective + child->problem->objective_offset. The lifted vector is not the exact image of the reduced point.lift_equality_substituted_assignmentclamps substituted variables to their bounds and rounds integer variables at Lines 42-47, so the parent objective ofliftedcan differ fromchild_objective. The callback then stores that value inc.h_best_objectiveand publishes it throughreport_cpu_incumbent. A consumer can therefore receive an incumbent whose reported objective does not match its assignment, and the comparison at Line 106 can keep a worse point.The post-solve path at Lines 130-134 already recomputes the objective in the parent model. Use the same computation in the callback.
🐛 Proposed fix: compute the objective from the lifted vector
🤖 Prompt for AI Agents