From 3e174501f2fa1b1ae5a14280323657350f198c74 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 13 Aug 2026 10:31:27 -0500 Subject: [PATCH 1/7] fix: give each cuOpt library its own seed counter seed_generator::seed_ was a single process-wide counter defined in seed_generator.cu. The two solvers seed it from unrelated inputs: routing/problem/problem.cu:80 set_seed(num_requests, num_orders, num_orders) mip_heuristics/solve.cu:374 if (settings.seed >= 0) set_seed(settings.seed) Routing derives its seed from the problem geometry, mathematical optimization takes it from the user's solver settings. Sharing one counter means whichever solver runs last overwrites the other's seed, so solving a VRP and then a MIP in the same process silently discards the user's settings.seed. Define the counter inline instead, so each library that links the header keeps its own, matching how the seed is actually supplied. Making it std::atomic also resolves the "should be thread local?" TODO: get_seed() was a plain seed_++, which is a data race across concurrent solves. The atomic hands out distinct values, though the order is still not deterministic under concurrency, so reproducibility continues to require a deterministic call order. seed_generator.cu existed only to define the member and is removed. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- cpp/src/CMakeLists.txt | 3 +-- cpp/src/utilities/seed_generator.cu | 10 ---------- cpp/src/utilities/seed_generator.cuh | 28 ++++++++++++++++++++++------ 3 files changed, 23 insertions(+), 18 deletions(-) delete mode 100644 cpp/src/utilities/seed_generator.cu 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/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..f3e5008e0a 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,20 +9,36 @@ #include #include +#include +#include +#include + namespace cuopt { -// TODO: should be thread local? +/** + * @brief Source of deterministic seeds for a single cuOpt solver library. + * + * The counter is defined inline, so each library that links this header keeps its own. + * That matches how the seed is actually supplied: routing derives it from the problem + * geometry while mathematical optimization takes it from the user's solver settings. + * Those are independent inputs, and a single shared counter meant whichever solver ran + * last silently overwrote the 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 across + * runs therefore still requires a deterministic call order. + */ class seed_generator { - static int64_t seed_; + static inline std::atomic seed_{0}; public: template static void set_seed(seed_t seed) { #ifdef BENCHMARK - seed_ = std::random_device{}(); + seed_.store(std::random_device{}(), std::memory_order_relaxed); #else - seed_ = static_cast(seed); + seed_.store(static_cast(seed), std::memory_order_relaxed); #endif } template @@ -31,7 +47,7 @@ class seed_generator { set_seed(seed1 + ((seed0 + seed1) * (seed0 + seed1 + 1) / 2), seeds...); } - static int64_t get_seed() { return seed_++; } + static int64_t get_seed() { return seed_.fetch_add(1, std::memory_order_relaxed); } public: seed_generator(seed_generator const&) = delete; From 88d27281276babc057ed0341ae0dfb3498f6d4c9 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 13 Aug 2026 15:12:40 -0500 Subject: [PATCH 2/7] feat: give routing a seed setting and per-problem seed source Adds set_seed/get_seed to routing's solver_settings_t, following the mip_solver_settings_t convention where -1 means "derive it", so existing behaviour is unchanged when the user does not set one. Routing previously had no seed control at all, despite being the component that overwrote the shared counter from problem geometry. Introduces seed_generator_t, an instance held by routing's problem_t and seeded in its constructor. The counter is a mutable atomic so get_seed() can be const: solution_t reaches the problem through a const pointer, and drawing a seed does not change the problem's logical state, so this avoids threading constness changes through the call graph. All 13 routing call sites now draw from the owning problem. ejection_pool_t has no route back to a problem, so random_shuffle() takes the seed as an argument instead; all four callers pass it. The process-wide seed_generator remains for now because the MIP heuristics still use it. It is removed once those call sites migrate. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- cpp/include/cuopt/routing/solver_settings.hpp | 16 +++++ cpp/src/routing/adapters/adapted_generator.cu | 2 +- cpp/src/routing/adapters/adapted_modifier.cu | 4 +- cpp/src/routing/diversity/diverse_solver.hpp | 2 +- cpp/src/routing/ges/eject_until_feasible.cu | 6 +- cpp/src/routing/ges/ejection_pool.cuh | 6 +- cpp/src/routing/ges/execute_insertion.cu | 4 +- cpp/src/routing/ges/guided_ejection_search.cu | 6 +- .../local_search/compute_insertions.cu | 8 +-- .../routing/local_search/fill_gpu_graph.cu | 2 +- cpp/src/routing/local_search/random_cross.cu | 4 +- .../routing/local_search/vrp/vrp_execute.cu | 2 +- cpp/src/routing/problem/problem.cu | 13 +++- cpp/src/routing/problem/problem.cuh | 4 ++ cpp/src/routing/solver_settings.cu | 12 ++++ cpp/src/utilities/seed_generator.cuh | 72 ++++++++++++++----- 16 files changed, 123 insertions(+), 40 deletions(-) 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/routing/adapters/adapted_generator.cu b/cpp/src/routing/adapters/adapted_generator.cu index 073be1ff1e..d702b3772c 100644 --- a/cpp/src/routing/adapters/adapted_generator.cu +++ b/cpp/src/routing/adapters/adapted_generator.cu @@ -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..90e6b63b4c 100644 --- a/cpp/src/routing/adapters/adapted_modifier.cu +++ b/cpp/src/routing/adapters/adapted_modifier.cu @@ -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..6930f69e65 100644 --- a/cpp/src/routing/diversity/diverse_solver.hpp +++ b/cpp/src/routing/diversity/diverse_solver.hpp @@ -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..3cf617482e 100644 --- a/cpp/src/routing/ges/eject_until_feasible.cu +++ b/cpp/src/routing/ges/eject_until_feasible.cu @@ -366,7 +366,7 @@ void solution_t::eject_until_feasible(bool add_slack_to_sol) 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()); + <<>>(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..a610f17af1 100644 --- a/cpp/src/routing/ges/ejection_pool.cuh +++ b/cpp/src/routing/ges/ejection_pool.cuh @@ -56,13 +56,15 @@ 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()); + <<<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..4536eeff73 100644 --- a/cpp/src/routing/ges/guided_ejection_search.cu +++ b/cpp/src/routing/ges/guided_ejection_search.cu @@ -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..3bea6ef6a3 100644 --- a/cpp/src/routing/local_search/compute_insertions.cu +++ b/cpp/src/routing/local_search/compute_insertions.cu @@ -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..5e535bcd3c 100644 --- a/cpp/src/routing/local_search/fill_gpu_graph.cu +++ b/cpp/src/routing/local_search/fill_gpu_graph.cu @@ -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..63998c389f 100644 --- a/cpp/src/routing/local_search/random_cross.cu +++ b/cpp/src/routing/local_search/random_cross.cu @@ -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..904145423a 100644 --- a/cpp/src/routing/problem/problem.cu +++ b/cpp/src/routing/problem/problem.cu @@ -77,8 +77,17 @@ 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()); + seed_generator::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()); + seed_generator::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..5598942db5 100644 --- a/cpp/src/routing/problem/problem.cuh +++ b/cpp/src/routing/problem/problem.cuh @@ -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.cuh b/cpp/src/utilities/seed_generator.cuh index f3e5008e0a..fd2a4af097 100644 --- a/cpp/src/utilities/seed_generator.cuh +++ b/cpp/src/utilities/seed_generator.cuh @@ -15,36 +15,76 @@ namespace cuopt { +namespace detail { + +// Folds several values into one seed. Shared by the instance and the legacy static API. +template +inline int64_t fold_seed(seed_t seed) +{ + return static_cast(seed); +} + +template +inline int64_t fold_seed(arg0 seed0, arg1 seed1, args... seeds) +{ + return fold_seed(seed1 + ((seed0 + seed1) * (seed0 + seed1 + 1) / 2), seeds...); +} + +} // namespace detail + /** - * @brief Source of deterministic seeds for a single cuOpt solver library. + * @brief Source of deterministic seeds, owned by the solver that uses it. * - * The counter is defined inline, so each library that links this header keeps its own. - * That matches how the seed is actually supplied: routing derives it from the problem - * geometry while mathematical optimization takes it from the user's solver settings. - * Those are independent inputs, and a single shared counter meant whichever solver ran - * last silently overwrote the other's seed. + * 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 across - * runs therefore still requires a deterministic call order. + * order in which they are handed out is not deterministic; reproducibility therefore + * still requires a deterministic call order. */ -class seed_generator { - static inline std::atomic seed_{0}; +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) {} + + seed_generator_t(seed_generator_t const&) = delete; + void operator=(seed_generator_t const&) = delete; + + template + void set_seed(args... seeds) { #ifdef BENCHMARK seed_.store(std::random_device{}(), std::memory_order_relaxed); #else - seed_.store(static_cast(seed), std::memory_order_relaxed); + seed_.store(detail::fold_seed(seeds...), std::memory_order_relaxed); #endif } - template - static void set_seed(arg0 seed0, arg1 seed1, args... seeds) + + int64_t get_seed() const { return seed_.fetch_add(1, std::memory_order_relaxed); } +}; + +/** + * @brief Legacy process-wide seed source. + * + * @deprecated Being replaced by seed_generator_t owned by each solver. Call sites are + * migrating; do not add new uses. + */ +class seed_generator { + static inline std::atomic seed_{0}; + + public: + template + static void set_seed(args... seeds) { - set_seed(seed1 + ((seed0 + seed1) * (seed0 + seed1 + 1) / 2), seeds...); +#ifdef BENCHMARK + seed_.store(std::random_device{}(), std::memory_order_relaxed); +#else + seed_.store(detail::fold_seed(seeds...), std::memory_order_relaxed); +#endif } static int64_t get_seed() { return seed_.fetch_add(1, std::memory_order_relaxed); } From 199b616d7e4090f904e20524ea9e5f9fc95a986a Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 14 Aug 2026 10:51:20 -0500 Subject: [PATCH 3/7] feat: move MIP heuristics onto a per-problem seed and drop the global Adds a seed_generator_t to mip::problem_t, seeded from settings.seed where the process-wide generator was seeded before, and moves the 32 MIP call sites onto it. With routing already migrated, nothing references the global and it is removed. Two call sites cannot reach a problem and take the seed explicitly rather than reintroducing a global: ejection_pool_t::random_shuffle() already gained a seed parameter with the routing change, and the feasibility jump host-LP path falls back to the simplex settings' random_seed, which it already receives. determinism_test.cu called seed_generator::set_seed() before each of three solves even though it already set settings.seed; that was working around the global persisting across solves. Those three lines are gone and the test now relies on settings.seed alone. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- .../diversity/diversity_manager.cu | 6 +-- .../mip_heuristics/diversity/population.cu | 2 +- .../recombiners/bound_prop_recombiner.cuh | 6 +-- .../diversity/recombiners/fp_recombiner.cuh | 2 +- .../recombiners/line_segment_recombiner.cuh | 2 +- .../diversity/recombiners/recombiner.cuh | 2 +- .../diversity/recombiners/sub_mip.cuh | 2 +- .../feasibility_jump/feasibility_jump.cu | 4 +- .../mip_heuristics/feasibility_jump/fj_cpu.cu | 8 ++-- .../feasibility_jump/fj_cpu_worker.cuh | 4 +- .../feasibility_pump/feasibility_pump.cu | 2 +- .../local_search/local_search.cu | 2 +- .../local_search/rounding/bounds_repair.cu | 2 +- .../local_search/rounding/constraint_prop.cu | 8 ++-- .../local_search/rounding/lb_bounds_repair.cu | 2 +- .../rounding/lb_constraint_prop.cu | 4 +- .../local_search/rounding/simple_rounding.cu | 8 ++-- cpp/src/mip_heuristics/problem/problem.cuh | 3 ++ cpp/src/mip_heuristics/solution/solution.cu | 2 +- cpp/src/mip_heuristics/solve.cu | 4 +- cpp/src/routing/adapters/adapted_generator.cu | 2 +- cpp/src/routing/adapters/adapted_modifier.cu | 2 +- cpp/src/routing/diversity/diverse_solver.hpp | 2 +- cpp/src/routing/ges/eject_until_feasible.cu | 6 +-- cpp/src/routing/ges/ejection_pool.cuh | 5 +-- cpp/src/routing/ges/guided_ejection_search.cu | 2 +- .../local_search/compute_insertions.cu | 2 +- .../routing/local_search/fill_gpu_graph.cu | 2 +- cpp/src/routing/local_search/random_cross.cu | 2 +- cpp/src/routing/problem/problem.cu | 3 -- cpp/src/routing/problem/problem.cuh | 2 +- cpp/src/utilities/seed_generator.cuh | 41 ++++++------------- cpp/tests/mip/determinism_test.cu | 4 -- 33 files changed, 64 insertions(+), 86 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 9d70ae17ee..945b36ed61 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -77,10 +77,10 @@ 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..2f9a2deea1 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()); @@ -883,7 +883,7 @@ bool constraint_prop_t::find_integer( 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) { + [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], 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..c8f2a71e2c 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 @@ -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(problem.seed_gen.get_seed()), cycle_vector(MAX_CYCLE_SEQUENCE, -1) { } 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..15f94fcf27 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 @@ -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..977485097a 100644 --- a/cpp/src/mip_heuristics/problem/problem.cuh +++ b/cpp/src/mip_heuristics/problem/problem.cuh @@ -14,6 +14,7 @@ #include "presolve_data.cuh" #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..943d8ec641 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -370,8 +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 +432,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 +452,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; diff --git a/cpp/src/routing/adapters/adapted_generator.cu b/cpp/src/routing/adapters/adapted_generator.cu index d702b3772c..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 */ diff --git a/cpp/src/routing/adapters/adapted_modifier.cu b/cpp/src/routing/adapters/adapted_modifier.cu index 90e6b63b4c..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 */ diff --git a/cpp/src/routing/diversity/diverse_solver.hpp b/cpp/src/routing/diversity/diverse_solver.hpp index 6930f69e65..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 */ diff --git a/cpp/src/routing/ges/eject_until_feasible.cu b/cpp/src/routing/ges/eject_until_feasible.cu index 3cf617482e..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, problem_ptr->seed_gen.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"); } diff --git a/cpp/src/routing/ges/ejection_pool.cuh b/cpp/src/routing/ges/ejection_pool.cuh index a610f17af1..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 */ @@ -63,8 +63,7 @@ struct ejection_pool_t { // 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); + device_random_shuffle<<<1, 1, 0, stream_>>>(stack_.data(), size(), seed); } bool empty() const diff --git a/cpp/src/routing/ges/guided_ejection_search.cu b/cpp/src/routing/ges/guided_ejection_search.cu index 4536eeff73..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 */ diff --git a/cpp/src/routing/local_search/compute_insertions.cu b/cpp/src/routing/local_search/compute_insertions.cu index 3bea6ef6a3..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 */ diff --git a/cpp/src/routing/local_search/fill_gpu_graph.cu b/cpp/src/routing/local_search/fill_gpu_graph.cu index 5e535bcd3c..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 */ diff --git a/cpp/src/routing/local_search/random_cross.cu b/cpp/src/routing/local_search/random_cross.cu index 63998c389f..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 */ diff --git a/cpp/src/routing/problem/problem.cu b/cpp/src/routing/problem/problem.cu index 904145423a..fcfd8484c9 100644 --- a/cpp/src/routing/problem/problem.cu +++ b/cpp/src/routing/problem/problem.cu @@ -81,12 +81,9 @@ problem_t::problem_t(const data_model_view_t& data_model_vie // 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()); - seed_generator::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()); - seed_generator::set_seed( - order_info.get_num_requests(), order_info.get_num_orders(), order_info.get_num_orders()); } } diff --git a/cpp/src/routing/problem/problem.cuh b/cpp/src/routing/problem/problem.cuh index 5598942db5..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 */ diff --git a/cpp/src/utilities/seed_generator.cuh b/cpp/src/utilities/seed_generator.cuh index fd2a4af097..9170e9dfa9 100644 --- a/cpp/src/utilities/seed_generator.cuh +++ b/cpp/src/utilities/seed_generator.cuh @@ -51,34 +51,21 @@ class seed_generator_t { seed_generator_t() = default; explicit seed_generator_t(int64_t initial) : seed_(initial) {} - seed_generator_t(seed_generator_t const&) = delete; - void operator=(seed_generator_t const&) = delete; - - template - void set_seed(args... seeds) + // 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_.store(std::random_device{}(), std::memory_order_relaxed); -#else - seed_.store(detail::fold_seed(seeds...), std::memory_order_relaxed); -#endif + } + seed_generator_t& operator=(seed_generator_t const& other) + { + seed_.store(other.seed_.load(std::memory_order_relaxed), std::memory_order_relaxed); + return *this; } - int64_t get_seed() const { return seed_.fetch_add(1, std::memory_order_relaxed); } -}; - -/** - * @brief Legacy process-wide seed source. - * - * @deprecated Being replaced by seed_generator_t owned by each solver. Call sites are - * migrating; do not add new uses. - */ -class seed_generator { - static inline std::atomic seed_{0}; - - public: template - static void set_seed(args... seeds) + void set_seed(args... seeds) { #ifdef BENCHMARK seed_.store(std::random_device{}(), std::memory_order_relaxed); @@ -87,11 +74,7 @@ class seed_generator { #endif } - static int64_t get_seed() { return seed_.fetch_add(1, std::memory_order_relaxed); } - - 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()); From cc55e0093bcd861d3c1187094533e79be398bf08 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 14 Aug 2026 11:36:58 -0500 Subject: [PATCH 4/7] style: apply clang-format Signed-off-by: Ramakrishna Prabhu --- .../diversity/diversity_manager.cu | 5 +++- .../local_search/rounding/constraint_prop.cu | 27 ++++++++++--------- cpp/src/mip_heuristics/problem/problem.cuh | 2 +- cpp/src/mip_heuristics/solve.cu | 5 ++-- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index 945b36ed61..85011e0c47 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -80,7 +80,10 @@ diversity_manager_t::diversity_manager_t(mip_solver_context_tseed_gen.get_seed()), stats(context.stats), 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"), + 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/local_search/rounding/constraint_prop.cu b/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu index 2f9a2deea1..497d6df338 100644 --- a/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu +++ b/cpp/src/mip_heuristics/local_search/rounding/constraint_prop.cu @@ -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 = 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); - }); + 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/problem/problem.cuh b/cpp/src/mip_heuristics/problem/problem.cuh index 977485097a..7029c1a160 100644 --- a/cpp/src/mip_heuristics/problem/problem.cuh +++ b/cpp/src/mip_heuristics/problem/problem.cuh @@ -14,8 +14,8 @@ #include "presolve_data.cuh" #include -#include #include +#include #include #include diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 943d8ec641..f49954cfe7 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -370,7 +370,6 @@ mip_solution_t solve_mip_helper(optimization_problem_t& op_p print_version_info(); - raft::common::nvtx::range fun_scope("Running solver"); auto timer = timer_t(time_limit); @@ -432,7 +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); } + 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; @@ -452,7 +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); } + 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; From 4e93be420b0ff64050592776982cf0ccfc6c8418 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 14 Aug 2026 12:53:47 -0500 Subject: [PATCH 5/7] fix: address review on seed ownership Reapply the configured seed after presolve replaces the problem. solve.cu seeded the generator, then assigned a fresh problem_t built from the reduced problem, which carries a default-constructed generator, so any settings.seed was silently discarded whenever presolve ran. Widen the multi-value seed fold to uint64_t. The pairing arithmetic was evaluated in the input type, and routing folds int problem dimensions, so the product overflowed a 32-bit int once two equal dimensions reached 181. Signed overflow is undefined behaviour; the unsigned type wraps deterministically. Pass the seed into lb_bounds_repair_t's constructor, which has no route back to a problem. That file and lb_constraint_prop.cu are in no source list and are never compiled, so the invalid reference survived a clean build. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- .../local_search/rounding/lb_bounds_repair.cu | 4 ++-- .../local_search/rounding/lb_bounds_repair.cuh | 4 +++- .../local_search/rounding/lb_constraint_prop.cu | 2 +- cpp/src/mip_heuristics/solve.cu | 2 ++ cpp/src/utilities/seed_generator.cuh | 13 ++++++++++--- 5 files changed, 18 insertions(+), 7 deletions(-) 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 c8f2a71e2c..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(problem.seed_gen.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 15f94fcf27..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()), diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index f49954cfe7..e0e9872724 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -600,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/utilities/seed_generator.cuh b/cpp/src/utilities/seed_generator.cuh index 9170e9dfa9..8b218fe35b 100644 --- a/cpp/src/utilities/seed_generator.cuh +++ b/cpp/src/utilities/seed_generator.cuh @@ -17,17 +17,24 @@ namespace cuopt { namespace detail { -// Folds several values into one seed. Shared by the instance and the legacy static API. +// 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(seed); + return static_cast(static_cast(seed)); } template inline int64_t fold_seed(arg0 seed0, arg1 seed1, args... seeds) { - return fold_seed(seed1 + ((seed0 + seed1) * (seed0 + seed1 + 1) / 2), 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 From dde911af4c7770c5d612af6def13afa8bc4a6bfa Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 18 Aug 2026 14:02:59 -0500 Subject: [PATCH 6/7] fix: hand out seeds from a thread-local counter Adopts the mechanism from Alice's earlier determinism work (3e214a86): each thread keeps its own counter, rebased from the owning solver's base seed whenever that base changes. A thread therefore walks the same sequence from a given base regardless of how workers interleave, so which seed a work item receives no longer depends on scheduling. The previous atomic counter made concurrent access safe but left the order nondeterministic, which is the part that matters for reproducibility across synchronisation points. The base stays per solver, so this composes with rather than replaces the per-problem ownership: the base fixes routing and MIP overwriting each other's seed, the thread-local counter fixes ordering. Also simplifies the class. base_seed_ is a plain int64_t, so the atomic, the mutable, and the hand-written copy and assignment operators are all gone, and problem_t gets its implicit copy and move back. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- cpp/src/utilities/seed_generator.cuh | 59 +++++++++++++++++----------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/cpp/src/utilities/seed_generator.cuh b/cpp/src/utilities/seed_generator.cuh index 8b218fe35b..285304e392 100644 --- a/cpp/src/utilities/seed_generator.cuh +++ b/cpp/src/utilities/seed_generator.cuh @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -45,43 +44,57 @@ inline int64_t fold_seed(arg0 seed0, arg1 seed1, args... seeds) * 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. + * The counter that hands out seeds is thread-local and is rebased whenever the owning + * solver's base seed changes. Each thread therefore walks its own deterministic sequence + * from that base, so the order in which concurrent workers happen to ask for seeds does + * not change which seed any of them receives. A shared counter would hand out values in a + * nondeterministic order and break reproducibility across synchronisation points. + * + * Two solvers configured with the *same* base seed and used from one thread continue a + * single sequence rather than restarting, since the rebase is triggered by a change of + * base. */ 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}; + int64_t base_seed_{0}; - public: - seed_generator_t() = default; - explicit seed_generator_t(int64_t initial) : seed_(initial) {} + struct thread_state_t { + int64_t counter{0}; + int64_t last_base{0}; + bool initialized{false}; + }; - // 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)) - { - } - seed_generator_t& operator=(seed_generator_t const& other) + // Shared by every generator on this thread; the base check rebases when the caller + // switches to a solver seeded differently. + static thread_state_t& local_state() { - seed_.store(other.seed_.load(std::memory_order_relaxed), std::memory_order_relaxed); - return *this; + thread_local thread_state_t state; + return state; } + public: + seed_generator_t() = default; + explicit seed_generator_t(int64_t initial) : base_seed_(initial) {} + template void set_seed(args... seeds) { #ifdef BENCHMARK - seed_.store(std::random_device{}(), std::memory_order_relaxed); + base_seed_ = static_cast(std::random_device{}()); #else - seed_.store(detail::fold_seed(seeds...), std::memory_order_relaxed); + base_seed_ = detail::fold_seed(seeds...); #endif } - int64_t get_seed() const { return seed_.fetch_add(1, std::memory_order_relaxed); } + int64_t get_seed() const + { + auto& state = local_state(); + if (!state.initialized || state.last_base != base_seed_) { + state.counter = base_seed_; + state.last_base = base_seed_; + state.initialized = true; + } + return state.counter++; + } }; } // namespace cuopt From 088ba4c1cf48ee04de9a13296ed1fa8310fde440 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 19 Aug 2026 17:10:45 -0500 Subject: [PATCH 7/7] Scope to routing; leave MIP on the shared seed generator Revert the MIP heuristics to `cuopt::seed_generator` and move routing's per-problem generator to `routing/utilities/seed_generator.cuh` as `routing::seed_generator_t`. Routing no longer touches the shared counter, which is the bug this PR set out to fix. The MIP half is not a mechanical substitution: the right shape there is per-worker RNGs as in `branch_and_bound/worker.hpp:110`, and the heuristics have no worker identity to hang one off yet. Tracked in #1749. Drops the thread-local counter along with it. It was introduced to keep seed assignment stable across concurrent MIP workers, and it handed the same value to two generators that alternated calls on one thread. Co-Authored-By: Claude Opus 5 --- cpp/src/CMakeLists.txt | 3 +- .../diversity/diversity_manager.cu | 9 +- .../mip_heuristics/diversity/population.cu | 2 +- .../recombiners/bound_prop_recombiner.cuh | 6 +- .../diversity/recombiners/fp_recombiner.cuh | 2 +- .../recombiners/line_segment_recombiner.cuh | 2 +- .../diversity/recombiners/recombiner.cuh | 2 +- .../diversity/recombiners/sub_mip.cuh | 2 +- .../feasibility_jump/feasibility_jump.cu | 4 +- .../mip_heuristics/feasibility_jump/fj_cpu.cu | 8 +- .../feasibility_jump/fj_cpu_worker.cuh | 4 +- .../feasibility_pump/feasibility_pump.cu | 2 +- .../local_search/local_search.cu | 2 +- .../local_search/rounding/bounds_repair.cu | 2 +- .../local_search/rounding/constraint_prop.cu | 33 ++++--- .../local_search/rounding/lb_bounds_repair.cu | 4 +- .../rounding/lb_bounds_repair.cuh | 4 +- .../rounding/lb_constraint_prop.cu | 6 +- .../local_search/rounding/simple_rounding.cu | 8 +- cpp/src/mip_heuristics/problem/problem.cuh | 3 - cpp/src/mip_heuristics/solution/solution.cu | 2 +- cpp/src/mip_heuristics/solve.cu | 7 +- cpp/src/routing/adapters/adapted_modifier.cu | 2 +- cpp/src/routing/diversity/diverse_solver.hpp | 2 +- .../ges/compute_delivery_insertions.cuh | 4 +- cpp/src/routing/ges/ejection_pool.cuh | 2 +- cpp/src/routing/ges/execute_insertion.cu | 2 +- .../lexicographic_search.cu | 2 +- .../ges/lexicographic_search/node_stack.cuh | 2 +- .../local_search/compute_insertions.cu | 2 +- .../routing/local_search/fill_gpu_graph.cu | 2 +- .../local_search/permutation_helper.cuh | 4 +- cpp/src/routing/problem/problem.cu | 2 +- cpp/src/routing/problem/problem.cuh | 2 +- cpp/src/routing/utilities/cuopt_utils.cuh | 4 +- cpp/src/routing/utilities/seed_generator.cuh | 91 ++++++++++++++++++ cpp/src/utilities/seed_generator.cu | 10 ++ cpp/src/utilities/seed_generator.cuh | 93 ++++--------------- cpp/tests/mip/determinism_test.cu | 4 + 39 files changed, 192 insertions(+), 155 deletions(-) create mode 100644 cpp/src/routing/utilities/seed_generator.cuh create mode 100644 cpp/src/utilities/seed_generator.cu diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index cbeeefbe8d..e8737cf6da 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -3,7 +3,8 @@ # SPDX-License-Identifier: Apache-2.0 # cmake-format: on -set(UTIL_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp +set(UTIL_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/seed_generator.cu + ${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 85011e0c47..9d70ae17ee 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -77,13 +77,10 @@ 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(context.problem_ptr->seed_gen.get_seed()), + rng(cuopt::seed_generator::get_seed()), stats(context.stats), - 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"), + 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"), 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 122f85a76f..553e5d6e93 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(context.problem_ptr->seed_gen.get_seed()), + rng(cuopt::seed_generator::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 e8f6631603..42fd838105 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(context.problem_ptr->seed_gen.get_seed()), + rng(cuopt::seed_generator::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 = this->context.problem_ptr->seed_gen.get_seed()] __device__(i_t idx) { + seed = cuopt::seed_generator::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)this->context.problem_ptr->seed_gen.get_seed()}; + thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::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 bc2c8183f1..85909c9d69 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->context.problem_ptr->seed_gen.get_seed()}; + thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::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 25e85ad8e1..a1e6e29c56 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)this->context.problem_ptr->seed_gen.get_seed()}; + thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::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 649a9082ce..0e9c64e796 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(context.problem_ptr->seed_gen.get_seed()); + std::default_random_engine rng_host(cuopt::seed_generator::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 d0e1c5917b..474becef25 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)this->context.problem_ptr->seed_gen.get_seed()}; + thrust::default_random_engine g{(unsigned int)cuopt::seed_generator::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 ba56cf0c91..1853ecfcbd 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(context.problem_ptr->seed_gen.get_seed()); + std::mt19937 rng(cuopt::seed_generator::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 = context.problem_ptr->seed_gen.get_seed(); + settings.seed = cuopt::seed_generator::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 1dfc9a6a2d..57a6a89479 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 : settings.random_seed; + fj_settings.seed = seed >= 0 ? seed : cuopt::seed_generator::get_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(solution.problem_ptr->seed_gen.get_seed()); + 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); } - fj_cpu->settings.seed = solution.problem_ptr->seed_gen.get_seed(); + fj_cpu->settings.seed = cuopt::seed_generator::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 = solution.problem_ptr->seed_gen.get_seed(); + fj_cpu->settings.seed = cuopt::seed_generator::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 703d703cae..ff6022c4c2 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 fall back to the simplex settings' random_seed. + // or -1 to draw from the global cuopt::seed_generator (the historical behavior). // In deterministic mode the caller MUST pass an explicit seed, otherwise the underlying - // shared seed source racing with concurrent callers breaks reproducibility. + // seed_generator::get_seed() 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 323789f9e9..bc47027ed5 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(context.problem_ptr->seed_gen.get_seed()), + rng(cuopt::seed_generator::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 2dc174e2bb..75c4185949 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(context.problem_ptr->seed_gen.get_seed()), + rng(cuopt::seed_generator::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 4b6c3066a7..ddc6db68a7 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(pb.seed_gen.get_seed()), + gen(cuopt::seed_generator::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 497d6df338..861432b720 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(context.problem_ptr->seed_gen.get_seed(), 0, 0) + rng(cuopt::seed_generator::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(context.problem_ptr->seed_gen.get_seed()); + std::mt19937 rng(cuopt::seed_generator::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(context.problem_ptr->seed_gen.get_seed()); + std::mt19937 rng(cuopt::seed_generator::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,20 +879,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 = 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); - }); + 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); + }); 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 27bc0fc935..676a6638d8 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, int64_t seed) +lb_bounds_repair_t::lb_bounds_repair_t(const raft::handle_t* handle_ptr) : 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(seed), + gen(cuopt::seed_generator::get_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 063eb9b543..579fc84bdd 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,9 +40,7 @@ struct lb_bounds_t { template class lb_bounds_repair_t { public: - // 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); + lb_bounds_repair_t(const raft::handle_t* handle_ptr); 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 9b36087d5d..bde8b08ce8 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, context.problem_ptr->seed_gen.get_seed()), + bounds_repair(context.problem_ptr->handle_ptr), 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(context.problem_ptr->seed_gen.get_seed(), 0, 0) + rng(cuopt::seed_generator::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(context.problem_ptr->seed_gen.get_seed()); + std::mt19937 rng(cuopt::seed_generator::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 1f877ced7c..404185fe26 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(), solution.problem_ptr->seed_gen.get_seed()); + solution.view(), cuopt::seed_generator::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(), solution.problem_ptr->seed_gen.get_seed(), n_randomly_rounded.data()); + solution.view(), cuopt::seed_generator::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(solution.problem_ptr->seed_gen.get_seed()); + thrust::default_random_engine rng(cuopt::seed_generator::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(), - solution.problem_ptr->seed_gen.get_seed(), + cuopt::seed_generator::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 7029c1a160..bcc3f06fc2 100644 --- a/cpp/src/mip_heuristics/problem/problem.cuh +++ b/cpp/src/mip_heuristics/problem/problem.cuh @@ -15,7 +15,6 @@ #include #include -#include #include #include @@ -328,8 +327,6 @@ 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 834d054725..3b00fca7a8 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(problem_ptr->seed_gen.get_seed()); + std::mt19937 rng(cuopt::seed_generator::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 e0e9872724..f55aca6878 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -370,6 +370,9 @@ 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); @@ -431,7 +434,6 @@ 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; @@ -451,7 +453,6 @@ 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; @@ -600,8 +601,6 @@ 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_modifier.cu b/cpp/src/routing/adapters/adapted_modifier.cu index a14fc65b4a..4675581a6d 100644 --- a/cpp/src/routing/adapters/adapted_modifier.cu +++ b/cpp/src/routing/adapters/adapted_modifier.cu @@ -5,7 +5,7 @@ */ /* clang-format on */ -#include +#include #include "../diversity/helpers.hpp" #include "../ges/guided_ejection_search.cuh" diff --git a/cpp/src/routing/diversity/diverse_solver.hpp b/cpp/src/routing/diversity/diverse_solver.hpp index 13e677419d..67bf300084 100644 --- a/cpp/src/routing/diversity/diverse_solver.hpp +++ b/cpp/src/routing/diversity/diverse_solver.hpp @@ -11,7 +11,7 @@ #include "helpers.hpp" #include "population.hpp" -#include +#include #include #include "../crossovers/dispose.hpp" #include "../crossovers/eax_recombiner.hpp" diff --git a/cpp/src/routing/ges/compute_delivery_insertions.cuh b/cpp/src/routing/ges/compute_delivery_insertions.cuh index 666919c902..8731743eb1 100644 --- a/cpp/src/routing/ges/compute_delivery_insertions.cuh +++ b/cpp/src/routing/ges/compute_delivery_insertions.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 */ @@ -15,7 +15,7 @@ #include #include -#include +#include namespace cuopt { namespace routing { diff --git a/cpp/src/routing/ges/ejection_pool.cuh b/cpp/src/routing/ges/ejection_pool.cuh index e08bc931a2..afd566f475 100644 --- a/cpp/src/routing/ges/ejection_pool.cuh +++ b/cpp/src/routing/ges/ejection_pool.cuh @@ -10,8 +10,8 @@ #include "../node/node.cuh" #include +#include #include -#include #include #include diff --git a/cpp/src/routing/ges/execute_insertion.cu b/cpp/src/routing/ges/execute_insertion.cu index ae43b4ac36..ddec22acee 100644 --- a/cpp/src/routing/ges/execute_insertion.cu +++ b/cpp/src/routing/ges/execute_insertion.cu @@ -7,8 +7,8 @@ #include "../solution/solution.cuh" +#include #include -#include #include "compute_delivery_insertions.cuh" #include "compute_fragment_ejections.cuh" #include "ejection_pool.cuh" diff --git a/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu b/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu index fa3a62d482..8be74cd348 100644 --- a/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu +++ b/cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu @@ -13,7 +13,7 @@ #include "lexicographic_search.cuh" #include -#include +#include #include "raft/core/span.hpp" #include "raft/random/device/sample.cuh" diff --git a/cpp/src/routing/ges/lexicographic_search/node_stack.cuh b/cpp/src/routing/ges/lexicographic_search/node_stack.cuh index 0f0263261e..3fa2c1fbf5 100644 --- a/cpp/src/routing/ges/lexicographic_search/node_stack.cuh +++ b/cpp/src/routing/ges/lexicographic_search/node_stack.cuh @@ -13,7 +13,7 @@ #include "../../solution/solution.cuh" #include -#include +#include #include "raft/core/span.hpp" diff --git a/cpp/src/routing/local_search/compute_insertions.cu b/cpp/src/routing/local_search/compute_insertions.cu index f0efa5dc7f..1f69065446 100644 --- a/cpp/src/routing/local_search/compute_insertions.cu +++ b/cpp/src/routing/local_search/compute_insertions.cu @@ -9,7 +9,7 @@ #include "compute_insertions.cuh" #include "delivery_insertion.cuh" -#include +#include #include "routing/utilities/cuopt_utils.cuh" #include "../routing_helpers.cuh" diff --git a/cpp/src/routing/local_search/fill_gpu_graph.cu b/cpp/src/routing/local_search/fill_gpu_graph.cu index b8fae427ff..5cb0e6c81e 100644 --- a/cpp/src/routing/local_search/fill_gpu_graph.cu +++ b/cpp/src/routing/local_search/fill_gpu_graph.cu @@ -8,7 +8,7 @@ #include "../solution/solution.cuh" #include "local_search.cuh" -#include +#include #include "../util_kernels/top_k.cuh" #include "cycle_finder/cycle_graph.hpp" #include "routing/utilities/cuopt_utils.cuh" diff --git a/cpp/src/routing/local_search/permutation_helper.cuh b/cpp/src/routing/local_search/permutation_helper.cuh index cc1bc37cb1..d590af946e 100644 --- a/cpp/src/routing/local_search/permutation_helper.cuh +++ b/cpp/src/routing/local_search/permutation_helper.cuh @@ -1,13 +1,13 @@ /* 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 */ #pragma once -#include +#include #include "../node/node.cuh" #include "../route/route.cuh" #include "../routing_helpers.cuh" diff --git a/cpp/src/routing/problem/problem.cu b/cpp/src/routing/problem/problem.cu index fcfd8484c9..6868736fc3 100644 --- a/cpp/src/routing/problem/problem.cu +++ b/cpp/src/routing/problem/problem.cu @@ -11,7 +11,7 @@ #include -#include +#include namespace cuopt { namespace routing { namespace detail { diff --git a/cpp/src/routing/problem/problem.cuh b/cpp/src/routing/problem/problem.cuh index f963e76b4b..46b6c4151b 100644 --- a/cpp/src/routing/problem/problem.cuh +++ b/cpp/src/routing/problem/problem.cuh @@ -18,7 +18,7 @@ #include #include #include -#include +#include #include diff --git a/cpp/src/routing/utilities/cuopt_utils.cuh b/cpp/src/routing/utilities/cuopt_utils.cuh index 41900ceebe..f94bc493c2 100644 --- a/cpp/src/routing/utilities/cuopt_utils.cuh +++ b/cpp/src/routing/utilities/cuopt_utils.cuh @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -10,7 +10,7 @@ #include "routing/utilities/constants.hpp" #include -#include +#include #include #include diff --git a/cpp/src/routing/utilities/seed_generator.cuh b/cpp/src/routing/utilities/seed_generator.cuh new file mode 100644 index 0000000000..172ec613f9 --- /dev/null +++ b/cpp/src/routing/utilities/seed_generator.cuh @@ -0,0 +1,91 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once +#include +#include + +#include +#include +#include + +namespace cuopt { +namespace routing { + +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 Routing's source of deterministic seeds, owned by the problem that uses it. + * + * `problem_t` holds one of these, seeded from the user's `solver_settings_t::set_seed` or, + * when none was given, from the problem's own dimensions. Routing previously drew from a + * process-wide counter shared with the MIP heuristics, so whichever solver constructed its + * problem last overwrote the other's seed. + * + * The counter is `mutable` and atomic so that `get_seed()` can be `const`: `solution_t` + * reaches its problem through a `const` pointer, and drawing a seed does not change the + * problem's logical state. Concurrent callers are handed distinct values, but the order in + * which they receive them is not fixed, so reproducibility still requires a deterministic + * call order. + */ +class seed_generator_t { + mutable std::atomic counter_{0}; + + public: + seed_generator_t() = default; + explicit seed_generator_t(int64_t initial) : counter_(initial) {} + + // std::atomic is neither copyable nor movable, which would delete problem_t's defaulted + // move constructor. Transfer the value instead so the owning problem stays movable. + seed_generator_t(seed_generator_t&& other) noexcept + : counter_(other.counter_.load(std::memory_order_relaxed)) + { + } + + seed_generator_t& operator=(seed_generator_t&& other) noexcept + { + counter_.store(other.counter_.load(std::memory_order_relaxed), std::memory_order_relaxed); + return *this; + } + + template + void set_seed(args... seeds) + { +#ifdef BENCHMARK + counter_.store(static_cast(std::random_device{}()), std::memory_order_relaxed); +#else + counter_.store(detail::fold_seed(seeds...), std::memory_order_relaxed); +#endif + } + + int64_t get_seed() const { return counter_.fetch_add(1, std::memory_order_relaxed); } +}; + +} // namespace routing +} // namespace cuopt diff --git a/cpp/src/utilities/seed_generator.cu b/cpp/src/utilities/seed_generator.cu new file mode 100644 index 0000000000..1da6662bc1 --- /dev/null +++ b/cpp/src/utilities/seed_generator.cu @@ -0,0 +1,10 @@ +/* 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 285304e392..dd5e79d847 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-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -9,92 +9,33 @@ #include #include -#include -#include - namespace cuopt { -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. - * - * The counter that hands out seeds is thread-local and is rebased whenever the owning - * solver's base seed changes. Each thread therefore walks its own deterministic sequence - * from that base, so the order in which concurrent workers happen to ask for seeds does - * not change which seed any of them receives. A shared counter would hand out values in a - * nondeterministic order and break reproducibility across synchronisation points. - * - * Two solvers configured with the *same* base seed and used from one thread continue a - * single sequence rather than restarting, since the rebase is triggered by a change of - * base. - */ -class seed_generator_t { - int64_t base_seed_{0}; - - struct thread_state_t { - int64_t counter{0}; - int64_t last_base{0}; - bool initialized{false}; - }; - - // Shared by every generator on this thread; the base check rebases when the caller - // switches to a solver seeded differently. - static thread_state_t& local_state() - { - thread_local thread_state_t state; - return state; - } +// TODO: should be thread local? +class seed_generator { + static int64_t seed_; public: - seed_generator_t() = default; - explicit seed_generator_t(int64_t initial) : base_seed_(initial) {} - - template - void set_seed(args... seeds) + template + static void set_seed(seed_t seed) { #ifdef BENCHMARK - base_seed_ = static_cast(std::random_device{}()); + seed_ = std::random_device{}(); #else - base_seed_ = detail::fold_seed(seeds...); + seed_ = static_cast(seed); #endif } - - int64_t get_seed() const + template + static void set_seed(arg0 seed0, arg1 seed1, args... seeds) { - auto& state = local_state(); - if (!state.initialized || state.last_base != base_seed_) { - state.counter = base_seed_; - state.last_base = base_seed_; - state.initialized = true; - } - return state.counter++; + set_seed(seed1 + ((seed0 + seed1) * (seed0 + seed1 + 1) / 2), seeds...); } + + static int64_t get_seed() { return seed_++; } + + public: + seed_generator(seed_generator const&) = delete; + void operator=(seed_generator const&) = delete; }; } // namespace cuopt diff --git a/cpp/tests/mip/determinism_test.cu b/cpp/tests/mip/determinism_test.cu index c940342e0f..8f63152d09 100644 --- a/cpp/tests/mip/determinism_test.cu +++ b/cpp/tests/mip/determinism_test.cu @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -202,8 +203,11 @@ 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());