diff --git a/cpp/include/cuopt/routing/solver_settings.hpp b/cpp/include/cuopt/routing/solver_settings.hpp index 3aae7ff0ef..b68e774375 100644 --- a/cpp/include/cuopt/routing/solver_settings.hpp +++ b/cpp/include/cuopt/routing/solver_settings.hpp @@ -64,12 +64,27 @@ class solver_settings_t { */ void dump_best_results(const std::string& file_path, i_t interval); + /** + * @brief Set the random seed used by the routing solver. + * + * Controls the initial seed for random number generation. Use -1 to derive the seed + * from the problem, which is the default and reproduces a given problem run to run. + * + * @param[in] seed The seed, or -1 to derive it from the problem + */ + void set_seed(i_t seed); + /** * @brief Return set solving time * @return Solving time set in seconds */ f_t get_time_limit() const noexcept; + /** + * @brief Return the random seed, or -1 if it is derived from the problem + */ + i_t get_seed() const noexcept; + /** * @brief Return true if verbose mode is enabled */ @@ -93,6 +108,7 @@ class solver_settings_t { i_t dump_interval_{std::numeric_limits::max()}; bool dump_best_results_{false}; std::string best_result_file_name_; + i_t seed_{-1}; }; } // namespace CUOPT_EXPORT routing diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index e8737cf6da..cbeeefbe8d 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -3,8 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # cmake-format: on -set(UTIL_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/seed_generator.cu - ${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp +set(UTIL_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/version_info.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/timestamp_utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/work_unit_scheduler.cpp) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 9d70ae17ee..85011e0c47 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -77,10 +77,13 @@ 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(context.problem_ptr->seed_gen.get_seed()), 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, context.problem_ptr->seed_gen.get_seed(), recombiner_alpha, "recombiner"), + mab_ls(mab_ls_config_t::n_of_arms, + context.problem_ptr->seed_gen.get_seed(), + ls_alpha, + "ls"), ls_hash_map(*context.problem_ptr) { int max_config = -1; diff --git a/cpp/src/mip_heuristics/diversity/population.cu b/cpp/src/mip_heuristics/diversity/population.cu index 553e5d6e93..122f85a76f 100644 --- a/cpp/src/mip_heuristics/diversity/population.cu +++ b/cpp/src/mip_heuristics/diversity/population.cu @@ -42,7 +42,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(context.problem_ptr->seed_gen.get_seed()), 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..e8f6631603 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/bound_prop_recombiner.cuh @@ -26,7 +26,7 @@ class bound_prop_recombiner_t : public recombiner_t { const raft::handle_t* handle_ptr) : recombiner_t(context, n_vars, handle_ptr), constraint_prop(constraint_prop_), - rng(cuopt::seed_generator::get_seed()), + rng(context.problem_ptr->seed_gen.get_seed()), vars_to_fix(n_vars, handle_ptr->get_stream()) { } @@ -65,7 +65,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->context.problem_ptr->seed_gen.get_seed()] __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 +151,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->context.problem_ptr->seed_gen.get_seed()}; 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/fp_recombiner.cuh b/cpp/src/mip_heuristics/diversity/recombiners/fp_recombiner.cuh index 85909c9d69..bc2c8183f1 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)cuopt::seed_generator::get_seed()}; + thrust::default_random_engine g{(unsigned int)this->context.problem_ptr->seed_gen.get_seed()}; 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 a1e6e29c56..25e85ad8e1 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/line_segment_recombiner.cuh @@ -40,7 +40,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->context.problem_ptr->seed_gen.get_seed()}; 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..649a9082ce 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/recombiner.cuh @@ -119,7 +119,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(context.problem_ptr->seed_gen.get_seed()); std::vector objective_indices_not_in_subproblem; std::set_difference(objective_indices.begin(), objective_indices.end(), diff --git a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh index 474becef25..d0e1c5917b 100644 --- a/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh +++ b/cpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuh @@ -57,7 +57,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->context.problem_ptr->seed_gen.get_seed()}; 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/feasibility_jump/feasibility_jump.cu b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu index 1853ecfcbd..ba56cf0c91 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu @@ -134,7 +134,7 @@ 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 rng(context.problem_ptr->seed_gen.get_seed()); constexpr f_t min_weight = 10.; constexpr f_t max_weight = 30.; // generate a range of weights between 10. and 30. @@ -671,7 +671,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 = context.problem_ptr->seed_gen.get_seed(); // ensure an updated copy of the settings is used device-side raft::copy(v.settings, &settings, 1, climber_stream); diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu index 57a6a89479..1dfc9a6a2d 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu.cu @@ -1492,7 +1492,7 @@ static std::unique_ptr> init_fj_cpu_from_host_lp( fj_settings.iteration_limit = std::numeric_limits::max(); fj_settings.update_weights = true; fj_settings.feasibility_run = false; - fj_settings.seed = seed >= 0 ? seed : cuopt::seed_generator::get_seed(); + fj_settings.seed = seed >= 0 ? seed : settings.random_seed; auto fj_cpu = std::make_unique>(preemption_flag); fj_cpu->view = typename fj_t::climber_data_t::view_t{}; @@ -1595,13 +1595,13 @@ std::unique_ptr> fj_t::create_cpu_climber( init_fj_cpu(*fj_cpu, solution, left_weights, right_weights, objective_weight); fj_cpu->settings = settings; if (randomize_params) { - auto rng = std::mt19937(cuopt::seed_generator::get_seed()); + auto rng = std::mt19937(solution.problem_ptr->seed_gen.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); } - fj_cpu->settings.seed = cuopt::seed_generator::get_seed(); + fj_cpu->settings.seed = solution.problem_ptr->seed_gen.get_seed(); return fj_cpu; // move } @@ -1786,7 +1786,7 @@ std::unique_ptr> init_fj_cpu_standalone( std::vector default_weights(problem.n_constraints, 1.0); init_fj_cpu(*fj_cpu, solution, default_weights, default_weights, 0.0); fj_cpu->settings = settings; - fj_cpu->settings.seed = cuopt::seed_generator::get_seed(); + fj_cpu->settings.seed = solution.problem_ptr->seed_gen.get_seed(); return fj_cpu; } diff --git a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh index ff6022c4c2..703d703cae 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh +++ b/cpp/src/mip_heuristics/feasibility_jump/fj_cpu_worker.cuh @@ -35,9 +35,9 @@ struct fj_cpu_worker_t { ~fj_cpu_worker_t() { stop(); } // `seed` selects the FJ RNG seed: pass a non-negative value for a deterministic seed, - // or -1 to draw from the global cuopt::seed_generator (the historical behavior). + // or -1 to fall back to the simplex settings' random_seed. // In deterministic mode the caller MUST pass an explicit seed, otherwise the underlying - // seed_generator::get_seed() racing with concurrent callers breaks reproducibility. + // shared seed source racing with concurrent callers breaks reproducibility. void create_worker(const simplex::lp_problem_t& problem, const std::vector& variable_types, const std::vector& seed_assignment, 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..323789f9e9 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 @@ -51,7 +51,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(context.problem_ptr->seed_gen.get_seed()), timer(20.) { } diff --git a/cpp/src/mip_heuristics/local_search/local_search.cu b/cpp/src/mip_heuristics/local_search/local_search.cu index 75c4185949..2dc174e2bb 100644 --- a/cpp/src/mip_heuristics/local_search/local_search.cu +++ b/cpp/src/mip_heuristics/local_search/local_search.cu @@ -39,7 +39,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(context.problem_ptr->seed_gen.get_seed()), problem_with_objective_cut(*context.problem_ptr, context.problem_ptr->handle_ptr) { const int n_cpufj = context.settings.heuristic_params.num_cpufj_threads; 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..4b6c3066a7 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/bounds_repair.cu @@ -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(pb.seed_gen.get_seed()), cycle_vector(MAX_CYCLE_SEQUENCE, -1) { } 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..497d6df338 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu @@ -41,7 +41,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(context.problem_ptr->seed_gen.get_seed(), 0, 0) { } @@ -604,7 +604,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(context.problem_ptr->seed_gen.get_seed()); 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 +848,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(context.problem_ptr->seed_gen.get_seed()); 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()); @@ -879,19 +879,20 @@ 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 = context.problem_ptr->seed_gen.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); + }); find_unset_integer_vars(sol, unset_integer_vars); } set_bounds_on_fixed_vars(sol); 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..27bc0fc935 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 @@ -19,7 +19,7 @@ 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 +27,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..063eb9b543 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,9 @@ struct lb_bounds_t { template class lb_bounds_repair_t { public: - lb_bounds_repair_t(const raft::handle_t* handle_ptr); + // The seed is supplied by the caller: this class has no route back to the problem + // that owns the seed source. + 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..9b36087d5d 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 @@ -25,7 +25,7 @@ lb_constraint_prop_t::lb_constraint_prop_t(mip_solver_context_thandle_ptr), + bounds_repair(context.problem_ptr->handle_ptr, context.problem_ptr->seed_gen.get_seed()), 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 +33,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(context.problem_ptr->seed_gen.get_seed(), 0, 0) { } @@ -765,7 +765,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(context.problem_ptr->seed_gen.get_seed()); 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()); 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 404185fe26..1f877ced7c 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/simple_rounding.cu @@ -112,7 +112,7 @@ void invoke_round_nearest(solution_t& solution) 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()); + solution.view(), solution.problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(solution.handle_ptr->get_stream()); } @@ -127,7 +127,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(0, solution.handle_ptr->get_stream()); random_nearest_rounding_kernel<<get_stream()>>>( - solution.view(), cuopt::seed_generator::get_seed(), n_randomly_rounded.data()); + solution.view(), solution.problem_ptr->seed_gen.get_seed(), 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; @@ -135,7 +135,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(solution.problem_ptr->seed_gen.get_seed()); // from the remaining integers, populate randomly. thrust::shuffle(solution.handle_ptr->get_thrust_policy(), shuffled_indices.begin(), @@ -143,7 +143,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(), + solution.problem_ptr->seed_gen.get_seed(), shuffled_indices.data(), n_randomly_rounded.data(), additional_roundings_needed); diff --git a/cpp/src/mip_heuristics/problem/problem.cuh b/cpp/src/mip_heuristics/problem/problem.cuh index bcc3f06fc2..7029c1a160 100644 --- a/cpp/src/mip_heuristics/problem/problem.cuh +++ b/cpp/src/mip_heuristics/problem/problem.cuh @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -327,6 +328,8 @@ class problem_t { /** name of the objective (only a single objective is currently allowed) */ std::string objective_name; f_t objective_offset; + // Seed source for this problem, seeded from the solver settings. + seed_generator_t seed_gen; bool is_scaled_{false}; bool preprocess_called{false}; bool objective_is_integral{false}; diff --git a/cpp/src/mip_heuristics/solution/solution.cu b/cpp/src/mip_heuristics/solution/solution.cu index 3b00fca7a8..834d054725 100644 --- a/cpp/src/mip_heuristics/solution/solution.cu +++ b/cpp/src/mip_heuristics/solution/solution.cu @@ -229,7 +229,7 @@ template void solution_t::assign_random_within_bounds(f_t ratio_of_vars_to_random_assign, bool only_integers) { - std::mt19937 rng(cuopt::seed_generator::get_seed()); + std::mt19937 rng(problem_ptr->seed_gen.get_seed()); auto stream = handle_ptr->get_stream(); std::vector h_assignment = host_copy(assignment, stream); std::uniform_real_distribution unif_prob(0, 1); diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index f55aca6878..e0e9872724 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -370,9 +370,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); @@ -434,6 +431,7 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p bool has_symmetry = false; if (settings.symmetry != 0) { mip::problem_t problem(op_problem); + if (settings.seed >= 0) { problem.seed_gen.set_seed(settings.seed); } simplex_solver_settings_t simplex_settings; simplex_settings.set_log(true); simplex_settings.time_limit = settings.time_limit; @@ -453,6 +451,7 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p std::optional> presolve_result_opt; mip::problem_t problem( op_problem, settings.get_tolerances(), settings.determinism_mode == CUOPT_MODE_DETERMINISTIC); + if (settings.seed >= 0) { problem.seed_gen.set_seed(settings.seed); } auto run_presolve = settings.presolver != presolver_t::None; bool has_set_solution_callback = false; @@ -601,6 +600,8 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p presolve_result_opt.emplace(std::move(result)); problem = mip::problem_t(presolve_result_opt->reduced_problem); + // The reduced problem is a fresh instance, so it carries a fresh seed source. + if (settings.seed >= 0) { problem.seed_gen.set_seed(settings.seed); } problem.set_papilo_presolve_data(presolver.get(), presolve_result_opt->reduced_to_original_map, presolve_result_opt->original_to_reduced_map, diff --git a/cpp/src/routing/adapters/adapted_generator.cu b/cpp/src/routing/adapters/adapted_generator.cu index 073be1ff1e..da4027add2 100644 --- a/cpp/src/routing/adapters/adapted_generator.cu +++ b/cpp/src/routing/adapters/adapted_generator.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -72,7 +72,7 @@ void generate_tsp_solution(adapted_sol_t& sol, for (i_t i = 0; i < (i_t)node_infos.size(); ++i) { node_infos[i] = sol.problem->get_node_info_of_node(i + sol.problem->order_info.depot_included_); } - std::mt19937 rng(seed_generator::get_seed()); + std::mt19937 rng(sol.problem->seed_gen.get_seed()); std::shuffle(node_infos.begin(), node_infos.end(), rng); std::vector>>> routes_to_add; routes_to_add.push_back({0, node_infos}); diff --git a/cpp/src/routing/adapters/adapted_modifier.cu b/cpp/src/routing/adapters/adapted_modifier.cu index b5f16ccbbd..a14fc65b4a 100644 --- a/cpp/src/routing/adapters/adapted_modifier.cu +++ b/cpp/src/routing/adapters/adapted_modifier.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -77,7 +77,7 @@ void adapted_modifier_t::add_unserviced_request( auto gpu_weight = get_cuopt_cost(final_weight); resource.ls.set_active_weights(gpu_weight, std::numeric_limits::max()); adapted_solution.sol.populate_ep_with_unserved(resource.ges.EP); - resource.ges.EP.random_shuffle(); + resource.ges.EP.random_shuffle(adapted_solution.sol.problem_ptr->seed_gen.get_seed()); resource.ges.squeeze_all_ep(); adapted_solution.populate_host_data(); adapted_solution.check_device_host_coherence(); @@ -101,7 +101,7 @@ void adapted_modifier_t::add_selected_unserviced_requests( auto gpu_weight = get_cuopt_cost(final_weight); resource.ls.set_active_weights(gpu_weight, std::numeric_limits::max()); adapted_solution.sol.populate_ep_with_selected_unserved(resource.ges.EP, unserviced_nodes); - resource.ges.EP.random_shuffle(); + resource.ges.EP.random_shuffle(adapted_solution.sol.problem_ptr->seed_gen.get_seed()); resource.ges.squeeze_all_ep(); adapted_solution.populate_host_data(); adapted_solution.check_device_host_coherence(); diff --git a/cpp/src/routing/diversity/diverse_solver.hpp b/cpp/src/routing/diversity/diverse_solver.hpp index ccbb2c989e..13e677419d 100644 --- a/cpp/src/routing/diversity/diverse_solver.hpp +++ b/cpp/src/routing/diversity/diverse_solver.hpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -245,7 +245,7 @@ struct solve { temp_pair(solution{p_, pool_allocator_.sol_handles[0].get()}, solution{p_, pool_allocator_.sol_handles[0].get()}), f(file_name), - rng(seed_generator::get_seed()), + rng(p->seed_gen.get_seed()), timer(timer_), improvement_timer(timer_), perturbation_count(0) diff --git a/cpp/src/routing/ges/eject_until_feasible.cu b/cpp/src/routing/ges/eject_until_feasible.cu index 6de2380870..5a05bde062 100644 --- a/cpp/src/routing/ges/eject_until_feasible.cu +++ b/cpp/src/routing/ges/eject_until_feasible.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -365,8 +365,8 @@ void solution_t::eject_until_feasible(bool add_slack_to_sol) bool is_set = set_shmem_of_kernel(eject_until_feasible_kernel, sh_size); cuopt_assert(is_set, "Not enough shared memory on device for get_all_feasible_insertion!"); cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); - eject_until_feasible_kernel - <<>>(view(), add_slack_to_sol, seed_generator::get_seed()); + eject_until_feasible_kernel<<>>( + view(), add_slack_to_sol, problem_ptr->seed_gen.get_seed()); compute_cost(); global_runtime_checks(false, true, "eject_until_feasible"); } @@ -385,7 +385,7 @@ void solution_t::populate_ep_with_unserved( EP.index_ = ep_index_out.value(stream); stream.synchronize(); if (EP.size() > 1) { - thrust::default_random_engine g(seed_generator::get_seed()); + thrust::default_random_engine g(problem_ptr->seed_gen.get_seed()); thrust::shuffle( sol_handle->get_thrust_policy(), EP.stack_.begin(), EP.stack_.begin() + EP.size(), g); } @@ -405,7 +405,7 @@ void solution_t::populate_ep_with_selected_unserved( raft::device_span(unserviced_device.data(), unserviced_device.size()); populate_ep_with_selected_unserved_kernel<<<1, TPB, 0, stream>>>( - view(), unserviced_view, EP.view(), ep_index_out.data(), seed_generator::get_seed()); + view(), unserviced_view, EP.view(), ep_index_out.data(), problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(stream); EP.index_ = ep_index_out.value(stream); stream.synchronize(); diff --git a/cpp/src/routing/ges/ejection_pool.cuh b/cpp/src/routing/ges/ejection_pool.cuh index 061b5ffb88..e08bc931a2 100644 --- a/cpp/src/routing/ges/ejection_pool.cuh +++ b/cpp/src/routing/ges/ejection_pool.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -56,13 +56,14 @@ struct ejection_pool_t { void push_back_last() { ++index_; } - void random_shuffle() + // The seed is supplied by the caller: the pool has no route back to the problem + // that owns the seed source. + void random_shuffle(int64_t seed) { // replace with thrust shuffle // how to get sol_handle::get_thrust_policy? if (size() > 1) - device_random_shuffle - <<<1, 1, 0, stream_>>>(stack_.data(), size(), seed_generator::get_seed()); + device_random_shuffle<<<1, 1, 0, stream_>>>(stack_.data(), size(), seed); } bool empty() const diff --git a/cpp/src/routing/ges/execute_insertion.cu b/cpp/src/routing/ges/execute_insertion.cu index dbcfc61250..ae43b4ac36 100644 --- a/cpp/src/routing/ges/execute_insertion.cu +++ b/cpp/src/routing/ges/execute_insertion.cu @@ -281,7 +281,7 @@ bool guided_ejection_search_t::execute_best_insertion_ejectio solution_ptr->get_num_orders(), solution_ptr->problem_ptr->get_max_break_dimensions(), solution_ptr->get_n_routes()); - int64_t seed = seed_generator::get_seed(); + int64_t seed = solution_ptr->problem_ptr->seed_gen.get_seed(); i_t* p_scores = p_scores_.data(); i_t fragment_size_arg = fragment_size; i_t fragment_step_arg = fragment_step; @@ -406,7 +406,7 @@ i_t guided_ejection_search_t::find_single_insertion( solution_ptr->get_num_orders(), solution_ptr->problem_ptr->get_max_break_dimensions(), solution_ptr->get_n_routes()), - seed_generator::get_seed()); + solution_ptr->problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(solution_ptr->sol_handle->get_stream()); diff --git a/cpp/src/routing/ges/guided_ejection_search.cu b/cpp/src/routing/ges/guided_ejection_search.cu index 442e7b2b67..1e88375a92 100644 --- a/cpp/src/routing/ges/guided_ejection_search.cu +++ b/cpp/src/routing/ges/guided_ejection_search.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -62,7 +62,7 @@ guided_ejection_search_t::guided_ejection_search_t( (solution.get_num_orders() + solution.problem_ptr->get_max_break_dimensions()), solution.sol_handle->get_stream()), feasible_candidates_size_(solution.sol_handle->get_stream()), - gen_candidate(seed_generator::get_seed()), + gen_candidate(solution.problem_ptr->seed_gen.get_seed()), p_scores_(solution.get_num_orders(), solution.sol_handle->get_stream()), inserted_requests(solution.get_num_orders(), solution.sol_handle->get_stream()), best_squeeze_per_cand(solution.get_num_requests(), solution.sol_handle->get_stream()), @@ -192,7 +192,7 @@ void guided_ejection_search_t::shuffle_pool() raft::common::nvtx::range fun_scope("shuffle_pool"); // include the ejected request in shuffle ++EP.index_; - EP.random_shuffle(); + EP.random_shuffle(solution_ptr->problem_ptr->seed_gen.get_seed()); --EP.index_; if (dump_intermediate) { dump_to_file("Shuffle"); } } @@ -439,7 +439,7 @@ bool guided_ejection_search_t::construct_feasible_solution() } solution_ptr->add_routes(new_routes); // permutate the EP for randomness - EP.random_shuffle(); + EP.random_shuffle(solution_ptr->problem_ptr->seed_gen.get_seed()); bool all_inserted = greedy_insert(); if (!all_inserted) { local_search_ptr_->perturb_solution(*solution_ptr); } diff --git a/cpp/src/routing/local_search/compute_insertions.cu b/cpp/src/routing/local_search/compute_insertions.cu index fa2da01aef..f0efa5dc7f 100644 --- a/cpp/src/routing/local_search/compute_insertions.cu +++ b/cpp/src/routing/local_search/compute_insertions.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -831,7 +831,7 @@ void find_insertions(solution_t& sol, cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); find_insertions_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); } else { // for cross the load-balance factor is always 4 move_candidates.number_of_blocks_per_ls_route = @@ -847,7 +847,7 @@ void find_insertions(solution_t& sol, cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); find_insertions_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); } else if (search_type == search_type_t::RANDOM) { // we don't search for relocates in random. n_blocks = sol.get_num_requests(); @@ -859,7 +859,7 @@ void find_insertions(solution_t& sol, cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); find_insertions_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); } } RAFT_CHECK_CUDA(sol.sol_handle->get_stream()); @@ -892,7 +892,7 @@ void find_unserviced_insertions(solution_t& sol, cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); find_insertions_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(sol.sol_handle->get_stream()); sol.sol_handle->sync_stream(); } diff --git a/cpp/src/routing/local_search/fill_gpu_graph.cu b/cpp/src/routing/local_search/fill_gpu_graph.cu index b0fb123824..b8fae427ff 100644 --- a/cpp/src/routing/local_search/fill_gpu_graph.cu +++ b/cpp/src/routing/local_search/fill_gpu_graph.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -159,7 +159,7 @@ void local_search_t::fill_gpu_graph(solution_tget_stream(); move_candidates.graph.special_index = solution.get_num_orders() + solution.n_routes; fill_intra_candidates<<>>( - solution.view(), move_candidates.view(), seed_generator::get_seed()); + solution.view(), move_candidates.view(), solution.problem_ptr->seed_gen.get_seed()); // +1 for special node i_t n_blocks = solution.get_num_requests() + 1; fill_graph_kernel diff --git a/cpp/src/routing/local_search/random_cross.cu b/cpp/src/routing/local_search/random_cross.cu index a54853513f..7d90c96eb6 100644 --- a/cpp/src/routing/local_search/random_cross.cu +++ b/cpp/src/routing/local_search/random_cross.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -204,7 +204,7 @@ void select_random_route_pairs(solution_t& sol, } select_random_route_pairs_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(sol.sol_handle->get_stream()); } @@ -217,7 +217,7 @@ void pick_random_move_per_route_pair(solution_t& sol, auto nblocks = (n_route_pair + nthreads - 1) / nthreads; pick_random_move_per_route_pair_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(sol.sol_handle->get_stream()); } diff --git a/cpp/src/routing/local_search/vrp/vrp_execute.cu b/cpp/src/routing/local_search/vrp/vrp_execute.cu index 5e417a9345..d65ec4fb36 100644 --- a/cpp/src/routing/local_search/vrp/vrp_execute.cu +++ b/cpp/src/routing/local_search/vrp/vrp_execute.cu @@ -394,7 +394,7 @@ i_t extract_non_overlapping_moves(solution_t& sol, cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); extract_non_overlapping_moves_kernel <<<1, TPB, sh_size, sol.sol_handle->get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); return move_candidates.vrp_move_candidates.n_of_selected_moves.value( sol.sol_handle->get_stream()); } diff --git a/cpp/src/routing/problem/problem.cu b/cpp/src/routing/problem/problem.cu index 4335b93734..fcfd8484c9 100644 --- a/cpp/src/routing/problem/problem.cu +++ b/cpp/src/routing/problem/problem.cu @@ -77,8 +77,14 @@ problem_t::problem_t(const data_model_view_t& data_model_vie initialize_incompatible(problem_ref); } - seed_generator::set_seed( - order_info.get_num_requests(), order_info.get_num_orders(), order_info.get_num_orders()); + // A user-supplied seed wins; otherwise derive one from the problem so that a given + // problem still reproduces run to run, which is the historical behaviour. + if (solver_settings_ptr != nullptr && solver_settings_ptr->get_seed() >= 0) { + seed_gen.set_seed(solver_settings_ptr->get_seed()); + } else { + seed_gen.set_seed( + order_info.get_num_requests(), order_info.get_num_orders(), order_info.get_num_orders()); + } } template diff --git a/cpp/src/routing/problem/problem.cuh b/cpp/src/routing/problem/problem.cuh index c2f00bf9f4..f963e76b4b 100644 --- a/cpp/src/routing/problem/problem.cuh +++ b/cpp/src/routing/problem/problem.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -267,6 +267,10 @@ class problem_t { const data_model_view_t* data_view_ptr; const solver_settings_t* solver_settings_ptr; + // Seed source for this problem. Seeded in the constructor from the solver settings, or + // derived from the problem when the user has not supplied one. + seed_generator_t seed_gen; + i_t get_num_orders() const; i_t get_num_requests() const; diff --git a/cpp/src/routing/solver_settings.cu b/cpp/src/routing/solver_settings.cu index 6267f39698..334a10638c 100644 --- a/cpp/src/routing/solver_settings.cu +++ b/cpp/src/routing/solver_settings.cu @@ -38,6 +38,12 @@ void solver_settings_t::dump_best_results(const std::string& file_path best_result_file_name_ = file_path; } +template +void solver_settings_t::set_seed(i_t seed) +{ + seed_ = seed; +} + template f_t solver_settings_t::get_time_limit() const noexcept { @@ -63,6 +69,12 @@ std::tuple solver_settings_t::get_dump_best_re return std::make_tuple(dump_interval_, dump_best_results_, best_result_file_name_); } +template +i_t solver_settings_t::get_seed() const noexcept +{ + return seed_; +} + template class CUOPT_EXPORT solver_settings_t; } // namespace routing } // namespace cuopt diff --git a/cpp/src/utilities/seed_generator.cu b/cpp/src/utilities/seed_generator.cu deleted file mode 100644 index 1da6662bc1..0000000000 --- a/cpp/src/utilities/seed_generator.cu +++ /dev/null @@ -1,10 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include - -int64_t cuopt::seed_generator::seed_ = 0; diff --git a/cpp/src/utilities/seed_generator.cuh b/cpp/src/utilities/seed_generator.cuh index dd5e79d847..8b218fe35b 100644 --- a/cpp/src/utilities/seed_generator.cuh +++ b/cpp/src/utilities/seed_generator.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -9,33 +9,79 @@ #include #include +#include +#include +#include + namespace cuopt { -// TODO: should be thread local? -class seed_generator { - static int64_t seed_; +namespace detail { + +// Folds several values into one seed using the Cantor pairing function. +// +// The arithmetic is done in uint64_t: routing folds `int` problem dimensions, and the +// product overflows a 32-bit int once two equal dimensions reach 181. Signed overflow is +// undefined behaviour, so widen first and let the unsigned type wrap deterministically. +template +inline int64_t fold_seed(seed_t seed) +{ + return static_cast(static_cast(seed)); +} + +template +inline int64_t fold_seed(arg0 seed0, arg1 seed1, args... seeds) +{ + const uint64_t a = static_cast(seed0); + const uint64_t b = static_cast(seed1); + const uint64_t sum = a + b; + return fold_seed(b + sum * (sum + 1) / 2, seeds...); +} + +} // namespace detail + +/** + * @brief Source of deterministic seeds, owned by the solver that uses it. + * + * Each solver holds its own generator, seeded from its own settings, so that two solvers + * running in the same process cannot overwrite each other's seed. + * + * @note Thread-safe. get_seed() hands out distinct values to concurrent callers, but the + * order in which they are handed out is not deterministic; reproducibility therefore + * still requires a deterministic call order. + */ +class seed_generator_t { + // Mutable so that a solver reachable only through a const pointer can still draw seeds; + // drawing a seed does not change the solver's logical state. + mutable std::atomic seed_{0}; public: - template - static void set_seed(seed_t seed) + seed_generator_t() = default; + explicit seed_generator_t(int64_t initial) : seed_(initial) {} + + // std::atomic is not copyable, so the counter value is transferred explicitly. Without + // these, declaring the copy operations deleted would also suppress the implicit move + // assignment of any class holding a generator (problem_t is move-assigned). + seed_generator_t(seed_generator_t const& other) + : seed_(other.seed_.load(std::memory_order_relaxed)) { -#ifdef BENCHMARK - seed_ = std::random_device{}(); -#else - seed_ = static_cast(seed); -#endif } - template - static void set_seed(arg0 seed0, arg1 seed1, args... seeds) + seed_generator_t& operator=(seed_generator_t const& other) { - set_seed(seed1 + ((seed0 + seed1) * (seed0 + seed1 + 1) / 2), seeds...); + seed_.store(other.seed_.load(std::memory_order_relaxed), std::memory_order_relaxed); + return *this; } - static int64_t get_seed() { return seed_++; } + template + void set_seed(args... seeds) + { +#ifdef BENCHMARK + seed_.store(std::random_device{}(), std::memory_order_relaxed); +#else + seed_.store(detail::fold_seed(seeds...), std::memory_order_relaxed); +#endif + } - public: - seed_generator(seed_generator const&) = delete; - void operator=(seed_generator const&) = delete; + int64_t get_seed() const { return seed_.fetch_add(1, std::memory_order_relaxed); } }; } // namespace cuopt diff --git a/cpp/tests/mip/determinism_test.cu b/cpp/tests/mip/determinism_test.cu index 8f63152d09..c940342e0f 100644 --- a/cpp/tests/mip/determinism_test.cu +++ b/cpp/tests/mip/determinism_test.cu @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -203,11 +202,8 @@ TEST_P(DeterministicBBInstanceTest, deterministic_across_runs) settings.work_limit = work_limit; settings.seed = seed; - cuopt::seed_generator::set_seed(seed); auto solution1 = solve_mip(&handle_, problem, settings); - cuopt::seed_generator::set_seed(seed); auto solution2 = solve_mip(&handle_, problem, settings); - cuopt::seed_generator::set_seed(seed); auto solution3 = solve_mip(&handle_, problem, settings); EXPECT_EQ(solution1.get_termination_status(), solution2.get_termination_status());