Skip to content

Commit 57545af

Browse files
refactor: keep the GPU warm-start path out of host-only translation units
populate_from_data_model_view() inlined both the GPU and CPU warm-start directions in one if/else. The GPU direction is only reachable when handle != nullptr, but the compiler instantiated both branches into every translation unit that included the header -- dragging convert_to_gpu_warmstart, convert_to_cpu_warmstart and pdlp_warm_start_data_t(view, stream) along with them. Split into three pieces: apply_warmstart_gpu_target() declared here, defined in libcuopt apply_warmstart_cpu_target_with_device() declared here, defined in libcuopt 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 still instantiate both and leave the undefined symbols behind. Note the two CPU-target variants. A kHostOnly caller has no device, so it cannot be holding device-resident warm start and the host-only helper suffices. A normal caller passing handle == nullptr *does* have a device (cython_solve.cu:181 does exactly this), so its warm start may be device-resident and needs the D2H convert_to_cpu_warmstart that the with_device variant performs. Collapsing these two into one host-only helper silently drops the caller's warm start. Also moves the trivial warm-start accessors -- which only hand back a reference and allocate nothing -- into solver_settings_accessors.cpp so host-only consumers can resolve them without pulling in the CUDA translation unit. Deliberately NOT included: making solver settings constructible without CUDA. That needs pdlp_warm_start_data_t to stop being a by-value member, and every mechanism for that either breaks the deep-copy semantics settings rely on (they are copied per-solver in run_concurrent and per-batch in batch mode) or requires hand-enumerating every scalar field in a copy constructor. It belongs with the header-separation work, which is already restructuring these types. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
1 parent 386b883 commit 57545af

6 files changed

Lines changed: 215 additions & 72 deletions

File tree

cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,54 @@ 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, including a
153+
* device-to-host copy when the warm start is device-resident.
154+
*
155+
* Declared here, defined in libcuopt (optimization_problem.cu). This is the null-handle
156+
* path for a normal (non host-only) caller: it has a device, so its settings may hold
157+
* device_uvector-backed warm start that must be brought to host before a remote solve.
158+
*/
159+
template <typename i_t, typename f_t>
160+
void apply_warmstart_cpu_target_with_device(solver_settings_t<i_t, f_t>* solver_settings);
161+
162+
/**
163+
* @brief Move warm-start data into the form a CPU / remote solve needs, host paths only.
164+
*
165+
* Handles the two cases reachable without a device: warm start already in host form
166+
* (nothing to do), and a warm-start view over host spans (copy it).
167+
*
168+
* Deliberately does NOT handle device-resident warm start -- that needs a D2H copy and
169+
* therefore CUDA. Callers that might be holding device data must use
170+
* apply_warmstart_cpu_target_with_device() instead; only a kHostOnly caller, which by
171+
* construction has no device to have populated it, may use this one.
172+
*/
173+
template <typename i_t, typename f_t>
174+
void apply_warmstart_cpu_target(solver_settings_t<i_t, f_t>* solver_settings)
175+
{
176+
auto& pdlp = solver_settings->get_pdlp_settings();
177+
178+
if (pdlp.get_cpu_pdlp_warm_start_data().is_populated()) { return; }
179+
180+
// Warmstart view (host spans from Cython) -> CPU backend: copy directly, no CUDA needed.
181+
if (solver_settings->get_pdlp_warm_start_data_view()
182+
.last_restart_duality_gap_dual_solution_.size() > 0) {
183+
pdlp.get_cpu_pdlp_warm_start_data() =
184+
cpu_pdlp_warm_start_data_t<i_t, f_t>(solver_settings->get_pdlp_warm_start_data_view());
185+
}
186+
}
187+
140188
/**
141189
* @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays.
142190
*
@@ -176,7 +224,7 @@ void adopt_from_mps_data_model(optimization_problem_interface_t<i_t, f_t>* probl
176224
* @param[in] solver_settings Optional solver settings (for warmstart data, GPU only)
177225
* @param[in] handle Optional RAFT handle (for warmstart data, GPU only)
178226
*/
179-
template <typename i_t, typename f_t>
227+
template <typename i_t, typename f_t, bool kHostOnly = false>
180228
void populate_from_data_model_view(
181229
optimization_problem_interface_t<i_t, f_t>* problem,
182230
cuopt::mathematical_optimization::io::data_model_view_t<i_t, f_t>* data_model,
@@ -209,57 +257,28 @@ void populate_from_data_model_view(
209257
problem->set_objective_scaling_factor(data_model->get_objective_scaling_factor());
210258
problem->set_objective_offset(data_model->get_objective_offset());
211259

212-
// Handle warmstart data with GPU↔CPU conversion if needed
260+
// Handle warmstart data with GPU<->CPU conversion if needed.
261+
//
262+
// Split into two helpers deliberately. The GPU direction is only reachable when
263+
// handle != nullptr, but a single inlined if/else instantiated BOTH directions into
264+
// every TU that includes this header -- which dragged convert_to_gpu_warmstart,
265+
// pdlp_warm_start_data_t(view, stream) and friends into the CUDA-free gRPC client.
266+
// apply_warmstart_gpu_target() is declared here and defined in libcuopt, so only
267+
// callers that actually pass a handle reference it.
268+
//
269+
// kHostOnly is a compile-time opt-out, not just a runtime one: `if constexpr` means a
270+
// host-only caller never *instantiates* the GPU branch, so it emits no reference to
271+
// apply_warmstart_gpu_target and needs no CUDA runtime to link.
213272
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-
}
273+
if constexpr (kHostOnly) {
274+
apply_warmstart_cpu_target(solver_settings);
275+
} else {
276+
if (handle != nullptr) {
277+
apply_warmstart_gpu_target(solver_settings, handle);
247278
} 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-
}
279+
// No handle, but this caller has a device: the warm start may be device-resident,
280+
// so it needs the variant that can copy it back to host.
281+
apply_warmstart_cpu_target_with_device(solver_settings);
263282
}
264283
}
265284
}

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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,4 +1637,77 @@ 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+
// Null-handle CPU-target warm-start handling for callers that do have a device.
1673+
//
1674+
// Mirrors apply_warmstart_cpu_target() but adds the case that one cannot handle: warm
1675+
// start already sitting in device_uvectors, which needs a D2H copy before a remote solve.
1676+
// Dropping this silently loses a user's warm start on the
1677+
// populate_from_data_model_view(..., handle=nullptr) path (see cython_solve.cu).
1678+
template <typename i_t, typename f_t>
1679+
void apply_warmstart_cpu_target_with_device(solver_settings_t<i_t, f_t>* solver_settings)
1680+
{
1681+
auto& pdlp = solver_settings->get_pdlp_settings();
1682+
1683+
// Already in host form.
1684+
if (pdlp.get_cpu_pdlp_warm_start_data().is_populated()) { return; }
1685+
1686+
// Warm-start view (host spans from Cython) -> CPU backend: copy directly, no CUDA needed.
1687+
if (solver_settings->get_pdlp_warm_start_data_view()
1688+
.last_restart_duality_gap_dual_solution_.size() > 0) {
1689+
pdlp.get_cpu_pdlp_warm_start_data() =
1690+
cpu_pdlp_warm_start_data_t<i_t, f_t>(solver_settings->get_pdlp_warm_start_data_view());
1691+
return;
1692+
}
1693+
1694+
// Device-resident warm start -> CPU backend: convert D2H.
1695+
auto& gpu_ws = pdlp.get_pdlp_warm_start_data();
1696+
if (gpu_ws.is_populated()) {
1697+
pdlp.get_cpu_pdlp_warm_start_data() =
1698+
convert_to_cpu_warmstart(gpu_ws, gpu_ws.current_primal_solution_.stream());
1699+
}
1700+
}
1701+
1702+
#if MIP_INSTANTIATE_FLOAT
1703+
template CUOPT_EXPORT void apply_warmstart_gpu_target(solver_settings_t<int, float>*,
1704+
const raft::handle_t*);
1705+
template CUOPT_EXPORT void apply_warmstart_cpu_target_with_device(solver_settings_t<int, float>*);
1706+
#endif
1707+
#if MIP_INSTANTIATE_DOUBLE
1708+
template CUOPT_EXPORT void apply_warmstart_gpu_target(solver_settings_t<int, double>*,
1709+
const raft::handle_t*);
1710+
template CUOPT_EXPORT void apply_warmstart_cpu_target_with_device(solver_settings_t<int, double>*);
1711+
#endif
1712+
16401713
} // namespace cuopt::mathematical_optimization

cpp/src/pdlp/solver_settings.cu

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -394,27 +394,6 @@ pdlp_warm_start_data_t<i_t, f_t>& pdlp_solver_settings_t<i_t, f_t>::get_pdlp_war
394394
return pdlp_warm_start_data_;
395395
}
396396

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
400-
{
401-
return cpu_pdlp_warm_start_data_;
402-
}
403-
404-
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
407-
{
408-
return cpu_pdlp_warm_start_data_;
409-
}
410-
411-
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
414-
{
415-
return pdlp_warm_start_data_view_;
416-
}
417-
418397
#if MIP_INSTANTIATE_FLOAT || PDLP_INSTANTIATE_FLOAT
419398
template class CUOPT_EXPORT pdlp_solver_settings_t<int, float>;
420399
#endif
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)