diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index 2cc81e3f27..230e40d7f8 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -41,6 +41,8 @@ EXCLUDE_ARGS=( --exclude "libcusolver.so.*" --exclude "libcusparse.so.*" --exclude "libcuopt.so" + --exclude "libcuopt_routing.so" + --exclude "libcuopt_mathopt.so" --exclude "libcuopt_client.so" --exclude "libnvJitLink.so*" --exclude "librapids_logger.so" diff --git a/ci/check_symbols.sh b/ci/check_symbols.sh index cd5085491a..6735a13267 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 (routing, client) 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 # Each component library keeps its own logger only while this state stays hidden; exporting it # silently merges them back into one. Nothing else catches that. diff --git a/ci/test_skills_assets.sh b/ci/test_skills_assets.sh index c75645cb93..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); 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/conda/recipes/libcuopt/recipe.yaml b/conda/recipes/libcuopt/recipe.yaml index fcd83f749c..02f6968a16 100644 --- a/conda/recipes/libcuopt/recipe.yaml +++ b/conda/recipes/libcuopt/recipe.yaml @@ -107,13 +107,18 @@ 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_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: ignore: # See https://github.com/rapidsai/build-planning/issues/160 - - lib/libcuopt.so + - lib/libcuopt_routing.so + - lib/libcuopt_mathopt.so - lib/libcuopt_client.so string: cuda${{ cuda_major }}_${{ datetime_string }}_${{ head_rev }} requirements: @@ -166,6 +171,8 @@ outputs: - package_contents: files: - lib/libcuopt.so + - lib/libcuopt_routing.so + - lib/libcuopt_mathopt.so - lib/libcuopt_client.so - bin/cuopt_cli - bin/cuopt_grpc_server diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 3d25c17f1d..bdcc18b754 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -567,12 +567,16 @@ if (BUILD_TESTS) endif () set(CUOPT_SRC_FILES) +set(CUOPT_ROUTING_SRC_FILES) +set(CUOPT_MATHOPT_SRC_FILES) set(CUOPT_CLIENT_SRC_FILES) set(MPS_FAST_SRC_FILES) add_subdirectory(src) if (HOST_LINEINFO) - set_source_files_properties(${CUOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") + set_source_files_properties( + ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTIES COMPILE_OPTIONS "-g1") endif () # Needed for the fast MPS parser, available on all x86-64-v3 compliant x86 CPUs (essentially since Haswell ~2013) @@ -585,11 +589,13 @@ 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 and parser source files (not gRPC infrastructure). +# Apply -UNDEBUG to solver and parser source files (not gRPC infrastructure). # Must happen before gRPC files are appended to CUOPT_CLIENT_SRC_FILES. # Uses APPEND to preserve any existing per-file options (e.g. -g1 from HOST_LINEINFO). if (DEFINE_ASSERT) - set_property(SOURCE ${CUOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + set_property( + SOURCE ${CUOPT_ROUTING_SRC_FILES} ${CUOPT_MATHOPT_SRC_FILES} ${CUOPT_CLIENT_SRC_FILES} + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} APPEND PROPERTY COMPILE_OPTIONS "-UNDEBUG") endif () @@ -616,10 +622,12 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/client/grpc_client.cpp src/grpc/client/grpc_client_env.cpp src/grpc/client/cython_grpc_client.cpp + src/grpc/client/solve_remote.cpp ) + # Routing (VRP) arm: everything that depends on the routing engine. Kept as # its own list so a routing-only gRPC client can be split out of the - # cuopt_grpc component without moving code around again. + # 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 @@ -628,14 +636,16 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/routing/cython_grpc_client_vrp.cpp ) + # 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}) if (CUOPT_ENABLE_GRPC_ROUTING) list(APPEND GRPC_INFRA_FILES ${GRPC_ROUTING_FILES}) endif () - # The routing arm stays in libcuopt: its mappers call routing accessors that live in CUDA - # 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. + # 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_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 @@ -644,10 +654,6 @@ if (NOT SKIP_GRPC_BUILD) list(APPEND CUOPT_CLIENT_SRC_FILES ${GRPC_ROUTING_FILES}) endif () - # solve_remote.cpp is the local-vs-remote dispatcher: it calls into the GPU - # solver, so it stays in cuopt_objs rather than moving down to cuopt_client. - list(APPEND CUOPT_SRC_FILES src/grpc/client/solve_remote.cpp) - list(APPEND GRPC_INFRA_FILES src/grpc/client/solve_remote.cpp) # Always keep NDEBUG defined for gRPC infrastructure files so that abseil # headers inline Mutex::Dtor() instead of emitting an external call. @@ -660,9 +666,72 @@ if (NOT SKIP_GRPC_BUILD) # links them from the library rather than compiling the protos itself. set_property(SOURCE ${GRPC_PROTO_GENERATED_FILES} DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} APPEND PROPERTY COMPILE_OPTIONS "$<$:-fvisibility=default>") + 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 + TBB::tbb + OpenMP::OpenMP_CXX) + +get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) + # - cuopt_client - CPU-only support library ---------------------------------------------------------- # # Host-side problem representation (parsers, data_model_view, mps_data_model, writers), the @@ -681,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 $) @@ -760,6 +836,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 @@ -768,10 +854,94 @@ target_link_libraries(cuopt_client $<$:gRPC::grpc++> ) + +# 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 + # 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 + raft::raft + CUDA::cublas + CUDA::cusparse + 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}" + # utilities/version_info.cpp includes highway, and utilities builds into this + # component since cuopt_base was removed. + "${highway_SOURCE_DIR}" +) +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 + # The parsers and the host-side problem representation live in cuopt_client. + cuopt_client + rmm::rmm + rapids_logger::rapids_logger + CCCL::CCCL + raft::raft + CUDA::cublas + CUDA::cusparse + ${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_link_libraries(cuopt_mathopt PRIVATE $) +add_dependencies(cuopt_mathopt hwy) +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_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 @@ -779,12 +949,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}>" @@ -801,9 +969,9 @@ target_include_directories(cuopt_objs PRIVATE ) target_include_directories(cuopt_objs SYSTEM PRIVATE - "${pslp_SOURCE_DIR}/include" - "${dejavu_SOURCE_DIR}" - "${highway_SOURCE_DIR}" + "${pslp_SOURCE_DIR}/include" + "${dejavu_SOURCE_DIR}" + "${highway_SOURCE_DIR}" ) target_include_directories(cuopt_objs @@ -857,43 +1025,14 @@ if (TARGET KaMinPar) add_dependencies(cuopt_objs KaMinPar) endif () -# ################################################################################################## -# - link libraries -------------------------------------------------------------------------------- - -set(CUOPT_PRIVATE_CUDA_LIBS - 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 cuopt::cuopt_client @@ -971,58 +1110,44 @@ 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. + +# 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() + +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 - cuopt::cuopt_client - 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 $) -add_dependencies(cuopt hwy) -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_routing cuopt_mathopt cuopt_client) +else() + target_link_libraries(cuopt INTERFACE cuopt_mathopt cuopt_client) +endif() if (WRITE_FATBIN) file(WRITE "${CUOPT_BINARY_DIR}/fatbin.ld" @@ -1033,7 +1158,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_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 () # ################################################################################################## @@ -1057,15 +1190,36 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () +set(CUOPT_COMPONENT_TARGETS cuopt_mathopt cuopt_client) +if(NOT SKIP_ROUTING_BUILD) + list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) +endif() + +# Export as cuopt::routing / cuopt::mathopt, 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() + # adds the .so files to the runtime deb package -install(TARGETS cuopt cuopt_client +install(TARGETS ${CUOPT_COMPONENT_TARGETS} DESTINATION ${_LIB_DEST} COMPONENT runtime EXPORT cuopt-exports ) -# adds the .so files to the development deb package -install(TARGETS cuopt cuopt_client +# 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 ) @@ -1089,11 +1243,19 @@ Provide targets for cuOpt. cuOpt library is a collection of GPU accelerated combinatorial optimization algorithms. +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. + ]=]) rapids_export(INSTALL cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt cuopt_client + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string ) @@ -1102,7 +1264,7 @@ rapids_export(INSTALL cuopt # - build export ------------------------------------------------------------------------------- rapids_export(BUILD cuopt EXPORT_SET cuopt-exports - GLOBAL_TARGETS cuopt cuopt_client + GLOBAL_TARGETS cuopt ${CUOPT_COMPONENT_EXPORT_NAMES} NAMESPACE cuopt:: DOCUMENTATION doc_string ) @@ -1162,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 @@ -1203,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 ) @@ -1233,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 ) @@ -1292,9 +1460,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 + cuopt_mathopt + $<$>:cuopt_routing> 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..8fac81605f --- /dev/null +++ b/cpp/cmake/libcuopt.so.in @@ -0,0 +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, 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@) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 96bdfc921a..40176159f4 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -202,7 +203,32 @@ 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. + // solve_lp/mip_remote live in cuopt_client, which this binary already links. + auto* cpu_prob = + dynamic_cast*>( + problem_interface.get()); + if (cpu_prob == nullptr) { + 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); + } else { + 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 = cuopt::mathematical_optimization::solve_mip(problem_interface.get(), mip_settings); 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 75b04b5c42..6452817dc8 100644 --- a/cpp/src/CMakeLists.txt +++ b/cpp/src/CMakeLists.txt @@ -27,6 +27,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 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} +) + +# PARSERS_SRC_FILES is deliberately absent here: the parsers are CUDA-free and build into +# cuopt_client, which cuopt_mathopt links. Listing them in both would compile them twice. +set(CUOPT_MATHOPT_SRC_FILES + ${UTIL_SRC_FILES} + ${LINEAR_ALGEBRA_SRC_FILES} + ${LP_SRC_FILES} + ${MATH_OPT_SRC_FILES} + ${MIP_SRC_FILES} + ${DUAL_SIMPLEX_SRC_FILES} + ${BARRIER_SRC_FILES} + ${BRANCH_AND_BOUND_SRC_FILES} + ${CUTS_SRC_FILES} +) + +set(CUOPT_ROUTING_SRC_FILES ${CUOPT_ROUTING_SRC_FILES} PARENT_SCOPE) +set(CUOPT_MATHOPT_SRC_FILES ${CUOPT_MATHOPT_SRC_FILES} PARENT_SCOPE) set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${UTIL_CLIENT_SRC_FILES} PARENT_SCOPE) +set(CUOPT_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 2d4412d00a..9958466580 100644 --- a/cpp/src/cuts/CMakeLists.txt +++ b/cpp/src/cuts/CMakeLists.txt @@ -9,5 +9,6 @@ set(CUTS_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/zero_half_mod2.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 f7851aa4fd..72398ebca2 100644 --- a/cpp/src/io/CMakeLists.txt +++ b/cpp/src/io/CMakeLists.txt @@ -27,3 +27,4 @@ set(PARSERS_SRC_FILES # are host integer helpers from header-only libcu++), so they build into cuopt_client. set(CUOPT_CLIENT_SRC_FILES ${CUOPT_CLIENT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE) set(MPS_FAST_SRC_FILES ${MPS_FAST_SRC_FILES} PARENT_SCOPE) +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 0baf3d8a4e..dc3bb85f95 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 @@ -84,10 +86,10 @@ 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, - coo_canonicalization_scratch_t& scratch); +CUOPT_EXPORT void canonicalize_coo_matrix(std::vector& rows, + std::vector& cols, + std::vector& vals, + coo_canonicalization_scratch_t& scratch); /** * @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 8e0f7203de..1fe479be02 100644 --- a/cpp/src/math_optimization/CMakeLists.txt +++ b/cpp/src/math_optimization/CMakeLists.txt @@ -12,6 +12,8 @@ list(PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/logger_entry.cpp ) +set(MATH_OPT_SRC_FILES ${MATH_OPT_SRC_FILES} PARENT_SCOPE) + # solver_settings_t is host-only apart from the device members in solver_settings.cu, # so the bulk of it builds into cuopt_client. The gRPC client takes a solver_settings_t # in its public API, so this is required for the client library to resolve standalone. diff --git a/cpp/src/mip_heuristics/CMakeLists.txt b/cpp/src/mip_heuristics/CMakeLists.txt index 6beadaac8d..f3154ccb88 100644 --- a/cpp/src/mip_heuristics/CMakeLists.txt +++ b/cpp/src/mip_heuristics/CMakeLists.txt @@ -58,6 +58,8 @@ else() set(MIP_SRC_FILES ${MIP_LP_NECESSARY_FILES} ${MIP_NON_LP_FILES}) endif() +set(MIP_SRC_FILES ${MIP_SRC_FILES} PARENT_SCOPE) + # Host-only members of mip_solver_settings_t (callbacks, tolerances). The gRPC client # needs them, so they build into cuopt_client; add_initial_solution stays in the .cu. set(MIP_CLIENT_SRC_FILES diff --git a/cpp/src/mip_heuristics/solve.cu b/cpp/src/mip_heuristics/solve.cu index 794eb7bec0..6a6c6d795c 100644 --- a/cpp/src/mip_heuristics/solve.cu +++ b/cpp/src/mip_heuristics/solve.cu @@ -1039,20 +1039,19 @@ 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"); +#ifdef CUOPT_ENABLE_GRPC return solve_mip_remote(*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"); + 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 auto* cpu_prob = dynamic_cast*>(problem_interface); diff --git a/cpp/src/pdlp/CMakeLists.txt b/cpp/src/pdlp/CMakeLists.txt index f53ccf2ea0..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 @@ -50,6 +49,8 @@ else() set(LP_SRC_FILES ${LP_CORE_FILES} ${LP_ADAPTER_FILES}) endif() +set(LP_SRC_FILES ${LP_SRC_FILES} PARENT_SCOPE) + # Host-only LP sources the gRPC client needs, so cuopt_client resolves standalone. Their # GPU-facing members were split into the CUDA translation units above. set(LP_CLIENT_FILES diff --git a/cpp/src/pdlp/solve.cu b/cpp/src/pdlp/solve.cu index 40b8d80378..fc3b79c6f0 100644 --- a/cpp/src/pdlp/solve.cu +++ b/cpp/src/pdlp/solve.cu @@ -8,6 +8,7 @@ #include #include #include + #include #include #include @@ -2723,7 +2724,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, @@ -2733,13 +2733,13 @@ std::unique_ptr> solve_lp( cuopt_expects(cpu_prob != nullptr, error_type_t::ValidationError, "Remote execution requires CPU memory backend"); +#ifdef CUOPT_ENABLE_GRPC return solve_lp_remote(*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"); + 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 auto* cpu_prob = dynamic_cast*>(problem_interface); diff --git a/cpp/src/routing/CMakeLists.txt b/cpp/src/routing/CMakeLists.txt index fda06d0809..e176c0ba65 100644 --- a/cpp/src/routing/CMakeLists.txt +++ b/cpp/src/routing/CMakeLists.txt @@ -58,5 +58,6 @@ set(ROUTING_CLIENT_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/assignment_accessors.cpp ) +set(ROUTING_SRC_FILES ${ROUTING_SRC_FILES} PARENT_SCOPE) 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/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() 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/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/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(); diff --git a/cpp/tests/routing/grpc/CMakeLists.txt b/cpp/tests/routing/grpc/CMakeLists.txt index 0ed85f93b8..a8c56ae79f 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}" ) +# 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 + 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/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..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 -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 bc287f0d49..737614b955 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_mathopt -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 adc1b8e171..7d904cd02b 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,10 +95,12 @@ else() endif() message(STATUS "libcuopt: Final RPATH = ${rpaths}") -set_property(TARGET cuopt PROPERTY INSTALL_RPATH ${rpaths} APPEND) -# cuopt_client needs these too: it PUBLIC-links rapids_logger, which +# cuopt is an INTERFACE target (libcuopt.so is a linker script), so it has no RPATH. +# 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. -set_property(TARGET cuopt_client 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_routing cuopt_mathopt cuopt_client 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..86f5798f70 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,42 +53,71 @@ 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_routing.so", False), + ("libcuopt_mathopt.so", True), + ] + 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 - # 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. - - 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. " - "This might lead to a generic error such as " - "'libcuopt.so missing' if the library cannot be found.", - RuntimeWarning, - ) - pass + 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.