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);