From 9b3d53bdcbbf4c03f468b7c1ffad2d9718cc8ae4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 23 Jul 2026 15:40:25 -0500 Subject: [PATCH 01/40] build(cmake): split libcuopt into cuopt_base / cuopt_routing / cuopt_lp + umbrella MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce three STATIC component libraries that logically partition the cuOpt sources by domain, then fold them into the existing libcuopt.so umbrella via --whole-archive (LINK_LIBRARY:WHOLE_ARCHIVE). Component libraries: - cuopt_base — utilities + linear algebra (logger, work scheduler) - cuopt_routing — VRP / routing engine; links cuopt_base - cuopt_lp — LP / MIP / numerical optimization; links cuopt_base Umbrella: - cuopt SHARED — re-exports all symbols from the three statics via --whole-archive; backward-compatible for GAMS (-lcuopt / libcuopt.so) CMake aliases exposed: cuopt::base, cuopt::routing, cuopt::lp, cuopt::cuopt No source files moved. External build output (libcuopt.so, headers, install layout) is unchanged. SKIP_ROUTING_BUILD=ON continues to work by omitting cuopt_routing from the build and umbrella link. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 288 +++++++++++++++-------- cpp/src/CMakeLists.txt | 31 ++- cpp/src/barrier/CMakeLists.txt | 1 + cpp/src/branch_and_bound/CMakeLists.txt | 1 + cpp/src/cuts/CMakeLists.txt | 1 + cpp/src/dual_simplex/CMakeLists.txt | 1 + cpp/src/io/CMakeLists.txt | 3 +- cpp/src/linear_algebra/CMakeLists.txt | 1 + cpp/src/math_optimization/CMakeLists.txt | 1 + cpp/src/mip_heuristics/CMakeLists.txt | 1 + cpp/src/pdlp/CMakeLists.txt | 1 + cpp/src/routing/CMakeLists.txt | 1 + 12 files changed, 227 insertions(+), 104 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 61f6eb91df..699b727655 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -454,6 +454,9 @@ if (BUILD_TESTS) endif () set(CUOPT_SRC_FILES) +set(CUOPT_BASE_SRC_FILES) +set(CUOPT_ROUTING_SRC_FILES) +set(CUOPT_LP_SRC_FILES) set(MPS_FAST_SRC_FILES) add_subdirectory(src) @@ -464,7 +467,9 @@ set_source_files_properties( PROPERTIES COMPILE_OPTIONS "--split-compile=0") if (HOST_LINEINFO) - set_source_files_properties(${CUOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") + set_source_files_properties( + ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_LP_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) @@ -477,16 +482,17 @@ 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 to solver source files (not gRPC infrastructure). # 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_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_LP_SRC_FILES} + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG") endif () if (NOT SKIP_GRPC_BUILD) - # Add gRPC mapper files and generated protobuf sources + # gRPC integration layer: maps proto <-> C++ LP API; lives in the umbrella, not in cuopt_lp set(GRPC_INFRA_FILES ${DATA_PROTO_SRCS} ${PROTO_SRCS} @@ -501,7 +507,6 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/client/cython_grpc_client.cpp src/grpc/client/solve_remote.cpp ) - list(APPEND CUOPT_SRC_FILES ${GRPC_INFRA_FILES}) # Always keep NDEBUG defined for gRPC infrastructure files so that abseil # headers inline Mutex::Dtor() instead of emitting an external call. @@ -512,67 +517,166 @@ if (NOT SKIP_GRPC_BUILD) APPEND PROPERTY COMPILE_OPTIONS "-DNDEBUG") endif (NOT SKIP_GRPC_BUILD) -add_library(cuopt SHARED - ${CUOPT_SRC_FILES} -) +# ################################################################################################## +# - cuopt component libraries (STATIC, position-independent for use in the umbrella SHARED) ------ -set_target_properties(cuopt - PROPERTIES BUILD_RPATH "\$ORIGIN" - INSTALL_RPATH "\$ORIGIN" - INTERFACE_POSITION_INDEPENDENT_CODE ON - CXX_SCAN_FOR_MODULES OFF -) +# Helper: apply compile options and common include paths to all cuOpt component libs +function(cuopt_configure_component target) + set_target_properties(${target} PROPERTIES + POSITION_INDEPENDENT_CODE ON + CXX_SCAN_FOR_MODULES OFF + ) + target_compile_options(${target} + PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" + "$<$:${CUOPT_CUDA_FLAGS}>" + ) + target_compile_definitions(${target} + PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" + ) + target_include_directories(${target} + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_BINARY_DIR}" + PUBLIC + "$" + "$" + INTERFACE + "$" + ) +endfunction() -target_compile_definitions(cuopt - PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" - PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API +# Compute git hash and generate build_info.hpp before any component is defined +execute_process( + COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE ) +message("-- Building with GIT_COMMIT_HASH = '${GIT_COMMIT_HASH}'") -target_compile_options(cuopt - PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" - "$<$:${CUOPT_CUDA_FLAGS}>" +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/src/utilities/build_info.hpp.in + ${CMAKE_CURRENT_BINARY_DIR}/include/utilities/build_info.hpp + @ONLY ) -if (WRITE_FATBIN) - file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" - [=[ - SECTIONS - { - .nvFatBinSegment : { *(.nvFatBinSegment) } - .nv_fatbin : { *(.nv_fatbin) } - } - ]=]) - target_link_options(cuopt PRIVATE "${CUOPT_BINARY_DIR}/fatbin.ld") -endif () +list(JOIN CMAKE_CUDA_ARCHITECTURES "," JOINED_CUDA_ARCHITECTURES) -add_library(cuopt::cuopt ALIAS cuopt) -# ################################################################################################## -# - include paths --------------------------------------------------------------------------------- -message(STATUS "target include directories CUDSS_INCLUDES = ${CUDSS_INCLUDE}") +set(CUOPT_PRIVATE_CUDA_LIBS + CUDA::cublasLt + CUDA::curand + CUDA::cusolver + TBB::tbb + OpenMP::OpenMP_CXX) + +get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) +# cuopt_base: utilities + linear algebra (stateful shared infra — logger, work scheduler) +add_library(cuopt_base STATIC ${CUOPT_BASE_SRC_FILES}) +cuopt_configure_component(cuopt_base) +target_include_directories(cuopt_base PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include") +target_compile_definitions(cuopt_base PUBLIC + CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" + CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}" +) +target_link_libraries(cuopt_base + PUBLIC + rmm::rmm + rapids_logger::rapids_logger + CCCL::CCCL + raft::raft + CUDA::cublas + CUDA::cusparse + PRIVATE + OpenMP::OpenMP_CXX + OpenMP::OpenMP_CUDA +) +add_library(cuopt::base ALIAS cuopt_base) + +# cuopt_routing: VRP / routing engine (omitted when SKIP_ROUTING_BUILD=ON) +if(NOT SKIP_ROUTING_BUILD) + add_library(cuopt_routing STATIC ${CUOPT_ROUTING_SRC_FILES}) + cuopt_configure_component(cuopt_routing) + target_link_libraries(cuopt_routing + PUBLIC cuopt_base + PRIVATE simde::simde OpenMP::OpenMP_CXX + ) + add_library(cuopt::routing ALIAS cuopt_routing) +endif() + +# cuopt_lp: LP / MIP / numerical optimization engine +add_library(cuopt_lp STATIC ${CUOPT_LP_SRC_FILES}) +cuopt_configure_component(cuopt_lp) +target_include_directories(cuopt_lp PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty" + "${CMAKE_CURRENT_SOURCE_DIR}/src/io" + "${CUDSS_INCLUDE}" + $<$:${BZIP2_INCLUDE_DIRS}> + $<$:${ZLIB_INCLUDE_DIRS}> +) # Adding Papilo as a system include messes up clang's include resolution if papilo is already installed as a conda package -target_include_directories(cuopt PRIVATE +target_include_directories(cuopt_lp PRIVATE "${papilo_SOURCE_DIR}/src" "${papilo_BINARY_DIR}" ) - -target_include_directories(cuopt SYSTEM PRIVATE +target_include_directories(cuopt_lp SYSTEM PRIVATE "${pslp_SOURCE_DIR}/include" "${dejavu_SOURCE_DIR}" ) +target_compile_definitions(cuopt_lp + PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API + PRIVATE CUDSS_MT_LIB_FILE_NAME="${CUDSS_MT_LIB_FILE_NAME}" +) +target_link_libraries(cuopt_lp + PUBLIC + cuopt_base + ${CUDSS_LIB_FILE} + PRIVATE + ${CUOPT_PRIVATE_CUDA_LIBS} + OpenMP::OpenMP_CUDA +) +target_link_libraries(cuopt_lp PRIVATE $) +add_dependencies(cuopt_lp PSLP) +add_library(cuopt::lp ALIAS cuopt_lp) + +# ################################################################################################## +# - cuopt: umbrella shared library ---------------------------------------------------------------- +# Re-exports all symbols from the three component libs via --whole-archive. +# Backward-compatible: GAMS and any consumer using -lcuopt / libcuopt.so is unaffected. +# gRPC integration layer (maps proto <-> C++ LP API) lives here, not in cuopt_lp. + +message(STATUS "target include directories CUDSS_INCLUDES = ${CUDSS_INCLUDE}") + +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" "// cuopt umbrella\n") + +if(NOT SKIP_GRPC_BUILD) + set(CUOPT_UMBRELLA_EXTRA_SRCS ${GRPC_INFRA_FILES}) +endif() + +add_library(cuopt SHARED + "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" + ${CUOPT_UMBRELLA_EXTRA_SRCS} +) + +set_target_properties(cuopt PROPERTIES + BUILD_RPATH "\$ORIGIN" + INSTALL_RPATH "\$ORIGIN" + INTERFACE_POSITION_INDEPENDENT_CODE ON + CXX_SCAN_FOR_MODULES OFF +) + +target_compile_options(cuopt + PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" + "$<$:${CUOPT_CUDA_FLAGS}>" +) target_include_directories(cuopt 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}" - "${CUDSS_INCLUDE}" - $<$:${BZIP2_INCLUDE_DIRS}> - $<$:${ZLIB_INCLUDE_DIRS}> PUBLIC "$" "$" @@ -580,67 +684,34 @@ target_include_directories(cuopt "$" ) -# Link PSLP by file to avoid export dependency tracking -target_link_libraries(cuopt PRIVATE $) -add_dependencies(cuopt PSLP) - -# ################################################################################################## -# - link libraries -------------------------------------------------------------------------------- - -set(CUOPT_PRIVATE_CUDA_LIBS - CUDA::curand - CUDA::cusolver - TBB::tbb - OpenMP::OpenMP_CXX) - -list(PREPEND CUOPT_PRIVATE_CUDA_LIBS CUDA::cublasLt) - -# Pass CUDSS_MT_LIB_FILE_NAME as a compile definition -get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) -target_compile_definitions(cuopt PRIVATE CUDSS_MT_LIB_FILE_NAME="${CUDSS_MT_LIB_FILE_NAME}") - -execute_process( - COMMAND git rev-parse --short HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE -) -message("-- Building with GIT_COMMIT_HASH = '${GIT_COMMIT_HASH}'") - -# Generate build_info.hpp from template -# configure_file() only updates the output if content changes, avoiding unnecessary rebuilds -configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/src/utilities/build_info.hpp.in - ${CMAKE_CURRENT_BINARY_DIR}/include/utilities/build_info.hpp - @ONLY -) - -# Add the generated include directory -target_include_directories(cuopt PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include) - -list(JOIN CMAKE_CUDA_ARCHITECTURES "," JOINED_CUDA_ARCHITECTURES) -target_compile_definitions(cuopt PUBLIC - CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" - CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}") +if(NOT SKIP_ROUTING_BUILD) + target_link_libraries(cuopt PUBLIC + $) +else() + target_link_libraries(cuopt PUBLIC + $) +endif() target_link_libraries(cuopt - PUBLIC - CUDA::cublas - CUDA::cusparse - rmm::rmm - rapids_logger::rapids_logger - CCCL::CCCL - raft::raft - ${CUDSS_LIB_FILE} PRIVATE - ${CUOPT_PRIVATE_CUDA_LIBS} - simde::simde - OpenMP::OpenMP_CXX - OpenMP::OpenMP_CUDA $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> ) +if (WRITE_FATBIN) + file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" + [=[ + SECTIONS + { + .nvFatBinSegment : { *(.nvFatBinSegment) } + .nv_fatbin : { *(.nv_fatbin) } + } + ]=]) + target_link_options(cuopt PRIVATE "${CUOPT_BINARY_DIR}/fatbin.ld") +endif () + +add_library(cuopt::cuopt ALIAS cuopt) + # ################################################################################################## # - generate tests -------------------------------------------------------------------------------- if (BUILD_TESTS) @@ -669,14 +740,25 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () -# adds the .so files to the runtime deb package +# Install component static libs (runtime: none — statics are build-time only; +# dev: headers are shared, statics go in the dev package for downstream cmake consumers) +set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_lp) +if(NOT SKIP_ROUTING_BUILD) + list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) +endif() +install(TARGETS ${CUOPT_COMPONENT_TARGETS} + DESTINATION ${_LIB_DEST} + COMPONENT dev + EXPORT cuopt-exports +) + +# Install umbrella shared library (the primary runtime artifact) install(TARGETS cuopt DESTINATION ${_LIB_DEST} COMPONENT runtime EXPORT cuopt-exports ) -# adds the .so files to the development deb package install(TARGETS cuopt DESTINATION ${_LIB_DEST} COMPONENT dev @@ -700,12 +782,14 @@ set(doc_string Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. +Component targets: cuopt::base, cuopt::routing, cuopt::lp +Umbrella target: cuopt::cuopt (re-exports all symbols — backward-compatible with -lcuopt) ]=]) rapids_export(INSTALL cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_TARGETS} NAMESPACE cuopt:: DOCUMENTATION doc_string ) @@ -714,7 +798,7 @@ rapids_export(INSTALL cuopt # - build export ------------------------------------------------------------------------------- rapids_export(BUILD cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_TARGETS} NAMESPACE cuopt:: DOCUMENTATION doc_string ) diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index e8737cf6da..8af5cf4dfc 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -25,5 +25,34 @@ add_subdirectory(barrier) add_subdirectory(branch_and_bound) add_subdirectory(cuts) -set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${UTIL_SRC_FILES} PARENT_SCOPE) +# Aggregate per-domain source lists for the three component libraries +set(CUOPT_BASE_SRC_FILES + ${UTIL_SRC_FILES} + ${LINEAR_ALGEBRA_SRC_FILES} +) + +set(CUOPT_ROUTING_SRC_FILES + ${ROUTING_SRC_FILES} +) + +set(CUOPT_LP_SRC_FILES + ${LP_SRC_FILES} + ${MATH_OPT_SRC_FILES} + ${MIP_SRC_FILES} + ${PARSERS_SRC_FILES} + ${DUAL_SIMPLEX_SRC_FILES} + ${BARRIER_SRC_FILES} + ${BRANCH_AND_BOUND_SRC_FILES} + ${CUTS_SRC_FILES} +) + +set(CUOPT_BASE_SRC_FILES ${CUOPT_BASE_SRC_FILES} PARENT_SCOPE) +set(CUOPT_ROUTING_SRC_FILES ${CUOPT_ROUTING_SRC_FILES} PARENT_SCOPE) +set(CUOPT_LP_SRC_FILES ${CUOPT_LP_SRC_FILES} PARENT_SCOPE) +set(CUOPT_SRC_FILES + ${CUOPT_BASE_SRC_FILES} + ${CUOPT_ROUTING_SRC_FILES} + ${CUOPT_LP_SRC_FILES} + PARENT_SCOPE +) set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/barrier/CMakeLists.txt b/cpp/src/barrier/CMakeLists.txt index 650bc733e9..83af7da102 100644 --- a/cpp/src/barrier/CMakeLists.txt +++ b/cpp/src/barrier/CMakeLists.txt @@ -10,5 +10,6 @@ set(BARRIER_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/pinned_host_allocator.cu ) +set(BARRIER_SRC_FILES ${BARRIER_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${BARRIER_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/branch_and_bound/CMakeLists.txt b/cpp/src/branch_and_bound/CMakeLists.txt index 1e40c1bbf1..c9f47c4d55 100644 --- a/cpp/src/branch_and_bound/CMakeLists.txt +++ b/cpp/src/branch_and_bound/CMakeLists.txt @@ -10,5 +10,6 @@ set(BRANCH_AND_BOUND_SRC_FILES ) +set(BRANCH_AND_BOUND_SRC_FILES ${BRANCH_AND_BOUND_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${BRANCH_AND_BOUND_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/cuts/CMakeLists.txt b/cpp/src/cuts/CMakeLists.txt index 813ac88a59..07d945135c 100644 --- a/cpp/src/cuts/CMakeLists.txt +++ b/cpp/src/cuts/CMakeLists.txt @@ -8,5 +8,6 @@ set(CUTS_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/objective_step.cpp ) +set(CUTS_SRC_FILES ${CUTS_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${CUTS_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/dual_simplex/CMakeLists.txt b/cpp/src/dual_simplex/CMakeLists.txt index 228f2aedd7..1f424c69ab 100644 --- a/cpp/src/dual_simplex/CMakeLists.txt +++ b/cpp/src/dual_simplex/CMakeLists.txt @@ -25,5 +25,6 @@ set(DUAL_SIMPLEX_SRC_FILES # Uncomment to enable debug info #set_source_files_properties(${DUAL_SIMPLEX_SRC_FILES} DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") +set(DUAL_SIMPLEX_SRC_FILES ${DUAL_SIMPLEX_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${DUAL_SIMPLEX_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/io/CMakeLists.txt b/cpp/src/io/CMakeLists.txt index cafcffb23f..29bd8dc1e2 100644 --- a/cpp/src/io/CMakeLists.txt +++ b/cpp/src/io/CMakeLists.txt @@ -23,5 +23,6 @@ set(PARSERS_SRC_FILES ${MPS_FAST_SRC_FILES} ) -set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE) +set(PARSERS_SRC_FILES ${PARSERS_SRC_FILES} PARENT_SCOPE) set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE) +set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/linear_algebra/CMakeLists.txt b/cpp/src/linear_algebra/CMakeLists.txt index 875a016544..7a8ad1fa00 100644 --- a/cpp/src/linear_algebra/CMakeLists.txt +++ b/cpp/src/linear_algebra/CMakeLists.txt @@ -9,5 +9,6 @@ set(LINEAR_ALGEBRA_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/vector_math.cpp ) +set(LINEAR_ALGEBRA_SRC_FILES ${LINEAR_ALGEBRA_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${LINEAR_ALGEBRA_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/math_optimization/CMakeLists.txt b/cpp/src/math_optimization/CMakeLists.txt index efa1600c54..3a118cf163 100644 --- a/cpp/src/math_optimization/CMakeLists.txt +++ b/cpp/src/math_optimization/CMakeLists.txt @@ -11,5 +11,6 @@ list(PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/tic_toc.cpp ) +set(MATH_OPT_SRC_FILES ${MATH_OPT_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${MATH_OPT_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/mip_heuristics/CMakeLists.txt b/cpp/src/mip_heuristics/CMakeLists.txt index 7705465512..4b8bcfd3a8 100644 --- a/cpp/src/mip_heuristics/CMakeLists.txt +++ b/cpp/src/mip_heuristics/CMakeLists.txt @@ -53,5 +53,6 @@ else() set(MIP_SRC_FILES ${MIP_LP_NECESSARY_FILES} ${MIP_NON_LP_FILES}) endif() +set(MIP_SRC_FILES ${MIP_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${MIP_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index f5f26837b6..c90332355b 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -44,4 +44,5 @@ else() set(LP_SRC_FILES ${LP_CORE_FILES} ${LP_ADAPTER_FILES}) endif() +set(LP_SRC_FILES ${LP_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${LP_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/routing/CMakeLists.txt b/cpp/src/routing/CMakeLists.txt index 452c4806da..c92b4d0f3f 100644 --- a/cpp/src/routing/CMakeLists.txt +++ b/cpp/src/routing/CMakeLists.txt @@ -50,4 +50,5 @@ set(ROUTING_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/utilities/check_input.cu ${CMAKE_CURRENT_SOURCE_DIR}/utilities/cython.cu) +set(ROUTING_SRC_FILES ${ROUTING_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${ROUTING_SRC_FILES} PARENT_SCOPE) From 94755a05d690f1c61458d2caf81af4316f6cd63d Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 24 Jul 2026 09:52:31 -0500 Subject: [PATCH 02/40] build(cmake): fix component library linking and include propagation Four issues found while validating the library split locally: - cuopt_routing was missing OpenMP::OpenMP_CUDA, causing routing CUDA files to reject #pragma omp directives as unknown in CUDA compiler mode - cuopt_lp was missing simde::simde, required by the fast MPS parser (io/experimental_mps_fast/) which uses SIMD intrinsics via simde headers - The umbrella cuopt target was missing src/io in its private include dirs, causing gRPC mapper files (grpc_problem_mapper.cpp) that include mps_parser_internal.hpp to fail to compile - WHOLE_ARCHIVE linkage on the umbrella was PUBLIC, propagating the static sub-libs as link dependencies to all consumers (test binaries). This caused double-definition errors when tests linked both libcuopt.so and the statics. Changed to PRIVATE and re-exposed the statics' transitive PUBLIC deps (rmm, raft, CCCL, CUDA libs) directly on the umbrella so that consumers receive the correct source-fetched include dirs. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 699b727655..380dc294f6 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -599,7 +599,7 @@ if(NOT SKIP_ROUTING_BUILD) cuopt_configure_component(cuopt_routing) target_link_libraries(cuopt_routing PUBLIC cuopt_base - PRIVATE simde::simde OpenMP::OpenMP_CXX + PRIVATE simde::simde OpenMP::OpenMP_CXX OpenMP::OpenMP_CUDA ) add_library(cuopt::routing ALIAS cuopt_routing) endif() @@ -634,6 +634,7 @@ target_link_libraries(cuopt_lp PRIVATE ${CUOPT_PRIVATE_CUDA_LIBS} OpenMP::OpenMP_CUDA + simde::simde ) target_link_libraries(cuopt_lp PRIVATE $) add_dependencies(cuopt_lp PSLP) @@ -673,6 +674,7 @@ target_compile_options(cuopt target_include_directories(cuopt PRIVATE "${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" @@ -685,18 +687,34 @@ target_include_directories(cuopt ) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt PUBLIC + target_link_libraries(cuopt PRIVATE $) else() - target_link_libraries(cuopt PUBLIC + target_link_libraries(cuopt PRIVATE $) endif() +# Re-expose the component statics' PUBLIC deps so that consumers of cuopt::cuopt +# (tests, downstream cmake, GAMS) receive the correct include dirs and link targets. target_link_libraries(cuopt + PUBLIC + rmm::rmm + rapids_logger::rapids_logger + CCCL::CCCL + raft::raft + CUDA::cublas + CUDA::cusparse + ${CUDSS_LIB_FILE} PRIVATE $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> ) +target_compile_definitions(cuopt PUBLIC + "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" + CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" + CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}" + CUSPARSE_ENABLE_EXPERIMENTAL_API +) if (WRITE_FATBIN) file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" From 0390d40edbdcced133ee4e8eac4ceb8e0387dfee Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 24 Jul 2026 13:48:52 -0500 Subject: [PATCH 03/40] build(conda): verify component static libs in libcuopt package_contents Add libcuopt_base.a, libcuopt_routing.a, and libcuopt_lp.a to the package_contents file check so CI fails fast if any of the three component static libraries are missing from the installed package. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- conda/recipes/libcuopt/recipe.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 77c957eade..475fafba73 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -162,6 +162,9 @@ outputs: - package_contents: files: - lib/libcuopt.so + - lib/libcuopt_base.a + - lib/libcuopt_routing.a + - lib/libcuopt_lp.a - bin/cuopt_cli - bin/cuopt_grpc_server about: From efc5256c70cd4df058dca90962e2ab250918eafa Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 27 Jul 2026 10:08:35 -0500 Subject: [PATCH 04/40] build(cmake): switch component libs to SHARED and move gRPC bridge into cuopt_lp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Component libs (cuopt_base, cuopt_routing, cuopt_lp) are now SHARED instead of STATIC. The umbrella libcuopt.so becomes a thin stub (~15 KB) carrying only DT_NEEDED entries for the three component libs; no code or WHOLE_ARCHIVE baking. The gRPC bridge (mapper + Cython client) moves from the umbrella into cuopt_lp where it semantically belongs — LP/MIP remote solve is an LP concern. The umbrella drops all gRPC sources, include dirs, and protobuf/gRPC link deps. Both RPATH settings use $ORIGIN so component libs find each other when co-installed. Conda package_contents check updated from .a to .so. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- conda/recipes/libcuopt/recipe.yaml | 6 +-- cpp/CMakeLists.txt | 86 +++++++++--------------------- 2 files changed, 28 insertions(+), 64 deletions(-) diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 475fafba73..f794204c75 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -162,9 +162,9 @@ outputs: - package_contents: files: - lib/libcuopt.so - - lib/libcuopt_base.a - - lib/libcuopt_routing.a - - lib/libcuopt_lp.a + - lib/libcuopt_base.so + - lib/libcuopt_routing.so + - lib/libcuopt_lp.so - bin/cuopt_cli - bin/cuopt_grpc_server about: diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 380dc294f6..9ebc097d31 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -492,7 +492,7 @@ if (DEFINE_ASSERT) endif () if (NOT SKIP_GRPC_BUILD) - # gRPC integration layer: maps proto <-> C++ LP API; lives in the umbrella, not in cuopt_lp + # gRPC integration layer: maps proto <-> C++ LP API; compiled into cuopt_lp set(GRPC_INFRA_FILES ${DATA_PROTO_SRCS} ${PROTO_SRCS} @@ -523,8 +523,9 @@ endif (NOT SKIP_GRPC_BUILD) # Helper: apply compile options and common include paths to all cuOpt component libs function(cuopt_configure_component target) set_target_properties(${target} PROPERTIES - POSITION_INDEPENDENT_CODE ON CXX_SCAN_FOR_MODULES OFF + BUILD_RPATH_USE_ORIGIN TRUE + INSTALL_RPATH "\$ORIGIN" ) target_compile_options(${target} PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" @@ -572,7 +573,7 @@ set(CUOPT_PRIVATE_CUDA_LIBS get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) # cuopt_base: utilities + linear algebra (stateful shared infra — logger, work scheduler) -add_library(cuopt_base STATIC ${CUOPT_BASE_SRC_FILES}) +add_library(cuopt_base SHARED ${CUOPT_BASE_SRC_FILES}) cuopt_configure_component(cuopt_base) target_include_directories(cuopt_base PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include") target_compile_definitions(cuopt_base PUBLIC @@ -595,7 +596,7 @@ add_library(cuopt::base ALIAS cuopt_base) # cuopt_routing: VRP / routing engine (omitted when SKIP_ROUTING_BUILD=ON) if(NOT SKIP_ROUTING_BUILD) - add_library(cuopt_routing STATIC ${CUOPT_ROUTING_SRC_FILES}) + add_library(cuopt_routing SHARED ${CUOPT_ROUTING_SRC_FILES}) cuopt_configure_component(cuopt_routing) target_link_libraries(cuopt_routing PUBLIC cuopt_base @@ -605,7 +606,7 @@ if(NOT SKIP_ROUTING_BUILD) endif() # cuopt_lp: LP / MIP / numerical optimization engine -add_library(cuopt_lp STATIC ${CUOPT_LP_SRC_FILES}) +add_library(cuopt_lp SHARED ${CUOPT_LP_SRC_FILES}) cuopt_configure_component(cuopt_lp) target_include_directories(cuopt_lp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty" @@ -638,47 +639,36 @@ target_link_libraries(cuopt_lp ) target_link_libraries(cuopt_lp PRIVATE $) add_dependencies(cuopt_lp PSLP) +if(NOT SKIP_GRPC_BUILD) + target_sources(cuopt_lp PRIVATE ${GRPC_INFRA_FILES}) + target_include_directories(cuopt_lp PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc" + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/client" + "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated" + ) + target_link_libraries(cuopt_lp PRIVATE protobuf::libprotobuf gRPC::grpc++) +endif() add_library(cuopt::lp ALIAS cuopt_lp) # ################################################################################################## -# - cuopt: umbrella shared library ---------------------------------------------------------------- -# Re-exports all symbols from the three component libs via --whole-archive. -# Backward-compatible: GAMS and any consumer using -lcuopt / libcuopt.so is unaffected. -# gRPC integration layer (maps proto <-> C++ LP API) lives here, not in cuopt_lp. +# - cuopt: thin umbrella shared library ----------------------------------------------------------- +# Links publicly against the three component shared libs so that -lcuopt keeps working for +# all existing consumers. No code of its own except the gRPC bridge (mapper + client) which +# sits here because it spans both the LP API and gRPC transport layers. message(STATUS "target include directories CUDSS_INCLUDES = ${CUDSS_INCLUDE}") -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" "// cuopt umbrella\n") +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" "// thin umbrella\n") -if(NOT SKIP_GRPC_BUILD) - set(CUOPT_UMBRELLA_EXTRA_SRCS ${GRPC_INFRA_FILES}) -endif() - -add_library(cuopt SHARED - "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" - ${CUOPT_UMBRELLA_EXTRA_SRCS} -) +add_library(cuopt SHARED "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp") set_target_properties(cuopt PROPERTIES - BUILD_RPATH "\$ORIGIN" + BUILD_RPATH_USE_ORIGIN TRUE INSTALL_RPATH "\$ORIGIN" - INTERFACE_POSITION_INDEPENDENT_CODE ON CXX_SCAN_FOR_MODULES OFF ) -target_compile_options(cuopt - PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" - "$<$:${CUOPT_CUDA_FLAGS}>" -) - target_include_directories(cuopt - PRIVATE - "${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}" PUBLIC "$" "$" @@ -687,35 +677,11 @@ target_include_directories(cuopt ) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt PRIVATE - $) + target_link_libraries(cuopt PUBLIC cuopt_base cuopt_routing cuopt_lp) else() - target_link_libraries(cuopt PRIVATE - $) + target_link_libraries(cuopt PUBLIC cuopt_base cuopt_lp) endif() -# Re-expose the component statics' PUBLIC deps so that consumers of cuopt::cuopt -# (tests, downstream cmake, GAMS) receive the correct include dirs and link targets. -target_link_libraries(cuopt - PUBLIC - rmm::rmm - rapids_logger::rapids_logger - CCCL::CCCL - raft::raft - CUDA::cublas - CUDA::cusparse - ${CUDSS_LIB_FILE} - PRIVATE - $<$:protobuf::libprotobuf> - $<$:gRPC::grpc++> -) -target_compile_definitions(cuopt PUBLIC - "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" - CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" - CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}" - CUSPARSE_ENABLE_EXPERIMENTAL_API -) - if (WRITE_FATBIN) file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" [=[ @@ -758,15 +724,13 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () -# Install component static libs (runtime: none — statics are build-time only; -# dev: headers are shared, statics go in the dev package for downstream cmake consumers) set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_lp) if(NOT SKIP_ROUTING_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) endif() install(TARGETS ${CUOPT_COMPONENT_TARGETS} DESTINATION ${_LIB_DEST} - COMPONENT dev + COMPONENT runtime EXPORT cuopt-exports ) From 08c8db47e01ba7e8f60e32af634fb34a3cf4e820 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 27 Jul 2026 14:33:43 -0500 Subject: [PATCH 05/40] build(cmake): extract gRPC bridge into libcuopt_grpc.so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves all gRPC infrastructure (proto mappers, Cython client, solve_remote) from cuopt_lp into a new cuopt_grpc SHARED component. cuopt_grpc links cuopt_lp + cuopt_routing (when built), keeping both core solver libs free of any gRPC/protobuf dependency. The grpc_server binary now links cuopt_grpc directly. The umbrella links cuopt_grpc when gRPC is built so -lcuopt continues to expose remote-solve symbols to existing consumers. When PR #1597 (VRP gRPC) lands, routing gRPC sources go into cuopt_grpc alongside the LP ones — no cross-dependency between cuopt_lp and cuopt_routing is needed. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- conda/recipes/libcuopt/recipe.yaml | 1 + cpp/CMakeLists.txt | 33 ++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index f794204c75..280f425734 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -165,6 +165,7 @@ outputs: - lib/libcuopt_base.so - lib/libcuopt_routing.so - lib/libcuopt_lp.so + - lib/libcuopt_grpc.so - bin/cuopt_cli - bin/cuopt_grpc_server about: diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 9ebc097d31..80c4c32baf 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -492,7 +492,7 @@ if (DEFINE_ASSERT) endif () if (NOT SKIP_GRPC_BUILD) - # gRPC integration layer: maps proto <-> C++ LP API; compiled into cuopt_lp + # gRPC integration layer: maps proto <-> C++ LP/routing APIs; compiled into cuopt_grpc set(GRPC_INFRA_FILES ${DATA_PROTO_SRCS} ${PROTO_SRCS} @@ -639,16 +639,27 @@ target_link_libraries(cuopt_lp ) target_link_libraries(cuopt_lp PRIVATE $) add_dependencies(cuopt_lp PSLP) +add_library(cuopt::lp ALIAS cuopt_lp) + +# cuopt_grpc: gRPC bridge — proto mappers + Cython client (LP and routing over gRPC) +# Depends on cuopt_lp and cuopt_routing so it can reference both APIs without forcing +# either component to take a gRPC dependency. if(NOT SKIP_GRPC_BUILD) - target_sources(cuopt_lp PRIVATE ${GRPC_INFRA_FILES}) - target_include_directories(cuopt_lp PRIVATE + add_library(cuopt_grpc SHARED ${GRPC_INFRA_FILES}) + cuopt_configure_component(cuopt_grpc) + target_include_directories(cuopt_grpc PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc" "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/client" "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated" + "${CMAKE_CURRENT_SOURCE_DIR}/src/io" ) - target_link_libraries(cuopt_lp PRIVATE protobuf::libprotobuf gRPC::grpc++) + if(NOT SKIP_ROUTING_BUILD) + target_link_libraries(cuopt_grpc PUBLIC cuopt_lp cuopt_routing PRIVATE protobuf::libprotobuf gRPC::grpc++) + else() + target_link_libraries(cuopt_grpc PUBLIC cuopt_lp PRIVATE protobuf::libprotobuf gRPC::grpc++) + endif() + add_library(cuopt::grpc ALIAS cuopt_grpc) endif() -add_library(cuopt::lp ALIAS cuopt_lp) # ################################################################################################## # - cuopt: thin umbrella shared library ----------------------------------------------------------- @@ -681,6 +692,9 @@ if(NOT SKIP_ROUTING_BUILD) else() target_link_libraries(cuopt PUBLIC cuopt_base cuopt_lp) endif() +if(NOT SKIP_GRPC_BUILD) + target_link_libraries(cuopt PUBLIC cuopt_grpc) +endif() if (WRITE_FATBIN) file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" @@ -728,6 +742,9 @@ set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_lp) if(NOT SKIP_ROUTING_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) endif() +if(NOT SKIP_GRPC_BUILD) + list(APPEND CUOPT_COMPONENT_TARGETS cuopt_grpc) +endif() install(TARGETS ${CUOPT_COMPONENT_TARGETS} DESTINATION ${_LIB_DEST} COMPONENT runtime @@ -764,8 +781,8 @@ set(doc_string Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. -Component targets: cuopt::base, cuopt::routing, cuopt::lp -Umbrella target: cuopt::cuopt (re-exports all symbols — backward-compatible with -lcuopt) +Component targets: cuopt::base, cuopt::routing, cuopt::lp, cuopt::grpc (when gRPC is built) +Umbrella target: cuopt::cuopt (links all component libs — backward-compatible with -lcuopt) ]=]) @@ -967,7 +984,7 @@ if (NOT SKIP_GRPC_BUILD) target_link_libraries(cuopt_grpc_server PUBLIC - cuopt + cuopt_grpc OpenMP::OpenMP_CXX PRIVATE protobuf::libprotobuf From a54e62a5b5abc992bbc6ce1763f9b150df4ddfe4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 27 Jul 2026 15:36:57 -0500 Subject: [PATCH 06/40] chore: fix copyright year in routing CMakeLists.txt Co-Authored-By: Claude Sonnet 4.6 --- cpp/src/routing/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/routing/CMakeLists.txt b/cpp/src/routing/CMakeLists.txt index c92b4d0f3f..fa9653076b 100644 --- a/cpp/src/routing/CMakeLists.txt +++ b/cpp/src/routing/CMakeLists.txt @@ -1,5 +1,5 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on From 6c24d1f6af468facaa91874754814a8777e0df7f Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 27 Jul 2026 16:23:25 -0500 Subject: [PATCH 07/40] fix(ci): exclude component libs from auditwheel repair in cuopt wheel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit libcuopt.so is now a thin umbrella with DT_NEEDED on libcuopt_base.so, libcuopt_routing.so, libcuopt_lp.so, and libcuopt_grpc.so. auditwheel traverses DT_NEEDED transitively and failed when it couldn't locate the component libs. Exclude them the same way libcuopt.so is excluded — they ship with the libcuopt wheel and are available at runtime. Co-Authored-By: Claude Sonnet 4.6 --- ci/build_wheel_cuopt.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index f624b27705..449bc19665 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -41,6 +41,10 @@ EXCLUDE_ARGS=( --exclude "libcusolver.so.*" --exclude "libcusparse.so.*" --exclude "libcuopt.so" + --exclude "libcuopt_base.so" + --exclude "libcuopt_routing.so" + --exclude "libcuopt_lp.so" + --exclude "libcuopt_grpc.so" --exclude "librapids_logger.so" --exclude "librmm.so" ) From 705167fa69e55fe7f64d15a55db026f0d336af71 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 27 Jul 2026 16:32:29 -0500 Subject: [PATCH 08/40] fix(cmake): include gRPC sources in cuopt_objs for cuopt_static test builds main appends GRPC_INFRA_FILES to CUOPT_SRC_FILES before creating cuopt_objs so cuopt_static (used by NUMOPT_INTERNAL_TEST) gets solve_lp_remote / solve_mip_remote. We dropped that line when we moved those files into cuopt_grpc, causing undefined-reference link failures in tests. Co-Authored-By: Claude Sonnet 4.6 --- cpp/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 9d9d74198a..cfb05e146c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -515,6 +515,9 @@ if (NOT SKIP_GRPC_BUILD) # at runtime with "undefined symbol: absl::…::Mutex::Dtor". set_property(SOURCE ${GRPC_INFRA_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY COMPILE_OPTIONS "-DNDEBUG") + # Include gRPC sources in CUOPT_SRC_FILES so cuopt_objs (and cuopt_static for tests) + # have solve_lp_remote / solve_mip_remote defined when CUOPT_ENABLE_GRPC is set. + list(APPEND CUOPT_SRC_FILES ${GRPC_INFRA_FILES}) endif (NOT SKIP_GRPC_BUILD) # ################################################################################################## From a42419b499ce83fa8baa6aeb087a27ee97d757ac Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 28 Jul 2026 10:40:56 -0500 Subject: [PATCH 09/40] fix(cmake): link cuopt_cli against cuopt_grpc for runtime symbol resolution cuopt_lp.so calls solve_lp/mip_remote (under CUOPT_ENABLE_GRPC) which are defined in cuopt_grpc.so. With --as-needed the linker was dropping libcuopt_grpc.so from executables that never directly referenced a grpc symbol, leaving solve_lp_remote unresolved at runtime. Route remote solves in cuopt_cli directly through solve_lp/mip_remote so libcuopt_grpc.so is a genuine DT_NEEDED of the binary; --as-needed then keeps it in the link and the symbol is in scope when libcuopt_lp.so needs it. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 1 + cpp/cuopt_cli.cpp | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index cfb05e146c..5270c70ca2 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -981,6 +981,7 @@ if (NOT BUILD_LP_ONLY) target_link_libraries(cuopt_cli PUBLIC cuopt + $<$:cuopt_grpc> OpenMP::OpenMP_CXX ${CUDSS_LIBRARIES} TBB::tbb diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index feb0e8cd76..992c7f455a 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -175,7 +176,22 @@ int run_single_file(const std::string& file_path, } try { - if (is_mip) { + if (memory_backend == cuopt::mathematical_optimization::memory_backend_t::CPU) { + // Remote execution: problem_interface holds a cpu_optimization_problem_t. + // Call solve_lp/mip_remote directly so libcuopt_grpc.so is a real DT_NEEDED + // dependency of this binary rather than an implicit runtime lookup. + auto* cpu_prob = static_cast*>(problem_interface.get()); + if (is_mip) { + auto& mip_settings = settings.get_mip_settings(); + auto solution = + cuopt::mathematical_optimization::solve_mip_remote(*cpu_prob, mip_settings); + } else { + auto& lp_settings = settings.get_pdlp_settings(); + auto solution = + cuopt::mathematical_optimization::solve_lp_remote(*cpu_prob, lp_settings); + } + } else if (is_mip) { auto& mip_settings = settings.get_mip_settings(); auto solution = cuopt::mathematical_optimization::solve_mip(problem_interface.get(), mip_settings); From 776bd3bae6e14fbeb290bd15765b996a259d49eb Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 28 Jul 2026 12:18:59 -0500 Subject: [PATCH 10/40] fix(cli): use is_remote_execution_enabled() not memory_backend==CPU memory_backend_t::CPU fires on any CPU-only host even when CUOPT_REMOTE_HOST is not set, incorrectly routing local solves through the gRPC client path. is_remote_execution_enabled() checks CUOPT_REMOTE_HOST + CUOPT_REMOTE_PORT and is the correct guard. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/cuopt_cli.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 992c7f455a..2374fe7583 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -176,7 +176,7 @@ int run_single_file(const std::string& file_path, } try { - if (memory_backend == cuopt::mathematical_optimization::memory_backend_t::CPU) { + if (cuopt::mathematical_optimization::is_remote_execution_enabled()) { // Remote execution: problem_interface holds a cpu_optimization_problem_t. // Call solve_lp/mip_remote directly so libcuopt_grpc.so is a real DT_NEEDED // dependency of this binary rather than an implicit runtime lookup. From 8511bd039dba987c7fe40c8f4276055c30ed8ebf Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 28 Jul 2026 14:19:08 -0500 Subject: [PATCH 11/40] style: apply clang-format to cuopt_cli.cpp Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/cuopt_cli.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 153ee59385..f2ee18980a 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -200,16 +200,15 @@ int run_single_file(const std::string& file_path, // Remote execution: problem_interface holds a cpu_optimization_problem_t. // Call solve_lp/mip_remote directly so libcuopt_grpc.so is a real DT_NEEDED // dependency of this binary rather than an implicit runtime lookup. - auto* cpu_prob = static_cast*>(problem_interface.get()); + auto* cpu_prob = + static_cast*>( + problem_interface.get()); if (is_mip) { auto& mip_settings = settings.get_mip_settings(); - auto solution = - cuopt::mathematical_optimization::solve_mip_remote(*cpu_prob, mip_settings); + auto solution = cuopt::mathematical_optimization::solve_mip_remote(*cpu_prob, mip_settings); } else { auto& lp_settings = settings.get_pdlp_settings(); - auto solution = - cuopt::mathematical_optimization::solve_lp_remote(*cpu_prob, lp_settings); + auto solution = cuopt::mathematical_optimization::solve_lp_remote(*cpu_prob, lp_settings); } } else if (is_mip) { auto& mip_settings = settings.get_mip_settings(); From f2b22e7d8868f031cd8b0a44cad2d774942f117a Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 28 Jul 2026 15:00:58 -0500 Subject: [PATCH 12/40] fix(build): link nccl_external into cuopt_lp distributed_pdlp files (multi_gpu_engine.cu, distributed_algorithms.cu) use NCCL APIs and are compiled into cuopt_lp. After the library split, cuopt_lp must declare its own NCCL dependency rather than inheriting it from the monolithic cuopt_static target. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 1ab920cce9..fbd8646f3d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -677,6 +677,7 @@ target_link_libraries(cuopt_lp ${CUOPT_PRIVATE_CUDA_LIBS} OpenMP::OpenMP_CUDA simde::simde + nccl_external ) target_link_libraries(cuopt_lp PRIVATE $) add_dependencies(cuopt_lp PSLP) From 90ee992ebec2f7e007c4ada26692521a369497b0 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 29 Jul 2026 09:25:04 -0500 Subject: [PATCH 13/40] =?UTF-8?q?fix(build):=20break=20circular=20libcuopt?= =?UTF-8?q?=5Flp=20=E2=86=92=20libcuopt=5Fgrpc=20symbol=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit solve.cu (LP) called solve_lp_remote/solve_mip_remote directly under CUOPT_ENABLE_GRPC. Those symbols live in libcuopt_grpc.so, which itself depends on libcuopt_lp.so — a circular dependency that caused PDLP_MG_TEST, docs, and server tests to fail when loading libcuopt_lp.so without libcuopt_grpc.so present. Fix: introduce a function-pointer registry in libcuopt_lp.so. - remote_solve_registry.cpp defines g_solve_lp_remote_fn / g_solve_mip_remote_fn (nullptr until gRPC is loaded) and register_remote_solvers(). - grpc_registration.cpp registers the real implementations via a __attribute__((constructor)) that fires when libcuopt_grpc.so is dlopen'd. - solve.cu (LP and MIP) call through the function pointers; a clear RuntimeError is raised if remote execution is requested but the gRPC component is not loaded. Also fix the -lcuopt C API contract: cuopt_umbrella.cpp now references cuOptDestroyProblem so --as-needed keeps libcuopt_lp.so in libcuopt.so's DT_NEEDED, allowing C programs to link with -lcuopt as before. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 11 ++++- .../remote_solve_registry.hpp | 40 +++++++++++++++++++ cpp/src/grpc/client/grpc_registration.cpp | 19 +++++++++ cpp/src/mip_heuristics/solve.cu | 14 +++---- cpp/src/pdlp/CMakeLists.txt | 1 + cpp/src/pdlp/remote_solve_registry.cpp | 17 ++++++++ cpp/src/pdlp/solve.cu | 13 +++--- 7 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp create mode 100644 cpp/src/grpc/client/grpc_registration.cpp create mode 100644 cpp/src/pdlp/remote_solve_registry.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index fbd8646f3d..8c9b7f7f5d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -544,6 +544,7 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/client/grpc_client_env.cpp src/grpc/client/cython_grpc_client.cpp src/grpc/client/solve_remote.cpp + src/grpc/client/grpc_registration.cpp ) # Always keep NDEBUG defined for gRPC infrastructure files so that abseil @@ -861,7 +862,15 @@ endif (BUILD_TESTS) # Links publicly against the four component shared libs so that -lcuopt keeps working for # all existing consumers. -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" "// thin umbrella\n") +# Reference cuOptDestroyProblem (from libcuopt_lp.so) so --as-needed keeps +# libcuopt_lp.so in DT_NEEDED, preserving the -lcuopt C API contract for users +# who compile C programs with -lcuopt. +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" [=[ +// Thin umbrella. References cuOptDestroyProblem so --as-needed keeps +// libcuopt_lp.so in DT_NEEDED, preserving the -lcuopt C API contract. +extern "C" void cuOptDestroyProblem(void*); +__attribute__((used)) void (* const _cuopt_lp_anchor)(void*) = &cuOptDestroyProblem; +]=]) add_library(cuopt SHARED "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp") add_library(cuopt::cuopt ALIAS cuopt) diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp new file mode 100644 index 0000000000..c0723d49c4 --- /dev/null +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -0,0 +1,40 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +// Forward declarations — full types live in libcuopt_lp / libcuopt_grpc headers. +namespace cuopt::mathematical_optimization { + +template +class cpu_optimization_problem_t; + +template +struct pdlp_solver_settings_t; + +template +struct mip_solver_settings_t; + +// Function pointer types — only the instantiation is supported. +using solve_lp_remote_fn_t = std::unique_ptr> (*)( + cpu_optimization_problem_t const&, pdlp_solver_settings_t const&); + +using solve_mip_remote_fn_t = std::unique_ptr> (*)( + cpu_optimization_problem_t const&, mip_solver_settings_t const&); + +// Defined in libcuopt_lp.so (remote_solve_registry.cpp). +// Set to nullptr until libcuopt_grpc.so is loaded and calls register_remote_solvers(). +extern solve_lp_remote_fn_t g_solve_lp_remote_fn; +extern solve_mip_remote_fn_t g_solve_mip_remote_fn; + +// Called by libcuopt_grpc.so's constructor to wire up the real implementations. +void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn); + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/grpc/client/grpc_registration.cpp b/cpp/src/grpc/client/grpc_registration.cpp new file mode 100644 index 0000000000..5bf4ef0c45 --- /dev/null +++ b/cpp/src/grpc/client/grpc_registration.cpp @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Registers the gRPC-based remote solve implementations with libcuopt_lp.so +// at dynamic-link time (before any user code runs). This breaks the circular +// dependency: libcuopt_lp.so holds nullable function pointers rather than a +// hard reference to symbols in libcuopt_grpc.so. + +#include +#include + +namespace { +__attribute__((constructor)) void register_grpc_remote_solvers() +{ + cuopt::mathematical_optimization::register_remote_solvers( + &cuopt::mathematical_optimization::solve_lp_remote, + &cuopt::mathematical_optimization::solve_mip_remote); +} +} // namespace diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 64d78efbc0..07db7f54c7 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -6,7 +6,7 @@ /* clang-format on */ #include -#include +#include #include #include @@ -908,20 +908,16 @@ std::unique_ptr> solve_mip( try { // Check if remote execution is enabled (always uses CPU backend) -#ifdef CUOPT_ENABLE_GRPC if (is_remote_execution_enabled()) { auto* cpu_prob = dynamic_cast*>(problem_interface); cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - return solve_mip_remote(*cpu_prob, settings); + cuopt_expects(g_solve_mip_remote_fn != nullptr, + error_type_t::RuntimeError, + "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); + return g_solve_mip_remote_fn(*cpu_prob, settings); } -#else - cuopt_expects( - !is_remote_execution_enabled(), - error_type_t::ValidationError, - "Remote execution was requested, but this build was compiled without gRPC support"); -#endif // Local execution - dispatch to appropriate overload based on problem type auto* cpu_prob = dynamic_cast*>(problem_interface); diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index f9b40b34c1..6b5ac9ebf6 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -5,6 +5,7 @@ # Core LP files always included set(LP_CORE_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/remote_solve_registry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu ${CMAKE_CURRENT_SOURCE_DIR}/optimization_problem.cu ${CMAKE_CURRENT_SOURCE_DIR}/cpu_optimization_problem.cpp diff --git a/cpp/src/pdlp/remote_solve_registry.cpp b/cpp/src/pdlp/remote_solve_registry.cpp new file mode 100644 index 0000000000..511c0046d0 --- /dev/null +++ b/cpp/src/pdlp/remote_solve_registry.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include + +namespace cuopt::mathematical_optimization { + +solve_lp_remote_fn_t g_solve_lp_remote_fn = nullptr; +solve_mip_remote_fn_t g_solve_mip_remote_fn = nullptr; + +void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn) +{ + g_solve_lp_remote_fn = lp_fn; + g_solve_mip_remote_fn = mip_fn; +} + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 2f33a7f8ae..9e643ba865 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -6,7 +6,7 @@ /* clang-format on */ #include -#include +#include #include #include #include @@ -2682,7 +2682,6 @@ std::unique_ptr> solve_lp( "problem_interface cannot be null"); // Check if remote execution is enabled (always uses CPU backend) -#ifdef CUOPT_ENABLE_GRPC if (is_remote_execution_enabled()) { cuopt_expects(!is_batch_mode, error_type_t::ValidationError, @@ -2692,13 +2691,11 @@ std::unique_ptr> solve_lp( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - return solve_lp_remote(*cpu_prob, settings); + cuopt_expects(g_solve_lp_remote_fn != nullptr, + error_type_t::RuntimeError, + "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); + return g_solve_lp_remote_fn(*cpu_prob, settings); } -#else - cuopt_expects(!is_remote_execution_enabled(), - error_type_t::ValidationError, - "Remote execution was requested, but this build was compiled without gRPC support"); -#endif // Local execution - dispatch to appropriate overload based on problem type auto* cpu_prob = dynamic_cast*>(problem_interface); From a83e0f4cb2581462c01000dff024db73847daef9 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 29 Jul 2026 13:00:52 -0500 Subject: [PATCH 14/40] fix(build): add libcuopt_grpc.so anchor to umbrella DT_NEEDED The grpc_registration constructor (which wires up g_solve_lp_remote_fn) only fires when libcuopt_grpc.so is loaded. Programs that link with -lcuopt must therefore have libcuopt_grpc.so in the load chain. Reference solve_lp_remote in cuopt_umbrella.cpp so --as-needed keeps libcuopt_grpc.so in libcuopt.so's DT_NEEDED alongside libcuopt_lp.so. Any binary that links with -lcuopt now gets both component libs loaded automatically, and the remote-solve function pointers are registered before user code runs. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 8c9b7f7f5d..97d61cf0e3 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -862,16 +862,33 @@ endif (BUILD_TESTS) # Links publicly against the four component shared libs so that -lcuopt keeps working for # all existing consumers. -# Reference cuOptDestroyProblem (from libcuopt_lp.so) so --as-needed keeps -# libcuopt_lp.so in DT_NEEDED, preserving the -lcuopt C API contract for users -# who compile C programs with -lcuopt. -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" [=[ -// Thin umbrella. References cuOptDestroyProblem so --as-needed keeps -// libcuopt_lp.so in DT_NEEDED, preserving the -lcuopt C API contract. -extern "C" void cuOptDestroyProblem(void*); -__attribute__((used)) void (* const _cuopt_lp_anchor)(void*) = &cuOptDestroyProblem; +# Reference one symbol from each component lib so --as-needed keeps them in +# DT_NEEDED. The constructor in grpc_registration.cpp fires when +# libcuopt_grpc.so is loaded and wires up the remote-solve function pointers +# in libcuopt_lp.so, so that must happen whenever -lcuopt is used. +set(_UMBRELLA_SRC [=[ +// Thin umbrella: one symbol reference per component so --as-needed keeps +// each library in DT_NEEDED. +extern "C" void cuOptDestroyProblem(void*); // libcuopt_lp.so +__attribute__((used)) void (* const _cuopt_lp_anchor)(void*) = &cuOptDestroyProblem; ]=]) +if(NOT SKIP_GRPC_BUILD) + string(APPEND _UMBRELLA_SRC [=[ +// solve_lp_remote is in libcuopt_grpc.so; pulling it in ensures the +// grpc_registration constructor runs and registers the remote-solve +// function pointers in libcuopt_lp.so. +#include +#include +namespace cuopt::mathematical_optimization { +__attribute__((used)) solve_lp_remote_fn_t const _cuopt_grpc_anchor = + &solve_lp_remote; +} +]=]) +endif() + +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" "${_UMBRELLA_SRC}") + add_library(cuopt SHARED "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp") add_library(cuopt::cuopt ALIAS cuopt) From 0f3b7e1bf8dead5016bcc55184416a3f31d9b572 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 29 Jul 2026 17:43:55 -0500 Subject: [PATCH 15/40] fix(build): load libcuopt_grpc.so on demand via dlopen for remote execution With the library split, --as-needed strips libcuopt.so and libcuopt_grpc.so from test binary DT_NEEDED since all C API symbols come from libcuopt_lp.so directly. This prevents the __attribute__((constructor)) in grpc_registration.cpp from firing, leaving g_solve_lp_remote_fn/g_solve_mip_remote_fn null. Fix: when remote execution is enabled and the function pointer is still null, call dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL) to load the gRPC component on demand. Its constructor registers the function pointers before we dereference them. If gRPC is not installed the error message is unchanged. Also add -lcuopt_lp to C example Makefiles and ci/test_skills_assets.sh to satisfy newer linkers that reject "DSO missing from command line" errors when symbols are transitively provided through libcuopt.so's DT_NEEDED. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- ci/test_skills_assets.sh | 2 +- cpp/src/mip_heuristics/solve.cu | 5 +++++ cpp/src/pdlp/solve.cu | 5 +++++ docs/cuopt/source/cuopt-c/convex/examples/Makefile | 2 +- docs/cuopt/source/cuopt-c/mip/examples/Makefile | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ci/test_skills_assets.sh b/ci/test_skills_assets.sh index c75645cb93..3436d721b9 100755 --- a/ci/test_skills_assets.sh +++ b/ci/test_skills_assets.sh @@ -111,7 +111,7 @@ if [[ -n "${CONDA_PREFIX:-}" ]]; then base=$(basename "$cfile" .c) rel="${cfile#"$REPO_ROOT/"}" log "Building and running C asset: $rel" - if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt); then + if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt -lcuopt_lp); then FAILED+=("$rel (build)") log "FAIL: $rel (build)" continue diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 07db7f54c7..370ae5ea4b 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -8,6 +8,8 @@ #include #include +#include + #include #include #include @@ -913,6 +915,9 @@ std::unique_ptr> solve_mip( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); + if (g_solve_mip_remote_fn == nullptr) { + dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); + } cuopt_expects(g_solve_mip_remote_fn != nullptr, error_type_t::RuntimeError, "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 9e643ba865..d46c01e047 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -7,6 +7,8 @@ #include #include + +#include #include #include #include @@ -2691,6 +2693,9 @@ std::unique_ptr> solve_lp( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); + if (g_solve_lp_remote_fn == nullptr) { + dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); + } cuopt_expects(g_solve_lp_remote_fn != nullptr, error_type_t::RuntimeError, "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index fef30244f1..59bc0e0bb0 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -46,7 +46,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index bc287f0d49..f8d91d2f5f 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -22,7 +22,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) From b872bfb3da840a3dc189c168ace2d67326a8d90f Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 29 Jul 2026 18:33:25 -0500 Subject: [PATCH 16/40] style: apply clang-format and fix copyright years Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- cpp/src/mip_heuristics/solve.cu | 4 +--- cpp/src/pdlp/solve.cu | 4 +--- docs/cuopt/source/cuopt-c/convex/examples/Makefile | 2 +- docs/cuopt/source/cuopt-c/mip/examples/Makefile | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 370ae5ea4b..fee37530bd 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -915,9 +915,7 @@ std::unique_ptr> solve_mip( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - if (g_solve_mip_remote_fn == nullptr) { - dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); - } + if (g_solve_mip_remote_fn == nullptr) { dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); } cuopt_expects(g_solve_mip_remote_fn != nullptr, error_type_t::RuntimeError, "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index d46c01e047..3e0ac5a070 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -2693,9 +2693,7 @@ std::unique_ptr> solve_lp( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - if (g_solve_lp_remote_fn == nullptr) { - dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); - } + if (g_solve_lp_remote_fn == nullptr) { dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); } cuopt_expects(g_solve_lp_remote_fn != nullptr, error_type_t::RuntimeError, "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 59bc0e0bb0..f54dfb1312 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index f8d91d2f5f..bc1b5983ae 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); From 7b32d1ba0de63992d9850544e4575c8c12508671 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 30 Jul 2026 11:29:40 -0500 Subject: [PATCH 17/40] fix(build): allow shlib undefined for split libs, ignore pypi in linkcheck C example Makefiles now pass -Wl,--allow-shlib-undefined so the linker accepts libcuopt_lp.so's transitive deps (librmm, libcudss, etc.) being resolved at runtime via RPATH rather than at link time. Required in wheel installs where auditwheel bundles those deps with mangled names outside the standard search path. Add pypi.org to sphinx linkcheck_ignore; pypi.org consistently times out in CI networks and the link was already present in introduction.rst. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- docs/cuopt/source/conf.py | 4 +++- docs/cuopt/source/cuopt-c/convex/examples/Makefile | 2 +- docs/cuopt/source/cuopt-c/mip/examples/Makefile | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/cuopt/source/conf.py b/docs/cuopt/source/conf.py index b4749d0167..33bc4c3da1 100644 --- a/docs/cuopt/source/conf.py +++ b/docs/cuopt/source/conf.py @@ -365,7 +365,7 @@ def write_project_json(app, _builder): linkcheck_workers = 5 linkcheck_rate_limit_timeout = 60 -# GitHub and GitLab link checker exceptions +# GitHub, GitLab, and PyPI link checker exceptions linkcheck_ignore = [ # GitHub (Rate Limited) r"https://github\.com/.*", @@ -377,6 +377,8 @@ def write_project_json(app, _builder): r"https://api\.gitlab\.com/.*", r"https://gitlab\.org/.*", r"https://api\.gitlab\.org/.*", + # PyPI (Unreliable in CI networks) + r"https://pypi\.org/.*", ] diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index f54dfb1312..d663bb518d 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -46,7 +46,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index bc1b5983ae..dace6ebd11 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -22,7 +22,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) From 80c1c341b39a25a556770279d2bf05e47711a813 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 30 Jul 2026 14:46:26 -0500 Subject: [PATCH 18/40] fix(build): add cuopt_lp/cuopt_base to pip RPATH in libcuopt CMakeLists After the library split, libcuopt_lp.so and libcuopt_base.so are the libs that directly reference libcudss, librmm, etc. They only had INSTALL_RPATH="$ORIGIN" (set in cpp/CMakeLists.txt). The nvidia pip-package paths ($ORIGIN/../../nvidia/cudss/lib, etc.) were only applied to the umbrella cuopt target, cuopt_cli, and cuopt_grpc_server. At runtime the dynamic linker uses each shared library's own RPATH to find its deps, so libcuopt_lp.so needed the nvidia paths too. Without them, libcudss.so.0 (from nvidia-cudss-cu12) could not be found when C examples were run as standalone binaries from wheel installs. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Ramakrishna Prabhu --- python/libcuopt/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 4d24169645..3c31190fe2 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -96,5 +96,7 @@ endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") set_property(TARGET cuopt PROPERTY INSTALL_RPATH ${rpaths} APPEND) +set_property(TARGET cuopt_base PROPERTY INSTALL_RPATH ${rpaths} APPEND) +set_property(TARGET cuopt_lp PROPERTY INSTALL_RPATH ${rpaths} APPEND) set_property(TARGET cuopt_cli PROPERTY INSTALL_RPATH ${rpaths} APPEND) set_property(TARGET cuopt_grpc_server PROPERTY INSTALL_RPATH ${rpaths} APPEND) From 91f5b8477429b3e901c8e8493e5154f32fffe3f7 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 5 Aug 2026 14:56:13 -0500 Subject: [PATCH 19/40] =?UTF-8?q?fix(build):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20DT=5FNEEDED=20anchors,=20export=20names,=20registry?= =?UTF-8?q?=20races?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add umbrella anchors for cuopt_base and cuopt_routing. libcuopt.so listed only libcuopt_lp.so and libcuopt_grpc.so in DT_NEEDED; with --as-needed the base and routing components were dropped, so routing vanished entirely under SKIP_GRPC_BUILD. - Export components as cuopt::base/routing/lp/grpc via EXPORT_NAME, matching the names advertised in the package doc string. - Make the remote-solve registry slots std::atomic. The gRPC ELF constructor publishes them during a lazy dlopen while other threads read them. - Move the lazy dlopen into ensure_remote_solvers_loaded(), shared by the LP and MIP dispatchers. - Validate the CPU-problem downcast in cuopt_cli instead of static_cast. - Add cuopt_routing and cuopt_grpc to the pip wheel RPATH list. - Fall back to "unknown" for GIT_COMMIT_HASH in non-git source trees. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 33 ++++++++++++- cpp/cuopt_cli.cpp | 6 ++- .../remote_solve_registry.hpp | 46 +++++++++++++++---- cpp/src/mip_heuristics/solve.cu | 9 ++-- cpp/src/pdlp/remote_solve_registry.cpp | 18 ++++++-- cpp/src/pdlp/solve.cu | 8 ++-- python/libcuopt/CMakeLists.txt | 10 ++-- 7 files changed, 101 insertions(+), 29 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 97d61cf0e3..6cf78e150a 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -594,7 +594,12 @@ execute_process( WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _git_hash_result + ERROR_QUIET ) +if(NOT _git_hash_result EQUAL 0 OR GIT_COMMIT_HASH STREQUAL "") + set(GIT_COMMIT_HASH "unknown") +endif() message("-- Building with GIT_COMMIT_HASH = '${GIT_COMMIT_HASH}'") configure_file( @@ -871,7 +876,21 @@ set(_UMBRELLA_SRC [=[ // each library in DT_NEEDED. extern "C" void cuOptDestroyProblem(void*); // libcuopt_lp.so __attribute__((used)) void (* const _cuopt_lp_anchor)(void*) = &cuOptDestroyProblem; + +namespace cuopt { void print_version_info(int); } // libcuopt_base.so +__attribute__((used)) void (* const _cuopt_base_anchor)(int) = &cuopt::print_version_info; +]=]) + +if(NOT SKIP_ROUTING_BUILD) + string(APPEND _UMBRELLA_SRC [=[ +#include +namespace cuopt::routing { +__attribute__((used)) assignment_t (* const _cuopt_routing_anchor)( + data_model_view_t const&, solver_settings_t const&) = + &solve; +} ]=]) +endif() if(NOT SKIP_GRPC_BUILD) string(APPEND _UMBRELLA_SRC [=[ @@ -960,6 +979,16 @@ endif() if(NOT SKIP_GRPC_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_grpc) endif() + +# Export as cuopt::base / cuopt::routing / cuopt::lp / cuopt::grpc, matching the +# build-tree aliases. +set(CUOPT_COMPONENT_EXPORT_NAMES "") +foreach(_component ${CUOPT_COMPONENT_TARGETS}) + string(REPLACE "cuopt_" "" _export_name "${_component}") + set_target_properties(${_component} PROPERTIES EXPORT_NAME ${_export_name}) + list(APPEND CUOPT_COMPONENT_EXPORT_NAMES ${_export_name}) +endforeach() + install(TARGETS ${CUOPT_COMPONENT_TARGETS} DESTINATION ${_LIB_DEST} COMPONENT runtime @@ -1003,7 +1032,7 @@ Umbrella target: cuopt::cuopt (links all component libs — backward-compatibl rapids_export(INSTALL cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_TARGETS} + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string ) @@ -1012,7 +1041,7 @@ rapids_export(INSTALL cuopt # - build export ------------------------------------------------------------------------------- rapids_export(BUILD cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_TARGETS} + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string ) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index f2ee18980a..3fa3384b43 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -201,8 +201,12 @@ int run_single_file(const std::string& file_path, // Call solve_lp/mip_remote directly so libcuopt_grpc.so is a real DT_NEEDED // dependency of this binary rather than an implicit runtime lookup. auto* cpu_prob = - static_cast*>( + dynamic_cast*>( problem_interface.get()); + if (cpu_prob == nullptr) { + CUOPT_LOG_ERROR("Remote execution requires the CPU memory backend."); + return -1; + } if (is_mip) { auto& mip_settings = settings.get_mip_settings(); auto solution = cuopt::mathematical_optimization::solve_mip_remote(*cpu_prob, mip_settings); diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp index c0723d49c4..c4d3b9feb4 100644 --- a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -8,6 +8,8 @@ #pragma once #include + +#include #include // Forward declarations — full types live in libcuopt_lp / libcuopt_grpc headers. @@ -17,24 +19,52 @@ template class cpu_optimization_problem_t; template -struct pdlp_solver_settings_t; +class pdlp_solver_settings_t; template -struct mip_solver_settings_t; +class mip_solver_settings_t; -// Function pointer types — only the instantiation is supported. +/** + * @brief Remote LP solve entry point implemented by libcuopt_grpc.so. + * + * The returned solution is owned by the caller. The callback must not propagate + * exceptions across the component boundary. Only the `` + * instantiation is supported. + */ using solve_lp_remote_fn_t = std::unique_ptr> (*)( cpu_optimization_problem_t const&, pdlp_solver_settings_t const&); +/** + * @brief Remote MIP solve entry point implemented by libcuopt_grpc.so. + * + * Same ownership and exception contract as @ref solve_lp_remote_fn_t. + */ using solve_mip_remote_fn_t = std::unique_ptr> (*)( cpu_optimization_problem_t const&, mip_solver_settings_t const&); -// Defined in libcuopt_lp.so (remote_solve_registry.cpp). -// Set to nullptr until libcuopt_grpc.so is loaded and calls register_remote_solvers(). -extern solve_lp_remote_fn_t g_solve_lp_remote_fn; -extern solve_mip_remote_fn_t g_solve_mip_remote_fn; +/** + * @brief Registry slots defined in libcuopt_lp.so (remote_solve_registry.cpp). + * + * Null until libcuopt_grpc.so is loaded and calls register_remote_solvers(). Atomic + * because the registering ELF constructor runs on whichever thread triggers the lazy + * dlopen while other threads may be reading the slots. + */ +extern std::atomic g_solve_lp_remote_fn; +extern std::atomic g_solve_mip_remote_fn; -// Called by libcuopt_grpc.so's constructor to wire up the real implementations. +/** + * @brief Wire up the real remote-solve implementations. + * + * Called by libcuopt_grpc.so's ELF constructor. Thread-safe. + */ void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn); +/** + * @brief Load libcuopt_grpc.so on demand so its constructor populates the registry. + * + * Idempotent and thread-safe; a failed load leaves the registry slots null so callers + * can report the failure themselves. + */ +void ensure_remote_solvers_loaded(); + } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index fee37530bd..474dadee28 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -8,8 +8,6 @@ #include #include -#include - #include #include #include @@ -915,11 +913,12 @@ std::unique_ptr> solve_mip( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - if (g_solve_mip_remote_fn == nullptr) { dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); } - cuopt_expects(g_solve_mip_remote_fn != nullptr, + ensure_remote_solvers_loaded(); + auto* remote_fn = g_solve_mip_remote_fn.load(std::memory_order_acquire); + cuopt_expects(remote_fn != nullptr, error_type_t::RuntimeError, "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); - return g_solve_mip_remote_fn(*cpu_prob, settings); + return remote_fn(*cpu_prob, settings); } // Local execution - dispatch to appropriate overload based on problem type diff --git a/cpp/src/pdlp/remote_solve_registry.cpp b/cpp/src/pdlp/remote_solve_registry.cpp index 511c0046d0..900c2abde6 100644 --- a/cpp/src/pdlp/remote_solve_registry.cpp +++ b/cpp/src/pdlp/remote_solve_registry.cpp @@ -3,15 +3,25 @@ #include +#include + namespace cuopt::mathematical_optimization { -solve_lp_remote_fn_t g_solve_lp_remote_fn = nullptr; -solve_mip_remote_fn_t g_solve_mip_remote_fn = nullptr; +std::atomic g_solve_lp_remote_fn{nullptr}; +std::atomic g_solve_mip_remote_fn{nullptr}; void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn) { - g_solve_lp_remote_fn = lp_fn; - g_solve_mip_remote_fn = mip_fn; + g_solve_lp_remote_fn.store(lp_fn, std::memory_order_release); + g_solve_mip_remote_fn.store(mip_fn, std::memory_order_release); +} + +void ensure_remote_solvers_loaded() +{ + if (g_solve_lp_remote_fn.load(std::memory_order_acquire) != nullptr) { return; } + // The constructor in libcuopt_grpc.so calls register_remote_solvers(). dlopen is + // itself thread-safe and refcounted, so a concurrent second call is harmless. + dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); } } // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 3e0ac5a070..cdbbc6f19f 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -2693,11 +2692,12 @@ std::unique_ptr> solve_lp( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - if (g_solve_lp_remote_fn == nullptr) { dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); } - cuopt_expects(g_solve_lp_remote_fn != nullptr, + ensure_remote_solvers_loaded(); + auto* remote_fn = g_solve_lp_remote_fn.load(std::memory_order_acquire); + cuopt_expects(remote_fn != nullptr, error_type_t::RuntimeError, "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); - return g_solve_lp_remote_fn(*cpu_prob, settings); + return remote_fn(*cpu_prob, settings); } // Local execution - dispatch to appropriate overload based on problem type diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 3c31190fe2..499eca9b67 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -95,8 +95,8 @@ else() endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") -set_property(TARGET cuopt PROPERTY INSTALL_RPATH ${rpaths} APPEND) -set_property(TARGET cuopt_base PROPERTY INSTALL_RPATH ${rpaths} APPEND) -set_property(TARGET cuopt_lp PROPERTY INSTALL_RPATH ${rpaths} APPEND) -set_property(TARGET cuopt_cli PROPERTY INSTALL_RPATH ${rpaths} APPEND) -set_property(TARGET cuopt_grpc_server PROPERTY INSTALL_RPATH ${rpaths} APPEND) +foreach(_target cuopt cuopt_base cuopt_routing cuopt_lp cuopt_grpc cuopt_cli cuopt_grpc_server) + if(TARGET ${_target}) + set_property(TARGET ${_target} PROPERTY INSTALL_RPATH ${rpaths} APPEND) + endif() +endforeach() From fe01c17ca0a32754b421f69630f7ba4e8328cbd5 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 6 Aug 2026 10:30:23 -0500 Subject: [PATCH 20/40] fix(build): gate remote-solver registry on an explicit ready flag register_remote_solvers() stored the LP callback before the MIP callback while ensure_remote_solvers_loaded() used the LP slot as its readiness sentinel. A concurrent MIP solve could observe the LP slot set, skip the lazy dlopen, then read a still-null MIP slot and fail with a spurious "gRPC component not loaded" error. Publish a separate ready flag after both callbacks instead, so readiness does not depend on callback count or store order. Also report dlopen failures via dlerror() rather than discarding them, and link cuopt_lp against ${CMAKE_DL_LIBS} since it calls dlopen directly. Use the documented APPEND-before-PROPERTY form in the pip RPATH loop. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 1 + .../remote_solve_registry.hpp | 7 +++++++ cpp/src/pdlp/remote_solve_registry.cpp | 17 +++++++++++++---- python/libcuopt/CMakeLists.txt | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6cf78e150a..60c00a861f 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -684,6 +684,7 @@ target_link_libraries(cuopt_lp OpenMP::OpenMP_CUDA simde::simde nccl_external + ${CMAKE_DL_LIBS} ) target_link_libraries(cuopt_lp PRIVATE $) add_dependencies(cuopt_lp PSLP) diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp index c4d3b9feb4..c8c301ceb2 100644 --- a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -52,6 +52,13 @@ using solve_mip_remote_fn_t = std::unique_ptr g_solve_lp_remote_fn; extern std::atomic g_solve_mip_remote_fn; +/** + * @brief Readiness flag, published after both callbacks are stored. + * + * Readers must observe this as true before trusting either slot. + */ +extern std::atomic g_remote_solvers_ready; + /** * @brief Wire up the real remote-solve implementations. * diff --git a/cpp/src/pdlp/remote_solve_registry.cpp b/cpp/src/pdlp/remote_solve_registry.cpp index 900c2abde6..12f3e13997 100644 --- a/cpp/src/pdlp/remote_solve_registry.cpp +++ b/cpp/src/pdlp/remote_solve_registry.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include @@ -9,19 +10,27 @@ namespace cuopt::mathematical_optimization { std::atomic g_solve_lp_remote_fn{nullptr}; std::atomic g_solve_mip_remote_fn{nullptr}; +std::atomic g_remote_solvers_ready{false}; void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn) { - g_solve_lp_remote_fn.store(lp_fn, std::memory_order_release); - g_solve_mip_remote_fn.store(mip_fn, std::memory_order_release); + g_solve_lp_remote_fn.store(lp_fn, std::memory_order_relaxed); + g_solve_mip_remote_fn.store(mip_fn, std::memory_order_relaxed); + // Published last with release ordering: a reader that observes the ready flag is + // guaranteed to observe both callbacks. Using a separate flag rather than one of the + // slots keeps the readiness condition independent of how many callbacks there are. + g_remote_solvers_ready.store(true, std::memory_order_release); } void ensure_remote_solvers_loaded() { - if (g_solve_lp_remote_fn.load(std::memory_order_acquire) != nullptr) { return; } + if (g_remote_solvers_ready.load(std::memory_order_acquire)) { return; } // The constructor in libcuopt_grpc.so calls register_remote_solvers(). dlopen is // itself thread-safe and refcounted, so a concurrent second call is harmless. - dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL); + if (dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL) == nullptr) { + const char* err = dlerror(); + CUOPT_LOG_DEBUG("Could not load libcuopt_grpc.so: %s", err != nullptr ? err : "unknown error"); + } } } // namespace cuopt::mathematical_optimization diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 499eca9b67..b9806d194a 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -97,6 +97,6 @@ message(STATUS "libcuopt: Final RPATH = ${rpaths}") foreach(_target cuopt cuopt_base cuopt_routing cuopt_lp cuopt_grpc cuopt_cli cuopt_grpc_server) if(TARGET ${_target}) - set_property(TARGET ${_target} PROPERTY INSTALL_RPATH ${rpaths} APPEND) + set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${rpaths}) endif() endforeach() From 1c4cdbda723dc35387216eae1543ed6ea5f86eb8 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 6 Aug 2026 16:07:54 -0500 Subject: [PATCH 21/40] build(cmake): rename cuopt_lp to cuopt_mathematical_optimization The component covers LP, QP, SOCP and MIP, so name it for what it is. Renames the target, the shipped libcuopt_mathematical_optimization.so, the cuopt::mathematical_optimization export, and the source-list variable. Also moves the linear algebra sources out of cuopt_base and into the mathematical optimization component: routing resolves only default_logger() and seed_generator::seed_ from base and references no linear algebra symbols, so they do not belong in the shared base. cuopt_base drops to 1.17 MB. Extracts the umbrella translation unit out of CMakeLists.txt into cmake/umbrella.cpp.in, configured with #cmakedefine rather than assembled with string(APPEND). The C++ now lives in a real .cpp file that can be formatted and read on its own. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- ci/build_wheel_cuopt.sh | 2 +- ci/test_skills_assets.sh | 2 +- conda/recipes/libcuopt/recipe.yaml | 2 +- cpp/CMakeLists.txt | 103 +++++++----------- cpp/cmake/umbrella.cpp.in | 47 ++++++++ .../remote_solve_registry.hpp | 4 +- .../solve_remote.hpp | 4 +- cpp/src/CMakeLists.txt | 8 +- cpp/src/grpc/client/grpc_registration.cpp | 4 +- .../source/cuopt-c/convex/examples/Makefile | 2 +- .../source/cuopt-c/mip/examples/Makefile | 2 +- python/libcuopt/CMakeLists.txt | 2 +- 12 files changed, 101 insertions(+), 81 deletions(-) create mode 100644 cpp/cmake/umbrella.cpp.in diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index 449bc19665..79ed9503c2 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -43,7 +43,7 @@ EXCLUDE_ARGS=( --exclude "libcuopt.so" --exclude "libcuopt_base.so" --exclude "libcuopt_routing.so" - --exclude "libcuopt_lp.so" + --exclude "libcuopt_mathematical_optimization.so" --exclude "libcuopt_grpc.so" --exclude "librapids_logger.so" --exclude "librmm.so" diff --git a/ci/test_skills_assets.sh b/ci/test_skills_assets.sh index 3436d721b9..27f54a2a71 100755 --- a/ci/test_skills_assets.sh +++ b/ci/test_skills_assets.sh @@ -111,7 +111,7 @@ if [[ -n "${CONDA_PREFIX:-}" ]]; then base=$(basename "$cfile" .c) rel="${cfile#"$REPO_ROOT/"}" log "Building and running C asset: $rel" - if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt -lcuopt_lp); then + if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt -lcuopt_mathematical_optimization); then FAILED+=("$rel (build)") log "FAIL: $rel (build)" continue diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 2e1346cfdd..4896c08da5 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -167,7 +167,7 @@ outputs: - lib/libcuopt.so - lib/libcuopt_base.so - lib/libcuopt_routing.so - - lib/libcuopt_lp.so + - lib/libcuopt_mathematical_optimization.so - lib/libcuopt_grpc.so - bin/cuopt_cli - bin/cuopt_grpc_server diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 19c0773145..15fe0302bf 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -494,7 +494,7 @@ endif () set(CUOPT_SRC_FILES) set(CUOPT_BASE_SRC_FILES) set(CUOPT_ROUTING_SRC_FILES) -set(CUOPT_LP_SRC_FILES) +set(CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES) set(MPS_FAST_SRC_FILES) add_subdirectory(src) @@ -506,7 +506,7 @@ set_source_files_properties( if (HOST_LINEINFO) set_source_files_properties( - ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_LP_SRC_FILES} + ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") endif () @@ -524,7 +524,7 @@ endif () # Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO). if (DEFINE_ASSERT) set_property( - SOURCE ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_LP_SRC_FILES} + SOURCE ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG") endif () @@ -652,10 +652,10 @@ if(NOT SKIP_ROUTING_BUILD) add_library(cuopt::routing ALIAS cuopt_routing) endif() -# cuopt_lp: LP / MIP / numerical optimization engine -add_library(cuopt_lp SHARED ${CUOPT_LP_SRC_FILES}) -cuopt_configure_component(cuopt_lp) -target_include_directories(cuopt_lp PRIVATE +# cuopt_mathematical_optimization: LP / MIP / numerical optimization engine +add_library(cuopt_mathematical_optimization SHARED ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES}) +cuopt_configure_component(cuopt_mathematical_optimization) +target_include_directories(cuopt_mathematical_optimization PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty" "${CMAKE_CURRENT_SOURCE_DIR}/src/io" "${CUDSS_INCLUDE}" @@ -663,19 +663,19 @@ target_include_directories(cuopt_lp PRIVATE $<$:${ZLIB_INCLUDE_DIRS}> ) # Adding Papilo as a system include messes up clang's include resolution if papilo is already installed as a conda package -target_include_directories(cuopt_lp PRIVATE +target_include_directories(cuopt_mathematical_optimization PRIVATE "${papilo_SOURCE_DIR}/src" "${papilo_BINARY_DIR}" ) -target_include_directories(cuopt_lp SYSTEM PRIVATE +target_include_directories(cuopt_mathematical_optimization SYSTEM PRIVATE "${pslp_SOURCE_DIR}/include" "${dejavu_SOURCE_DIR}" ) -target_compile_definitions(cuopt_lp +target_compile_definitions(cuopt_mathematical_optimization PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API PRIVATE CUDSS_MT_LIB_FILE_NAME="${CUDSS_MT_LIB_FILE_NAME}" ) -target_link_libraries(cuopt_lp +target_link_libraries(cuopt_mathematical_optimization PUBLIC cuopt_base ${CUDSS_LIB_FILE} @@ -686,19 +686,19 @@ target_link_libraries(cuopt_lp nccl_external ${CMAKE_DL_LIBS} ) -target_link_libraries(cuopt_lp PRIVATE $) -add_dependencies(cuopt_lp PSLP) -target_include_directories(cuopt_lp SYSTEM PRIVATE +target_link_libraries(cuopt_mathematical_optimization PRIVATE $) +add_dependencies(cuopt_mathematical_optimization PSLP) +target_include_directories(cuopt_mathematical_optimization SYSTEM PRIVATE $) -target_compile_definitions(cuopt_lp PRIVATE TBB_PREVIEW_GLOBAL_CONTROL KAMINPAR_64BIT_EDGE_IDS) -target_link_libraries(cuopt_lp PRIVATE $) +target_compile_definitions(cuopt_mathematical_optimization PRIVATE TBB_PREVIEW_GLOBAL_CONTROL KAMINPAR_64BIT_EDGE_IDS) +target_link_libraries(cuopt_mathematical_optimization PRIVATE $) if (TARGET KaMinPar) - add_dependencies(cuopt_lp KaMinPar) + add_dependencies(cuopt_mathematical_optimization KaMinPar) endif () -add_library(cuopt::lp ALIAS cuopt_lp) +add_library(cuopt::mathematical_optimization ALIAS cuopt_mathematical_optimization) # cuopt_grpc: gRPC bridge — proto mappers + Cython client (LP and routing over gRPC) -# Depends on cuopt_lp and cuopt_routing so it can reference both APIs without forcing +# Depends on cuopt_mathematical_optimization and cuopt_routing so it can reference both APIs without forcing # either component to take a gRPC dependency. if(NOT SKIP_GRPC_BUILD) add_library(cuopt_grpc SHARED ${GRPC_INFRA_FILES}) @@ -710,9 +710,9 @@ if(NOT SKIP_GRPC_BUILD) "${CMAKE_CURRENT_SOURCE_DIR}/src/io" ) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt_grpc PUBLIC cuopt_lp cuopt_routing PRIVATE protobuf::libprotobuf gRPC::grpc++) + target_link_libraries(cuopt_grpc PUBLIC cuopt_mathematical_optimization cuopt_routing PRIVATE protobuf::libprotobuf gRPC::grpc++) else() - target_link_libraries(cuopt_grpc PUBLIC cuopt_lp PRIVATE protobuf::libprotobuf gRPC::grpc++) + target_link_libraries(cuopt_grpc PUBLIC cuopt_mathematical_optimization PRIVATE protobuf::libprotobuf gRPC::grpc++) endif() add_library(cuopt::grpc ALIAS cuopt_grpc) endif() @@ -865,51 +865,24 @@ endif (BUILD_TESTS) # ################################################################################################## # - cuopt: thin umbrella shared library ----------------------------------------------------------- -# Links publicly against the four component shared libs so that -lcuopt keeps working for -# all existing consumers. - -# Reference one symbol from each component lib so --as-needed keeps them in -# DT_NEEDED. The constructor in grpc_registration.cpp fires when -# libcuopt_grpc.so is loaded and wires up the remote-solve function pointers -# in libcuopt_lp.so, so that must happen whenever -lcuopt is used. -set(_UMBRELLA_SRC [=[ -// Thin umbrella: one symbol reference per component so --as-needed keeps -// each library in DT_NEEDED. -extern "C" void cuOptDestroyProblem(void*); // libcuopt_lp.so -__attribute__((used)) void (* const _cuopt_lp_anchor)(void*) = &cuOptDestroyProblem; - -namespace cuopt { void print_version_info(int); } // libcuopt_base.so -__attribute__((used)) void (* const _cuopt_base_anchor)(int) = &cuopt::print_version_info; -]=]) +# Links publicly against the component shared libs so that -lcuopt keeps working for +# all existing consumers. Its only translation unit is generated from +# cmake/umbrella.cpp.in -- see that file for why the anchors are needed. if(NOT SKIP_ROUTING_BUILD) - string(APPEND _UMBRELLA_SRC [=[ -#include -namespace cuopt::routing { -__attribute__((used)) assignment_t (* const _cuopt_routing_anchor)( - data_model_view_t const&, solver_settings_t const&) = - &solve; -} -]=]) + set(CUOPT_ANCHOR_ROUTING TRUE) endif() - if(NOT SKIP_GRPC_BUILD) - string(APPEND _UMBRELLA_SRC [=[ -// solve_lp_remote is in libcuopt_grpc.so; pulling it in ensures the -// grpc_registration constructor runs and registers the remote-solve -// function pointers in libcuopt_lp.so. -#include -#include -namespace cuopt::mathematical_optimization { -__attribute__((used)) solve_lp_remote_fn_t const _cuopt_grpc_anchor = - &solve_lp_remote; -} -]=]) + set(CUOPT_ANCHOR_GRPC TRUE) endif() -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp" "${_UMBRELLA_SRC}") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/umbrella.cpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/umbrella.cpp" + @ONLY +) -add_library(cuopt SHARED "${CMAKE_CURRENT_BINARY_DIR}/cuopt_umbrella.cpp") +add_library(cuopt SHARED "${CMAKE_CURRENT_BINARY_DIR}/umbrella.cpp") add_library(cuopt::cuopt ALIAS cuopt) set_target_properties(cuopt PROPERTIES @@ -932,9 +905,9 @@ target_compile_definitions(cuopt ) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt PUBLIC cuopt_base cuopt_routing cuopt_lp) + target_link_libraries(cuopt PUBLIC cuopt_base cuopt_routing cuopt_mathematical_optimization) else() - target_link_libraries(cuopt PUBLIC cuopt_base cuopt_lp) + target_link_libraries(cuopt PUBLIC cuopt_base cuopt_mathematical_optimization) endif() if(NOT SKIP_GRPC_BUILD) target_link_libraries(cuopt PUBLIC cuopt_grpc) @@ -973,7 +946,7 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () -set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_lp) +set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_mathematical_optimization) if(NOT SKIP_ROUTING_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) endif() @@ -981,7 +954,7 @@ if(NOT SKIP_GRPC_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_grpc) endif() -# Export as cuopt::base / cuopt::routing / cuopt::lp / cuopt::grpc, matching the +# Export as cuopt::base / cuopt::routing / cuopt::mathematical_optimization / cuopt::grpc, matching the # build-tree aliases. set(CUOPT_COMPONENT_EXPORT_NAMES "") foreach(_component ${CUOPT_COMPONENT_TARGETS}) @@ -996,7 +969,7 @@ install(TARGETS ${CUOPT_COMPONENT_TARGETS} EXPORT cuopt-exports ) -# Install umbrella shared library (the primary runtime artifact) +# Install the umbrella shared library (the primary runtime artifact) install(TARGETS cuopt DESTINATION ${_LIB_DEST} COMPONENT runtime @@ -1026,7 +999,7 @@ set(doc_string Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. -Component targets: cuopt::base, cuopt::routing, cuopt::lp, cuopt::grpc (when gRPC is built) +Component targets: cuopt::base, cuopt::routing, cuopt::mathematical_optimization, cuopt::grpc (when gRPC is built) Umbrella target: cuopt::cuopt (links all component libs — backward-compatible with -lcuopt) ]=]) diff --git a/cpp/cmake/umbrella.cpp.in b/cpp/cmake/umbrella.cpp.in new file mode 100644 index 0000000000..a3b7c904ee --- /dev/null +++ b/cpp/cmake/umbrella.cpp.in @@ -0,0 +1,47 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Generated from cmake/umbrella.cpp.in -- do not edit the generated copy. +// +// The umbrella libcuopt.so has no code of its own; it exists so that -lcuopt keeps +// resolving for consumers that linked the pre-split library. Because it defines +// nothing, --as-needed would drop every component from DT_NEEDED. Referencing one +// exported symbol per component keeps them all linked in. + +#cmakedefine CUOPT_ANCHOR_ROUTING +#cmakedefine CUOPT_ANCHOR_GRPC + +// libcuopt_mathematical_optimization.so +extern "C" void cuOptDestroyProblem(void*); +__attribute__((used)) void (*const _cuopt_mathematical_optimization_anchor)(void*) = + &cuOptDestroyProblem; + +// libcuopt_base.so +namespace cuopt { +void print_version_info(int); +} +__attribute__((used)) void (*const _cuopt_base_anchor)(int) = &cuopt::print_version_info; + +#ifdef CUOPT_ANCHOR_ROUTING +// libcuopt_routing.so +#include +namespace cuopt::routing { +__attribute__((used)) assignment_t (*const _cuopt_routing_anchor)( + data_model_view_t const&, solver_settings_t const&) = &solve; +} +#endif + +#ifdef CUOPT_ANCHOR_GRPC +// libcuopt_grpc.so. Anchoring solve_lp_remote also guarantees the ELF constructor in +// grpc_registration.cpp runs, which registers the remote-solve callbacks in +// libcuopt_mathematical_optimization.so. +#include +#include +namespace cuopt::mathematical_optimization { +__attribute__((used)) solve_lp_remote_fn_t const _cuopt_grpc_anchor = &solve_lp_remote; +} +#endif diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp index c8c301ceb2..2e008a0825 100644 --- a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -12,7 +12,7 @@ #include #include -// Forward declarations — full types live in libcuopt_lp / libcuopt_grpc headers. +// Forward declarations — full types live in libcuopt_mathematical_optimization / libcuopt_grpc headers. namespace cuopt::mathematical_optimization { template @@ -43,7 +43,7 @@ using solve_mip_remote_fn_t = std::unique_ptr const&, mip_solver_settings_t const&); /** - * @brief Registry slots defined in libcuopt_lp.so (remote_solve_registry.cpp). + * @brief Registry slots defined in libcuopt_mathematical_optimization.so (remote_solve_registry.cpp). * * Null until libcuopt_grpc.so is loaded and calls register_remote_solvers(). Atomic * because the registering ELF constructor runs on whichever thread triggers the lazy diff --git a/cpp/include/cuopt/mathematical_optimization/solve_remote.hpp b/cpp/include/cuopt/mathematical_optimization/solve_remote.hpp index 3636195660..159e18f868 100644 --- a/cpp/include/cuopt/mathematical_optimization/solve_remote.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solve_remote.hpp @@ -19,10 +19,10 @@ template class cpu_optimization_problem_t; template -struct pdlp_solver_settings_t; +class pdlp_solver_settings_t; template -struct mip_solver_settings_t; +class mip_solver_settings_t; // ============================================================================ // Remote Execution Functions diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 8af5cf4dfc..94c54d330d 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -28,14 +28,14 @@ add_subdirectory(cuts) # Aggregate per-domain source lists for the three component libraries set(CUOPT_BASE_SRC_FILES ${UTIL_SRC_FILES} - ${LINEAR_ALGEBRA_SRC_FILES} ) set(CUOPT_ROUTING_SRC_FILES ${ROUTING_SRC_FILES} ) -set(CUOPT_LP_SRC_FILES +set(CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES + ${LINEAR_ALGEBRA_SRC_FILES} ${LP_SRC_FILES} ${MATH_OPT_SRC_FILES} ${MIP_SRC_FILES} @@ -48,11 +48,11 @@ set(CUOPT_LP_SRC_FILES set(CUOPT_BASE_SRC_FILES ${CUOPT_BASE_SRC_FILES} PARENT_SCOPE) set(CUOPT_ROUTING_SRC_FILES ${CUOPT_ROUTING_SRC_FILES} PARENT_SCOPE) -set(CUOPT_LP_SRC_FILES ${CUOPT_LP_SRC_FILES} PARENT_SCOPE) +set(CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} - ${CUOPT_LP_SRC_FILES} + ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} PARENT_SCOPE ) set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/grpc/client/grpc_registration.cpp b/cpp/src/grpc/client/grpc_registration.cpp index 5bf4ef0c45..9ed54d4b1c 100644 --- a/cpp/src/grpc/client/grpc_registration.cpp +++ b/cpp/src/grpc/client/grpc_registration.cpp @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// Registers the gRPC-based remote solve implementations with libcuopt_lp.so +// Registers the gRPC-based remote solve implementations with libcuopt_mathematical_optimization.so // at dynamic-link time (before any user code runs). This breaks the circular -// dependency: libcuopt_lp.so holds nullable function pointers rather than a +// dependency: libcuopt_mathematical_optimization.so holds nullable function pointers rather than a // hard reference to symbols in libcuopt_grpc.so. #include diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 4195376ca5..43585f9247 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -46,7 +46,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathematical_optimization -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index dace6ebd11..51f9b409f2 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -22,7 +22,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_lp -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathematical_optimization -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index b9806d194a..1e417a2e7c 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -95,7 +95,7 @@ else() endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") -foreach(_target cuopt cuopt_base cuopt_routing cuopt_lp cuopt_grpc cuopt_cli cuopt_grpc_server) +foreach(_target cuopt cuopt_base cuopt_routing cuopt_mathematical_optimization cuopt_grpc cuopt_cli cuopt_grpc_server) if(TARGET ${_target}) set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${rpaths}) endif() From b3b870245bd13e31e5675a5df9923d36c5b4ed43 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 6 Aug 2026 22:15:11 -0500 Subject: [PATCH 22/40] build(cmake): ship libcuopt.so as a linker script Bare -lcuopt stopped resolving once libcuopt.so became a thin umbrella: the linker does not satisfy a consumer's undefined symbols through a dependency's DT_NEEDED (--no-copy-dt-needed-entries, the default since binutils 2.22), so C consumers had to name each component and the example Makefiles had to paper over it with --allow-shlib-undefined. Ship libcuopt.so as a GNU ld script naming the components instead. This is how glibc ships libc.so. -lcuopt resolves to the whole set again and consumers get a direct DT_NEEDED on each component, so the umbrella library and its --as-needed anchor symbols are no longer needed and are removed. cuopt::cuopt becomes an INTERFACE target, leaving CMake and Cython consumers unchanged. libcuopt.so is no longer an ELF object, so it cannot be dlopen()ed: load.py loads the components directly, and the wheel omits the script since nothing in it links. Anything previously linked against libcuopt.so needs a rebuild. Also fixes WRITE_FATBIN, which applied fatbin.ld to the umbrella. That target held no device code after the split, so the section grouping had silently become a no-op; it now applies to the components carrying the fatbins. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- conda/recipes/libcuopt/recipe.yaml | 5 ++- cpp/CMakeLists.txt | 60 +++++++++++++++--------------- cpp/cmake/libcuopt.so.in | 8 ++++ cpp/cmake/umbrella.cpp.in | 47 ----------------------- python/libcuopt/CMakeLists.txt | 3 +- python/libcuopt/libcuopt/load.py | 35 +++++++++++++++-- python/libcuopt/pyproject.toml | 5 +++ 7 files changed, 80 insertions(+), 83 deletions(-) create mode 100644 cpp/cmake/libcuopt.so.in delete mode 100644 cpp/cmake/umbrella.cpp.in diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 4896c08da5..e57dc069fb 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -113,7 +113,10 @@ outputs: prefix_detection: ignore: # See https://github.com/rapidsai/build-planning/issues/160 - - lib/libcuopt.so + - lib/libcuopt_base.so + - lib/libcuopt_routing.so + - lib/libcuopt_mathematical_optimization.so + - lib/libcuopt_grpc.so string: cuda${{ cuda_major }}_${{ date_string }}_${{ head_rev }} requirements: build: diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 15fe0302bf..44a55ea45e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -864,53 +864,44 @@ if (BUILD_TESTS) endif (BUILD_TESTS) # ################################################################################################## -# - cuopt: thin umbrella shared library ----------------------------------------------------------- -# Links publicly against the component shared libs so that -lcuopt keeps working for -# all existing consumers. Its only translation unit is generated from -# cmake/umbrella.cpp.in -- see that file for why the anchors are needed. +# - cuopt: linker script + INTERFACE target ------------------------------------------------------- +# libcuopt.so is a GNU ld script naming the component libraries, so -lcuopt keeps resolving +# for consumers that linked the pre-split library. cuopt::cuopt is the CMake equivalent. +# See cmake/libcuopt.so.in for why a real ELF cannot serve this purpose. +set(CUOPT_LINKER_SCRIPT_INPUTS "libcuopt_base.so") if(NOT SKIP_ROUTING_BUILD) - set(CUOPT_ANCHOR_ROUTING TRUE) + string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_routing.so") endif() +string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_mathematical_optimization.so") if(NOT SKIP_GRPC_BUILD) - set(CUOPT_ANCHOR_GRPC TRUE) + string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_grpc.so") endif() +set(CUOPT_LINKER_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/libcuopt.so") configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/umbrella.cpp.in" - "${CMAKE_CURRENT_BINARY_DIR}/umbrella.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/libcuopt.so.in" + "${CUOPT_LINKER_SCRIPT}" @ONLY ) -add_library(cuopt SHARED "${CMAKE_CURRENT_BINARY_DIR}/umbrella.cpp") +add_library(cuopt INTERFACE) add_library(cuopt::cuopt ALIAS cuopt) -set_target_properties(cuopt PROPERTIES - BUILD_RPATH_USE_ORIGIN TRUE - INSTALL_RPATH "\$ORIGIN" - CXX_SCAN_FOR_MODULES OFF -) - target_include_directories(cuopt - PUBLIC + INTERFACE "$" "$" - INTERFACE "$" ) -target_compile_definitions(cuopt - PUBLIC - "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" - CUSPARSE_ENABLE_EXPERIMENTAL_API -) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt PUBLIC cuopt_base cuopt_routing cuopt_mathematical_optimization) + target_link_libraries(cuopt INTERFACE cuopt_base cuopt_routing cuopt_mathematical_optimization) else() - target_link_libraries(cuopt PUBLIC cuopt_base cuopt_mathematical_optimization) + target_link_libraries(cuopt INTERFACE cuopt_base cuopt_mathematical_optimization) endif() if(NOT SKIP_GRPC_BUILD) - target_link_libraries(cuopt PUBLIC cuopt_grpc) + target_link_libraries(cuopt INTERFACE cuopt_grpc) endif() if (WRITE_FATBIN) @@ -922,7 +913,15 @@ if (WRITE_FATBIN) .nv_fatbin : { *(.nv_fatbin) } } ]=]) - target_link_options(cuopt PRIVATE "${CUOPT_BINARY_DIR}/fatbin.ld") + # Applies to the components that actually carry CUDA fatbins. Before the split this + # was set on libcuopt.so, which held all the device code; it now holds none. + set(_CUOPT_FATBIN_TARGETS cuopt_base cuopt_mathematical_optimization) + if(NOT SKIP_ROUTING_BUILD) + list(APPEND _CUOPT_FATBIN_TARGETS cuopt_routing) + endif() + foreach(_target ${_CUOPT_FATBIN_TARGETS}) + target_link_options(${_target} PRIVATE "${CUOPT_BINARY_DIR}/fatbin.ld") + endforeach() endif () # ################################################################################################## @@ -969,14 +968,15 @@ install(TARGETS ${CUOPT_COMPONENT_TARGETS} EXPORT cuopt-exports ) -# Install the umbrella shared library (the primary runtime artifact) +# cuopt::cuopt carries no artifact of its own; it is exported so that +# target_link_libraries(app cuopt::cuopt) keeps pulling in every component. install(TARGETS cuopt - DESTINATION ${_LIB_DEST} - COMPONENT runtime EXPORT cuopt-exports ) -install(TARGETS cuopt +# libcuopt.so is the ld script that keeps -lcuopt working. It is a link-time artifact, +# so it belongs to the dev component; nothing loads it at runtime. +install(FILES "${CUOPT_LINKER_SCRIPT}" DESTINATION ${_LIB_DEST} COMPONENT dev ) diff --git a/cpp/cmake/libcuopt.so.in b/cpp/cmake/libcuopt.so.in new file mode 100644 index 0000000000..0e80a0fba3 --- /dev/null +++ b/cpp/cmake/libcuopt.so.in @@ -0,0 +1,8 @@ +/* GNU ld script -- generated from cmake/libcuopt.so.in, do not edit the generated copy. + libcuopt.so is not an ELF object. cuOpt ships as component libraries, and the linker + does not resolve a consumer's undefined symbols through a dependency's DT_NEEDED + (--no-copy-dt-needed-entries, the default since binutils 2.22). Naming this script + libcuopt.so keeps -lcuopt resolving to the whole set, exactly as it did before the + library was split. Consumers end up with a direct DT_NEEDED on each component. + Anything that loads cuOpt at runtime must dlopen the components, not this file. */ +INPUT(@CUOPT_LINKER_SCRIPT_INPUTS@) diff --git a/cpp/cmake/umbrella.cpp.in b/cpp/cmake/umbrella.cpp.in deleted file mode 100644 index a3b7c904ee..0000000000 --- a/cpp/cmake/umbrella.cpp.in +++ /dev/null @@ -1,47 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -// Generated from cmake/umbrella.cpp.in -- do not edit the generated copy. -// -// The umbrella libcuopt.so has no code of its own; it exists so that -lcuopt keeps -// resolving for consumers that linked the pre-split library. Because it defines -// nothing, --as-needed would drop every component from DT_NEEDED. Referencing one -// exported symbol per component keeps them all linked in. - -#cmakedefine CUOPT_ANCHOR_ROUTING -#cmakedefine CUOPT_ANCHOR_GRPC - -// libcuopt_mathematical_optimization.so -extern "C" void cuOptDestroyProblem(void*); -__attribute__((used)) void (*const _cuopt_mathematical_optimization_anchor)(void*) = - &cuOptDestroyProblem; - -// libcuopt_base.so -namespace cuopt { -void print_version_info(int); -} -__attribute__((used)) void (*const _cuopt_base_anchor)(int) = &cuopt::print_version_info; - -#ifdef CUOPT_ANCHOR_ROUTING -// libcuopt_routing.so -#include -namespace cuopt::routing { -__attribute__((used)) assignment_t (*const _cuopt_routing_anchor)( - data_model_view_t const&, solver_settings_t const&) = &solve; -} -#endif - -#ifdef CUOPT_ANCHOR_GRPC -// libcuopt_grpc.so. Anchoring solve_lp_remote also guarantees the ELF constructor in -// grpc_registration.cpp runs, which registers the remote-solve callbacks in -// libcuopt_mathematical_optimization.so. -#include -#include -namespace cuopt::mathematical_optimization { -__attribute__((used)) solve_lp_remote_fn_t const _cuopt_grpc_anchor = &solve_lp_remote; -} -#endif diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 1e417a2e7c..d288ea3999 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -95,7 +95,8 @@ else() endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") -foreach(_target cuopt cuopt_base cuopt_routing cuopt_mathematical_optimization cuopt_grpc cuopt_cli cuopt_grpc_server) +# cuopt is an INTERFACE target (libcuopt.so is a linker script), so it has no RPATH. +foreach(_target cuopt_base cuopt_routing cuopt_mathematical_optimization cuopt_grpc cuopt_cli cuopt_grpc_server) if(TARGET ${_target}) set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${rpaths}) endif() diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index 77dd6ed161..43f7eb1366 100644 --- a/python/libcuopt/libcuopt/load.py +++ b/python/libcuopt/libcuopt/load.py @@ -53,7 +53,33 @@ def load_library(): != "false" ) - soname = "libcuopt.so" + # cuOpt ships as component libraries. libcuopt.so is a linker script, + # not an ELF object, so it cannot be dlopen()ed -- load the components + # instead. Each pulls its own dependencies in through DT_NEEDED, so this + # order only needs to be valid, not exhaustive. routing and grpc are + # optional (SKIP_ROUTING_BUILD, SKIP_GRPC_BUILD) and may be absent. + components = [ + ("libcuopt_base.so", True), + ("libcuopt_routing.so", False), + ("libcuopt_mathematical_optimization.so", True), + ("libcuopt_grpc.so", False), + ] + loaded = [] + for soname, required in components: + lib = _load_component(soname, prefer_system_installation, required) + if lib is not None: + loaded.append(lib) + return loaded + + +def _load_component( + soname: str, prefer_system_installation: bool, required: bool +): + """Load one cuOpt component. + + Returns the handle, or ``None`` if it could not be loaded. Failing to + load an optional component is silent; failing a required one warns. + """ libcuopt_lib = None if prefer_system_installation: # Prefer a system library if one is present to @@ -76,6 +102,8 @@ def load_library(): # If none of the searches above succeed, just silently return None # and rely on other mechanisms (like RPATHs on other DSOs) to # help the loader find the library. + if not required: + return None import warnings @@ -84,11 +112,10 @@ def load_library(): f"Error: {str(e)}. " "Falling back to relying on system loader. " "cuOpt functionality may be unavailable. " - "This might lead to a generic error such as " - "'libcuopt.so missing' if the library cannot be found.", + f"This might lead to a generic error such as " + f"'{soname} missing' if the library cannot be found.", RuntimeWarning, ) - pass # The caller almost never needs to do anything with this library, but no # harm in offering the option since this object at least provides a handle # to inspect where libcuopt was loaded from. diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 14d73cc5ee..416285dd06 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -65,6 +65,11 @@ sdist.reproducible = true wheel.packages = ["libcuopt"] wheel.install-dir = "libcuopt" wheel.py-api = "py3" +# libcuopt.so is a GNU ld script, not an ELF object. It exists so that -lcuopt keeps +# working for C consumers, who link against the conda or deb packages. Shipping a +# non-ELF file with a .so name would trip auditwheel, and nothing in the wheel loads +# it -- load.py dlopen()s the components directly. +wheel.exclude = ["**/libcuopt.so"] [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" From 6647ae493fcbab3a2603ec7b9be1c3c9465a4068 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 6 Aug 2026 22:17:30 -0500 Subject: [PATCH 23/40] style: apply clang-format and refresh copyright year Rewraps comments that exceeded the column limit after the cuopt_mathematical_optimization rename. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- .../mathematical_optimization/remote_solve_registry.hpp | 6 ++++-- python/libcuopt/libcuopt/load.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp index 2e008a0825..d0968cd0b6 100644 --- a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -12,7 +12,8 @@ #include #include -// Forward declarations — full types live in libcuopt_mathematical_optimization / libcuopt_grpc headers. +// Forward declarations — full types live in libcuopt_mathematical_optimization / libcuopt_grpc +// headers. namespace cuopt::mathematical_optimization { template @@ -43,7 +44,8 @@ using solve_mip_remote_fn_t = std::unique_ptr const&, mip_solver_settings_t const&); /** - * @brief Registry slots defined in libcuopt_mathematical_optimization.so (remote_solve_registry.cpp). + * @brief Registry slots defined in libcuopt_mathematical_optimization.so + * (remote_solve_registry.cpp). * * Null until libcuopt_grpc.so is loaded and calls register_remote_solvers(). Atomic * because the registering ELF constructor runs on whichever thread triggers the lazy diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index 43f7eb1366..fbc092973a 100644 --- a/python/libcuopt/libcuopt/load.py +++ b/python/libcuopt/libcuopt/load.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 From 34070b16118f7791393f63aecbcd5578841d9d62 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 7 Aug 2026 14:17:24 -0500 Subject: [PATCH 24/40] chore: drop stray files committed by mistake '1' is shell-redirection output and small_mip.mps is a local test problem; neither belongs in the repository. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- 1 | 424 -------------------------------------------------- small_mip.mps | 44 ------ 2 files changed, 468 deletions(-) delete mode 100644 1 delete mode 100644 small_mip.mps diff --git a/1 b/1 deleted file mode 100644 index 9ac6ba7ab0..0000000000 --- a/1 +++ /dev/null @@ -1,424 +0,0 @@ -Setting parameter absolute_dual_tolerance to 1.000000e-05 -Setting parameter relative_dual_tolerance to 1.000000e-05 -Setting parameter absolute_primal_tolerance to 1.000000e-05 -Setting parameter relative_primal_tolerance to 1.000000e-05 -Setting parameter absolute_gap_tolerance to 1.000000e-05 -Setting parameter relative_gap_tolerance to 1.000000e-05 -Setting parameter primal_infeasible_tolerance to 1.000000e-05 -Setting parameter dual_infeasible_tolerance to 1.000000e-05 -Setting parameter time_limit to 1.020000e+01 -Setting parameter iteration_limit to 10 -Setting parameter infeasibility_detection to false -Setting parameter mip_heuristics_only to true -Setting parameter mip_clique_cuts to 0 -Setting parameter method to 2 -Setting parameter user_problem_file to -Setting parameter mip_symmetry to 0 -Setting parameter mip_cut_min_orthogonality to 0.000000e+00 -Setting parameter mip_hyper_heuristic_relaxed_lp_time_limit to 1.000000e+00 -Setting parameter mip_hyper_heuristic_cycle_detection_length to 30 -Setting parameter mip_batch_pdlp_strong_branching to 1 -Setting parameter mip_hyper_diving_min_node_depth to 10 -Setting parameter mip_hyper_diving_farkas to -1 -Setting parameter mip_scaling to 1 -Setting parameter mip_hyper_diving_line_search to -1 -Setting parameter mip_hyper_heuristic_root_lp_time_ratio to 1.000000e-01 -Setting parameter strict_infeasibility to true -Setting parameter work_limit to 7.200000e+03 -Setting parameter mip_hyper_diving_iteration_limit_factor to 5.000000e-02 -Setting parameter mip_hyper_diving_node_limit to 500 -Setting parameter mip_hyper_heuristic_rins_fix_rate to 5.000000e-01 -Setting parameter dual_infeasible_tolerance to 1.000000e-02 -Setting parameter mip_hyper_heuristic_max_iterations_without_improvement to 8 -Setting parameter mip_reduced_cost_strengthening to 0 -Setting parameter absolute_dual_tolerance to 0.000000e+00 -Setting parameter mip_hyper_heuristic_rins_max_time_limit to 2.000000e+01 -Setting parameter mip_batch_pdlp_reliability_branching to 1 -Setting parameter mip_hyper_diving_backtrack_limit to 5 -Setting parameter mip_hyper_diving_pseudocost to -1 -Setting parameter mip_hyper_heuristic_presolve_max_time to 6.000000e+01 -Setting parameter mip_hyper_heuristic_num_cpufj_threads to 8 -Setting parameter pdlp_precision to 0 -Setting parameter folding to 0 -Setting parameter dual_postsolve to false -Setting parameter save_best_primal_so_far to true -Setting parameter log_file to 1 -Setting parameter cudss_deterministic to true -Setting parameter mip_mixed_integer_rounding_cuts to 0 -Setting parameter mip_cut_change_threshold to -5.000000e-01 -Setting parameter mip_knapsack_cuts to 0 -Setting parameter mip_reliability_branching to 0 -Setting parameter barrier_step_scale to 5.000000e-01 -Setting parameter per_constraint_residual to true -Setting parameter mip_probing to false -Setting parameter mip_hyper_heuristic_presolve_time_ratio to 1.000000e-01 -Setting parameter mip_implied_bound_cuts to 0 -Setting parameter mip_relative_tolerance to 1.000000e-02 -Setting parameter absolute_gap_tolerance to 0.000000e+00 -Setting parameter iteration_limit to 9999999 -Setting parameter mip_zero_half_cuts to 0 -Setting parameter presolve to 1 -Setting parameter node_limit to 2147483646 -Setting parameter mip_absolute_tolerance to 0.000000e+00 -Setting parameter augmented to 0 -Setting parameter solution_file to -Setting parameter relative_primal_tolerance to 0.000000e+00 -Setting parameter dualize to 0 -Setting parameter mip_hyper_heuristic_stagnation_trigger to 3 -Setting parameter pdlp_solver_mode to 3 -Setting parameter mip_hyper_heuristic_n_of_minimums_for_exit to 7000 -Setting parameter absolute_primal_tolerance to 0.000000e+00 -Setting parameter mip_hyper_heuristic_root_lp_max_time to 1.500000e+01 -Setting parameter mip_determinism_mode to 1 -Setting parameter relative_dual_tolerance to 0.000000e+00 -Setting parameter mip_relative_gap to 2.000000e-04 -Setting parameter crossover to true -Setting parameter mip_integrality_tolerance to 0.000000e+00 -Setting parameter mip_hyper_diving_vector_length to -1 -Setting parameter eliminate_dense_columns to false -Setting parameter mip_objective_step to 0 -Setting parameter mip_semi_continuous_big_m to 5.000000e+09 -Setting parameter mip_mixed_integer_gomory_cuts to 0 -Setting parameter mip_strong_chvatal_gomory_cuts to 0 -Setting parameter primal_infeasible_tolerance to 1.000000e-02 -Setting parameter relative_gap_tolerance to 0.000000e+00 -Setting parameter presolve_file to 1 -Setting parameter mip_hyper_heuristic_related_vars_time_limit to 3.000000e+01 -Setting parameter ordering to 0 -Setting parameter mip_strong_branching_simplex_iteration_limit to 0 -Setting parameter first_primal_feasible to true -Setting parameter mip_hyper_heuristic_rins_time_limit to 3.000000e+00 -Setting parameter barrier_dual_initial_point to 0 -Setting parameter mip_hyper_heuristic_initial_infeasibility_weight to 1.000000e+03 -Setting parameter mip_cut_passes to 9 -Setting parameter mip_absolute_gap to 1.000000e-02 -Setting parameter random_seed to 0 -Setting parameter num_cpu_threads to 0 -Setting parameter infeasibility_detection to true -Setting parameter barrier_iterative_refinement to false -Setting parameter time_limit to 3.600000e+03 -Setting parameter mip_hyper_diving_show_type to false -Setting parameter mip_flow_cover_cuts to 0 -Setting parameter mip_hyper_diving_coefficient to -1 -Setting parameter log_to_console to false -Setting parameter num_gpus to 2 -Setting parameter mip_hyper_diving_guided to -1 -Setting parameter mip_hyper_heuristic_enabled_recombiners to 15 -Setting parameter mip_hyper_heuristic_population_size to 32 -Setting parameter iteration_limit to 9999999 -Setting parameter node_limit to 2147483646 -Setting parameter pdlp_solver_mode to 3 -Setting parameter method to 2 -Setting parameter num_cpu_threads to 0 -Setting parameter augmented to 0 -Setting parameter folding to 0 -Setting parameter dualize to 0 -Setting parameter ordering to 0 -Setting parameter barrier_dual_initial_point to 0 -Setting parameter mip_cut_passes to 9 -Setting parameter mip_mixed_integer_rounding_cuts to 0 -Setting parameter mip_mixed_integer_gomory_cuts to 0 -Setting parameter mip_knapsack_cuts to 0 -Setting parameter mip_flow_cover_cuts to 0 -Setting parameter mip_clique_cuts to 0 -Setting parameter mip_zero_half_cuts to 0 -Setting parameter mip_implied_bound_cuts to 0 -Setting parameter mip_strong_chvatal_gomory_cuts to 0 -Setting parameter mip_reduced_cost_strengthening to 0 -Setting parameter mip_objective_step to 0 -Setting parameter num_gpus to 2 -Setting parameter num_gpus to 2 -Setting parameter mip_batch_pdlp_strong_branching to 1 -Setting parameter mip_batch_pdlp_reliability_branching to 1 -Setting parameter mip_strong_branching_simplex_iteration_limit to 0 -Setting parameter presolve to 1 -Setting parameter presolve to 1 -Setting parameter mip_determinism_mode to 1 -Setting parameter random_seed to 0 -Setting parameter mip_reliability_branching to 0 -Setting parameter pdlp_precision to 0 -Setting parameter mip_symmetry to 0 -Setting parameter mip_scaling to 1 -Setting parameter mip_hyper_heuristic_population_size to 32 -Setting parameter mip_hyper_heuristic_num_cpufj_threads to 8 -Setting parameter mip_hyper_heuristic_stagnation_trigger to 3 -Setting parameter mip_hyper_heuristic_max_iterations_without_improvement to 8 -Setting parameter mip_hyper_heuristic_n_of_minimums_for_exit to 7000 -Setting parameter mip_hyper_heuristic_enabled_recombiners to 15 -Setting parameter mip_hyper_heuristic_cycle_detection_length to 30 -Setting parameter mip_hyper_diving_line_search to -1 -Setting parameter mip_hyper_diving_pseudocost to -1 -Setting parameter mip_hyper_diving_guided to -1 -Setting parameter mip_hyper_diving_coefficient to -1 -Setting parameter mip_hyper_diving_farkas to -1 -Setting parameter mip_hyper_diving_vector_length to -1 -Setting parameter mip_hyper_diving_min_node_depth to 10 -Setting parameter mip_hyper_diving_node_limit to 500 -Setting parameter mip_hyper_diving_backtrack_limit to 5 -Setting parameter time_limit to 3.600000e+03 -Setting parameter time_limit to 3.600000e+03 -Setting parameter work_limit to 7.200000e+03 -Setting parameter absolute_dual_tolerance to 0.000000e+00 -Setting parameter relative_dual_tolerance to 0.000000e+00 -Setting parameter absolute_primal_tolerance to 0.000000e+00 -Setting parameter relative_primal_tolerance to 0.000000e+00 -Setting parameter absolute_gap_tolerance to 0.000000e+00 -Setting parameter relative_gap_tolerance to 0.000000e+00 -Setting parameter mip_absolute_tolerance to 0.000000e+00 -Setting parameter mip_relative_tolerance to 1.000000e-02 -Setting parameter mip_integrality_tolerance to 0.000000e+00 -Setting parameter mip_absolute_gap to 1.000000e-02 -Setting parameter mip_relative_gap to 2.000000e-04 -Setting parameter primal_infeasible_tolerance to 1.000000e-02 -Setting parameter dual_infeasible_tolerance to 1.000000e-02 -Setting parameter mip_cut_change_threshold to -5.000000e-01 -Setting parameter mip_cut_min_orthogonality to 0.000000e+00 -Setting parameter barrier_step_scale to 5.000000e-01 -Setting parameter mip_hyper_heuristic_presolve_time_ratio to 1.000000e-01 -Setting parameter mip_hyper_heuristic_presolve_max_time to 6.000000e+01 -Setting parameter mip_hyper_heuristic_root_lp_time_ratio to 1.000000e-01 -Setting parameter mip_hyper_heuristic_root_lp_max_time to 1.500000e+01 -Setting parameter mip_hyper_heuristic_rins_time_limit to 3.000000e+00 -Setting parameter mip_hyper_heuristic_rins_max_time_limit to 2.000000e+01 -Setting parameter mip_hyper_heuristic_rins_fix_rate to 5.000000e-01 -Setting parameter mip_hyper_heuristic_initial_infeasibility_weight to 1.000000e+03 -Setting parameter mip_hyper_heuristic_relaxed_lp_time_limit to 1.000000e+00 -Setting parameter mip_hyper_heuristic_related_vars_time_limit to 3.000000e+01 -Setting parameter mip_semi_continuous_big_m to 5.000000e+09 -Setting parameter mip_hyper_diving_iteration_limit_factor to 5.000000e-02 -Setting parameter infeasibility_detection to true -Setting parameter strict_infeasibility to true -Setting parameter per_constraint_residual to true -Setting parameter save_best_primal_so_far to true -Setting parameter first_primal_feasible to true -Setting parameter mip_heuristics_only to true -Setting parameter log_to_console to false -Setting parameter log_to_console to false -Setting parameter crossover to true -Setting parameter eliminate_dense_columns to false -Setting parameter cudss_deterministic to true -Setting parameter dual_postsolve to false -Setting parameter barrier_iterative_refinement to false -Setting parameter mip_probing to false -Setting parameter mip_hyper_diving_show_type to false -Setting parameter log_file to 1 -Setting parameter log_file to 1 -Setting parameter solution_file to -Setting parameter solution_file to -Setting parameter user_problem_file to -Setting parameter user_problem_file to -Setting parameter presolve_file to 1 -Setting parameter presolve_file to 1 -Parameters loaded from: /tmp/pytest-of-luffy/pytest-2/test_solver_settings0/solver_settings_load.config -Setting parameter iteration_limit to 9999999 -Setting parameter node_limit to 2147483646 -Setting parameter pdlp_solver_mode to 3 -Setting parameter method to 2 -Setting parameter num_cpu_threads to 0 -Setting parameter augmented to 0 -Setting parameter folding to 0 -Setting parameter dualize to 0 -Setting parameter ordering to 0 -Setting parameter barrier_dual_initial_point to 0 -Setting parameter mip_cut_passes to 9 -Setting parameter mip_mixed_integer_rounding_cuts to 0 -Setting parameter mip_mixed_integer_gomory_cuts to 0 -Setting parameter mip_knapsack_cuts to 0 -Setting parameter mip_flow_cover_cuts to 0 -Setting parameter mip_clique_cuts to 0 -Setting parameter mip_zero_half_cuts to 0 -Setting parameter mip_implied_bound_cuts to 0 -Setting parameter mip_strong_chvatal_gomory_cuts to 0 -Setting parameter mip_reduced_cost_strengthening to 0 -Setting parameter mip_objective_step to 0 -Setting parameter num_gpus to 2 -Setting parameter mip_batch_pdlp_strong_branching to 1 -Setting parameter mip_batch_pdlp_reliability_branching to 1 -Setting parameter mip_strong_branching_simplex_iteration_limit to 0 -Setting parameter presolve to 1 -Setting parameter mip_determinism_mode to 1 -Setting parameter random_seed to 0 -Setting parameter mip_reliability_branching to 0 -Setting parameter pdlp_precision to 0 -Setting parameter mip_symmetry to 0 -Setting parameter mip_scaling to 1 -Setting parameter mip_hyper_heuristic_population_size to 32 -Setting parameter mip_hyper_heuristic_num_cpufj_threads to 8 -Setting parameter mip_hyper_heuristic_stagnation_trigger to 3 -Setting parameter mip_hyper_heuristic_max_iterations_without_improvement to 8 -Setting parameter mip_hyper_heuristic_n_of_minimums_for_exit to 7000 -Setting parameter mip_hyper_heuristic_enabled_recombiners to 15 -Setting parameter mip_hyper_heuristic_cycle_detection_length to 30 -Setting parameter mip_hyper_diving_line_search to -1 -Setting parameter mip_hyper_diving_pseudocost to -1 -Setting parameter mip_hyper_diving_guided to -1 -Setting parameter mip_hyper_diving_coefficient to -1 -Setting parameter mip_hyper_diving_farkas to -1 -Setting parameter mip_hyper_diving_vector_length to -1 -Setting parameter mip_hyper_diving_min_node_depth to 10 -Setting parameter mip_hyper_diving_node_limit to 500 -Setting parameter mip_hyper_diving_backtrack_limit to 5 -Setting parameter time_limit to 3.600000e+03 -Setting parameter work_limit to 7.200000e+03 -Setting parameter absolute_dual_tolerance to 0.000000e+00 -Setting parameter relative_dual_tolerance to 0.000000e+00 -Setting parameter absolute_primal_tolerance to 0.000000e+00 -Setting parameter relative_primal_tolerance to 0.000000e+00 -Setting parameter absolute_gap_tolerance to 0.000000e+00 -Setting parameter relative_gap_tolerance to 0.000000e+00 -Setting parameter mip_absolute_tolerance to 0.000000e+00 -Setting parameter mip_relative_tolerance to 1.000000e-02 -Setting parameter mip_integrality_tolerance to 0.000000e+00 -Setting parameter mip_absolute_gap to 1.000000e-02 -Setting parameter mip_relative_gap to 2.000000e-04 -Setting parameter primal_infeasible_tolerance to 1.000000e-02 -Setting parameter dual_infeasible_tolerance to 1.000000e-02 -Setting parameter mip_cut_change_threshold to -5.000000e-01 -Setting parameter mip_cut_min_orthogonality to 0.000000e+00 -Setting parameter barrier_step_scale to 5.000000e-01 -Setting parameter mip_hyper_heuristic_presolve_time_ratio to 1.000000e-01 -Setting parameter mip_hyper_heuristic_presolve_max_time to 6.000000e+01 -Setting parameter mip_hyper_heuristic_root_lp_time_ratio to 1.000000e-01 -Setting parameter mip_hyper_heuristic_root_lp_max_time to 1.500000e+01 -Setting parameter mip_hyper_heuristic_rins_time_limit to 3.000000e+00 -Setting parameter mip_hyper_heuristic_rins_max_time_limit to 2.000000e+01 -Setting parameter mip_hyper_heuristic_rins_fix_rate to 5.000000e-01 -Setting parameter mip_hyper_heuristic_initial_infeasibility_weight to 1.000000e+03 -Setting parameter mip_hyper_heuristic_relaxed_lp_time_limit to 1.000000e+00 -Setting parameter mip_hyper_heuristic_related_vars_time_limit to 3.000000e+01 -Setting parameter mip_semi_continuous_big_m to 5.000000e+09 -Setting parameter mip_hyper_diving_iteration_limit_factor to 5.000000e-02 -Setting parameter infeasibility_detection to true -Setting parameter strict_infeasibility to true -Setting parameter per_constraint_residual to true -Setting parameter save_best_primal_so_far to true -Setting parameter first_primal_feasible to true -Setting parameter mip_heuristics_only to true -Setting parameter log_to_console to false -Setting parameter crossover to true -Setting parameter eliminate_dense_columns to false -Setting parameter cudss_deterministic to true -Setting parameter dual_postsolve to false -Setting parameter barrier_iterative_refinement to false -Setting parameter mip_probing to false -Setting parameter mip_hyper_diving_show_type to false -Setting parameter log_file to 1 -Setting parameter solution_file to -Setting parameter user_problem_file to -Setting parameter presolve_file to 1 -Setting parameter iteration_limit to 9999999 -Setting parameter node_limit to 2147483646 -Setting parameter pdlp_solver_mode to 3 -Setting parameter method to 2 -Setting parameter num_cpu_threads to 0 -Setting parameter augmented to 0 -Setting parameter folding to 0 -Setting parameter dualize to 0 -Setting parameter ordering to 0 -Setting parameter barrier_dual_initial_point to 0 -Setting parameter mip_cut_passes to 9 -Setting parameter mip_mixed_integer_rounding_cuts to 0 -Setting parameter mip_mixed_integer_gomory_cuts to 0 -Setting parameter mip_knapsack_cuts to 0 -Setting parameter mip_flow_cover_cuts to 0 -Setting parameter mip_clique_cuts to 0 -Setting parameter mip_zero_half_cuts to 0 -Setting parameter mip_implied_bound_cuts to 0 -Setting parameter mip_strong_chvatal_gomory_cuts to 0 -Setting parameter mip_reduced_cost_strengthening to 0 -Setting parameter mip_objective_step to 0 -Setting parameter num_gpus to 2 -Setting parameter mip_batch_pdlp_strong_branching to 1 -Setting parameter mip_batch_pdlp_reliability_branching to 1 -Setting parameter mip_strong_branching_simplex_iteration_limit to 0 -Setting parameter presolve to 1 -Setting parameter mip_determinism_mode to 1 -Setting parameter random_seed to 0 -Setting parameter mip_reliability_branching to 0 -Setting parameter pdlp_precision to 0 -Setting parameter mip_symmetry to 0 -Setting parameter mip_scaling to 1 -Setting parameter mip_hyper_heuristic_population_size to 32 -Setting parameter mip_hyper_heuristic_num_cpufj_threads to 8 -Setting parameter mip_hyper_heuristic_stagnation_trigger to 3 -Setting parameter mip_hyper_heuristic_max_iterations_without_improvement to 8 -Setting parameter mip_hyper_heuristic_n_of_minimums_for_exit to 7000 -Setting parameter mip_hyper_heuristic_enabled_recombiners to 15 -Setting parameter mip_hyper_heuristic_cycle_detection_length to 30 -Setting parameter mip_hyper_diving_line_search to -1 -Setting parameter mip_hyper_diving_pseudocost to -1 -Setting parameter mip_hyper_diving_guided to -1 -Setting parameter mip_hyper_diving_coefficient to -1 -Setting parameter mip_hyper_diving_farkas to -1 -Setting parameter mip_hyper_diving_vector_length to -1 -Setting parameter mip_hyper_diving_min_node_depth to 10 -Setting parameter mip_hyper_diving_node_limit to 500 -Setting parameter mip_hyper_diving_backtrack_limit to 5 -Setting parameter time_limit to 3.600000e+03 -Setting parameter work_limit to 7.200000e+03 -Setting parameter absolute_dual_tolerance to 0.000000e+00 -Setting parameter relative_dual_tolerance to 0.000000e+00 -Setting parameter absolute_primal_tolerance to 0.000000e+00 -Setting parameter relative_primal_tolerance to 0.000000e+00 -Setting parameter absolute_gap_tolerance to 0.000000e+00 -Setting parameter relative_gap_tolerance to 0.000000e+00 -Setting parameter mip_absolute_tolerance to 0.000000e+00 -Setting parameter mip_relative_tolerance to 1.000000e-02 -Setting parameter mip_integrality_tolerance to 0.000000e+00 -Setting parameter mip_absolute_gap to 1.000000e-02 -Setting parameter mip_relative_gap to 2.000000e-04 -Setting parameter primal_infeasible_tolerance to 1.000000e-02 -Setting parameter dual_infeasible_tolerance to 1.000000e-02 -Setting parameter mip_cut_change_threshold to -5.000000e-01 -Setting parameter mip_cut_min_orthogonality to 0.000000e+00 -Setting parameter barrier_step_scale to 5.000000e-01 -Setting parameter mip_hyper_heuristic_presolve_time_ratio to 1.000000e-01 -Setting parameter mip_hyper_heuristic_presolve_max_time to 6.000000e+01 -Setting parameter mip_hyper_heuristic_root_lp_time_ratio to 1.000000e-01 -Setting parameter mip_hyper_heuristic_root_lp_max_time to 1.500000e+01 -Setting parameter mip_hyper_heuristic_rins_time_limit to 3.000000e+00 -Setting parameter mip_hyper_heuristic_rins_max_time_limit to 2.000000e+01 -Setting parameter mip_hyper_heuristic_rins_fix_rate to 5.000000e-01 -Setting parameter mip_hyper_heuristic_initial_infeasibility_weight to 1.000000e+03 -Setting parameter mip_hyper_heuristic_relaxed_lp_time_limit to 1.000000e+00 -Setting parameter mip_hyper_heuristic_related_vars_time_limit to 3.000000e+01 -Setting parameter mip_semi_continuous_big_m to 5.000000e+09 -Setting parameter mip_hyper_diving_iteration_limit_factor to 5.000000e-02 -Setting parameter infeasibility_detection to true -Setting parameter strict_infeasibility to true -Setting parameter per_constraint_residual to true -Setting parameter save_best_primal_so_far to true -Setting parameter first_primal_feasible to true -Setting parameter mip_heuristics_only to true -Setting parameter log_to_console to false -Setting parameter crossover to true -Setting parameter eliminate_dense_columns to false -Setting parameter cudss_deterministic to true -Setting parameter dual_postsolve to false -Setting parameter barrier_iterative_refinement to false -Setting parameter mip_probing to false -Setting parameter mip_hyper_diving_show_type to false -Setting parameter log_file to 1 -Setting parameter solution_file to -Setting parameter user_problem_file to -Setting parameter presolve_file to 1 -cuOpt version: 26.8.0, git hash: 9b12a98d, host arch: x86_64, device archs: 75-real -CPU: AMD Ryzen Threadripper PRO 3975WX 32-Cores, threads (physical/logical): 32/64, RAM: 33.14 GiB -CUDA 13.3, device: Quadro RTX 8000 (ID 0), VRAM: 47.24 GiB -CUDA device UUID: b7f2b679-057e-ca93-f5bd-22907d9b1b3c - -Solving a problem with 2 constraints, 1 variables (0 integers), and 2 nonzeros -Problem scaling: -Objective coefficents range: [1e+00, 1e+00] -Constraint matrix coefficients range: [1e+00, 1e+00] -Constraint rhs / bounds range: [0e+00, 1e+00] -Variable bounds range: [0e+00, 0e+00] - - -Running Papilo presolve (git hash 741a2b9c) -Presolve status: reduced the problem -Presolve removed: 2 constraints, 1 variables, 2 nonzeros -Presolved problem: 0 constraints, 0 variables (0 integer), 0 nonzeros -Presolve completely solved the problem -Papilo presolve time: 0.00s -Status: Optimal Objective: 0.000000 diff --git a/small_mip.mps b/small_mip.mps deleted file mode 100644 index 5b312835f6..0000000000 --- a/small_mip.mps +++ /dev/null @@ -1,44 +0,0 @@ -NAME SMALLMIP -ROWS - N OBJ - L c1 - G c2 - E R2 - L R3 - G c5 -COLUMNS - MARK0001 'MARKER' 'INTORG' - x1 c1 1 - x1 c2 2 - x1 c5 1 - x1 OBJ 2 - x2 c1 1 - x2 R2 1 - x2 c5 1 - x2 OBJ 3 - x3 c1 1 - x3 c2 1 - x3 c5 1 - x3 OBJ 1 - x4 c2 -1 - x4 R3 1 - x4 c5 1 - x4 OBJ 1 - x5 R2 3 - x5 R3 1 - x5 c5 1 - x5 OBJ 4 - MARK0001 'MARKER' 'INTEND' -RHS - RHS1 c1 10 - RHS1 c2 3 - RHS1 R2 7 - RHS1 R3 8 - RHS1 c5 5 -BOUNDS - UI BOUND1 x1 inf - UI BOUND1 x2 4 - UI BOUND1 x3 6 - UI BOUND1 x4 inf - UI BOUND1 x5 inf -ENDATA From 38b15e132dd62ad80b19ab4184104198799061a8 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 10 Aug 2026 13:54:43 -0500 Subject: [PATCH 25/40] fix(build): drop argparse from the cuopt INTERFACE target cuopt became an INTERFACE target when libcuopt.so turned into a linker script, and PRIVATE/PUBLIC are not valid on a target that compiles nothing, so the libcuopt wheel build failed to configure: CMake Error at CMakeLists.txt:65 (target_link_libraries): INTERFACE library can only be used with the INTERFACE keyword of target_link_libraries The call was redundant anyway. argparse is used only by cuopt_cli.cpp and grpc_server_main.cpp, and cpp/CMakeLists.txt already links argparse::argparse into both cuopt_cli and cuopt_grpc_server. Verified by running ci/build_wheel_libcuopt.sh in the CI wheel image: the wheel builds, auditwheel repair succeeds, pydistcheck reports no errors and twine passes. The wheel contains the four component libraries and not the linker script. python/libcuopt is only configured by the wheel build, which is why a local cpp/build could not catch this. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- python/libcuopt/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index d288ea3999..8aaf765a74 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -62,9 +62,9 @@ set(CUOPT_BUILD_TESTUTIL OFF) add_subdirectory(../../cpp cuopt-cpp) -target_link_libraries(cuopt PRIVATE - argparse -) +# cuopt is an INTERFACE target (libcuopt.so is a linker script) and compiles nothing, +# so it has no use for argparse. cuopt_cli and cuopt_grpc_server, which do, already link +# argparse::argparse in cpp/CMakeLists.txt. target_link_libraries(cuopt_cli PRIVATE argparse ) From c7b70da1ec4cb606db98a29b190a892da08a565e Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 10 Aug 2026 15:06:52 -0500 Subject: [PATCH 26/40] fix(build): ship libcuopt.so linker script in the wheel The wheel installs 2020 C/C++ headers including cuopt_c.h, and the C API docs tell users to pip install libcuopt-cuXX then locate libcuopt.so and link -lcuopt. Excluding the linker script left that path with headers but nothing to link against. The exclusion was guarding against auditwheel choking on a non-ELF .so, which it does not do: auditwheel's elf_file_filter parses each candidate and skips anything raising ELFError, so the script is copied through untouched. Its --exclude option skips SONAMEs from being grafted, not files from being processed, so no exception was needed in the first place. Verified with ci/build_wheel_libcuopt.sh in the CI wheel image: auditwheel repair, pydistcheck and twine all pass, and the repaired wheel contains the linker script alongside the four component libraries. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- python/libcuopt/pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 416285dd06..14d73cc5ee 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -65,11 +65,6 @@ sdist.reproducible = true wheel.packages = ["libcuopt"] wheel.install-dir = "libcuopt" wheel.py-api = "py3" -# libcuopt.so is a GNU ld script, not an ELF object. It exists so that -lcuopt keeps -# working for C consumers, who link against the conda or deb packages. Shipping a -# non-ELF file with a .so name would trip auditwheel, and nothing in the wheel loads -# it -- load.py dlopen()s the components directly. -wheel.exclude = ["**/libcuopt.so"] [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" From 503c1fbb9b4f6e05cd8431262b7e8a4608912e3d Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 12 Aug 2026 11:42:24 -0500 Subject: [PATCH 27/40] build(cmake): shorten cuopt_mathematical_optimization to cuopt_mathopt 37 characters is a long name for a shared library next to librmm.so and libraft.so, and it would propagate into the per-solver package names in #1635 as libcuopt-mathematical-optimization-cuXX. mathopt still satisfies the intent of moving away from lp, and renaming before those packages exist is far cheaper than after. Applied consistently: the target, libcuopt_mathopt.so, the cuopt::mathopt export, and CUOPT_MATHOPT_SRC_FILES. The C++ namespace cuopt::mathematical_optimization and the cuopt/mathematical_optimization header directory are unchanged -- those are API surface, not the library name. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Ramakrishna Prabhu --- ci/build_wheel_cuopt.sh | 2 +- ci/test_skills_assets.sh | 2 +- conda/recipes/libcuopt/recipe.yaml | 8 +-- cpp/CMakeLists.txt | 56 +++++++++---------- .../remote_solve_registry.hpp | 4 +- cpp/src/CMakeLists.txt | 6 +- cpp/src/grpc/client/grpc_registration.cpp | 4 +- .../source/cuopt-c/convex/examples/Makefile | 2 +- .../source/cuopt-c/mip/examples/Makefile | 2 +- python/libcuopt/CMakeLists.txt | 2 +- python/libcuopt/libcuopt/load.py | 2 +- 11 files changed, 45 insertions(+), 45 deletions(-) diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index 79ed9503c2..ac63fdce65 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -43,7 +43,7 @@ EXCLUDE_ARGS=( --exclude "libcuopt.so" --exclude "libcuopt_base.so" --exclude "libcuopt_routing.so" - --exclude "libcuopt_mathematical_optimization.so" + --exclude "libcuopt_mathopt.so" --exclude "libcuopt_grpc.so" --exclude "librapids_logger.so" --exclude "librmm.so" diff --git a/ci/test_skills_assets.sh b/ci/test_skills_assets.sh index 27f54a2a71..6358ea405f 100755 --- a/ci/test_skills_assets.sh +++ b/ci/test_skills_assets.sh @@ -111,7 +111,7 @@ if [[ -n "${CONDA_PREFIX:-}" ]]; then base=$(basename "$cfile" .c) rel="${cfile#"$REPO_ROOT/"}" log "Building and running C asset: $rel" - if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt -lcuopt_mathematical_optimization); then + if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt -lcuopt_mathopt); then FAILED+=("$rel (build)") log "FAIL: $rel (build)" continue diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 0e2713d036..1017bf9108 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -109,8 +109,8 @@ outputs: content: | cmake --install cpp/build # libcuopt.so is a linker script, so the components are what carry symbols. - # Only mathematical_optimization provides the C API. - ./ci/check_symbols.sh cpp/build/libcuopt_mathematical_optimization.so + # Only cuopt_mathopt provides the C API. + ./ci/check_symbols.sh cpp/build/libcuopt_mathopt.so ./ci/check_symbols.sh cpp/build/libcuopt_base.so --no-public-api-check ./ci/check_symbols.sh cpp/build/libcuopt_routing.so --no-public-api-check ./ci/check_symbols.sh cpp/build/libcuopt_grpc.so --no-public-api-check @@ -121,7 +121,7 @@ outputs: # See https://github.com/rapidsai/build-planning/issues/160 - lib/libcuopt_base.so - lib/libcuopt_routing.so - - lib/libcuopt_mathematical_optimization.so + - lib/libcuopt_mathopt.so - lib/libcuopt_grpc.so string: cuda${{ cuda_major }}_${{ datetime_string }}_${{ head_rev }} requirements: @@ -177,7 +177,7 @@ outputs: - lib/libcuopt.so - lib/libcuopt_base.so - lib/libcuopt_routing.so - - lib/libcuopt_mathematical_optimization.so + - lib/libcuopt_mathopt.so - lib/libcuopt_grpc.so - bin/cuopt_cli - bin/cuopt_grpc_server diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 09d591d976..8d4b729b71 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -494,7 +494,7 @@ endif () set(CUOPT_SRC_FILES) set(CUOPT_BASE_SRC_FILES) set(CUOPT_ROUTING_SRC_FILES) -set(CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES) +set(CUOPT_MATHOPT_SRC_FILES) set(MPS_FAST_SRC_FILES) add_subdirectory(src) @@ -506,7 +506,7 @@ set_source_files_properties( if (HOST_LINEINFO) set_source_files_properties( - ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} + ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") endif () @@ -524,7 +524,7 @@ endif () # Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO). if (DEFINE_ASSERT) set_property( - SOURCE ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} + SOURCE ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG") endif () @@ -660,10 +660,10 @@ if(NOT SKIP_ROUTING_BUILD) add_library(cuopt::routing ALIAS cuopt_routing) endif() -# cuopt_mathematical_optimization: LP / MIP / numerical optimization engine -add_library(cuopt_mathematical_optimization SHARED ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES}) -cuopt_configure_component(cuopt_mathematical_optimization) -target_include_directories(cuopt_mathematical_optimization PRIVATE +# cuopt_mathopt: LP / MIP / numerical optimization engine +add_library(cuopt_mathopt SHARED ${CUOPT_MATHOPT_SRC_FILES}) +cuopt_configure_component(cuopt_mathopt) +target_include_directories(cuopt_mathopt PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty" "${CMAKE_CURRENT_SOURCE_DIR}/src/io" "${CUDSS_INCLUDE}" @@ -671,19 +671,19 @@ target_include_directories(cuopt_mathematical_optimization PRIVATE $<$:${ZLIB_INCLUDE_DIRS}> ) # Adding Papilo as a system include messes up clang's include resolution if papilo is already installed as a conda package -target_include_directories(cuopt_mathematical_optimization PRIVATE +target_include_directories(cuopt_mathopt PRIVATE "${papilo_SOURCE_DIR}/src" "${papilo_BINARY_DIR}" ) -target_include_directories(cuopt_mathematical_optimization SYSTEM PRIVATE +target_include_directories(cuopt_mathopt SYSTEM PRIVATE "${pslp_SOURCE_DIR}/include" "${dejavu_SOURCE_DIR}" ) -target_compile_definitions(cuopt_mathematical_optimization +target_compile_definitions(cuopt_mathopt PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API PRIVATE CUDSS_MT_LIB_FILE_NAME="${CUDSS_MT_LIB_FILE_NAME}" ) -target_link_libraries(cuopt_mathematical_optimization +target_link_libraries(cuopt_mathopt PUBLIC cuopt_base ${CUDSS_LIB_FILE} @@ -694,19 +694,19 @@ target_link_libraries(cuopt_mathematical_optimization nccl_external ${CMAKE_DL_LIBS} ) -target_link_libraries(cuopt_mathematical_optimization PRIVATE $) -add_dependencies(cuopt_mathematical_optimization PSLP) -target_include_directories(cuopt_mathematical_optimization SYSTEM PRIVATE +target_link_libraries(cuopt_mathopt PRIVATE $) +add_dependencies(cuopt_mathopt PSLP) +target_include_directories(cuopt_mathopt SYSTEM PRIVATE $) -target_compile_definitions(cuopt_mathematical_optimization PRIVATE TBB_PREVIEW_GLOBAL_CONTROL KAMINPAR_64BIT_EDGE_IDS) -target_link_libraries(cuopt_mathematical_optimization PRIVATE $) +target_compile_definitions(cuopt_mathopt PRIVATE TBB_PREVIEW_GLOBAL_CONTROL KAMINPAR_64BIT_EDGE_IDS) +target_link_libraries(cuopt_mathopt PRIVATE $) if (TARGET KaMinPar) - add_dependencies(cuopt_mathematical_optimization KaMinPar) + add_dependencies(cuopt_mathopt KaMinPar) endif () -add_library(cuopt::mathematical_optimization ALIAS cuopt_mathematical_optimization) +add_library(cuopt::mathopt ALIAS cuopt_mathopt) # cuopt_grpc: gRPC bridge — proto mappers + Cython client (LP and routing over gRPC) -# Depends on cuopt_mathematical_optimization and cuopt_routing so it can reference both APIs without forcing +# Depends on cuopt_mathopt and cuopt_routing so it can reference both APIs without forcing # either component to take a gRPC dependency. if(NOT SKIP_GRPC_BUILD) add_library(cuopt_grpc SHARED ${GRPC_INFRA_FILES}) @@ -718,9 +718,9 @@ if(NOT SKIP_GRPC_BUILD) "${CMAKE_CURRENT_SOURCE_DIR}/src/io" ) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt_grpc PUBLIC cuopt_mathematical_optimization cuopt_routing PRIVATE protobuf::libprotobuf gRPC::grpc++) + target_link_libraries(cuopt_grpc PUBLIC cuopt_mathopt cuopt_routing PRIVATE protobuf::libprotobuf gRPC::grpc++) else() - target_link_libraries(cuopt_grpc PUBLIC cuopt_mathematical_optimization PRIVATE protobuf::libprotobuf gRPC::grpc++) + target_link_libraries(cuopt_grpc PUBLIC cuopt_mathopt PRIVATE protobuf::libprotobuf gRPC::grpc++) endif() add_library(cuopt::grpc ALIAS cuopt_grpc) endif() @@ -885,7 +885,7 @@ set(CUOPT_LINKER_SCRIPT_INPUTS "libcuopt_base.so") if(NOT SKIP_ROUTING_BUILD) string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_routing.so") endif() -string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_mathematical_optimization.so") +string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_mathopt.so") if(NOT SKIP_GRPC_BUILD) string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_grpc.so") endif() @@ -908,9 +908,9 @@ target_include_directories(cuopt ) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt INTERFACE cuopt_base cuopt_routing cuopt_mathematical_optimization) + target_link_libraries(cuopt INTERFACE cuopt_base cuopt_routing cuopt_mathopt) else() - target_link_libraries(cuopt INTERFACE cuopt_base cuopt_mathematical_optimization) + target_link_libraries(cuopt INTERFACE cuopt_base cuopt_mathopt) endif() if(NOT SKIP_GRPC_BUILD) target_link_libraries(cuopt INTERFACE cuopt_grpc) @@ -927,7 +927,7 @@ if (WRITE_FATBIN) ]=]) # Applies to the components that actually carry CUDA fatbins. Before the split this # was set on libcuopt.so, which held all the device code; it now holds none. - set(_CUOPT_FATBIN_TARGETS cuopt_base cuopt_mathematical_optimization) + set(_CUOPT_FATBIN_TARGETS cuopt_base cuopt_mathopt) if(NOT SKIP_ROUTING_BUILD) list(APPEND _CUOPT_FATBIN_TARGETS cuopt_routing) endif() @@ -957,7 +957,7 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () -set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_mathematical_optimization) +set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_mathopt) if(NOT SKIP_ROUTING_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) endif() @@ -965,7 +965,7 @@ if(NOT SKIP_GRPC_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_grpc) endif() -# Export as cuopt::base / cuopt::routing / cuopt::mathematical_optimization / cuopt::grpc, matching the +# Export as cuopt::base / cuopt::routing / cuopt::mathopt / cuopt::grpc, matching the # build-tree aliases. set(CUOPT_COMPONENT_EXPORT_NAMES "") foreach(_component ${CUOPT_COMPONENT_TARGETS}) @@ -1011,7 +1011,7 @@ set(doc_string Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. -Component targets: cuopt::base, cuopt::routing, cuopt::mathematical_optimization, cuopt::grpc (when gRPC is built) +Component targets: cuopt::base, cuopt::routing, cuopt::mathopt, cuopt::grpc (when gRPC is built) Umbrella target: cuopt::cuopt (links all component libs — backward-compatible with -lcuopt) ]=]) diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp index cd2559b05f..b2b0da55ab 100644 --- a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -14,7 +14,7 @@ #include #include -// Forward declarations — full types live in libcuopt_mathematical_optimization / libcuopt_grpc +// Forward declarations — full types live in libcuopt_mathopt / libcuopt_grpc // headers. namespace cuopt::mathematical_optimization { @@ -46,7 +46,7 @@ using solve_mip_remote_fn_t = std::unique_ptr const&, mip_solver_settings_t const&); /** - * @brief Registry slots defined in libcuopt_mathematical_optimization.so + * @brief Registry slots defined in libcuopt_mathopt.so * (remote_solve_registry.cpp). * * Null until libcuopt_grpc.so is loaded and calls register_remote_solvers(). Atomic diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 94c54d330d..264d315b19 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -34,7 +34,7 @@ set(CUOPT_ROUTING_SRC_FILES ${ROUTING_SRC_FILES} ) -set(CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES +set(CUOPT_MATHOPT_SRC_FILES ${LINEAR_ALGEBRA_SRC_FILES} ${LP_SRC_FILES} ${MATH_OPT_SRC_FILES} @@ -48,11 +48,11 @@ set(CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES set(CUOPT_BASE_SRC_FILES ${CUOPT_BASE_SRC_FILES} PARENT_SCOPE) set(CUOPT_ROUTING_SRC_FILES ${CUOPT_ROUTING_SRC_FILES} PARENT_SCOPE) -set(CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} PARENT_SCOPE) +set(CUOPT_MATHOPT_SRC_FILES ${CUOPT_MATHOPT_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} - ${CUOPT_MATHEMATICAL_OPTIMIZATION_SRC_FILES} + ${CUOPT_MATHOPT_SRC_FILES} PARENT_SCOPE ) set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/grpc/client/grpc_registration.cpp b/cpp/src/grpc/client/grpc_registration.cpp index 9ed54d4b1c..a7ec47bda6 100644 --- a/cpp/src/grpc/client/grpc_registration.cpp +++ b/cpp/src/grpc/client/grpc_registration.cpp @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// Registers the gRPC-based remote solve implementations with libcuopt_mathematical_optimization.so +// Registers the gRPC-based remote solve implementations with libcuopt_mathopt.so // at dynamic-link time (before any user code runs). This breaks the circular -// dependency: libcuopt_mathematical_optimization.so holds nullable function pointers rather than a +// dependency: libcuopt_mathopt.so holds nullable function pointers rather than a // hard reference to symbols in libcuopt_grpc.so. #include diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 43585f9247..327f2bff23 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -46,7 +46,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathematical_optimization -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index 51f9b409f2..a6c3a82ffc 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -22,7 +22,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathematical_optimization -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 8aaf765a74..b3bdb5b1ec 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -96,7 +96,7 @@ endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") # cuopt is an INTERFACE target (libcuopt.so is a linker script), so it has no RPATH. -foreach(_target cuopt_base cuopt_routing cuopt_mathematical_optimization cuopt_grpc cuopt_cli cuopt_grpc_server) +foreach(_target cuopt_base cuopt_routing cuopt_mathopt cuopt_grpc cuopt_cli cuopt_grpc_server) if(TARGET ${_target}) set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${rpaths}) endif() diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index fbc092973a..3fa7fc2f96 100644 --- a/python/libcuopt/libcuopt/load.py +++ b/python/libcuopt/libcuopt/load.py @@ -61,7 +61,7 @@ def load_library(): components = [ ("libcuopt_base.so", True), ("libcuopt_routing.so", False), - ("libcuopt_mathematical_optimization.so", True), + ("libcuopt_mathopt.so", True), ("libcuopt_grpc.so", False), ] loaded = [] From 51a090a7f9e3b2487145ddd76d78e5c2b6ade0b8 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 9 Sep 2026 15:58:55 -0500 Subject: [PATCH 28/40] build(cmake): drop cuopt_base and fold its sources into cuopt_mathopt cuopt_base had one consumer. libcuopt_mathopt.so referenced 9 symbols from it -- seed_generator::seed_, print_version_info, six work_unit_scheduler_t methods and printTimestamp -- while libcuopt_routing.so, libcuopt_grpc.so, libcuopt_client.so, cuopt_cli and cuopt_grpc_server referenced none, and routing carried no DT_NEEDED on it at all. Its rationale had eroded from three directions since it was written. The logger became header-only per component in #1778, linear algebra moved into cuopt_mathopt after review, and #1809 replaced the process-wide seed generator with per-worker RNGs. What remained was UTIL_SRC_FILES, four utility files, shipped as a 1 MB shared library for a single caller. The argument for a separate library is that duplicating stateful code into two components gives two copies of the global state. That no longer applies when only one component uses it. UTIL_SRC_FILES now builds into cuopt_mathopt. Since routing inherited rmm/raft/CCCL/cublas/cusparse transitively through cuopt_base's PUBLIC link interface, both components now declare those explicitly, and the architecture defines that version_info.cpp reports move to cuopt_mathopt. Also drops the component from the linker script, the umbrella INTERFACE lists, the fatbin targets, the export set, the conda recipe, the wheel exclusions and load.py. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- ci/build_wheel_cuopt.sh | 1 - conda/recipes/libcuopt/recipe.yaml | 3 -- cpp/CMakeLists.txt | 60 +++++++++++++----------------- cpp/src/CMakeLists.txt | 12 +++--- python/libcuopt/CMakeLists.txt | 2 +- python/libcuopt/libcuopt/load.py | 1 - 6 files changed, 31 insertions(+), 48 deletions(-) diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index b4aad82930..ac03c4f8b9 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -41,7 +41,6 @@ EXCLUDE_ARGS=( --exclude "libcusolver.so.*" --exclude "libcusparse.so.*" --exclude "libcuopt.so" - --exclude "libcuopt_base.so" --exclude "libcuopt_routing.so" --exclude "libcuopt_mathopt.so" --exclude "libcuopt_grpc.so" diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 884f0226d3..8b8c24dc95 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -110,7 +110,6 @@ outputs: # libcuopt.so is a linker script, so the components are what carry symbols. # Only cuopt_mathopt provides the C API. ./ci/check_symbols.sh cpp/build/libcuopt_mathopt.so - ./ci/check_symbols.sh cpp/build/libcuopt_base.so --no-public-api-check ./ci/check_symbols.sh cpp/build/libcuopt_routing.so --no-public-api-check ./ci/check_symbols.sh cpp/build/libcuopt_grpc.so --no-public-api-check dynamic_linking: @@ -118,7 +117,6 @@ outputs: prefix_detection: ignore: # See https://github.com/rapidsai/build-planning/issues/160 - - lib/libcuopt_base.so - lib/libcuopt_routing.so - lib/libcuopt_mathopt.so - lib/libcuopt_grpc.so @@ -173,7 +171,6 @@ outputs: - package_contents: files: - lib/libcuopt.so - - lib/libcuopt_base.so - lib/libcuopt_routing.so - lib/libcuopt_mathopt.so - lib/libcuopt_grpc.so diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 755f4fd072..f3aeaf50b5 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -547,7 +547,6 @@ if (BUILD_TESTS) endif () set(CUOPT_SRC_FILES) -set(CUOPT_BASE_SRC_FILES) set(CUOPT_ROUTING_SRC_FILES) set(CUOPT_MATHOPT_SRC_FILES) set(MPS_FAST_SRC_FILES) @@ -555,7 +554,7 @@ add_subdirectory(src) if (HOST_LINEINFO) set_source_files_properties( - ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} + ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") endif () @@ -573,7 +572,7 @@ endif () # Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO). if (DEFINE_ASSERT) set_property( - SOURCE ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} + SOURCE ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG") endif () @@ -702,34 +701,18 @@ set(CUOPT_PRIVATE_CUDA_LIBS get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) -# cuopt_base: utilities + linear algebra (stateful shared infra — logger, work scheduler) -add_library(cuopt_base SHARED ${CUOPT_BASE_SRC_FILES}) -cuopt_configure_component(cuopt_base) -target_include_directories(cuopt_base PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include") -target_compile_definitions(cuopt_base PUBLIC - CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" - CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}" -) -target_link_libraries(cuopt_base - PUBLIC - rmm::rmm - rapids_logger::rapids_logger - CCCL::CCCL - raft::raft - CUDA::cublas - CUDA::cusparse - PRIVATE - OpenMP::OpenMP_CXX - OpenMP::OpenMP_CUDA -) -add_library(cuopt::base ALIAS cuopt_base) - # cuopt_routing: VRP / routing engine (omitted when SKIP_ROUTING_BUILD=ON) if(NOT SKIP_ROUTING_BUILD) add_library(cuopt_routing SHARED ${CUOPT_ROUTING_SRC_FILES}) cuopt_configure_component(cuopt_routing) target_link_libraries(cuopt_routing - PUBLIC cuopt_base + PUBLIC + rmm::rmm + rapids_logger::rapids_logger + CCCL::CCCL + raft::raft + CUDA::cublas + CUDA::cusparse PRIVATE simde::simde OpenMP::OpenMP_CXX OpenMP::OpenMP_CUDA ) add_library(cuopt::routing ALIAS cuopt_routing) @@ -756,11 +739,19 @@ target_include_directories(cuopt_mathopt SYSTEM PRIVATE ) target_compile_definitions(cuopt_mathopt PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API + # utilities/version_info.cpp reports these; it builds here now that cuopt_base is gone. + CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" + CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}" PRIVATE CUDSS_MT_LIB_FILE_NAME="${CUDSS_MT_LIB_FILE_NAME}" ) target_link_libraries(cuopt_mathopt PUBLIC - cuopt_base + rmm::rmm + rapids_logger::rapids_logger + CCCL::CCCL + raft::raft + CUDA::cublas + CUDA::cusparse ${CUDSS_LIB_FILE} PRIVATE ${CUOPT_PRIVATE_CUDA_LIBS} @@ -956,11 +947,10 @@ endif (BUILD_TESTS) # for consumers that linked the pre-split library. cuopt::cuopt is the CMake equivalent. # See cmake/libcuopt.so.in for why a real ELF cannot serve this purpose. -set(CUOPT_LINKER_SCRIPT_INPUTS "libcuopt_base.so") +set(CUOPT_LINKER_SCRIPT_INPUTS "libcuopt_mathopt.so") if(NOT SKIP_ROUTING_BUILD) string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_routing.so") endif() -string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_mathopt.so") if(NOT SKIP_GRPC_BUILD) string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_grpc.so") endif() @@ -983,9 +973,9 @@ target_include_directories(cuopt ) if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt INTERFACE cuopt_base cuopt_routing cuopt_mathopt) + target_link_libraries(cuopt INTERFACE cuopt_routing cuopt_mathopt) else() - target_link_libraries(cuopt INTERFACE cuopt_base cuopt_mathopt) + target_link_libraries(cuopt INTERFACE cuopt_mathopt) endif() if(NOT SKIP_GRPC_BUILD) target_link_libraries(cuopt INTERFACE cuopt_grpc) @@ -1002,7 +992,7 @@ if (WRITE_FATBIN) ]=]) # Applies to the components that actually carry CUDA fatbins. Before the split this # was set on libcuopt.so, which held all the device code; it now holds none. - set(_CUOPT_FATBIN_TARGETS cuopt_base cuopt_mathopt) + set(_CUOPT_FATBIN_TARGETS cuopt_mathopt) if(NOT SKIP_ROUTING_BUILD) list(APPEND _CUOPT_FATBIN_TARGETS cuopt_routing) endif() @@ -1032,7 +1022,7 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () -set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_mathopt) +set(CUOPT_COMPONENT_TARGETS cuopt_mathopt) if(NOT SKIP_ROUTING_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) endif() @@ -1040,7 +1030,7 @@ if(NOT SKIP_GRPC_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_grpc) endif() -# Export as cuopt::base / cuopt::routing / cuopt::mathopt / cuopt::grpc, matching the +# Export as cuopt::routing / cuopt::mathopt / cuopt::grpc, matching the # build-tree aliases. set(CUOPT_COMPONENT_EXPORT_NAMES "") foreach(_component ${CUOPT_COMPONENT_TARGETS}) @@ -1086,7 +1076,7 @@ set(doc_string Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. -Component targets: cuopt::base, cuopt::routing, cuopt::mathopt, cuopt::grpc (when gRPC is built) +Component targets: cuopt::routing, cuopt::mathopt, cuopt::grpc (when gRPC is built) Umbrella target: cuopt::cuopt (links all component libs — backward-compatible with -lcuopt) ]=]) diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index 03831bcb28..027dfc69d4 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -24,16 +24,16 @@ add_subdirectory(barrier) add_subdirectory(branch_and_bound) add_subdirectory(cuts) -# Aggregate per-domain source lists for the three component libraries -set(CUOPT_BASE_SRC_FILES - ${UTIL_SRC_FILES} -) - +# Aggregate per-domain source lists for the component libraries. +# UTIL_SRC_FILES lives in cuopt_mathopt rather than a separate cuopt_base: mathopt is its +# only consumer (routing, grpc and client reference none of its symbols), so a standalone +# library would exist to serve one caller. set(CUOPT_ROUTING_SRC_FILES ${ROUTING_SRC_FILES} ) set(CUOPT_MATHOPT_SRC_FILES + ${UTIL_SRC_FILES} ${LINEAR_ALGEBRA_SRC_FILES} ${LP_SRC_FILES} ${MATH_OPT_SRC_FILES} @@ -45,11 +45,9 @@ set(CUOPT_MATHOPT_SRC_FILES ${CUTS_SRC_FILES} ) -set(CUOPT_BASE_SRC_FILES ${CUOPT_BASE_SRC_FILES} PARENT_SCOPE) set(CUOPT_ROUTING_SRC_FILES ${CUOPT_ROUTING_SRC_FILES} PARENT_SCOPE) set(CUOPT_MATHOPT_SRC_FILES ${CUOPT_MATHOPT_SRC_FILES} PARENT_SCOPE) set(CUOPT_SRC_FILES - ${CUOPT_BASE_SRC_FILES} ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} PARENT_SCOPE diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index b3bdb5b1ec..f7e1919a66 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -96,7 +96,7 @@ endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") # cuopt is an INTERFACE target (libcuopt.so is a linker script), so it has no RPATH. -foreach(_target cuopt_base cuopt_routing cuopt_mathopt cuopt_grpc cuopt_cli cuopt_grpc_server) +foreach(_target cuopt_routing cuopt_mathopt cuopt_grpc cuopt_cli cuopt_grpc_server) if(TARGET ${_target}) set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${rpaths}) endif() diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index 3fa7fc2f96..12b7f380b9 100644 --- a/python/libcuopt/libcuopt/load.py +++ b/python/libcuopt/libcuopt/load.py @@ -59,7 +59,6 @@ def load_library(): # order only needs to be valid, not exhaustive. routing and grpc are # optional (SKIP_ROUTING_BUILD, SKIP_GRPC_BUILD) and may be absent. components = [ - ("libcuopt_base.so", True), ("libcuopt_routing.so", False), ("libcuopt_mathopt.so", True), ("libcuopt_grpc.so", False), From 87e602bb1b11ea12dce901f7565042a5edf2490a Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 10 Sep 2026 10:37:25 -0500 Subject: [PATCH 29/40] refactor: build the routing gRPC arm into cuopt_client The routing gRPC mappers were the last thing keeping a GPU-free client from covering VRP. They stayed out of cuopt_client because they reached into the routing engine, but the reach turned out to be shallow: 14 symbols, all trivial host-only accessors that happen to sit in CUDA translation units. grpc_routing_settings_mapper 8 routing::solver_settings_t getters/setters grpc_routing_solution_mapper 6 routing::assignment_t getters grpc_routing_problem_mapper 0 grpc_client_vrp 0 cython_grpc_client_vrp 0 The VRP client itself needed nothing from the engine. routing/solver_settings.cu is renamed to .cpp -- the whole file was already host code, plain accessors over scalar members, and its header pulls in no CUDA. The six assignment_t accessors move to assignment_accessors.cpp and are instantiated per member rather than with `template class`, which would also instantiate the device-facing members and pull CUDA back in. This is the same split #1801 through #1803 applied to the LP/MIP settings, and the approach #1804 anticipated for routing. libcuopt_client.so grows 2.3 MB to 2.4 MB and keeps its defining properties: no CUDA, rmm or raft in NEEDED, no undefined cuopt:: symbols, and no DT_NEEDED on any other cuOpt library. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 5 +- cpp/src/routing/CMakeLists.txt | 10 ++- cpp/src/routing/assignment.cu | 36 ---------- cpp/src/routing/assignment_accessors.cpp | 72 +++++++++++++++++++ ...solver_settings.cu => solver_settings.cpp} | 0 5 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 cpp/src/routing/assignment_accessors.cpp rename cpp/src/routing/{solver_settings.cu => solver_settings.cpp} (100%) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 773f1b5a18..015b938682 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -617,8 +617,11 @@ if (NOT SKIP_GRPC_BUILD) # translation units, so moving it down would create a libcuopt -> cuopt_client -> libcuopt # cycle. Those accessors need splitting out first, as was done for the LP/MIP settings. list(APPEND CUOPT_CLIENT_SRC_FILES ${GRPC_PROTO_GENERATED_FILES} ${GRPC_MATHOPT_FILES}) + # The routing arm now builds into cuopt_client alongside the LP/MIP arm. Its mappers only + # touch routing::solver_settings_t and assignment_t accessors, which are host-only and + # build there too, so this no longer drags in the routing engine. if (CUOPT_ENABLE_GRPC_ROUTING) - list(APPEND CUOPT_SRC_FILES ${GRPC_ROUTING_FILES}) + list(APPEND CUOPT_CLIENT_SRC_FILES ${GRPC_ROUTING_FILES}) endif () # solve_remote.cpp is the local-vs-remote dispatcher: it calls into the GPU diff --git a/cpp/src/routing/CMakeLists.txt b/cpp/src/routing/CMakeLists.txt index 844eff833c..fda06d0809 100644 --- a/cpp/src/routing/CMakeLists.txt +++ b/cpp/src/routing/CMakeLists.txt @@ -46,9 +46,17 @@ set(ROUTING_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/fleet_order_info.cu ${CMAKE_CURRENT_SOURCE_DIR}/solve.cu ${CMAKE_CURRENT_SOURCE_DIR}/solver.cu - ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu ${CMAKE_CURRENT_SOURCE_DIR}/cpu_routing_problem.cu ${CMAKE_CURRENT_SOURCE_DIR}/utilities/check_input.cu ${CMAKE_CURRENT_SOURCE_DIR}/utilities/cython.cu) +# routing::solver_settings_t is entirely host code -- plain getters and setters over scalar +# members. It builds into cuopt_client so the routing gRPC mappers, which only touch these +# accessors, do not have to reach into the CUDA side of the routing engine. +set(ROUTING_CLIENT_SRC_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/assignment_accessors.cpp +) + set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${ROUTING_SRC_FILES} PARENT_SCOPE) +set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${ROUTING_CLIENT_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/routing/assignment.cu b/cpp/src/routing/assignment.cu index 14862914ef..43a40b3302 100644 --- a/cpp/src/routing/assignment.cu +++ b/cpp/src/routing/assignment.cu @@ -79,24 +79,6 @@ assignment_t::assignment_t(i_t vehicle_count, { } -template -double assignment_t::get_total_objective() const -{ - return total_objective_value_; -} - -template -const std::map& assignment_t::get_objectives() const noexcept -{ - return objective_values_; -} - -template -i_t assignment_t::get_vehicle_count() const -{ - return vehicle_count_; -} - template double assignment_t::get_runtime() const noexcept { @@ -205,12 +187,6 @@ void assignment_t::to_csv(std::string_view filename, rmm::cuda_stream_view myfile << truck_id[i] << ",\t" << route[i] << ",\t" << arrival_stamp[i] << std::endl; } -template -std::string assignment_t::get_status_string() const noexcept -{ - return solution_string_; -} - template void assignment_t::set_vehicle_count(i_t vehicle_count) { @@ -223,18 +199,6 @@ void assignment_t::set_status(solution_status_t status) status_ = status; } -template -solution_status_t assignment_t::get_status() const -{ - return status_; -} - -template -cuopt::logic_error assignment_t::get_error_status() const noexcept -{ - return error_status_; -} - template void assignment_t::print(std::ostream& os) const noexcept { diff --git a/cpp/src/routing/assignment_accessors.cpp b/cpp/src/routing/assignment_accessors.cpp new file mode 100644 index 0000000000..f86a99ab02 --- /dev/null +++ b/cpp/src/routing/assignment_accessors.cpp @@ -0,0 +1,72 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Host-only members of assignment_t, split out of assignment.cu so they can build into the +// CUDA-free cuopt_client. They read scalar members only and never touch the device_uvectors +// the rest of the class holds, so compiling them here emits no CUDA runtime dependency. +// +// The routing gRPC solution mapper needs exactly these; keeping them in the CUDA translation +// unit is what forced that mapper into a component linked against the routing engine. + +#include +#include +#include + +#include +#include + +namespace cuopt { +namespace routing { + +template +double assignment_t::get_total_objective() const +{ + return total_objective_value_; +} + +template +const std::map& assignment_t::get_objectives() const noexcept +{ + return objective_values_; +} + +template +i_t assignment_t::get_vehicle_count() const +{ + return vehicle_count_; +} + +template +std::string assignment_t::get_status_string() const noexcept +{ + return solution_string_; +} + +template +solution_status_t assignment_t::get_status() const +{ + return status_; +} + +template +cuopt::logic_error assignment_t::get_error_status() const noexcept +{ + return error_status_; +} + +// Instantiated per member rather than with `template class`: that would also instantiate the +// device-facing members defined in assignment.cu and pull CUDA into this translation unit. +template CUOPT_EXPORT double assignment_t::get_total_objective() const; +template CUOPT_EXPORT const std::map& assignment_t::get_objectives() + const noexcept; +template CUOPT_EXPORT int assignment_t::get_vehicle_count() const; +template CUOPT_EXPORT std::string assignment_t::get_status_string() const noexcept; +template CUOPT_EXPORT solution_status_t assignment_t::get_status() const; +template CUOPT_EXPORT cuopt::logic_error assignment_t::get_error_status() const noexcept; + +} // namespace routing +} // namespace cuopt diff --git a/cpp/src/routing/solver_settings.cu b/cpp/src/routing/solver_settings.cpp similarity index 100% rename from cpp/src/routing/solver_settings.cu rename to cpp/src/routing/solver_settings.cpp From 398fca21b650416308a3a8ba72d88401cec7cdf4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 10 Sep 2026 11:43:58 -0500 Subject: [PATCH 30/40] test: cover the routing gRPC path end to end and at the settings mapper The routing gRPC arm had almost no C++ coverage. GRPC_INTEGRATION_TEST held no routing cases at all, and of the two mappers that read the accessors this branch moves, neither had a test -- only the problem mapper did, and it is the one that touches no accessors. Adds two tests that catch different things. DefaultServerTests.SolveVRP follows the same shape as the LP and MIP cases in that fixture: submit a problem, poll to completion, fetch the solution, check it. The problem is built in code rather than loaded from a fixture file so it does not depend on the routing datasets, and the assertions avoid pinning a particular route ordering -- only that the solve succeeded and left no order unserved. GRPC_ROUTING_SETTINGS_MAPPER_TEST round-trips routing::solver_settings_t through the proto. Six of its eight accessors previously had no test at all. It covers the presence semantics an end-to-end solve cannot see: an unset time_limit must not be serialized, since the solver derives its default from absence, while an explicit zero must survive. Both were checked by mutation. Removing the time_limit presence guard leaves SolveVRP passing -- it sets an explicit limit, so it never exercises that path -- while the mapper test fails. The end-to-end test is the right primary but is not a superset of the mapper test. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- .../grpc/grpc_integration_test.cpp | 62 +++++++++ cpp/tests/routing/grpc/CMakeLists.txt | 37 ++++++ .../grpc_routing_settings_mapper_test.cpp | 120 ++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 cpp/tests/routing/grpc/grpc_routing_settings_mapper_test.cpp diff --git a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp index 4462d262f1..2d20dff6e4 100644 --- a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp @@ -43,6 +43,8 @@ #include #include #include +#include +#include #include #include "grpc_client.hpp" @@ -1159,6 +1161,66 @@ TEST_F(DefaultServerTests, CancelRunningJob) // -- Delete should cancel queued / running jobs -- +// VRP over gRPC, in the same shape as the LP and MIP cases above: submit a problem, +// poll to completion, fetch the solution and check it. This is the only routing case in +// this suite, and it is the end-to-end exercise of the routing mappers -- the problem and +// settings on the way in, the solution on the way back. +TEST_F(DefaultServerTests, SolveVRP) +{ + auto client = create_client(); + ASSERT_NE(client, nullptr); + + // Four locations on a line, depot at 0, one vehicle. Any correct solver serves all + // three orders, so the assertions do not depend on a particular route ordering. + constexpr int32_t kLocations = 4; + cuopt::routing::cpu_routing_problem_t problem; + problem.num_locations = kLocations; + problem.fleet_size = 1; + problem.num_orders = kLocations; + + cuopt::routing::cpu_cost_matrix_t cost; + cost.vehicle_type = 0; + cost.matrix.resize(static_cast(kLocations) * kLocations); + for (int32_t i = 0; i < kLocations; ++i) { + for (int32_t j = 0; j < kLocations; ++j) { + cost.matrix[static_cast(i) * kLocations + j] = static_cast(std::abs(i - j)); + } + } + problem.cost_matrices.push_back(std::move(cost)); + problem.vehicle_start_locations.assign(problem.fleet_size, 0); + problem.vehicle_return_locations.assign(problem.fleet_size, 0); + + cuopt::routing::solver_settings_t settings; + settings.set_time_limit(10.0f); + + auto submit_result = client->submit_vrp(problem, settings); + ASSERT_TRUE(submit_result.success) << submit_result.error_message; + EXPECT_FALSE(submit_result.job_id.empty()); + + job_status_t final_status = job_status_t::QUEUED; + for (int i = 0; i < 60; ++i) { + auto status = client->check_status(submit_result.job_id); + ASSERT_TRUE(status.success) << status.error_message; + final_status = status.status; + if (final_status == job_status_t::COMPLETED || final_status == job_status_t::FAILED) break; + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + ASSERT_EQ(final_status, job_status_t::COMPLETED) + << "Status: " << job_status_to_string(final_status); + + auto result = client->get_vrp_result(submit_result.job_id); + ASSERT_TRUE(result.success) << result.error_message; + + const auto& solution = result.solution; + EXPECT_EQ(solution.status, 0) << solution.status_message; + EXPECT_GT(solution.vehicle_count, 0); + EXPECT_GT(solution.total_objective_value, 0.0); + EXPECT_FALSE(solution.route.empty()); + EXPECT_EQ(solution.route.size(), solution.truck_id.size()); + EXPECT_TRUE(solution.unserviced_nodes.empty()) + << solution.unserviced_nodes.size() << " orders left unserved"; +} + TEST_F(DefaultServerTests, DeleteQueuedJobPreventsRun) { auto client = create_client(); diff --git a/cpp/tests/routing/grpc/CMakeLists.txt b/cpp/tests/routing/grpc/CMakeLists.txt index 6ca249eaa8..0ed85f93b8 100644 --- a/cpp/tests/routing/grpc/CMakeLists.txt +++ b/cpp/tests/routing/grpc/CMakeLists.txt @@ -62,6 +62,43 @@ endif() add_test(NAME GRPC_ROUTING_PROBLEM_MAPPER_TEST COMMAND GRPC_ROUTING_PROBLEM_MAPPER_TEST) set_tests_properties(GRPC_ROUTING_PROBLEM_MAPPER_TEST PROPERTIES LABELS "routing") +# Settings mapper round-trip (no running server). This mapper reads +# routing::solver_settings_t, so it covers the host-only accessors that build into +# cuopt_client, including the presence semantics for an unset time_limit. +add_executable(GRPC_ROUTING_SETTINGS_MAPPER_TEST + ${CMAKE_CURRENT_SOURCE_DIR}/grpc_routing_settings_mapper_test.cpp +) + +target_include_directories(GRPC_ROUTING_SETTINGS_MAPPER_TEST + PRIVATE + "${CUOPT_SOURCE_DIR}/include" + "${CUOPT_SOURCE_DIR}/src/grpc" + "${CUOPT_SOURCE_DIR}/src/grpc/codegen/generated" + "${CMAKE_BINARY_DIR}" +) + +target_link_libraries(GRPC_ROUTING_SETTINGS_MAPPER_TEST + PRIVATE + cuopt + GTest::gtest + GTest::gtest_main + protobuf::libprotobuf +) + +if(NOT DEFINED INSTALL_TARGET OR "${INSTALL_TARGET}" STREQUAL "") + target_link_options(GRPC_ROUTING_SETTINGS_MAPPER_TEST PRIVATE -Wl,--enable-new-dtags) +endif() + +add_test(NAME GRPC_ROUTING_SETTINGS_MAPPER_TEST COMMAND GRPC_ROUTING_SETTINGS_MAPPER_TEST) +set_tests_properties(GRPC_ROUTING_SETTINGS_MAPPER_TEST PROPERTIES LABELS "routing") + +install( + TARGETS GRPC_ROUTING_SETTINGS_MAPPER_TEST + COMPONENT testing + DESTINATION bin/gtests/libcuopt + EXCLUDE_FROM_ALL +) + install( TARGETS GRPC_ROUTING_PROBLEM_MAPPER_TEST COMPONENT testing diff --git a/cpp/tests/routing/grpc/grpc_routing_settings_mapper_test.cpp b/cpp/tests/routing/grpc/grpc_routing_settings_mapper_test.cpp new file mode 100644 index 0000000000..ef53063e75 --- /dev/null +++ b/cpp/tests/routing/grpc/grpc_routing_settings_mapper_test.cpp @@ -0,0 +1,120 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +// Round-trip coverage for the routing settings mapper. This is the mapper that reads +// routing::solver_settings_t, so it exercises the host-only accessors that build into +// cuopt_client rather than the routing engine. + +#include "routing/grpc_routing_settings_mapper.hpp" + +#include +#include + +#include + +#include +#include + +namespace { + +using settings_t = cuopt::routing::solver_settings_t; + +} // namespace + +TEST(RoutingSettingsMapper, RoundTripPreservesEveryField) +{ + settings_t original; + original.set_time_limit(12.5f); + original.set_verbose_mode(true); + original.set_error_logging_mode(true); + original.dump_best_results("/tmp/best_results.json", 7); + + cuopt::remote::RoutingSolverSettings pb; + cuopt::routing::map_routing_settings_to_proto(original, &pb); + + settings_t restored; + cuopt::routing::map_proto_to_routing_settings(pb, restored); + + EXPECT_FLOAT_EQ(restored.get_time_limit(), 12.5f); + EXPECT_TRUE(restored.get_verbose_mode()); + EXPECT_TRUE(restored.get_error_logging_mode()); + + auto [interval, dump, path] = restored.get_dump_best_results(); + EXPECT_EQ(interval, 7); + EXPECT_TRUE(dump); + EXPECT_EQ(path, "/tmp/best_results.json"); +} + +// An unset time_limit must not be serialized: the solver derives its own default +// (num_orders / 5) from absence, so emitting the sentinel would silently override it. +TEST(RoutingSettingsMapper, UnsetTimeLimitIsNotSerialized) +{ + settings_t defaults; + ASSERT_EQ(defaults.get_time_limit(), std::numeric_limits::max()); + + cuopt::remote::RoutingSolverSettings pb; + cuopt::routing::map_routing_settings_to_proto(defaults, &pb); + + EXPECT_FALSE(pb.has_time_limit()); + + settings_t restored; + cuopt::routing::map_proto_to_routing_settings(pb, restored); + EXPECT_EQ(restored.get_time_limit(), std::numeric_limits::max()); +} + +// Zero is a legitimate time limit and is distinct from "unset", so it has to survive +// the round trip rather than being folded into absence. +TEST(RoutingSettingsMapper, ZeroTimeLimitSurvivesRoundTrip) +{ + settings_t original; + original.set_time_limit(0.0f); + + cuopt::remote::RoutingSolverSettings pb; + cuopt::routing::map_routing_settings_to_proto(original, &pb); + EXPECT_TRUE(pb.has_time_limit()); + + settings_t restored; + cuopt::routing::map_proto_to_routing_settings(pb, restored); + EXPECT_FLOAT_EQ(restored.get_time_limit(), 0.0f); +} + +// dump_best_results is only emitted when enabled, so a default-constructed settings +// object must not carry a path across the wire. +TEST(RoutingSettingsMapper, DumpBestResultsOmittedWhenDisabled) +{ + settings_t defaults; + cuopt::remote::RoutingSolverSettings pb; + cuopt::routing::map_routing_settings_to_proto(defaults, &pb); + + EXPECT_TRUE(pb.dump_best_results_path().empty()); + + settings_t restored; + cuopt::routing::map_proto_to_routing_settings(pb, restored); + auto [interval, dump, path] = restored.get_dump_best_results(); + EXPECT_FALSE(dump); + EXPECT_TRUE(path.empty()); +} + +TEST(RoutingSettingsMapper, BooleanFlagsRoundTripIndependently) +{ + for (bool verbose : {false, true}) { + for (bool error_logging : {false, true}) { + settings_t original; + original.set_verbose_mode(verbose); + original.set_error_logging_mode(error_logging); + + cuopt::remote::RoutingSolverSettings pb; + cuopt::routing::map_routing_settings_to_proto(original, &pb); + + settings_t restored; + cuopt::routing::map_proto_to_routing_settings(pb, restored); + + EXPECT_EQ(restored.get_verbose_mode(), verbose); + EXPECT_EQ(restored.get_error_logging_mode(), error_logging); + } + } +} From ca91feaa1076f7feb1a981b57c263f721831fd4c Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 11 Sep 2026 09:21:10 -0500 Subject: [PATCH 31/40] docs: correct the CLI's remote-execution comment The comment said the direct solve_lp/mip_remote calls make libcuopt_grpc.so a real DT_NEEDED of cuopt_cli. That stopped being true once solve_remote.cpp moved into cuopt_client: the binary links cuopt_mathopt and cuopt_client, and carries no DT_NEEDED on libcuopt_grpc.so at all. The reasoning still holds, for a different reason. Calling the remote entry points directly resolves them at link time from cuopt_client rather than relying on ensure_remote_solvers_loaded() to dlopen libcuopt_grpc.so, which is the path solve_lp/solve_mip take through the registry. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- cpp/cuopt_cli.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 7c7b5abc52..6c2f2a4c9e 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -205,8 +205,10 @@ int run_single_file(const std::string& file_path, try { if (cuopt::mathematical_optimization::is_remote_execution_enabled()) { // Remote execution: problem_interface holds a cpu_optimization_problem_t. - // Call solve_lp/mip_remote directly so libcuopt_grpc.so is a real DT_NEEDED - // dependency of this binary rather than an implicit runtime lookup. + // Call solve_lp/mip_remote directly rather than going through the registry that + // solve_lp/solve_mip use. Both entry points live in cuopt_client, which this binary + // already links, so the remote path resolves at link time instead of depending on + // ensure_remote_solvers_loaded() dlopening libcuopt_grpc.so at run time. auto* cpu_prob = dynamic_cast*>( problem_interface.get()); From a0cc29ebb49bc3013cf27dc8f5e5e22210fffde3 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 11 Sep 2026 10:41:45 -0500 Subject: [PATCH 32/40] build(cmake): fold cuopt_grpc into cuopt_mathopt and fix routing's link Two changes to the component boundaries, both found by measuring the built libraries rather than reading the CMake. cuopt_grpc no longer earns a component. After #1884 moved the routing arm into cuopt_client, and solve_remote.cpp moved there too, the library was 18 KB holding one source file: an ELF constructor calling register_remote_solvers. It exported no cuOpt symbols at all -- its 11 dynamic symbols were incidental raft and libstdc++ template instantiations leaked from headers. As a shipped package it would have been a wheel and a conda output containing a constructor and nothing callable. grpc_registration.cpp now builds into cuopt_mathopt, which owns the registry it fills. It still cannot build into cuopt_client: that would leave the client library with an undefined register_remote_solvers and cost it the standalone property. With the constructor in the same library as the registry, the dlopen in ensure_remote_solvers_loaded() became dead code and is gone, along with the dlfcn include; the function stays as a documented no-op because two call sites read better for it. Separately, cuopt_routing referenced 8 symbols from cuopt_client with no DT_NEEDED on it -- the assignment_t and solver_settings_t accessors that #1884 moved. It resolved only because something else had pulled cuopt_client in. A routing-only install, which is the point of #1635, would have failed at load. cuopt_routing now links cuopt_client explicitly. cuopt_grpc_server had been reaching cuopt_routing transitively through cuopt_grpc, so it now links both engines directly. It is the one artifact that solves both VRP and LP/MIP, so the dependency belongs in the open. Every cross-library reference now has a matching DT_NEEDED, and cuopt_client remains a leaf: no CUDA, no rmm, no raft, no undefined cuopt:: symbols. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- ci/build_wheel_cuopt.sh | 1 - conda/recipes/libcuopt/recipe.yaml | 3 - cpp/CMakeLists.txt | 66 +++++++------------ .../remote_solve_registry.hpp | 12 ++-- cpp/src/grpc/client/grpc_registration.cpp | 11 ++-- cpp/src/pdlp/remote_solve_registry.cpp | 16 ++--- python/libcuopt/CMakeLists.txt | 2 +- python/libcuopt/libcuopt/load.py | 1 - 8 files changed, 43 insertions(+), 69 deletions(-) diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index 7814e7c3bf..230e40d7f8 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -43,7 +43,6 @@ EXCLUDE_ARGS=( --exclude "libcuopt.so" --exclude "libcuopt_routing.so" --exclude "libcuopt_mathopt.so" - --exclude "libcuopt_grpc.so" --exclude "libcuopt_client.so" --exclude "libnvJitLink.so*" --exclude "librapids_logger.so" diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 78e8f48067..4a5700bafe 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -111,7 +111,6 @@ outputs: # Only cuopt_mathopt provides the C API. ./ci/check_symbols.sh cpp/build/libcuopt_mathopt.so ./ci/check_symbols.sh cpp/build/libcuopt_routing.so --no-public-api-check - ./ci/check_symbols.sh cpp/build/libcuopt_grpc.so --no-public-api-check dynamic_linking: overlinking_behavior: "error" prefix_detection: @@ -119,7 +118,6 @@ outputs: # See https://github.com/rapidsai/build-planning/issues/160 - lib/libcuopt_routing.so - lib/libcuopt_mathopt.so - - lib/libcuopt_grpc.so - lib/libcuopt_client.so string: cuda${{ cuda_major }}_${{ datetime_string }}_${{ head_rev }} requirements: @@ -174,7 +172,6 @@ outputs: - lib/libcuopt.so - lib/libcuopt_routing.so - lib/libcuopt_mathopt.so - - lib/libcuopt_grpc.so - lib/libcuopt_client.so - bin/cuopt_cli - bin/cuopt_grpc_server diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index e2d3cebca3..8b3cfcce76 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -605,16 +605,13 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/client/solve_remote.cpp ) - # The registration shim spans two components by construction: its ELF constructor - # publishes solve_lp_remote / solve_mip_remote, which live in cuopt_client, into the - # registry in cuopt_mathopt. Putting it in cuopt_client would leave that library with an - # undefined register_remote_solvers and destroy its standalone property. - set(GRPC_COMPONENT_FILES - src/grpc/client/grpc_registration.cpp - ) + # The registration shim builds into cuopt_mathopt, which owns the registry it fills. + # It cannot go into cuopt_client: that would leave libcuopt_client.so with an undefined + # register_remote_solvers and destroy its standalone property. + list(APPEND CUOPT_MATHOPT_SRC_FILES src/grpc/client/grpc_registration.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. + # routing arm without moving code around again. set(GRPC_ROUTING_FILES src/grpc/routing/grpc_routing_problem_mapper.cpp src/grpc/routing/grpc_routing_settings_mapper.cpp @@ -625,14 +622,15 @@ if (NOT SKIP_GRPC_BUILD) # GRPC_INFRA_FILES exists only to carry the -DNDEBUG property below; it is not a target # source list. - set(GRPC_INFRA_FILES ${GRPC_PROTO_GENERATED_FILES} ${GRPC_MATHOPT_FILES} ${GRPC_COMPONENT_FILES}) + set(GRPC_INFRA_FILES ${GRPC_PROTO_GENERATED_FILES} ${GRPC_MATHOPT_FILES} + src/grpc/client/grpc_registration.cpp) if (CUOPT_ENABLE_GRPC_ROUTING) list(APPEND GRPC_INFRA_FILES ${GRPC_ROUTING_FILES}) endif () # The protocol and the LP/MIP arm are CUDA-free, so they build into cuopt_client. The # routing arm calls routing accessors that live in CUDA translation units, so it builds - # into cuopt_grpc alongside the dispatcher instead. + # into cuopt_client alongside the LP/MIP arm. list(APPEND CUOPT_CLIENT_SRC_FILES ${GRPC_PROTO_GENERATED_FILES} ${GRPC_MATHOPT_FILES}) # The routing arm now builds into cuopt_client alongside the LP/MIP arm. Its mappers only # touch routing::solver_settings_t and assignment_t accessors, which are host-only and @@ -654,9 +652,8 @@ if (NOT SKIP_GRPC_BUILD) set_property(SOURCE ${GRPC_PROTO_GENERATED_FILES} DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} APPEND PROPERTY COMPILE_OPTIONS "$<$:-fvisibility=default>") - # cuopt_static (for tests) needs the dispatcher; the components get their sources from - # CUOPT_CLIENT_SRC_FILES and GRPC_COMPONENT_FILES. - list(APPEND CUOPT_SRC_FILES ${GRPC_COMPONENT_FILES}) + # cuopt_static (for tests) needs the registration shim too. + list(APPEND CUOPT_SRC_FILES src/grpc/client/grpc_registration.cpp) endif (NOT SKIP_GRPC_BUILD) # ################################################################################################## @@ -834,6 +831,11 @@ if(NOT SKIP_ROUTING_BUILD) cuopt_configure_component(cuopt_routing) target_link_libraries(cuopt_routing PUBLIC + # The routing engine reads its own assignment_t and solver_settings_t accessors, + # and those are host-only so they build into cuopt_client. Without this the + # symbols resolve only because something else pulled cuopt_client in, which a + # routing-only install would not do. + cuopt_client rmm::rmm rapids_logger::rapids_logger CCCL::CCCL @@ -900,26 +902,6 @@ if (TARGET KaMinPar) endif () add_library(cuopt::mathopt ALIAS cuopt_mathopt) -# cuopt_grpc: gRPC bridge — proto mappers + Cython client (LP and routing over gRPC) -# Depends on cuopt_mathopt and cuopt_routing so it can reference both APIs without forcing -# either component to take a gRPC dependency. -if(NOT SKIP_GRPC_BUILD) - add_library(cuopt_grpc SHARED ${GRPC_COMPONENT_FILES}) - cuopt_configure_component(cuopt_grpc) - target_include_directories(cuopt_grpc PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc" - "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/client" - "${CMAKE_CURRENT_SOURCE_DIR}/src/grpc/codegen/generated" - "${CMAKE_CURRENT_SOURCE_DIR}/src/io" - ) - if(NOT SKIP_ROUTING_BUILD) - target_link_libraries(cuopt_grpc PUBLIC cuopt_mathopt cuopt_routing PRIVATE protobuf::libprotobuf gRPC::grpc++) - else() - target_link_libraries(cuopt_grpc PUBLIC cuopt_mathopt PRIVATE protobuf::libprotobuf gRPC::grpc++) - endif() - add_library(cuopt::grpc ALIAS cuopt_grpc) -endif() - # ################################################################################################## # - cuopt_objs: OBJECT library used by cuopt_static for internal test builds --------------------- add_library(cuopt_objs OBJECT @@ -1085,7 +1067,6 @@ if(NOT SKIP_ROUTING_BUILD) string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_routing.so") endif() if(NOT SKIP_GRPC_BUILD) - string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_grpc.so") endif() set(CUOPT_LINKER_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/libcuopt.so") @@ -1110,9 +1091,6 @@ if(NOT SKIP_ROUTING_BUILD) else() target_link_libraries(cuopt INTERFACE cuopt_mathopt cuopt_client) endif() -if(NOT SKIP_GRPC_BUILD) - target_link_libraries(cuopt INTERFACE cuopt_grpc) -endif() if (WRITE_FATBIN) file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" @@ -1159,11 +1137,8 @@ set(CUOPT_COMPONENT_TARGETS cuopt_mathopt cuopt_client) if(NOT SKIP_ROUTING_BUILD) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) endif() -if(NOT SKIP_GRPC_BUILD) - list(APPEND CUOPT_COMPONENT_TARGETS cuopt_grpc) -endif() -# Export as cuopt::routing / cuopt::mathopt / cuopt::grpc, matching the +# Export as cuopt::routing / cuopt::mathopt, matching the # build-tree aliases. set(CUOPT_COMPONENT_EXPORT_NAMES "") foreach(_component ${CUOPT_COMPONENT_TARGETS}) @@ -1210,7 +1185,7 @@ set(doc_string Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. -Component targets: cuopt::routing, cuopt::mathopt, cuopt::grpc (when gRPC is built) +Component targets: cuopt::routing, cuopt::mathopt Umbrella target: cuopt::cuopt (links all component libs — backward-compatible with -lcuopt) ]=]) @@ -1289,7 +1264,6 @@ if (NOT BUILD_LP_ONLY) target_link_libraries(cuopt_cli PUBLIC cuopt - $<$:cuopt_grpc> OpenMP::OpenMP_CXX ${CUDSS_LIBRARIES} TBB::tbb @@ -1418,9 +1392,13 @@ if (NOT SKIP_GRPC_BUILD) find_library(UUID_LIBRARY uuid REQUIRED) + # Links both engines directly. It used to reach cuopt_routing transitively through + # cuopt_grpc; that component is gone, and the server is the one artifact that solves + # both VRP and LP/MIP, so the dependency is now explicit. target_link_libraries(cuopt_grpc_server PUBLIC - cuopt_grpc + cuopt_mathopt + $<$>:cuopt_routing> OpenMP::OpenMP_CXX PRIVATE protobuf::libprotobuf diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp index b2b0da55ab..ca91be1f0b 100644 --- a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -14,7 +14,7 @@ #include #include -// Forward declarations — full types live in libcuopt_mathopt / libcuopt_grpc +// Forward declarations — full types live in libcuopt_mathopt / libcuopt_client // headers. namespace cuopt::mathematical_optimization { @@ -28,7 +28,7 @@ template class mip_solver_settings_t; /** - * @brief Remote LP solve entry point implemented by libcuopt_grpc.so. + * @brief Remote LP solve entry point implemented in libcuopt_client.so. * * The returned solution is owned by the caller. The callback must not propagate * exceptions across the component boundary. Only the `` @@ -38,7 +38,7 @@ using solve_lp_remote_fn_t = std::unique_ptr const&, pdlp_solver_settings_t const&); /** - * @brief Remote MIP solve entry point implemented by libcuopt_grpc.so. + * @brief Remote MIP solve entry point implemented in libcuopt_client.so. * * Same ownership and exception contract as @ref solve_lp_remote_fn_t. */ @@ -49,7 +49,7 @@ using solve_mip_remote_fn_t = std::unique_ptr g_remote_solvers_ready; /** * @brief Wire up the real remote-solve implementations. * - * Called by libcuopt_grpc.so's ELF constructor. Thread-safe. + * Called by grpc_registration.cpp's ELF constructor. Thread-safe. */ CUOPT_EXPORT void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn); /** - * @brief Load libcuopt_grpc.so on demand so its constructor populates the registry. + * @brief Retained hook; registration now happens in this library's own constructor. * * Idempotent and thread-safe; a failed load leaves the registry slots null so callers * can report the failure themselves. diff --git a/cpp/src/grpc/client/grpc_registration.cpp b/cpp/src/grpc/client/grpc_registration.cpp index a7ec47bda6..6c7eb604f8 100644 --- a/cpp/src/grpc/client/grpc_registration.cpp +++ b/cpp/src/grpc/client/grpc_registration.cpp @@ -1,10 +1,13 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// Registers the gRPC-based remote solve implementations with libcuopt_mathopt.so -// at dynamic-link time (before any user code runs). This breaks the circular -// dependency: libcuopt_mathopt.so holds nullable function pointers rather than a -// hard reference to symbols in libcuopt_grpc.so. +// Registers the gRPC-based remote solve implementations with the registry at +// dynamic-link time, before any user code runs. +// +// This builds into libcuopt_mathopt.so, which owns the registry. It cannot build into +// libcuopt_client.so, where solve_lp_remote and solve_mip_remote live: that would leave +// the client library with an undefined register_remote_solvers and cost it the standalone +// property that lets it ship without CUDA. #include #include diff --git a/cpp/src/pdlp/remote_solve_registry.cpp b/cpp/src/pdlp/remote_solve_registry.cpp index 12f3e13997..eb5079bcdb 100644 --- a/cpp/src/pdlp/remote_solve_registry.cpp +++ b/cpp/src/pdlp/remote_solve_registry.cpp @@ -4,8 +4,6 @@ #include #include -#include - namespace cuopt::mathematical_optimization { std::atomic g_solve_lp_remote_fn{nullptr}; @@ -24,13 +22,13 @@ void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t m void ensure_remote_solvers_loaded() { - if (g_remote_solvers_ready.load(std::memory_order_acquire)) { return; } - // The constructor in libcuopt_grpc.so calls register_remote_solvers(). dlopen is - // itself thread-safe and refcounted, so a concurrent second call is harmless. - if (dlopen("libcuopt_grpc.so", RTLD_NOW | RTLD_GLOBAL) == nullptr) { - const char* err = dlerror(); - CUOPT_LOG_DEBUG("Could not load libcuopt_grpc.so: %s", err != nullptr ? err : "unknown error"); - } + // Nothing to load. grpc_registration.cpp builds into this library, so its ELF + // constructor has already called register_remote_solvers() by the time any code here + // runs. The function stays because callers read better for it, and because a future + // arrangement that moves registration back out would need somewhere to hook. + // + // It previously dlopen'd libcuopt_grpc.so, which held the constructor as a separate + // component. That component was 18 KB exporting nothing, so it was folded in here. } } // namespace cuopt::mathematical_optimization diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 337d3e5e6f..7d904cd02b 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -99,7 +99,7 @@ message(STATUS "libcuopt: Final RPATH = ${rpaths}") # cuopt_client is in the list because it PUBLIC-links rapids_logger, which # build_wheel_libcuopt.sh excludes from vendoring, so the only way to find it is # $ORIGIN/../../rapids_logger/lib64 from this list. -foreach(_target cuopt_routing cuopt_mathopt cuopt_grpc cuopt_client cuopt_cli cuopt_grpc_server) +foreach(_target cuopt_routing cuopt_mathopt cuopt_client cuopt_cli cuopt_grpc_server) if(TARGET ${_target}) set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${rpaths}) endif() diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index 12b7f380b9..3914da18d4 100644 --- a/python/libcuopt/libcuopt/load.py +++ b/python/libcuopt/libcuopt/load.py @@ -61,7 +61,6 @@ def load_library(): components = [ ("libcuopt_routing.so", False), ("libcuopt_mathopt.so", True), - ("libcuopt_grpc.so", False), ] loaded = [] for soname, required in components: From 6923f68c6316257b2c564aa0bb0c97677505fb8e Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 11 Sep 2026 11:29:51 -0500 Subject: [PATCH 33/40] refactor: drop the remote-solve registry and dispatch directly Addresses review: the atomic function pointers, the ELF-constructor registration and the dlopen all existed to break a dependency cycle that no longer exists. When the registry was written, solve_lp_remote and solve_mip_remote lived in the gRPC component, which depended on the math-opt library, so math-opt could not call them. They now live in cuopt_client, which is a leaf, and cuopt_mathopt already links it. The dependency is one-way, so the indirection buys nothing. Both call sites collapse from a load-acquire on an atomic, a null check and an indirect call to a direct call. Deleted: remote_solve_registry.{hpp,cpp}, grpc_registration.cpp, both std::atomic function pointers, the ready flag with its release/acquire pairing, register_remote_solvers and ensure_remote_solvers_loaded. The one thing the registry did handle was a build without gRPC, where the remote entry points do not exist and the null pointer degraded to a runtime error. That is now a compile-time guard on CUOPT_ENABLE_GRPC, which is already defined globally, and the same error text is kept for the no-gRPC case. This also removes solve_lp_remote_fn_t and solve_mip_remote_fn_t, which a reviewer had asked to unify into one type -- there is now no type to unify. Remote execution verified end to end: CpuOnlyWithServerTest.lp_solve and .mip_solve pass against a forked server, exercising the new direct dispatch through the C API. log_callback_remote still fails, unchanged, as #1878. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 9 +-- cpp/cuopt_cli.cpp | 5 +- .../remote_solve_registry.hpp | 81 ------------------- cpp/src/grpc/client/grpc_registration.cpp | 22 ----- cpp/src/mip_heuristics/solve.cu | 13 +-- cpp/src/pdlp/CMakeLists.txt | 1 - cpp/src/pdlp/remote_solve_registry.cpp | 34 -------- cpp/src/pdlp/solve.cu | 14 ++-- 8 files changed, 16 insertions(+), 163 deletions(-) delete mode 100644 cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp delete mode 100644 cpp/src/grpc/client/grpc_registration.cpp delete mode 100644 cpp/src/pdlp/remote_solve_registry.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 8b3cfcce76..09e03f1171 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -605,10 +605,6 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/client/solve_remote.cpp ) - # The registration shim builds into cuopt_mathopt, which owns the registry it fills. - # It cannot go into cuopt_client: that would leave libcuopt_client.so with an undefined - # register_remote_solvers and destroy its standalone property. - list(APPEND CUOPT_MATHOPT_SRC_FILES src/grpc/client/grpc_registration.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 # routing arm without moving code around again. @@ -622,8 +618,7 @@ if (NOT SKIP_GRPC_BUILD) # GRPC_INFRA_FILES exists only to carry the -DNDEBUG property below; it is not a target # source list. - set(GRPC_INFRA_FILES ${GRPC_PROTO_GENERATED_FILES} ${GRPC_MATHOPT_FILES} - src/grpc/client/grpc_registration.cpp) + set(GRPC_INFRA_FILES ${GRPC_PROTO_GENERATED_FILES} ${GRPC_MATHOPT_FILES}) if (CUOPT_ENABLE_GRPC_ROUTING) list(APPEND GRPC_INFRA_FILES ${GRPC_ROUTING_FILES}) endif () @@ -652,8 +647,6 @@ if (NOT SKIP_GRPC_BUILD) set_property(SOURCE ${GRPC_PROTO_GENERATED_FILES} DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} APPEND PROPERTY COMPILE_OPTIONS "$<$:-fvisibility=default>") - # cuopt_static (for tests) needs the registration shim too. - list(APPEND CUOPT_SRC_FILES src/grpc/client/grpc_registration.cpp) endif (NOT SKIP_GRPC_BUILD) # ################################################################################################## diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 6c2f2a4c9e..69a846a7ed 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -205,10 +205,7 @@ int run_single_file(const std::string& file_path, try { if (cuopt::mathematical_optimization::is_remote_execution_enabled()) { // Remote execution: problem_interface holds a cpu_optimization_problem_t. - // Call solve_lp/mip_remote directly rather than going through the registry that - // solve_lp/solve_mip use. Both entry points live in cuopt_client, which this binary - // already links, so the remote path resolves at link time instead of depending on - // ensure_remote_solvers_loaded() dlopening libcuopt_grpc.so at run time. + // solve_lp/mip_remote live in cuopt_client, which this binary already links. auto* cpu_prob = dynamic_cast*>( problem_interface.get()); diff --git a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp deleted file mode 100644 index ca91be1f0b..0000000000 --- a/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp +++ /dev/null @@ -1,81 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#pragma once - -#include - -#include - -#include -#include - -// Forward declarations — full types live in libcuopt_mathopt / libcuopt_client -// headers. -namespace cuopt::mathematical_optimization { - -template -class cpu_optimization_problem_t; - -template -class pdlp_solver_settings_t; - -template -class mip_solver_settings_t; - -/** - * @brief Remote LP solve entry point implemented in libcuopt_client.so. - * - * The returned solution is owned by the caller. The callback must not propagate - * exceptions across the component boundary. Only the `` - * instantiation is supported. - */ -using solve_lp_remote_fn_t = std::unique_ptr> (*)( - cpu_optimization_problem_t const&, pdlp_solver_settings_t const&); - -/** - * @brief Remote MIP solve entry point implemented in libcuopt_client.so. - * - * Same ownership and exception contract as @ref solve_lp_remote_fn_t. - */ -using solve_mip_remote_fn_t = std::unique_ptr> (*)( - cpu_optimization_problem_t const&, mip_solver_settings_t const&); - -/** - * @brief Registry slots defined in libcuopt_mathopt.so - * (remote_solve_registry.cpp). - * - * Null until register_remote_solvers() runs from this library's ELF constructor. Atomic - * because the registering ELF constructor runs on whichever thread triggers the lazy - * dlopen while other threads may be reading the slots. - */ -extern std::atomic g_solve_lp_remote_fn; -extern std::atomic g_solve_mip_remote_fn; - -/** - * @brief Readiness flag, published after both callbacks are stored. - * - * Readers must observe this as true before trusting either slot. - */ -extern std::atomic g_remote_solvers_ready; - -/** - * @brief Wire up the real remote-solve implementations. - * - * Called by grpc_registration.cpp's ELF constructor. Thread-safe. - */ -CUOPT_EXPORT void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn); - -/** - * @brief Retained hook; registration now happens in this library's own constructor. - * - * Idempotent and thread-safe; a failed load leaves the registry slots null so callers - * can report the failure themselves. - */ -void ensure_remote_solvers_loaded(); - -} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/grpc/client/grpc_registration.cpp b/cpp/src/grpc/client/grpc_registration.cpp deleted file mode 100644 index 6c7eb604f8..0000000000 --- a/cpp/src/grpc/client/grpc_registration.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Registers the gRPC-based remote solve implementations with the registry at -// dynamic-link time, before any user code runs. -// -// This builds into libcuopt_mathopt.so, which owns the registry. It cannot build into -// libcuopt_client.so, where solve_lp_remote and solve_mip_remote live: that would leave -// the client library with an undefined register_remote_solvers and cost it the standalone -// property that lets it ship without CUDA. - -#include -#include - -namespace { -__attribute__((constructor)) void register_grpc_remote_solvers() -{ - cuopt::mathematical_optimization::register_remote_solvers( - &cuopt::mathematical_optimization::solve_lp_remote, - &cuopt::mathematical_optimization::solve_mip_remote); -} -} // namespace diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 48a6fec31e..c43e342d63 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include @@ -977,12 +977,13 @@ std::unique_ptr> solve_mip( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - ensure_remote_solvers_loaded(); - auto* remote_fn = g_solve_mip_remote_fn.load(std::memory_order_acquire); - cuopt_expects(remote_fn != nullptr, +#ifdef CUOPT_ENABLE_GRPC + return solve_mip_remote(*cpu_prob, settings); +#else + cuopt_expects(false, error_type_t::RuntimeError, - "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); - return remote_fn(*cpu_prob, settings); + "Remote execution requires cuOpt built with gRPC support"); +#endif } // Local execution - dispatch to appropriate overload based on problem type diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index 1ff2fbf020..ed37f80788 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -5,7 +5,6 @@ # Core LP files always included set(LP_CORE_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/remote_solve_registry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings_accessors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/optimization_problem.cu diff --git a/cpp/src/pdlp/remote_solve_registry.cpp b/cpp/src/pdlp/remote_solve_registry.cpp deleted file mode 100644 index eb5079bcdb..0000000000 --- a/cpp/src/pdlp/remote_solve_registry.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include -#include - -namespace cuopt::mathematical_optimization { - -std::atomic g_solve_lp_remote_fn{nullptr}; -std::atomic g_solve_mip_remote_fn{nullptr}; -std::atomic g_remote_solvers_ready{false}; - -void register_remote_solvers(solve_lp_remote_fn_t lp_fn, solve_mip_remote_fn_t mip_fn) -{ - g_solve_lp_remote_fn.store(lp_fn, std::memory_order_relaxed); - g_solve_mip_remote_fn.store(mip_fn, std::memory_order_relaxed); - // Published last with release ordering: a reader that observes the ready flag is - // guaranteed to observe both callbacks. Using a separate flag rather than one of the - // slots keeps the readiness condition independent of how many callbacks there are. - g_remote_solvers_ready.store(true, std::memory_order_release); -} - -void ensure_remote_solvers_loaded() -{ - // Nothing to load. grpc_registration.cpp builds into this library, so its ELF - // constructor has already called register_remote_solvers() by the time any code here - // runs. The function stays because callers read better for it, and because a future - // arrangement that moves registration back out would need somewhere to hook. - // - // It previously dlopen'd libcuopt_grpc.so, which held the constructor as a separate - // component. That component was 18 KB exporting nothing, so it was folded in here. -} - -} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index cecca94bc5..33d25112b8 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include @@ -2734,12 +2734,12 @@ std::unique_ptr> solve_lp( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); - ensure_remote_solvers_loaded(); - auto* remote_fn = g_solve_lp_remote_fn.load(std::memory_order_acquire); - cuopt_expects(remote_fn != nullptr, - error_type_t::RuntimeError, - "Remote execution requires the gRPC component (libcuopt_grpc.so) to be loaded"); - return remote_fn(*cpu_prob, settings); +#ifdef CUOPT_ENABLE_GRPC + return solve_lp_remote(*cpu_prob, settings); +#else + cuopt_expects( + false, error_type_t::RuntimeError, "Remote execution requires cuOpt built with gRPC support"); +#endif } // Local execution - dispatch to appropriate overload based on problem type From c28657f51a9f40959d944e3e21fce1372a449901 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 11 Sep 2026 13:38:10 -0500 Subject: [PATCH 34/40] fix(cmake): link rmm into cuopt_client so it resolves standalone Every conda-python-tests job failed to import the Cython extensions: ImportError: libcuopt_client.so: undefined symbol: _ZN3rmm10_RMM_26_1016cuda_stream_viewC1EP11CUstream_st cuopt_client_objs already lists rmm::rmm, but an OBJECT library does not propagate its link interface to a target built from $, so libcuopt_client.so carried two undefined rmm symbols and no DT_NEEDED to resolve them. That went unnoticed because libcuopt.so was a real ELF linking rmm, so anything loading the client had rmm present already. Once libcuopt.so became a linker script, a Cython extension loading the client directly had nothing to resolve against. The two symbols are cuda_stream_view's constructor, emitted by the static initializers rmm's header creates in every including translation unit for globals the client never names, and device_buffer's destructor, instantiated through the std::variant in linear_programming_ret_t whose GPU alternative the remote client never constructs. Neither is load-bearing, and #1890 tracks removing the need; this restores correctness now. librmm does not reintroduce CUDA: 1.5 MB, no CUDA in its own NEEDED, and its only CUDA-ish dependency is cuda-version, a 21.6 KB metapackage with no files. A GPU-free client install still works. The check that missed this counted undefined cuopt:: symbols and NEEDED entries, never undefined rmm:: symbols. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 09e03f1171..400684351e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -809,6 +809,16 @@ target_link_libraries(cuopt_client PUBLIC rapids_logger::rapids_logger CCCL::CCCL + # rmm is linked, not merely included. The client sources transitively include + # via pdlp/solver_settings.hpp, and two symbols leak out + # of those headers rather than staying inline: cuda_stream_view's constructor and + # device_buffer's destructor. cuopt_client_objs already lists rmm::rmm, but an + # OBJECT library does not propagate its link interface to a target built from + # $, so without this the shared library carries two undefined + # rmm symbols and no DT_NEEDED to resolve them. That resolved by accident while + # libcuopt.so was a real ELF linking rmm; once it became a linker script, loading + # the client directly -- which is what a Cython extension does -- failed. + rmm::rmm PRIVATE simde::simde OpenMP::OpenMP_CXX From 9d70fc8e340043b93e6b44c5e851c351809f39e4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 11 Sep 2026 15:46:28 -0500 Subject: [PATCH 35/40] fix: share the log-callback thread_local across components (#1878) CpuOnlyWithServerTest.log_callback_remote failed on every conda-cpp-tests config: the remote solve succeeded but no streamed log lines reached the callback. The components are built with hidden visibility, so each got its own copy of the inline thread_local holding the registration. cuOptSetLogCallback lives in cuopt_c.cpp, which builds into cuopt_mathopt, while the remote-solve log forwarding reads the registration from solve_remote.cpp in cuopt_client. Two copies, so the remote path saw no callback and silently delivered nothing. libcuopt_client t_log_callback exported (client uses default visibility) libcuopt_mathopt own hidden copy libcuopt_routing own hidden copy Marking it CUOPT_EXPORT gives every component default visibility for that symbol, so the dynamic linker collapses the copies into one. Registration stays scoped per thread, which is the property that keeps concurrent solves from capturing each other's callback; sharing across libraries does not weaken it. The header comment said the per-library copy was intended. That was true of the per-thread scoping, not of the per-library split, which was an accident of hidden visibility -- so the comment is rewritten to say why the export is load-bearing. This is a split-only bug: it passes on a monolithic build, which is why it survived until libcuopt.so became a linker script. Verified: log_callback_remote now passes, and the full C++ suite is clean when run serially. Under -j2 five binaries fail with 22 RMM out-of-memory hits from GPU contention on one card; all four affected binaries pass individually. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- cpp/src/utilities/logger.hpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cpp/src/utilities/logger.hpp b/cpp/src/utilities/logger.hpp index 2b6c56119e..7c9f952d48 100644 --- a/cpp/src/utilities/logger.hpp +++ b/cpp/src/utilities/logger.hpp @@ -66,12 +66,20 @@ struct thread_log_callback_t { void* user_data = nullptr; }; -// Per-thread, and inline because the logger is header-only since #1778. Note the -// consequence of hidden visibility: each component library gets its own copy, so a -// callback registered through one library is not seen by another. That is intended -- -// registration is scoped to the solve that installed it, and every solver constructs its -// own init_logger_t on entry. -inline thread_local thread_log_callback_t t_log_callback; +// Per-thread, and inline because the logger is header-only since #1778. +// +// CUOPT_EXPORT is load-bearing, not decoration. The components are built with hidden +// visibility, so without it each one gets its own copy of this thread_local and a callback +// registered through one library is invisible to another. That is not a theoretical +// concern: cuOptSetLogCallback lives in cuopt_c.cpp, which builds into cuopt_mathopt, +// while the remote-solve log forwarding reads it from solve_remote.cpp in cuopt_client. +// With separate copies the remote path saw no callback and silently delivered no log +// lines (#1878). Default visibility lets the dynamic linker collapse the copies into one. +// +// Registration remains scoped per thread, which is the property that keeps concurrent +// solves from capturing each other's callback. Sharing across libraries does not weaken +// it -- a thread still sees only what it registered. +inline thread_local CUOPT_EXPORT thread_log_callback_t t_log_callback; } // namespace detail inline log_callback_registration_t current_log_callback() From 4aaf197acb98e231af046c1ece9b0e147abbbfc3 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 11 Sep 2026 19:44:38 -0500 Subject: [PATCH 36/40] test: move SolveVRP away from the worker-killing tests SolveVRP timed out with the job still QUEUED on one conda-cpp-tests config. The cause is placement, not the test body. DefaultServerTests shares one server across the suite, and gtest runs in declaration order, so the test sat immediately after CancelRunningJob: 22 CancelRunningJob cancels a running job, kills a worker 23 SolveVRP stuck in QUEUED, 30s timeout 24 DeleteQueuedJobPreventsRun failed 25 DeleteRunningJobCancelsWorker failed Everything from the worker kill onward failed. That is the mechanism already recorded in #1716: a job submitted after a cancel test kills the worker can sit in QUEUED indefinitely. The two Delete tests are the known #1814 pair and fail there regardless; SolveVRP became a new casualty by being placed among them. It is a test of the routing mappers, not of recovering from a killed worker, so it now sits with the other solve tests after SolveMIPBlocking, with a comment recording why it belongs there. The underlying server issue is untouched and still tracked in #1716 and #1814. Whole suite now passes locally, 58/58. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- .../grpc/grpc_integration_test.cpp | 125 +++++++++--------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp index 2d20dff6e4..ead06c8c3b 100644 --- a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp @@ -803,6 +803,71 @@ TEST_F(DefaultServerTests, SolveMIPBlocking) // -- Explicit Async LP Flow (submit/poll/get/delete) -- +// VRP over gRPC, in the same shape as the LP and MIP cases above: submit a problem, +// poll to completion, fetch the solution and check it. This is the only routing case in +// this suite, and it is the end-to-end exercise of the routing mappers -- the problem and +// settings on the way in, the solution on the way back. +// +// Keep it here with the other solve tests rather than further down. The fixture shares one +// server across the suite, and the cancel and delete tests below kill workers; a job +// submitted after them can sit in QUEUED forever (#1716). This test is about the routing +// mappers, not about recovering from a killed worker, so it should not run in their wake. +TEST_F(DefaultServerTests, SolveVRP) +{ + auto client = create_client(); + ASSERT_NE(client, nullptr); + + // Four locations on a line, depot at 0, one vehicle. Any correct solver serves all + // three orders, so the assertions do not depend on a particular route ordering. + constexpr int32_t kLocations = 4; + cuopt::routing::cpu_routing_problem_t problem; + problem.num_locations = kLocations; + problem.fleet_size = 1; + problem.num_orders = kLocations; + + cuopt::routing::cpu_cost_matrix_t cost; + cost.vehicle_type = 0; + cost.matrix.resize(static_cast(kLocations) * kLocations); + for (int32_t i = 0; i < kLocations; ++i) { + for (int32_t j = 0; j < kLocations; ++j) { + cost.matrix[static_cast(i) * kLocations + j] = static_cast(std::abs(i - j)); + } + } + problem.cost_matrices.push_back(std::move(cost)); + problem.vehicle_start_locations.assign(problem.fleet_size, 0); + problem.vehicle_return_locations.assign(problem.fleet_size, 0); + + cuopt::routing::solver_settings_t settings; + settings.set_time_limit(10.0f); + + auto submit_result = client->submit_vrp(problem, settings); + ASSERT_TRUE(submit_result.success) << submit_result.error_message; + EXPECT_FALSE(submit_result.job_id.empty()); + + job_status_t final_status = job_status_t::QUEUED; + for (int i = 0; i < 60; ++i) { + auto status = client->check_status(submit_result.job_id); + ASSERT_TRUE(status.success) << status.error_message; + final_status = status.status; + if (final_status == job_status_t::COMPLETED || final_status == job_status_t::FAILED) break; + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + ASSERT_EQ(final_status, job_status_t::COMPLETED) + << "Status: " << job_status_to_string(final_status); + + auto result = client->get_vrp_result(submit_result.job_id); + ASSERT_TRUE(result.success) << result.error_message; + + const auto& solution = result.solution; + EXPECT_EQ(solution.status, 0) << solution.status_message; + EXPECT_GT(solution.vehicle_count, 0); + EXPECT_GT(solution.total_objective_value, 0.0); + EXPECT_FALSE(solution.route.empty()); + EXPECT_EQ(solution.route.size(), solution.truck_id.size()); + EXPECT_TRUE(solution.unserviced_nodes.empty()) + << solution.unserviced_nodes.size() << " orders left unserved"; +} + TEST_F(DefaultServerTests, ExplicitAsyncLPFlow) { auto client = create_client(); @@ -1161,66 +1226,6 @@ TEST_F(DefaultServerTests, CancelRunningJob) // -- Delete should cancel queued / running jobs -- -// VRP over gRPC, in the same shape as the LP and MIP cases above: submit a problem, -// poll to completion, fetch the solution and check it. This is the only routing case in -// this suite, and it is the end-to-end exercise of the routing mappers -- the problem and -// settings on the way in, the solution on the way back. -TEST_F(DefaultServerTests, SolveVRP) -{ - auto client = create_client(); - ASSERT_NE(client, nullptr); - - // Four locations on a line, depot at 0, one vehicle. Any correct solver serves all - // three orders, so the assertions do not depend on a particular route ordering. - constexpr int32_t kLocations = 4; - cuopt::routing::cpu_routing_problem_t problem; - problem.num_locations = kLocations; - problem.fleet_size = 1; - problem.num_orders = kLocations; - - cuopt::routing::cpu_cost_matrix_t cost; - cost.vehicle_type = 0; - cost.matrix.resize(static_cast(kLocations) * kLocations); - for (int32_t i = 0; i < kLocations; ++i) { - for (int32_t j = 0; j < kLocations; ++j) { - cost.matrix[static_cast(i) * kLocations + j] = static_cast(std::abs(i - j)); - } - } - problem.cost_matrices.push_back(std::move(cost)); - problem.vehicle_start_locations.assign(problem.fleet_size, 0); - problem.vehicle_return_locations.assign(problem.fleet_size, 0); - - cuopt::routing::solver_settings_t settings; - settings.set_time_limit(10.0f); - - auto submit_result = client->submit_vrp(problem, settings); - ASSERT_TRUE(submit_result.success) << submit_result.error_message; - EXPECT_FALSE(submit_result.job_id.empty()); - - job_status_t final_status = job_status_t::QUEUED; - for (int i = 0; i < 60; ++i) { - auto status = client->check_status(submit_result.job_id); - ASSERT_TRUE(status.success) << status.error_message; - final_status = status.status; - if (final_status == job_status_t::COMPLETED || final_status == job_status_t::FAILED) break; - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - ASSERT_EQ(final_status, job_status_t::COMPLETED) - << "Status: " << job_status_to_string(final_status); - - auto result = client->get_vrp_result(submit_result.job_id); - ASSERT_TRUE(result.success) << result.error_message; - - const auto& solution = result.solution; - EXPECT_EQ(solution.status, 0) << solution.status_message; - EXPECT_GT(solution.vehicle_count, 0); - EXPECT_GT(solution.total_objective_value, 0.0); - EXPECT_FALSE(solution.route.empty()); - EXPECT_EQ(solution.route.size(), solution.truck_id.size()); - EXPECT_TRUE(solution.unserviced_nodes.empty()) - << solution.unserviced_nodes.size() << " orders left unserved"; -} - TEST_F(DefaultServerTests, DeleteQueuedJobPreventsRun) { auto client = create_client(); From 2e9753a2578c0c883f50ff6c2983e9fc31a5d2e2 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 14 Sep 2026 11:11:15 -0500 Subject: [PATCH 37/40] docs: steer consumers to the component targets over -lcuopt Review feedback: -lcuopt should not be the path of least resistance now that the components exist. It pulls both solver engines whether or not a consumer uses them, which is the opposite of what the split is for. Keeps -lcuopt working, so this release stays non-breaking, but documents it as a compatibility shim in both places a consumer would look: - cmake/libcuopt.so.in, which is the generated linker script itself - the installed CMake config's doc string, naming cuopt::mathopt, cuopt::routing and cuopt::client as the targets to prefer Also records the two consequences of the script not being a real shared object -- runtime loaders must dlopen the components, and a .so that is a text script is surprising -- as reasons to move off -lcuopt rather than rely on it. Verified the regenerated script is unchanged in substance and -lcuopt still links. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 10 ++++++++-- cpp/cmake/libcuopt.so.in | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 400684351e..75921c835b 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1188,8 +1188,14 @@ set(doc_string Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. -Component targets: cuopt::routing, cuopt::mathopt -Umbrella target: cuopt::cuopt (links all component libs — backward-compatible with -lcuopt) + +Prefer linking the component you use: + cuopt::mathopt LP / MIP / QP / SOCP + cuopt::routing VRP / TSP / PDP + cuopt::client host-side problem representation and gRPC client (no CUDA) + +cuopt::cuopt links every component and exists for backward compatibility with -lcuopt. +It pulls both solver engines whether or not you use them, so prefer a component target. ]=]) diff --git a/cpp/cmake/libcuopt.so.in b/cpp/cmake/libcuopt.so.in index 0e80a0fba3..8fac81605f 100644 --- a/cpp/cmake/libcuopt.so.in +++ b/cpp/cmake/libcuopt.so.in @@ -1,8 +1,24 @@ /* GNU ld script -- generated from cmake/libcuopt.so.in, do not edit the generated copy. + + COMPATIBILITY SHIM. Prefer linking the components you actually use: + + -lcuopt_mathopt LP / MIP / QP / SOCP + -lcuopt_routing VRP / TSP / PDP + -lcuopt_client host-side problem representation and gRPC client + + or, from CMake, cuopt::mathopt / cuopt::routing rather than cuopt::cuopt. Linking a + single component keeps a consumer's dependencies to what it needs, which is the point + of the split and what the per-solver packages in #1635 build on. + libcuopt.so is not an ELF object. cuOpt ships as component libraries, and the linker does not resolve a consumer's undefined symbols through a dependency's DT_NEEDED (--no-copy-dt-needed-entries, the default since binutils 2.22). Naming this script libcuopt.so keeps -lcuopt resolving to the whole set, exactly as it did before the - library was split. Consumers end up with a direct DT_NEEDED on each component. - Anything that loads cuOpt at runtime must dlopen the components, not this file. */ + library was split, so existing link lines keep working. Consumers end up with a direct + DT_NEEDED on each component. + + Two consequences of it not being a real shared object: anything that loads cuOpt at + runtime must dlopen the components rather than this file, and a file named .so that is + actually a text script is surprising. Both are reasons to move off -lcuopt rather than + to rely on it. */ INPUT(@CUOPT_LINKER_SCRIPT_INPUTS@) From 99f0fad484ccff35e4ae3cb1a38d95d94265ba77 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 15 Sep 2026 09:56:57 -0500 Subject: [PATCH 38/40] fix: address review on the linker script, CLI, accessors and loader Five findings from review, four of them real link-time or load-time bugs. libcuopt.so omitted libcuopt_client.so. I had reasoned the client would arrive through the engines' DT_NEEDED, which is exactly what --no-copy-dt-needed-entries forbids and the whole reason the script exists. Reproduced against a client-owned symbol: undefined reference to cuopt::...::grpc_client_t::cancel_job(...) libcuopt_client.so: error adding symbols: DSO missing from command line The script now names it, and a -lcuopt consumer gets a direct DT_NEEDED on all three components. cuopt_cli called solve_lp_remote / solve_mip_remote unguarded. solve_remote.cpp only builds when gRPC is enabled, while cuopt_cli is gated on BUILD_LP_ONLY, so a SKIP_GRPC_BUILD tree would fail to link the CLI. Now behind CUOPT_ENABLE_GRPC, with an error return in the non-gRPC branch. solver_settings_accessors.cpp was in both LP_CORE_FILES and LP_CLIENT_FILES, so the same accessors were exported from cuopt_mathopt and cuopt_client both. Removed from the core list; mathopt already links the client. load.py handled OSError around the system-installation attempt but not the wheel fallback beneath it, so an optional component that exists and fails to load would raise before the remaining components were tried. Both attempts are now inside one handler. --allow-shlib-undefined is gone from the two C example Makefiles. Verified both examples link without it, which they do now that the client is linked properly and named in the script. Keeping it would hide the next bug of this family. Co-Authored-By: Claude Opus 5 Signed-off-by: Ramakrishna Prabhu --- cpp/CMakeLists.txt | 9 ++- cpp/cuopt_cli.cpp | 8 +++ cpp/src/pdlp/CMakeLists.txt | 1 - .../source/cuopt-c/convex/examples/Makefile | 2 +- .../source/cuopt-c/mip/examples/Makefile | 2 +- python/libcuopt/libcuopt/load.py | 68 ++++++++++--------- 6 files changed, 52 insertions(+), 38 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index cb3e28994b..561116fac7 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1109,12 +1109,15 @@ endif (BUILD_TESTS) # for consumers that linked the pre-split library. cuopt::cuopt is the CMake equivalent. # See cmake/libcuopt.so.in for why a real ELF cannot serve this purpose. -set(CUOPT_LINKER_SCRIPT_INPUTS "libcuopt_mathopt.so") +# cuopt_client is named explicitly rather than left to arrive through the engines' +# DT_NEEDED. That transitive resolution is exactly what --no-copy-dt-needed-entries +# forbids, so without it a -lcuopt consumer calling anything the client owns -- the +# parsers, the host problem representation, the gRPC client -- fails to link with +# "DSO missing from command line". +set(CUOPT_LINKER_SCRIPT_INPUTS "libcuopt_mathopt.so libcuopt_client.so") if(NOT SKIP_ROUTING_BUILD) string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_routing.so") endif() -if(NOT SKIP_GRPC_BUILD) -endif() set(CUOPT_LINKER_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/libcuopt.so") configure_file( diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 69a846a7ed..40176159f4 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -213,6 +213,7 @@ int run_single_file(const std::string& file_path, CUOPT_LOG_ERROR("Remote execution requires the CPU memory backend."); return -1; } +#ifdef CUOPT_ENABLE_GRPC if (is_mip) { auto& mip_settings = settings.get_mip_settings(); auto solution = cuopt::mathematical_optimization::solve_mip_remote(*cpu_prob, mip_settings); @@ -220,6 +221,13 @@ int run_single_file(const std::string& file_path, auto& lp_settings = settings.get_pdlp_settings(); auto solution = cuopt::mathematical_optimization::solve_lp_remote(*cpu_prob, lp_settings); } +#else + // solve_remote.cpp only builds when gRPC is enabled, so these entry points do not + // exist in a SKIP_GRPC_BUILD tree. cuopt_cli is still built there (it is gated on + // BUILD_LP_ONLY, not on gRPC), so without this the link fails. + CUOPT_LOG_ERROR("Remote execution requires cuOpt built with gRPC support."); + return -1; +#endif } else if (is_mip) { auto& mip_settings = settings.get_mip_settings(); auto solution = diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index ed37f80788..12d59382d9 100644 --- a/cpp/src/pdlp/CMakeLists.txt +++ b/cpp/src/pdlp/CMakeLists.txt @@ -6,7 +6,6 @@ # Core LP files always included set(LP_CORE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings.cu - ${CMAKE_CURRENT_SOURCE_DIR}/solver_settings_accessors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/optimization_problem.cu ${CMAKE_CURRENT_SOURCE_DIR}/cpu_optimization_problem_to_gpu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/backend_selection.cpp diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 327f2bff23..f713c22f9d 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -46,7 +46,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index a6c3a82ffc..fb5c0aa8af 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -22,7 +22,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,--allow-shlib-undefined -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index 3914da18d4..86f5798f70 100644 --- a/python/libcuopt/libcuopt/load.py +++ b/python/libcuopt/libcuopt/load.py @@ -79,41 +79,45 @@ def _load_component( load an optional component is silent; failing a required one warns. """ libcuopt_lib = None - if prefer_system_installation: - # Prefer a system library if one is present to - # avoid clobbering symbols that other packages might expect, but if no - # other library is present use the one in the wheel. - try: - libcuopt_lib = _load_system_installation(soname) - except OSError: - libcuopt_lib = _load_wheel_installation(soname) - else: - # Prefer the libraries bundled in this package. If they aren't found - # (which might be the case in builds where the library - # was prebuilt before packaging the wheel), look for a - # system installation. - try: + try: + if prefer_system_installation: + # Prefer a system library if one is present to + # avoid clobbering symbols that other packages might expect, but if no + # other library is present use the one in the wheel. + try: + libcuopt_lib = _load_system_installation(soname) + except OSError: + # The fallback needs to sit inside the outer handler too. An + # optional component that is present but fails to load would + # otherwise raise out of here and stop the remaining components + # from being loaded at all. + libcuopt_lib = _load_wheel_installation(soname) + else: + # Prefer the libraries bundled in this package. If they aren't found + # (which might be the case in builds where the library + # was prebuilt before packaging the wheel), look for a + # system installation. libcuopt_lib = _load_wheel_installation(soname) if libcuopt_lib is None: libcuopt_lib = _load_system_installation(soname) - except OSError as e: - # If none of the searches above succeed, just silently return None - # and rely on other mechanisms (like RPATHs on other DSOs) to - # help the loader find the library. - if not required: - return None - - import warnings - - warnings.warn( - f"Failed to load libcuopt library: {soname}. " - f"Error: {str(e)}. " - "Falling back to relying on system loader. " - "cuOpt functionality may be unavailable. " - f"This might lead to a generic error such as " - f"'{soname} missing' if the library cannot be found.", - RuntimeWarning, - ) + except OSError as e: + # If none of the searches above succeed, just silently return None + # and rely on other mechanisms (like RPATHs on other DSOs) to + # help the loader find the library. + if not required: + return None + + import warnings + + warnings.warn( + f"Failed to load libcuopt library: {soname}. " + f"Error: {str(e)}. " + "Falling back to relying on system loader. " + "cuOpt functionality may be unavailable. " + f"This might lead to a generic error such as " + f"'{soname} missing' if the library cannot be found.", + RuntimeWarning, + ) # The caller almost never needs to do anything with this library, but no # harm in offering the option since this object at least provides a handle # to inspect where libcuopt was loaded from. From 6f07ee6b7ecd9f797c18b8aba108b3648eb087b8 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 16 Sep 2026 09:41:43 -0500 Subject: [PATCH 39/40] test(cmake): link tests against the components they exercise The tests all linked the cuopt umbrella, so none of them proved a component carries its own symbols and dependencies. Each now names what it uses: C_API_TEST cuopt_mathopt routing example tests cuopt_routing GRPC_VRP_TEST_DRIVER cuopt_routing + cuopt_client routing gRPC mapper tests cuopt_client cuopttestutils compiled routing code while linking the umbrella and being linked PUBLIC into LP tests, which handed every consumer both engines and would have masked the change above. It now names cuopt_routing and is guarded on SKIP_ROUTING_BUILD, and C_API_TEST no longer takes it at all. The C doc examples and the skills asset CI dropped the redundant -lcuopt from alongside -lcuopt_mathopt; the asset CI keeps one explicit -lcuopt build so the compatibility shim stays covered. Verified on the built artifacts rather than from the build succeeding: DT_NEEDED now names libcuopt_mathopt.so alone for C_API_TEST, libcuopt_client.so alone for both mapper tests, and routing + client for the routing examples, with no libcuopt.so anywhere. The relinked tests pass. Co-Authored-By: Claude Opus 5 --- ci/test_skills_assets.sh | 18 +++++++++++++++++- cpp/tests/CMakeLists.txt | 11 +++++++++-- cpp/tests/examples/routing/CMakeLists.txt | 4 +++- cpp/tests/linear_programming/CMakeLists.txt | 7 +++++-- cpp/tests/routing/grpc/CMakeLists.txt | 13 ++++++++++--- .../source/cuopt-c/convex/examples/Makefile | 2 +- .../cuopt/source/cuopt-c/mip/examples/Makefile | 2 +- 7 files changed, 46 insertions(+), 11 deletions(-) diff --git a/ci/test_skills_assets.sh b/ci/test_skills_assets.sh index 6358ea405f..daf7bf5804 100755 --- a/ci/test_skills_assets.sh +++ b/ci/test_skills_assets.sh @@ -111,7 +111,7 @@ if [[ -n "${CONDA_PREFIX:-}" ]]; then base=$(basename "$cfile" .c) rel="${cfile#"$REPO_ROOT/"}" log "Building and running C asset: $rel" - if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt -lcuopt_mathopt); then + if ! (cd "$dir" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" -o "$base" "$(basename "$cfile")" -lcuopt_mathopt); then FAILED+=("$rel (build)") log "FAIL: $rel (build)" continue @@ -128,6 +128,22 @@ if [[ -n "${CONDA_PREFIX:-}" ]]; then log "FAIL: $rel" fi done < <(find "${SKILLS_ASSETS}" -path "*/assets/*" -name "*.c" -type f -print0 | sort -z) + + # Legacy compatibility check. The assets above link -lcuopt_mathopt so they prove the + # component stands alone; this one build keeps the -lcuopt shim covered, since it stays + # supported for existing consumers even though it is no longer the recommended path. + legacy_cfile=$(find "${SKILLS_ASSETS}" -path "*/assets/*" -name "*.c" -type f | sort | head -n 1) + if [[ -n "${legacy_cfile}" ]]; then + legacy_rel="${legacy_cfile#"$REPO_ROOT/"}" + log "Building legacy -lcuopt compatibility check: ${legacy_rel}" + if (cd "$(dirname "${legacy_cfile}")" && "${CC}" -I"${INCLUDE_PATH}" -L"${LIB_PATH}" \ + -o "$(basename "${legacy_cfile}" .c).legacy" "$(basename "${legacy_cfile}")" -lcuopt); then + log "PASS: ${legacy_rel} (legacy -lcuopt)" + else + FAILED+=("${legacy_rel} (legacy -lcuopt)") + log "FAIL: ${legacy_rel} (legacy -lcuopt)" + fi + fi else log "CONDA_PREFIX not set, skipping C asset tests" fi diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index ee519002b5..2462149820 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # cmake-format: on -if(BUILD_TESTS) +if(BUILD_TESTS AND NOT SKIP_ROUTING_BUILD) add_library(cuopttestutils STATIC routing/utilities/check_constraints.cu ) @@ -20,9 +20,12 @@ if(BUILD_TESTS) "${dejavu_SOURCE_DIR}" ) + # cuopttestutils compiles routing/utilities/check_constraints.cu, so it belongs to the + # routing engine and names it directly. Linking the cuopt umbrella here would hand every + # consumer both engines and mask whether a test's own link line is complete. target_link_libraries(cuopttestutils PUBLIC - cuopt + cuopt_routing OpenMP::OpenMP_CXX OpenMP::OpenMP_CUDA GTest::gmock @@ -82,6 +85,10 @@ function(ConfigureTest CMAKE_TEST_NAME) set(_cuopt_link_target cuopt) set(_extra_libs cuopttestutils) endif() + # cuopttestutils only exists when the routing engine is built. + if(SKIP_ROUTING_BUILD) + set(_extra_libs "") + endif() add_executable(${CMAKE_TEST_NAME} ${CT_UNPARSED_ARGUMENTS}) target_include_directories(${CMAKE_TEST_NAME} PRIVATE diff --git a/cpp/tests/examples/routing/CMakeLists.txt b/cpp/tests/examples/routing/CMakeLists.txt index deeaadc50d..1e6140e38c 100644 --- a/cpp/tests/examples/routing/CMakeLists.txt +++ b/cpp/tests/examples/routing/CMakeLists.txt @@ -23,9 +23,11 @@ foreach(target "${CMAKE_CURRENT_SOURCE_DIR}/../../routing/utilities" "${CMAKE_CURRENT_SOURCE_DIR}/../../../src/utilities" ) + # Links cuopt_routing directly so these examples verify the routing component is + # self-contained, rather than passing by way of the cuopt umbrella. target_link_libraries(${target} PRIVATE - cuopt + cuopt_routing cuopttestutils OpenMP::OpenMP_CXX ) diff --git a/cpp/tests/linear_programming/CMakeLists.txt b/cpp/tests/linear_programming/CMakeLists.txt index 144b1998d3..e2c347913c 100644 --- a/cpp/tests/linear_programming/CMakeLists.txt +++ b/cpp/tests/linear_programming/CMakeLists.txt @@ -71,10 +71,13 @@ if (NOT SKIP_C_PYTHON_ADAPTERS) "${CMAKE_CURRENT_SOURCE_DIR}/c_api_tests" ) + # Links cuopt_mathopt rather than the cuopt umbrella: this test is the standing check + # that the mathopt component carries the whole C API on its own. cuopttestutils is + # deliberately absent -- it is routing code, and taking it would pull the routing + # engine back in and hide an incomplete mathopt link line. target_link_libraries(C_API_TEST PRIVATE - cuopt - cuopttestutils + cuopt_mathopt c_api_tester GTest::gmock GTest::gmock_main diff --git a/cpp/tests/routing/grpc/CMakeLists.txt b/cpp/tests/routing/grpc/CMakeLists.txt index 0ed85f93b8..158b08ff83 100644 --- a/cpp/tests/routing/grpc/CMakeLists.txt +++ b/cpp/tests/routing/grpc/CMakeLists.txt @@ -19,9 +19,12 @@ target_include_directories(GRPC_VRP_TEST_DRIVER "${CMAKE_BINARY_DIR}" ) +# Names both halves it actually uses: the routing engine for the solve, and cuopt_client +# for the host-side problem representation and gRPC client. target_link_libraries(GRPC_VRP_TEST_DRIVER PRIVATE - cuopt + cuopt_routing + cuopt_client gRPC::grpc++ protobuf::libprotobuf nlohmann_json::nlohmann_json @@ -47,9 +50,11 @@ target_include_directories(GRPC_ROUTING_PROBLEM_MAPPER_TEST "${CMAKE_BINARY_DIR}" ) +# The mappers are host-only and live in cuopt_client, so the test links that component +# alone -- if a mapper ever reaches into the routing engine, this link line fails loudly. target_link_libraries(GRPC_ROUTING_PROBLEM_MAPPER_TEST PRIVATE - cuopt + cuopt_client GTest::gtest GTest::gtest_main protobuf::libprotobuf @@ -77,9 +82,11 @@ target_include_directories(GRPC_ROUTING_SETTINGS_MAPPER_TEST "${CMAKE_BINARY_DIR}" ) +# The mappers are host-only and live in cuopt_client, so the test links that component +# alone -- if a mapper ever reaches into the routing engine, this link line fails loudly. target_link_libraries(GRPC_ROUTING_SETTINGS_MAPPER_TEST PRIVATE - cuopt + cuopt_client GTest::gtest GTest::gtest_main protobuf::libprotobuf diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index f713c22f9d..f89430bf47 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -46,7 +46,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index fb5c0aa8af..737614b955 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -22,7 +22,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) From bf680eb8be0535f0b73c68261095392417f9ad0b Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 16 Sep 2026 11:34:10 -0500 Subject: [PATCH 40/40] fix(cmake): stop cuopt_client exporting logger state, and finish the link lines cuopt_client was built from cuopt_client_objs, which never got the hidden visibility the other components get. Visibility is fixed when the objects are compiled, so setting it on the library built from them has no effect: the client exported cuopt::default_logger() and cuopt::global_log_buffer(), which is the case ci/check_symbols.sh rejects because the per-component loggers collapse into one. Setting it on cuopt_client_objs drops those four symbols and 92 others, with every consumer still linking -- the client's public surface was already annotated. The reason this survived is that the conda recipe only symbol-checked mathopt and routing, so the client was never checked at all. It is now, and the stale component names in check_symbols.sh (base, grpc) are corrected. Also finishes what the test relinking started: cuopt_cli cuopt_mathopt + cuopt_client (never used routing; --as-needed was already dropping it) solve_LP, solve_MIP cuopt_mathopt GRPC_VRP_TEST_DRIVER cuopt_client alone -- it drives a server over gRPC and never calls the routing engine in process Verified in a conda env matching the pinned RAPIDS 26.10 rather than the drifted 26.12 one: clean build, all four symbol checks pass, client exports zero logger symbols, and the full C++ suite is 129/129. Co-Authored-By: Claude Opus 5 --- ci/check_symbols.sh | 2 +- conda/recipes/libcuopt/recipe.yaml | 1 + cpp/CMakeLists.txt | 19 ++++++++++++++++--- cpp/tests/routing/grpc/CMakeLists.txt | 6 +++--- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ci/check_symbols.sh b/ci/check_symbols.sh index a56938ec92..6735a13267 100755 --- a/ci/check_symbols.sh +++ b/ci/check_symbols.sh @@ -10,7 +10,7 @@ LIBRARY="${1}" # The forbidden-symbol checks apply to every cuOpt library. The required public API # check only applies to the component that provides the C API, so components that do -# not (base, routing, grpc) pass --no-public-api-check. +# not (routing, client) pass --no-public-api-check. CHECK_PUBLIC_API=1 if [[ "${2:-}" == "--no-public-api-check" ]]; then CHECK_PUBLIC_API=0 diff --git a/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index 4a5700bafe..02f6968a16 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -111,6 +111,7 @@ outputs: # Only cuopt_mathopt provides the C API. ./ci/check_symbols.sh cpp/build/libcuopt_mathopt.so ./ci/check_symbols.sh cpp/build/libcuopt_routing.so --no-public-api-check + ./ci/check_symbols.sh cpp/build/libcuopt_client.so --no-public-api-check dynamic_linking: overlinking_behavior: "error" prefix_detection: diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 561116fac7..bdcc18b754 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -750,6 +750,13 @@ add_library(cuopt_client_objs OBJECT ${CUOPT_CLIENT_SRC_FILES}) set_target_properties(cuopt_client_objs PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_SCAN_FOR_MODULES OFF + # Visibility is decided when the objects are compiled, so it has to be set here + # rather than on the cuopt_client library built from them. Without it the client + # exported cuopt::default_logger() and cuopt::global_log_buffer(), which collapses + # the per-component loggers into one -- the case ci/check_symbols.sh rejects. + CXX_VISIBILITY_PRESET hidden + CUDA_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON ) add_library(cuopt_client SHARED $) @@ -1317,9 +1324,13 @@ if (NOT BUILD_LP_ONLY) "$" ) + # The CLI solves LP/MIP and parses problems; it never calls the routing engine, so it + # names mathopt and client rather than the cuopt umbrella. Both are named explicitly + # because --no-copy-dt-needed-entries will not let it reach client through mathopt. target_link_libraries(cuopt_cli PUBLIC - cuopt + cuopt_mathopt + cuopt_client OpenMP::OpenMP_CXX ${CUDSS_LIBRARIES} TBB::tbb @@ -1358,9 +1369,10 @@ if (BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY) PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" "$<$:${CUOPT_CUDA_FLAGS}>" ) + # LP/MIP benchmark driver: mathopt only, no routing. target_link_libraries(solve_MIP PUBLIC - cuopt + cuopt_mathopt OpenMP::OpenMP_CXX PRIVATE ) @@ -1388,9 +1400,10 @@ if (BUILD_LP_BENCHMARKS) PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" "$<$:${CUOPT_CUDA_FLAGS}>" ) + # LP/MIP benchmark driver: mathopt only, no routing. target_link_libraries(solve_LP PUBLIC - cuopt + cuopt_mathopt OpenMP::OpenMP_CXX PRIVATE ) diff --git a/cpp/tests/routing/grpc/CMakeLists.txt b/cpp/tests/routing/grpc/CMakeLists.txt index 158b08ff83..a8c56ae79f 100644 --- a/cpp/tests/routing/grpc/CMakeLists.txt +++ b/cpp/tests/routing/grpc/CMakeLists.txt @@ -19,11 +19,11 @@ target_include_directories(GRPC_VRP_TEST_DRIVER "${CMAKE_BINARY_DIR}" ) -# Names both halves it actually uses: the routing engine for the solve, and cuopt_client -# for the host-side problem representation and gRPC client. +# The driver hands the problem to a server over gRPC and never calls the routing engine +# in-process, so cuopt_client -- the host problem representation and gRPC client -- is the +# whole of what it needs. target_link_libraries(GRPC_VRP_TEST_DRIVER PRIVATE - cuopt_routing cuopt_client gRPC::grpc++ protobuf::libprotobuf