diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index ec82c4b423..e9d1333cb3 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -79,10 +79,16 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_thandle_ptr), sub_mip_recombiner( context, population, context.problem_ptr->n_variables, context.problem_ptr->handle_ptr), - rng(cuopt::seed_generator::get_seed()), + rng(mip_derive_seed(context.base_seed, mip_rng_component_id_t::diversity_manager, 0)), stats(context.stats), - mab_recombiner(0, cuopt::seed_generator::get_seed(), recombiner_alpha, "recombiner"), - mab_ls(mab_ls_config_t::n_of_arms, cuopt::seed_generator::get_seed(), ls_alpha, "ls"), + mab_recombiner(0, + mip_derive_seed(context.base_seed, mip_rng_component_id_t::diversity_manager, 1), + recombiner_alpha, + "recombiner"), + mab_ls(mab_ls_config_t::n_of_arms, + mip_derive_seed(context.base_seed, mip_rng_component_id_t::diversity_manager, 2), + ls_alpha, + "ls"), ls_hash_map(*context.problem_ptr) { int max_config = -1; @@ -439,7 +445,7 @@ template void diversity_manager_t::run_fj_alone(solution_t& solution) { CUOPT_LOG_INFO("Running FJ alone!"); - solution.round_nearest(); + solution.round_nearest(rng()); ls.fj.settings.mode = fj_mode_t::EXIT_NON_IMPROVING; ls.fj.settings.n_of_minimums_for_exit = 20000 * 1000; ls.fj.settings.update_weights = true; @@ -693,7 +699,7 @@ solution_t diversity_manager_t::run_solver() if (ls.lp_optimal_exists) { solution_t lp_rounded_sol(*problem_ptr); lp_rounded_sol.copy_new_assignment(lp_optimal_solution); - lp_rounded_sol.round_nearest(); + lp_rounded_sol.round_nearest(rng()); lp_rounded_sol.compute_feasibility(); population.add_solution(std::move(lp_rounded_sol)); ls.start_cpufj_lptopt_scratch_threads(population); diff --git a/cpp/src/mip_heuristics/diversity/population.cu b/cpp/src/mip_heuristics/diversity/population.cu index e6fffa97b2..63f6d0fadf 100644 --- a/cpp/src/mip_heuristics/diversity/population.cu +++ b/cpp/src/mip_heuristics/diversity/population.cu @@ -14,7 +14,6 @@ #include #include #include -#include #include @@ -42,7 +41,7 @@ population_t::population_t(std::string const& name_, max_solutions(max_solutions_), infeasibility_importance(infeasibility_weight_), weights(0, context.problem_ptr->handle_ptr), - rng(cuopt::seed_generator::get_seed()), + rng(mip_derive_seed(context.base_seed, mip_rng_component_id_t::population)), early_exit_primal_generation(false), population_hash_map(*problem_ptr), timer(0) diff --git a/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh index 42fd838105..1504be3d73 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh @@ -11,9 +11,10 @@ #include #include +#include #include #include -#include +#include namespace cuopt::mathematical_optimization::mip { @@ -24,9 +25,9 @@ class bound_prop_recombiner_t : public recombiner_t { i_t n_vars, constraint_prop_t& constraint_prop_, const raft::handle_t* handle_ptr) - : recombiner_t(context, n_vars, handle_ptr), + : recombiner_t( + context, n_vars, handle_ptr, mip_rng_component_id_t::recombiner_bound_prop), constraint_prop(constraint_prop_), - rng(cuopt::seed_generator::get_seed()), vars_to_fix(n_vars, handle_ptr->get_stream()) { } @@ -65,7 +66,7 @@ class bound_prop_recombiner_t : public recombiner_t { offspring_view, int_tol, probing_values = probing_values.data(), - seed = cuopt::seed_generator::get_seed()] __device__(i_t idx) { + seed = this->rng.next_u64()] __device__(i_t idx) { f_t guiding_val = guiding_view.assignment[idx]; f_t other_val = other_view.assignment[idx]; cuopt_assert(guiding_view.problem.check_variable_within_bounds(idx, guiding_val), ""); @@ -151,7 +152,7 @@ class bound_prop_recombiner_t : public recombiner_t { if (n_different_vars > (i_t)bp_recombiner_config_t::max_n_of_vars_from_other) { fixed_from_guiding = n_vars_from_other - bp_recombiner_config_t::max_n_of_vars_from_other; n_vars_from_other = bp_recombiner_config_t::max_n_of_vars_from_other; - thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::get_seed()}; + thrust::default_random_engine g{this->rng.next_u32()}; thrust::shuffle(a.handle_ptr->get_thrust_policy(), this->remaining_indices.data(), this->remaining_indices.data() + n_different_vars, @@ -245,7 +246,6 @@ class bound_prop_recombiner_t : public recombiner_t { rmm::device_uvector vars_to_fix; constraint_prop_t& constraint_prop; - thrust::default_random_engine rng; }; } // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh index 85909c9d69..b25bd6fcf5 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh @@ -11,9 +11,9 @@ #include #include +#include #include #include -#include #include @@ -29,7 +29,7 @@ class fp_recombiner_t : public recombiner_t { line_segment_search_t& line_segment_search, rmm::device_uvector& lp_optimal_solution, const raft::handle_t* handle_ptr) - : recombiner_t(context, n_vars, handle_ptr), + : recombiner_t(context, n_vars, handle_ptr, mip_rng_component_id_t::recombiner_fp), vars_to_fix(n_vars, handle_ptr->get_stream()), fp(context, fj, constraint_prop, line_segment_search, lp_optimal_solution) { @@ -53,7 +53,7 @@ class fp_recombiner_t : public recombiner_t { i_t n_vars_from_other = n_different_vars; if (n_vars_from_other > (i_t)fp_recombiner_config_t::max_n_of_vars_from_other) { n_vars_from_other = fp_recombiner_config_t::max_n_of_vars_from_other; - thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::get_seed()}; + thrust::default_random_engine g{this->rng.next_u32()}; thrust::shuffle(a.handle_ptr->get_thrust_policy(), this->remaining_indices.data(), this->remaining_indices.data() + n_different_vars, @@ -114,7 +114,7 @@ class fp_recombiner_t : public recombiner_t { } // unfix the assignment on given result no matter if it is feasible offspring.unfix_variables(fixed_assignment, variable_map); - if (!run_fp) { offspring.round_nearest(); } + if (!run_fp) { offspring.round_nearest(this->rng.next_u64()); } cuopt_assert(offspring.test_number_all_integer(), "All must be integers after offspring"); offspring.compute_feasibility(); bool same_as_parents = this->check_if_offspring_is_same_as_parents(offspring, a, b); diff --git a/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh index a1e6e29c56..615a3e5b4f 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh @@ -10,9 +10,9 @@ #include "recombiner.cuh" #include +#include #include #include -#include namespace cuopt::mathematical_optimization::mip { @@ -23,7 +23,9 @@ class line_segment_recombiner_t : public recombiner_t { i_t n_vars, line_segment_search_t& line_segment_search_, const raft::handle_t* handle_ptr) - : recombiner_t(context, n_vars, handle_ptr), line_segment_search(line_segment_search_) + : recombiner_t( + context, n_vars, handle_ptr, mip_rng_component_id_t::recombiner_line_segment), + line_segment_search(line_segment_search_) { } @@ -40,7 +42,7 @@ class line_segment_recombiner_t : public recombiner_t { i_t n_vars_from_other = remaining_variables; if (n_vars_from_other > (i_t)ls_recombiner_config_t::max_n_of_vars_from_other) { n_vars_from_other = ls_recombiner_config_t::max_n_of_vars_from_other; - thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::get_seed()}; + thrust::default_random_engine g{this->rng.next_u32()}; thrust::shuffle(guiding_solution.handle_ptr->get_thrust_policy(), this->remaining_indices.data(), this->remaining_indices.data() + remaining_variables, diff --git a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh index 0e9c64e796..ce7dfd94f2 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh @@ -10,12 +10,13 @@ #include "recombiner_configs.hpp" #include "recombiner_stats.hpp" +#include #include #include #include #include #include -#include +#include #include #include @@ -65,8 +66,11 @@ class recombiner_t { public: recombiner_t(mip_solver_context_t& context_, i_t n_integer_vars, - const raft::handle_t* handle_ptr) + const raft::handle_t* handle_ptr, + mip_rng_component_id_t component_id) : context(context_), + rng(mip_derive_seed(context.base_seed, component_id), + mip_derive_stream(context.base_seed, component_id)), remaining_indices(n_integer_vars, handle_ptr->get_stream()), n_remaining(handle_ptr->get_stream()) { @@ -119,7 +123,7 @@ class recombiner_t { objective_indices.size()); if (objective_indices.size() > 0 && objective_indices_in_subproblem.size() < 0.4 * remaining_variables) { - std::default_random_engine rng_host(cuopt::seed_generator::get_seed()); + std::default_random_engine rng_host(rng.next_i64()); std::vector objective_indices_not_in_subproblem; std::set_difference(objective_indices.begin(), objective_indices.end(), @@ -219,6 +223,7 @@ class recombiner_t { } mip_solver_context_t& context; + splitmix64_t rng; rmm::device_uvector remaining_indices; rmm::device_scalar n_remaining; static std::vector enabled_recombiners; diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index 95e5c45a8c..24a56dbdd7 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -25,7 +25,8 @@ class sub_mip_recombiner_t : public recombiner_t { population_t& population, i_t n_vars, const raft::handle_t* handle_ptr) - : recombiner_t(context, n_vars, handle_ptr), + : recombiner_t( + context, n_vars, handle_ptr, mip_rng_component_id_t::recombiner_sub_mip), vars_to_fix(n_vars, handle_ptr->get_stream()), context(context), population(population) @@ -57,7 +58,7 @@ class sub_mip_recombiner_t : public recombiner_t { i_t n_vars_from_other = n_different_vars; if (n_vars_from_other > (i_t)sub_mip_recombiner_config_t::max_n_of_vars_from_other) { n_vars_from_other = sub_mip_recombiner_config_t::max_n_of_vars_from_other; - thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::get_seed()}; + thrust::default_random_engine g{this->rng.next_u32()}; thrust::shuffle(a.handle_ptr->get_thrust_policy(), this->remaining_indices.data(), this->remaining_indices.data() + n_different_vars, @@ -156,7 +157,7 @@ class sub_mip_recombiner_t : public recombiner_t { offspring .clamp_within_bounds(); // Scaling might bring some very slight variable bound violations } else { - offspring.round_nearest(); + offspring.round_nearest(this->rng.next_u64()); } cuopt_func_call(offspring.test_variable_bounds()); cuopt_assert(offspring.test_number_all_integer(), "All must be integers after offspring"); diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu index ba14e657d5..a6c95b3297 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu @@ -15,9 +15,11 @@ template early_cpufj_t::early_cpufj_t( const optimization_problem_t& op_problem, const typename mip_solver_settings_t::tolerances_t& tolerances, - early_incumbent_callback_t incumbent_callback) + early_incumbent_callback_t incumbent_callback, + uint64_t seed) : early_heuristic_t>( - op_problem, tolerances, std::move(incumbent_callback)) + op_problem, tolerances, std::move(incumbent_callback)), + seed_(seed) { } @@ -36,7 +38,8 @@ void early_cpufj_t::start() this->preemption_flag_.store(false); this->start_time_ = std::chrono::steady_clock::now(); - fj_cpu_ = init_fj_cpu_standalone(*this->problem_ptr_, *this->solution_ptr_, preemption_flag_); + fj_cpu_ = + init_fj_cpu_standalone(*this->problem_ptr_, *this->solution_ptr_, preemption_flag_, seed_); fj_cpu_->log_prefix = "[Early CPUFJ] "; diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh index e2bb2c07b2..61ef3ff51f 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh @@ -20,7 +20,8 @@ class early_cpufj_t : public early_heuristic_t public: early_cpufj_t(const optimization_problem_t& op_problem, const typename mip_solver_settings_t::tolerances_t& tolerances, - early_incumbent_callback_t incumbent_callback); + early_incumbent_callback_t incumbent_callback, + uint64_t seed); ~early_cpufj_t(); @@ -32,6 +33,9 @@ class early_cpufj_t : public early_heuristic_t private: std::unique_ptr> fj_cpu_; std::atomic preemption_flag_{false}; + // Explicit seed for this climber's FJ RNG, resolved once from the solve's base seed (see + // mip_solver_context_t::base_seed) since this heuristic runs before that context exists. + uint64_t seed_; }; } // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu index 463f074f59..a049c76afa 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/early_gpufj.cu @@ -51,7 +51,8 @@ void early_gpufj_t::start() fj_settings.update_weights = true; fj_settings.feasibility_run = false; - fj_ptr_ = std::make_unique>(*context_ptr_, fj_settings); + fj_ptr_ = std::make_unique>( + *context_ptr_, fj_settings, mip_rng_component_id_t::early_gpufj); fj_ptr_->improvement_callback = [this](f_t user_obj, const std::vector& h_assignment) { f_t solver_obj = this->problem_ptr_->get_solver_obj_from_user_obj(user_obj); diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu index 4efd73e454..ff776a8ed5 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -43,8 +42,12 @@ static constexpr int iterations_per_graph = 50; #endif template -fj_t::fj_t(mip_solver_context_t& context_, fj_settings_t in_settings) - : context(context_), +fj_t::fj_t(mip_solver_context_t& context_, + fj_settings_t in_settings, + mip_rng_component_id_t seed_component_id) + : rng(mip_derive_seed(context_.base_seed, seed_component_id), + mip_derive_stream(context_.base_seed, seed_component_id)), + context(context_), pb_ptr(context.problem_ptr), handle_ptr(const_cast(pb_ptr->handle_ptr)), settings(in_settings), @@ -135,12 +138,12 @@ void fj_t::reset_weights(const rmm::cuda_stream_view& climber_stream, template void fj_t::randomize_weights(const raft::handle_t* handle_ptr) { - std::mt19937 rng(cuopt::seed_generator::get_seed()); + std::mt19937 host_rng(rng.next_i64()); constexpr f_t min_weight = 10.; constexpr f_t max_weight = 30.; // generate a range of weights between 10. and 30. auto h_cstr_vec = - get_random_uniform_vector(cstr_weights.size(), rng, min_weight, max_weight); + get_random_uniform_vector(cstr_weights.size(), host_rng, min_weight, max_weight); f_t h_max_weight = *std::max_element(h_cstr_vec.begin(), h_cstr_vec.end()); max_cstr_weight.set_value_async(h_max_weight, handle_ptr->get_stream()); raft::copy(cstr_weights.data(), h_cstr_vec.data(), h_cstr_vec.size(), handle_ptr->get_stream()); @@ -672,7 +675,7 @@ void fj_t::run_step_device(const rmm::cuda_stream_view& climber_stream auto& data = *climbers[climber_idx]; auto v = data.view(); - settings.seed = cuopt::seed_generator::get_seed(); + settings.seed = rng.next_i64(); // ensure an updated copy of the settings is used device-side raft::copy(v.settings, &settings, 1, climber_stream); @@ -1128,7 +1131,7 @@ i_t fj_t::solve(solution_t& solution) // if time limit exceeded: round all remaining fractionals if any by nearest rounding. if (climbers[0]->fractional_variables.set_size.value(handle_ptr->get_stream()) > 0) { - solution.round_nearest(); + solution.round_nearest(rng.next_u64()); } } diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh index 07c3be022f..93596ed307 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include @@ -213,7 +215,9 @@ class fj_t { using move_score_info_t = fj_move_score_info_base_t; using move_candidate_t = fj_move_candidate_t; - fj_t(mip_solver_context_t& context, fj_settings_t settings = fj_settings_t{}); + fj_t(mip_solver_context_t& context, + fj_settings_t settings = fj_settings_t{}, + mip_rng_component_id_t seed_component_id = mip_rng_component_id_t::local_search_cpu_fj); ~fj_t(); void reset_cuda_graph(); i_t solve(solution_t& solution); @@ -248,6 +252,18 @@ class fj_t { // executed after a roudning FJ run if any fractionals remain to eliminate them void round_remaining_fractionals(solution_t& solution, i_t climber_idx = 0); + // Draws the next seed from this instance's persistent RNG (see `rng` below). Used by callers + // outside fj_t (e.g. line_segment_search_t) that need a seed derived from this fj_t's stream + // without being able to reseed or otherwise mutate it directly. + uint64_t next_seed() { return rng.next_u64(); } + + private: + // Persistent RNG seeded once from context.base_seed and this instance's fixed component id, + // used for every seed this fj_t (and the CPU climbers it creates) needs. Never a runtime + // thread id -- see mip_rng_component_id_t. Private: external callers must go through + // next_seed() rather than reseed or otherwise mutate this directly. + splitmix64_t rng; + public: mip_solver_context_t& context; problem_t* pb_ptr; diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index f774431e75..54f925a6f2 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -1880,13 +1880,13 @@ std::unique_ptr> fj_t::create_cpu_climber( init_fj_cpu(*fj_cpu, solution, left_weights, right_weights, objective_weight, probing_cache); fj_cpu->settings = settings; if (randomize_params) { - auto rng = std::mt19937(cuopt::seed_generator::get_seed()); - fj_cpu->mtm_viol_samples = std::uniform_int_distribution(15, 50)(rng); - fj_cpu->mtm_sat_samples = std::uniform_int_distribution(10, 30)(rng); - fj_cpu->nnz_samples = std::uniform_int_distribution(2000, 15000)(rng); - fj_cpu->perturb_interval = std::uniform_int_distribution(50, 500)(rng); + auto host_rng = std::mt19937(rng.next_i64()); + fj_cpu->mtm_viol_samples = std::uniform_int_distribution(15, 50)(host_rng); + fj_cpu->mtm_sat_samples = std::uniform_int_distribution(10, 30)(host_rng); + fj_cpu->nnz_samples = std::uniform_int_distribution(2000, 15000)(host_rng); + fj_cpu->perturb_interval = std::uniform_int_distribution(50, 500)(host_rng); } - fj_cpu->settings.seed = cuopt::seed_generator::get_seed(); + fj_cpu->settings.seed = rng.next_i64(); return fj_cpu; // move } @@ -2073,6 +2073,7 @@ std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, + uint64_t seed, fj_settings_t settings) { raft::common::nvtx::range scope("init_fj_cpu_standalone"); @@ -2084,7 +2085,7 @@ std::unique_ptr> init_fj_cpu_standalone( const probing_cache_t* no_implications = nullptr; init_fj_cpu(*fj_cpu, solution, default_weights, default_weights, 0.0, no_implications); fj_cpu->settings = settings; - fj_cpu->settings.seed = cuopt::seed_generator::get_seed(); + fj_cpu->settings.seed = seed; return fj_cpu; } @@ -2165,6 +2166,7 @@ template std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, + uint64_t seed, fj_settings_t settings); template void finalize_fj_cpu_host_initialization( fj_cpu_climber_t& fj_cpu, @@ -2185,6 +2187,7 @@ template std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, + uint64_t seed, fj_settings_t settings); template void finalize_fj_cpu_host_initialization( fj_cpu_climber_t& fj_cpu, diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh index 411b4083f7..63749a57cd 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh @@ -229,6 +229,7 @@ std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, + uint64_t seed, fj_settings_t settings = fj_settings_t{}); } // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu b/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu index bc47027ed5..d7fece97ad 100644 --- a/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu +++ b/cpp/src/mip_heuristics/local_search/feasibility_pump/feasibility_pump.cu @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -51,7 +50,7 @@ feasibility_pump_t::feasibility_pump_t( orig_variable_types(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), lp_optimal_solution(lp_optimal_solution_), - rng(cuopt::seed_generator::get_seed()), + rng(mip_derive_seed(context.base_seed, mip_rng_component_id_t::feasibility_pump)), timer(20.) { } @@ -274,7 +273,7 @@ template void feasibility_pump_t::perturbate(solution_t& solution) { constexpr f_t change_ratio = 0.1; - solution.assign_random_within_bounds(change_ratio, true); + solution.assign_random_within_bounds(rng(), change_ratio, true); } template @@ -473,7 +472,7 @@ bool feasibility_pump_t::run_single_fp_descent(solution_t& s { raft::common::nvtx::range fun_scope("run_single_fp_descent"); // start by doing nearest rounding - solution.round_nearest(); + solution.round_nearest(rng()); raft::copy(last_rounding.data(), solution.assignment.data(), solution.assignment.size(), diff --git a/cpp/src/mip_heuristics/local_search/line_segment_search/line_segment_search.cu b/cpp/src/mip_heuristics/local_search/line_segment_search/line_segment_search.cu index 4e52c33104..e2ef66a625 100644 --- a/cpp/src/mip_heuristics/local_search/line_segment_search/line_segment_search.cu +++ b/cpp/src/mip_heuristics/local_search/line_segment_search/line_segment_search.cu @@ -161,7 +161,7 @@ bool line_segment_search_t::search_line_segment( bool is_feasible = false; // if (!settings.recombiner_mode) { if (true) { - is_feasible = solution.round_nearest(); + is_feasible = solution.round_nearest(fj.next_seed()); } else { fj.settings.mode = fj_mode_t::ROUNDING; fj.settings.update_weights = false; diff --git a/cpp/src/mip_heuristics/local_search/local_search.cu b/cpp/src/mip_heuristics/local_search/local_search.cu index 23edf555cd..b1713b4d6d 100644 --- a/cpp/src/mip_heuristics/local_search/local_search.cu +++ b/cpp/src/mip_heuristics/local_search/local_search.cu @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -39,7 +38,7 @@ local_search_t::local_search_t(mip_solver_context_t& context constraint_prop, line_segment_search, lp_optimal_solution_), - rng(cuopt::seed_generator::get_seed()), + rng(mip_derive_seed(context.base_seed, mip_rng_component_id_t::local_search)), problem_with_objective_cut(*context.problem_ptr, context.problem_ptr->handle_ptr) { const int n_cpufj = context.settings.heuristic_params.num_cpufj_threads; @@ -65,7 +64,7 @@ void local_search_t::start_cpufj_scratch_threads(population_t 0) solution.assign_random_within_bounds(0.4); + if (counter > 0) solution.assign_random_within_bounds(rng(), 0.4); cpu_fj = fj.create_cpu_climber(solution, default_weights, default_weights, @@ -117,7 +116,7 @@ void local_search_t::start_cpufj_lptopt_scratch_threads( solution_t solution_lp(*context.problem_ptr); solution_lp.copy_new_assignment( host_copy(lp_optimal_solution, context.problem_ptr->handle_ptr->get_stream())); - solution_lp.round_random_nearest(500); + solution_lp.round_random_nearest(500, rng()); scratch_cpu_fj_on_lp_opt = fj.create_cpu_climber(solution_lp, default_weights, default_weights, @@ -504,7 +503,7 @@ bool local_search_t::check_fj_on_lp_optimal(solution_t& solu if (perturb) { CUOPT_LOG_DEBUG("Perturbating solution on initial fj on optimal run!"); f_t perturbation_ratio = 0.2; - solution.assign_random_within_bounds(perturbation_ratio); + solution.assign_random_within_bounds(rng(), perturbation_ratio); } cuopt_func_call(solution.test_variable_bounds(false)); f_t lp_run_time_after_feasible = std::min(1., timer.remaining_time()); @@ -593,7 +592,7 @@ bool local_search_t::run_staged_fp(solution_t& solution, if (is_feasible) { break; } if (timer.check_time_limit()) { fp.revert_relaxation(solution); - solution.round_nearest(); + solution.round_nearest(rng()); CUOPT_LOG_DEBUG("Time limit reached during binary stage!"); return false; } @@ -620,7 +619,7 @@ bool local_search_t::run_staged_fp(solution_t& solution, if (is_feasible) { return true; } if (timer.check_time_limit()) { CUOPT_LOG_DEBUG("FP time limit reached during integer stage!"); - solution.round_nearest(); + solution.round_nearest(rng()); return false; } is_feasible = fp.restart_fp(solution); diff --git a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu index ddc6db68a7..2779a757e6 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu @@ -16,13 +16,13 @@ #include #include #include -#include namespace cuopt::mathematical_optimization::mip { template bounds_repair_t::bounds_repair_t(const problem_t& pb, - bound_presolve_t& bound_presolve_) + bound_presolve_t& bound_presolve_, + uint64_t seed) : bound_presolve(bound_presolve_), candidates(pb.handle_ptr), best_bounds(pb.handle_ptr), @@ -31,7 +31,7 @@ bounds_repair_t::bounds_repair_t(const problem_t& pb, violated_constraints(0, pb.handle_ptr->get_stream()), violated_cstr_map(0, pb.handle_ptr->get_stream()), total_vio(pb.handle_ptr->get_stream()), - gen(cuopt::seed_generator::get_seed()), + gen(seed), cycle_vector(MAX_CYCLE_SEQUENCE, -1) { } diff --git a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh index 8331749849..6cb07c13e2 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh +++ b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh @@ -16,6 +16,8 @@ #include #include +#include + namespace cuopt::mathematical_optimization::mip { // from the paper, probability of choosing random candidate= noise parameter @@ -111,7 +113,9 @@ struct candidates_t { template class bounds_repair_t { public: - bounds_repair_t(const problem_t& p, bound_presolve_t& bound_presolve); + bounds_repair_t(const problem_t& p, + bound_presolve_t& bound_presolve, + uint64_t seed); void resize(const problem_t& problem); void reset(); f_t get_ii_violation(problem_t& problem); diff --git a/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu b/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu index 861432b720..969406471b 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu @@ -9,7 +9,6 @@ #include #include #include -#include #include "constraint_prop.cuh" #include "simple_rounding.cuh" @@ -33,7 +32,9 @@ constraint_prop_t::constraint_prop_t(mip_solver_context_t& c temp_sol(*context.problem_ptr), bounds_update(context), multi_probe(context), - bounds_repair(*context.problem_ptr, bounds_update), + bounds_repair(*context.problem_ptr, + bounds_update, + mip_derive_seed(context.base_seed, mip_rng_component_id_t::constraint_prop, 1)), conditional_bounds_update(*context.problem_ptr), set_vars(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), unset_vars(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), @@ -41,7 +42,9 @@ 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(mip_derive_seed(context.base_seed, mip_rng_component_id_t::constraint_prop, 0), + mip_derive_stream(context.base_seed, mip_rng_component_id_t::constraint_prop, 0), + 0) { } @@ -604,7 +607,7 @@ thrust::pair constraint_prop_t::generate_double_probing_pair if (probing_config.has_value()) { // for now get the first one auto [from_first, from_second] = probing_config.value().get().probing_values[unset_var_idx]; - std::mt19937 rng(cuopt::seed_generator::get_seed()); + std::mt19937 rng(this->rng.next_i64()); std::uniform_real_distribution dist(0.0f, 1.0f); f_t random_value = dist(rng); f_t average_value = (from_first + from_second) / 2; @@ -848,7 +851,7 @@ bool constraint_prop_t::find_integer( { using crit_t = termination_criterion_t; auto& unset_integer_vars = unset_vars; - std::mt19937 rng(cuopt::seed_generator::get_seed()); + std::mt19937 rng(this->rng.next_i64()); lb_restore.resize(sol.problem_ptr->n_variables, sol.handle_ptr->get_stream()); ub_restore.resize(sol.problem_ptr->n_variables, sol.handle_ptr->get_stream()); assignment_restore.resize(sol.problem_ptr->n_variables, sol.handle_ptr->get_stream()); @@ -863,7 +866,7 @@ bool constraint_prop_t::find_integer( multi_probe.resize(*sol.problem_ptr); if (max_timer.check_time_limit()) { CUOPT_LOG_DEBUG("Time limit is reached before bounds prop rounding!"); - sol.round_nearest(); + sol.round_nearest(this->rng.next_u64()); expand_device_copy(orig_sol.assignment, sol.assignment, sol.handle_ptr->get_stream()); cuopt_func_call(orig_sol.test_variable_bounds()); return orig_sol.compute_feasibility(); @@ -879,19 +882,19 @@ bool constraint_prop_t::find_integer( // round first unset_integer_vars.size() - 50, leave last 50 to be rounded by the algo i_t n_to_round = std::max(unset_integer_vars.size() - 50, 0lu); if (n_to_round > 0) { - thrust::for_each( - sol.handle_ptr->get_thrust_policy(), - unset_integer_vars.begin(), - unset_integer_vars.begin() + n_to_round, - [sol = sol.view(), seed = cuopt::seed_generator::get_seed()] __device__(i_t var_idx) { - raft::random::PCGenerator rng(seed, var_idx, 0); - auto var_bnd = sol.problem.variable_bounds[var_idx]; - sol.assignment[var_idx] = round_nearest(sol.assignment[var_idx], - get_lower(var_bnd), - get_upper(var_bnd), - sol.problem.tolerances.integrality_tolerance, - rng); - }); + thrust::for_each(sol.handle_ptr->get_thrust_policy(), + unset_integer_vars.begin(), + unset_integer_vars.begin() + n_to_round, + [sol = sol.view(), seed = this->rng.next_u64()] __device__(i_t var_idx) { + raft::random::PCGenerator rng(seed, var_idx, 0); + auto var_bnd = sol.problem.variable_bounds[var_idx]; + sol.assignment[var_idx] = + round_nearest(sol.assignment[var_idx], + get_lower(var_bnd), + get_upper(var_bnd), + sol.problem.tolerances.integrality_tolerance, + rng); + }); find_unset_integer_vars(sol, unset_integer_vars); } set_bounds_on_fixed_vars(sol); @@ -936,7 +939,7 @@ bool constraint_prop_t::find_integer( if (max_timer.check_time_limit()) { 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(); + sol.round_nearest(this->rng.next_u64()); timeout_happened = true; break; } diff --git a/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cu b/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cu index 676a6638d8..68e0a5a757 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cu @@ -14,12 +14,11 @@ #include #include #include -#include namespace cuopt::mathematical_optimization::mip { template -lb_bounds_repair_t::lb_bounds_repair_t(const raft::handle_t* handle_ptr) +lb_bounds_repair_t::lb_bounds_repair_t(const raft::handle_t* handle_ptr, uint64_t seed) : candidates(handle_ptr), best_bounds(handle_ptr), cstr_violations_up(0, handle_ptr->get_stream()), @@ -27,7 +26,7 @@ lb_bounds_repair_t::lb_bounds_repair_t(const raft::handle_t* handle_pt violated_constraints(0, handle_ptr->get_stream()), violated_cstr_map(0, handle_ptr->get_stream()), total_vio(handle_ptr->get_stream()), - gen(cuopt::seed_generator::get_seed()), + gen(seed), cycle_vector(MAX_CYCLE_SEQUENCE, -1) { } diff --git a/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cuh b/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cuh index 579fc84bdd..fd3edeea42 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cuh +++ b/cpp/src/mip_heuristics/local_search/rounding/lb_bounds_repair.cuh @@ -15,6 +15,8 @@ #include #include "bounds_repair.cuh" +#include + namespace cuopt::mathematical_optimization::mip { template @@ -40,7 +42,7 @@ struct lb_bounds_t { template class lb_bounds_repair_t { public: - lb_bounds_repair_t(const raft::handle_t* handle_ptr); + lb_bounds_repair_t(const raft::handle_t* handle_ptr, uint64_t seed); void resize(const load_balanced_problem_t& problem); void reset(); std::tuple get_ii_violation( diff --git a/cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu b/cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu index bde8b08ce8..f1de4d12ca 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/lb_constraint_prop.cu @@ -8,7 +8,6 @@ #include #include #include -#include #include "lb_constraint_prop.cuh" #include "simple_rounding.cuh" @@ -25,7 +24,9 @@ lb_constraint_prop_t::lb_constraint_prop_t(mip_solver_context_thandle_ptr), + bounds_repair( + context.problem_ptr->handle_ptr, + mip_derive_seed(context.base_seed, mip_rng_component_id_t::lb_constraint_prop, 1)), unset_vars(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), temp_assignment(context.problem_ptr->n_variables, context.problem_ptr->handle_ptr->get_stream()), @@ -33,7 +34,9 @@ lb_constraint_prop_t::lb_constraint_prop_t(mip_solver_context_thandle_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(mip_derive_seed(context.base_seed, mip_rng_component_id_t::lb_constraint_prop, 0), + mip_derive_stream(context.base_seed, mip_rng_component_id_t::lb_constraint_prop, 0), + 0) { } @@ -765,7 +768,7 @@ bool lb_constraint_prop_t::find_integer( using crit_t = termination_criterion_t; auto& unset_integer_vars = unset_vars; - std::mt19937 rng(cuopt::seed_generator::get_seed()); + std::mt19937 rng(this->rng.next_i64()); bounds_restore.resize(2 * orig_sol.problem_ptr->n_variables, orig_sol.handle_ptr->get_stream()); assignment_restore.resize(orig_sol.problem_ptr->n_variables, orig_sol.handle_ptr->get_stream()); @@ -780,7 +783,7 @@ bool lb_constraint_prop_t::find_integer( if (max_timer.check_time_limit()) { CUOPT_LOG_DEBUG("Time limit is reached before bounds prop rounding!"); - orig_sol.round_nearest(); + orig_sol.round_nearest(this->rng.next_u64()); cuopt_func_call(orig_sol.test_variable_bounds()); return orig_sol.compute_feasibility(); } @@ -933,7 +936,7 @@ bool lb_constraint_prop_t::find_integer( lb_bounds_update.infeas_constraints_count); expand_device_copy(orig_sol.assignment, assignment, orig_sol.handle_ptr->get_stream()); - orig_sol.round_nearest(); + orig_sol.round_nearest(this->rng.next_u64()); cuopt_assert(orig_sol.test_number_all_integer(), "All integers must be rounded"); cuopt_func_call(orig_sol.test_variable_bounds()); diff --git a/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu b/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu index 2d5aae0b0d..a44872aba9 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -102,7 +101,7 @@ bool invoke_simple_rounding(solution_t& solution) } template -void invoke_round_nearest(solution_t& solution) +void invoke_round_nearest(solution_t& solution, uint64_t seed) { i_t TPB = 128; bool brute_force_found_feas = check_brute_force_rounding(solution); @@ -112,14 +111,17 @@ void invoke_round_nearest(solution_t& solution) if (simple_round) { return; } i_t n_blocks = (solution.problem_ptr->n_integer_vars + TPB - 1) / TPB; - nearest_rounding_kernel<<get_stream()>>>( - solution.view(), cuopt::seed_generator::get_seed()); + nearest_rounding_kernel + <<get_stream()>>>(solution.view(), seed); RAFT_CHECK_CUDA(solution.handle_ptr->get_stream()); } template -void invoke_random_round_nearest(solution_t& solution, i_t n_target_random_rounds) +void invoke_random_round_nearest(solution_t& solution, + i_t n_target_random_rounds, + uint64_t seed) { + pcgenerator_t seed_rng(seed); i_t TPB = 128; i_t n_blocks = (solution.problem_ptr->n_variables + TPB - 1) / TPB; i_t n_integers = solution.compute_number_of_integers(); @@ -128,7 +130,7 @@ void invoke_random_round_nearest(solution_t& solution, i_t n_target_ra solution.problem_ptr->n_integer_vars); rmm::device_scalar n_randomly_rounded(zero_v, solution.handle_ptr->get_stream()); random_nearest_rounding_kernel<<get_stream()>>>( - solution.view(), cuopt::seed_generator::get_seed(), n_randomly_rounded.data()); + solution.view(), seed_rng.next_u64(), n_randomly_rounded.data()); i_t h_n_random_rounds = n_randomly_rounded.value(solution.handle_ptr->get_stream()); CUOPT_LOG_TRACE("Randomly rounded integers %d", h_n_random_rounds); i_t additional_roundings_needed = n_target_random_rounds - h_n_random_rounds; @@ -136,7 +138,7 @@ void invoke_random_round_nearest(solution_t& solution, i_t n_target_ra // TODO sort the remaining integers with fractionality and round them randomly rmm::device_uvector shuffled_indices(solution.problem_ptr->integer_indices, solution.handle_ptr->get_stream()); - thrust::default_random_engine rng(cuopt::seed_generator::get_seed()); + thrust::default_random_engine rng(seed_rng.next_u32()); // from the remaining integers, populate randomly. thrust::shuffle(solution.handle_ptr->get_thrust_policy(), shuffled_indices.begin(), @@ -144,7 +146,7 @@ void invoke_random_round_nearest(solution_t& solution, i_t n_target_ra rng); random_rounding_kernel <<<1, 1, 0, solution.handle_ptr->get_stream()>>>(solution.view(), - cuopt::seed_generator::get_seed(), + seed_rng.next_u64(), shuffled_indices.data(), n_randomly_rounded.data(), additional_roundings_needed); @@ -152,7 +154,7 @@ void invoke_random_round_nearest(solution_t& solution, i_t n_target_ra CUOPT_LOG_TRACE("Randomly rounded integers, after adding close integers too %d", h_n_random_rounds); } - solution.round_nearest(); + solution.round_nearest(seed_rng.next_u64()); RAFT_CHECK_CUDA(solution.handle_ptr->get_stream()); } @@ -173,9 +175,10 @@ void invoke_correct_integers(solution_t& solution, f_t tol) #define INSTANTIATE(F_TYPE) \ template bool check_brute_force_rounding(solution_t & solution); \ - template void invoke_random_round_nearest(solution_t & solution, \ - int n_target_random_rounds); \ - template void invoke_round_nearest(solution_t & solution); \ + template void invoke_random_round_nearest( \ + solution_t & solution, int n_target_random_rounds, uint64_t seed); \ + template void invoke_round_nearest(solution_t & solution, \ + uint64_t seed); \ template bool invoke_simple_rounding(solution_t & solution); \ template void invoke_correct_integers(solution_t & solution, \ F_TYPE tol); diff --git a/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cuh b/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cuh index 31fe25a81a..b1d7dbea00 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cuh +++ b/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cuh @@ -12,13 +12,15 @@ namespace cuopt::mathematical_optimization::mip { template -void invoke_round_nearest(solution_t& solution); +void invoke_round_nearest(solution_t& solution, uint64_t seed); template bool invoke_simple_rounding(solution_t& solution); template -void invoke_random_round_nearest(solution_t& solution, i_t n_target_random_rounds); +void invoke_random_round_nearest(solution_t& solution, + i_t n_target_random_rounds, + uint64_t seed); template void invoke_correct_integers(solution_t& solution, f_t tol); diff --git a/cpp/src/mip_heuristics/mip_constants.hpp b/cpp/src/mip_heuristics/mip_constants.hpp index f3fb68343a..2d96afd0c7 100644 --- a/cpp/src/mip_heuristics/mip_constants.hpp +++ b/cpp/src/mip_heuristics/mip_constants.hpp @@ -9,6 +9,12 @@ #include +#include +#include + +#include +#include + #define MIP_INSTANTIATE_FLOAT CUOPT_INSTANTIATE_FLOAT #define MIP_INSTANTIATE_DOUBLE CUOPT_INSTANTIATE_DOUBLE @@ -39,3 +45,70 @@ #define MIP_DEFAULT_STEAL_CHANCE 0.05 #define MIP_DEFAULT_NODES_PER_STEAL 10 #define MIP_DEFAULT_MAX_STEAL_ATTEMPTS 3 + +namespace cuopt::mathematical_optimization::mip { + +// Fixed logical identity used to seed each singleton heuristics component's own RNG stream from +// mip_solver_context_t::base_seed (see cpp/src/mip_heuristics/solver_context.cuh). These are NOT +// runtime thread/task ids -- OMP tasks can migrate between OS threads, so identity must be a +// compile-time-fixed constant to keep seeding reproducible across runs. Components that spawn a +// variable number of parallel workers (e.g. local-search CPU-FJ climbers) additionally offset by +// their own fixed slot index on top of the relevant id below. +enum class mip_rng_component_id_t : uint64_t { + diversity_manager = 10000, + population, + local_search, + feasibility_pump, + constraint_prop, + lb_constraint_prop, + recombiner_bound_prop, + recombiner_fp, + recombiner_line_segment, + recombiner_default, + recombiner_sub_mip, + local_search_cpu_fj, + early_cpufj, + early_gpufj, + line_segment_search, +}; + +// Resolves the solve-wide base seed: the user's requested seed if non-negative, otherwise a +// fresh random one. Called wherever a base seed is needed before/independently of +// mip_solver_context_t (which resolves it the same way for its own base_seed field) -- when +// requested_seed >= 0 both resolve to the identical value, which is what deterministic mode +// requires; when requested_seed < 0 each call draws independently, which is fine since no +// reproducibility is promised in that case. +inline uint64_t mip_resolve_base_seed(int64_t requested_seed) +{ + if (requested_seed >= 0) { return requested_seed; } + return std::random_device{}(); +} + +// Derives a well-mixed, reproducible 64-bit seed from the solve's base seed plus a fixed logical +// identity, for owners that need a raw seed value (e.g. to hand to std::mt19937 or a multi-armed +// bandit) rather than owning a PCG-family generator built from (seed, stream) -- see +// mip_derive_stream for the matching stream value. `index` further distinguishes multiple +// independent draws made by the same component (e.g. one per parallel worker slot). +inline uint64_t mip_derive_seed(uint64_t base_seed, + mip_rng_component_id_t component_id, + uint64_t index = 0) +{ + splitmix64_t seed_gen(base_seed + static_cast(component_id), index); + return seed_gen.next_u64(); +} + +// Derives a well-mixed, reproducible 64-bit stream/subsequence value from the solve's base seed +// plus a fixed logical identity, for owners that construct their own PCG-family generator +// (raft::PCGenerator, cuopt::pcgenerator_t) and need both a seed (mip_derive_seed) and an +// independent stream. `index` further distinguishes multiple independent draws made by the same +// component (e.g. one per parallel worker slot); pass the same `index` to mip_derive_seed and +// mip_derive_stream to get the matching seed/stream pair for one generator instance. +inline uint64_t mip_derive_stream(uint64_t base_seed, + mip_rng_component_id_t component_id, + uint64_t index = 0) +{ + splitmix64_t seed_gen(base_seed + static_cast(component_id), index); + return seed_gen.generate_stream(); +} + +} // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/solution/solution.cu b/cpp/src/mip_heuristics/solution/solution.cu index 3b00fca7a8..64cd156747 100644 --- a/cpp/src/mip_heuristics/solution/solution.cu +++ b/cpp/src/mip_heuristics/solution/solution.cu @@ -14,7 +14,6 @@ #include #include #include -#include #include @@ -226,10 +225,11 @@ void solution_t::copy_new_assignment(const rmm::device_uvector& d } template -void solution_t::assign_random_within_bounds(f_t ratio_of_vars_to_random_assign, +void solution_t::assign_random_within_bounds(uint64_t seed, + f_t ratio_of_vars_to_random_assign, bool only_integers) { - std::mt19937 rng(cuopt::seed_generator::get_seed()); + std::mt19937 rng(seed); auto stream = handle_ptr->get_stream(); std::vector h_assignment = host_copy(assignment, stream); std::uniform_real_distribution unif_prob(0, 1); @@ -367,20 +367,20 @@ void solution_t::compute_infeasibility() } template -bool solution_t::round_nearest() +bool solution_t::round_nearest(uint64_t seed) { clamp_within_bounds(); - invoke_round_nearest(*this); + invoke_round_nearest(*this, seed); cuopt_assert(compute_max_variable_violation() == 0., "Variables are not within bounds"); cuopt_assert(test_number_all_integer(), "Not all variables are integers"); return compute_feasibility(); } template -bool solution_t::round_random_nearest(i_t n_target_random_rounds) +bool solution_t::round_random_nearest(i_t n_target_random_rounds, uint64_t seed) { clamp_within_bounds(); - invoke_random_round_nearest(*this, n_target_random_rounds); + invoke_random_round_nearest(*this, n_target_random_rounds, seed); cuopt_assert(compute_max_variable_violation() == 0., "Variables are not within bounds"); cuopt_assert(test_number_all_integer(), "Not all variables are integers"); return compute_feasibility(); diff --git a/cpp/src/mip_heuristics/solution/solution.cuh b/cpp/src/mip_heuristics/solution/solution.cuh index f243937d3e..8c38cdf6ce 100644 --- a/cpp/src/mip_heuristics/solution/solution.cuh +++ b/cpp/src/mip_heuristics/solution/solution.cuh @@ -43,7 +43,8 @@ class solution_t { // returns the host assignment as a vector std::vector get_host_assignment(); // assigns random within bounds - void assign_random_within_bounds(f_t ratio_of_vars_to_random_assign = 1.0, + void assign_random_within_bounds(uint64_t seed, + f_t ratio_of_vars_to_random_assign = 1.0, bool only_integers = false); // sets given pairs of var/value to the assignment void set_vars_to_values(const std::vector>& var_val_pairs); @@ -51,9 +52,9 @@ class solution_t { void copy_new_assignment(const std::vector& h_assignment); void copy_new_assignment(const rmm::device_uvector& d_assignment); // rounds integer variables to the nearest integer val, returns whether the rounding is feasible - bool round_nearest(); + bool round_nearest(uint64_t seed); // rounds integers to random if fractionality is between 0.25 and 0.75. otherwise, to nearest - bool round_random_nearest(i_t n_target_random_rounds); + bool round_random_nearest(i_t n_target_random_rounds, uint64_t seed); bool round_simple(); // makes the approximate integer values up to INTEGRALITY TOLERANCE whole integers void correct_integer_precision(); diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 162a5ba291..ad608eeb49 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -28,7 +28,6 @@ #include #include #include -#include #include #include @@ -293,7 +292,10 @@ mip_solution_t run_mip_solver( no_bound); }; early_cpufj = std::make_unique>( - *problem.original_problem_ptr, settings.get_tolerances(), incumbent_callback); + *problem.original_problem_ptr, + settings.get_tolerances(), + incumbent_callback, + mip::mip_derive_seed(solver.context.base_seed, mip::mip_rng_component_id_t::early_cpufj)); // Convert initial_upper_bound from user-space to the CPUFJ's solver-space (papilo-presolved). // problem.get_solver_obj_from_user_obj uses the papilo offset/scale (matching the CPUFJ). if (std::isfinite(initial_upper_bound)) { @@ -370,9 +372,6 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p print_version_info(); - // Initialize seed generator if a specific seed is requested - if (settings.seed >= 0) { cuopt::seed_generator::set_seed(settings.seed); } - raft::common::nvtx::range fun_scope("Running solver"); auto timer = timer_t(time_limit); @@ -546,8 +545,12 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p }; // Start early CPUFJ on original problem (will restart on presolved problem after Papilo) - early_cpufj = std::make_unique>( - op_problem, settings.get_tolerances(), early_fj_callback); + const uint64_t early_fj_base_seed = mip::mip_resolve_base_seed(settings.seed); + early_cpufj = std::make_unique>( + op_problem, + settings.get_tolerances(), + early_fj_callback, + mip::mip_derive_seed(early_fj_base_seed, mip::mip_rng_component_id_t::early_cpufj)); early_cpufj->start(); CUOPT_LOG_DEBUG("Started early CPUFJ on original problem"); diff --git a/cpp/src/mip_heuristics/solver_context.cuh b/cpp/src/mip_heuristics/solver_context.cuh index f98386cbaf..19a0f119b0 100644 --- a/cpp/src/mip_heuristics/solver_context.cuh +++ b/cpp/src/mip_heuristics/solver_context.cuh @@ -7,6 +7,7 @@ #include +#include #include #include #include @@ -40,7 +41,10 @@ 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_) - : handle_ptr(handle_ptr_), problem_ptr(problem_ptr_), settings(settings_) + : handle_ptr(handle_ptr_), + problem_ptr(problem_ptr_), + settings(settings_), + base_seed(mip_resolve_base_seed(settings_.seed)) { cuopt_assert(problem_ptr != nullptr, "problem_ptr is nullptr"); stats.set_solution_bound(problem_ptr->maximize ? std::numeric_limits::infinity() @@ -57,6 +61,11 @@ struct mip_solver_context_t { diversity_manager_t* diversity_manager_ptr{nullptr}; std::atomic preempt_heuristic_solver_ = false; const mip_solver_settings_t settings; + // Single source of truth for all MIP heuristics RNGs: settings.seed if the user requested a + // specific one (>= 0), otherwise a seed drawn once at solve start. Every worker/component + // derives its own independent RNG stream from this plus a fixed logical identity (never a + // runtime thread id, since OMP tasks can migrate between OS threads). + const uint64_t base_seed; solver_stats_t stats; // Work limit context for tracking work units in deterministic mode (shared across all timers in // GPU heuristic loop) diff --git a/cpp/src/utilities/splitmix64.hpp b/cpp/src/utilities/splitmix64.hpp new file mode 100644 index 0000000000..8b6d34c301 --- /dev/null +++ b/cpp/src/utilities/splitmix64.hpp @@ -0,0 +1,130 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once +#include +#include + +namespace cuopt { + +class splitmix64_t { + public: + static constexpr uint64_t default_seed = 0xBAD0FF1CED15EA5EUL; + static constexpr uint64_t default_stream = 0x9E3779B97F4A7C15UL; + + /// Creates a new instance of the SplitMix64 generator. + /// + /// @param[in] seed generator seed + /// @param[in] stream generator increment + splitmix64_t(uint64_t seed = default_seed, uint64_t stream = default_stream) + : state_(seed), stream_(stream) + { + next_state(); + } + + /// Creates a new instance of the SplitMix64 generator + /// from an `other` generator. + /// + /// @param[in] other SplitMix64 generator to use as the data source + splitmix64_t(const splitmix64_t& other) : state_(other.state_), stream_(other.stream_) {} + + /// Default destructor. + ~splitmix64_t() = default; + + /// Reseeds the generator. + /// + /// @param[in] seed generator seed (optional) + /// @param[in] stream generator increment (optional) + constexpr void seed(uint64_t seed = default_seed, uint64_t stream = default_stream) + { + state_ = seed; + stream_ = stream; + } + + /// @returns the next uniformly distributed 32-bit unsigned integer. + constexpr uint32_t next_u32() { return next_u64() >> 32; } + + /// @returns the next uniformly distributed non-negative 32-bit signed integer in [0, + /// INT32_MAX]. + constexpr int32_t next_i32() + { + int32_t ret; + uint32_t val; + val = next_u32(); + ret = int32_t(val & 0x7fffffff); + return ret; + } + + ///@returns the next uniformly distributed 64-bit unsigned integer. + constexpr uint64_t next_u64() { return mix64(next_state()); } + + /// @returns the next uniformly distributed non-negative 64-bit signed integer in [0, + /// INT64_MAX]. + constexpr int64_t next_i64() + { + int64_t ret; + uint64_t val; + val = next_u64(); + ret = int64_t(val & 0x7fffffffffffffff); + return ret; + } + + /// @returns a uniformly distributed float in [0, 1). + float next_float() { return (next_u32() >> 8) * 0x1.0p-24; } + + /// @returns a uniformly distributed double in [0, 1). + double next_double() { return (next_u64() >> 11) * 0x1.0p-53; } + + /// "Splits" the generator, creating a new instance of SplitMix64 in the process. + uint64_t generate_stream() + { + next_state(); + uint64_t new_stream = mix_stream(next_state()); + return new_stream; + } + + private: + uint64_t state_; ///< internal state + uint64_t stream_; ///< increment + + /// "Mixes" (i.e., scramble and blend) the bits of a number `z` + /// to generate a new random stream. + /// + /// @param[in] z any integer to use as the source + /// @returns the random stream to use in another instance of SplitMix64 + static uint64_t mix_stream(uint64_t z) + { + z = (z ^ (z >> 33)) * 0xFF51AFD7ED558CCDUL; + z = (z ^ (z >> 33)) * 0xC4CEB9FE1A85EC53UL; + z ^= (z >> 33); + z |= 1; + std::bitset<64> b(z ^ (z >> 1)); + return b.count() < 24 ? z ^ 0xAAAAAAAAAAAAAAAA : z; + } + + /// "Mixes" (i.e., scramble and blend) the bits of a number `z` + /// to generate the next pseudorandom number in the sequence. + /// + /// @param[in] z any integer to use as the source + /// @returns a pseudorandom number + static constexpr uint64_t mix64(uint64_t z) + { + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL; + return z ^ (z >> 31); + } + + /// Update the internal state and returns the old state. + constexpr uint64_t next_state() + { + uint64_t oldstate = state_; + state_ += stream_; + return oldstate; + } +}; + +} // namespace cuopt diff --git a/cpp/tests/mip/determinism_test.cu b/cpp/tests/mip/determinism_test.cu index 8f63152d09..c940342e0f 100644 --- a/cpp/tests/mip/determinism_test.cu +++ b/cpp/tests/mip/determinism_test.cu @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -203,11 +202,8 @@ TEST_P(DeterministicBBInstanceTest, deterministic_across_runs) settings.work_limit = work_limit; settings.seed = seed; - cuopt::seed_generator::set_seed(seed); auto solution1 = solve_mip(&handle_, problem, settings); - cuopt::seed_generator::set_seed(seed); auto solution2 = solve_mip(&handle_, problem, settings); - cuopt::seed_generator::set_seed(seed); auto solution3 = solve_mip(&handle_, problem, settings); EXPECT_EQ(solution1.get_termination_status(), solution2.get_termination_status());