diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index b375cc4c56..c2dc69a8ba 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -550,11 +550,12 @@ if (BUILD_TESTS) endif () set(CUOPT_SRC_FILES) +set(CUOPT_CLIENT_SRC_FILES) set(MPS_FAST_SRC_FILES) add_subdirectory(src) if (HOST_LINEINFO) - set_source_files_properties(${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") + set_source_files_properties(${CUOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") endif () # Needed for the fast MPS parser, available on all x86-64-v3 compliant x86 CPUs (essentially since Haswell ~2013) @@ -567,11 +568,11 @@ endif () # TODO: figure out a set of flags for ARM that fits the range of CPUs we wish to support (neoverse?) # NEON should be universal on aarch64 and enough for our purposes (parsing) though -# Apply -UNDEBUG only to solver source files (not gRPC infrastructure). -# Must happen before gRPC files are appended to CUOPT_SRC_FILES. +# Apply -UNDEBUG only to solver and parser source files (not gRPC infrastructure). +# Must happen before gRPC files are appended to CUOPT_CLIENT_SRC_FILES. # Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO). if (DEFINE_ASSERT) - set_property(SOURCE ${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + set_property(SOURCE ${CUOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG") endif () @@ -598,9 +599,7 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/client/grpc_client.cpp src/grpc/client/grpc_client_env.cpp src/grpc/client/cython_grpc_client.cpp - src/grpc/client/solve_remote.cpp ) - # Routing (VRP) arm: everything that depends on the routing engine. Kept as # its own list so a routing-only gRPC client can be split out of the # cuopt_grpc component without moving code around again. @@ -616,7 +615,17 @@ if (NOT SKIP_GRPC_BUILD) if (CUOPT_ENABLE_GRPC_ROUTING) list(APPEND GRPC_INFRA_FILES ${GRPC_ROUTING_FILES}) endif () - list(APPEND CUOPT_SRC_FILES ${GRPC_INFRA_FILES}) + + # Both arms are CUDA-free -- the routing mappers reference no raft/rmm/thrust + # either -- so the wire protocol and clients build into cuopt_client, shared by + # libcuopt, cuopt_grpc_server and the Python client extensions. One mapper + # implementation, not a client-side fork of it. + list(APPEND CUOPT_CLIENT_SRC_FILES ${GRPC_INFRA_FILES}) + + # solve_remote.cpp is the local-vs-remote dispatcher: it calls into the GPU + # solver, so it stays in cuopt_objs rather than moving down to cuopt_client. + list(APPEND CUOPT_SRC_FILES src/grpc/client/solve_remote.cpp) + list(APPEND GRPC_INFRA_FILES src/grpc/client/solve_remote.cpp) # Always keep NDEBUG defined for gRPC infrastructure files so that abseil # headers inline Mutex::Dtor() instead of emitting an external call. @@ -631,6 +640,120 @@ if (NOT SKIP_GRPC_BUILD) APPEND PROPERTY COMPILE_OPTIONS "$<$:-fvisibility=default>") endif (NOT SKIP_GRPC_BUILD) +# ################################################################################################## +# - cuopt_client - CPU-only support library ---------------------------------------------------------- +# +# Holds the host-side problem representation (parsers, data_model_view, mps_data_model, +# writers), the gRPC wire protocol (generated protos + mappers), and the gRPC client. +# None of it touches CUDA, so this library links no CUDA runtime. +# +# It exists so the Python extension modules that never call into the GPU -- data_model, +# solver_settings, io, and the gRPC client -- can link something other than libcuopt.so, +# which is what makes a GPU-free client install possible. libcuopt and cuopt_grpc_server +# both link it, so there is exactly one implementation of the mappers, not a client fork. +# +# LANGUAGES is deliberately not CUDA here: adding a .cu file to CUOPT_CLIENT_SRC_FILES +# should fail loudly rather than quietly reintroduce a CUDA dependency. +# Built as an OBJECT library first, mirroring cuopt_objs/cuopt. The shared library below +# keeps hidden visibility and exports only the curated CUOPT_EXPORT surface, while +# cuopt_static (for internal tests) links the objects directly -- internal symbols such as +# the fast MPS parser's mps_phase_registry_t are not exported, and the internal test +# binaries need them. +add_library(cuopt_client_objs OBJECT ${CUOPT_CLIENT_SRC_FILES}) +# NOTE: default visibility, deliberately unlike cuopt_objs. +# +# cuopt_objs can hide everything not marked CUOPT_EXPORT because libcuopt has a curated +# public C++ API. cuopt_client is different: it was carved out of the *internals*, so +# libcuopt itself depends on ~214 of its symbols (the whole cpu_optimization_problem_t / +# data_model_view_t / mps_data_model_t / grpc_client_t surface). Those are internal +# cross-library references, not a public API, and hiding them makes libcuopt.so fail to +# load with e.g. "undefined symbol: grpc_client_t::solve_mip". +# +# Curating them behind CUOPT_EXPORT would mean annotating essentially every host-side +# method, so default visibility is the right trade here. +set_target_properties(cuopt_client_objs + PROPERTIES POSITION_INDEPENDENT_CODE ON + CXX_SCAN_FOR_MODULES OFF +) + +add_library(cuopt_client SHARED $) +add_library(cuopt::cuopt_client ALIAS cuopt_client) + +target_include_directories(cuopt_client + PUBLIC + "$" + "$" + INTERFACE + "$" +) + +target_compile_definitions(cuopt_client + PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" +) + +set_target_properties(cuopt_client + PROPERTIES POSITION_INDEPENDENT_CODE ON + CXX_SCAN_FOR_MODULES OFF + BUILD_RPATH "\$ORIGIN" + INSTALL_RPATH "\$ORIGIN" + LINKER_LANGUAGE CXX +) + +target_compile_definitions(cuopt_client_objs + PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" +) + +target_compile_options(cuopt_client_objs + PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" +) + +target_include_directories(cuopt_client_objs + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty" + "${CMAKE_CURRENT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}/src/io" + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc" + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/client" + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated" + "${CMAKE_CURRENT_BINARY_DIR}" + "${CMAKE_CURRENT_BINARY_DIR}/include" + $<$:${BZIP2_INCLUDE_DIRS}> + $<$:${ZLIB_INCLUDE_DIRS}> + PUBLIC + "$" + "$" + INTERFACE + "$" +) + +# CCCL is a compile-time (header-only) dependency here: the fast MPS parser uses +# host helpers from (ceil_div, round_up). It pulls in no CUDA runtime. +# bzip2 / zlib / lz4 are dlopen'd at runtime by file_to_string.cpp, so they are +# header-only here too and deliberately absent from the link line. +# The OBJECT library needs these for their INTERFACE include dirs / defines at compile time. +target_link_libraries(cuopt_client_objs + PUBLIC + rapids_logger::rapids_logger + CCCL::CCCL + PRIVATE + simde::simde + OpenMP::OpenMP_CXX + $<$:protobuf::libprotobuf> + $<$:gRPC::grpc++> +) + +target_link_libraries(cuopt_client + PUBLIC + rapids_logger::rapids_logger + CCCL::CCCL + PRIVATE + simde::simde + OpenMP::OpenMP_CXX + ${CMAKE_DL_LIBS} + $<$:protobuf::libprotobuf> + $<$:gRPC::grpc++> +) + add_library(cuopt_objs OBJECT ${CUOPT_SRC_FILES} ) @@ -757,6 +880,7 @@ target_compile_definitions(cuopt_objs PUBLIC target_link_libraries(cuopt_objs PUBLIC + cuopt::cuopt_client CUDA::cublas CUDA::cusparse rmm::rmm @@ -777,7 +901,10 @@ target_link_libraries(cuopt_objs # - generate tests -------------------------------------------------------------------------------- if (BUILD_TESTS) include(CTest) - add_library(cuopt_static STATIC $) + # Embeds cuopt_client_objs directly rather than linking libcuopt_client.so: the internal + # test binaries reach parser internals that the shared library deliberately does not + # export. Do not also link cuopt::cuopt_client here -- that would duplicate every symbol. + add_library(cuopt_static STATIC $ $) target_link_libraries(cuopt_static PUBLIC CUDA::cublas @@ -839,6 +966,7 @@ target_include_directories(cuopt ) target_link_libraries(cuopt PUBLIC + cuopt::cuopt_client CUDA::cublas CUDA::cusparse rmm::rmm @@ -904,14 +1032,14 @@ else () endif () # adds the .so files to the runtime deb package -install(TARGETS cuopt +install(TARGETS cuopt cuopt_client DESTINATION ${_LIB_DEST} COMPONENT runtime EXPORT cuopt-exports ) # adds the .so files to the development deb package -install(TARGETS cuopt +install(TARGETS cuopt cuopt_client DESTINATION ${_LIB_DEST} COMPONENT dev ) @@ -939,7 +1067,7 @@ cuOpt library is a collection of GPU accelerated combinatorial optimization algo rapids_export(INSTALL cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt + GLOBAL_TARGETS cuopt cuopt_client NAMESPACE cuopt:: DOCUMENTATION doc_string ) @@ -948,7 +1076,7 @@ rapids_export(INSTALL cuopt # - build export ------------------------------------------------------------------------------- rapids_export(BUILD cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt + GLOBAL_TARGETS cuopt cuopt_client NAMESPACE cuopt:: DOCUMENTATION doc_string ) diff --git a/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp b/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp index 28aa91a82f..97d53b2d28 100644 --- a/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp +++ b/cpp/include/cuopt/mathematical_optimization/cpu_optimization_problem.hpp @@ -173,9 +173,11 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t> to_optimization_problem( - raft::handle_t const* handle_ptr = nullptr) override; /** * @brief Write the optimization problem to an MPS file. @@ -207,6 +209,13 @@ class cpu_optimization_problem_t : public optimization_problem_interface_t + friend std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t&, raft::handle_t const*); + problem_category_t problem_category_ = problem_category_t::LP; bool maximize_{false}; i_t n_vars_{0}; diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp index bdfc2ffbd4..355a317da1 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem.hpp @@ -352,12 +352,8 @@ class optimization_problem_t : public optimization_problem_interface_t template optimization_problem_t convert_to_other_prec(rmm::cuda_stream_view stream) const; - /** - * @brief Returns nullptr since this is already a GPU problem. - * @return nullptr - */ - std::unique_ptr> to_optimization_problem( - raft::handle_t const* handle_ptr = nullptr) override; + // to_optimization_problem() is a free function declared at the bottom of this header, + // not a virtual member -- see the note in optimization_problem_interface.hpp. // ============================================================================ // C API support: Copy to host (polymorphic) @@ -427,5 +423,26 @@ class optimization_problem_t : public optimization_problem_interface_t std::vector row_names_{}; }; +/** + * @brief Convert a problem to a GPU-backed optimization_problem_t. + * + * For optimization_problem_t (GPU): returns nullptr (already is one). + * For cpu_optimization_problem_t: creates a new GPU problem, copies data, returns it. + * + * Usage pattern: + * auto temp = to_optimization_problem(problem_interface, &handle); + * optimization_problem_t& op = temp ? *temp : static_cast(problem); + * + * A free function rather than a virtual member so that cpu_optimization_problem_t's vtable + * carries no GPU-defined entry; see optimization_problem_interface.hpp. + * + * @param problem The problem to convert. + * @param handle_ptr RAFT handle with CUDA resources. Required for CPU->GPU conversion. + * @return unique_ptr to a new GPU problem, or nullptr if it already is one. + */ +template +std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t& problem, raft::handle_t const* handle_ptr = nullptr); + } // namespace CUOPT_EXPORT mathematical_optimization } // namespace cuopt diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp index 5927703f03..51796aa60d 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem_interface.hpp @@ -478,22 +478,13 @@ class optimization_problem_interface_t { // Conversion // ============================================================================ - /** - * @brief Convert to a GPU-backed optimization_problem_t. - * - * For optimization_problem_t (GPU): returns nullptr (already is one). - * For cpu_optimization_problem_t: creates new GPU problem, copies data, returns owned pointer. - * - * Usage pattern: - * auto temp = problem_interface->to_optimization_problem(&handle); - * optimization_problem_t& op = temp ? *temp : static_cast(*this); - * - * @param handle_ptr RAFT handle with CUDA resources for GPU memory allocation. - * Required for CPU->GPU conversion. Ignored for GPU problems. - * @return unique_ptr to new GPU problem, or nullptr if already a GPU problem - */ - virtual std::unique_ptr> to_optimization_problem( - raft::handle_t const* handle_ptr = nullptr) = 0; + // NOTE: CPU -> GPU conversion is deliberately NOT a virtual member here. + // + // As a virtual, it occupied a slot in cpu_optimization_problem_t's vtable, and vtable + // relocations are resolved eagerly at load time. That made every library containing + // the vtable -- including the CUDA-free cuopt_client -- unable to load without + // libcuopt.so present. It is now the free function to_optimization_problem() declared + // in optimization_problem.hpp, which lives in libcuopt where the GPU types do. }; } // namespace cuopt::mathematical_optimization diff --git a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp index b1f81b8edb..50a7d19b4f 100644 --- a/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp +++ b/cpp/include/cuopt/mathematical_optimization/optimization_problem_utils.hpp @@ -137,6 +137,39 @@ void populate_from_mps_data_model(optimization_problem_interface_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 +void apply_warmstart_gpu_target(solver_settings_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 +void apply_warmstart_cpu_target(solver_settings_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(solver_settings->get_pdlp_warm_start_data_view()); + } +} + /** * @brief Transfer parsed MPS/QPS storage into a CPU-backed problem without copying payload arrays. * @@ -176,7 +209,7 @@ void adopt_from_mps_data_model(optimization_problem_interface_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 +template void populate_from_data_model_view( optimization_problem_interface_t* problem, cuopt::mathematical_optimization::io::data_model_view_t* data_model, @@ -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 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 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(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 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); } } } diff --git a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp index 0882f75e0f..1ec38be996 100644 --- a/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp +++ b/cpp/include/cuopt/mathematical_optimization/pdlp/solver_settings.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -371,8 +372,24 @@ class pdlp_solver_settings_t { /** Initial pdlp iteration */ // TODO batch mode: tmp std::optional initial_pdlp_iteration_; - /** GPU-backed warm start data (device_uvector), used by C++ API and local GPU solves */ - pdlp_warm_start_data_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_; + + /** Lazily allocate pdlp_warm_start_data_ and return it. Defined in a CUDA TU. */ + pdlp_warm_start_data_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 pdlp_warm_start_data_view_; /** CPU-backed warm start data (std::vector), used for remote execution on CPU-only hosts */ diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index e8737cf6da..e697f7aebc 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -4,11 +4,15 @@ # cmake-format: on set(UTIL_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/seed_generator.cu - ${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/version_info.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/timestamp_utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/work_unit_scheduler.cpp) +# logger.cpp backs , which both the parsers and the gRPC +# sources include, so it belongs to the CPU library. Without this, cuopt_client would +# have an undefined reference to the logger. +set(UTIL_CLIENT_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/logger.cpp) + add_subdirectory(linear_algebra) add_subdirectory(pdlp) add_subdirectory(math_optimization) @@ -26,4 +30,5 @@ add_subdirectory(branch_and_bound) add_subdirectory(cuts) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${UTIL_SRC_FILES} PARENT_SCOPE) +set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${UTIL_CLIENT_SRC_FILES} PARENT_SCOPE) set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/grpc/client/cython_grpc_client.cpp b/cpp/src/grpc/client/cython_grpc_client.cpp index 74409fc93e..0d574d9df6 100644 --- a/cpp/src/grpc/client/cython_grpc_client.cpp +++ b/cpp/src/grpc/client/cython_grpc_client.cpp @@ -109,7 +109,10 @@ grpc_submit_result_t grpc_python_client_t::submit( } cuopt::mathematical_optimization::cpu_optimization_problem_t cpu_problem; - cuopt::mathematical_optimization::populate_from_data_model_view( + // : 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( &cpu_problem, data_model, settings, nullptr); const bool is_mip = diff --git a/cpp/src/grpc/client/solve_remote.cpp b/cpp/src/grpc/client/solve_remote.cpp index eabea39e05..ff808a9fdd 100644 --- a/cpp/src/grpc/client/solve_remote.cpp +++ b/cpp/src/grpc/client/solve_remote.cpp @@ -14,6 +14,7 @@ #include #include "grpc_client.hpp" +#include #include #include #include @@ -22,8 +23,6 @@ #include #include -#include - namespace cuopt::mathematical_optimization { // Buffer added to the solver's time_limit to account for worker startup, @@ -152,7 +151,7 @@ std::unique_ptr> solve_mip_remote( auto mip_callbacks = settings.get_mip_callbacks(); const auto var_types = cpu_problem.get_variable_types_host(); const bool has_sc_variables = - thrust::count(var_types.begin(), var_types.end(), var_t::SEMI_CONTINUOUS) > 0; + std::count(var_types.begin(), var_types.end(), var_t::SEMI_CONTINUOUS) > 0; if (has_sc_variables && !mip_callbacks.empty()) { CUOPT_LOG_WARN( "Disabling remote MIP get/set callbacks: semi-continuous models are not " diff --git a/cpp/src/grpc/server/grpc_worker.cpp b/cpp/src/grpc/server/grpc_worker.cpp index 250b640031..aa048f34bb 100644 --- a/cpp/src/grpc/server/grpc_worker.cpp +++ b/cpp/src/grpc/server/grpc_worker.cpp @@ -425,7 +425,7 @@ static SolveResult run_mip_solve(DeserializedJob& dj, } SERVER_LOG_INFO("[Worker] Converting CPU problem to GPU problem..."); - auto gpu_problem = dj.problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(dj.problem, &handle); SERVER_LOG_INFO("[Worker] Calling solve_mip..."); auto gpu_solution = cuopt::mathematical_optimization::solve_mip(*gpu_problem, dj.mip_settings); @@ -486,7 +486,7 @@ static SolveResult run_lp_solve(DeserializedJob& dj, dj.lp_settings.log_to_console = config.log_to_console; SERVER_LOG_INFO("[Worker] Converting CPU problem to GPU problem..."); - auto gpu_problem = dj.problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(dj.problem, &handle); SERVER_LOG_INFO("[Worker] Calling solve_lp..."); auto gpu_solution = cuopt::mathematical_optimization::solve_lp(*gpu_problem, dj.lp_settings); diff --git a/cpp/src/io/CMakeLists.txt b/cpp/src/io/CMakeLists.txt index cafcffb23f..c234a2ff43 100644 --- a/cpp/src/io/CMakeLists.txt +++ b/cpp/src/io/CMakeLists.txt @@ -23,5 +23,10 @@ set(PARSERS_SRC_FILES ${MPS_FAST_SRC_FILES} ) -set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE) +# The parsers and the host-side problem representation are CUDA-free (the only +# `cuda::` uses are host integer helpers from header-only libcu++), so they build +# into the CPU-only cuopt_client library rather than into cuopt_objs. That is what +# lets the Python data_model / solver_settings / io extension modules link a +# library with no CUDA runtime dependency. +set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE) set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/math_optimization/CMakeLists.txt b/cpp/src/math_optimization/CMakeLists.txt index efa1600c54..d68063e323 100644 --- a/cpp/src/math_optimization/CMakeLists.txt +++ b/cpp/src/math_optimization/CMakeLists.txt @@ -5,11 +5,20 @@ list(PREPEND MATH_OPT_SRC_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu + ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings_gpu.cu ${CMAKE_CURRENT_SOURCE_DIR}/solution_reader.cu ${CMAKE_CURRENT_SOURCE_DIR}/solution_writer.cu ${CMAKE_CURRENT_SOURCE_DIR}/tic_toc.cpp ) +# solver_settings_t is host-only apart from the device members in solver_settings_gpu.cu, +# so the bulk of it builds into cuopt_client. The gRPC client takes a solver_settings_t +# in its public API, so this is required for the client library to resolve standalone. +set(MATH_OPT_CLIENT_SRC_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cpp + ) + set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${MATH_OPT_SRC_FILES} PARENT_SCOPE) +set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} + ${MATH_OPT_CLIENT_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/math_optimization/solver_settings.cu b/cpp/src/math_optimization/solver_settings.cpp similarity index 90% rename from cpp/src/math_optimization/solver_settings.cu rename to cpp/src/math_optimization/solver_settings.cpp index 820f41ee3e..33a71ae332 100644 --- a/cpp/src/math_optimization/solver_settings.cu +++ b/cpp/src/math_optimization/solver_settings.cpp @@ -412,85 +412,6 @@ std::string solver_settings_t::get_parameter_as_string(const std::stri throw std::invalid_argument("Parameter " + name + " not found"); } -template -void solver_settings_t::set_initial_pdlp_primal_solution(const f_t* solution, - i_t size, - rmm::cuda_stream_view stream) -{ - pdlp_settings.set_initial_primal_solution(solution, size, stream); -} - -template -void solver_settings_t::set_initial_pdlp_dual_solution(const f_t* solution, - i_t size, - rmm::cuda_stream_view stream) -{ - pdlp_settings.set_initial_dual_solution(solution, size, stream); -} - -template -void solver_settings_t::set_pdlp_warm_start_data( - const f_t* current_primal_solution, - const f_t* current_dual_solution, - const f_t* initial_primal_average, - const f_t* initial_dual_average, - const f_t* current_ATY, - const f_t* sum_primal_solutions, - const f_t* sum_dual_solutions, - const f_t* last_restart_duality_gap_primal_solution, - const f_t* last_restart_duality_gap_dual_solution, - i_t primal_size, - i_t dual_size, - f_t initial_primal_weight, - f_t initial_step_size, - i_t total_pdlp_iterations, - i_t total_pdhg_iterations, - f_t last_candidate_kkt_score, - f_t last_restart_kkt_score, - f_t sum_solution_weight, - i_t iterations_since_last_restart) -{ - pdlp_settings.set_pdlp_warm_start_data(current_primal_solution, - current_dual_solution, - initial_primal_average, - initial_dual_average, - current_ATY, - sum_primal_solutions, - sum_dual_solutions, - last_restart_duality_gap_primal_solution, - last_restart_duality_gap_dual_solution, - primal_size, - dual_size, - initial_primal_weight, - initial_step_size, - total_pdlp_iterations, - total_pdhg_iterations, - last_candidate_kkt_score, - last_restart_kkt_score, - sum_solution_weight, - iterations_since_last_restart); -} - -template -const rmm::device_uvector& solver_settings_t::get_initial_pdlp_primal_solution() - const -{ - return pdlp_settings.get_initial_primal_solution(); -} - -template -const rmm::device_uvector& solver_settings_t::get_initial_pdlp_dual_solution() const -{ - return pdlp_settings.get_initial_dual_solution(); -} - -template -void solver_settings_t::add_initial_mip_solution(const f_t* solution, - i_t size, - rmm::cuda_stream_view stream) -{ - mip_settings.add_initial_solution(solution, size, stream); -} template void solver_settings_t::set_mip_callback(internals::base_solution_callback_t* callback, diff --git a/cpp/src/math_optimization/solver_settings_gpu.cu b/cpp/src/math_optimization/solver_settings_gpu.cu new file mode 100644 index 0000000000..7843f1e0ae --- /dev/null +++ b/cpp/src/math_optimization/solver_settings_gpu.cu @@ -0,0 +1,135 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Device-facing members of solver_settings_t, split out of solver_settings.cu. +// +// Everything else in that class is host-only parameter handling, so the remainder now +// builds as solver_settings.cpp into the CUDA-free cuopt_client library. Only these +// members take an rmm::cuda_stream_view or hand back a device_uvector, so they are the +// only ones that must stay in a CUDA TU inside libcuopt. +// +// The `template class` instantiation in solver_settings.cpp cannot emit these members +// (their definitions are not visible there), so they are instantiated explicitly below. + +#include + +#include +#include + +#include + +namespace cuopt { +namespace CUOPT_EXPORT mathematical_optimization { + +template +void solver_settings_t::set_initial_pdlp_primal_solution(const f_t* solution, + i_t size, + rmm::cuda_stream_view stream) +{ + pdlp_settings.set_initial_primal_solution(solution, size, stream); +} + +template +void solver_settings_t::set_initial_pdlp_dual_solution(const f_t* solution, + i_t size, + rmm::cuda_stream_view stream) +{ + pdlp_settings.set_initial_dual_solution(solution, size, stream); +} + +template +void solver_settings_t::set_pdlp_warm_start_data( + const f_t* current_primal_solution, + const f_t* current_dual_solution, + const f_t* initial_primal_average, + const f_t* initial_dual_average, + const f_t* current_ATY, + const f_t* sum_primal_solutions, + const f_t* sum_dual_solutions, + const f_t* last_restart_duality_gap_primal_solution, + const f_t* last_restart_duality_gap_dual_solution, + i_t primal_size, + i_t dual_size, + f_t initial_primal_weight, + f_t initial_step_size, + i_t total_pdlp_iterations, + i_t total_pdhg_iterations, + f_t last_candidate_kkt_score, + f_t last_restart_kkt_score, + f_t sum_solution_weight, + i_t iterations_since_last_restart) +{ + pdlp_settings.set_pdlp_warm_start_data(current_primal_solution, + current_dual_solution, + initial_primal_average, + initial_dual_average, + current_ATY, + sum_primal_solutions, + sum_dual_solutions, + last_restart_duality_gap_primal_solution, + last_restart_duality_gap_dual_solution, + primal_size, + dual_size, + initial_primal_weight, + initial_step_size, + total_pdlp_iterations, + total_pdhg_iterations, + last_candidate_kkt_score, + last_restart_kkt_score, + sum_solution_weight, + iterations_since_last_restart); +} + +template +const rmm::device_uvector& solver_settings_t::get_initial_pdlp_primal_solution() + const +{ + return pdlp_settings.get_initial_primal_solution(); +} + +template +const rmm::device_uvector& solver_settings_t::get_initial_pdlp_dual_solution() const +{ + return pdlp_settings.get_initial_dual_solution(); +} + +template +void solver_settings_t::add_initial_mip_solution(const f_t* solution, + i_t size, + rmm::cuda_stream_view stream) +{ + mip_settings.add_initial_solution(solution, size, stream); +} + +#if MIP_INSTANTIATE_FLOAT +template CUOPT_EXPORT void solver_settings_t::set_initial_pdlp_primal_solution( + const float*, int, rmm::cuda_stream_view); +template CUOPT_EXPORT void solver_settings_t::set_initial_pdlp_dual_solution( + const float*, int, rmm::cuda_stream_view); +template CUOPT_EXPORT const rmm::device_uvector& +solver_settings_t::get_initial_pdlp_primal_solution() const; +template CUOPT_EXPORT const rmm::device_uvector& +solver_settings_t::get_initial_pdlp_dual_solution() const; +template CUOPT_EXPORT void solver_settings_t::add_initial_mip_solution( + const float*, int, rmm::cuda_stream_view); +#endif + +#if MIP_INSTANTIATE_DOUBLE +template CUOPT_EXPORT void solver_settings_t::set_initial_pdlp_primal_solution( + const double*, int, rmm::cuda_stream_view); +template CUOPT_EXPORT void solver_settings_t::set_initial_pdlp_dual_solution( + const double*, int, rmm::cuda_stream_view); +template CUOPT_EXPORT const rmm::device_uvector& +solver_settings_t::get_initial_pdlp_primal_solution() const; +template CUOPT_EXPORT const rmm::device_uvector& +solver_settings_t::get_initial_pdlp_dual_solution() const; +template CUOPT_EXPORT void solver_settings_t::add_initial_mip_solution( + const double*, int, rmm::cuda_stream_view); +#endif + +} // namespace CUOPT_EXPORT mathematical_optimization +} // namespace cuopt diff --git a/cpp/src/mip_heuristics/CMakeLists.txt b/cpp/src/mip_heuristics/CMakeLists.txt index 6ad1009d84..2b4c97ea0f 100644 --- a/cpp/src/mip_heuristics/CMakeLists.txt +++ b/cpp/src/mip_heuristics/CMakeLists.txt @@ -54,5 +54,13 @@ else() set(MIP_SRC_FILES ${MIP_LP_NECESSARY_FILES} ${MIP_NON_LP_FILES}) endif() +# Host-only members of mip_solver_settings_t (callbacks, tolerances). The gRPC client +# needs them, so they build into cuopt_client; add_initial_solution stays in the .cu. +set(MIP_CLIENT_SRC_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cpp +) + set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${MIP_SRC_FILES} PARENT_SCOPE) +set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} + ${MIP_CLIENT_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 162a5ba291..9b53f2a06f 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -894,7 +894,7 @@ std::unique_ptr> solve_mip( raft::handle_t handle(stream); // Convert CPU problem to GPU problem - auto gpu_problem = cpu_problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(cpu_problem, &handle); // Synchronize before solving to ensure conversion is complete stream.synchronize(); diff --git a/cpp/src/mip_heuristics/solver_settings.cpp b/cpp/src/mip_heuristics/solver_settings.cpp new file mode 100644 index 0000000000..564248c472 --- /dev/null +++ b/cpp/src/mip_heuristics/solver_settings.cpp @@ -0,0 +1,67 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Host-only members of mip_solver_settings_t, split out of solver_settings.cu. +// +// Only add_initial_solution() touches the device (it copies into an rmm::device_uvector), +// so it stays in the CUDA TU while these build into the CUDA-free cuopt_client library. +// The gRPC client reaches get_mip_callbacks() via solve_remote's callback handling. +// +// Instantiated per-member rather than with `template class`: the class holds +// device_uvector-backed initial_solutions, so instantiating all of it here would pull +// device code into the client library. + +#include +#include +#include + +#include + +namespace cuopt::mathematical_optimization { + +template +void mip_solver_settings_t::set_mip_callback( + internals::base_solution_callback_t* callback, void* user_data) +{ + if (callback == nullptr) { return; } + callback->set_user_data(user_data); + mip_callbacks_.push_back(callback); +} + +template +const std::vector +mip_solver_settings_t::get_mip_callbacks() const +{ + return mip_callbacks_; +} + +template +typename mip_solver_settings_t::tolerances_t +mip_solver_settings_t::get_tolerances() const noexcept +{ + return tolerances; +} + +#if MIP_INSTANTIATE_FLOAT +template CUOPT_EXPORT void mip_solver_settings_t::set_mip_callback( + internals::base_solution_callback_t*, void*); +template CUOPT_EXPORT const std::vector +mip_solver_settings_t::get_mip_callbacks() const; +template CUOPT_EXPORT mip_solver_settings_t::tolerances_t +mip_solver_settings_t::get_tolerances() const noexcept; +#endif + +#if MIP_INSTANTIATE_DOUBLE +template CUOPT_EXPORT void mip_solver_settings_t::set_mip_callback( + internals::base_solution_callback_t*, void*); +template CUOPT_EXPORT const std::vector +mip_solver_settings_t::get_mip_callbacks() const; +template CUOPT_EXPORT mip_solver_settings_t::tolerances_t +mip_solver_settings_t::get_tolerances() const noexcept; +#endif + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/mip_heuristics/solver_settings.cu b/cpp/src/mip_heuristics/solver_settings.cu index 8b454c949b..a5325137bf 100644 --- a/cpp/src/mip_heuristics/solver_settings.cu +++ b/cpp/src/mip_heuristics/solver_settings.cu @@ -24,29 +24,6 @@ void mip_solver_settings_t::add_initial_solution(const f_t* initial_so raft::copy(initial_solutions.back()->data(), initial_solution, size, stream); } -template -void mip_solver_settings_t::set_mip_callback( - internals::base_solution_callback_t* callback, void* user_data) -{ - if (callback == nullptr) { return; } - callback->set_user_data(user_data); - mip_callbacks_.push_back(callback); -} - -template -const std::vector -mip_solver_settings_t::get_mip_callbacks() const -{ - return mip_callbacks_; -} - -template -typename mip_solver_settings_t::tolerances_t -mip_solver_settings_t::get_tolerances() const noexcept -{ - return tolerances; -} - // Explicit template instantiations for common types #if MIP_INSTANTIATE_FLOAT template class CUOPT_EXPORT mip_solver_settings_t; diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 2f90f94872..677f9cd8e0 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -7,7 +7,7 @@ set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu ${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 ${CMAKE_CURRENT_SOURCE_DIR}/backend_selection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utilities/problem_checking.cu ${CMAKE_CURRENT_SOURCE_DIR}/solve.cu @@ -49,4 +49,14 @@ else() set(LP_SRC_FILES ${LP_CORE_FILES} ${LP_ADAPTER_FILES}) endif() +# Host-only LP sources that the gRPC client needs. These build into cuopt_client so the +# client library resolves standalone, without libcuopt.so. Their GPU-facing members were +# split into separate CUDA translation units above (e.g. cpu_optimization_problem_to_gpu.cpp). +set(LP_CLIENT_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/cpu_optimization_problem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/solution_conversion_cpu.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings_accessors.cpp +) + set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${LP_SRC_FILES} PARENT_SCOPE) +set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${LP_CLIENT_FILES} PARENT_SCOPE) diff --git a/cpp/src/pdlp/cpu_optimization_problem.cpp b/cpp/src/pdlp/cpu_optimization_problem.cpp index 4b970eb6ec..8e310b287b 100644 --- a/cpp/src/pdlp/cpu_optimization_problem.cpp +++ b/cpp/src/pdlp/cpu_optimization_problem.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -634,100 +633,6 @@ std::vector cpu_optimization_problem_t::get_variable_types_host return variable_types_; } -// ============================================================================== -// Conversion to optimization_problem_t -// ============================================================================== - -template -std::unique_ptr> -cpu_optimization_problem_t::to_optimization_problem(raft::handle_t const* handle_ptr) -{ - if (handle_ptr == nullptr) { - throw std::runtime_error( - "cpu_optimization_problem_t::to_optimization_problem(): " - "handle_ptr is null. A RAFT handle with CUDA resources is required to convert " - "a CPU-backed problem to a GPU-backed optimization_problem_t."); - } - - auto gpu_problem = std::make_unique>(handle_ptr); - - // Set scalar values - gpu_problem->set_maximize(maximize_); - gpu_problem->set_objective_scaling_factor(objective_scaling_factor_); - gpu_problem->set_objective_offset(objective_offset_); - gpu_problem->set_problem_category(problem_category_); - - // Set string values - if (!objective_name_.empty()) gpu_problem->set_objective_name(objective_name_); - if (!problem_name_.empty()) gpu_problem->set_problem_name(problem_name_); - if (!var_names_.empty()) gpu_problem->set_variable_names(var_names_); - if (!row_names_.empty()) gpu_problem->set_row_names(row_names_); - - // Set CSR constraint matrix (data will be copied to GPU by optimization_problem_t setters) - // Use A_offsets_ presence as the guard: a valid CSR can have zero non-zeros but still - // needs row offsets to define the number of constraints. - if (!A_offsets_.empty()) { - gpu_problem->set_csr_constraint_matrix(A_.data(), - A_.size(), - A_indices_.data(), - A_indices_.size(), - A_offsets_.data(), - A_offsets_.size()); - } - - // Set constraint bounds - if (!b_.empty()) { gpu_problem->set_constraint_bounds(b_.data(), b_.size()); } - - // Set objective coefficients - if (!c_.empty()) { gpu_problem->set_objective_coefficients(c_.data(), c_.size()); } - - // Set quadratic objective if present (GPU setter symmetrizes once: H = Q + Q^T) - if (!Q_values_.empty()) { - gpu_problem->set_quadratic_objective_matrix(Q_values_.data(), - Q_values_.size(), - Q_indices_.data(), - Q_indices_.size(), - Q_offsets_.data(), - Q_offsets_.size()); - } - - if (!quadratic_constraints_.empty()) { - gpu_problem->set_quadratic_constraints( - std::vector::quadratic_constraint_t>( - quadratic_constraints_)); - } - - // Set variable bounds - if (!variable_lower_bounds_.empty()) { - gpu_problem->set_variable_lower_bounds(variable_lower_bounds_.data(), - variable_lower_bounds_.size()); - } - if (!variable_upper_bounds_.empty()) { - gpu_problem->set_variable_upper_bounds(variable_upper_bounds_.data(), - variable_upper_bounds_.size()); - } - - // Set variable types - if (!variable_types_.empty()) { - gpu_problem->set_variable_types(variable_types_.data(), variable_types_.size()); - } - - // Set constraint bounds - if (!constraint_lower_bounds_.empty()) { - gpu_problem->set_constraint_lower_bounds(constraint_lower_bounds_.data(), - constraint_lower_bounds_.size()); - } - if (!constraint_upper_bounds_.empty()) { - gpu_problem->set_constraint_upper_bounds(constraint_upper_bounds_.data(), - constraint_upper_bounds_.size()); - } - - // Set row types - if (!row_types_.empty()) { gpu_problem->set_row_types(row_types_.data(), row_types_.size()); } - - return gpu_problem; -} - // ============================================================================== // File I/O // ============================================================================== diff --git a/cpp/src/pdlp/cpu_optimization_problem_to_gpu.cpp b/cpp/src/pdlp/cpu_optimization_problem_to_gpu.cpp new file mode 100644 index 0000000000..040d01dd39 --- /dev/null +++ b/cpp/src/pdlp/cpu_optimization_problem_to_gpu.cpp @@ -0,0 +1,147 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// CPU -> GPU conversion for cpu_optimization_problem_t. +// +// Split out of cpu_optimization_problem.cpp so that the rest of that class -- which is +// pure host code -- can be compiled into the CUDA-free cuopt_client library. This is the +// only member that constructs an optimization_problem_t, so it is the only one that needs +// and a raft handle. It stays in cuopt_objs (libcuopt). +// +// The explicit member instantiations at the bottom are required: the `template class` +// instantiation in cpu_optimization_problem.cpp no longer sees this definition, so it +// cannot emit this member. + +#include +#include +#include + +// Required: the explicit instantiations below are guarded on MIP_INSTANTIATE_*. +// Without this header those macros are undefined and this TU emits no symbols. +#include + +#include +#include +#include + +namespace cuopt::mathematical_optimization { + +// Free function (was a virtual member; see optimization_problem_interface.hpp). +// Dispatches on the concrete type: a GPU problem is already what the caller wants, so it +// yields nullptr, matching the previous optimization_problem_t override. +template +std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t& problem, raft::handle_t const* handle_ptr) +{ + auto* cpu_problem = dynamic_cast*>(&problem); + if (cpu_problem == nullptr) { + // Already a GPU-backed problem. + return nullptr; + } + auto& self = *cpu_problem; + + if (handle_ptr == nullptr) { + throw std::runtime_error( + "cpu_optimization_problem_t::to_optimization_problem(): " + "handle_ptr is null. A RAFT handle with CUDA resources is required to convert " + "a CPU-backed problem to a GPU-backed optimization_problem_t."); + } + + auto gpu_problem = std::make_unique>(handle_ptr); + + // Set scalar values + gpu_problem->set_maximize(self.maximize_); + gpu_problem->set_objective_scaling_factor(self.objective_scaling_factor_); + gpu_problem->set_objective_offset(self.objective_offset_); + gpu_problem->set_problem_category(self.problem_category_); + + // Set string values + if (!self.objective_name_.empty()) gpu_problem->set_objective_name(self.objective_name_); + if (!self.problem_name_.empty()) gpu_problem->set_problem_name(self.problem_name_); + if (!self.var_names_.empty()) gpu_problem->set_variable_names(self.var_names_); + if (!self.row_names_.empty()) gpu_problem->set_row_names(self.row_names_); + + // Set CSR constraint matrix (data will be copied to GPU by optimization_problem_t setters) + // Use self.A_offsets_ presence as the guard: a valid CSR can have zero non-zeros but still + // needs row offsets to define the number of constraints. + if (!self.A_offsets_.empty()) { + gpu_problem->set_csr_constraint_matrix(self.A_.data(), + self.A_.size(), + self.A_indices_.data(), + self.A_indices_.size(), + self.A_offsets_.data(), + self.A_offsets_.size()); + } + + // Set constraint bounds + if (!self.b_.empty()) { gpu_problem->set_constraint_bounds(self.b_.data(), self.b_.size()); } + + // Set objective coefficients + if (!self.c_.empty()) { gpu_problem->set_objective_coefficients(self.c_.data(), self.c_.size()); } + + // Set quadratic objective if present (GPU setter symmetrizes once: H = Q + Q^T) + if (!self.Q_values_.empty()) { + gpu_problem->set_quadratic_objective_matrix(self.Q_values_.data(), + self.Q_values_.size(), + self.Q_indices_.data(), + self.Q_indices_.size(), + self.Q_offsets_.data(), + self.Q_offsets_.size()); + } + + if (!self.quadratic_constraints_.empty()) { + gpu_problem->set_quadratic_constraints( + std::vector::quadratic_constraint_t>( + self.quadratic_constraints_)); + } + + // Set variable bounds + if (!self.variable_lower_bounds_.empty()) { + gpu_problem->set_variable_lower_bounds(self.variable_lower_bounds_.data(), + self.variable_lower_bounds_.size()); + } + if (!self.variable_upper_bounds_.empty()) { + gpu_problem->set_variable_upper_bounds(self.variable_upper_bounds_.data(), + self.variable_upper_bounds_.size()); + } + + // Set variable types + if (!self.variable_types_.empty()) { + gpu_problem->set_variable_types(self.variable_types_.data(), self.variable_types_.size()); + } + + // Set constraint bounds + if (!self.constraint_lower_bounds_.empty()) { + gpu_problem->set_constraint_lower_bounds(self.constraint_lower_bounds_.data(), + self.constraint_lower_bounds_.size()); + } + if (!self.constraint_upper_bounds_.empty()) { + gpu_problem->set_constraint_upper_bounds(self.constraint_upper_bounds_.data(), + self.constraint_upper_bounds_.size()); + } + + // Set row types + if (!self.row_types_.empty()) { gpu_problem->set_row_types(self.row_types_.data(), self.row_types_.size()); } + + return gpu_problem; +} + + +// ============================================================================== +// Template instantiations matching cpu_optimization_problem.cpp +// ============================================================================== + +#if MIP_INSTANTIATE_FLOAT +template CUOPT_EXPORT std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t&, raft::handle_t const*); +#endif +#if MIP_INSTANTIATE_DOUBLE +template CUOPT_EXPORT std::unique_ptr> to_optimization_problem( + optimization_problem_interface_t&, raft::handle_t const*); +#endif + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/optimization_problem.cu b/cpp/src/pdlp/optimization_problem.cu index 95457e2556..70d6112978 100644 --- a/cpp/src/pdlp/optimization_problem.cu +++ b/cpp/src/pdlp/optimization_problem.cu @@ -639,14 +639,6 @@ raft::handle_t const* optimization_problem_t::get_handle_ptr() const n // Conversion // ============================================================================== -template -std::unique_ptr> -optimization_problem_t::to_optimization_problem(raft::handle_t const* /*handle_ptr*/) -{ - // Already a GPU problem, return nullptr - return nullptr; -} - // ============================================================================== // Host Getters (copy from GPU to CPU) // ============================================================================== @@ -1645,4 +1637,44 @@ template CUOPT_EXPORT optimization_problem_t 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 +void apply_warmstart_gpu_target(solver_settings_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 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 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*, const raft::handle_t*); +#endif +#if MIP_INSTANTIATE_DOUBLE +template void apply_warmstart_gpu_target(solver_settings_t*, const raft::handle_t*); +#endif + } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solution_conversion.cu b/cpp/src/pdlp/solution_conversion.cu index 8293629e6f..533f5ccc3a 100644 --- a/cpp/src/pdlp/solution_conversion.cu +++ b/cpp/src/pdlp/solution_conversion.cu @@ -132,95 +132,9 @@ cuopt::cython::mip_ret_t gpu_mip_solution_t::to_mip_ret_t() } // =========================== -// CPU LP Solution Conversion -// =========================== - -template -cuopt::cython::linear_programming_ret_t -cpu_lp_solution_t::to_cpu_linear_programming_ret_t() -{ - using cpu_solutions_t = cuopt::cython::linear_programming_ret_t::cpu_solutions_t; - cuopt::cython::linear_programming_ret_t ret; - - cpu_solutions_t cpu; - cpu.primal_solution_ = std::move(primal_solution_); - cpu.dual_solution_ = std::move(dual_solution_); - cpu.reduced_cost_ = std::move(reduced_cost_); - - if (!pdlp_warm_start_data_.current_primal_solution_.empty()) { - cpu.current_primal_solution_ = std::move(pdlp_warm_start_data_.current_primal_solution_); - cpu.current_dual_solution_ = std::move(pdlp_warm_start_data_.current_dual_solution_); - cpu.initial_primal_average_ = std::move(pdlp_warm_start_data_.initial_primal_average_); - cpu.initial_dual_average_ = std::move(pdlp_warm_start_data_.initial_dual_average_); - cpu.current_ATY_ = std::move(pdlp_warm_start_data_.current_ATY_); - cpu.sum_primal_solutions_ = std::move(pdlp_warm_start_data_.sum_primal_solutions_); - cpu.sum_dual_solutions_ = std::move(pdlp_warm_start_data_.sum_dual_solutions_); - cpu.last_restart_duality_gap_primal_solution_ = - std::move(pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_); - cpu.last_restart_duality_gap_dual_solution_ = - std::move(pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_); - - ret.initial_primal_weight_ = pdlp_warm_start_data_.initial_primal_weight_; - ret.initial_step_size_ = pdlp_warm_start_data_.initial_step_size_; - ret.total_pdlp_iterations_ = pdlp_warm_start_data_.total_pdlp_iterations_; - ret.total_pdhg_iterations_ = pdlp_warm_start_data_.total_pdhg_iterations_; - ret.last_candidate_kkt_score_ = pdlp_warm_start_data_.last_candidate_kkt_score_; - ret.last_restart_kkt_score_ = pdlp_warm_start_data_.last_restart_kkt_score_; - ret.sum_solution_weight_ = pdlp_warm_start_data_.sum_solution_weight_; - ret.iterations_since_last_restart_ = pdlp_warm_start_data_.iterations_since_last_restart_; - } - - ret.solutions_ = std::move(cpu); - - ret.termination_status_ = termination_status_; - ret.error_status_ = error_status_.get_error_type(); - ret.error_message_ = std::string(error_status_.what()); - ret.l2_primal_residual_ = l2_primal_residual_; - ret.l2_dual_residual_ = l2_dual_residual_; - ret.primal_objective_ = primal_objective_; - ret.dual_objective_ = dual_objective_; - ret.gap_ = gap_; - ret.nb_iterations_ = num_iterations_; - ret.solve_time_ = solve_time_; - ret.solved_by_ = solved_by_; - - return ret; -} - -// =========================== -// CPU MIP Solution Conversion -// =========================== - -template -cuopt::cython::mip_ret_t cpu_mip_solution_t::to_cpu_mip_ret_t() -{ - cuopt::cython::mip_ret_t ret; - - ret.solution_ = std::move(solution_); - - ret.termination_status_ = termination_status_; - ret.error_status_ = error_status_.get_error_type(); - ret.error_message_ = std::string(error_status_.what()); - ret.objective_ = objective_; - ret.mip_gap_ = mip_gap_; - ret.solution_bound_ = solution_bound_; - ret.total_solve_time_ = total_solve_time_; - ret.presolve_time_ = presolve_time_; - ret.max_constraint_violation_ = max_constraint_violation_; - ret.max_int_violation_ = max_int_violation_; - ret.max_variable_bound_violation_ = max_variable_bound_violation_; - ret.nodes_ = num_nodes_; - ret.simplex_iterations_ = num_simplex_iterations_; - - return ret; -} - // Explicit template instantiations template CUOPT_EXPORT cuopt::cython::linear_programming_ret_t gpu_lp_solution_t::to_linear_programming_ret_t(); template CUOPT_EXPORT cuopt::cython::mip_ret_t gpu_mip_solution_t::to_mip_ret_t(); -template CUOPT_EXPORT cuopt::cython::linear_programming_ret_t -cpu_lp_solution_t::to_cpu_linear_programming_ret_t(); -template CUOPT_EXPORT cuopt::cython::mip_ret_t cpu_mip_solution_t::to_cpu_mip_ret_t(); } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solution_conversion_cpu.cpp b/cpp/src/pdlp/solution_conversion_cpu.cpp new file mode 100644 index 0000000000..242df4c824 --- /dev/null +++ b/cpp/src/pdlp/solution_conversion_cpu.cpp @@ -0,0 +1,112 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Host-side solution conversions, split out of solution_conversion.cu. +// +// cpu_lp_solution_t / cpu_mip_solution_t hold std::vector data and simply move it into +// the cython ret structs -- no device memory involved. Keeping them in a .cu TU forced +// the gRPC client to depend on libcuopt.so purely to resolve these two symbols, so they +// live in cuopt_client instead. The GPU counterparts stay in solution_conversion.cu. + +#include +#include +#include + +#include +#include + +namespace cuopt::mathematical_optimization { + +// CPU LP Solution Conversion +// =========================== + +template +cuopt::cython::linear_programming_ret_t +cpu_lp_solution_t::to_cpu_linear_programming_ret_t() +{ + using cpu_solutions_t = cuopt::cython::linear_programming_ret_t::cpu_solutions_t; + cuopt::cython::linear_programming_ret_t ret; + + cpu_solutions_t cpu; + cpu.primal_solution_ = std::move(primal_solution_); + cpu.dual_solution_ = std::move(dual_solution_); + cpu.reduced_cost_ = std::move(reduced_cost_); + + if (!pdlp_warm_start_data_.current_primal_solution_.empty()) { + cpu.current_primal_solution_ = std::move(pdlp_warm_start_data_.current_primal_solution_); + cpu.current_dual_solution_ = std::move(pdlp_warm_start_data_.current_dual_solution_); + cpu.initial_primal_average_ = std::move(pdlp_warm_start_data_.initial_primal_average_); + cpu.initial_dual_average_ = std::move(pdlp_warm_start_data_.initial_dual_average_); + cpu.current_ATY_ = std::move(pdlp_warm_start_data_.current_ATY_); + cpu.sum_primal_solutions_ = std::move(pdlp_warm_start_data_.sum_primal_solutions_); + cpu.sum_dual_solutions_ = std::move(pdlp_warm_start_data_.sum_dual_solutions_); + cpu.last_restart_duality_gap_primal_solution_ = + std::move(pdlp_warm_start_data_.last_restart_duality_gap_primal_solution_); + cpu.last_restart_duality_gap_dual_solution_ = + std::move(pdlp_warm_start_data_.last_restart_duality_gap_dual_solution_); + + ret.initial_primal_weight_ = pdlp_warm_start_data_.initial_primal_weight_; + ret.initial_step_size_ = pdlp_warm_start_data_.initial_step_size_; + ret.total_pdlp_iterations_ = pdlp_warm_start_data_.total_pdlp_iterations_; + ret.total_pdhg_iterations_ = pdlp_warm_start_data_.total_pdhg_iterations_; + ret.last_candidate_kkt_score_ = pdlp_warm_start_data_.last_candidate_kkt_score_; + ret.last_restart_kkt_score_ = pdlp_warm_start_data_.last_restart_kkt_score_; + ret.sum_solution_weight_ = pdlp_warm_start_data_.sum_solution_weight_; + ret.iterations_since_last_restart_ = pdlp_warm_start_data_.iterations_since_last_restart_; + } + + ret.solutions_ = std::move(cpu); + + ret.termination_status_ = termination_status_; + ret.error_status_ = error_status_.get_error_type(); + ret.error_message_ = std::string(error_status_.what()); + ret.l2_primal_residual_ = l2_primal_residual_; + ret.l2_dual_residual_ = l2_dual_residual_; + ret.primal_objective_ = primal_objective_; + ret.dual_objective_ = dual_objective_; + ret.gap_ = gap_; + ret.nb_iterations_ = num_iterations_; + ret.solve_time_ = solve_time_; + ret.solved_by_ = solved_by_; + + return ret; +} + +// =========================== +// CPU MIP Solution Conversion +// =========================== + +template +cuopt::cython::mip_ret_t cpu_mip_solution_t::to_cpu_mip_ret_t() +{ + cuopt::cython::mip_ret_t ret; + + ret.solution_ = std::move(solution_); + + ret.termination_status_ = termination_status_; + ret.error_status_ = error_status_.get_error_type(); + ret.error_message_ = std::string(error_status_.what()); + ret.objective_ = objective_; + ret.mip_gap_ = mip_gap_; + ret.solution_bound_ = solution_bound_; + ret.total_solve_time_ = total_solve_time_; + ret.presolve_time_ = presolve_time_; + ret.max_constraint_violation_ = max_constraint_violation_; + ret.max_int_violation_ = max_int_violation_; + ret.max_variable_bound_violation_ = max_variable_bound_violation_; + ret.nodes_ = num_nodes_; + ret.simplex_iterations_ = num_simplex_iterations_; + + return ret; +} + +// Explicit template instantiations +template CUOPT_EXPORT cuopt::cython::linear_programming_ret_t +cpu_lp_solution_t::to_cpu_linear_programming_ret_t(); +template CUOPT_EXPORT cuopt::cython::mip_ret_t cpu_mip_solution_t::to_cpu_mip_ret_t(); + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index b8cb4aead8..f264526a62 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2679,7 +2679,7 @@ std::unique_ptr> solve_lp( raft::handle_t handle(stream); // Convert CPU problem to GPU problem - auto gpu_problem = cpu_problem.to_optimization_problem(&handle); + auto gpu_problem = to_optimization_problem(cpu_problem, &handle); // Synchronize before solving to ensure conversion is complete stream.synchronize(); diff --git a/cpp/src/pdlp/solver_settings.cu b/cpp/src/pdlp/solver_settings.cu index 33d8f1a64b..77bc0512da 100644 --- a/cpp/src/pdlp/solver_settings.cu +++ b/cpp/src/pdlp/solver_settings.cu @@ -95,7 +95,10 @@ void pdlp_solver_settings_t::set_pdlp_warm_start_data( const rmm::device_uvector& var_mapping, const rmm::device_uvector& 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) { @@ -382,37 +385,28 @@ std::optional pdlp_solver_settings_t::get_initial_pdlp_iteration( } template -const pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() - const noexcept -{ - return pdlp_warm_start_data_; -} - -template -pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() -{ - return pdlp_warm_start_data_; -} - -template -const cpu_pdlp_warm_start_data_t& -pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept +pdlp_warm_start_data_t& pdlp_solver_settings_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>(); + } + 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 -cpu_pdlp_warm_start_data_t& -pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept +const pdlp_warm_start_data_t& pdlp_solver_settings_t::get_pdlp_warm_start_data() + const noexcept { - return cpu_pdlp_warm_start_data_; + return ensure_pdlp_warm_start_data(); } template -const pdlp_warm_start_data_view_t& -pdlp_solver_settings_t::get_pdlp_warm_start_data_view() const noexcept +pdlp_warm_start_data_t& pdlp_solver_settings_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 diff --git a/cpp/src/pdlp/solver_settings_accessors.cpp b/cpp/src/pdlp/solver_settings_accessors.cpp new file mode 100644 index 0000000000..9d5efc7f9a --- /dev/null +++ b/cpp/src/pdlp/solver_settings_accessors.cpp @@ -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 +#include + +// 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 + +namespace cuopt::mathematical_optimization { + +template +const cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept +{ + return cpu_pdlp_warm_start_data_; +} + +template +cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept +{ + return cpu_pdlp_warm_start_data_; +} + +template +const pdlp_warm_start_data_view_t& +pdlp_solver_settings_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& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept; +template CUOPT_EXPORT cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept; +template CUOPT_EXPORT const pdlp_warm_start_data_view_t& +pdlp_solver_settings_t::get_pdlp_warm_start_data_view() const noexcept; +#endif + +#if MIP_INSTANTIATE_DOUBLE +template CUOPT_EXPORT const cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() const noexcept; +template CUOPT_EXPORT cpu_pdlp_warm_start_data_t& +pdlp_solver_settings_t::get_cpu_pdlp_warm_start_data() noexcept; +template CUOPT_EXPORT const pdlp_warm_start_data_view_t& +pdlp_solver_settings_t::get_pdlp_warm_start_data_view() const noexcept; +#endif + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu index b34faa88b4..85dba0227b 100644 --- a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu @@ -305,7 +305,7 @@ TEST_F(SolutionInterfaceTest, gpu_problem_to_optimization_problem) EXPECT_EQ(problem->get_n_constraints(), kNCons); // GPU problem's to_optimization_problem() returns nullptr (already a GPU problem) - auto concrete = problem->to_optimization_problem(&handle); + auto concrete = to_optimization_problem(*problem, &handle); EXPECT_EQ(concrete, nullptr); // Verify the data is still accessible directly on the problem @@ -340,7 +340,7 @@ TEST_F(SolutionInterfaceTest, cpu_problem_to_optimization_problem) EXPECT_EQ(problem->get_n_variables(), kNVars); EXPECT_EQ(problem->get_n_constraints(), kNCons); - auto concrete = problem->to_optimization_problem(&handle); + auto concrete = to_optimization_problem(*problem, &handle); ASSERT_NE(concrete, nullptr); EXPECT_EQ(concrete->get_n_variables(), kNVars); EXPECT_EQ(concrete->get_n_constraints(), kNCons);