From 48c0f4de2c76d465464562a00c0f066ab5f44f75 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 25 Aug 2026 15:26:41 -0500 Subject: [PATCH] refactor: make solver settings constructible without CUDA Two things forced CUDA on anything that merely constructed or inspected solver settings, even when it never touched a device. 1. pdlp_solver_settings_t held pdlp_warm_start_data_t by value. That type owns nine rmm::device_uvector, and its default constructor is out-of-line in a CUDA translation unit because device_uvector has no default ctor -- it needs a stream, and building even a zero-size one calls cudaGetDevice. So constructing settings pulled in libcuopt. It is now held by shared_ptr, allocated lazily via ensure_pdlp_warm_start_data(). shared_ptr rather than unique_ptr specifically: shared_ptr type-erases its deleter into the control block at construction, so a host-only translation unit can copy and destroy the member without the complete type. unique_ptr would only move the problem from the constructor to the destructor. The ~88 device-side uses inside set_pdlp_warm_start_data() are unchanged; a local reference alias keeps that code reading as before. 2. populate_from_data_model_view() inlined both the GPU and CPU warm-start paths in one if/else. The GPU direction is only reachable when handle != nullptr, but the compiler instantiated both branches into every translation unit including the header -- dragging convert_to_gpu_warmstart, convert_to_cpu_warmstart and pdlp_warm_start_data_t(view, stream) along with it. Split into apply_warmstart_gpu_target() (declared in the header, defined in optimization_problem.cu) and apply_warmstart_cpu_target() (host-only, inline), selected by a kHostOnly template parameter dispatched with `if constexpr`. The compile-time dispatch is the point: a host-only caller never *instantiates* the GPU branch, so it emits no reference to it. A runtime `if` would not help. Also moves the warm-start accessors that need no allocation into solver_settings_accessors.cpp, leaving the CUDA TU with only members that do. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- .../optimization_problem_utils.hpp | 102 +++++++++--------- .../pdlp/solver_settings.hpp | 21 +++- cpp/src/grpc/client/cython_grpc_client.cpp | 5 +- cpp/src/pdlp/CMakeLists.txt | 1 + cpp/src/pdlp/optimization_problem.cu | 40 +++++++ cpp/src/pdlp/solver_settings.cu | 40 +++---- cpp/src/pdlp/solver_settings_accessors.cpp | 68 ++++++++++++ 7 files changed, 201 insertions(+), 76 deletions(-) create mode 100644 cpp/src/pdlp/solver_settings_accessors.cpp diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp index b1f81b8edb..50a7d19b4f 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp @@ -137,6 +137,39 @@ void populate_from_mps_data_model(optimization_problem_interface_t* pr } } +/** + * @brief Move warm-start data into the form a GPU solve needs (H2D / view->device_uvector). + * + * Declared here, defined in libcuopt (optimization_problem.cu): it touches device memory, + * so keeping it out-of-line is what lets CUDA-free consumers of this header link without + * a CUDA runtime. Only call it with a real handle. + */ +template +void apply_warmstart_gpu_target(solver_settings_t* solver_settings, + const raft::handle_t* handle); + +/** + * @brief Move warm-start data into the form a CPU / remote solve needs. + * + * Host-only by construction. A CPU-only caller cannot be holding device-resident warm + * start (there is no device to have populated it), so that case is rejected rather than + * converted -- converting would require a D2H copy and thus CUDA. + */ +template +void apply_warmstart_cpu_target(solver_settings_t* solver_settings) +{ + auto& pdlp = solver_settings->get_pdlp_settings(); + + if (pdlp.get_cpu_pdlp_warm_start_data().is_populated()) { return; } + + // Warmstart view (host spans from Cython) -> CPU backend: copy directly, no CUDA needed. + if (solver_settings->get_pdlp_warm_start_data_view() + .last_restart_duality_gap_dual_solution_.size() > 0) { + pdlp.get_cpu_pdlp_warm_start_data() = + cpu_pdlp_warm_start_data_t(solver_settings->get_pdlp_warm_start_data_view()); + } +} + /** * @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays. * @@ -176,7 +209,7 @@ void adopt_from_mps_data_model(optimization_problem_interface_t* probl * @param[in] solver_settings Optional solver settings (for warmstart data, GPU only) * @param[in] handle Optional RAFT handle (for warmstart data, GPU only) */ -template +template void populate_from_data_model_view( optimization_problem_interface_t* problem, cuopt::mathematical_optimization::io::data_model_view_t* data_model, @@ -209,57 +242,26 @@ void populate_from_data_model_view( problem->set_objective_scaling_factor(data_model->get_objective_scaling_factor()); problem->set_objective_offset(data_model->get_objective_offset()); - // Handle warmstart data with GPU↔CPU conversion if needed + // Handle warmstart data with GPU<->CPU conversion if needed. + // + // Split into two helpers deliberately. The GPU direction is only reachable when + // handle != nullptr, but a single inlined if/else instantiated BOTH directions into + // every TU that includes this header -- which dragged convert_to_gpu_warmstart, + // pdlp_warm_start_data_t(view, stream) and friends into the CUDA-free gRPC client. + // apply_warmstart_gpu_target() is declared here and defined in libcuopt, so only + // callers that actually pass a handle reference it. + // + // kHostOnly is a compile-time opt-out, not just a runtime one: `if constexpr` means a + // host-only caller never *instantiates* the GPU branch, so it emits no reference to + // apply_warmstart_gpu_target and needs no CUDA runtime to link. if (solver_settings != nullptr) { - bool target_is_gpu = (handle != nullptr); - - // Check which warmstart type is populated - // Note: Python sets the VIEW (spans), so check both view and data for GPU warmstart - // CPU warmstart is set directly in the data structure - bool has_gpu_warmstart_view = (solver_settings->get_pdlp_warm_start_data_view() - .last_restart_duality_gap_dual_solution_.size() > 0); - bool has_gpu_warmstart_data = - solver_settings->get_pdlp_settings().get_pdlp_warm_start_data().is_populated(); - bool has_cpu_warmstart = - solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data().is_populated(); - - bool has_gpu_warmstart = has_gpu_warmstart_view || has_gpu_warmstart_data; - - if (has_gpu_warmstart || has_cpu_warmstart) { - if (target_is_gpu) { - // Target is GPU backend - if (has_gpu_warmstart_view) { - // GPU warmstart from Python → GPU backend: copy view (spans) to data (device_uvectors) - // Python sets the view (spans over cuDF), but solver needs device_uvectors - pdlp_warm_start_data_t pdlp_warm_start_data( - solver_settings->get_pdlp_warm_start_data_view(), handle->get_stream()); - solver_settings->get_pdlp_settings().set_pdlp_warm_start_data(pdlp_warm_start_data); - } else if (has_gpu_warmstart_data) { - // GPU warmstart from C++ API → GPU backend: data already set, nothing to do - // The device_uvectors are already populated in the settings - } else { - // CPU warmstart → GPU backend: convert H2D - pdlp_warm_start_data_t gpu_warmstart = convert_to_gpu_warmstart( - solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data(), - handle->get_stream()); - solver_settings->get_pdlp_settings().set_pdlp_warm_start_data(gpu_warmstart); - } + if constexpr (kHostOnly) { + apply_warmstart_cpu_target(solver_settings); + } else { + if (handle != nullptr) { + apply_warmstart_gpu_target(solver_settings, handle); } else { - // Target is CPU backend (remote execution) - if (has_cpu_warmstart) { - // CPU warmstart → CPU backend: data already in correct form, nothing to do - } else if (has_gpu_warmstart_view) { - // Warmstart view (host spans from Cython) → CPU backend: copy directly, no CUDA needed - solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data() = - cpu_pdlp_warm_start_data_t(solver_settings->get_pdlp_warm_start_data_view()); - } else { - // GPU warmstart data (device_uvectors) → CPU backend: convert D2H - auto& gpu_ws = solver_settings->get_pdlp_settings().get_pdlp_warm_start_data(); - cpu_pdlp_warm_start_data_t cpu_warmstart = - convert_to_cpu_warmstart(gpu_ws, gpu_ws.current_primal_solution_.stream()); - solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data() = - std::move(cpu_warmstart); - } + apply_warmstart_cpu_target(solver_settings); } } } diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 0882f75e0f..1ec38be996 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -371,8 +372,24 @@ class pdlp_solver_settings_t { /** Initial pdlp iteration */ // TODO batch mode: tmp std::optional initial_pdlp_iteration_; - /** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves */ - pdlp_warm_start_data_t pdlp_warm_start_data_; + /** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves. + * + * Held by shared_ptr rather than by value so that constructing a settings object needs + * no CUDA. pdlp_warm_start_data_t owns nine rmm::device_uvector, and its default ctor is + * out-of-line in a CUDA TU (device_uvector has no default ctor -- it needs a stream, and + * building even a zero-size one calls cudaGetDevice). By value, that made every consumer + * of solver_settings_t -- including the CUDA-free gRPC client -- depend on libcuopt. + * + * shared_ptr specifically, not unique_ptr: shared_ptr type-erases its deleter into the + * control block at construction, so a host-only TU can copy and destroy this member + * without the complete type. unique_ptr would just move the problem to the destructor. + * + * Null until a GPU consumer first needs it; use ensure_pdlp_warm_start_data(). + */ + mutable std::shared_ptr> pdlp_warm_start_data_; + + /** Lazily allocate pdlp_warm_start_data_ and return it. Defined in a CUDA TU. */ + pdlp_warm_start_data_t& ensure_pdlp_warm_start_data() const; /** Warm start data as spans over external memory, used by Cython/Python interface */ pdlp_warm_start_data_view_t pdlp_warm_start_data_view_; /** CPU-backed warm start data (std::vector), used for remote execution on CPU-only hosts */ diff --git a/cpp/src/grpc/client/cython_grpc_client.cpp b/cpp/src/grpc/client/cython_grpc_client.cpp index 74409fc93e..0d574d9df6 100644 --- a/cpp/src/grpc/client/cython_grpc_client.cpp +++ b/cpp/src/grpc/client/cython_grpc_client.cpp @@ -109,7 +109,10 @@ grpc_submit_result_t grpc_python_client_t::submit( } cuopt::mathematical_optimization::cpu_optimization_problem_t cpu_problem; - cuopt::mathematical_optimization::populate_from_data_model_view( + // : this is a remote client, so the GPU warm-start + // path is unreachable here. Selecting it explicitly keeps the device conversions from + // being instantiated into cuopt_client. + cuopt::mathematical_optimization::populate_from_data_model_view( &cpu_problem, data_model, settings, nullptr); const bool is_mip = diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index b6a1f8a46d..1b1439b350 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -6,6 +6,7 @@ # Core LP files always included set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu + ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings_accessors.cpp ${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 diff --git a/cpp/src/pdlp/optimization_problem.cu b/cpp/src/pdlp/optimization_problem.cu index 31f8a315d4..70d6112978 100644 --- a/cpp/src/pdlp/optimization_problem.cu +++ b/cpp/src/pdlp/optimization_problem.cu @@ -1637,4 +1637,44 @@ template CUOPT_EXPORT optimization_problem_t rmm::cuda_stream_view) const; #endif + +// GPU-target warm-start handling, declared in optimization_problem_utils.hpp. +// +// Defined here rather than inline in the header so that CUDA-free consumers of that +// header (the gRPC client in cuopt_client) never instantiate the device conversions. +template +void apply_warmstart_gpu_target(solver_settings_t* solver_settings, + const raft::handle_t* handle) +{ + auto& pdlp = solver_settings->get_pdlp_settings(); + + const bool has_view = (solver_settings->get_pdlp_warm_start_data_view() + .last_restart_duality_gap_dual_solution_.size() > 0); + const bool has_device_data = pdlp.get_pdlp_warm_start_data().is_populated(); + const bool has_host_data = pdlp.get_cpu_pdlp_warm_start_data().is_populated(); + + if (!has_view && !has_device_data && !has_host_data) { return; } + + if (has_view) { + // Warmstart from Python (spans over cuDF) -> solver needs device_uvectors. + pdlp_warm_start_data_t warm_start(solver_settings->get_pdlp_warm_start_data_view(), + handle->get_stream()); + pdlp.set_pdlp_warm_start_data(warm_start); + } else if (has_device_data) { + // Already device-resident from the C++ API: nothing to do. + } else { + // Host warmstart -> GPU backend: convert H2D. + pdlp_warm_start_data_t warm_start = + convert_to_gpu_warmstart(pdlp.get_cpu_pdlp_warm_start_data(), handle->get_stream()); + pdlp.set_pdlp_warm_start_data(warm_start); + } +} + +#if MIP_INSTANTIATE_FLOAT +template void apply_warmstart_gpu_target(solver_settings_t*, const raft::handle_t*); +#endif +#if MIP_INSTANTIATE_DOUBLE +template void apply_warmstart_gpu_target(solver_settings_t*, const raft::handle_t*); +#endif + } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solver_settings.cu b/cpp/src/pdlp/solver_settings.cu index 33d8f1a64b..77bc0512da 100644 --- a/cpp/src/pdlp/solver_settings.cu +++ b/cpp/src/pdlp/solver_settings.cu @@ -95,7 +95,10 @@ void pdlp_solver_settings_t::set_pdlp_warm_start_data( const rmm::device_uvector& var_mapping, const rmm::device_uvector& constraint_mapping) { - pdlp_warm_start_data_ = std::move(pdlp_warm_start_data_view); + // pdlp_warm_start_data_ is a shared_ptr now (see solver_settings.hpp); alias it so the + // device code below reads unchanged. + auto& pdlp_warm_start_data_ = ensure_pdlp_warm_start_data(); + pdlp_warm_start_data_ = std::move(pdlp_warm_start_data_view); // A var_mapping was given if (var_mapping.size() != 0) { @@ -382,37 +385,28 @@ std::optional pdlp_solver_settings_t::get_initial_pdlp_iteration( } template -const pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() - const noexcept -{ - return pdlp_warm_start_data_; -} - -template -pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() -{ - return pdlp_warm_start_data_; -} - -template -const cpu_pdlp_warm_start_data_t& -pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept +pdlp_warm_start_data_t& pdlp_solver_settings_t::ensure_pdlp_warm_start_data() + const { - return cpu_pdlp_warm_start_data_; + if (!pdlp_warm_start_data_) { + pdlp_warm_start_data_ = std::make_shared>(); + } + return *pdlp_warm_start_data_; } +// These two live here rather than in solver_settings_accessors.cpp: they may have to +// allocate the device-backed warm-start object, so they need CUDA. template -cpu_pdlp_warm_start_data_t& -pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept +const pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() + const noexcept { - return cpu_pdlp_warm_start_data_; + return ensure_pdlp_warm_start_data(); } template -const pdlp_warm_start_data_view_t& -pdlp_solver_settings_t::get_pdlp_warm_start_data_view() const noexcept +pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() { - return pdlp_warm_start_data_view_; + return ensure_pdlp_warm_start_data(); } #if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT diff --git a/cpp/src/pdlp/solver_settings_accessors.cpp b/cpp/src/pdlp/solver_settings_accessors.cpp new file mode 100644 index 0000000000..9d5efc7f9a --- /dev/null +++ b/cpp/src/pdlp/solver_settings_accessors.cpp @@ -0,0 +1,68 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Warm-start accessors of pdlp_solver_settings_t, split out of solver_settings.cu. +// +// These are trivial `return member_;` getters -- they hand back a reference and emit no +// device code, even where the referent is a GPU type. The gRPC client needs them, so they +// build into the CUDA-free cuopt_client library while the rest of the class (which does +// real thrust/rmm work) stays in solver_settings.cu. +// +// Only these members are instantiated below, deliberately NOT `template class`: the class +// holds a pdlp_warm_start_data_t, so instantiating all of it here would pull in device +// ctor/dtor code that belongs in the CUDA TU. + +#include +#include + +// Required: the explicit instantiations below are guarded on MIP_INSTANTIATE_* / +// PDLP_INSTANTIATE_*. Without this header those macros are undefined, the guards +// evaluate false, and this TU silently compiles to zero symbols. +#include + +namespace cuopt::mathematical_optimization { + +template +const cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept +{ + return cpu_pdlp_warm_start_data_; +} + +template +cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept +{ + return cpu_pdlp_warm_start_data_; +} + +template +const pdlp_warm_start_data_view_t& +pdlp_solver_settings_t::get_pdlp_warm_start_data_view() const noexcept +{ + return pdlp_warm_start_data_view_; +} + +#if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT +template CUOPT_EXPORT const cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept; +template CUOPT_EXPORT cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept; +template CUOPT_EXPORT const pdlp_warm_start_data_view_t& +pdlp_solver_settings_t::get_pdlp_warm_start_data_view() const noexcept; +#endif + +#if MIP_INSTANTIATE_DOUBLE +template CUOPT_EXPORT const cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept; +template CUOPT_EXPORT cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept; +template CUOPT_EXPORT const pdlp_warm_start_data_view_t& +pdlp_solver_settings_t::get_pdlp_warm_start_data_view() const noexcept; +#endif + +} // namespace cuopt::mathematical_optimization