Skip to content

refactor: make solver settings constructible without CUDA - #1803

Draft
ramakrishnap-nv wants to merge 1 commit into
split/2-devirtualize-to-optimization-problemfrom
split/3-settings-host-constructible
Draft

refactor: make solver settings constructible without CUDA#1803
ramakrishnap-nv wants to merge 1 commit into
split/2-devirtualize-to-optimization-problemfrom
split/3-settings-host-constructible

Conversation

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator

3 of 4 toward a CUDA-free client library. Stacked on #1802.

This is the subtlest PR of the four — two small changes, each resting on a specific language guarantee. Worth reading closely; that's why it's separated out.

1. Warm-start data behind a shared_ptr

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 TU — device_uvector has no default ctor (it needs a stream), and constructing even a zero-size one calls cudaGetDevice. So merely constructing settings pulled in libcuopt.

Now held by shared_ptr, allocated lazily.

Why shared_ptr and not unique_ptr: shared_ptr type-erases its deleter into the control block at construction, so a host-only TU can copy and destroy the member without the complete type. unique_ptr bakes the deleter into the type, which would just move the problem from the constructor to the destructor.

The ~88 device-side uses inside set_pdlp_warm_start_data() are untouched — a local reference alias keeps that code reading exactly as before.

2. Compile-time opt-out of the GPU warm-start path

populate_from_data_model_view() inlined both directions in one if/else. The GPU branch is only reachable when handle != nullptr, but the compiler instantiated both into every TU including the header, dragging in convert_to_gpu_warmstart, convert_to_cpu_warmstart and pdlp_warm_start_data_t(view, stream).

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:

if constexpr (kHostOnly) {
  apply_warmstart_cpu_target(solver_settings);
} else {
  if (handle != nullptr) { apply_warmstart_gpu_target(solver_settings, handle); }
  else                   { apply_warmstart_cpu_target(solver_settings); }
}

Why if constexpr and not a runtime if: the goal is that a host-only caller never instantiates the GPU branch, so it emits no reference to it. A runtime branch would still instantiate both and leave the undefined symbols. This one change removed 5 of the 6 GPU references from the client.

Also moves the warm-start accessors that need no allocation into solver_settings_accessors.cpp, leaving the CUDA TU with only members that do.

Testing

Full build + 126 test binaries, 0 errors. 111/125 pass; the 14 failures are cudaErrorUnknown from a locally wedged nvidia_uvm, identical on unmodified main.

🤖 Generated with Claude Code

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>
@copy-pr-bot

copy-pr-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@ramakrishnap-nv

Copy link
Copy Markdown
Collaborator Author

/ok to test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant