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..da4027add2 100644 --- a/cpp/src/routing/adapters/adapted_generator.cu +++ b/cpp/src/routing/adapters/adapted_generator.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -72,7 +72,7 @@ void generate_tsp_solution(adapted_sol_t& sol, for (i_t i = 0; i < (i_t)node_infos.size(); ++i) { node_infos[i] = sol.problem->get_node_info_of_node(i + sol.problem->order_info.depot_included_); } - std::mt19937 rng(seed_generator::get_seed()); + std::mt19937 rng(sol.problem->seed_gen.get_seed()); std::shuffle(node_infos.begin(), node_infos.end(), rng); std::vector>>> routes_to_add; routes_to_add.push_back({0, node_infos}); diff --git a/cpp/src/routing/adapters/adapted_modifier.cu b/cpp/src/routing/adapters/adapted_modifier.cu index b5f16ccbbd..4675581a6d 100644 --- a/cpp/src/routing/adapters/adapted_modifier.cu +++ b/cpp/src/routing/adapters/adapted_modifier.cu @@ -1,11 +1,11 @@ /* 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 */ -#include +#include #include "../diversity/helpers.hpp" #include "../ges/guided_ejection_search.cuh" @@ -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..67bf300084 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 */ @@ -11,7 +11,7 @@ #include "helpers.hpp" #include "population.hpp" -#include +#include #include #include "../crossovers/dispose.hpp" #include "../crossovers/eax_recombiner.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/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/eject_until_feasible.cu b/cpp/src/routing/ges/eject_until_feasible.cu index 6de2380870..5a05bde062 100644 --- a/cpp/src/routing/ges/eject_until_feasible.cu +++ b/cpp/src/routing/ges/eject_until_feasible.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -365,8 +365,8 @@ void solution_t::eject_until_feasible(bool add_slack_to_sol) bool is_set = set_shmem_of_kernel(eject_until_feasible_kernel, sh_size); cuopt_assert(is_set, "Not enough shared memory on device for get_all_feasible_insertion!"); cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); - eject_until_feasible_kernel - <<>>(view(), add_slack_to_sol, seed_generator::get_seed()); + eject_until_feasible_kernel<<>>( + view(), add_slack_to_sol, problem_ptr->seed_gen.get_seed()); compute_cost(); global_runtime_checks(false, true, "eject_until_feasible"); } @@ -385,7 +385,7 @@ void solution_t::populate_ep_with_unserved( EP.index_ = ep_index_out.value(stream); stream.synchronize(); if (EP.size() > 1) { - thrust::default_random_engine g(seed_generator::get_seed()); + thrust::default_random_engine g(problem_ptr->seed_gen.get_seed()); thrust::shuffle( sol_handle->get_thrust_policy(), EP.stack_.begin(), EP.stack_.begin() + EP.size(), g); } @@ -405,7 +405,7 @@ void solution_t::populate_ep_with_selected_unserved( raft::device_span(unserviced_device.data(), unserviced_device.size()); populate_ep_with_selected_unserved_kernel<<<1, TPB, 0, stream>>>( - view(), unserviced_view, EP.view(), ep_index_out.data(), seed_generator::get_seed()); + view(), unserviced_view, EP.view(), ep_index_out.data(), problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(stream); EP.index_ = ep_index_out.value(stream); stream.synchronize(); diff --git a/cpp/src/routing/ges/ejection_pool.cuh b/cpp/src/routing/ges/ejection_pool.cuh index 061b5ffb88..afd566f475 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 */ @@ -10,8 +10,8 @@ #include "../node/node.cuh" #include +#include #include -#include #include #include @@ -56,13 +56,14 @@ struct ejection_pool_t { void push_back_last() { ++index_; } - void random_shuffle() + // The seed is supplied by the caller: the pool has no route back to the problem + // that owns the seed source. + void random_shuffle(int64_t seed) { // replace with thrust shuffle // how to get sol_handle::get_thrust_policy? if (size() > 1) - device_random_shuffle - <<<1, 1, 0, stream_>>>(stack_.data(), size(), seed_generator::get_seed()); + device_random_shuffle<<<1, 1, 0, stream_>>>(stack_.data(), size(), seed); } bool empty() const diff --git a/cpp/src/routing/ges/execute_insertion.cu b/cpp/src/routing/ges/execute_insertion.cu index dbcfc61250..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" @@ -281,7 +281,7 @@ bool guided_ejection_search_t::execute_best_insertion_ejectio solution_ptr->get_num_orders(), solution_ptr->problem_ptr->get_max_break_dimensions(), solution_ptr->get_n_routes()); - int64_t seed = seed_generator::get_seed(); + int64_t seed = solution_ptr->problem_ptr->seed_gen.get_seed(); i_t* p_scores = p_scores_.data(); i_t fragment_size_arg = fragment_size; i_t fragment_step_arg = fragment_step; @@ -406,7 +406,7 @@ i_t guided_ejection_search_t::find_single_insertion( solution_ptr->get_num_orders(), solution_ptr->problem_ptr->get_max_break_dimensions(), solution_ptr->get_n_routes()), - seed_generator::get_seed()); + solution_ptr->problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(solution_ptr->sol_handle->get_stream()); diff --git a/cpp/src/routing/ges/guided_ejection_search.cu b/cpp/src/routing/ges/guided_ejection_search.cu index 442e7b2b67..1e88375a92 100644 --- a/cpp/src/routing/ges/guided_ejection_search.cu +++ b/cpp/src/routing/ges/guided_ejection_search.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -62,7 +62,7 @@ guided_ejection_search_t::guided_ejection_search_t( (solution.get_num_orders() + solution.problem_ptr->get_max_break_dimensions()), solution.sol_handle->get_stream()), feasible_candidates_size_(solution.sol_handle->get_stream()), - gen_candidate(seed_generator::get_seed()), + gen_candidate(solution.problem_ptr->seed_gen.get_seed()), p_scores_(solution.get_num_orders(), solution.sol_handle->get_stream()), inserted_requests(solution.get_num_orders(), solution.sol_handle->get_stream()), best_squeeze_per_cand(solution.get_num_requests(), solution.sol_handle->get_stream()), @@ -192,7 +192,7 @@ void guided_ejection_search_t::shuffle_pool() raft::common::nvtx::range fun_scope("shuffle_pool"); // include the ejected request in shuffle ++EP.index_; - EP.random_shuffle(); + EP.random_shuffle(solution_ptr->problem_ptr->seed_gen.get_seed()); --EP.index_; if (dump_intermediate) { dump_to_file("Shuffle"); } } @@ -439,7 +439,7 @@ bool guided_ejection_search_t::construct_feasible_solution() } solution_ptr->add_routes(new_routes); // permutate the EP for randomness - EP.random_shuffle(); + EP.random_shuffle(solution_ptr->problem_ptr->seed_gen.get_seed()); bool all_inserted = greedy_insert(); if (!all_inserted) { local_search_ptr_->perturb_solution(*solution_ptr); } diff --git a/cpp/src/routing/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 fa2da01aef..1f69065446 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 */ @@ -9,7 +9,7 @@ #include "compute_insertions.cuh" #include "delivery_insertion.cuh" -#include +#include #include "routing/utilities/cuopt_utils.cuh" #include "../routing_helpers.cuh" @@ -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..5cb0e6c81e 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 */ @@ -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" @@ -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/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/local_search/random_cross.cu b/cpp/src/routing/local_search/random_cross.cu index a54853513f..7d90c96eb6 100644 --- a/cpp/src/routing/local_search/random_cross.cu +++ b/cpp/src/routing/local_search/random_cross.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -204,7 +204,7 @@ void select_random_route_pairs(solution_t& sol, } select_random_route_pairs_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(sol.sol_handle->get_stream()); } @@ -217,7 +217,7 @@ void pick_random_move_per_route_pair(solution_t& sol, auto nblocks = (n_route_pair + nthreads - 1) / nthreads; pick_random_move_per_route_pair_kernel <<get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); RAFT_CHECK_CUDA(sol.sol_handle->get_stream()); } diff --git a/cpp/src/routing/local_search/vrp/vrp_execute.cu b/cpp/src/routing/local_search/vrp/vrp_execute.cu index 5e417a9345..d65ec4fb36 100644 --- a/cpp/src/routing/local_search/vrp/vrp_execute.cu +++ b/cpp/src/routing/local_search/vrp/vrp_execute.cu @@ -394,7 +394,7 @@ i_t extract_non_overlapping_moves(solution_t& sol, cuopt_expects(is_set, error_type_t::OutOfMemoryError, "Not enough shared memory on device"); extract_non_overlapping_moves_kernel <<<1, TPB, sh_size, sol.sol_handle->get_stream()>>>( - sol.view(), move_candidates.view(), seed_generator::get_seed()); + sol.view(), move_candidates.view(), sol.problem_ptr->seed_gen.get_seed()); return move_candidates.vrp_move_candidates.n_of_selected_moves.value( sol.sol_handle->get_stream()); } diff --git a/cpp/src/routing/problem/problem.cu b/cpp/src/routing/problem/problem.cu index 4335b93734..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 { @@ -77,8 +77,14 @@ problem_t::problem_t(const data_model_view_t& data_model_vie initialize_incompatible(problem_ref); } - seed_generator::set_seed( - order_info.get_num_requests(), order_info.get_num_orders(), order_info.get_num_orders()); + // A user-supplied seed wins; otherwise derive one from the problem so that a given + // problem still reproduces run to run, which is the historical behaviour. + if (solver_settings_ptr != nullptr && solver_settings_ptr->get_seed() >= 0) { + seed_gen.set_seed(solver_settings_ptr->get_seed()); + } else { + seed_gen.set_seed( + order_info.get_num_requests(), order_info.get_num_orders(), order_info.get_num_orders()); + } } template diff --git a/cpp/src/routing/problem/problem.cuh b/cpp/src/routing/problem/problem.cuh index c2f00bf9f4..46b6c4151b 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 */ @@ -18,7 +18,7 @@ #include #include #include -#include +#include #include @@ -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/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