Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0f5ae25
refactor: split host-only members out of CUDA translation units
ramakrishnap-nv Aug 25, 2026
386b883
refactor: make to_optimization_problem a free function
ramakrishnap-nv Aug 25, 2026
57545af
refactor: keep the GPU warm-start path out of host-only translation u…
ramakrishnap-nv Aug 25, 2026
5f80e9e
Merge branch 'main' into split/1-host-device-tus
ramakrishnap-nv Aug 28, 2026
7e5737f
test: add gtest coverage for the host/device solver-settings and CPU …
ramakrishnap-nv Aug 28, 2026
e55ebec
test: cover the <int, float> solver_settings_t GPU-facing instantiations
ramakrishnap-nv Aug 28, 2026
c92008a
fix: unbreak conda-cpp-build after macro-comma break in solver_settin…
ramakrishnap-nv Aug 28, 2026
4cd9ff1
fix: revert <int, float> solver_settings_t tests, float is never inst…
ramakrishnap-nv Aug 28, 2026
c77ffde
Merge remote-tracking branch 'origin/main' into pr-1801
ramakrishnap-nv Aug 28, 2026
fd9e28d
Merge remote-tracking branch 'origin/main' into split/1-host-device-tus
ramakrishnap-nv Aug 31, 2026
020cf6f
refactor: move the unsupported-feature predicate out of the client's …
ramakrishnap-nv Aug 31, 2026
e10b5b3
Merge remote-tracking branch 'origin/main' into split/1-host-device-tus
ramakrishnap-nv Aug 31, 2026
7de9056
refactor: pass the problem and settings into should_disable_unsupported
ramakrishnap-nv Aug 31, 2026
781c544
Merge remote-tracking branch 'origin/split/1-host-device-tus' into HEAD
ramakrishnap-nv Sep 1, 2026
0f0a506
Merge remote-tracking branch 'origin/split/2-devirtualize-to-optimiza…
ramakrishnap-nv Sep 1, 2026
9745294
Merge remote-tracking branch 'origin/main' into HEAD
ramakrishnap-nv Sep 2, 2026
d1c11c4
Merge remote-tracking branch 'origin/split/2-devirtualize-to-optimiza…
ramakrishnap-nv Sep 2, 2026
63172d0
docs: trim redundant comments and drop the orphaned doxygen block
ramakrishnap-nv Sep 2, 2026
c4b4f01
Merge branch 'main' into split/2-devirtualize-to-optimization-problem
ramakrishnap-nv Sep 2, 2026
144b308
Merge remote-tracking branch 'origin/main' into HEAD
ramakrishnap-nv Sep 8, 2026
f817d73
Merge remote-tracking branch 'origin/split/2-devirtualize-to-optimiza…
ramakrishnap-nv Sep 8, 2026
12e2613
Merge remote-tracking branch 'origin/main' into HEAD
ramakrishnap-nv Sep 8, 2026
b809784
fix: instantiate the float warm-start helpers in PDLP-only builds
ramakrishnap-nv Sep 8, 2026
4bda7e8
fix: match the warm-start float guard to the accessors it calls
ramakrishnap-nv Sep 8, 2026
33e3338
refactor: name the warm-start helpers after what they copy and where
ramakrishnap-nv Sep 8, 2026
0199465
Merge remote-tracking branch 'origin/main' into split/3-settings-host…
ramakrishnap-nv Sep 8, 2026
2212af6
Merge branch 'main' into split/3-settings-host-constructible
ramakrishnap-nv Sep 9, 2026
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,54 @@ void populate_from_mps_data_model(optimization_problem_interface_t<i_t, f_t>* pr
}
}

/**
* @brief Copy 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 copy_warmstart_data_to_device(solver_settings_t<i_t, f_t>& solver_settings,
const raft::handle_t* handle);

/**
* @brief Copy 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 <typename i_t, typename f_t>
void copy_warmstart_data_to_host(solver_settings_t<i_t, f_t>& solver_settings);

/**
* @brief Copy a host-span warm-start view into the form a CPU / remote solve needs.
*
* 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
* copy_warmstart_data_to_host() instead; only a kHostOnly caller, which by construction
* has no device to have populated it, may use this one.
*/
template <typename i_t, typename f_t>
void copy_warmstart_view_to_host(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());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays.
*
Expand Down Expand Up @@ -176,7 +224,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 +257,18 @@ 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
// `if constexpr`, not a runtime branch: a kHostOnly caller never *instantiates* the GPU
// helper, so it emits no reference to it 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) {
copy_warmstart_view_to_host(*solver_settings);
} else {
if (handle != nullptr) {
copy_warmstart_data_to_device(*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);
}
// 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.
copy_warmstart_data_to_host(*solver_settings);
}
}
}
Expand Down
3 changes: 2 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,8 @@ 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(
// kHostOnly=true: remote client, so the GPU warm-start path is unreachable here.
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
78 changes: 78 additions & 0 deletions cpp/src/pdlp/optimization_problem.cu
Original file line number Diff line number Diff line change
Expand Up @@ -1637,4 +1637,82 @@ 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 copy_warmstart_data_to_device(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);
}
}

// Null-handle CPU-target warm-start handling for callers that do have a device.
//
// Mirrors copy_warmstart_view_to_host() 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 <typename i_t, typename f_t>
void copy_warmstart_data_to_host(solver_settings_t<i_t, f_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<i_t, f_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());
}
}
Comment thread
ramakrishnap-nv marked this conversation as resolved.

// MIP_INSTANTIATE_FLOAT, not the wider MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT used
// elsewhere: both helpers call solver_settings_t<i_t, f_t>::get_pdlp_settings() and
// ::get_pdlp_warm_start_data_view(), which math_optimization/solver_settings.cpp only
// instantiates under MIP_INSTANTIATE_FLOAT. Widening the guard here without widening it
// there leaves libcuopt.so with undefined references to those accessors.
#if MIP_INSTANTIATE_FLOAT
template CUOPT_EXPORT void copy_warmstart_data_to_device(solver_settings_t<int, float>&,
const raft::handle_t*);
template CUOPT_EXPORT void copy_warmstart_data_to_host(solver_settings_t<int, float>&);
#endif
#if MIP_INSTANTIATE_DOUBLE
template CUOPT_EXPORT void copy_warmstart_data_to_device(solver_settings_t<int, double>&,
const raft::handle_t*);
template CUOPT_EXPORT void copy_warmstart_data_to_host(solver_settings_t<int, double>&);
#endif

} // namespace cuopt::mathematical_optimization
21 changes: 0 additions & 21 deletions cpp/src/pdlp/solver_settings.cu
Original file line number Diff line number Diff line change
Expand Up @@ -394,27 +394,6 @@ pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_war
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
{
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 class CUOPT_EXPORT pdlp_solver_settings_t<int, float>;
#endif
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