Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,39 @@ void populate_from_mps_data_model(optimization_problem_interface_t<i_t, f_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 <typename i_t, typename f_t>
void apply_warmstart_gpu_target(solver_settings_t<i_t, f_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 <typename i_t, typename f_t>
void apply_warmstart_cpu_target(solver_settings_t<i_t, f_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<i_t, f_t>(solver_settings->get_pdlp_warm_start_data_view());
}
}

/**
* @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays.
*
Expand Down Expand Up @@ -176,7 +209,7 @@ void adopt_from_mps_data_model(optimization_problem_interface_t<i_t, f_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 <typename i_t, typename f_t>
template <typename i_t, typename f_t, bool kHostOnly = false>
void populate_from_data_model_view(
optimization_problem_interface_t<i_t, f_t>* problem,
cuopt::mathematical_optimization::io::data_model_view_t<i_t, f_t>* data_model,
Expand Down Expand Up @@ -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<i_t, f_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<i_t, f_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<i_t, f_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<i_t, f_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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <cuopt/mathematical_optimization/pdlp/pdlp_hyper_params.cuh>
#include <cuopt/mathematical_optimization/pdlp/pdlp_warm_start_data.hpp>
#include <cuopt/mathematical_optimization/utilities/internals.hpp>
#include <memory>
#include <optional>
#include <raft/core/device_span.hpp>
#include <rmm/device_uvector.hpp>
Expand Down Expand Up @@ -371,8 +372,24 @@ class pdlp_solver_settings_t {
/** Initial pdlp iteration */
// TODO batch mode: tmp
std::optional<i_t> initial_pdlp_iteration_;
/** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves */
pdlp_warm_start_data_t<i_t, f_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_t<i_t, f_t>> pdlp_warm_start_data_;

/** Lazily allocate pdlp_warm_start_data_ and return it. Defined in a CUDA TU. */
pdlp_warm_start_data_t<i_t, f_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<i_t, f_t> pdlp_warm_start_data_view_;
/** CPU-backed warm start data (std::vector), used for remote execution on CPU-only hosts */
Expand Down
5 changes: 4 additions & 1 deletion cpp/src/grpc/client/cython_grpc_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ grpc_submit_result_t grpc_python_client_t::submit(
}

cuopt::mathematical_optimization::cpu_optimization_problem_t<int, double> cpu_problem;
cuopt::mathematical_optimization::populate_from_data_model_view(
// <int, double, /*kHostOnly=*/true>: 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<int, double, true>(
&cpu_problem, data_model, settings, nullptr);

const bool is_mip =
Expand Down
1 change: 1 addition & 0 deletions cpp/src/pdlp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions cpp/src/pdlp/optimization_problem.cu
Original file line number Diff line number Diff line change
Expand Up @@ -1637,4 +1637,44 @@ template CUOPT_EXPORT optimization_problem_t<int32_t, float>
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 <typename i_t, typename f_t>
void apply_warmstart_gpu_target(solver_settings_t<i_t, f_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<i_t, f_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<i_t, f_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<int, float>*, const raft::handle_t*);
#endif
#if MIP_INSTANTIATE_DOUBLE
template void apply_warmstart_gpu_target(solver_settings_t<int, double>*, const raft::handle_t*);
#endif

} // namespace cuopt::mathematical_optimization
40 changes: 17 additions & 23 deletions cpp/src/pdlp/solver_settings.cu
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ void pdlp_solver_settings_t<i_t, f_t>::set_pdlp_warm_start_data(
const rmm::device_uvector<i_t>& var_mapping,
const rmm::device_uvector<i_t>& 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) {
Expand Down Expand Up @@ -382,37 +385,28 @@ std::optional<i_t> pdlp_solver_settings_t<i_t, f_t>::get_initial_pdlp_iteration(
}

template <typename i_t, typename f_t>
const pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data()
const noexcept
{
return pdlp_warm_start_data_;
}

template <typename i_t, typename f_t>
pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data()
{
return pdlp_warm_start_data_;
}

template <typename i_t, typename f_t>
const cpu_pdlp_warm_start_data_t<i_t, f_t>&
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() const noexcept
pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_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<pdlp_warm_start_data_t<i_t, f_t>>();
}
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 <typename i_t, typename f_t>
cpu_pdlp_warm_start_data_t<i_t, f_t>&
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() noexcept
const pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data()
const noexcept
{
return cpu_pdlp_warm_start_data_;
return ensure_pdlp_warm_start_data();
}

template <typename i_t, typename f_t>
const pdlp_warm_start_data_view_t<i_t, f_t>&
pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data_view() const noexcept
pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_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
Expand Down
68 changes: 68 additions & 0 deletions cpp/src/pdlp/solver_settings_accessors.cpp
Original file line number Diff line number Diff line change
@@ -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 <cuopt/export.hpp>
#include <cuopt/mathematical_optimization/pdlp/solver_settings.hpp>

// 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 <mip_heuristics/mip_constants.hpp>

namespace cuopt::mathematical_optimization {

template <typename i_t, typename f_t>
const cpu_pdlp_warm_start_data_t<i_t, f_t>&
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() const noexcept
{
return cpu_pdlp_warm_start_data_;
}

template <typename i_t, typename f_t>
cpu_pdlp_warm_start_data_t<i_t, f_t>&
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() noexcept
{
return cpu_pdlp_warm_start_data_;
}

template <typename i_t, typename f_t>
const pdlp_warm_start_data_view_t<i_t, f_t>&
pdlp_solver_settings_t<i_t, f_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<int, float>&
pdlp_solver_settings_t<int, float>::get_cpu_pdlp_warm_start_data() const noexcept;
template CUOPT_EXPORT cpu_pdlp_warm_start_data_t<int, float>&
pdlp_solver_settings_t<int, float>::get_cpu_pdlp_warm_start_data() noexcept;
template CUOPT_EXPORT const pdlp_warm_start_data_view_t<int, float>&
pdlp_solver_settings_t<int, float>::get_pdlp_warm_start_data_view() const noexcept;
#endif

#if MIP_INSTANTIATE_DOUBLE
template CUOPT_EXPORT const cpu_pdlp_warm_start_data_t<int, double>&
pdlp_solver_settings_t<int, double>::get_cpu_pdlp_warm_start_data() const noexcept;
template CUOPT_EXPORT cpu_pdlp_warm_start_data_t<int, double>&
pdlp_solver_settings_t<int, double>::get_cpu_pdlp_warm_start_data() noexcept;
template CUOPT_EXPORT const pdlp_warm_start_data_view_t<int, double>&
pdlp_solver_settings_t<int, double>::get_pdlp_warm_start_data_view() const noexcept;
#endif

} // namespace cuopt::mathematical_optimization
Loading