From 691f27022308d3de10e53793e455254becabb429 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 25 Aug 2026 15:26:21 -0500 Subject: [PATCH] refactor: make to_optimization_problem a free function cpu_optimization_problem_t::to_optimization_problem() was a virtual member on optimization_problem_interface_t. That put it in the vtable of every implementer, including the CPU one -- so cpu_optimization_problem_t's vtable held an entry that only libcuopt can define. Vtable relocations are resolved eagerly at load time, unlike ordinary function calls, so this cannot be deferred or hidden behind lazy binding. Any library carrying that vtable is unloadable without libcuopt.so present. It is now a free function declared in optimization_problem.hpp and defined in cpu_optimization_problem_to_gpu.cpp, dispatching on the concrete type: auto gpu = to_optimization_problem(problem, &handle); The GPU override was a one-line `return nullptr` ("already a GPU problem"), so the dispatch is a single dynamic_cast and the semantics are unchanged -- a GPU-backed problem still yields nullptr. cpu_optimization_problem_t befriends the function to reach its host-side storage. Six call sites updated across pdlp/solve.cu, mip_heuristics/solve.cu, grpc/server/grpc_worker.cpp and solution_interface_test.cu. Splitting the definition into its own translation unit also keeps and the raft handle out of cpu_optimization_problem.cpp, which is otherwise pure host code. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- .../cpu_optimization_problem.hpp | 13 +- .../optimization_problem.hpp | 29 +++- .../optimization_problem_interface.hpp | 23 +-- cpp/src/grpc/server/grpc_worker.cpp | 4 +- cpp/src/mip_heuristics/solve.cu | 2 +- cpp/src/pdlp/CMakeLists.txt | 1 + cpp/src/pdlp/cpu_optimization_problem.cpp | 95 ----------- .../pdlp/cpu_optimization_problem_to_gpu.cpp | 160 ++++++++++++++++++ cpp/src/pdlp/optimization_problem.cu | 8 - cpp/src/pdlp/solve.cu | 2 +- .../unit_tests/solution_interface_test.cu | 4 +- 11 files changed, 208 insertions(+), 133 deletions(-) create mode 100644 cpp/src/pdlp/cpu_optimization_problem_to_gpu.cpp diff --git a/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp b/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp index 28aa91a82f..97d53b2d28 100644 --- a/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp +++ b/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp @@ -173,9 +173,11 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t> to_optimization_problem( - raft::handle_t const* handle_ptr = nullptr) override; /** * @brief Write the optimization problem to an MPS file. @@ -207,6 +209,13 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t + friend std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t&, raft::handle_t const*); + problem_category_t problem_category_ = problem_category_t::LP; bool maximize_{false}; i_t n_vars_{0}; diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp index bdfc2ffbd4..355a317da1 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp @@ -352,12 +352,8 @@ class optimization_problem_t : public optimization_problem_interface_t template optimization_problem_t convert_to_other_prec(rmm::cuda_stream_view stream) const; - /** - * @brief Returns nullptr since this is already a GPU problem. - * @return nullptr - */ - std::unique_ptr> to_optimization_problem( - raft::handle_t const* handle_ptr = nullptr) override; + // to_optimization_problem() is a free function declared at the bottom of this header, + // not a virtual member -- see the note in optimization_problem_interface.hpp. // ============================================================================ // C API support: Copy to host (polymorphic) @@ -427,5 +423,26 @@ class optimization_problem_t : public optimization_problem_interface_t std::vector row_names_{}; }; +/** + * @brief Convert a problem to a GPU-backed optimization_problem_t. + * + * For optimization_problem_t (GPU): returns nullptr (already is one). + * For cpu_optimization_problem_t: creates a new GPU problem, copies data, returns it. + * + * Usage pattern: + * auto temp = to_optimization_problem(problem_interface, &handle); + * optimization_problem_t& op = temp ? *temp : static_cast(problem); + * + * A free function rather than a virtual member so that cpu_optimization_problem_t's vtable + * carries no GPU-defined entry; see optimization_problem_interface.hpp. + * + * @param problem The problem to convert. + * @param handle_ptr RAFT handle with CUDA resources. Required for CPU->GPU conversion. + * @return unique_ptr to a new GPU problem, or nullptr if it already is one. + */ +template +std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t& problem, raft::handle_t const* handle_ptr = nullptr); + } // namespace CUOPT_EXPORT mathematical_optimization } // namespace cuopt diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp index 5927703f03..51796aa60d 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp @@ -478,22 +478,13 @@ class optimization_problem_interface_t { // Conversion // ============================================================================ - /** - * @brief Convert to a GPU-backed optimization_problem_t. - * - * For optimization_problem_t (GPU): returns nullptr (already is one). - * For cpu_optimization_problem_t: creates new GPU problem, copies data, returns owned pointer. - * - * Usage pattern: - * auto temp = problem_interface->to_optimization_problem(&handle); - * optimization_problem_t& op = temp ? *temp : static_cast(*this); - * - * @param handle_ptr RAFT handle with CUDA resources for GPU memory allocation. - * Required for CPU->GPU conversion. Ignored for GPU problems. - * @return unique_ptr to new GPU problem, or nullptr if already a GPU problem - */ - virtual std::unique_ptr> to_optimization_problem( - raft::handle_t const* handle_ptr = nullptr) = 0; + // NOTE: CPU -> GPU conversion is deliberately NOT a virtual member here. + // + // As a virtual, it occupied a slot in cpu_optimization_problem_t's vtable, and vtable + // relocations are resolved eagerly at load time. That made every library containing + // the vtable -- including the CUDA-free cuopt_client -- unable to load without + // libcuopt.so present. It is now the free function to_optimization_problem() declared + // in optimization_problem.hpp, which lives in libcuopt where the GPU types do. }; } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/grpc/server/grpc_worker.cpp b/cpp/src/grpc/server/grpc_worker.cpp index 250b640031..aa048f34bb 100644 --- a/cpp/src/grpc/server/grpc_worker.cpp +++ b/cpp/src/grpc/server/grpc_worker.cpp @@ -425,7 +425,7 @@ static SolveResult run_mip_solve(DeserializedJob& dj, } SERVER_LOG_INFO("[Worker] Converting CPU problem to GPU problem..."); - auto gpu_problem = dj.problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(dj.problem, &handle); SERVER_LOG_INFO("[Worker] Calling solve_mip..."); auto gpu_solution = cuopt::mathematical_optimization::solve_mip(*gpu_problem, dj.mip_settings); @@ -486,7 +486,7 @@ static SolveResult run_lp_solve(DeserializedJob& dj, dj.lp_settings.log_to_console = config.log_to_console; SERVER_LOG_INFO("[Worker] Converting CPU problem to GPU problem..."); - auto gpu_problem = dj.problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(dj.problem, &handle); SERVER_LOG_INFO("[Worker] Calling solve_lp..."); auto gpu_solution = cuopt::mathematical_optimization::solve_lp(*gpu_problem, dj.lp_settings); diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 162a5ba291..9b53f2a06f 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -894,7 +894,7 @@ std::unique_ptr> solve_mip( raft::handle_t handle(stream); // Convert CPU problem to GPU problem - auto gpu_problem = cpu_problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(cpu_problem, &handle); // Synchronize before solving to ensure conversion is complete stream.synchronize(); diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 44dced14bc..b6a1f8a46d 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -8,6 +8,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu ${CMAKE_CURRENT_SOURCE_DIR}/optimization_problem.cu ${CMAKE_CURRENT_SOURCE_DIR}/cpu_optimization_problem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cpu_optimization_problem_to_gpu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/backend_selection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/problem_checking.cu ${CMAKE_CURRENT_SOURCE_DIR}/solve.cu diff --git a/cpp/src/pdlp/cpu_optimization_problem.cpp b/cpp/src/pdlp/cpu_optimization_problem.cpp index 4b970eb6ec..8e310b287b 100644 --- a/cpp/src/pdlp/cpu_optimization_problem.cpp +++ b/cpp/src/pdlp/cpu_optimization_problem.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -634,100 +633,6 @@ std::vector cpu_optimization_problem_t::get_variable_types_host return variable_types_; } -// ============================================================================== -// Conversion to optimization_problem_t -// ============================================================================== - -template -std::unique_ptr> -cpu_optimization_problem_t::to_optimization_problem(raft::handle_t const* handle_ptr) -{ - if (handle_ptr == nullptr) { - throw std::runtime_error( - "cpu_optimization_problem_t::to_optimization_problem(): " - "handle_ptr is null. A RAFT handle with CUDA resources is required to convert " - "a CPU-backed problem to a GPU-backed optimization_problem_t."); - } - - auto gpu_problem = std::make_unique>(handle_ptr); - - // Set scalar values - gpu_problem->set_maximize(maximize_); - gpu_problem->set_objective_scaling_factor(objective_scaling_factor_); - gpu_problem->set_objective_offset(objective_offset_); - gpu_problem->set_problem_category(problem_category_); - - // Set string values - if (!objective_name_.empty()) gpu_problem->set_objective_name(objective_name_); - if (!problem_name_.empty()) gpu_problem->set_problem_name(problem_name_); - if (!var_names_.empty()) gpu_problem->set_variable_names(var_names_); - if (!row_names_.empty()) gpu_problem->set_row_names(row_names_); - - // Set CSR constraint matrix (data will be copied to GPU by optimization_problem_t setters) - // Use A_offsets_ presence as the guard: a valid CSR can have zero non-zeros but still - // needs row offsets to define the number of constraints. - if (!A_offsets_.empty()) { - gpu_problem->set_csr_constraint_matrix(A_.data(), - A_.size(), - A_indices_.data(), - A_indices_.size(), - A_offsets_.data(), - A_offsets_.size()); - } - - // Set constraint bounds - if (!b_.empty()) { gpu_problem->set_constraint_bounds(b_.data(), b_.size()); } - - // Set objective coefficients - if (!c_.empty()) { gpu_problem->set_objective_coefficients(c_.data(), c_.size()); } - - // Set quadratic objective if present (GPU setter symmetrizes once: H = Q + Q^T) - if (!Q_values_.empty()) { - gpu_problem->set_quadratic_objective_matrix(Q_values_.data(), - Q_values_.size(), - Q_indices_.data(), - Q_indices_.size(), - Q_offsets_.data(), - Q_offsets_.size()); - } - - if (!quadratic_constraints_.empty()) { - gpu_problem->set_quadratic_constraints( - std::vector::quadratic_constraint_t>( - quadratic_constraints_)); - } - - // Set variable bounds - if (!variable_lower_bounds_.empty()) { - gpu_problem->set_variable_lower_bounds(variable_lower_bounds_.data(), - variable_lower_bounds_.size()); - } - if (!variable_upper_bounds_.empty()) { - gpu_problem->set_variable_upper_bounds(variable_upper_bounds_.data(), - variable_upper_bounds_.size()); - } - - // Set variable types - if (!variable_types_.empty()) { - gpu_problem->set_variable_types(variable_types_.data(), variable_types_.size()); - } - - // Set constraint bounds - if (!constraint_lower_bounds_.empty()) { - gpu_problem->set_constraint_lower_bounds(constraint_lower_bounds_.data(), - constraint_lower_bounds_.size()); - } - if (!constraint_upper_bounds_.empty()) { - gpu_problem->set_constraint_upper_bounds(constraint_upper_bounds_.data(), - constraint_upper_bounds_.size()); - } - - // Set row types - if (!row_types_.empty()) { gpu_problem->set_row_types(row_types_.data(), row_types_.size()); } - - return gpu_problem; -} - // ============================================================================== // File I/O // ============================================================================== diff --git a/cpp/src/pdlp/cpu_optimization_problem_to_gpu.cpp b/cpp/src/pdlp/cpu_optimization_problem_to_gpu.cpp new file mode 100644 index 0000000000..5824c74737 --- /dev/null +++ b/cpp/src/pdlp/cpu_optimization_problem_to_gpu.cpp @@ -0,0 +1,160 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// CPU -> GPU conversion for cpu_optimization_problem_t. +// +// Split out of cpu_optimization_problem.cpp so that the rest of that class -- which is +// pure host code -- can be compiled into the CUDA-free cuopt_client library. This is the +// only member that constructs an optimization_problem_t, so it is the only one that needs +// and a raft handle. It stays in cuopt_objs (libcuopt). +// +// The explicit member instantiations at the bottom are required: the `template class` +// instantiation in cpu_optimization_problem.cpp no longer sees this definition, so it +// cannot emit this member. + +#include +#include +#include + +// Required: the explicit instantiations below are guarded on MIP_INSTANTIATE_*. +// Without this header those macros are undefined and this TU emits no symbols. +#include + +#include +#include +#include + +namespace cuopt::mathematical_optimization { + +// Free function (was a virtual member; see optimization_problem_interface.hpp). +// Dispatches on the concrete type: a GPU problem is already what the caller wants, so it +// yields nullptr, matching the previous optimization_problem_t override. +// +// Unrecognised implementations are rejected rather than yielding nullptr. As a pure virtual +// this was enforced at compile time -- every implementer had to provide an override. A free +// function cannot enforce that, and returning nullptr would be actively unsafe: the +// documented fallback static_casts the original reference to optimization_problem_t&, which +// is undefined behaviour for any other type. +template +std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t& problem, raft::handle_t const* handle_ptr) +{ + auto* cpu_problem = dynamic_cast*>(&problem); + if (cpu_problem == nullptr) { + if (dynamic_cast*>(&problem) != nullptr) { + // Already a GPU-backed problem; nothing to convert. + return nullptr; + } + throw std::runtime_error( + "to_optimization_problem(): unsupported optimization_problem_interface_t " + "implementation. Only optimization_problem_t and cpu_optimization_problem_t " + "are supported."); + } + auto& self = *cpu_problem; + + if (handle_ptr == nullptr) { + throw std::runtime_error( + "cpu_optimization_problem_t::to_optimization_problem(): " + "handle_ptr is null. A RAFT handle with CUDA resources is required to convert " + "a CPU-backed problem to a GPU-backed optimization_problem_t."); + } + + auto gpu_problem = std::make_unique>(handle_ptr); + + // Set scalar values + gpu_problem->set_maximize(self.maximize_); + gpu_problem->set_objective_scaling_factor(self.objective_scaling_factor_); + gpu_problem->set_objective_offset(self.objective_offset_); + gpu_problem->set_problem_category(self.problem_category_); + + // Set string values + if (!self.objective_name_.empty()) gpu_problem->set_objective_name(self.objective_name_); + if (!self.problem_name_.empty()) gpu_problem->set_problem_name(self.problem_name_); + if (!self.var_names_.empty()) gpu_problem->set_variable_names(self.var_names_); + if (!self.row_names_.empty()) gpu_problem->set_row_names(self.row_names_); + + // Set CSR constraint matrix (data will be copied to GPU by optimization_problem_t setters) + // Use A_offsets_ presence as the guard: a valid CSR can have zero non-zeros but still + // needs row offsets to define the number of constraints. + if (!self.A_offsets_.empty()) { + gpu_problem->set_csr_constraint_matrix(self.A_.data(), + self.A_.size(), + self.A_indices_.data(), + self.A_indices_.size(), + self.A_offsets_.data(), + self.A_offsets_.size()); + } + + // Set constraint bounds + if (!self.b_.empty()) { gpu_problem->set_constraint_bounds(self.b_.data(), self.b_.size()); } + + // Set objective coefficients + if (!self.c_.empty()) { gpu_problem->set_objective_coefficients(self.c_.data(), self.c_.size()); } + + // Set quadratic objective if present (GPU setter symmetrizes once: H = Q + Q^T) + if (!self.Q_values_.empty()) { + gpu_problem->set_quadratic_objective_matrix(self.Q_values_.data(), + self.Q_values_.size(), + self.Q_indices_.data(), + self.Q_indices_.size(), + self.Q_offsets_.data(), + self.Q_offsets_.size()); + } + + if (!self.quadratic_constraints_.empty()) { + gpu_problem->set_quadratic_constraints( + std::vector::quadratic_constraint_t>( + self.quadratic_constraints_)); + } + + // Set variable bounds + if (!self.variable_lower_bounds_.empty()) { + gpu_problem->set_variable_lower_bounds(self.variable_lower_bounds_.data(), + self.variable_lower_bounds_.size()); + } + if (!self.variable_upper_bounds_.empty()) { + gpu_problem->set_variable_upper_bounds(self.variable_upper_bounds_.data(), + self.variable_upper_bounds_.size()); + } + + // Set variable types + if (!self.variable_types_.empty()) { + gpu_problem->set_variable_types(self.variable_types_.data(), self.variable_types_.size()); + } + + // Set constraint bounds + if (!self.constraint_lower_bounds_.empty()) { + gpu_problem->set_constraint_lower_bounds(self.constraint_lower_bounds_.data(), + self.constraint_lower_bounds_.size()); + } + if (!self.constraint_upper_bounds_.empty()) { + gpu_problem->set_constraint_upper_bounds(self.constraint_upper_bounds_.data(), + self.constraint_upper_bounds_.size()); + } + + // Set row types + if (!self.row_types_.empty()) { + gpu_problem->set_row_types(self.row_types_.data(), self.row_types_.size()); + } + + return gpu_problem; +} + +// ============================================================================== +// Template instantiations matching cpu_optimization_problem.cpp +// ============================================================================== + +#if MIP_INSTANTIATE_FLOAT +template CUOPT_EXPORT std::unique_ptr> +to_optimization_problem(optimization_problem_interface_t&, raft::handle_t const*); +#endif +#if MIP_INSTANTIATE_DOUBLE +template CUOPT_EXPORT std::unique_ptr> +to_optimization_problem(optimization_problem_interface_t&, raft::handle_t const*); +#endif + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/optimization_problem.cu b/cpp/src/pdlp/optimization_problem.cu index 95457e2556..31f8a315d4 100644 --- a/cpp/src/pdlp/optimization_problem.cu +++ b/cpp/src/pdlp/optimization_problem.cu @@ -639,14 +639,6 @@ raft::handle_t const* optimization_problem_t::get_handle_ptr() const n // Conversion // ============================================================================== -template -std::unique_ptr> -optimization_problem_t::to_optimization_problem(raft::handle_t const* /*handle_ptr*/) -{ - // Already a GPU problem, return nullptr - return nullptr; -} - // ============================================================================== // Host Getters (copy from GPU to CPU) // ============================================================================== diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index b8cb4aead8..f264526a62 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2679,7 +2679,7 @@ std::unique_ptr> solve_lp( raft::handle_t handle(stream); // Convert CPU problem to GPU problem - auto gpu_problem = cpu_problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(cpu_problem, &handle); // Synchronize before solving to ensure conversion is complete stream.synchronize(); diff --git a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu index b34faa88b4..85dba0227b 100644 --- a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu @@ -305,7 +305,7 @@ TEST_F(SolutionInterfaceTest, gpu_problem_to_optimization_problem) EXPECT_EQ(problem->get_n_constraints(), kNCons); // GPU problem's to_optimization_problem() returns nullptr (already a GPU problem) - auto concrete = problem->to_optimization_problem(&handle); + auto concrete = to_optimization_problem(*problem, &handle); EXPECT_EQ(concrete, nullptr); // Verify the data is still accessible directly on the problem @@ -340,7 +340,7 @@ TEST_F(SolutionInterfaceTest, cpu_problem_to_optimization_problem) EXPECT_EQ(problem->get_n_variables(), kNVars); EXPECT_EQ(problem->get_n_constraints(), kNCons); - auto concrete = problem->to_optimization_problem(&handle); + auto concrete = to_optimization_problem(*problem, &handle); ASSERT_NE(concrete, nullptr); EXPECT_EQ(concrete->get_n_variables(), kNVars); EXPECT_EQ(concrete->get_n_constraints(), kNCons);