Skip to content

Commit 907c4d5

Browse files
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 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
1 parent f6fe4bd commit 907c4d5

7 files changed

Lines changed: 200 additions & 76 deletions

File tree

cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,39 @@ void populate_from_mps_data_model(optimization_problem_interface_t<i_t, f_t>* pr
137137
}
138138
}
139139

140+
/**
141+
* @brief Move warm-start data into the form a GPU solve needs (H2D / view->device_uvector).
142+
*
143+
* Declared here, defined in libcuopt (optimization_problem.cu): it touches device memory,
144+
* so keeping it out-of-line is what lets CUDA-free consumers of this header link without
145+
* a CUDA runtime. Only call it with a real handle.
146+
*/
147+
template <typename i_t, typename f_t>
148+
void apply_warmstart_gpu_target(solver_settings_t<i_t, f_t>* solver_settings,
149+
const raft::handle_t* handle);
150+
151+
/**
152+
* @brief Move warm-start data into the form a CPU / remote solve needs.
153+
*
154+
* Host-only by construction. A CPU-only caller cannot be holding device-resident warm
155+
* start (there is no device to have populated it), so that case is rejected rather than
156+
* converted -- converting would require a D2H copy and thus CUDA.
157+
*/
158+
template <typename i_t, typename f_t>
159+
void apply_warmstart_cpu_target(solver_settings_t<i_t, f_t>* solver_settings)
160+
{
161+
auto& pdlp = solver_settings->get_pdlp_settings();
162+
163+
if (pdlp.get_cpu_pdlp_warm_start_data().is_populated()) { return; }
164+
165+
// Warmstart view (host spans from Cython) -> CPU backend: copy directly, no CUDA needed.
166+
if (solver_settings->get_pdlp_warm_start_data_view()
167+
.last_restart_duality_gap_dual_solution_.size() > 0) {
168+
pdlp.get_cpu_pdlp_warm_start_data() =
169+
cpu_pdlp_warm_start_data_t<i_t, f_t>(solver_settings->get_pdlp_warm_start_data_view());
170+
}
171+
}
172+
140173
/**
141174
* @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays.
142175
*
@@ -176,7 +209,7 @@ void adopt_from_mps_data_model(optimization_problem_interface_t<i_t, f_t>* probl
176209
* @param[in] solver_settings Optional solver settings (for warmstart data, GPU only)
177210
* @param[in] handle Optional RAFT handle (for warmstart data, GPU only)
178211
*/
179-
template <typename i_t, typename f_t>
212+
template <typename i_t, typename f_t, bool kHostOnly = false>
180213
void populate_from_data_model_view(
181214
optimization_problem_interface_t<i_t, f_t>* problem,
182215
cuopt::mathematical_optimization::io::data_model_view_t<i_t, f_t>* data_model,
@@ -209,57 +242,26 @@ void populate_from_data_model_view(
209242
problem->set_objective_scaling_factor(data_model->get_objective_scaling_factor());
210243
problem->set_objective_offset(data_model->get_objective_offset());
211244

212-
// Handle warmstart data with GPU↔CPU conversion if needed
245+
// Handle warmstart data with GPU<->CPU conversion if needed.
246+
//
247+
// Split into two helpers deliberately. The GPU direction is only reachable when
248+
// handle != nullptr, but a single inlined if/else instantiated BOTH directions into
249+
// every TU that includes this header -- which dragged convert_to_gpu_warmstart,
250+
// pdlp_warm_start_data_t(view, stream) and friends into the CUDA-free gRPC client.
251+
// apply_warmstart_gpu_target() is declared here and defined in libcuopt, so only
252+
// callers that actually pass a handle reference it.
253+
//
254+
// kHostOnly is a compile-time opt-out, not just a runtime one: `if constexpr` means a
255+
// host-only caller never *instantiates* the GPU branch, so it emits no reference to
256+
// apply_warmstart_gpu_target and needs no CUDA runtime to link.
213257
if (solver_settings != nullptr) {
214-
bool target_is_gpu = (handle != nullptr);
215-
216-
// Check which warmstart type is populated
217-
// Note: Python sets the VIEW (spans), so check both view and data for GPU warmstart
218-
// CPU warmstart is set directly in the data structure
219-
bool has_gpu_warmstart_view = (solver_settings->get_pdlp_warm_start_data_view()
220-
.last_restart_duality_gap_dual_solution_.size() > 0);
221-
bool has_gpu_warmstart_data =
222-
solver_settings->get_pdlp_settings().get_pdlp_warm_start_data().is_populated();
223-
bool has_cpu_warmstart =
224-
solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data().is_populated();
225-
226-
bool has_gpu_warmstart = has_gpu_warmstart_view || has_gpu_warmstart_data;
227-
228-
if (has_gpu_warmstart || has_cpu_warmstart) {
229-
if (target_is_gpu) {
230-
// Target is GPU backend
231-
if (has_gpu_warmstart_view) {
232-
// GPU warmstart from Python → GPU backend: copy view (spans) to data (device_uvectors)
233-
// Python sets the view (spans over cuDF), but solver needs device_uvectors
234-
pdlp_warm_start_data_t<i_t, f_t> pdlp_warm_start_data(
235-
solver_settings->get_pdlp_warm_start_data_view(), handle->get_stream());
236-
solver_settings->get_pdlp_settings().set_pdlp_warm_start_data(pdlp_warm_start_data);
237-
} else if (has_gpu_warmstart_data) {
238-
// GPU warmstart from C++ API → GPU backend: data already set, nothing to do
239-
// The device_uvectors are already populated in the settings
240-
} else {
241-
// CPU warmstart → GPU backend: convert H2D
242-
pdlp_warm_start_data_t<i_t, f_t> gpu_warmstart = convert_to_gpu_warmstart(
243-
solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data(),
244-
handle->get_stream());
245-
solver_settings->get_pdlp_settings().set_pdlp_warm_start_data(gpu_warmstart);
246-
}
258+
if constexpr (kHostOnly) {
259+
apply_warmstart_cpu_target(solver_settings);
260+
} else {
261+
if (handle != nullptr) {
262+
apply_warmstart_gpu_target(solver_settings, handle);
247263
} else {
248-
// Target is CPU backend (remote execution)
249-
if (has_cpu_warmstart) {
250-
// CPU warmstart → CPU backend: data already in correct form, nothing to do
251-
} else if (has_gpu_warmstart_view) {
252-
// Warmstart view (host spans from Cython) → CPU backend: copy directly, no CUDA needed
253-
solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data() =
254-
cpu_pdlp_warm_start_data_t<i_t, f_t>(solver_settings->get_pdlp_warm_start_data_view());
255-
} else {
256-
// GPU warmstart data (device_uvectors) → CPU backend: convert D2H
257-
auto& gpu_ws = solver_settings->get_pdlp_settings().get_pdlp_warm_start_data();
258-
cpu_pdlp_warm_start_data_t<i_t, f_t> cpu_warmstart =
259-
convert_to_cpu_warmstart(gpu_ws, gpu_ws.current_primal_solution_.stream());
260-
solver_settings->get_pdlp_settings().get_cpu_pdlp_warm_start_data() =
261-
std::move(cpu_warmstart);
262-
}
264+
apply_warmstart_cpu_target(solver_settings);
263265
}
264266
}
265267
}

cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <cuopt/mathematical_optimization/pdlp/pdlp_hyper_params.cuh>
1414
#include <cuopt/mathematical_optimization/pdlp/pdlp_warm_start_data.hpp>
1515
#include <cuopt/mathematical_optimization/utilities/internals.hpp>
16+
#include <memory>
1617
#include <optional>
1718
#include <raft/core/device_span.hpp>
1819
#include <rmm/device_uvector.hpp>
@@ -371,8 +372,24 @@ class pdlp_solver_settings_t {
371372
/** Initial pdlp iteration */
372373
// TODO batch mode: tmp
373374
std::optional<i_t> initial_pdlp_iteration_;
374-
/** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves */
375-
pdlp_warm_start_data_t<i_t, f_t> pdlp_warm_start_data_;
375+
/** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves.
376+
*
377+
* Held by shared_ptr rather than by value so that constructing a settings object needs
378+
* no CUDA. pdlp_warm_start_data_t owns nine rmm::device_uvector, and its default ctor is
379+
* out-of-line in a CUDA TU (device_uvector has no default ctor -- it needs a stream, and
380+
* building even a zero-size one calls cudaGetDevice). By value, that made every consumer
381+
* of solver_settings_t -- including the CUDA-free gRPC client -- depend on libcuopt.
382+
*
383+
* shared_ptr specifically, not unique_ptr: shared_ptr type-erases its deleter into the
384+
* control block at construction, so a host-only TU can copy and destroy this member
385+
* without the complete type. unique_ptr would just move the problem to the destructor.
386+
*
387+
* Null until a GPU consumer first needs it; use ensure_pdlp_warm_start_data().
388+
*/
389+
mutable std::shared_ptr<pdlp_warm_start_data_t<i_t, f_t>> pdlp_warm_start_data_;
390+
391+
/** Lazily allocate pdlp_warm_start_data_ and return it. Defined in a CUDA TU. */
392+
pdlp_warm_start_data_t<i_t, f_t>& ensure_pdlp_warm_start_data() const;
376393
/** Warm start data as spans over external memory, used by Cython/Python interface */
377394
pdlp_warm_start_data_view_t<i_t, f_t> pdlp_warm_start_data_view_;
378395
/** CPU-backed warm start data (std::vector), used for remote execution on CPU-only hosts */

cpp/src/grpc/client/cython_grpc_client.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ grpc_submit_result_t grpc_python_client_t::submit(
109109
}
110110

111111
cuopt::mathematical_optimization::cpu_optimization_problem_t<int, double> cpu_problem;
112-
cuopt::mathematical_optimization::populate_from_data_model_view(
112+
// <int, double, /*kHostOnly=*/true>: this is a remote client, so the GPU warm-start
113+
// path is unreachable here. Selecting it explicitly keeps the device conversions from
114+
// being instantiated into cuopt_client.
115+
cuopt::mathematical_optimization::populate_from_data_model_view<int, double, true>(
113116
&cpu_problem, data_model, settings, nullptr);
114117

115118
const bool is_mip =

cpp/src/pdlp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Core LP files always included
77
set(LP_CORE_FILES
88
${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu
9+
${CMAKE_CURRENT_SOURCE_DIR}/solver_settings_accessors.cpp
910
${CMAKE_CURRENT_SOURCE_DIR}/optimization_problem.cu
1011
${CMAKE_CURRENT_SOURCE_DIR}/cpu_optimization_problem.cpp
1112
${CMAKE_CURRENT_SOURCE_DIR}/cpu_optimization_problem_to_gpu.cpp

cpp/src/pdlp/optimization_problem.cu

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,4 +1637,43 @@ template CUOPT_EXPORT optimization_problem_t<int32_t, float>
16371637
rmm::cuda_stream_view) const;
16381638
#endif
16391639

1640+
// GPU-target warm-start handling, declared in optimization_problem_utils.hpp.
1641+
//
1642+
// Defined here rather than inline in the header so that CUDA-free consumers of that
1643+
// header (the gRPC client in cuopt_client) never instantiate the device conversions.
1644+
template <typename i_t, typename f_t>
1645+
void apply_warmstart_gpu_target(solver_settings_t<i_t, f_t>* solver_settings,
1646+
const raft::handle_t* handle)
1647+
{
1648+
auto& pdlp = solver_settings->get_pdlp_settings();
1649+
1650+
const bool has_view = (solver_settings->get_pdlp_warm_start_data_view()
1651+
.last_restart_duality_gap_dual_solution_.size() > 0);
1652+
const bool has_device_data = pdlp.get_pdlp_warm_start_data().is_populated();
1653+
const bool has_host_data = pdlp.get_cpu_pdlp_warm_start_data().is_populated();
1654+
1655+
if (!has_view && !has_device_data && !has_host_data) { return; }
1656+
1657+
if (has_view) {
1658+
// Warmstart from Python (spans over cuDF) -> solver needs device_uvectors.
1659+
pdlp_warm_start_data_t<i_t, f_t> warm_start(solver_settings->get_pdlp_warm_start_data_view(),
1660+
handle->get_stream());
1661+
pdlp.set_pdlp_warm_start_data(warm_start);
1662+
} else if (has_device_data) {
1663+
// Already device-resident from the C++ API: nothing to do.
1664+
} else {
1665+
// Host warmstart -> GPU backend: convert H2D.
1666+
pdlp_warm_start_data_t<i_t, f_t> warm_start =
1667+
convert_to_gpu_warmstart(pdlp.get_cpu_pdlp_warm_start_data(), handle->get_stream());
1668+
pdlp.set_pdlp_warm_start_data(warm_start);
1669+
}
1670+
}
1671+
1672+
#if MIP_INSTANTIATE_FLOAT
1673+
template void apply_warmstart_gpu_target(solver_settings_t<int, float>*, const raft::handle_t*);
1674+
#endif
1675+
#if MIP_INSTANTIATE_DOUBLE
1676+
template void apply_warmstart_gpu_target(solver_settings_t<int, double>*, const raft::handle_t*);
1677+
#endif
1678+
16401679
} // namespace cuopt::mathematical_optimization

cpp/src/pdlp/solver_settings.cu

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ void pdlp_solver_settings_t<i_t, f_t>::set_pdlp_warm_start_data(
9595
const rmm::device_uvector<i_t>& var_mapping,
9696
const rmm::device_uvector<i_t>& constraint_mapping)
9797
{
98-
pdlp_warm_start_data_ = std::move(pdlp_warm_start_data_view);
98+
// pdlp_warm_start_data_ is a shared_ptr now (see solver_settings.hpp); alias it so the
99+
// device code below reads unchanged.
100+
auto& pdlp_warm_start_data_ = ensure_pdlp_warm_start_data();
101+
pdlp_warm_start_data_ = std::move(pdlp_warm_start_data_view);
99102

100103
// A var_mapping was given
101104
if (var_mapping.size() != 0) {
@@ -382,37 +385,28 @@ std::optional<i_t> pdlp_solver_settings_t<i_t, f_t>::get_initial_pdlp_iteration(
382385
}
383386

384387
template <typename i_t, typename f_t>
385-
const pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data()
386-
const noexcept
387-
{
388-
return pdlp_warm_start_data_;
389-
}
390-
391-
template <typename i_t, typename f_t>
392-
pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data()
393-
{
394-
return pdlp_warm_start_data_;
395-
}
396-
397-
template <typename i_t, typename f_t>
398-
const cpu_pdlp_warm_start_data_t<i_t, f_t>&
399-
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() const noexcept
388+
pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::ensure_pdlp_warm_start_data()
389+
const
400390
{
401-
return cpu_pdlp_warm_start_data_;
391+
if (!pdlp_warm_start_data_) {
392+
pdlp_warm_start_data_ = std::make_shared<pdlp_warm_start_data_t<i_t, f_t>>();
393+
}
394+
return *pdlp_warm_start_data_;
402395
}
403396

397+
// These two live here rather than in solver_settings_accessors.cpp: they may have to
398+
// allocate the device-backed warm-start object, so they need CUDA.
404399
template <typename i_t, typename f_t>
405-
cpu_pdlp_warm_start_data_t<i_t, f_t>&
406-
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() noexcept
400+
const pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data()
401+
const noexcept
407402
{
408-
return cpu_pdlp_warm_start_data_;
403+
return ensure_pdlp_warm_start_data();
409404
}
410405

411406
template <typename i_t, typename f_t>
412-
const pdlp_warm_start_data_view_t<i_t, f_t>&
413-
pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data_view() const noexcept
407+
pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data()
414408
{
415-
return pdlp_warm_start_data_view_;
409+
return ensure_pdlp_warm_start_data();
416410
}
417411

418412
#if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* clang-format off */
2+
/*
3+
* SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
/* clang-format on */
7+
8+
// Warm-start accessors of pdlp_solver_settings_t, split out of solver_settings.cu.
9+
//
10+
// These are trivial `return member_;` getters -- they hand back a reference and emit no
11+
// device code, even where the referent is a GPU type. The gRPC client needs them, so they
12+
// build into the CUDA-free cuopt_client library while the rest of the class (which does
13+
// real thrust/rmm work) stays in solver_settings.cu.
14+
//
15+
// Only these members are instantiated below, deliberately NOT `template class`: the class
16+
// holds a pdlp_warm_start_data_t, so instantiating all of it here would pull in device
17+
// ctor/dtor code that belongs in the CUDA TU.
18+
19+
#include <cuopt/export.hpp>
20+
#include <cuopt/mathematical_optimization/pdlp/solver_settings.hpp>
21+
22+
// Required: the explicit instantiations below are guarded on MIP_INSTANTIATE_* /
23+
// PDLP_INSTANTIATE_*. Without this header those macros are undefined, the guards
24+
// evaluate false, and this TU silently compiles to zero symbols.
25+
#include <mip_heuristics/mip_constants.hpp>
26+
27+
namespace cuopt::mathematical_optimization {
28+
29+
template <typename i_t, typename f_t>
30+
const cpu_pdlp_warm_start_data_t<i_t, f_t>&
31+
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() const noexcept
32+
{
33+
return cpu_pdlp_warm_start_data_;
34+
}
35+
36+
template <typename i_t, typename f_t>
37+
cpu_pdlp_warm_start_data_t<i_t, f_t>&
38+
pdlp_solver_settings_t<i_t, f_t>::get_cpu_pdlp_warm_start_data() noexcept
39+
{
40+
return cpu_pdlp_warm_start_data_;
41+
}
42+
43+
template <typename i_t, typename f_t>
44+
const pdlp_warm_start_data_view_t<i_t, f_t>&
45+
pdlp_solver_settings_t<i_t, f_t>::get_pdlp_warm_start_data_view() const noexcept
46+
{
47+
return pdlp_warm_start_data_view_;
48+
}
49+
50+
#if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT
51+
template CUOPT_EXPORT const cpu_pdlp_warm_start_data_t<int, float>&
52+
pdlp_solver_settings_t<int, float>::get_cpu_pdlp_warm_start_data() const noexcept;
53+
template CUOPT_EXPORT cpu_pdlp_warm_start_data_t<int, float>&
54+
pdlp_solver_settings_t<int, float>::get_cpu_pdlp_warm_start_data() noexcept;
55+
template CUOPT_EXPORT const pdlp_warm_start_data_view_t<int, float>&
56+
pdlp_solver_settings_t<int, float>::get_pdlp_warm_start_data_view() const noexcept;
57+
#endif
58+
59+
#if MIP_INSTANTIATE_DOUBLE
60+
template CUOPT_EXPORT const cpu_pdlp_warm_start_data_t<int, double>&
61+
pdlp_solver_settings_t<int, double>::get_cpu_pdlp_warm_start_data() const noexcept;
62+
template CUOPT_EXPORT cpu_pdlp_warm_start_data_t<int, double>&
63+
pdlp_solver_settings_t<int, double>::get_cpu_pdlp_warm_start_data() noexcept;
64+
template CUOPT_EXPORT const pdlp_warm_start_data_view_t<int, double>&
65+
pdlp_solver_settings_t<int, double>::get_pdlp_warm_start_data_view() const noexcept;
66+
#endif
67+
68+
} // namespace cuopt::mathematical_optimization

0 commit comments

Comments
 (0)