diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp index b1f81b8edb..2d9dff5fed 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp @@ -137,6 +137,54 @@ 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, including a + * device-to-host copy when the warm start is device-resident. + * + * Declared here, defined in libcuopt (optimization_problem.cu). This is the null-handle + * path for a normal (non host-only) caller: it has a device, so its settings may hold + * device_uvector-backed warm start that must be brought to host before a remote solve. + */ +template +void apply_warmstart_cpu_target_with_device(solver_settings_t* solver_settings); + +/** + * @brief Move warm-start data into the form a CPU / remote solve needs, host paths only. + * + * Handles the two cases reachable without a device: warm start already in host form + * (nothing to do), and a warm-start view over host spans (copy it). + * + * Deliberately does NOT handle device-resident warm start -- that needs a D2H copy and + * therefore CUDA. Callers that might be holding device data must use + * apply_warmstart_cpu_target_with_device() instead; only a kHostOnly caller, which by + * construction has no device to have populated it, may use this one. + */ +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 +224,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 +257,28 @@ 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); - } + // No handle, but this caller has a device: the warm start may be device-resident, + // so it needs the variant that can copy it back to host. + apply_warmstart_cpu_target_with_device(solver_settings); } } } 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..d2dd105284 100644 --- a/cpp/src/pdlp/optimization_problem.cu +++ b/cpp/src/pdlp/optimization_problem.cu @@ -1637,4 +1637,77 @@ 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); + } +} + +// Null-handle CPU-target warm-start handling for callers that do have a device. +// +// Mirrors apply_warmstart_cpu_target() but adds the case that one cannot handle: warm +// start already sitting in device_uvectors, which needs a D2H copy before a remote solve. +// Dropping this silently loses a user's warm start on the +// populate_from_data_model_view(..., handle=nullptr) path (see cython_solve.cu). +template +void apply_warmstart_cpu_target_with_device(solver_settings_t* solver_settings) +{ + auto& pdlp = solver_settings->get_pdlp_settings(); + + // Already in host form. + if (pdlp.get_cpu_pdlp_warm_start_data().is_populated()) { return; } + + // Warm-start 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()); + return; + } + + // Device-resident warm start -> CPU backend: convert D2H. + auto& gpu_ws = pdlp.get_pdlp_warm_start_data(); + if (gpu_ws.is_populated()) { + pdlp.get_cpu_pdlp_warm_start_data() = + convert_to_cpu_warmstart(gpu_ws, gpu_ws.current_primal_solution_.stream()); + } +} + +#if MIP_INSTANTIATE_FLOAT +template CUOPT_EXPORT void apply_warmstart_gpu_target(solver_settings_t*, + const raft::handle_t*); +template CUOPT_EXPORT void apply_warmstart_cpu_target_with_device(solver_settings_t*); +#endif +#if MIP_INSTANTIATE_DOUBLE +template CUOPT_EXPORT void apply_warmstart_gpu_target(solver_settings_t*, + const raft::handle_t*); +template CUOPT_EXPORT void apply_warmstart_cpu_target_with_device(solver_settings_t*); +#endif + } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solver_settings.cu b/cpp/src/pdlp/solver_settings.cu index 33d8f1a64b..6ae0a84828 100644 --- a/cpp/src/pdlp/solver_settings.cu +++ b/cpp/src/pdlp/solver_settings.cu @@ -394,27 +394,6 @@ pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_war 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 -{ - 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 class CUOPT_EXPORT pdlp_solver_settings_t; #endif 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