From 381e79365f67970d231650afb822adbe36dc4fbe Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 25 Aug 2026 14:56:32 -0500 Subject: [PATCH 1/6] Replace process-wide seed_generator with per-worker RNGs in MIP heuristics cuopt::seed_generator was a single process-wide static counter shared across the OpenMP thread pool (B&B worker tasks, CPU/GPU FJ tasks, and local-search CPU-FJ climbers all run concurrently), so concurrent get_seed() calls raced and the order callers received values in was undefined -- deterministic-mode reproducibility was best-effort rather than guaranteed. Every mip_heuristics owner that used to draw from the global generator now holds its own persistent RNG, seeded once from mip_solver_context_t::base_seed (resolved from settings.seed, or a fresh random seed if unset) plus a fixed logical component id -- never a runtime thread id, since OMP tasks can migrate between OS threads. This follows the same pattern branch_and_bound_worker_t already uses. Context-less helpers (bounds_repair_t, lb_bounds_repair_t, solution_t::round_nearest/round_random_nearest/ assign_random_within_bounds, simple_rounding.cu's free functions) now take an explicit seed from their caller instead of reaching for global state. cpp/src/utilities/seed_generator.cuh is left in place since routing still depends on it (tracked separately by NVIDIA/cuopt#1717); one intentional fallback remains in fj_cpu.cu, reachable only from branch_and_bound.cpp's non-deterministic-mode root-cut path, which is out of scope here. Fixes NVIDIA/cuopt#1749 --- .../diversity/diversity_manager.cu | 16 +++-- .../mip_heuristics/diversity/population.cu | 3 +- .../recombiners/bound_prop_recombiner.cuh | 12 ++-- .../diversity/recombiners/fp_recombiner.cuh | 8 +-- .../recombiners/line_segment_recombiner.cuh | 8 ++- .../diversity/recombiners/recombiner.cuh | 12 +++- .../diversity/recombiners/sub_mip.cuh | 7 ++- .../feasibility_jump/early_cpufj.cu | 9 ++- .../feasibility_jump/early_cpufj.cuh | 6 +- .../feasibility_jump/early_gpufj.cu | 3 +- .../feasibility_jump/feasibility_jump.cu | 16 +++-- .../feasibility_jump/feasibility_jump.cuh | 10 +++- .../mip_heuristics/feasibility_jump/fj_cpu.cu | 17 +++--- .../feasibility_jump/fj_cpu.cuh | 1 + .../feasibility_pump/feasibility_pump.cu | 7 +-- .../line_segment_search.cu | 2 +- .../local_search/local_search.cu | 13 ++-- .../local_search/rounding/bounds_repair.cu | 6 +- .../local_search/rounding/bounds_repair.cuh | 4 +- .../local_search/rounding/constraint_prop.cu | 41 ++++++------- .../local_search/rounding/lb_bounds_repair.cu | 5 +- .../rounding/lb_bounds_repair.cuh | 2 +- .../rounding/lb_constraint_prop.cu | 13 ++-- .../local_search/rounding/simple_rounding.cu | 27 +++++---- .../local_search/rounding/simple_rounding.cuh | 6 +- cpp/src/mip_heuristics/mip_constants.hpp | 59 +++++++++++++++++++ cpp/src/mip_heuristics/solution/solution.cu | 14 ++--- cpp/src/mip_heuristics/solution/solution.cuh | 7 ++- cpp/src/mip_heuristics/solve.cu | 17 +++--- cpp/src/mip_heuristics/solver_context.cuh | 11 +++- cpp/tests/mip/determinism_test.cu | 4 -- 31 files changed, 239 insertions(+), 127 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index ec82c4b423..7419d38aad 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(static_cast(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(static_cast(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..dd249c9c1b 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_i64()] __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{(unsigned int)this->rng.next_i64()}; 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..b28cb762a2 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{(unsigned int)this->rng.next_i64()}; 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_i64()); } 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..f670b4f26d 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{(unsigned int)this->rng.next_i64()}; 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..dc394587c2 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,12 @@ 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(static_cast(context.base_seed) + cuopt::pcgenerator_t::default_seed + + static_cast(component_id), + cuopt::pcgenerator_t::default_stream ^ static_cast(component_id)), remaining_indices(n_integer_vars, handle_ptr->get_stream()), n_remaining(handle_ptr->get_stream()) { @@ -119,7 +124,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 +224,7 @@ class recombiner_t { } mip_solver_context_t& context; + cuopt::pcgenerator_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..366c4550b3 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{(unsigned int)this->rng.next_i64()}; 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_i64()); } 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..8aca5d67c3 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, + int64_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..a1432dfe0c 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, + int64_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. + int64_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..5379c465e4 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,13 @@ static constexpr int iterations_per_graph = 50; #endif template -fj_t::fj_t(mip_solver_context_t& context_, fj_settings_t in_settings) +fj_t::fj_t(mip_solver_context_t& context_, + fj_settings_t in_settings, + mip_rng_component_id_t seed_component_id) : context(context_), + rng(static_cast(context.base_seed) + cuopt::pcgenerator_t::default_seed + + static_cast(seed_component_id), + cuopt::pcgenerator_t::default_stream ^ static_cast(seed_component_id)), pb_ptr(context.problem_ptr), handle_ptr(const_cast(pb_ptr->handle_ptr)), settings(in_settings), @@ -135,12 +139,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 +676,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 +1132,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_i64()); } } diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh index 07c3be022f..9a078b80f7 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); @@ -250,6 +254,10 @@ class fj_t { public: mip_solver_context_t& context; + // 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. + cuopt::pcgenerator_t rng; problem_t* pb_ptr; raft::handle_t* handle_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..f196e6ff11 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, + int64_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, + int64_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, + int64_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..8df700226a 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, + int64_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..86b2c26dfd 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(static_cast(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(static_cast(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..e2f399424e 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.rng.next_i64()); } 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..a45bebccca 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(static_cast(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, static_cast(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(static_cast(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(static_cast(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(static_cast(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..5f524247ae 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_, + int64_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..3c141bfa70 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh +++ b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh @@ -111,7 +111,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, + int64_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..4564017f06 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,7 @@ 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), 0, 0) { } @@ -604,7 +605,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 +849,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 +864,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_i64()); 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 +880,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_i64()] __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 +937,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_i64()); 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..44dc873b3b 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, int64_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..730292d661 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 @@ -40,7 +40,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, int64_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..bf2a828ebf 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,7 @@ 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), 0, 0) { } @@ -765,7 +766,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 +781,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_i64()); cuopt_func_call(orig_sol.test_variable_bounds()); return orig_sol.compute_feasibility(); } @@ -933,7 +934,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_i64()); 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..14a4f82a32 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, int64_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, + int64_t seed) { + cuopt::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_i64(), 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_i64()); // 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_i64(), 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_i64()); 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, int64_t seed); \ + template void invoke_round_nearest(solution_t & solution, \ + int64_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..46e4f2b63b 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, int64_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, + int64_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..a2f7da0add 100644 --- a/cpp/src/mip_heuristics/mip_constants.hpp +++ b/cpp/src/mip_heuristics/mip_constants.hpp @@ -9,6 +9,11 @@ #include +#include + +#include +#include + #define MIP_INSTANTIATE_FLOAT CUOPT_INSTANTIATE_FLOAT #define MIP_INSTANTIATE_DOUBLE CUOPT_INSTANTIATE_DOUBLE @@ -39,3 +44,57 @@ #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 : uint32_t { + diversity_manager = 0, + 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 int64_t mip_resolve_base_seed(int64_t requested_seed) +{ + return requested_seed >= 0 ? requested_seed : static_cast(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 cuopt::pcgenerator_t directly. `index` further distinguishes +// multiple independent draws made by the same component (e.g. one per parallel worker slot). +inline int64_t mip_derive_seed(int64_t base_seed, + mip_rng_component_id_t component_id, + uint32_t index = 0) +{ + cuopt::pcgenerator_t gen( + static_cast(base_seed) + cuopt::pcgenerator_t::default_seed + + static_cast(component_id), + cuopt::pcgenerator_t::default_stream ^ (static_cast(component_id) + index)); + return gen.next_i64(); +} + +} // 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..ba16bc86ef 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(int64_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(int64_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, int64_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..81df4b9da6 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(int64_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(int64_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, int64_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..6a83917138 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 int64_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..6f49ef0e2e 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 int64_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/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()); From b6b62b9114122c1272a7d169de98c7577b372ec0 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 26 Aug 2026 16:22:50 -0500 Subject: [PATCH 2/6] Address CodeRabbit review: private fj_t::rng, direct cstdint includes fj_t::rng is now private with a next_seed() accessor, so external callers (line_segment_search_t) can't reseed or otherwise mutate it. bounds_repair.cuh/lb_bounds_repair.cuh get a direct include since they use int64_t unqualified. --- .../feasibility_jump/feasibility_jump.cu | 4 ++-- .../feasibility_jump/feasibility_jump.cuh | 14 +++++++++++--- .../line_segment_search/line_segment_search.cu | 2 +- .../local_search/rounding/bounds_repair.cuh | 2 ++ .../local_search/rounding/lb_bounds_repair.cuh | 2 ++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu index 5379c465e4..7b6afd8278 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu @@ -45,10 +45,10 @@ template fj_t::fj_t(mip_solver_context_t& context_, fj_settings_t in_settings, mip_rng_component_id_t seed_component_id) - : context(context_), - rng(static_cast(context.base_seed) + cuopt::pcgenerator_t::default_seed + + : rng(static_cast(context_.base_seed) + cuopt::pcgenerator_t::default_seed + static_cast(seed_component_id), cuopt::pcgenerator_t::default_stream ^ static_cast(seed_component_id)), + context(context_), pb_ptr(context.problem_ptr), handle_ptr(const_cast(pb_ptr->handle_ptr)), settings(in_settings), diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh index 9a078b80f7..752b052782 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh @@ -252,12 +252,20 @@ 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); - public: - mip_solver_context_t& context; + // 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. + int64_t next_seed() { return rng.next_i64(); } + + 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. + // thread id -- see mip_rng_component_id_t. Private: external callers must go through + // next_seed() rather than reseed or otherwise mutate this directly. cuopt::pcgenerator_t rng; + + public: + mip_solver_context_t& context; problem_t* pb_ptr; raft::handle_t* handle_ptr; 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 e2f399424e..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(fj.rng.next_i64()); + 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/rounding/bounds_repair.cuh b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh index 3c141bfa70..5ac906d143 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 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 730292d661..6a53aa98d7 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 From 829552831e17912feb3a3addfe6b5ca9a0c07b02 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 27 Aug 2026 21:47:09 +0200 Subject: [PATCH 3/6] added splitmix64 for generating seed/streams for the entire solver. Signed-off-by: Nicolas L. Guidotti --- .../recombiners/bound_prop_recombiner.cuh | 2 +- .../diversity/recombiners/recombiner.cuh | 5 +- .../feasibility_jump/feasibility_jump.cu | 5 +- .../local_search/rounding/bounds_repair.cu | 2 +- .../local_search/rounding/bounds_repair.cuh | 2 +- .../local_search/rounding/constraint_prop.cu | 4 +- .../local_search/rounding/lb_bounds_repair.cu | 2 +- .../rounding/lb_bounds_repair.cuh | 2 +- .../rounding/lb_constraint_prop.cu | 4 +- cpp/src/mip_heuristics/mip_constants.hpp | 37 +++-- cpp/src/mip_heuristics/solve.cu | 6 +- cpp/src/utilities/splitmix64.hpp | 129 ++++++++++++++++++ 12 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 cpp/src/utilities/splitmix64.hpp 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 dd249c9c1b..da01c14dec 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh @@ -152,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)this->rng.next_i64()}; + 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, diff --git a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh index dc394587c2..c46199839d 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh @@ -69,9 +69,8 @@ class recombiner_t { const raft::handle_t* handle_ptr, mip_rng_component_id_t component_id) : context(context_), - rng(static_cast(context.base_seed) + cuopt::pcgenerator_t::default_seed + - static_cast(component_id), - cuopt::pcgenerator_t::default_stream ^ static_cast(component_id)), + 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()) { diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu index 7b6afd8278..ba0856b62b 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu @@ -45,9 +45,8 @@ template fj_t::fj_t(mip_solver_context_t& context_, fj_settings_t in_settings, mip_rng_component_id_t seed_component_id) - : rng(static_cast(context_.base_seed) + cuopt::pcgenerator_t::default_seed + - static_cast(seed_component_id), - cuopt::pcgenerator_t::default_stream ^ static_cast(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)), 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 5f524247ae..2779a757e6 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu @@ -22,7 +22,7 @@ namespace cuopt::mathematical_optimization::mip { template bounds_repair_t::bounds_repair_t(const problem_t& pb, bound_presolve_t& bound_presolve_, - int64_t seed) + uint64_t seed) : bound_presolve(bound_presolve_), candidates(pb.handle_ptr), best_bounds(pb.handle_ptr), 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 5ac906d143..6cb07c13e2 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh +++ b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cuh @@ -115,7 +115,7 @@ class bounds_repair_t { public: bounds_repair_t(const problem_t& p, bound_presolve_t& bound_presolve, - int64_t seed); + 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 4564017f06..c2507a0238 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu @@ -42,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(mip_derive_seed(context.base_seed, mip_rng_component_id_t::constraint_prop, 0), 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) { } 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 44dc873b3b..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 @@ -18,7 +18,7 @@ namespace cuopt::mathematical_optimization::mip { template -lb_bounds_repair_t::lb_bounds_repair_t(const raft::handle_t* handle_ptr, int64_t seed) +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()), 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 6a53aa98d7..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 @@ -42,7 +42,7 @@ struct lb_bounds_t { template class lb_bounds_repair_t { public: - lb_bounds_repair_t(const raft::handle_t* handle_ptr, int64_t seed); + 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 bf2a828ebf..0c52b06cef 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 @@ -34,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(mip_derive_seed(context.base_seed, mip_rng_component_id_t::lb_constraint_prop, 0), 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) { } diff --git a/cpp/src/mip_heuristics/mip_constants.hpp b/cpp/src/mip_heuristics/mip_constants.hpp index a2f7da0add..e319987f9a 100644 --- a/cpp/src/mip_heuristics/mip_constants.hpp +++ b/cpp/src/mip_heuristics/mip_constants.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -53,8 +54,8 @@ namespace cuopt::mathematical_optimization::mip { // 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 : uint32_t { - diversity_manager = 0, +enum class mip_rng_component_id_t : uint64_t { + diversity_manager = 10000, population, local_search, feasibility_pump, @@ -84,17 +85,29 @@ inline int64_t mip_resolve_base_seed(int64_t requested_seed) // 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 cuopt::pcgenerator_t directly. `index` further distinguishes -// multiple independent draws made by the same component (e.g. one per parallel worker slot). -inline int64_t mip_derive_seed(int64_t base_seed, - mip_rng_component_id_t component_id, - uint32_t index = 0) +// 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) { - cuopt::pcgenerator_t gen( - static_cast(base_seed) + cuopt::pcgenerator_t::default_seed + - static_cast(component_id), - cuopt::pcgenerator_t::default_stream ^ (static_cast(component_id) + index)); - return gen.next_i64(); + 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/solve.cu b/cpp/src/mip_heuristics/solve.cu index 6a83917138..a611538d8f 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -295,7 +295,8 @@ mip_solution_t run_mip_solver( *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)); + static_cast(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)) { @@ -550,7 +551,8 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p op_problem, settings.get_tolerances(), early_fj_callback, - mip::mip_derive_seed(early_fj_base_seed, mip::mip_rng_component_id_t::early_cpufj)); + static_cast( + 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/utilities/splitmix64.hpp b/cpp/src/utilities/splitmix64.hpp new file mode 100644 index 0000000000..5b2cf74e26 --- /dev/null +++ b/cpp/src/utilities/splitmix64.hpp @@ -0,0 +1,129 @@ +/* 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) + { + } + + /// 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 From c160a8664cc125ddea77677a739b2ea90825e6c5 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Thu, 27 Aug 2026 21:47:49 +0200 Subject: [PATCH 4/6] added missing files Signed-off-by: Nicolas L. Guidotti --- cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh | 2 +- .../diversity/recombiners/line_segment_recombiner.cuh | 2 +- cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh index b28cb762a2..8b86a112c8 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh @@ -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)this->rng.next_i64()}; + 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, 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 f670b4f26d..615a3e5b4f 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh @@ -42,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)this->rng.next_i64()}; + 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/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index 366c4550b3..230c73d527 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -58,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)this->rng.next_i64()}; + 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, From 67b677a4db98e004e860f39226ce5869b3d7b367 Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 28 Aug 2026 09:32:27 +0200 Subject: [PATCH 5/6] fixed incorrect offset when creating the random seed/stream. use uint64_t instead of int64_t as seed. Signed-off-by: Nicolas L. Guidotti --- .../diversity/diversity_manager.cu | 4 ++-- .../recombiners/bound_prop_recombiner.cuh | 2 +- .../diversity/recombiners/fp_recombiner.cuh | 2 +- .../diversity/recombiners/recombiner.cuh | 4 ++-- .../diversity/recombiners/sub_mip.cuh | 2 +- .../feasibility_jump/early_cpufj.cu | 2 +- .../feasibility_jump/early_cpufj.cuh | 4 ++-- .../feasibility_jump/feasibility_jump.cu | 2 +- .../feasibility_jump/feasibility_jump.cuh | 4 ++-- .../mip_heuristics/feasibility_jump/fj_cpu.cu | 6 +++--- .../mip_heuristics/feasibility_jump/fj_cpu.cuh | 2 +- .../feasibility_pump/feasibility_pump.cu | 4 ++-- .../local_search/local_search.cu | 10 +++++----- .../local_search/rounding/constraint_prop.cu | 6 +++--- .../rounding/lb_constraint_prop.cu | 4 ++-- .../local_search/rounding/simple_rounding.cu | 18 +++++++++--------- .../local_search/rounding/simple_rounding.cuh | 4 ++-- cpp/src/mip_heuristics/mip_constants.hpp | 9 +++++---- cpp/src/mip_heuristics/solution/solution.cu | 6 +++--- cpp/src/mip_heuristics/solution/solution.cuh | 6 +++--- cpp/src/mip_heuristics/solve.cu | 10 ++++------ cpp/src/mip_heuristics/solver_context.cuh | 2 +- 22 files changed, 56 insertions(+), 57 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 7419d38aad..e9d1333cb3 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -445,7 +445,7 @@ template void diversity_manager_t::run_fj_alone(solution_t& solution) { CUOPT_LOG_INFO("Running FJ alone!"); - solution.round_nearest(static_cast(rng())); + 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; @@ -699,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(static_cast(rng())); + 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/recombiners/bound_prop_recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh index da01c14dec..1504be3d73 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh @@ -66,7 +66,7 @@ class bound_prop_recombiner_t : public recombiner_t { offspring_view, int_tol, probing_values = probing_values.data(), - seed = this->rng.next_i64()] __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), ""); diff --git a/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh index 8b86a112c8..b25bd6fcf5 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh @@ -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(this->rng.next_i64()); } + 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/recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh index c46199839d..ce7dfd94f2 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include #include @@ -223,7 +223,7 @@ class recombiner_t { } mip_solver_context_t& context; - cuopt::pcgenerator_t rng; + 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 230c73d527..24a56dbdd7 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -157,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(this->rng.next_i64()); + 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 8aca5d67c3..a6c95b3297 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cu @@ -16,7 +16,7 @@ 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, - int64_t seed) + uint64_t seed) : early_heuristic_t>( op_problem, tolerances, std::move(incumbent_callback)), seed_(seed) diff --git a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh index a1432dfe0c..61ef3ff51f 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/early_cpufj.cuh @@ -21,7 +21,7 @@ class early_cpufj_t : public early_heuristic_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, - int64_t seed); + uint64_t seed); ~early_cpufj_t(); @@ -35,7 +35,7 @@ class early_cpufj_t : public early_heuristic_t 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. - int64_t seed_; + uint64_t seed_; }; } // namespace cuopt::mathematical_optimization::mip diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu index ba0856b62b..ff776a8ed5 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu @@ -1131,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(rng.next_i64()); + 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 752b052782..93596ed307 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cuh @@ -255,14 +255,14 @@ class fj_t { // 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. - int64_t next_seed() { return rng.next_i64(); } + 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. - cuopt::pcgenerator_t rng; + splitmix64_t rng; public: mip_solver_context_t& context; diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index f196e6ff11..54f925a6f2 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -2073,7 +2073,7 @@ std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, - int64_t seed, + uint64_t seed, fj_settings_t settings) { raft::common::nvtx::range scope("init_fj_cpu_standalone"); @@ -2166,7 +2166,7 @@ template std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, - int64_t seed, + uint64_t seed, fj_settings_t settings); template void finalize_fj_cpu_host_initialization( fj_cpu_climber_t& fj_cpu, @@ -2187,7 +2187,7 @@ template std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, - int64_t seed, + 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 8df700226a..63749a57cd 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cuh @@ -229,7 +229,7 @@ std::unique_ptr> init_fj_cpu_standalone( problem_t& problem, solution_t& solution, std::atomic& preemption_flag, - int64_t seed, + 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 86b2c26dfd..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 @@ -273,7 +273,7 @@ template void feasibility_pump_t::perturbate(solution_t& solution) { constexpr f_t change_ratio = 0.1; - solution.assign_random_within_bounds(static_cast(rng()), change_ratio, true); + solution.assign_random_within_bounds(rng(), change_ratio, true); } template @@ -472,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(static_cast(rng())); + solution.round_nearest(rng()); raft::copy(last_rounding.data(), solution.assignment.data(), solution.assignment.size(), diff --git a/cpp/src/mip_heuristics/local_search/local_search.cu b/cpp/src/mip_heuristics/local_search/local_search.cu index a45bebccca..b1713b4d6d 100644 --- a/cpp/src/mip_heuristics/local_search/local_search.cu +++ b/cpp/src/mip_heuristics/local_search/local_search.cu @@ -64,7 +64,7 @@ void local_search_t::start_cpufj_scratch_threads(population_t 0) solution.assign_random_within_bounds(static_cast(rng()), 0.4); + if (counter > 0) solution.assign_random_within_bounds(rng(), 0.4); cpu_fj = fj.create_cpu_climber(solution, default_weights, default_weights, @@ -116,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, static_cast(rng())); + solution_lp.round_random_nearest(500, rng()); scratch_cpu_fj_on_lp_opt = fj.create_cpu_climber(solution_lp, default_weights, default_weights, @@ -503,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(static_cast(rng()), 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()); @@ -592,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(static_cast(rng())); + solution.round_nearest(rng()); CUOPT_LOG_DEBUG("Time limit reached during binary stage!"); return false; } @@ -619,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(static_cast(rng())); + solution.round_nearest(rng()); return false; } is_feasible = fp.restart_fp(solution); 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 c2507a0238..969406471b 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu @@ -866,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(this->rng.next_i64()); + 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(); @@ -885,7 +885,7 @@ bool constraint_prop_t::find_integer( 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_i64()] __device__(i_t var_idx) { + [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] = @@ -939,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(this->rng.next_i64()); + sol.round_nearest(this->rng.next_u64()); timeout_happened = true; break; } 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 0c52b06cef..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 @@ -783,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(this->rng.next_i64()); + orig_sol.round_nearest(this->rng.next_u64()); cuopt_func_call(orig_sol.test_variable_bounds()); return orig_sol.compute_feasibility(); } @@ -936,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(this->rng.next_i64()); + 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 14a4f82a32..a44872aba9 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu @@ -101,7 +101,7 @@ bool invoke_simple_rounding(solution_t& solution) } template -void invoke_round_nearest(solution_t& solution, int64_t seed) +void invoke_round_nearest(solution_t& solution, uint64_t seed) { i_t TPB = 128; bool brute_force_found_feas = check_brute_force_rounding(solution); @@ -119,9 +119,9 @@ void invoke_round_nearest(solution_t& solution, int64_t seed) template void invoke_random_round_nearest(solution_t& solution, i_t n_target_random_rounds, - int64_t seed) + uint64_t seed) { - cuopt::pcgenerator_t seed_rng(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(); @@ -130,7 +130,7 @@ void invoke_random_round_nearest(solution_t& solution, 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(), seed_rng.next_i64(), 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; @@ -138,7 +138,7 @@ void invoke_random_round_nearest(solution_t& solution, // 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(seed_rng.next_i64()); + 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(), @@ -146,7 +146,7 @@ void invoke_random_round_nearest(solution_t& solution, rng); random_rounding_kernel <<<1, 1, 0, solution.handle_ptr->get_stream()>>>(solution.view(), - seed_rng.next_i64(), + seed_rng.next_u64(), shuffled_indices.data(), n_randomly_rounded.data(), additional_roundings_needed); @@ -154,7 +154,7 @@ void invoke_random_round_nearest(solution_t& solution, CUOPT_LOG_TRACE("Randomly rounded integers, after adding close integers too %d", h_n_random_rounds); } - solution.round_nearest(seed_rng.next_i64()); + solution.round_nearest(seed_rng.next_u64()); RAFT_CHECK_CUDA(solution.handle_ptr->get_stream()); } @@ -176,9 +176,9 @@ 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, int64_t seed); \ + solution_t & solution, int n_target_random_rounds, uint64_t seed); \ template void invoke_round_nearest(solution_t & solution, \ - int64_t seed); \ + 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 46e4f2b63b..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,7 +12,7 @@ namespace cuopt::mathematical_optimization::mip { template -void invoke_round_nearest(solution_t& solution, int64_t seed); +void invoke_round_nearest(solution_t& solution, uint64_t seed); template bool invoke_simple_rounding(solution_t& solution); @@ -20,7 +20,7 @@ bool invoke_simple_rounding(solution_t& solution); template void invoke_random_round_nearest(solution_t& solution, i_t n_target_random_rounds, - int64_t seed); + 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 e319987f9a..2d96afd0c7 100644 --- a/cpp/src/mip_heuristics/mip_constants.hpp +++ b/cpp/src/mip_heuristics/mip_constants.hpp @@ -78,9 +78,10 @@ enum class mip_rng_component_id_t : uint64_t { // 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 int64_t mip_resolve_base_seed(int64_t requested_seed) +inline uint64_t mip_resolve_base_seed(int64_t requested_seed) { - return requested_seed >= 0 ? requested_seed : static_cast(std::random_device{}()); + 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 @@ -92,7 +93,7 @@ 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); + splitmix64_t seed_gen(base_seed + static_cast(component_id), index); return seed_gen.next_u64(); } @@ -106,7 +107,7 @@ 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); + splitmix64_t seed_gen(base_seed + static_cast(component_id), index); return seed_gen.generate_stream(); } diff --git a/cpp/src/mip_heuristics/solution/solution.cu b/cpp/src/mip_heuristics/solution/solution.cu index ba16bc86ef..64cd156747 100644 --- a/cpp/src/mip_heuristics/solution/solution.cu +++ b/cpp/src/mip_heuristics/solution/solution.cu @@ -225,7 +225,7 @@ void solution_t::copy_new_assignment(const rmm::device_uvector& d } template -void solution_t::assign_random_within_bounds(int64_t seed, +void solution_t::assign_random_within_bounds(uint64_t seed, f_t ratio_of_vars_to_random_assign, bool only_integers) { @@ -367,7 +367,7 @@ void solution_t::compute_infeasibility() } template -bool solution_t::round_nearest(int64_t seed) +bool solution_t::round_nearest(uint64_t seed) { clamp_within_bounds(); invoke_round_nearest(*this, seed); @@ -377,7 +377,7 @@ bool solution_t::round_nearest(int64_t seed) } template -bool solution_t::round_random_nearest(i_t n_target_random_rounds, int64_t seed) +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, seed); diff --git a/cpp/src/mip_heuristics/solution/solution.cuh b/cpp/src/mip_heuristics/solution/solution.cuh index 81df4b9da6..8c38cdf6ce 100644 --- a/cpp/src/mip_heuristics/solution/solution.cuh +++ b/cpp/src/mip_heuristics/solution/solution.cuh @@ -43,7 +43,7 @@ class solution_t { // returns the host assignment as a vector std::vector get_host_assignment(); // assigns random within bounds - void assign_random_within_bounds(int64_t seed, + 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 @@ -52,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(int64_t seed); + 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, int64_t seed); + 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 a611538d8f..ad608eeb49 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -295,8 +295,7 @@ mip_solution_t run_mip_solver( *problem.original_problem_ptr, settings.get_tolerances(), incumbent_callback, - static_cast(mip::mip_derive_seed(solver.context.base_seed, - mip::mip_rng_component_id_t::early_cpufj))); + 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)) { @@ -546,13 +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) - const int64_t early_fj_base_seed = mip::mip_resolve_base_seed(settings.seed); - early_cpufj = std::make_unique>( + 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, - static_cast( - mip::mip_derive_seed(early_fj_base_seed, mip::mip_rng_component_id_t::early_cpufj))); + 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 6f49ef0e2e..19a0f119b0 100644 --- a/cpp/src/mip_heuristics/solver_context.cuh +++ b/cpp/src/mip_heuristics/solver_context.cuh @@ -65,7 +65,7 @@ struct mip_solver_context_t { // 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 int64_t base_seed; + 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) From 4154f613968687881dd807864843db68ee37b11f Mon Sep 17 00:00:00 2001 From: "Nicolas L. Guidotti" Date: Fri, 28 Aug 2026 14:24:08 +0200 Subject: [PATCH 6/6] mix splitmix64 state once at the start so different streams produces a different number right from the start Signed-off-by: Nicolas L. Guidotti --- cpp/src/utilities/splitmix64.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/src/utilities/splitmix64.hpp b/cpp/src/utilities/splitmix64.hpp index 5b2cf74e26..8b6d34c301 100644 --- a/cpp/src/utilities/splitmix64.hpp +++ b/cpp/src/utilities/splitmix64.hpp @@ -23,6 +23,7 @@ class splitmix64_t { 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