diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index f624b27705..ac63fdce65 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_mathopt.so" + --exclude "libcuopt_grpc.so" --exclude "librapids_logger.so" --exclude "librmm.so" ) diff --git a/ci/check_symbols.sh b/ci/check_symbols.sh index 6185092d78..b5dd9c366a 100755 --- a/ci/check_symbols.sh +++ b/ci/check_symbols.sh @@ -8,6 +8,14 @@ echo "checking for symbol visibility issues" 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. +CHECK_PUBLIC_API=1 +if [[ "${2:-}" == "--no-public-api-check" ]]; then + CHECK_PUBLIC_API=0 +fi + echo "" echo "Checking exported symbols in '${LIBRARY}'" symbol_file="$(mktemp)" @@ -89,16 +97,18 @@ required_symbols=( cuOptDestroyProblem ) -exported_funcs="$(readelf --dyn-syms --wide "${LIBRARY}" | awk '$7 != "UND" && $4 == "FUNC" { print $8 }')" - -for sym in "${required_symbols[@]}"; do - echo "Checking that required symbol '${sym}' is exported..." - if ! grep -qxF "${sym}" <<< "${exported_funcs}"; then - echo "ERROR: Required public API symbol '${sym}' is not exported from ${LIBRARY}." - echo "ERROR: Symbol visibility may be over-restricted and hiding the public API." - failed=1 - fi -done +if [[ "${CHECK_PUBLIC_API}" -eq 1 ]]; then + exported_funcs="$(readelf --dyn-syms --wide "${LIBRARY}" | awk '$7 != "UND" && $4 == "FUNC" { print $8 }')" + + for sym in "${required_symbols[@]}"; do + echo "Checking that required symbol '${sym}' is exported..." + if ! grep -qxF "${sym}" <<< "${exported_funcs}"; then + echo "ERROR: Required public API symbol '${sym}' is not exported from ${LIBRARY}." + echo "ERROR: Symbol visibility may be over-restricted and hiding the public API." + failed=1 + fi + done +fi if [[ "${failed}" -ne 0 ]]; then exit 1 diff --git a/ci/test_skills_assets.sh b/ci/test_skills_assets.sh index c75645cb93..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); 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 b6172827d1..1017bf9108 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -108,13 +108,21 @@ outputs: script: content: | cmake --install cpp/build - ./ci/check_symbols.sh cpp/build/libcuopt.so + # 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: overlinking_behavior: "error" 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_mathopt.so + - lib/libcuopt_grpc.so string: cuda${{ cuda_major }}_${{ datetime_string }}_${{ head_rev }} requirements: build: @@ -167,6 +175,10 @@ outputs: - package_contents: files: - lib/libcuopt.so + - lib/libcuopt_base.so + - lib/libcuopt_routing.so + - lib/libcuopt_mathopt.so + - lib/libcuopt_grpc.so - bin/cuopt_cli - bin/cuopt_grpc_server about: diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 4ce11b830b..8d4b729b71 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -492,6 +492,9 @@ 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) add_subdirectory(src) @@ -502,7 +505,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_MATHOPT_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) @@ -515,16 +520,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_MATHOPT_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/routing APIs; compiled into cuopt_grpc set(GRPC_INFRA_FILES ${DATA_PROTO_SRCS} ${PROTO_SRCS} @@ -538,8 +544,8 @@ 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 ) - 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. @@ -550,12 +556,180 @@ if (NOT SKIP_GRPC_BUILD) APPEND PROPERTY COMPILE_OPTIONS "-DNDEBUG") set_property(SOURCE ${PROTO_SRCS} ${GRPC_PROTO_SRCS} ${GRPC_SERVICE_SRCS} ${DATA_PROTO_SRCS} DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} APPEND PROPERTY COMPILE_OPTIONS "$<$:-fvisibility=default>") + + # 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) +# ################################################################################################## +# - cuopt component libraries (SHARED) ----------------------------------------------------------- + +# Helper: apply compile options, RPATH, and common include paths to all cuOpt component libs +function(cuopt_configure_component target) + set_target_properties(${target} PROPERTIES + CXX_SCAN_FOR_MODULES OFF + BUILD_RPATH_USE_ORIGIN TRUE + INSTALL_RPATH "\$ORIGIN" + # Same hidden-by-default visibility that #1625 applies to cuopt_objs. The + # components are the shipped artifacts, so they need it too. + CXX_VISIBILITY_PRESET hidden + CUDA_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + ) + 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() + +# 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 + 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( + ${CMAKE_CURRENT_SOURCE_DIR}/src/utilities/build_info.hpp.in + ${CMAKE_CURRENT_BINARY_DIR}/include/utilities/build_info.hpp + @ONLY +) + +list(JOIN CMAKE_CUDA_ARCHITECTURES "," JOINED_CUDA_ARCHITECTURES) + +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 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 + PRIVATE simde::simde OpenMP::OpenMP_CXX OpenMP::OpenMP_CUDA + ) + add_library(cuopt::routing ALIAS cuopt_routing) +endif() + +# 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}" + $<$:${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_mathopt PRIVATE + "${papilo_SOURCE_DIR}/src" + "${papilo_BINARY_DIR}" +) +target_include_directories(cuopt_mathopt SYSTEM PRIVATE + "${pslp_SOURCE_DIR}/include" + "${dejavu_SOURCE_DIR}" +) +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_mathopt + PUBLIC + cuopt_base + ${CUDSS_LIB_FILE} + PRIVATE + ${CUOPT_PRIVATE_CUDA_LIBS} + OpenMP::OpenMP_CUDA + simde::simde + nccl_external + ${CMAKE_DL_LIBS} +) +target_link_libraries(cuopt_mathopt PRIVATE $) +add_dependencies(cuopt_mathopt PSLP) +target_include_directories(cuopt_mathopt SYSTEM 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_mathopt 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_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" + ) + 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 ${CUOPT_SRC_FILES} ) - set_target_properties(cuopt_objs PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden @@ -563,12 +737,10 @@ set_target_properties(cuopt_objs VISIBILITY_INLINES_HIDDEN ON CXX_SCAN_FOR_MODULES OFF ) - target_compile_definitions(cuopt_objs - PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" - PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API + PUBLIC "CUOPT_LOG_ACTIVE_LEVEL=RAPIDS_LOGGER_LOG_LEVEL_${LIBCUOPT_LOGGING_LEVEL}" + PUBLIC CUSPARSE_ENABLE_EXPERIMENTAL_API ) - target_compile_options(cuopt_objs PRIVATE "$<$:${CUOPT_CXX_FLAGS}>" "$<$:${CUOPT_CUDA_FLAGS}>" @@ -585,8 +757,8 @@ target_include_directories(cuopt_objs PRIVATE ) target_include_directories(cuopt_objs SYSTEM PRIVATE - "${pslp_SOURCE_DIR}/include" - "${dejavu_SOURCE_DIR}" + "${pslp_SOURCE_DIR}/include" + "${dejavu_SOURCE_DIR}" ) target_include_directories(cuopt_objs @@ -612,13 +784,6 @@ target_include_directories(cuopt_objs target_link_libraries(cuopt_objs PRIVATE $) add_dependencies(cuopt_objs PSLP) -# Link KaMinPar by file to avoid export dependency tracking (mirrors PSLP above). -# KaMinPar is a from-source static library fully embedded into libcuopt.so; it is never -# installed (INSTALL_KAMINPAR OFF) and consumers of cuopt::cuopt never use it, so it must -# not leak into cuopt's exported link interface (otherwise rapids_export fails with -# "target KaMinPar is not in any export set"). libKaMinPar.a is self-contained (the -# kaminpar-common OBJECT lib is archived into it); we only need its public headers -# (, which pulls in stdlib + TBB) at compile time. target_include_directories(cuopt_objs SYSTEM PRIVATE $) # partitioner.cpp includes . Because KaMinPar is linked by file, cuopt @@ -637,45 +802,14 @@ if (TARGET KaMinPar) add_dependencies(cuopt_objs KaMinPar) endif () -# ################################################################################################## -# - 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_objs 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 +target_compile_definitions(cuopt_objs + PRIVATE CUDSS_MT_LIB_FILE_NAME="${CUDSS_MT_LIB_FILE_NAME}" + PUBLIC + CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" + CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}" ) - -# Add the generated include directory target_include_directories(cuopt_objs PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include) -list(JOIN CMAKE_CUDA_ARCHITECTURES "," JOINED_CUDA_ARCHITECTURES) -target_compile_definitions(cuopt_objs PUBLIC - CUOPT_CUDA_ARCHITECTURES="${JOINED_CUDA_ARCHITECTURES}" - CUOPT_CPU_ARCHITECTURE="${CMAKE_SYSTEM_PROCESSOR}") - target_link_libraries(cuopt_objs PUBLIC CUDA::cublas @@ -741,55 +875,46 @@ if (BUILD_TESTS) add_subdirectory(tests) endif (BUILD_TESTS) -add_library(cuopt SHARED $) -add_library(cuopt::cuopt ALIAS cuopt) -set_target_properties(cuopt - PROPERTIES BUILD_RPATH "\$ORIGIN" - INSTALL_RPATH "\$ORIGIN" - INTERFACE_POSITION_INDEPENDENT_CODE ON - CXX_SCAN_FOR_MODULES OFF - LINKER_LANGUAGE CUDA +# ################################################################################################## +# - 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) + 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() + +set(CUOPT_LINKER_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/libcuopt.so") +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/libcuopt.so.in" + "${CUOPT_LINKER_SCRIPT}" + @ONLY ) -# cuopt needs PUBLIC interface for consumers + +add_library(cuopt INTERFACE) +add_library(cuopt::cuopt ALIAS cuopt) + target_include_directories(cuopt - PUBLIC + INTERFACE "$" "$" - INTERFACE "$" ) -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} - nccl_external - simde::simde - OpenMP::OpenMP_CXX - OpenMP::OpenMP_CUDA - $<$:protobuf::libprotobuf> - $<$:gRPC::grpc++> -) -target_link_libraries(cuopt PRIVATE $) -add_dependencies(cuopt PSLP) -target_link_libraries(cuopt PRIVATE $) -if (TARGET KaMinPar) - add_dependencies(cuopt KaMinPar) -endif () -# Propagate compile definitions that consumers need when including cuopt headers. -# These were on cuopt directly before the cuopt_objs refactor; $ -# does not carry INTERFACE properties, so we restore them explicitly. -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 INTERFACE cuopt_base cuopt_routing cuopt_mathopt) +else() + target_link_libraries(cuopt INTERFACE cuopt_base cuopt_mathopt) +endif() +if(NOT SKIP_GRPC_BUILD) + target_link_libraries(cuopt INTERFACE cuopt_grpc) +endif() if (WRITE_FATBIN) file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" @@ -800,7 +925,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_mathopt) + 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 () # ################################################################################################## @@ -824,15 +957,38 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () -# adds the .so files to the runtime deb package -install(TARGETS cuopt +set(CUOPT_COMPONENT_TARGETS cuopt_base cuopt_mathopt) +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::base / cuopt::routing / cuopt::mathopt / 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 EXPORT cuopt-exports ) -# adds the .so files to the development deb package +# 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 + EXPORT cuopt-exports +) + +# 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 ) @@ -855,12 +1011,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::mathopt, cuopt::grpc (when gRPC is built) +Umbrella target: cuopt::cuopt (links all component libs — backward-compatible with -lcuopt) ]=]) rapids_export(INSTALL cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string ) @@ -869,7 +1027,7 @@ rapids_export(INSTALL cuopt # - build export ------------------------------------------------------------------------------- rapids_export(BUILD cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string ) @@ -932,6 +1090,7 @@ if (NOT BUILD_LP_ONLY) target_link_libraries(cuopt_cli PUBLIC cuopt + $<$:cuopt_grpc> OpenMP::OpenMP_CXX ${CUDSS_LIBRARIES} TBB::tbb @@ -1056,7 +1215,7 @@ if (NOT SKIP_GRPC_BUILD) target_link_libraries(cuopt_grpc_server PUBLIC - cuopt + cuopt_grpc OpenMP::OpenMP_CXX PRIVATE protobuf::libprotobuf 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/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index e070425eab..3fa3384b43 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -195,7 +196,25 @@ int run_single_file(const std::string& file_path, } try { - if (is_mip) { + 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. + auto* cpu_prob = + 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); + } 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); 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..b2b0da55ab --- /dev/null +++ b/cpp/include/cuopt/mathematical_optimization/remote_solve_registry.hpp @@ -0,0 +1,81 @@ +/* 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_grpc +// 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 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&); + +/** + * @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 + * 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 libcuopt_grpc.so'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. + * + * 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/include/cuopt/mathematical_optimization/solve_remote.hpp b/cpp/include/cuopt/mathematical_optimization/solve_remote.hpp index bdb19f9c9d..3c162875f7 100644 --- a/cpp/include/cuopt/mathematical_optimization/solve_remote.hpp +++ b/cpp/include/cuopt/mathematical_optimization/solve_remote.hpp @@ -21,10 +21,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/include/cuopt/utilities/timestamp_utils.hpp b/cpp/include/cuopt/utilities/timestamp_utils.hpp index 9c24f25339..f2876c1e95 100644 --- a/cpp/include/cuopt/utilities/timestamp_utils.hpp +++ b/cpp/include/cuopt/utilities/timestamp_utils.hpp @@ -1,12 +1,14 @@ /* clang-format off */ /* - * 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 */ /* clang-format on */ #pragma once +#include + #include namespace cuopt { @@ -34,7 +36,7 @@ double getCurrentTimestamp(); * * @param label The label to print with the timestamp */ -void printTimestamp(const std::string& label); +CUOPT_EXPORT void printTimestamp(const std::string& label); } // namespace utilities } // namespace cuopt diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt index e8737cf6da..264d315b19 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} +) + +set(CUOPT_ROUTING_SRC_FILES + ${ROUTING_SRC_FILES} +) + +set(CUOPT_MATHOPT_SRC_FILES + ${LINEAR_ALGEBRA_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_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 +) 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/grpc/client/grpc_registration.cpp b/cpp/src/grpc/client/grpc_registration.cpp new file mode 100644 index 0000000000..a7ec47bda6 --- /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_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. + +#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/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/io/mps_parser_internal.hpp b/cpp/src/io/mps_parser_internal.hpp index 2bc5443f11..bf0267a4df 100644 --- a/cpp/src/io/mps_parser_internal.hpp +++ b/cpp/src/io/mps_parser_internal.hpp @@ -7,6 +7,8 @@ #pragma once +#include + #include #include @@ -63,9 +65,9 @@ void check_symmetric_offdiagonal_pairs(const std::vector& rows, * - Sorts output by (row, col). */ template -void canonicalize_coo_matrix(std::vector& rows, - std::vector& cols, - std::vector& vals); +CUOPT_EXPORT void canonicalize_coo_matrix(std::vector& rows, + std::vector& cols, + std::vector& vals); /** * @brief Different possible types of 'ROWS' 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/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index f55aca6878..058c0d5fd0 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 @@ -926,20 +926,18 @@ 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); + 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 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 2f90f94872..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 @@ -49,4 +50,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/pdlp/remote_solve_registry.cpp b/cpp/src/pdlp/remote_solve_registry.cpp new file mode 100644 index 0000000000..12f3e13997 --- /dev/null +++ b/cpp/src/pdlp/remote_solve_registry.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include +#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() +{ + 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"); + } +} + +} // namespace cuopt::mathematical_optimization diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 9e54bb1a11..a6090a8145 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -7,7 +7,8 @@ #include #include -#include +#include + #include #include #include @@ -2706,7 +2707,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, @@ -2716,13 +2716,13 @@ 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); + 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); } -#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/routing/CMakeLists.txt b/cpp/src/routing/CMakeLists.txt index 452c4806da..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 @@ -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) diff --git a/cpp/src/utilities/seed_generator.cuh b/cpp/src/utilities/seed_generator.cuh index dd5e79d847..b57752336d 100644 --- a/cpp/src/utilities/seed_generator.cuh +++ b/cpp/src/utilities/seed_generator.cuh @@ -1,18 +1,20 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ #pragma once + +#include #include #include namespace cuopt { // TODO: should be thread local? -class seed_generator { +class CUOPT_EXPORT seed_generator { static int64_t seed_; public: diff --git a/cpp/src/utilities/version_info.hpp b/cpp/src/utilities/version_info.hpp index cfbd4a4d7e..b503b9ed32 100644 --- a/cpp/src/utilities/version_info.hpp +++ b/cpp/src/utilities/version_info.hpp @@ -6,7 +6,9 @@ /* clang-format on */ #pragma once +#include + namespace cuopt { // Prints devices [0, num_devices). Defaults to the first visible device. -void print_version_info(int num_devices = 1); +CUOPT_EXPORT void print_version_info(int num_devices = 1); } // namespace cuopt diff --git a/cpp/src/utilities/work_unit_scheduler.hpp b/cpp/src/utilities/work_unit_scheduler.hpp index bc84f0513f..d2af901551 100644 --- a/cpp/src/utilities/work_unit_scheduler.hpp +++ b/cpp/src/utilities/work_unit_scheduler.hpp @@ -4,6 +4,8 @@ */ #pragma once +#include + #include #include @@ -13,7 +15,7 @@ namespace cuopt { struct work_limit_context_t; -class work_unit_scheduler_t { +class CUOPT_EXPORT work_unit_scheduler_t { public: explicit work_unit_scheduler_t(double sync_interval = 5.0); diff --git a/docs/cuopt/source/conf.py b/docs/cuopt/source/conf.py index d078382e77..0d979c51b0 100644 --- a/docs/cuopt/source/conf.py +++ b/docs/cuopt/source/conf.py @@ -369,7 +369,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/.*", @@ -381,6 +381,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 ac369773e7..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 -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 bc287f0d49..a6c3a82ffc 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"); @@ -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_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 4d24169645..b3bdb5b1ec 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 ) @@ -95,6 +95,9 @@ else() endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") -set_property(TARGET cuopt 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) +# 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) + if(TARGET ${_target}) + set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${rpaths}) + endif() +endforeach() diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index 77dd6ed161..3fa7fc2f96 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 @@ -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_mathopt.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.