From a33c9d1d753e0b2c29a135077b045da215d75e71 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 29 Sep 2026 15:02:38 -0500 Subject: [PATCH 01/36] build(wheel): make libcuopt a thin metapackage libcuopt currently bundles libcuopt_client.so, libcuopt_mathopt.so and libcuopt_routing.so directly, even though the libcuopt-client / libcuopt-mathopt / libcuopt-routing wheels added in #1929 already carry the same binaries. #1929 tried making libcuopt depend on them instead and reverted it before merge, because cuopt-config.cmake exported one _IMPORT_PREFIX for all three libraries and find_package(cuopt) couldn't resolve cuopt::client/::mathopt/::routing once they lived in sibling wheels. That blocker is already gone: cpp/CMakeLists.txt now gives each component its own export set and _IMPORT_PREFIX, and cuopt-config.cmake's FINAL_CODE_BLOCK already falls back to searching CMAKE_PREFIX_PATH for a component's -targets.cmake when it isn't co-located -- built specifically to resolve sibling wheels (#1635). The RPATH wiring for cuopt_grpc_server to find the component wheels at runtime was also already in place (python/cmake/cuopt_wheel_build.cmake). This finishes wiring that mechanism through: - libcuopt's install.components is now ["dev", "cuopt", "grpc-server"], mirroring the two components (dev + cuopt) conda's libcuopt metapackage already installs, plus grpc-server since wheels have no separate cuopt-grpc-server package. - libcuopt depends on libcuopt-client/-mathopt/-routing instead of bundling the CUDA stack itself. - python/cuopt's build now also installs the three component wheels, so find_package(cuopt) can resolve their targets files via CMAKE_PREFIX_PATH. - libcuopt/load.py delegates to libcuopt_mathopt.load_library() and libcuopt_routing.load_library() instead of dlopen()ing local copies that no longer ship in this wheel. pip install libcuopt is unchanged for users; it now pulls the three component wheels as dependencies instead of embedding them, cutting the wheel from ~470MB to just the linker script, headers, CMake config and cuopt_grpc_server. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/build.yaml | 7 +- .github/workflows/pr.yaml | 9 ++- ci/build_wheel_cuopt.sh | 13 ++++ ci/build_wheel_libcuopt.sh | 3 + dependencies.yaml | 14 +++- python/cuopt/pyproject.toml | 3 + python/libcuopt/libcuopt/load.py | 128 ++++--------------------------- python/libcuopt/pyproject.toml | 18 +++-- 8 files changed, 73 insertions(+), 122 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6d49b8b1d9..6d66ada900 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -371,7 +371,12 @@ jobs: package-type: cpp publish-wheel-search-key: cuopt_wheel_cpp_libcuopt_routing wheel-build-cuopt: - needs: [build-details, wheel-build-libcuopt] + needs: + - build-details + - wheel-build-libcuopt + - wheel-build-libcuopt-client + - wheel-build-libcuopt-mathopt + - wheel-build-libcuopt-routing permissions: actions: read contents: read diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index ce7783e025..8f3a81e299 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -673,7 +673,14 @@ jobs: build-datetime: ${{ needs.build-details.outputs.build-datetime }} script: ci/build_wheel_libcuopt_routing.sh wheel-build-cuopt: - needs: [build-details, wheel-build-libcuopt, compute-matrix-filters, changed-files] + needs: + - build-details + - wheel-build-libcuopt + - wheel-build-libcuopt-client + - wheel-build-libcuopt-mathopt + - wheel-build-libcuopt-routing + - compute-matrix-filters + - changed-files if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels permissions: actions: read diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index 230e40d7f8..7cfc143c5e 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -31,6 +31,19 @@ LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_ echo "libcuopt-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo ${LIBCUOPT_WHEELHOUSE}/libcuopt_*.whl)" >> "${PIP_CONSTRAINT}" +# libcuopt is now a thin metapackage: find_package(cuopt) resolves cuopt::client / +# ::mathopt / ::routing from these component wheels' own CMake config, not from +# libcuopt's. Download and constrain them too, so they're installed alongside +# libcuopt when this build's dependencies get resolved. +LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") +echo "libcuopt-client @ file://$(echo ${LIBCUOPT_CLIENT_WHEELHOUSE}/libcuopt_client-*.whl)" >> "${PIP_CONSTRAINT}" + +LIBCUOPT_MATHOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_mathopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") +echo "libcuopt-mathopt-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo ${LIBCUOPT_MATHOPT_WHEELHOUSE}/libcuopt_mathopt_*.whl)" >> "${PIP_CONSTRAINT}" + +LIBCUOPT_ROUTING_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_routing cuopt --cuda "$RAPIDS_CUDA_VERSION")") +echo "libcuopt-routing-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo ${LIBCUOPT_ROUTING_WHEELHOUSE}/libcuopt_routing_*.whl)" >> "${PIP_CONSTRAINT}" + EXCLUDE_ARGS=( --exclude "libraft.so" --exclude "libcublas.so.*" diff --git a/ci/build_wheel_libcuopt.sh b/ci/build_wheel_libcuopt.sh index 807cfb8f21..eb9a18d8ca 100755 --- a/ci/build_wheel_libcuopt.sh +++ b/ci/build_wheel_libcuopt.sh @@ -99,6 +99,9 @@ EXCLUDE_ARGS=( --exclude "libcurand.so.*" --exclude "libcusolver.so.*" --exclude "libcusparse.so.*" + --exclude "libcuopt_client.so" + --exclude "libcuopt_mathopt.so" + --exclude "libcuopt_routing.so" --exclude "libnccl.so.*" --exclude "libnvJitLink.so*" --exclude "librapids_logger.so" diff --git a/dependencies.yaml b/dependencies.yaml index a09be27dd3..f5e12f0d5e 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -139,6 +139,9 @@ files: - depends_on_libraft_headers - depends_on_librmm - depends_on_rapids_logger + # libcuopt is a thin metapackage: it ships only the linker script, headers, CMake + # config, and cuopt_grpc_server, and depends on the three component wheels for the + # actual engine libraries -- mirroring the conda libcuopt metapackage (#1635). py_run_libcuopt: output: pyproject pyproject_dir: python/libcuopt @@ -148,8 +151,9 @@ files: extras: table: project includes: - - cuda_wheels - - depends_on_libraft_headers + - depends_on_libcuopt_client + - depends_on_libcuopt_mathopt + - depends_on_libcuopt_routing - depends_on_librmm - depends_on_rapids_logger # One set of keys per component wheel. The point of the split is in py_run_*: @@ -276,6 +280,12 @@ files: - build_common - build_python_common - depends_on_libcuopt + # libcuopt is now a thin metapackage, so find_package(cuopt) needs these + # installed alongside it to resolve cuopt::client/mathopt/routing from their + # own wheels' CMake config (see ci/build_wheel_cuopt.sh). + - depends_on_libcuopt_client + - depends_on_libcuopt_mathopt + - depends_on_libcuopt_routing - depends_on_rapids_logger - depends_on_cupy - depends_on_pylibraft diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 06eb1c9998..9efcb388b7 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -104,6 +104,9 @@ requires = [ "cmake>=4.0", "cupy-cuda13x[ctk]>=14.0.1,!=14.1.0", "cython>=3.2.2,<3.3.0a0", + "libcuopt-client==26.10.*,>=0.0.0a0", + "libcuopt-mathopt==26.10.*,>=0.0.0a0", + "libcuopt-routing==26.10.*,>=0.0.0a0", "libcuopt==26.10.*,>=0.0.0a0", "ninja", "pylibraft==26.10.*,>=0.0.0a0", diff --git a/python/libcuopt/libcuopt/load.py b/python/libcuopt/libcuopt/load.py index 86f5798f70..28817fe3a5 100644 --- a/python/libcuopt/libcuopt/load.py +++ b/python/libcuopt/libcuopt/load.py @@ -2,123 +2,29 @@ # SPDX-License-Identifier: Apache-2.0 -import ctypes -import os - -# Loading with RTLD_LOCAL adds the library itself to the loader's -# loaded library cache without loading any symbols into the global -# namespace. This allows libraries that express a dependency on -# this library to be loaded later and successfully satisfy this dependency -# without polluting the global symbol table with symbols from -# libcudf that could conflict with symbols from other DSOs. -PREFERRED_LOAD_FLAG = ctypes.RTLD_LOCAL - - -def _load_system_installation(soname: str): - """Try to dlopen() the library indicated by ``soname`` - Raises ``OSError`` if library cannot be loaded. +def load_library(): + """Dynamically load libcuopt.so and its dependencies. + + libcuopt is now a thin metapackage: it carries no engine library itself + (libcuopt.so is a linker script, not an ELF object, so it cannot be + dlopen()ed anyway). The actual libraries ship in the libcuopt-client, + libcuopt-mathopt and libcuopt-routing wheels, so loading delegates to + their own load_library(), which already handles the client-before-engine + DT_NEEDED ordering and the system/wheel installation fallback. """ - return ctypes.CDLL(soname, PREFERRED_LOAD_FLAG) - - -def _load_wheel_installation(soname: str): - """Try to dlopen() the library indicated by ``soname`` + loaded = [] - Returns ``None`` if the library cannot be loaded. - """ - if os.path.isfile( - lib := os.path.join(os.path.dirname(__file__), "lib64", soname) - ): - return ctypes.CDLL(lib, PREFERRED_LOAD_FLAG) - return None + # mathopt is required; routing is optional (SKIP_ROUTING_BUILD) and may + # not be installed at all. + import libcuopt_mathopt + loaded.extend(libcuopt_mathopt.load_library()) -def load_library(): - """Dynamically load libcuopt.so and its dependencies""" try: - # librmm and libraft must be loaded before libcuopt - # because libcuopt references them. - import libraft - import librmm - import rapids_logger - - rapids_logger.load_library() - librmm.load_library() - libraft.load_library() + import libcuopt_routing except ModuleNotFoundError: pass + else: + loaded.extend(libcuopt_routing.load_library()) - prefer_system_installation = ( - os.getenv("RAPIDS_LIBCUOPT_PREFER_SYSTEM_LIBRARY", "false").lower() - != "false" - ) - - # 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 - try: - if prefer_system_installation: - # Prefer a system library if one is present to - # avoid clobbering symbols that other packages might expect, but if no - # other library is present use the one in the wheel. - try: - libcuopt_lib = _load_system_installation(soname) - except OSError: - # The fallback needs to sit inside the outer handler too. An - # optional component that is present but fails to load would - # otherwise raise out of here and stop the remaining components - # from being loaded at all. - libcuopt_lib = _load_wheel_installation(soname) - else: - # Prefer the libraries bundled in this package. If they aren't found - # (which might be the case in builds where the library - # was prebuilt before packaging the wheel), look for a - # system installation. - libcuopt_lib = _load_wheel_installation(soname) - if libcuopt_lib is None: - libcuopt_lib = _load_system_installation(soname) - except OSError as e: - # If none of the searches above succeed, just silently return None - # and rely on other mechanisms (like RPATHs on other DSOs) to - # help the loader find the library. - if not required: - return None - - import warnings - - warnings.warn( - f"Failed to load libcuopt library: {soname}. " - f"Error: {str(e)}. " - "Falling back to relying on system loader. " - "cuOpt functionality may be unavailable. " - f"This might lead to a generic error such as " - f"'{soname} missing' if the library cannot be found.", - RuntimeWarning, - ) - # 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. - return libcuopt_lib diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 6e656a7518..0930a055df 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -30,11 +30,10 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "cuda-toolkit[cublas,cudart,curand,cusolver,cusparse,nvtx]==13.*", + "libcuopt-client==26.10.*,>=0.0.0a0", + "libcuopt-mathopt==26.10.*,>=0.0.0a0", + "libcuopt-routing==26.10.*,>=0.0.0a0", "librmm==26.10.*,>=0.0.0a0", - "nvidia-cudss-cu13>=0.7,<0.8", - "nvidia-nccl-cu13>=2.19", - "nvidia-nvjitlink>=13.3,<14", "rapids-logger==0.3.*", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. @@ -65,9 +64,14 @@ sdist.reproducible = true wheel.packages = ["libcuopt"] wheel.install-dir = "libcuopt" wheel.py-api = "py3" -# install.components is deliberately unset: this wheel still ships every component. -# Trimming it needs per-component CMake packages, so find_package(cuopt) can resolve each -# from its own prefix, and care with files rapids-cmake installs without a COMPONENT. +# libcuopt is a thin metapackage: the client/mathopt/routing engine libraries now come +# from their own wheels (find_package(cuopt) resolves them via the component wheels' +# CMake config, since #1635 gave each its own export set / _IMPORT_PREFIX). "dev" is the +# linker script, "cuopt" is rapids_export's own component (cuopt-config.cmake + the +# umbrella export set) -- the same two components the conda libcuopt metapackage +# installs (conda/recipes/libcuopt/recipe.yaml). "grpc-server" is added because wheels +# have no separate cuopt-grpc-server package the way conda does. +install.components = ["dev", "cuopt", "grpc-server"] [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" From 44220345b902ea464ec5e2070612b8a2dbd6fc26 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 29 Sep 2026 15:02:47 -0500 Subject: [PATCH 02/36] ci: TEMPORARILY disable conda/java PR jobs for faster wheel-only iteration Disables conda-cpp-build, conda-cpp-tests, multi-gpu-cpp-tests, conda-python-build, conda-python-tests, docs-build, java-static-build-matrix, java-static-build, java-static-test and java-build (each via `if: false`), and drops them from pr-builder's needs list, so PR CI only runs the wheel jobs while iterating on the thin-libcuopt-metapackage change. MUST BE REVERTED before merging -- this is scoped to speeding up iteration on this branch, not a real reduction in what main's CI covers. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/pr.yaml | 64 ++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8f3a81e299..7e0c881df2 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -21,16 +21,19 @@ jobs: - compute-matrix-filters - changed-files - checks - - conda-cpp-build - - conda-cpp-tests - - java-build - - java-static-build-matrix - - java-static-build - - java-static-test - - conda-python-build - - conda-python-tests - - docs-build - - multi-gpu-cpp-tests + # TEMPORARY: conda/java jobs disabled below with `if: false` for faster + # iteration on the thin-libcuopt-metapackage wheel change. Re-enable (drop the + # `if: false` lines and restore this needs list) before merging. + # - conda-cpp-build + # - conda-cpp-tests + # - java-build + # - java-static-build-matrix + # - java-static-build + # - java-static-test + # - conda-python-build + # - conda-python-tests + # - docs-build + # - multi-gpu-cpp-tests - wheel-build-libcuopt - wheel-build-libcuopt-client - wheel-build-libcuopt-mathopt @@ -389,11 +392,8 @@ jobs: conda-cpp-build: needs: [build-details, checks, compute-matrix-filters, changed-files] # Consumed by C++, Java, Python, and docs jobs. - if: >- - fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp || - fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || - fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda || - fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false permissions: actions: read contents: read @@ -415,7 +415,8 @@ jobs: packages: read pull-requests: read uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@release/26.10 - if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request script: ci/test_cpp.sh @@ -435,15 +436,15 @@ jobs: packages: read pull-requests: read uses: ./.github/workflows/multi_gpu_cpp_test.yaml - if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request conda-python-build: needs: [build-details, conda-cpp-build, changed-files] # Consumed by conda-python-tests and docs-build. - if: >- - fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda || - fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false permissions: actions: read contents: read @@ -467,7 +468,8 @@ jobs: packages: read pull-requests: read uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/26.10 - if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request run_codecov: false @@ -482,7 +484,8 @@ jobs: pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.10 - if: fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request node_type: "gpu-l4-latest-1" @@ -498,9 +501,8 @@ jobs: permissions: contents: read uses: rapidsai/shared-workflows/.github/workflows/compute-matrix.yaml@main - if: >- - fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || - fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request matrix_name: conda-cpp-build @@ -519,9 +521,8 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJSON(needs.java-static-build-matrix.outputs.matrix) }} - if: >- - fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || - fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request node_type: "cpu16" @@ -557,6 +558,8 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJSON(needs.java-static-build-matrix.outputs.matrix) }} + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request node_type: "gpu-l4-latest-1" @@ -584,9 +587,8 @@ jobs: pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.10 - if: >- - fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || - fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp + # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. + if: false with: build_type: pull-request node_type: "gpu-l4-latest-1" From b6a998ffe0d02f81e450814771b8502cc38e6cf5 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 29 Sep 2026 16:08:53 -0500 Subject: [PATCH 03/36] fix(wheel): install the Unspecified CMake component into libcuopt too wheel-build-cuopt failed in CI (#2013) configuring the cuopt wheel: CMake Error at .../librmm/lib64/rapids/cmake/cub/cub-config.cmake:9 (libcudacxx_update_language_compat_flags): Unknown CMake command "libcudacxx_update_language_compat_flags". In the Rocky8 CI image (no system CCCL), rapids-cmake CPM-fetches CCCL and writes self-contained "found package" redirects under lib64/rapids/cmake/{cub,libcudacxx,cccl}/*.cmake with no COMPONENT tag, so they land in CMake's default "Unspecified" component. libcuopt's install.components didn't include it, so cuopt-config.cmake's find_dependency(rmm) fell through to librmm's own bundled copy of those redirects instead of the self-consistent set that used to sit alongside cuopt-config.cmake -- and that copy hits a CCCL find_package ordering bug (cub-config.cmake calls a function libcudacxx-config.cmake defines, but CMake's found-package caching can skip re-processing libcudacxx from that same copy once it's already been resolved via a different path). Adding "Unspecified" back restores those redirect files (and a handful of small headers/static libs) without reintroducing the actual engine binaries, which still have explicit component tags (client/mathopt/routing) that stay excluded. Co-Authored-By: Claude Sonnet 5 --- python/libcuopt/pyproject.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 0930a055df..1779302815 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -71,7 +71,16 @@ wheel.py-api = "py3" # umbrella export set) -- the same two components the conda libcuopt metapackage # installs (conda/recipes/libcuopt/recipe.yaml). "grpc-server" is added because wheels # have no separate cuopt-grpc-server package the way conda does. -install.components = ["dev", "cuopt", "grpc-server"] +# +# "Unspecified" (CMake's default component name for install() calls with no COMPONENT) +# must be included too: rapids-cmake writes self-contained "found package" redirects for +# CPM-fetched CCCL (lib64/rapids/cmake/{cub,libcudacxx,cccl}/*.cmake) with no COMPONENT. +# Without them here, cuopt-config.cmake's find_dependency(rmm) falls through to +# libcuopt-rmm's own bundled copy of those redirects instead of a self-consistent one +# installed alongside cuopt-config.cmake, which hits a CCCL find_package ordering bug +# (cub-config.cmake calls a function libcudacxx-config.cmake defines, but CMake's +# found-package caching can skip re-processing libcudacxx from that same copy). See #2013. +install.components = ["dev", "cuopt", "grpc-server", "Unspecified"] [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" From a378050bca3b76ff0929509720cd0f9bcd40d7d5 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 29 Sep 2026 21:43:27 -0500 Subject: [PATCH 04/36] fix(wheel): add missing mathopt/routing include dirs for client-only Cython modules internals, parser_wrapper, and grpc_client link only cuopt::client for headers, but each also cimports mathopt and/or routing headers. That worked when headers were one shared tree; now that mathopt-dev/routing-dev are separate wheels, add their include dirs explicitly without linking the libraries. Verified locally: full python/cuopt CMake build against split install prefixes (mimicking separate wheels) now succeeds end to end. Co-Authored-By: Claude Sonnet 5 --- python/cuopt/cuopt/grpc/client/CMakeLists.txt | 7 +++++++ .../cuopt/linear_programming/internals/CMakeLists.txt | 6 ++++++ python/cuopt/cuopt/linear_programming/io/CMakeLists.txt | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/python/cuopt/cuopt/grpc/client/CMakeLists.txt b/python/cuopt/cuopt/grpc/client/CMakeLists.txt index ff05235d86..7385b90548 100644 --- a/python/cuopt/cuopt/grpc/client/CMakeLists.txt +++ b/python/cuopt/cuopt/grpc/client/CMakeLists.txt @@ -18,3 +18,10 @@ rapids_cython_create_modules( MODULE_PREFIX grpc_client_ ASSOCIATED_TARGETS cuopt ) + +# grpc_client.cxx cimports routing and mathopt headers; both are now separate wheels, +# so add their include dirs without linking the libraries. +target_include_directories(grpc_client_grpc_client PRIVATE + $ + $ +) diff --git a/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt index d818df4d21..56e1ffb0fb 100644 --- a/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/internals/CMakeLists.txt @@ -16,3 +16,9 @@ rapids_cython_create_modules( ) target_include_directories(internals_internals PRIVATE "${CMAKE_CURRENT_LIST_DIR}") + +# internals.pyx also cimports a mathopt header (callbacks_implems.hpp); mathopt-dev is +# now a separate wheel, so add its include dir without linking the library. +target_include_directories(internals_internals PRIVATE + $ +) diff --git a/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt index a2c5f35fe4..b06f87097b 100644 --- a/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt +++ b/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt @@ -8,3 +8,9 @@ set(linked_libraries cuopt::client) rapids_cython_create_modules(SOURCE_FILES "${cython_sources}" LINKED_LIBRARIES "${linked_libraries}" ASSOCIATED_TARGETS cuopt CXX) + +# parser_wrapper.pyx cimports mathopt headers; mathopt-dev is now a separate wheel, so +# add its include dir without linking the library. +target_include_directories(parser_wrapper PRIVATE + $ +) From adaa6375317853d6ac4a78c73d35382ff96995fe Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 29 Sep 2026 22:26:41 -0500 Subject: [PATCH 05/36] fix(client): move status codes out of mathopt's constants.h error.hpp (client-dev, the leaf header) included mathematical_optimization/constants.h (mathopt-dev) just for 4 generic status codes. That worked when headers were one shared tree; split across wheels, any client-only consumer that pulls in error.hpp (nearly everything) fails to find it. Moved those constants to a new status_codes.h in client-dev; constants.h includes it for existing mathopt callers. Verified locally: python/cuopt builds clean against split install prefixes. Co-Authored-By: Claude Sonnet 5 --- cpp/CMakeLists.txt | 2 +- cpp/include/cuopt/error.hpp | 2 +- .../mathematical_optimization/constants.h | 10 ++-------- cpp/include/cuopt/status_codes.h | 20 +++++++++++++++++++ 4 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 cpp/include/cuopt/status_codes.h diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 091239b147..20c29d726a 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1299,7 +1299,7 @@ endif () # Headers follow their component. Shared ones go to client-dev, the leaf every # other component links. -install(FILES include/cuopt/error.hpp include/cuopt/export.hpp +install(FILES include/cuopt/error.hpp include/cuopt/export.hpp include/cuopt/status_codes.h DESTINATION ${_INCLUDE_DEST} COMPONENT client-dev ) diff --git a/cpp/include/cuopt/error.hpp b/cpp/include/cuopt/error.hpp index 40be060c5b..119fe7dfab 100644 --- a/cpp/include/cuopt/error.hpp +++ b/cpp/include/cuopt/error.hpp @@ -7,7 +7,7 @@ #pragma once #include -#include "cuopt/mathematical_optimization/constants.h" +#include "cuopt/status_codes.h" #include diff --git a/cpp/include/cuopt/mathematical_optimization/constants.h b/cpp/include/cuopt/mathematical_optimization/constants.h index 01afb9f98f..d8b68bfee0 100644 --- a/cpp/include/cuopt/mathematical_optimization/constants.h +++ b/cpp/include/cuopt/mathematical_optimization/constants.h @@ -235,14 +235,8 @@ /* @brief File format constants for problem I/O */ #define CUOPT_FILE_FORMAT_MPS 0 -/* @brief Status codes constants */ -#define CUOPT_SUCCESS 0 -#define CUOPT_INVALID_ARGUMENT 1 -#define CUOPT_MPS_FILE_ERROR 2 -#define CUOPT_MPS_PARSE_ERROR 3 -#define CUOPT_VALIDATION_ERROR 4 -#define CUOPT_OUT_OF_MEMORY 5 -#define CUOPT_RUNTIME_ERROR 6 +/* @brief Status codes constants -- shared with cuopt::client, defined in status_codes.h */ +#include "cuopt/status_codes.h" #define CUOPT_PRESOLVE_DEFAULT -1 #define CUOPT_PRESOLVE_OFF 0 diff --git a/cpp/include/cuopt/status_codes.h b/cpp/include/cuopt/status_codes.h new file mode 100644 index 0000000000..dc9fa006e8 --- /dev/null +++ b/cpp/include/cuopt/status_codes.h @@ -0,0 +1,20 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#ifndef CUOPT_STATUS_CODES_H +#define CUOPT_STATUS_CODES_H + +/* @brief Status codes constants */ +#define CUOPT_SUCCESS 0 +#define CUOPT_INVALID_ARGUMENT 1 +#define CUOPT_MPS_FILE_ERROR 2 +#define CUOPT_MPS_PARSE_ERROR 3 +#define CUOPT_VALIDATION_ERROR 4 +#define CUOPT_OUT_OF_MEMORY 5 +#define CUOPT_RUNTIME_ERROR 6 + +#endif // CUOPT_STATUS_CODES_H From 7fd4080168da7c186e59c7681fa8eeddb0d9007b Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 06:34:43 -0500 Subject: [PATCH 06/36] fix(wheel): drop libcuopt's dead cuopt_cli entry point cuopt_cli binary is COMPONENT mathopt, no longer in libcuopt's install components, so libcuopt's cuopt_cli wrapper exec'd a path that no longer exists (FileNotFoundError, then a segfault in the test suite). It also collided with libcuopt-mathopt's own working cuopt_cli entry point in the same venv bin/. Removed the entry point and wrapper from libcuopt; libcuopt-mathopt's stays as the one real cuopt_cli, pulled in transitively via libcuopt's dependency on it. Co-Authored-By: Claude Sonnet 5 --- python/libcuopt/libcuopt/_cli_wrapper.py | 17 ----------------- python/libcuopt/pyproject.toml | 1 - 2 files changed, 18 deletions(-) delete mode 100644 python/libcuopt/libcuopt/_cli_wrapper.py diff --git a/python/libcuopt/libcuopt/_cli_wrapper.py b/python/libcuopt/libcuopt/_cli_wrapper.py deleted file mode 100644 index 78e95280b7..0000000000 --- a/python/libcuopt/libcuopt/_cli_wrapper.py +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -import os -import sys - - -def main(): - """ - This connects to cli binary which situated under libcuopt/bin folder - - execv replaces this process rather than spawning a child, so signals sent - to the console script's pid reach the solver directly instead of stopping - at a Python parent that forwards nothing. - """ - cli_path = os.path.join(os.path.dirname(__file__), "bin", "cuopt_cli") - os.execv(cli_path, [cli_path] + sys.argv[1:]) diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 1779302815..3c990b34f4 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -45,7 +45,6 @@ Source = "https://github.com/nvidia/cuopt" libcuopt = "libcuopt" [project.scripts] -cuopt_cli = "libcuopt._cli_wrapper:main" cuopt_grpc_server = "libcuopt._grpc_server_wrapper:main" [tool.pydistcheck] From bcf829f4aca910c0c09a8bf16c92356d4739aaf7 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 08:58:10 -0500 Subject: [PATCH 07/36] ci: retrigger after stuck runner From 1bdcc38120c08acd0523fa60055ccca7c0119bca Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 09:55:34 -0500 Subject: [PATCH 08/36] fix(ci): constrain libcuopt-client/-mathopt/-routing in test scripts wheel-tests-cuopt crashed with random heap-corruption exceptions (std::bad_array_new_length, std::bad_alloc) in cuopt_cli --help across multiple GPU types. Root cause: these scripts only pinned cuopt/libcuopt to this run's build; libcuopt's now-thin dependency on libcuopt-client/ -mathopt/-routing let pip resolve those from the published nightly index instead, so cuopt (built against this run's ABI) loaded a mismatched libcuopt_mathopt.so at runtime. Co-Authored-By: Claude Sonnet 5 --- ci/test_self_hosted_service.sh | 14 ++++++++++++++ ci/test_wheel_cuopt.sh | 10 ++++++++++ ci/test_wheel_cuopt_server.sh | 14 ++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/ci/test_self_hosted_service.sh b/ci/test_self_hosted_service.sh index 803b55be46..6e29c19291 100755 --- a/ci/test_self_hosted_service.sh +++ b/ci/test_self_hosted_service.sh @@ -8,13 +8,27 @@ set -euo pipefail source rapids-init-pip # Download the cuopt built in the previous step +RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen "${RAPIDS_CUDA_VERSION}")" LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SERVER_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-server cuopt --pure --arch any --cuda "$RAPIDS_CUDA_VERSION")") +# libcuopt is a thin metapackage: pip would otherwise resolve libcuopt-client/-mathopt/-routing +# from the published index instead of this run's build, mismatching the ABI cuopt was built +# against. +LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") +LIBCUOPT_MATHOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_mathopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") +LIBCUOPT_ROUTING_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_routing cuopt --cuda "$RAPIDS_CUDA_VERSION")") + # generate constraints (possibly pinning to oldest support versions of dependencies) rapids-generate-pip-constraints test_python "${PIP_CONSTRAINT}" +cat >> "${PIP_CONSTRAINT}" < "${PIP_CONSTRAINT}" <> "${PIP_CONSTRAINT}" < Date: Wed, 30 Sep 2026 11:15:13 -0500 Subject: [PATCH 09/36] Revert "ci: TEMPORARILY disable conda/java PR jobs for faster wheel-only iteration" This reverts commit 44220345b902ea464ec5e2070612b8a2dbd6fc26. --- .github/workflows/pr.yaml | 64 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 7e0c881df2..8f3a81e299 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -21,19 +21,16 @@ jobs: - compute-matrix-filters - changed-files - checks - # TEMPORARY: conda/java jobs disabled below with `if: false` for faster - # iteration on the thin-libcuopt-metapackage wheel change. Re-enable (drop the - # `if: false` lines and restore this needs list) before merging. - # - conda-cpp-build - # - conda-cpp-tests - # - java-build - # - java-static-build-matrix - # - java-static-build - # - java-static-test - # - conda-python-build - # - conda-python-tests - # - docs-build - # - multi-gpu-cpp-tests + - conda-cpp-build + - conda-cpp-tests + - java-build + - java-static-build-matrix + - java-static-build + - java-static-test + - conda-python-build + - conda-python-tests + - docs-build + - multi-gpu-cpp-tests - wheel-build-libcuopt - wheel-build-libcuopt-client - wheel-build-libcuopt-mathopt @@ -392,8 +389,11 @@ jobs: conda-cpp-build: needs: [build-details, checks, compute-matrix-filters, changed-files] # Consumed by C++, Java, Python, and docs jobs. - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: >- + fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp || + fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || + fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda || + fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs permissions: actions: read contents: read @@ -415,8 +415,7 @@ jobs: packages: read pull-requests: read uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@release/26.10 - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request script: ci/test_cpp.sh @@ -436,15 +435,15 @@ jobs: packages: read pull-requests: read uses: ./.github/workflows/multi_gpu_cpp_test.yaml - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request conda-python-build: needs: [build-details, conda-cpp-build, changed-files] # Consumed by conda-python-tests and docs-build. - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: >- + fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda || + fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs permissions: actions: read contents: read @@ -468,8 +467,7 @@ jobs: packages: read pull-requests: read uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@release/26.10 - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda with: build_type: pull-request run_codecov: false @@ -484,8 +482,7 @@ jobs: pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.10 - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: fromJSON(needs.changed-files.outputs.changed_file_groups).build_docs with: build_type: pull-request node_type: "gpu-l4-latest-1" @@ -501,8 +498,9 @@ jobs: permissions: contents: read uses: rapidsai/shared-workflows/.github/workflows/compute-matrix.yaml@main - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: >- + fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || + fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request matrix_name: conda-cpp-build @@ -521,8 +519,9 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJSON(needs.java-static-build-matrix.outputs.matrix) }} - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: >- + fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || + fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request node_type: "cpu16" @@ -558,8 +557,6 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJSON(needs.java-static-build-matrix.outputs.matrix) }} - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false with: build_type: pull-request node_type: "gpu-l4-latest-1" @@ -587,8 +584,9 @@ jobs: pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@release/26.10 - # TEMPORARY: disabled for faster wheel-only CI iteration. Revert before merging. - if: false + if: >- + fromJSON(needs.changed-files.outputs.changed_file_groups).test_java || + fromJSON(needs.changed-files.outputs.changed_file_groups).test_cpp with: build_type: pull-request node_type: "gpu-l4-latest-1" From 6ff40255c2717d55cef3ded56673690725e07808 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 11:20:19 -0500 Subject: [PATCH 10/36] chore: trim redundant comments Co-Authored-By: Claude Sonnet 5 --- ci/build_wheel_cuopt.sh | 4 ---- ci/test_self_hosted_service.sh | 3 --- ci/test_wheel_cuopt.sh | 3 --- ci/test_wheel_cuopt_server.sh | 3 --- dependencies.yaml | 8 ++------ python/libcuopt/pyproject.toml | 21 +++++---------------- 6 files changed, 7 insertions(+), 35 deletions(-) diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index 7cfc143c5e..f5ac65f51b 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -31,10 +31,6 @@ LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_ echo "libcuopt-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo ${LIBCUOPT_WHEELHOUSE}/libcuopt_*.whl)" >> "${PIP_CONSTRAINT}" -# libcuopt is now a thin metapackage: find_package(cuopt) resolves cuopt::client / -# ::mathopt / ::routing from these component wheels' own CMake config, not from -# libcuopt's. Download and constrain them too, so they're installed alongside -# libcuopt when this build's dependencies get resolved. LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") echo "libcuopt-client @ file://$(echo ${LIBCUOPT_CLIENT_WHEELHOUSE}/libcuopt_client-*.whl)" >> "${PIP_CONSTRAINT}" diff --git a/ci/test_self_hosted_service.sh b/ci/test_self_hosted_service.sh index 6e29c19291..ea9839378d 100755 --- a/ci/test_self_hosted_service.sh +++ b/ci/test_self_hosted_service.sh @@ -13,9 +13,6 @@ LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_ CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SERVER_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-server cuopt --pure --arch any --cuda "$RAPIDS_CUDA_VERSION")") -# libcuopt is a thin metapackage: pip would otherwise resolve libcuopt-client/-mathopt/-routing -# from the published index instead of this run's build, mismatching the ABI cuopt was built -# against. LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") LIBCUOPT_MATHOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_mathopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") LIBCUOPT_ROUTING_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_routing cuopt --cuda "$RAPIDS_CUDA_VERSION")") diff --git a/ci/test_wheel_cuopt.sh b/ci/test_wheel_cuopt.sh index 004dc4c0f9..3d7c88c590 100755 --- a/ci/test_wheel_cuopt.sh +++ b/ci/test_wheel_cuopt.sh @@ -20,9 +20,6 @@ LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_ CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SH_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-sh-client cuopt --pure --arch any)") -# libcuopt is a thin metapackage: pip would otherwise resolve libcuopt-client/-mathopt/-routing -# from the published index instead of this run's build, mismatching the ABI cuopt was built -# against. LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") LIBCUOPT_MATHOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_mathopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") LIBCUOPT_ROUTING_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_routing cuopt --cuda "$RAPIDS_CUDA_VERSION")") diff --git a/ci/test_wheel_cuopt_server.sh b/ci/test_wheel_cuopt_server.sh index 455cc73a0e..42bd50ec00 100755 --- a/ci/test_wheel_cuopt_server.sh +++ b/ci/test_wheel_cuopt_server.sh @@ -19,9 +19,6 @@ CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_pyt CUOPT_SERVER_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-server cuopt --pure --arch any --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SH_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-sh-client cuopt --pure --arch any)") -# libcuopt is a thin metapackage: pip would otherwise resolve libcuopt-client/-mathopt/-routing -# from the published index instead of this run's build, mismatching the ABI cuopt was built -# against. LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") LIBCUOPT_MATHOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_mathopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") LIBCUOPT_ROUTING_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_routing cuopt --cuda "$RAPIDS_CUDA_VERSION")") diff --git a/dependencies.yaml b/dependencies.yaml index f5e12f0d5e..31c1b49d58 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -139,9 +139,7 @@ files: - depends_on_libraft_headers - depends_on_librmm - depends_on_rapids_logger - # libcuopt is a thin metapackage: it ships only the linker script, headers, CMake - # config, and cuopt_grpc_server, and depends on the three component wheels for the - # actual engine libraries -- mirroring the conda libcuopt metapackage (#1635). + # Thin metapackage: depends on the three component wheels instead of bundling them. py_run_libcuopt: output: pyproject pyproject_dir: python/libcuopt @@ -280,9 +278,7 @@ files: - build_common - build_python_common - depends_on_libcuopt - # libcuopt is now a thin metapackage, so find_package(cuopt) needs these - # installed alongside it to resolve cuopt::client/mathopt/routing from their - # own wheels' CMake config (see ci/build_wheel_cuopt.sh). + # needed for find_package(cuopt) to resolve cuopt::client/mathopt/routing - depends_on_libcuopt_client - depends_on_libcuopt_mathopt - depends_on_libcuopt_routing diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index 3c990b34f4..bd3d8fbe50 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -63,22 +63,11 @@ sdist.reproducible = true wheel.packages = ["libcuopt"] wheel.install-dir = "libcuopt" wheel.py-api = "py3" -# libcuopt is a thin metapackage: the client/mathopt/routing engine libraries now come -# from their own wheels (find_package(cuopt) resolves them via the component wheels' -# CMake config, since #1635 gave each its own export set / _IMPORT_PREFIX). "dev" is the -# linker script, "cuopt" is rapids_export's own component (cuopt-config.cmake + the -# umbrella export set) -- the same two components the conda libcuopt metapackage -# installs (conda/recipes/libcuopt/recipe.yaml). "grpc-server" is added because wheels -# have no separate cuopt-grpc-server package the way conda does. -# -# "Unspecified" (CMake's default component name for install() calls with no COMPONENT) -# must be included too: rapids-cmake writes self-contained "found package" redirects for -# CPM-fetched CCCL (lib64/rapids/cmake/{cub,libcudacxx,cccl}/*.cmake) with no COMPONENT. -# Without them here, cuopt-config.cmake's find_dependency(rmm) falls through to -# libcuopt-rmm's own bundled copy of those redirects instead of a self-consistent one -# installed alongside cuopt-config.cmake, which hits a CCCL find_package ordering bug -# (cub-config.cmake calls a function libcudacxx-config.cmake defines, but CMake's -# found-package caching can skip re-processing libcudacxx from that same copy). See #2013. +# Thin metapackage: engine libraries come from their own wheels now (#1635). +# "dev" = linker script, "cuopt" = rapids_export's config/export set, "grpc-server" = +# cuopt_grpc_server. "Unspecified" carries rapids-cmake's CCCL redirect files, needed +# for find_dependency(rmm) to resolve cleanly instead of hitting librmm's own +# (order-dependent) copy -- see #2013. install.components = ["dev", "cuopt", "grpc-server", "Unspecified"] [tool.scikit-build.metadata.version] From 5f66ed1d2a04050d723d8559bfe609cae90a8d21 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 11:25:53 -0500 Subject: [PATCH 11/36] fix(release): bump libcuopt-client/-mathopt/-routing pins on release libcuopt and cuopt now depend on these by version-pinned string; without this the release version-bump script left them stale. Co-Authored-By: Claude Sonnet 5 --- ci/release/update-version.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh index dbe706a4c7..607371396c 100755 --- a/ci/release/update-version.sh +++ b/ci/release/update-version.sh @@ -103,6 +103,9 @@ DEPENDENCIES=( cuopt-server cuopt-sh-client libcuopt + libcuopt-client + libcuopt-mathopt + libcuopt-routing libcuopt-tests libraft-headers librmm From 03e1774709156b5161abfdc8f9c6516c60af8d89 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 15:36:16 -0500 Subject: [PATCH 12/36] fix(ci): protect the 'cuda' dnf repo against transient CDN 404s java-static-build and test_java_static.sh ran an unprotected dnf install before ever calling update_rockylinux_repo.sh, so the very first dnf call in a fresh container -- which triggers a metadata refresh across every enabled repo, including the flaky 'cuda' one -- had no retry protection. The wheel build scripts already called update_rockylinux_repo.sh first, but its 3-attempt retry budget wasn't enough to survive today's CDN-edge-propagation outage on developer.download.nvidia.com (HTTP 404 on specific repodata checksums). Move the repo-fix call before the first dnf install, and widen the retry budget to 6 attempts. Co-Authored-By: Claude Sonnet 5 --- ci/test_java_static.sh | 4 ++++ ci/utils/update_rockylinux_repo.sh | 6 ++++-- java/cuopt/ci/setup_java_static_env.sh | 7 +++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ci/test_java_static.sh b/ci/test_java_static.sh index 20fcfe935c..c7df091da5 100755 --- a/ci/test_java_static.sh +++ b/ci/test_java_static.sh @@ -43,6 +43,10 @@ rapids-logger "Testing $(basename "${CUOPT_JAVA_JAR}")" # copy of everything else it needs. See #1817 and the java-static-classifiers PR discussion for # why this moved off a fresh `conda create`: that env-solve was slow and consistently synced up # concurrent matrix jobs' cold Maven Central resolution, which is what triggered repeated 429s. +# Runs before the dnf install below: the first dnf call in a fresh container triggers dnf's +# repo metadata refresh, including the flaky 'cuda' repo. +bash "${REPO_ROOT}/ci/utils/update_rockylinux_repo.sh" + rapids-logger "Installing a JDK (dnf's own maven package is too old; see MAVEN_VERSION below)" MAVEN_VERSION="3.9.16" # Matches maven.compiler.release in java/cuopt/pom.xml (bumped to 17 in #1865); an older JDK diff --git a/ci/utils/update_rockylinux_repo.sh b/ci/utils/update_rockylinux_repo.sh index b2a78adb7a..2b74516ecf 100755 --- a/ci/utils/update_rockylinux_repo.sh +++ b/ci/utils/update_rockylinux_repo.sh @@ -56,10 +56,12 @@ skip_if_unavailable=False metadata_expire=1h EOF -# Clean DNF cache and refresh repository metadata with retry logic +# Clean DNF cache and refresh repository metadata with retry logic. This also covers the +# 'cuda' repo (NVIDIA's developer.download.nvidia.com), which is prone to transient CDN +# edge-propagation 404s; 6 attempts gives it a real chance to ride those out. dnf clean all -max_attempts=3 +max_attempts=6 attempt=1 while [ $attempt -le $max_attempts ]; do if dnf makecache --refresh; then diff --git a/java/cuopt/ci/setup_java_static_env.sh b/java/cuopt/ci/setup_java_static_env.sh index 3139f1c9b9..6610c633d7 100644 --- a/java/cuopt/ci/setup_java_static_env.sh +++ b/java/cuopt/ci/setup_java_static_env.sh @@ -31,6 +31,10 @@ fi # shellcheck disable=SC1091 source rapids-init-pip +# Runs before any dnf install (not just Boost/TBB/cuDSS below): the very first dnf call in a +# fresh container is what triggers dnf's repo metadata refresh, including the flaky 'cuda' repo. +bash ci/utils/update_rockylinux_repo.sh + rapids-logger "Installing JDK, Maven, ninja and cmake" # dnf's own maven and cmake packages are too old for this project's needs; get ninja/cmake from # PyPI, matching how the build side already gets modern tooling in this family of images, and @@ -66,8 +70,7 @@ export CC="/opt/rh/gcc-toolset-${TOOLSET_VERSION}/root/usr/bin/gcc" export CXX="/opt/rh/gcc-toolset-${TOOLSET_VERSION}/root/usr/bin/g++" export CUDAHOSTCXX="${CXX}" -rapids-logger "Installing rockylinux repo, Boost, TBB and cuDSS" -bash ci/utils/update_rockylinux_repo.sh +rapids-logger "Installing Boost, TBB and cuDSS" bash ci/utils/install_boost_tbb.sh # cpp/CMakeLists.txt never links Papilo's own CMake target (only its source/build directories, # to avoid confusing clang's include resolution -- see its "Adding Papilo as a system include" From 68e363d32bb001f9a0fe0381099d25e511b8ccd2 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 15:41:52 -0500 Subject: [PATCH 13/36] Revert "fix(ci): protect the 'cuda' dnf repo against transient CDN 404s" This reverts commit 03e1774709156b5161abfdc8f9c6516c60af8d89. --- ci/test_java_static.sh | 4 ---- ci/utils/update_rockylinux_repo.sh | 6 ++---- java/cuopt/ci/setup_java_static_env.sh | 7 ++----- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/ci/test_java_static.sh b/ci/test_java_static.sh index c7df091da5..20fcfe935c 100755 --- a/ci/test_java_static.sh +++ b/ci/test_java_static.sh @@ -43,10 +43,6 @@ rapids-logger "Testing $(basename "${CUOPT_JAVA_JAR}")" # copy of everything else it needs. See #1817 and the java-static-classifiers PR discussion for # why this moved off a fresh `conda create`: that env-solve was slow and consistently synced up # concurrent matrix jobs' cold Maven Central resolution, which is what triggered repeated 429s. -# Runs before the dnf install below: the first dnf call in a fresh container triggers dnf's -# repo metadata refresh, including the flaky 'cuda' repo. -bash "${REPO_ROOT}/ci/utils/update_rockylinux_repo.sh" - rapids-logger "Installing a JDK (dnf's own maven package is too old; see MAVEN_VERSION below)" MAVEN_VERSION="3.9.16" # Matches maven.compiler.release in java/cuopt/pom.xml (bumped to 17 in #1865); an older JDK diff --git a/ci/utils/update_rockylinux_repo.sh b/ci/utils/update_rockylinux_repo.sh index 2b74516ecf..b2a78adb7a 100755 --- a/ci/utils/update_rockylinux_repo.sh +++ b/ci/utils/update_rockylinux_repo.sh @@ -56,12 +56,10 @@ skip_if_unavailable=False metadata_expire=1h EOF -# Clean DNF cache and refresh repository metadata with retry logic. This also covers the -# 'cuda' repo (NVIDIA's developer.download.nvidia.com), which is prone to transient CDN -# edge-propagation 404s; 6 attempts gives it a real chance to ride those out. +# Clean DNF cache and refresh repository metadata with retry logic dnf clean all -max_attempts=6 +max_attempts=3 attempt=1 while [ $attempt -le $max_attempts ]; do if dnf makecache --refresh; then diff --git a/java/cuopt/ci/setup_java_static_env.sh b/java/cuopt/ci/setup_java_static_env.sh index 6610c633d7..3139f1c9b9 100644 --- a/java/cuopt/ci/setup_java_static_env.sh +++ b/java/cuopt/ci/setup_java_static_env.sh @@ -31,10 +31,6 @@ fi # shellcheck disable=SC1091 source rapids-init-pip -# Runs before any dnf install (not just Boost/TBB/cuDSS below): the very first dnf call in a -# fresh container is what triggers dnf's repo metadata refresh, including the flaky 'cuda' repo. -bash ci/utils/update_rockylinux_repo.sh - rapids-logger "Installing JDK, Maven, ninja and cmake" # dnf's own maven and cmake packages are too old for this project's needs; get ninja/cmake from # PyPI, matching how the build side already gets modern tooling in this family of images, and @@ -70,7 +66,8 @@ export CC="/opt/rh/gcc-toolset-${TOOLSET_VERSION}/root/usr/bin/gcc" export CXX="/opt/rh/gcc-toolset-${TOOLSET_VERSION}/root/usr/bin/g++" export CUDAHOSTCXX="${CXX}" -rapids-logger "Installing Boost, TBB and cuDSS" +rapids-logger "Installing rockylinux repo, Boost, TBB and cuDSS" +bash ci/utils/update_rockylinux_repo.sh bash ci/utils/install_boost_tbb.sh # cpp/CMakeLists.txt never links Papilo's own CMake target (only its source/build directories, # to avoid confusing clang's include resolution -- see its "Adding Papilo as a system include" From 75e09573417885b91396b4d204ee3cba42462424 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 16:24:28 -0500 Subject: [PATCH 14/36] fix(java): generate CuOptConstants from status_codes.h too status_codes.h moved the status-code #defines out of constants.h, but generate_constants.sh only scanned the one header pom.xml gave it, so CuOptConstants.java lost CUOPT_MPS_FILE_ERROR etc. and NativeIntegrationTest failed to compile. Let it take multiple header inputs and pass both. Verified locally: the generated CuOptConstants.java now has CUOPT_SUCCESS, CUOPT_MPS_FILE_ERROR, CUOPT_VALIDATION_ERROR, etc. again. Co-Authored-By: Claude Sonnet 5 --- java/cuopt/pom.xml | 1 + java/cuopt/scripts/generate_constants.sh | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/java/cuopt/pom.xml b/java/cuopt/pom.xml index 161909c2b8..e6bd9c2888 100644 --- a/java/cuopt/pom.xml +++ b/java/cuopt/pom.xml @@ -158,6 +158,7 @@ SPDX-License-Identifier: Apache-2.0 ${project.basedir}/scripts/generate_constants.sh ${project.basedir}/../../cpp/include/cuopt/mathematical_optimization/constants.h + ${project.basedir}/../../cpp/include/cuopt/status_codes.h ${project.build.directory}/generated-sources/cuopt diff --git a/java/cuopt/scripts/generate_constants.sh b/java/cuopt/scripts/generate_constants.sh index 85ae723688..efb768e6af 100755 --- a/java/cuopt/scripts/generate_constants.sh +++ b/java/cuopt/scripts/generate_constants.sh @@ -4,8 +4,13 @@ set -euo pipefail -HEADER=${1:?missing constants.h path} -OUT_DIR=${2:?missing output directory} +if [ "$#" -lt 2 ]; then + echo "usage: $0 [header2.h ...] " >&2 + exit 1 +fi + +OUT_DIR=${*: -1} +HEADERS=("${@:1:$#-1}") PACKAGE_DIR="${OUT_DIR}/com/nvidia/cuopt/mathematicaloptimization" OUT_FILE="${PACKAGE_DIR}/CuOptConstants.java" @@ -17,7 +22,8 @@ mkdir -p "${PACKAGE_DIR}" echo " * SPDX-License-Identifier: Apache-2.0" echo " *" echo " * AUTO-GENERATED FILE. DO NOT EDIT." - echo " * Generated from cpp/include/cuopt/mathematical_optimization/constants.h." + echo " * Generated from cpp/include/cuopt/mathematical_optimization/constants.h and" + echo " * cpp/include/cuopt/status_codes.h." echo " * Run 'mvn generate-sources' from java/cuopt to regenerate." echo " */" echo "package com.nvidia.cuopt.mathematicaloptimization;" @@ -38,6 +44,6 @@ mkdir -p "${PACKAGE_DIR}" printf(" public static final String %s = %s;%s", name, value, "\n") } } - ' "${HEADER}" + ' "${HEADERS[@]}" echo "}" } > "${OUT_FILE}" From bb10b1c7d0cd8a4bb8a29e30a16936181da2ba9b Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 21:38:27 -0500 Subject: [PATCH 15/36] fix(ci): match per-component libcuopt* wheel paths in C example header lookup find_cuopt_libraries() only recognized /libcuopt/include/, so after the mathopt-dev component (and cuopt_c.h) moved into libcuopt-mathopt's own wheel, it fell through to a path-counting branch that lands one directory too deep, breaking wheel-tests-cuopt-server's C example build. --- ci/test_doc_examples.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ci/test_doc_examples.sh b/ci/test_doc_examples.sh index 5de8526a05..a5a7d10641 100755 --- a/ci/test_doc_examples.sh +++ b/ci/test_doc_examples.sh @@ -485,9 +485,10 @@ find_cuopt_libraries() { found_header=$(find "${search_dir}" -name "cuopt_c.h" -path "*/mathematical_optimization/*" 2>/dev/null | head -1) if [ -n "${found_header}" ]; then - # Check if this is a Python package installation (contains libcuopt/include) - if echo "${found_header}" | grep -q "/libcuopt/include/"; then - # Python package structure: /path/to/libcuopt/include/cuopt/mathematical_optimization/cuopt_c.h + # Check if this is a Python package installation (contains libcuopt*/include, e.g. + # libcuopt/ or the per-component libcuopt_client/, libcuopt_mathopt/, libcuopt_routing/) + if echo "${found_header}" | grep -qE "/libcuopt[a-z_]*/include/"; then + # Python package structure: /path/to/libcuopt*/include/cuopt/mathematical_optimization/cuopt_c.h # Extract the include directory by going up 3 directories from the header file include_path=$(dirname "$(dirname "$(dirname "${found_header}")")") else From 4f3657346b44c70e4de0c631e68bf82219a0d5ff Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 30 Sep 2026 21:59:38 -0500 Subject: [PATCH 16/36] build(wheel): build libcuopt-client/-mathopt/-routing/-libcuopt in dependency order Each component wheel recompiled the whole cpp/ tree from scratch and discarded everything but its own component, including a throwaway copy of cuopt_client. Mirror the real pip install graph instead: client builds first, mathopt/routing link against its published wheel via find_package(cuopt), and libcuopt links against all three the same way. CUOPT_FIND_{CLIENT,MATHOPT,ROUTING}_EXTERNALLY opt a component's CMake block out of being built locally once find_package(cuopt) has already resolved it from CMAKE_PREFIX_PATH. libcuopt-client now also ships the "cuopt" install component (cuopt-config.cmake), since it's the one wheel every later step always has installed. --- .github/workflows/build.yaml | 9 +++-- .github/workflows/pr.yaml | 48 +++++++++++++----------- ci/build_wheel_libcuopt.sh | 13 ++++++- ci/build_wheel_libcuopt_mathopt.sh | 5 ++- ci/build_wheel_libcuopt_routing.sh | 5 ++- cpp/CMakeLists.txt | 52 +++++++++++++++++++------- dependencies.yaml | 5 +++ python/libcuopt/pyproject.toml | 3 ++ python/libcuopt_client/pyproject.toml | 2 +- python/libcuopt_mathopt/pyproject.toml | 1 + python/libcuopt_routing/pyproject.toml | 1 + 11 files changed, 103 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6d66ada900..6b6c2c9a1c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -208,7 +208,8 @@ jobs: id-token: write packages: read pull-requests: read - needs: [build-details] + # Links against the client/mathopt/routing wheels' installed .so's instead of recompiling them. + needs: [build-details, wheel-build-libcuopt-client, wheel-build-libcuopt-mathopt, wheel-build-libcuopt-routing] secrets: inherit # zizmor: ignore[secrets-inherit] uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.10 with: @@ -291,7 +292,8 @@ jobs: id-token: write packages: read pull-requests: read - needs: [build-details] + # Links against the client wheel's installed libcuopt_client.so instead of recompiling it. + needs: [build-details, wheel-build-libcuopt-client] secrets: inherit # zizmor: ignore[secrets-inherit] uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.10 with: @@ -334,7 +336,8 @@ jobs: id-token: write packages: read pull-requests: read - needs: [build-details] + # Links against the client wheel's installed libcuopt_client.so instead of recompiling it. + needs: [build-details, wheel-build-libcuopt-client] secrets: inherit # zizmor: ignore[secrets-inherit] uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.10 with: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8f3a81e299..8d35f6c9cc 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -595,25 +595,6 @@ jobs: script: "ci/test_java.sh" artifact-name: "cuopt-java" file_to_upload: "java/cuopt/target/" - wheel-build-libcuopt: - needs: [build-details, compute-matrix-filters, changed-files] - if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels - permissions: - actions: read - contents: read - id-token: write - packages: read - pull-requests: read - secrets: inherit # zizmor: ignore[secrets-inherit] - uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.10 - with: - # build for every combination of arch and CUDA version, but only for the latest Python - matrix_filter: ${{ needs.compute-matrix-filters.outputs.libcuopt_filter }} - package-type: cpp - package-name: libcuopt - build_type: pull-request - build-datetime: ${{ needs.build-details.outputs.build-datetime }} - script: ci/build_wheel_libcuopt.sh wheel-build-libcuopt-client: needs: [build-details, compute-matrix-filters, changed-files] if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels @@ -635,7 +616,7 @@ jobs: build-datetime: ${{ needs.build-details.outputs.build-datetime }} script: ci/build_wheel_libcuopt_client.sh wheel-build-libcuopt-mathopt: - needs: [build-details, compute-matrix-filters, changed-files] + needs: [build-details, compute-matrix-filters, changed-files, wheel-build-libcuopt-client] if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels permissions: actions: read @@ -654,7 +635,7 @@ jobs: build-datetime: ${{ needs.build-details.outputs.build-datetime }} script: ci/build_wheel_libcuopt_mathopt.sh wheel-build-libcuopt-routing: - needs: [build-details, compute-matrix-filters, changed-files] + needs: [build-details, compute-matrix-filters, changed-files, wheel-build-libcuopt-client] if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels permissions: actions: read @@ -672,6 +653,31 @@ jobs: build_type: pull-request build-datetime: ${{ needs.build-details.outputs.build-datetime }} script: ci/build_wheel_libcuopt_routing.sh + wheel-build-libcuopt: + needs: + - build-details + - compute-matrix-filters + - changed-files + - wheel-build-libcuopt-client + - wheel-build-libcuopt-mathopt + - wheel-build-libcuopt-routing + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_wheels + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + secrets: inherit # zizmor: ignore[secrets-inherit] + uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@release/26.10 + with: + # build for every combination of arch and CUDA version, but only for the latest Python + matrix_filter: ${{ needs.compute-matrix-filters.outputs.libcuopt_filter }} + package-type: cpp + package-name: libcuopt + build_type: pull-request + build-datetime: ${{ needs.build-details.outputs.build-datetime }} + script: ci/build_wheel_libcuopt.sh wheel-build-cuopt: needs: - build-details diff --git a/ci/build_wheel_libcuopt.sh b/ci/build_wheel_libcuopt.sh index eb9a18d8ca..9ec403657a 100755 --- a/ci/build_wheel_libcuopt.sh +++ b/ci/build_wheel_libcuopt.sh @@ -9,6 +9,17 @@ source rapids-init-pip package_name="libcuopt" package_dir="python/libcuopt" +RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen "${RAPIDS_CUDA_VERSION}")" + +LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") +echo "libcuopt-client @ file://$(echo ${LIBCUOPT_CLIENT_WHEELHOUSE}/libcuopt_client-*.whl)" >> "${PIP_CONSTRAINT}" + +LIBCUOPT_MATHOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_mathopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") +echo "libcuopt-mathopt-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo ${LIBCUOPT_MATHOPT_WHEELHOUSE}/libcuopt_mathopt_*.whl)" >> "${PIP_CONSTRAINT}" + +LIBCUOPT_ROUTING_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_routing cuopt --cuda "$RAPIDS_CUDA_VERSION")") +echo "libcuopt-routing-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo ${LIBCUOPT_ROUTING_WHEELHOUSE}/libcuopt_routing_*.whl)" >> "${PIP_CONSTRAINT}" + # Install rockylinux repo if command -v dnf &> /dev/null; then bash ci/utils/update_rockylinux_repo.sh @@ -37,7 +48,7 @@ python ci/utils/install_modern_libgomp.py "${MODERN_LIBGOMP_DIR}" # Also build our own cuDSS threading layer against this same libgomp (see cpp/CMakeLists.txt, # cpp/src/barrier/cudss_mtlayer_cuopt.cpp), instead of cuDSS's prebuilt one, so both actually # share one instance, not just the same flavor. Conda keeps cuDSS's default (#1219 discussion). -export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON" +export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON;-DCUOPT_FIND_MATHOPT_EXTERNALLY=ON;-DCUOPT_FIND_ROUTING_EXTERNALLY=ON" # auditwheel repair does its own dependency resolution separately from the compiler; without # this it can't see our fetched copy and silently vendors the old Rocky 8 system one instead. diff --git a/ci/build_wheel_libcuopt_mathopt.sh b/ci/build_wheel_libcuopt_mathopt.sh index 8a86a4b50a..5263cdedbf 100755 --- a/ci/build_wheel_libcuopt_mathopt.sh +++ b/ci/build_wheel_libcuopt_mathopt.sh @@ -13,6 +13,9 @@ source rapids-init-pip package_name="libcuopt_mathopt" package_dir="python/libcuopt_mathopt" +LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") +echo "libcuopt-client @ file://$(echo ${LIBCUOPT_CLIENT_WHEELHOUSE}/libcuopt_client-*.whl)" >> "${PIP_CONSTRAINT}" + # Install rockylinux repo if command -v dnf &> /dev/null; then bash ci/utils/update_rockylinux_repo.sh @@ -41,7 +44,7 @@ python ci/utils/install_modern_libgomp.py "${MODERN_LIBGOMP_DIR}" # Also build our own cuDSS threading layer against this same libgomp (see cpp/CMakeLists.txt, # cpp/src/barrier/cudss_mtlayer_cuopt.cpp), instead of cuDSS's prebuilt one, so both actually # share one instance, not just the same flavor. Conda keeps cuDSS's default (#1219 discussion). -export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON" +export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON" # auditwheel repair does its own dependency resolution separately from the compiler; without # this it can't see our fetched copy and silently vendors the old Rocky 8 system one instead. diff --git a/ci/build_wheel_libcuopt_routing.sh b/ci/build_wheel_libcuopt_routing.sh index a5ec02ce09..c8ea0c80ee 100755 --- a/ci/build_wheel_libcuopt_routing.sh +++ b/ci/build_wheel_libcuopt_routing.sh @@ -13,6 +13,9 @@ source rapids-init-pip package_name="libcuopt_routing" package_dir="python/libcuopt_routing" +LIBCUOPT_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") +echo "libcuopt-client @ file://$(echo ${LIBCUOPT_CLIENT_WHEELHOUSE}/libcuopt_client-*.whl)" >> "${PIP_CONSTRAINT}" + # Install rockylinux repo if command -v dnf &> /dev/null; then bash ci/utils/update_rockylinux_repo.sh @@ -41,7 +44,7 @@ python ci/utils/install_modern_libgomp.py "${MODERN_LIBGOMP_DIR}" # Also build our own cuDSS threading layer against this same libgomp (see cpp/CMakeLists.txt, # cpp/src/barrier/cudss_mtlayer_cuopt.cpp), instead of cuDSS's prebuilt one, so both actually # share one instance, not just the same flavor. Conda keeps cuDSS's default (#1219 discussion). -export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON" +export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON" # auditwheel repair does its own dependency resolution separately from the compiler; without # this it can't see our fetched copy and silently vendors the old Rocky 8 system one instead. diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 20c29d726a..22e393fc9e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -736,6 +736,13 @@ set(CUOPT_PRIVATE_CUDA_LIBS get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) +option(CUOPT_FIND_CLIENT_EXTERNALLY "Resolve cuopt::client via find_package(cuopt) instead of building it" OFF) +option(CUOPT_FIND_MATHOPT_EXTERNALLY "Resolve cuopt::mathopt via find_package(cuopt) instead of building it" OFF) +option(CUOPT_FIND_ROUTING_EXTERNALLY "Resolve cuopt::routing via find_package(cuopt) instead of building it" OFF) +if(CUOPT_FIND_CLIENT_EXTERNALLY OR CUOPT_FIND_MATHOPT_EXTERNALLY OR CUOPT_FIND_ROUTING_EXTERNALLY) + find_package(cuopt "${RAPIDS_VERSION}" QUIET) +endif() + # - cuopt_client - CPU-only support library ---------------------------------------------------------- # # Host-side problem representation (parsers, data_model_view, mps_data_model, writers), the @@ -745,6 +752,7 @@ get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) # LANGUAGES is deliberately not CUDA: adding a .cu file here should fail loudly rather than # quietly reintroduce a CUDA dependency. Built as an OBJECT library first (mirrored below by # cuopt_routing_objs / cuopt_mathopt_objs) so cuopt_static can link the objects directly. +if(NOT (CUOPT_FIND_CLIENT_EXTERNALLY AND TARGET cuopt::client)) add_library(cuopt_client_objs OBJECT ${CUOPT_CLIENT_SRC_FILES}) # Default visibility, deliberately unlike the other *_objs: carved out of the internals rather # than designed as an export surface, so libcuopt resolves ~239 symbols from it. @@ -762,6 +770,7 @@ set_target_properties(cuopt_client_objs add_library(cuopt_client SHARED $) add_library(cuopt::cuopt_client ALIAS cuopt_client) +add_library(cuopt::client ALIAS cuopt_client) target_include_directories(cuopt_client PUBLIC @@ -852,6 +861,7 @@ target_link_libraries(cuopt_client $<$:protobuf::libprotobuf> $<$:gRPC::grpc++> ) +endif() # ################################################################################################## @@ -882,7 +892,7 @@ endif () # # OBJECT library first, mirroring cuopt_client_objs, so cuopt_static links the same compiled # objects instead of recompiling routing a second time. -if(NOT SKIP_ROUTING_BUILD) +if(NOT SKIP_ROUTING_BUILD AND NOT (CUOPT_FIND_ROUTING_EXTERNALLY AND TARGET cuopt::routing)) add_library(cuopt_routing_objs OBJECT ${CUOPT_ROUTING_SRC_FILES}) set_target_properties(cuopt_routing_objs PROPERTIES POSITION_INDEPENDENT_CODE ON @@ -912,7 +922,7 @@ if(NOT SKIP_ROUTING_BUILD) # library's PUBLIC link interface. target_link_libraries(cuopt_routing_objs PUBLIC - cuopt_client + cuopt::client rmm::rmm rapids_logger::rapids_logger CCCL::CCCL @@ -930,7 +940,7 @@ if(NOT SKIP_ROUTING_BUILD) # 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 + cuopt::client rmm::rmm rapids_logger::rapids_logger CCCL::CCCL @@ -945,6 +955,7 @@ endif() # cuopt_mathopt: LP / MIP / numerical optimization engine # # OBJECT library first, same reason as cuopt_routing_objs above. +if(NOT (CUOPT_FIND_MATHOPT_EXTERNALLY AND TARGET cuopt::mathopt)) add_library(cuopt_mathopt_objs OBJECT ${CUOPT_MATHOPT_SRC_FILES}) set_target_properties(cuopt_mathopt_objs PROPERTIES POSITION_INDEPENDENT_CODE ON @@ -999,7 +1010,7 @@ target_compile_definitions(cuopt_mathopt_objs # Repeated on cuopt_mathopt below -- see the note on cuopt_routing_objs above. target_link_libraries(cuopt_mathopt_objs PUBLIC - cuopt_client + cuopt::client rmm::rmm rapids_logger::rapids_logger CCCL::CCCL @@ -1042,7 +1053,7 @@ target_compile_definitions(cuopt_mathopt target_link_libraries(cuopt_mathopt PUBLIC # The parsers and the host-side problem representation live in cuopt_client. - cuopt_client + cuopt::client rmm::rmm rapids_logger::rapids_logger CCCL::CCCL @@ -1068,6 +1079,7 @@ if (TARGET KaMinPar) add_dependencies(cuopt_mathopt KaMinPar) endif () add_library(cuopt::mathopt ALIAS cuopt_mathopt) +endif() # ################################################################################################## @@ -1162,6 +1174,7 @@ configure_file( @ONLY ) +if(NOT TARGET cuopt::cuopt) add_library(cuopt INTERFACE) add_library(cuopt::cuopt ALIAS cuopt) @@ -1171,6 +1184,7 @@ target_include_directories(cuopt "$" "$" ) +endif() # Deliberately no component links. CMake will not export a target that references # targets outside its own export set, and each component exports separately now so its @@ -1189,8 +1203,11 @@ if (WRITE_FATBIN) ]=]) # Applies to the components that actually carry CUDA fatbins. Before the split this # was set on libcuopt.so, which held all the device code; it now holds none. - set(_CUOPT_FATBIN_TARGETS cuopt_mathopt) - if(NOT SKIP_ROUTING_BUILD) + set(_CUOPT_FATBIN_TARGETS "") + if(TARGET cuopt_mathopt) + list(APPEND _CUOPT_FATBIN_TARGETS cuopt_mathopt) + endif() + if(NOT SKIP_ROUTING_BUILD AND TARGET cuopt_routing) list(APPEND _CUOPT_FATBIN_TARGETS cuopt_routing) endif() foreach(_target ${_CUOPT_FATBIN_TARGETS}) @@ -1230,8 +1247,13 @@ else () set(_INCLUDE_DEST include/cuopt/) endif () -set(CUOPT_COMPONENT_TARGETS cuopt_mathopt cuopt_client) -if(NOT SKIP_ROUTING_BUILD) +set(CUOPT_COMPONENT_TARGETS "") +foreach(_c cuopt_mathopt cuopt_client) + if(TARGET ${_c}) + list(APPEND CUOPT_COMPONENT_TARGETS ${_c}) + endif() +endforeach() +if(NOT SKIP_ROUTING_BUILD AND TARGET cuopt_routing) list(APPEND CUOPT_COMPONENT_TARGETS cuopt_routing) endif() @@ -1277,9 +1299,11 @@ endforeach() # only -- CMake requires everything an exported target references to share its export set, # which would defeat the split -- so the installed config synthesises them instead, from # whichever components that installation actually has. See the FINAL_CODE_BLOCK below. +if(TARGET cuopt) install(TARGETS cuopt EXPORT cuopt-exports ) +endif() # 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. @@ -1409,6 +1433,7 @@ set_property(TARGET cuopt::cuopt APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${_cuopt_umbrella_links}") ]=]) +if(TARGET cuopt) rapids_export(INSTALL cuopt EXPORT_SET cuopt-exports GLOBAL_TARGETS ${CUOPT_COMPONENT_EXPORT_NAMES} @@ -1426,6 +1451,7 @@ rapids_export(BUILD cuopt DOCUMENTATION doc_string FINAL_CODE_BLOCK cuopt_code_string ) +endif() # ################################################################################################## # - make documentation ---------------------------------------------------------------------------- @@ -1487,8 +1513,8 @@ if (NOT BUILD_LP_ONLY) # because --no-copy-dt-needed-entries will not let it reach client through mathopt. target_link_libraries(cuopt_cli PUBLIC - cuopt_mathopt - cuopt_client + cuopt::mathopt + cuopt::client OpenMP::OpenMP_CXX ${CUDSS_LIBRARIES} TBB::tbb @@ -1647,8 +1673,8 @@ if (NOT SKIP_GRPC_BUILD) # both VRP and LP/MIP, so the dependency is now explicit. target_link_libraries(cuopt_grpc_server PUBLIC - cuopt_mathopt - $<$>:cuopt_routing> + cuopt::mathopt + $<$>:cuopt::routing> OpenMP::OpenMP_CXX PRIVATE protobuf::libprotobuf diff --git a/dependencies.yaml b/dependencies.yaml index 31c1b49d58..4dfcb0556f 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -136,6 +136,9 @@ files: key: requires includes: - build_common + - depends_on_libcuopt_client + - depends_on_libcuopt_mathopt + - depends_on_libcuopt_routing - depends_on_libraft_headers - depends_on_librmm - depends_on_rapids_logger @@ -206,6 +209,7 @@ files: key: requires includes: - build_common + - depends_on_libcuopt_client - depends_on_libraft_headers - depends_on_librmm - depends_on_rapids_logger @@ -241,6 +245,7 @@ files: key: requires includes: - build_common + - depends_on_libcuopt_client - depends_on_libraft_headers - depends_on_librmm - depends_on_rapids_logger diff --git a/python/libcuopt/pyproject.toml b/python/libcuopt/pyproject.toml index bd3d8fbe50..29135bfd3d 100644 --- a/python/libcuopt/pyproject.toml +++ b/python/libcuopt/pyproject.toml @@ -81,6 +81,9 @@ dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=4.0", + "libcuopt-client==26.10.*,>=0.0.0a0", + "libcuopt-mathopt==26.10.*,>=0.0.0a0", + "libcuopt-routing==26.10.*,>=0.0.0a0", "librmm==26.10.*,>=0.0.0a0", "ninja", "rapids-logger==0.3.*", diff --git a/python/libcuopt_client/pyproject.toml b/python/libcuopt_client/pyproject.toml index 05803e0e59..7c800d50cc 100644 --- a/python/libcuopt_client/pyproject.toml +++ b/python/libcuopt_client/pyproject.toml @@ -57,7 +57,7 @@ wheel.install-dir = "libcuopt_client" wheel.py-api = "py3" # The whole point of this package: stage only this component's install rules, # so the wheel carries one library instead of all three. -install.components = ["client", "client-dev"] +install.components = ["client", "client-dev", "cuopt"] [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" diff --git a/python/libcuopt_mathopt/pyproject.toml b/python/libcuopt_mathopt/pyproject.toml index 481b2864df..23b64eb67b 100644 --- a/python/libcuopt_mathopt/pyproject.toml +++ b/python/libcuopt_mathopt/pyproject.toml @@ -80,6 +80,7 @@ dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=4.0", + "libcuopt-client==26.10.*,>=0.0.0a0", "librmm==26.10.*,>=0.0.0a0", "ninja", "rapids-logger==0.3.*", diff --git a/python/libcuopt_routing/pyproject.toml b/python/libcuopt_routing/pyproject.toml index 5e1ac15d61..9af1c2174b 100644 --- a/python/libcuopt_routing/pyproject.toml +++ b/python/libcuopt_routing/pyproject.toml @@ -74,6 +74,7 @@ dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=4.0", + "libcuopt-client==26.10.*,>=0.0.0a0", "librmm==26.10.*,>=0.0.0a0", "ninja", "rapids-logger==0.3.*", From 01bdee6b34e8a7db939722ed5b7defecfaa5bd6c Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 09:32:04 -0500 Subject: [PATCH 17/36] fix(wheel): stop the per-package CMakeLists.txt short-circuit from false-tripping Each python/libcuopt*/CMakeLists.txt returns early when find_package(cuopt) already resolves cuopt, to skip rebuilding when a full install is already present. Now that libcuopt-client ships cuopt-config.cmake (needed for mathopt/routing/libcuopt to resolve it as a build-time dependency), that generic check trips the moment the sibling wheel is installed, even though this package's own target was never built -- producing an empty wheel. Check the specific target (or binary, for libcuopt) each package is actually responsible for instead. Also fixes find_package(cuopt) running a second time from cpp/CMakeLists.txt when the wrapper already resolved it: re-including the targets file from a non-origin directory trips CMake's IMPORTED_GLOBAL promotion check. --- cpp/CMakeLists.txt | 3 ++- python/libcuopt/CMakeLists.txt | 15 ++++++--------- python/libcuopt_client/CMakeLists.txt | 6 ++++-- python/libcuopt_mathopt/CMakeLists.txt | 6 ++++-- python/libcuopt_routing/CMakeLists.txt | 6 ++++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 22e393fc9e..8cf8ebfacd 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -739,7 +739,8 @@ get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) option(CUOPT_FIND_CLIENT_EXTERNALLY "Resolve cuopt::client via find_package(cuopt) instead of building it" OFF) option(CUOPT_FIND_MATHOPT_EXTERNALLY "Resolve cuopt::mathopt via find_package(cuopt) instead of building it" OFF) option(CUOPT_FIND_ROUTING_EXTERNALLY "Resolve cuopt::routing via find_package(cuopt) instead of building it" OFF) -if(CUOPT_FIND_CLIENT_EXTERNALLY OR CUOPT_FIND_MATHOPT_EXTERNALLY OR CUOPT_FIND_ROUTING_EXTERNALLY) +if((CUOPT_FIND_CLIENT_EXTERNALLY OR CUOPT_FIND_MATHOPT_EXTERNALLY OR CUOPT_FIND_ROUTING_EXTERNALLY) + AND NOT TARGET cuopt::client AND NOT TARGET cuopt::mathopt AND NOT TARGET cuopt::routing) find_package(cuopt "${RAPIDS_VERSION}" QUIET) endif() diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 4c9a2b47ce..577f92b163 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -15,17 +15,14 @@ project( LANGUAGES CXX CUDA ) -# Debug: Print CUDA compiler version early -message(STATUS "=============================================================") -message(STATUS "libcuopt: CMAKE_CUDA_COMPILER_VERSION = ${CMAKE_CUDA_COMPILER_VERSION}") -message(STATUS "libcuopt: CUDAToolkit_VERSION = ${CUDAToolkit_VERSION}") -message(STATUS "=============================================================") - -# Check if cuopt is already available. If so, it is the user's responsibility to ensure that the -# CMake package is also available at build time of the Python cuopt package. +# Already installed: nothing to build, the wheel just stages the component. Checking for +# the cuopt_grpc_server binary itself, not cuopt_FOUND, matters now that this build installs +# the client/mathopt/routing sibling wheels as dependencies -- those alone resolve cuopt_FOUND +# but not a prebuilt cuopt_grpc_server. find_package(cuopt "${RAPIDS_VERSION}") +find_program(CUOPT_GRPC_SERVER_PREBUILT cuopt_grpc_server) -if(cuopt_FOUND) +if(cuopt_FOUND AND CUOPT_GRPC_SERVER_PREBUILT) return() endif() diff --git a/python/libcuopt_client/CMakeLists.txt b/python/libcuopt_client/CMakeLists.txt index e5fde2401b..f9fedc5e74 100644 --- a/python/libcuopt_client/CMakeLists.txt +++ b/python/libcuopt_client/CMakeLists.txt @@ -15,9 +15,11 @@ project( LANGUAGES CXX CUDA ) -# Already installed: nothing to build, the wheel just stages the component. +# Already installed: nothing to build, the wheel just stages the component. Checking +# cuopt::client specifically, not cuopt_FOUND, matters now that libcuopt-client ships +# cuopt-config.cmake: a sibling wheel resolving it must not short-circuit this build. find_package(cuopt "${RAPIDS_VERSION}") -if(cuopt_FOUND) +if(TARGET cuopt::client) return() endif() unset(cuopt_FOUND) diff --git a/python/libcuopt_mathopt/CMakeLists.txt b/python/libcuopt_mathopt/CMakeLists.txt index 0ebf0ad09d..9c35df545d 100644 --- a/python/libcuopt_mathopt/CMakeLists.txt +++ b/python/libcuopt_mathopt/CMakeLists.txt @@ -15,9 +15,11 @@ project( LANGUAGES CXX CUDA ) -# Already installed: nothing to build, the wheel just stages the component. +# Already installed: nothing to build, the wheel just stages the component. Checking +# cuopt::mathopt specifically, not cuopt_FOUND, matters now that this build installs the +# client sibling wheel as a dependency -- that alone resolves cuopt_FOUND but not mathopt. find_package(cuopt "${RAPIDS_VERSION}") -if(cuopt_FOUND) +if(TARGET cuopt::mathopt) return() endif() unset(cuopt_FOUND) diff --git a/python/libcuopt_routing/CMakeLists.txt b/python/libcuopt_routing/CMakeLists.txt index 03afc376da..953573b8d3 100644 --- a/python/libcuopt_routing/CMakeLists.txt +++ b/python/libcuopt_routing/CMakeLists.txt @@ -15,9 +15,11 @@ project( LANGUAGES CXX CUDA ) -# Already installed: nothing to build, the wheel just stages the component. +# Already installed: nothing to build, the wheel just stages the component. Checking +# cuopt::routing specifically, not cuopt_FOUND, matters now that this build installs the +# client sibling wheel as a dependency -- that alone resolves cuopt_FOUND but not routing. find_package(cuopt "${RAPIDS_VERSION}") -if(cuopt_FOUND) +if(TARGET cuopt::routing) return() endif() unset(cuopt_FOUND) From 6e2865726d6d9b98c6dd3550fc03a1a448afd28f Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 10:08:41 -0500 Subject: [PATCH 18/36] fix(wheel): ship Unspecified in libcuopt-client too, for the same reason as libcuopt mathopt/routing now resolve cuopt-dependencies.cmake from the client wheel, whose find_dependency(rmm) call needs the same rapids-cmake CCCL "found package" redirect files libcuopt's own Unspecified component already carries (#2013) -- without them it falls through to librmm's own bundled copy, which calls a CMake command (libcudacxx_update_language_compat_flags) that doesn't exist in this CCCL version. --- python/libcuopt_client/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/libcuopt_client/pyproject.toml b/python/libcuopt_client/pyproject.toml index 7c800d50cc..51a630f291 100644 --- a/python/libcuopt_client/pyproject.toml +++ b/python/libcuopt_client/pyproject.toml @@ -57,7 +57,7 @@ wheel.install-dir = "libcuopt_client" wheel.py-api = "py3" # The whole point of this package: stage only this component's install rules, # so the wheel carries one library instead of all three. -install.components = ["client", "client-dev", "cuopt"] +install.components = ["client", "client-dev", "cuopt", "Unspecified"] [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" From a147831f2afc4186dee6047a2e432fa443920501 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 11:30:58 -0500 Subject: [PATCH 19/36] fix(wheel): generate the gRPC proto headers even when cuopt_client isn't built locally cuopt_grpc_server only links the generated protobuf symbols from cuopt_client.so, it never compiles the .pb.cc files itself -- the headers were only ever generated as a side effect of compiling cuopt_client_objs. With CUOPT_FIND_CLIENT_EXTERNALLY resolving cuopt::client externally, client_objs is skipped, so nothing triggered the protoc custom commands and cuopt_remote.pb.h was missing when grpc_server_types.hpp included it. --- cpp/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 8cf8ebfacd..927a30fef9 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -672,6 +672,12 @@ if (NOT SKIP_GRPC_BUILD) set_property(SOURCE ${GRPC_PROTO_GENERATED_FILES} DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} APPEND PROPERTY COMPILE_OPTIONS "$<$:-fvisibility=default>") + # cuopt_grpc_server only needs the generated headers (it links the generated symbols from + # cuopt_client.so rather than compiling the protos itself), but those headers are otherwise + # only generated as a side effect of compiling cuopt_client_objs, which is skipped when + # CUOPT_FIND_CLIENT_EXTERNALLY resolves cuopt::client externally. + add_custom_target(cuopt_grpc_proto_headers DEPENDS ${GRPC_PROTO_GENERATED_FILES}) + endif (NOT SKIP_GRPC_BUILD) # ################################################################################################## @@ -1635,6 +1641,7 @@ if (NOT SKIP_GRPC_BUILD) src/grpc/server/grpc_job_management.cpp src/grpc/server/grpc_service_impl.cpp ) + add_dependencies(cuopt_grpc_server cuopt_grpc_proto_headers) set_target_properties(cuopt_grpc_server PROPERTIES From eea1c184cbe5dd0a1c9509a4146c76f74024b734 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 13:15:57 -0500 Subject: [PATCH 20/36] fix(docs): add status_codes.h to the Doxygen INPUT list constants.h's status-code defines moved to status_codes.h earlier in this PR, but Doxygen's INPUT list was never updated, so doxygendefine directives for CUOPT_MPS_FILE_ERROR etc. in convex-c-api.rst could no longer find them, failing the warnings-as-errors docs build. --- cpp/doxygen/Doxyfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/doxygen/Doxyfile b/cpp/doxygen/Doxyfile index 8bd2dfa906..5df5653fb9 100644 --- a/cpp/doxygen/Doxyfile +++ b/cpp/doxygen/Doxyfile @@ -773,7 +773,8 @@ WARN_LOGFILE = INPUT = main_page.md \ ../include/cuopt/mathematical_optimization/cuopt_c.h \ - ../include/cuopt/mathematical_optimization/constants.h + ../include/cuopt/mathematical_optimization/constants.h \ + ../include/cuopt/status_codes.h # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses From 1b0f97bb8670bf5c062cf6ebb917baf7093c6503 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 14:33:37 -0500 Subject: [PATCH 21/36] perf(wheel): skip compiling the routing engine in libcuopt-mathopt's build The mathopt wheel only stages the mathopt/mathopt-dev components, so the routing engine it was compiling in full was never shipped. SKIP_ROUTING_BUILD already exists and is fully wired through (including cuopt_grpc_server's conditional cuopt::routing link); mathopt's build script just never passed it. --- ci/build_wheel_libcuopt_mathopt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/build_wheel_libcuopt_mathopt.sh b/ci/build_wheel_libcuopt_mathopt.sh index 5263cdedbf..8b5405d72e 100755 --- a/ci/build_wheel_libcuopt_mathopt.sh +++ b/ci/build_wheel_libcuopt_mathopt.sh @@ -44,7 +44,7 @@ python ci/utils/install_modern_libgomp.py "${MODERN_LIBGOMP_DIR}" # Also build our own cuDSS threading layer against this same libgomp (see cpp/CMakeLists.txt, # cpp/src/barrier/cudss_mtlayer_cuopt.cpp), instead of cuDSS's prebuilt one, so both actually # share one instance, not just the same flavor. Conda keeps cuDSS's default (#1219 discussion). -export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON" +export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON;-DSKIP_ROUTING_BUILD=ON" # auditwheel repair does its own dependency resolution separately from the compiler; without # this it can't see our fetched copy and silently vendors the old Rocky 8 system one instead. From dac462798fd33a0e79c069c16b1526ee9f31f7a4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 15:16:21 -0500 Subject: [PATCH 22/36] perf(wheel): skip compiling the mathopt engine in libcuopt-routing's build Mirrors SKIP_ROUTING_BUILD's existing pattern: SKIP_MATHOPT_BUILD gates cuopt_mathopt_objs/cuopt_mathopt, cuopt_cli (mathopt-only tool), and the static lib / linker script / CPack component lists the same way. routing's wheel never shipped any of these, so this was pure wasted compute. cuopt_grpc_server can't support the same split -- grpc_worker.cpp calls into the mathopt engine with no SKIP_ROUTING_BUILD-style opt-out -- so it now fails configure loudly if SKIP_MATHOPT_BUILD is set without SKIP_GRPC_BUILD; routing's wheel sets both, matching that neither wheel ships grpc-server. --- ci/build_wheel_libcuopt_mathopt.sh | 2 +- ci/build_wheel_libcuopt_routing.sh | 2 +- cpp/CMakeLists.txt | 29 +++++++++++++++++++++++------ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/ci/build_wheel_libcuopt_mathopt.sh b/ci/build_wheel_libcuopt_mathopt.sh index 8b5405d72e..9063dfe343 100755 --- a/ci/build_wheel_libcuopt_mathopt.sh +++ b/ci/build_wheel_libcuopt_mathopt.sh @@ -44,7 +44,7 @@ python ci/utils/install_modern_libgomp.py "${MODERN_LIBGOMP_DIR}" # Also build our own cuDSS threading layer against this same libgomp (see cpp/CMakeLists.txt, # cpp/src/barrier/cudss_mtlayer_cuopt.cpp), instead of cuDSS's prebuilt one, so both actually # share one instance, not just the same flavor. Conda keeps cuDSS's default (#1219 discussion). -export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON;-DSKIP_ROUTING_BUILD=ON" +export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON;-DSKIP_ROUTING_BUILD=ON;-DSKIP_GRPC_BUILD=ON" # auditwheel repair does its own dependency resolution separately from the compiler; without # this it can't see our fetched copy and silently vendors the old Rocky 8 system one instead. diff --git a/ci/build_wheel_libcuopt_routing.sh b/ci/build_wheel_libcuopt_routing.sh index c8ea0c80ee..6111986054 100755 --- a/ci/build_wheel_libcuopt_routing.sh +++ b/ci/build_wheel_libcuopt_routing.sh @@ -44,7 +44,7 @@ python ci/utils/install_modern_libgomp.py "${MODERN_LIBGOMP_DIR}" # Also build our own cuDSS threading layer against this same libgomp (see cpp/CMakeLists.txt, # cpp/src/barrier/cudss_mtlayer_cuopt.cpp), instead of cuDSS's prebuilt one, so both actually # share one instance, not just the same flavor. Conda keeps cuDSS's default (#1219 discussion). -export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON" +export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON;-DSKIP_MATHOPT_BUILD=ON;-DSKIP_GRPC_BUILD=ON" # auditwheel repair does its own dependency resolution separately from the compiler; without # this it can't see our fetched copy and silently vendors the old Rocky 8 system one instead. diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 927a30fef9..9327a21c17 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -52,6 +52,7 @@ option(BUILD_LP_ONLY "Build only linear programming components, exclude routing option(BUILD_MIP_BENCHMARKS "Build MIP benchmarks" OFF) option(SKIP_C_PYTHON_ADAPTERS "Skip building C and Python adapter files (cython_solve.cu and cuopt_c.cpp)" OFF) option(SKIP_ROUTING_BUILD "Skip building routing components" OFF) +option(SKIP_MATHOPT_BUILD "Skip building mathopt (LP/MIP) components" OFF) option(SKIP_GRPC_BUILD "Skip building gRPC and protobuf components" OFF) option(CUOPT_BUILD_STATIC_LIB "Build libcuopt_static.a for embedding into a self-contained consumer" OFF) option(WRITE_FATBIN "Enable fatbin writing" ON) @@ -64,6 +65,7 @@ message(VERBOSE "cuOpt: Disable OpenMP: ${DISABLE_OPENMP}") message(VERBOSE "cuOpt: Build LP-only mode: ${BUILD_LP_ONLY}") message(VERBOSE "cuOpt: Skip C/Python adapters: ${SKIP_C_PYTHON_ADAPTERS}") message(VERBOSE "cuOpt: Skip routing build: ${SKIP_ROUTING_BUILD}") +message(VERBOSE "cuOpt: Skip mathopt build: ${SKIP_MATHOPT_BUILD}") message(VERBOSE "cuOpt: Build with debug line information for host code: ${HOST_LINEINFO}") message(VERBOSE "cuOpt: fatbin: ${WRITE_FATBIN}") @@ -959,10 +961,10 @@ if(NOT SKIP_ROUTING_BUILD AND NOT (CUOPT_FIND_ROUTING_EXTERNALLY AND TARGET cuop add_library(cuopt::routing ALIAS cuopt_routing) endif() -# cuopt_mathopt: LP / MIP / numerical optimization engine +# cuopt_mathopt: LP / MIP / numerical optimization engine (omitted when SKIP_MATHOPT_BUILD=ON) # # OBJECT library first, same reason as cuopt_routing_objs above. -if(NOT (CUOPT_FIND_MATHOPT_EXTERNALLY AND TARGET cuopt::mathopt)) +if(NOT SKIP_MATHOPT_BUILD AND NOT (CUOPT_FIND_MATHOPT_EXTERNALLY AND TARGET cuopt::mathopt)) add_library(cuopt_mathopt_objs OBJECT ${CUOPT_MATHOPT_SRC_FILES}) set_target_properties(cuopt_mathopt_objs PROPERTIES POSITION_INDEPENDENT_CODE ON @@ -1106,9 +1108,12 @@ if (BUILD_TESTS OR (BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY) OR CUOPT_BUILD_S if(NOT SKIP_ROUTING_BUILD) set(_cuopt_static_routing_objs $) endif() + if(NOT SKIP_MATHOPT_BUILD) + set(_cuopt_static_mathopt_objs $) + endif() add_library(cuopt_static STATIC $ - $ + ${_cuopt_static_mathopt_objs} ${_cuopt_static_routing_objs} ) target_link_libraries(cuopt_static @@ -1169,7 +1174,10 @@ endif (BUILD_TESTS) # 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") +set(CUOPT_LINKER_SCRIPT_INPUTS "libcuopt_client.so") +if(NOT SKIP_MATHOPT_BUILD) + string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_mathopt.so") +endif() if(NOT SKIP_ROUTING_BUILD) string(APPEND CUOPT_LINKER_SCRIPT_INPUTS " libcuopt_routing.so") endif() @@ -1232,7 +1240,10 @@ set(CPACK_DEB_COMPONENT_INSTALL ON) # runtime .deb empty now that nothing installs under `runtime`. # `cuopt` is rapids_export's own component, holding cuopt-config.cmake and the export # set. It is easy to miss because it is named after the project rather than a library. -set(CPACK_COMPONENTS_ALL runtime dev cuopt client mathopt client-dev mathopt-dev) +set(CPACK_COMPONENTS_ALL runtime dev cuopt client client-dev) +if(NOT SKIP_MATHOPT_BUILD) + list(APPEND CPACK_COMPONENTS_ALL mathopt mathopt-dev) +endif() if(NOT SKIP_ROUTING_BUILD) list(APPEND CPACK_COMPONENTS_ALL routing routing-dev) endif() @@ -1487,7 +1498,7 @@ rapids_cpm_find( GIT_SHALLOW TRUE ) -if (NOT BUILD_LP_ONLY) +if (NOT BUILD_LP_ONLY AND NOT SKIP_MATHOPT_BUILD) add_executable(cuopt_cli cuopt_cli.cpp) # PIE executable: auditwheel/patchelf expands .dynstr/RPATH when repairing wheels; non-PIE @@ -1676,6 +1687,12 @@ if (NOT SKIP_GRPC_BUILD) find_library(UUID_LIBRARY uuid REQUIRED) + # grpc_worker.cpp calls into the mathopt engine unconditionally, unlike routing which has + # a SKIP_ROUTING_BUILD opt-out. A routing-only wheel should set SKIP_GRPC_BUILD=ON instead. + if (SKIP_MATHOPT_BUILD) + message(FATAL_ERROR "cuopt_grpc_server requires mathopt; set SKIP_GRPC_BUILD=ON too") + endif () + # 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. From 5fbe2768022c408e0d301f540e724c603bd8ec35 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 15:48:15 -0500 Subject: [PATCH 23/36] fix(wheel): guard cuopt_wheel_build.cmake's cuopt_cli link for SKIP_MATHOPT_BUILD cuopt_wheel_build.cmake unconditionally linked argparse into cuopt_cli, but cuopt_cli doesn't exist when SKIP_MATHOPT_BUILD=ON -- routing's wheel build never exercised this path through the actual wheel wrapper before now. --- python/cmake/cuopt_wheel_build.cmake | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/python/cmake/cuopt_wheel_build.cmake b/python/cmake/cuopt_wheel_build.cmake index 4c1e681ebb..2da53a4805 100644 --- a/python/cmake/cuopt_wheel_build.cmake +++ b/python/cmake/cuopt_wheel_build.cmake @@ -41,10 +41,12 @@ add_subdirectory(../../cpp cuopt-cpp) # 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 -) +# argparse::argparse in cpp/CMakeLists.txt. cuopt_cli doesn't exist when SKIP_MATHOPT_BUILD=ON. +if (TARGET cuopt_cli) + target_link_libraries(cuopt_cli PRIVATE + argparse + ) +endif () set(rpaths "$ORIGIN/../lib64" From 49c18bb4dc26f799cfe7630d351c58ba0ba6b37f Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 17:11:44 -0500 Subject: [PATCH 24/36] fix(wheel): revert SKIP_GRPC_BUILD for libcuopt-mathopt, it breaks remote execution cuopt_mathopt's own solve entry points (mip_heuristics/solve.cu, pdlp/solve.cu) have #ifdef CUOPT_ENABLE_GRPC-gated remote-execution dispatch built directly into the engine, unlike routing (which has no such path -- grep confirms zero CUOPT_ENABLE_GRPC references under cpp/src/routing/). SKIP_GRPC_BUILD=ON silently compiled that capability out, confirmed by real GPU test failures ("Remote execution requires cuOpt built with gRPC support") on rtxpro6000 runners. SKIP_ROUTING_BUILD stays; it doesn't affect this. --- ci/build_wheel_libcuopt_mathopt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/build_wheel_libcuopt_mathopt.sh b/ci/build_wheel_libcuopt_mathopt.sh index 9063dfe343..8b5405d72e 100755 --- a/ci/build_wheel_libcuopt_mathopt.sh +++ b/ci/build_wheel_libcuopt_mathopt.sh @@ -44,7 +44,7 @@ python ci/utils/install_modern_libgomp.py "${MODERN_LIBGOMP_DIR}" # Also build our own cuDSS threading layer against this same libgomp (see cpp/CMakeLists.txt, # cpp/src/barrier/cudss_mtlayer_cuopt.cpp), instead of cuDSS's prebuilt one, so both actually # share one instance, not just the same flavor. Conda keeps cuDSS's default (#1219 discussion). -export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON;-DSKIP_ROUTING_BUILD=ON;-DSKIP_GRPC_BUILD=ON" +export SKBUILD_CMAKE_ARGS="-DOpenMP_gomp_LIBRARY:FILEPATH=${MODERN_LIBGOMP_DIR}/libgomp.so.1.0.0;-DCUOPT_BUILD_CUSTOM_CUDSS_MTLAYER=ON;-DCUOPT_FIND_CLIENT_EXTERNALLY=ON;-DSKIP_ROUTING_BUILD=ON" # auditwheel repair does its own dependency resolution separately from the compiler; without # this it can't see our fetched copy and silently vendors the old Rocky 8 system one instead. From b77b7c28a1153b4cf860c04160a4491fa6540552 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 17:17:47 -0500 Subject: [PATCH 25/36] perf(wheel): cache the gRPC/Abseil/Protobuf source build with sccache install_protobuf_grpc.sh's own cmake invocations ran with no compiler launcher configured, so every libcuopt* wheel build recompiled gRPC from source uncached on every single run. ci/build_wheel.sh sources rapids-configure-sccache too, but only after this step; source it earlier so CMAKE_{C,CXX,CUDA}_COMPILER_LAUNCHER is already set when install_protobuf_grpc.sh's builds pick it up. --- ci/build_wheel_libcuopt.sh | 3 +++ ci/build_wheel_libcuopt_client.sh | 3 +++ ci/build_wheel_libcuopt_mathopt.sh | 3 +++ ci/build_wheel_libcuopt_routing.sh | 3 +++ 4 files changed, 12 insertions(+) diff --git a/ci/build_wheel_libcuopt.sh b/ci/build_wheel_libcuopt.sh index 9ec403657a..90cc41a599 100755 --- a/ci/build_wheel_libcuopt.sh +++ b/ci/build_wheel_libcuopt.sh @@ -36,6 +36,9 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi +# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. +source rapids-configure-sccache + # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) bash ci/utils/install_protobuf_grpc.sh diff --git a/ci/build_wheel_libcuopt_client.sh b/ci/build_wheel_libcuopt_client.sh index e561fd271b..2ac2129c6e 100755 --- a/ci/build_wheel_libcuopt_client.sh +++ b/ci/build_wheel_libcuopt_client.sh @@ -29,6 +29,9 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi +# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. +source rapids-configure-sccache + # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) bash ci/utils/install_protobuf_grpc.sh diff --git a/ci/build_wheel_libcuopt_mathopt.sh b/ci/build_wheel_libcuopt_mathopt.sh index 8b5405d72e..f35b3a15d4 100755 --- a/ci/build_wheel_libcuopt_mathopt.sh +++ b/ci/build_wheel_libcuopt_mathopt.sh @@ -32,6 +32,9 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi +# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. +source rapids-configure-sccache + # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) bash ci/utils/install_protobuf_grpc.sh diff --git a/ci/build_wheel_libcuopt_routing.sh b/ci/build_wheel_libcuopt_routing.sh index 6111986054..aee0fece74 100755 --- a/ci/build_wheel_libcuopt_routing.sh +++ b/ci/build_wheel_libcuopt_routing.sh @@ -32,6 +32,9 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi +# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. +source rapids-configure-sccache + # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) bash ci/utils/install_protobuf_grpc.sh From 45562d0a8a20bd9ca20b925361f9ff3f3a0a2d9e Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 17:18:33 -0500 Subject: [PATCH 26/36] style: drop comment on rapids-configure-sccache lines --- ci/build_wheel_libcuopt.sh | 1 - ci/build_wheel_libcuopt_client.sh | 1 - ci/build_wheel_libcuopt_mathopt.sh | 1 - ci/build_wheel_libcuopt_routing.sh | 1 - 4 files changed, 4 deletions(-) diff --git a/ci/build_wheel_libcuopt.sh b/ci/build_wheel_libcuopt.sh index 90cc41a599..23175f93aa 100755 --- a/ci/build_wheel_libcuopt.sh +++ b/ci/build_wheel_libcuopt.sh @@ -36,7 +36,6 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi -# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. source rapids-configure-sccache # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) diff --git a/ci/build_wheel_libcuopt_client.sh b/ci/build_wheel_libcuopt_client.sh index 2ac2129c6e..fd42997708 100755 --- a/ci/build_wheel_libcuopt_client.sh +++ b/ci/build_wheel_libcuopt_client.sh @@ -29,7 +29,6 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi -# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. source rapids-configure-sccache # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) diff --git a/ci/build_wheel_libcuopt_mathopt.sh b/ci/build_wheel_libcuopt_mathopt.sh index f35b3a15d4..0a7308906c 100755 --- a/ci/build_wheel_libcuopt_mathopt.sh +++ b/ci/build_wheel_libcuopt_mathopt.sh @@ -32,7 +32,6 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi -# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. source rapids-configure-sccache # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) diff --git a/ci/build_wheel_libcuopt_routing.sh b/ci/build_wheel_libcuopt_routing.sh index aee0fece74..3388809077 100755 --- a/ci/build_wheel_libcuopt_routing.sh +++ b/ci/build_wheel_libcuopt_routing.sh @@ -32,7 +32,6 @@ elif command -v apt-get &> /dev/null; then apt-get install -y uuid-dev fi -# ci/build_wheel.sh sources this too, but only after install_protobuf_grpc.sh already ran. source rapids-configure-sccache # Install Protobuf + gRPC (protoc + grpc_cpp_plugin) From 2031cd2184c628899e9c1259712da3f9b665cc3b Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 18:49:55 -0500 Subject: [PATCH 27/36] fix(ci): find status_codes.h from its own wheel for the C examples build constants.h (libcuopt-mathopt) includes status_codes.h, which lives in a different wheel (libcuopt-client) since the earlier split. find_cuopt_libraries() only ever added one -I for wherever cuopt_c.h was found, so the C examples failed with "cuopt/status_codes.h: No such file or directory" once mathopt's own include tree stopped containing it. Search for it separately and pass its directory as a second -I via a new EXTRA_CFLAGS Makefile variable. --- ci/test_doc_examples.sh | 21 +++++++++++++++++-- .../source/cuopt-c/convex/examples/Makefile | 2 +- .../source/cuopt-c/mip/examples/Makefile | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ci/test_doc_examples.sh b/ci/test_doc_examples.sh index a5a7d10641..3dd2a40fdb 100755 --- a/ci/test_doc_examples.sh +++ b/ci/test_doc_examples.sh @@ -364,6 +364,7 @@ test_c_examples() { local make_vars="" [ -n "${include_path}" ] && make_vars="${make_vars} INCLUDE_PATH=${include_path}" [ -n "${lib_path}" ] && make_vars="${make_vars} LIBCUOPT_LIBRARY_PATH=${lib_path}" + [ -n "${client_include_path}" ] && make_vars="${make_vars} EXTRA_CFLAGS=-I${client_include_path}" if make clean > "${RESULTS_DIR}/c-clean-${relative_path//\//_}.log" 2>&1 && \ make ${make_vars} all > "${RESULTS_DIR}/c-build-${relative_path//\//_}.log" 2>&1; then @@ -461,6 +462,7 @@ find_cuopt_libraries() { # Reset global variables include_path="" lib_path="" + client_include_path="" # Get Python site-packages directory local site_packages="" @@ -508,8 +510,22 @@ find_cuopt_libraries() { fi fi - # Break early if both found - if [ -n "${include_path}" ] && [ -n "${lib_path}" ]; then + # status_codes.h lives in libcuopt-client's own wheel, a different package than + # cuopt_c.h (libcuopt-mathopt), but mathematical_optimization/constants.h includes it. + if [ -z "${client_include_path}" ] && [ -n "${include_path}" ] && \ + [ ! -f "${include_path}/cuopt/status_codes.h" ]; then + local found_status_codes + found_status_codes=$(find "${search_dir}" -path "*/cuopt/status_codes.h" 2>/dev/null | head -1) + if [ -n "${found_status_codes}" ]; then + # status_codes.h sits directly under cuopt/ (unlike cuopt_c.h, nested one more + # level under mathematical_optimization/), so both layouts need only 2 dirnames. + client_include_path=$(dirname "$(dirname "${found_status_codes}")") + fi + fi + + # Break early if everything needed is found + if [ -n "${include_path}" ] && [ -n "${lib_path}" ] && \ + { [ -n "${client_include_path}" ] || [ -f "${include_path}/cuopt/status_codes.h" ]; }; then break fi done @@ -526,6 +542,7 @@ find_cuopt_libraries() { else log_info "Found: INCLUDE_PATH=${include_path}" log_info "Found: LIBCUOPT_LIBRARY_PATH=${lib_path}" + [ -n "${client_include_path}" ] && log_info "Found: client_include_path=${client_include_path}" return 0 fi } diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index f89430bf47..8a2e302832 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -45,7 +45,7 @@ CC = gcc # Note: The test script automatically finds and passes these paths # Compiler flags -CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra +CFLAGS = -I$(INCLUDE_PATH) $(EXTRA_CFLAGS) -Wall -Wextra LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index 737614b955..300bd88523 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -21,7 +21,7 @@ CC = gcc # Compiler flags -CFLAGS = -I$(INCLUDE_PATH) -Wall -Wextra +CFLAGS = -I$(INCLUDE_PATH) $(EXTRA_CFLAGS) -Wall -Wextra LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) # Automatically discover all C source files in current directory From 1b8ec6f2f7a34141632923cb60032aaea9016085 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 20:12:01 -0500 Subject: [PATCH 28/36] fix(ci): link C examples against libcuopt_client too libcuopt_mathopt.so has real undefined references into libcuopt_client.so (host-side problem-representation types like cpu_optimization_problem_t) since the client/mathopt wheel split, but the MIP/convex C example Makefiles only linked -lcuopt_mathopt. Add EXTRA_LDFLAGS to both Makefiles and have test_doc_examples.sh discover libcuopt_client.so's directory and pass -lcuopt_client (+ -L/-rpath when it lives elsewhere) through it, mirroring the existing EXTRA_CFLAGS/client_include_path pattern for status_codes.h. Verified locally end-to-end: built a simulated site-packages tree, linked both example directories against the real cpp/build artifacts, and ran the resulting binaries to confirm rpath resolution at runtime. Co-Authored-By: Claude Sonnet 5 --- ci/test_doc_examples.sh | 44 +++++++++++++++---- .../source/cuopt-c/convex/examples/Makefile | 2 +- .../source/cuopt-c/mip/examples/Makefile | 2 +- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/ci/test_doc_examples.sh b/ci/test_doc_examples.sh index 3dd2a40fdb..8e53fa658a 100755 --- a/ci/test_doc_examples.sh +++ b/ci/test_doc_examples.sh @@ -33,6 +33,8 @@ GRPC_PID="" # C library paths (set by find_cuopt_libraries) include_path="" lib_path="" +client_include_path="" +client_lib_path="" # Create results directory mkdir -p "${RESULTS_DIR}" @@ -361,13 +363,21 @@ test_c_examples() { # Clean and build if [ -f "Makefile" ]; then log_info " Building C examples..." - local make_vars="" - [ -n "${include_path}" ] && make_vars="${make_vars} INCLUDE_PATH=${include_path}" - [ -n "${lib_path}" ] && make_vars="${make_vars} LIBCUOPT_LIBRARY_PATH=${lib_path}" - [ -n "${client_include_path}" ] && make_vars="${make_vars} EXTRA_CFLAGS=-I${client_include_path}" + local make_vars=() + [ -n "${include_path}" ] && make_vars+=("INCLUDE_PATH=${include_path}") + [ -n "${lib_path}" ] && make_vars+=("LIBCUOPT_LIBRARY_PATH=${lib_path}") + [ -n "${client_include_path}" ] && make_vars+=("EXTRA_CFLAGS=-I${client_include_path}") + # libcuopt_mathopt.so has real undefined references into libcuopt_client.so (see + # find_cuopt_libraries), so link it explicitly; add -L/-rpath only if it lives in a + # separate directory from libcuopt_mathopt.so. + local extra_ldflags="-lcuopt_client" + if [ -n "${client_lib_path}" ] && [ "${client_lib_path}" != "${lib_path}" ]; then + extra_ldflags="-L${client_lib_path} -Wl,-rpath,${client_lib_path} ${extra_ldflags}" + fi + make_vars+=("EXTRA_LDFLAGS=${extra_ldflags}") if make clean > "${RESULTS_DIR}/c-clean-${relative_path//\//_}.log" 2>&1 && \ - make ${make_vars} all > "${RESULTS_DIR}/c-build-${relative_path//\//_}.log" 2>&1; then + make "${make_vars[@]}" all > "${RESULTS_DIR}/c-build-${relative_path//\//_}.log" 2>&1; then log_success " C examples built successfully" else log_failure " Failed to build C examples" @@ -463,6 +473,7 @@ find_cuopt_libraries() { include_path="" lib_path="" client_include_path="" + client_lib_path="" # Get Python site-packages directory local site_packages="" @@ -502,9 +513,10 @@ find_cuopt_libraries() { fi if [ -z "${lib_path}" ]; then - # Search for libcuopt.so in both lib and lib64 directories + # The C examples link -lcuopt_mathopt directly, not the libcuopt umbrella (which + # no longer bundles it -- it's in libcuopt-mathopt's own wheel since the split). local found_lib - found_lib=$(find "${search_dir}" -name "libcuopt.so" \( -path "*/lib/*" -o -path "*/lib64/*" \) 2>/dev/null | head -1) + found_lib=$(find "${search_dir}" -name "libcuopt_mathopt.so" \( -path "*/lib/*" -o -path "*/lib64/*" \) 2>/dev/null | head -1) if [ -n "${found_lib}" ]; then lib_path=$(dirname "${found_lib}") fi @@ -523,9 +535,22 @@ find_cuopt_libraries() { fi fi + # libcuopt_mathopt.so has real undefined references into libcuopt_client.so (the + # host-side problem-representation types) since the client/mathopt split, so the C + # examples need to link it too, not just libcuopt_mathopt. + if [ -z "${client_lib_path}" ] && [ -n "${lib_path}" ] && \ + [ ! -f "${lib_path}/libcuopt_client.so" ]; then + local found_client_lib + found_client_lib=$(find "${search_dir}" -name "libcuopt_client.so" \( -path "*/lib/*" -o -path "*/lib64/*" \) 2>/dev/null | head -1) + if [ -n "${found_client_lib}" ]; then + client_lib_path=$(dirname "${found_client_lib}") + fi + fi + # Break early if everything needed is found if [ -n "${include_path}" ] && [ -n "${lib_path}" ] && \ - { [ -n "${client_include_path}" ] || [ -f "${include_path}/cuopt/status_codes.h" ]; }; then + { [ -n "${client_include_path}" ] || [ -f "${include_path}/cuopt/status_codes.h" ]; } && \ + { [ -n "${client_lib_path}" ] || [ -f "${lib_path}/libcuopt_client.so" ]; }; then break fi done @@ -536,13 +561,14 @@ find_cuopt_libraries() { log_failure " Missing: INCLUDE_PATH (searched for cuopt_c.h)" fi if [ -z "${lib_path}" ]; then - log_failure " Missing: LIBCUOPT_LIBRARY_PATH (searched for libcuopt.so)" + log_failure " Missing: LIBCUOPT_LIBRARY_PATH (searched for libcuopt_mathopt.so)" fi return 1 else log_info "Found: INCLUDE_PATH=${include_path}" log_info "Found: LIBCUOPT_LIBRARY_PATH=${lib_path}" [ -n "${client_include_path}" ] && log_info "Found: client_include_path=${client_include_path}" + [ -n "${client_lib_path}" ] && log_info "Found: client_lib_path=${client_lib_path}" return 0 fi } diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 8a2e302832..5592356a74 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) $(EXTRA_CFLAGS) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) $(EXTRA_LDFLAGS) # 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 300bd88523..2176e65d54 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -22,7 +22,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) $(EXTRA_CFLAGS) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) $(EXTRA_LDFLAGS) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) From 798463fa77b4bf43f569aa91b2a0760e92ae877b Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 20:14:14 -0500 Subject: [PATCH 29/36] fix(docs): make CLIENT_INCLUDE_PATH/CLIENT_LIBRARY_PATH explicit, documented Makefile vars Replace the EXTRA_CFLAGS/EXTRA_LDFLAGS escape hatch from the previous commit with named, documented variables. libcuopt-mathopt and libcuopt-client are permanently separate wheels/install components post-split, not a CI-only detail, so the Makefile's own usage docs, check-paths validation, and help output now say so directly instead of requiring a user to know about an undocumented extra-flags mechanism. Co-Authored-By: Claude Sonnet 5 --- ci/test_doc_examples.sh | 16 +++---- .../source/cuopt-c/convex/examples/Makefile | 47 ++++++++++--------- .../source/cuopt-c/mip/examples/Makefile | 25 +++++----- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/ci/test_doc_examples.sh b/ci/test_doc_examples.sh index 8e53fa658a..e38f6f4a91 100755 --- a/ci/test_doc_examples.sh +++ b/ci/test_doc_examples.sh @@ -366,15 +366,13 @@ test_c_examples() { local make_vars=() [ -n "${include_path}" ] && make_vars+=("INCLUDE_PATH=${include_path}") [ -n "${lib_path}" ] && make_vars+=("LIBCUOPT_LIBRARY_PATH=${lib_path}") - [ -n "${client_include_path}" ] && make_vars+=("EXTRA_CFLAGS=-I${client_include_path}") - # libcuopt_mathopt.so has real undefined references into libcuopt_client.so (see - # find_cuopt_libraries), so link it explicitly; add -L/-rpath only if it lives in a - # separate directory from libcuopt_mathopt.so. - local extra_ldflags="-lcuopt_client" - if [ -n "${client_lib_path}" ] && [ "${client_lib_path}" != "${lib_path}" ]; then - extra_ldflags="-L${client_lib_path} -Wl,-rpath,${client_lib_path} ${extra_ldflags}" - fi - make_vars+=("EXTRA_LDFLAGS=${extra_ldflags}") + # libcuopt-mathopt and libcuopt-client are separate wheels/install components since + # the split; mathopt links against client's problem-representation types, so the + # Makefiles need both pairs of paths. For non-split installs (local build, conda) + # client_include_path/client_lib_path are unset because status_codes.h and + # libcuopt_client.so already live alongside mathopt's own paths -- fall back to those. + make_vars+=("CLIENT_INCLUDE_PATH=${client_include_path:-${include_path}}") + make_vars+=("CLIENT_LIBRARY_PATH=${client_lib_path:-${lib_path}}") if make clean > "${RESULTS_DIR}/c-clean-${relative_path//\//_}.log" 2>&1 && \ make "${make_vars[@]}" all > "${RESULTS_DIR}/c-build-${relative_path//\//_}.log" 2>&1; then diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 5592356a74..69cdb79a3b 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -20,7 +20,8 @@ # # Prerequisites: # - cuOpt C library installed or built locally -# - Set INCLUDE_PATH and LIBCUOPT_LIBRARY_PATH (see below) +# - Set INCLUDE_PATH/LIBCUOPT_LIBRARY_PATH (libcuopt-mathopt) and +# CLIENT_INCLUDE_PATH/CLIENT_LIBRARY_PATH (libcuopt-client) (see below) # Compiler CC = gcc @@ -28,25 +29,36 @@ CC = gcc # Set include and library paths # REQUIRED: Set these paths either: # 1. As environment variables -# 2. On command line: make INCLUDE_PATH=... LIBCUOPT_LIBRARY_PATH=... +# 2. On command line: make INCLUDE_PATH=... LIBCUOPT_LIBRARY_PATH=... CLIENT_INCLUDE_PATH=... CLIENT_LIBRARY_PATH=... # 3. Uncomment and edit below for your system: # -# INCLUDE_PATH = /path/to/cuopt/include -# LIBCUOPT_LIBRARY_PATH = /path/to/cuopt/lib +# libcuopt-mathopt and libcuopt-client are separate pip packages (and, for non-pip installs, +# separate install components); the mathopt engine links against client's problem-representation +# types, so both sets of paths are required to build these examples. +# +# INCLUDE_PATH = /path/to/libcuopt_mathopt/include +# LIBCUOPT_LIBRARY_PATH = /path/to/libcuopt_mathopt/lib +# CLIENT_INCLUDE_PATH = /path/to/libcuopt_client/include +# CLIENT_LIBRARY_PATH = /path/to/libcuopt_client/lib # # Example for local build: # INCLUDE_PATH = ../../../cpp/include # LIBCUOPT_LIBRARY_PATH = ../../../cpp/build +# CLIENT_INCLUDE_PATH = ../../../cpp/include +# CLIENT_LIBRARY_PATH = ../../../cpp/build # # Example for conda install: # INCLUDE_PATH = $(CONDA_PREFIX)/include # LIBCUOPT_LIBRARY_PATH = $(CONDA_PREFIX)/lib +# CLIENT_INCLUDE_PATH = $(CONDA_PREFIX)/include +# CLIENT_LIBRARY_PATH = $(CONDA_PREFIX)/lib # # Note: The test script automatically finds and passes these paths # Compiler flags -CFLAGS = -I$(INCLUDE_PATH) $(EXTRA_CFLAGS) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) $(EXTRA_LDFLAGS) +CFLAGS = -I$(INCLUDE_PATH) -I$(CLIENT_INCLUDE_PATH) -Wall -Wextra +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -L$(CLIENT_LIBRARY_PATH) -lcuopt_mathopt -lcuopt_client \ + -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) -Wl,-rpath,$(CLIENT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) @@ -82,22 +94,13 @@ clean: # Check if required paths are set check-paths: - @if [ -z "$(INCLUDE_PATH)" ]; then \ - echo "ERROR: INCLUDE_PATH is not set"; \ - echo ""; \ - echo "Please set it using one of these methods:"; \ - echo " 1. Environment: export INCLUDE_PATH=/path/to/cuopt/include"; \ - echo " 2. Command line: make INCLUDE_PATH=/path/to/cuopt/include LIBCUOPT_LIBRARY_PATH=/path/to/lib"; \ - echo " 3. Edit Makefile and uncomment/set the path variables"; \ - echo ""; \ - exit 1; \ - fi - @if [ -z "$(LIBCUOPT_LIBRARY_PATH)" ]; then \ - echo "ERROR: LIBCUOPT_LIBRARY_PATH is not set"; \ + @if [ -z "$(INCLUDE_PATH)" ] || [ -z "$(LIBCUOPT_LIBRARY_PATH)" ] || \ + [ -z "$(CLIENT_INCLUDE_PATH)" ] || [ -z "$(CLIENT_LIBRARY_PATH)" ]; then \ + echo "ERROR: one or more required paths are not set"; \ echo ""; \ - echo "Please set it using one of these methods:"; \ - echo " 1. Environment: export LIBCUOPT_LIBRARY_PATH=/path/to/cuopt/lib"; \ - echo " 2. Command line: make LIBCUOPT_LIBRARY_PATH=/path/to/lib"; \ + echo "Please set them using one of these methods:"; \ + echo " 1. Environment: export INCLUDE_PATH=... LIBCUOPT_LIBRARY_PATH=... CLIENT_INCLUDE_PATH=... CLIENT_LIBRARY_PATH=..."; \ + echo " 2. Command line: make INCLUDE_PATH=... LIBCUOPT_LIBRARY_PATH=... CLIENT_INCLUDE_PATH=... CLIENT_LIBRARY_PATH=..."; \ echo " 3. Edit Makefile and uncomment/set the path variables"; \ echo ""; \ exit 1; \ @@ -130,6 +133,8 @@ help: @echo "Current Configuration:" @echo " INCLUDE_PATH: $(INCLUDE_PATH)" @echo " LIBCUOPT_LIBRARY_PATH: $(LIBCUOPT_LIBRARY_PATH)" + @echo " CLIENT_INCLUDE_PATH: $(CLIENT_INCLUDE_PATH)" + @echo " CLIENT_LIBRARY_PATH: $(CLIENT_LIBRARY_PATH)" @echo "" @echo "Discovered Files:" @echo " C sources: $(words $(SOURCES))" diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index 2176e65d54..9cae3439ad 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -15,14 +15,20 @@ # make clean - Remove all built executables # # Required variables (pass on command line): -# make INCLUDE_PATH=/path/to/cuopt/include LIBCUOPT_LIBRARY_PATH=/path/to/cuopt/lib +# make INCLUDE_PATH=/path/to/libcuopt_mathopt/include LIBCUOPT_LIBRARY_PATH=/path/to/libcuopt_mathopt/lib \ +# CLIENT_INCLUDE_PATH=/path/to/libcuopt_client/include CLIENT_LIBRARY_PATH=/path/to/libcuopt_client/lib +# +# libcuopt-mathopt and libcuopt-client are separate pip packages (and, for non-pip installs, +# separate install components); the mathopt engine links against client's problem-representation +# types, so both sets of paths are required to build these examples. # Compiler CC = gcc # Compiler flags -CFLAGS = -I$(INCLUDE_PATH) $(EXTRA_CFLAGS) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -lcuopt_mathopt -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) $(EXTRA_LDFLAGS) +CFLAGS = -I$(INCLUDE_PATH) -I$(CLIENT_INCLUDE_PATH) -Wall -Wextra +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -L$(CLIENT_LIBRARY_PATH) -lcuopt_mathopt -lcuopt_client \ + -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) -Wl,-rpath,$(CLIENT_LIBRARY_PATH) # Automatically discover all C source files in current directory SOURCES = $(wildcard *.c) @@ -39,13 +45,10 @@ clean: rm -f $(TARGETS) check-paths: - @if [ -z "$(INCLUDE_PATH)" ]; then \ - echo "ERROR: INCLUDE_PATH is not set"; \ - echo "Usage: make INCLUDE_PATH=/path/to/cuopt/include LIBCUOPT_LIBRARY_PATH=/path/to/cuopt/lib"; \ - exit 1; \ - fi - @if [ -z "$(LIBCUOPT_LIBRARY_PATH)" ]; then \ - echo "ERROR: LIBCUOPT_LIBRARY_PATH is not set"; \ - echo "Usage: make INCLUDE_PATH=/path/to/cuopt/include LIBCUOPT_LIBRARY_PATH=/path/to/cuopt/lib"; \ + @if [ -z "$(INCLUDE_PATH)" ] || [ -z "$(LIBCUOPT_LIBRARY_PATH)" ] || \ + [ -z "$(CLIENT_INCLUDE_PATH)" ] || [ -z "$(CLIENT_LIBRARY_PATH)" ]; then \ + echo "ERROR: one or more required paths are not set"; \ + echo "Usage: make INCLUDE_PATH=/path/to/libcuopt_mathopt/include LIBCUOPT_LIBRARY_PATH=/path/to/libcuopt_mathopt/lib \\"; \ + echo " CLIENT_INCLUDE_PATH=/path/to/libcuopt_client/include CLIENT_LIBRARY_PATH=/path/to/libcuopt_client/lib"; \ exit 1; \ fi From c923d076e67b0abf0266493715d3d082de297ea4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Thu, 1 Oct 2026 20:30:45 -0500 Subject: [PATCH 30/36] style: drop unnecessary comments from CLIENT_*_PATH changes Co-Authored-By: Claude Sonnet 5 --- ci/test_doc_examples.sh | 12 ------------ docs/cuopt/source/cuopt-c/convex/examples/Makefile | 4 ---- docs/cuopt/source/cuopt-c/mip/examples/Makefile | 4 ---- 3 files changed, 20 deletions(-) diff --git a/ci/test_doc_examples.sh b/ci/test_doc_examples.sh index e38f6f4a91..b7dd5ec1a2 100755 --- a/ci/test_doc_examples.sh +++ b/ci/test_doc_examples.sh @@ -366,11 +366,6 @@ test_c_examples() { local make_vars=() [ -n "${include_path}" ] && make_vars+=("INCLUDE_PATH=${include_path}") [ -n "${lib_path}" ] && make_vars+=("LIBCUOPT_LIBRARY_PATH=${lib_path}") - # libcuopt-mathopt and libcuopt-client are separate wheels/install components since - # the split; mathopt links against client's problem-representation types, so the - # Makefiles need both pairs of paths. For non-split installs (local build, conda) - # client_include_path/client_lib_path are unset because status_codes.h and - # libcuopt_client.so already live alongside mathopt's own paths -- fall back to those. make_vars+=("CLIENT_INCLUDE_PATH=${client_include_path:-${include_path}}") make_vars+=("CLIENT_LIBRARY_PATH=${client_lib_path:-${lib_path}}") @@ -520,22 +515,15 @@ find_cuopt_libraries() { fi fi - # status_codes.h lives in libcuopt-client's own wheel, a different package than - # cuopt_c.h (libcuopt-mathopt), but mathematical_optimization/constants.h includes it. if [ -z "${client_include_path}" ] && [ -n "${include_path}" ] && \ [ ! -f "${include_path}/cuopt/status_codes.h" ]; then local found_status_codes found_status_codes=$(find "${search_dir}" -path "*/cuopt/status_codes.h" 2>/dev/null | head -1) if [ -n "${found_status_codes}" ]; then - # status_codes.h sits directly under cuopt/ (unlike cuopt_c.h, nested one more - # level under mathematical_optimization/), so both layouts need only 2 dirnames. client_include_path=$(dirname "$(dirname "${found_status_codes}")") fi fi - # libcuopt_mathopt.so has real undefined references into libcuopt_client.so (the - # host-side problem-representation types) since the client/mathopt split, so the C - # examples need to link it too, not just libcuopt_mathopt. if [ -z "${client_lib_path}" ] && [ -n "${lib_path}" ] && \ [ ! -f "${lib_path}/libcuopt_client.so" ]; then local found_client_lib diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 69cdb79a3b..5df7f305de 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -32,10 +32,6 @@ CC = gcc # 2. On command line: make INCLUDE_PATH=... LIBCUOPT_LIBRARY_PATH=... CLIENT_INCLUDE_PATH=... CLIENT_LIBRARY_PATH=... # 3. Uncomment and edit below for your system: # -# libcuopt-mathopt and libcuopt-client are separate pip packages (and, for non-pip installs, -# separate install components); the mathopt engine links against client's problem-representation -# types, so both sets of paths are required to build these examples. -# # INCLUDE_PATH = /path/to/libcuopt_mathopt/include # LIBCUOPT_LIBRARY_PATH = /path/to/libcuopt_mathopt/lib # CLIENT_INCLUDE_PATH = /path/to/libcuopt_client/include diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index 9cae3439ad..7c2bbca725 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -17,10 +17,6 @@ # Required variables (pass on command line): # make INCLUDE_PATH=/path/to/libcuopt_mathopt/include LIBCUOPT_LIBRARY_PATH=/path/to/libcuopt_mathopt/lib \ # CLIENT_INCLUDE_PATH=/path/to/libcuopt_client/include CLIENT_LIBRARY_PATH=/path/to/libcuopt_client/lib -# -# libcuopt-mathopt and libcuopt-client are separate pip packages (and, for non-pip installs, -# separate install components); the mathopt engine links against client's problem-representation -# types, so both sets of paths are required to build these examples. # Compiler CC = gcc From 6f6307a2fd221e8b9c1f7cbf793fd046c213adef Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 2 Oct 2026 09:50:22 -0500 Subject: [PATCH 31/36] fix(docs): keep libcuopt_client.so as a direct NEEDED entry in C examples The linker's --as-needed default silently dropped -lcuopt_client from the executable's NEEDED list, since the example code itself never references a client symbol directly (only libcuopt_mathopt.so does). The executable's own RUNPATH only resolves its direct NEEDED entries, not libcuopt_mathopt.so's transitive NEEDED on libcuopt_client.so, so the binary failed at runtime with "libcuopt_client.so: cannot open shared object file" despite linking cleanly. Root-caused by downloading the real wheels from the failing CI run and reproducing locally with readelf -d, which showed libcuopt_client.so missing from NEEDED even though RUNPATH was correct. -Wl,--no-as-needed keeps it a direct dependency, verified via readelf across all 9 examples. Co-Authored-By: Claude Sonnet 5 --- docs/cuopt/source/cuopt-c/convex/examples/Makefile | 2 +- docs/cuopt/source/cuopt-c/mip/examples/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cuopt/source/cuopt-c/convex/examples/Makefile b/docs/cuopt/source/cuopt-c/convex/examples/Makefile index 5df7f305de..6078af69c3 100644 --- a/docs/cuopt/source/cuopt-c/convex/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/convex/examples/Makefile @@ -53,7 +53,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -I$(CLIENT_INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -L$(CLIENT_LIBRARY_PATH) -lcuopt_mathopt -lcuopt_client \ +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -L$(CLIENT_LIBRARY_PATH) -Wl,--no-as-needed -lcuopt_mathopt -lcuopt_client \ -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) -Wl,-rpath,$(CLIENT_LIBRARY_PATH) # Automatically discover all C source files in current directory diff --git a/docs/cuopt/source/cuopt-c/mip/examples/Makefile b/docs/cuopt/source/cuopt-c/mip/examples/Makefile index 7c2bbca725..edf0535154 100644 --- a/docs/cuopt/source/cuopt-c/mip/examples/Makefile +++ b/docs/cuopt/source/cuopt-c/mip/examples/Makefile @@ -23,7 +23,7 @@ CC = gcc # Compiler flags CFLAGS = -I$(INCLUDE_PATH) -I$(CLIENT_INCLUDE_PATH) -Wall -Wextra -LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -L$(CLIENT_LIBRARY_PATH) -lcuopt_mathopt -lcuopt_client \ +LDFLAGS = -L$(LIBCUOPT_LIBRARY_PATH) -L$(CLIENT_LIBRARY_PATH) -Wl,--no-as-needed -lcuopt_mathopt -lcuopt_client \ -Wl,-rpath,$(LIBCUOPT_LIBRARY_PATH) -Wl,-rpath,$(CLIENT_LIBRARY_PATH) # Automatically discover all C source files in current directory From 9777972bbabab143c5f55a4ec28432fede5be151 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 2 Oct 2026 09:55:19 -0500 Subject: [PATCH 32/36] fix(wheel): give cuopt_mathopt/cuopt_routing their own rpath to libcuopt_client They both have a real NEEDED on libcuopt_client.so, but only cuopt_cli and cuopt_grpc_server carried component_rpaths. Python never hits this because its loader opens libcuopt_client.so first, so glibc satisfies the NEEDED from the already-loaded object -- but any non-Python consumer linking only -lcuopt_mathopt or -lcuopt_routing (the obvious, pre-split usage pattern) has no such guarantee and fails at runtime with "libcuopt_client.so: cannot open shared object file", same as the C docs examples just hit. Verified by patching a real downloaded libcuopt_mathopt.so with the equivalent rpath via patchelf and confirming a naive consumer (plain -lcuopt_mathopt, no explicit client link) resolves libcuopt_client.so through the library's own rpath. Co-Authored-By: Claude Sonnet 5 --- python/cmake/cuopt_wheel_build.cmake | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/cmake/cuopt_wheel_build.cmake b/python/cmake/cuopt_wheel_build.cmake index 2da53a4805..067aee148d 100644 --- a/python/cmake/cuopt_wheel_build.cmake +++ b/python/cmake/cuopt_wheel_build.cmake @@ -84,16 +84,15 @@ foreach(_target cuopt_routing cuopt_mathopt cuopt_client cuopt_cli cuopt_grpc_se endif() endforeach() -# Executables also need the sibling component wheels. The libraries do not: load_library() -# loads them in dependency order, so glibc satisfies each DT_NEEDED from an already-loaded -# object. An executable has no Python in the process, so cuopt_cli shipped in -# libcuopt-mathopt would not find libcuopt_client.so, which is never vendored. +# cuopt_mathopt and cuopt_routing both have a real NEEDED on libcuopt_client.so, never +# vendored into their own wheels, so they need their own rpath to it too -- not just +# cuopt_cli/cuopt_grpc_server. set(component_rpaths "$ORIGIN/../../libcuopt_client/lib64" "$ORIGIN/../../libcuopt_mathopt/lib64" "$ORIGIN/../../libcuopt_routing/lib64" ) -foreach(_target cuopt_cli cuopt_grpc_server) +foreach(_target cuopt_mathopt cuopt_routing cuopt_cli cuopt_grpc_server) if(TARGET ${_target}) set_property(TARGET ${_target} APPEND PROPERTY INSTALL_RPATH ${component_rpaths}) endif() From 0ee4e1c66752f445a1eeabe377271a4443cae833 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 2 Oct 2026 13:21:44 -0500 Subject: [PATCH 33/36] fix(ci): require client paths in find_cuopt_libraries' final success check The early-break condition already required client_include_path/client_lib_path (or the non-split-install file fallback), but the final success/failure gate only checked include_path/lib_path, so a genuinely broken split install (client wheel missing) returned success anyway and only failed later with a confusing compile error instead of a clear discovery failure. Verified against three scenarios: split install with client present (success), split install with client missing (now correctly fails with a clear message), and non-split install where status_codes.h/libcuopt_client.so are co-located with mathopt's own paths (still succeeds). Co-Authored-By: Claude Sonnet 5 --- ci/test_doc_examples.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ci/test_doc_examples.sh b/ci/test_doc_examples.sh index b7dd5ec1a2..2b27783067 100755 --- a/ci/test_doc_examples.sh +++ b/ci/test_doc_examples.sh @@ -541,7 +541,17 @@ find_cuopt_libraries() { fi done - if [ -z "${include_path}" ] || [ -z "${lib_path}" ]; then + local client_include_found=false + if [ -n "${client_include_path}" ] || [ -f "${include_path}/cuopt/status_codes.h" ]; then + client_include_found=true + fi + local client_lib_found=false + if [ -n "${client_lib_path}" ] || [ -f "${lib_path}/libcuopt_client.so" ]; then + client_lib_found=true + fi + + if [ -z "${include_path}" ] || [ -z "${lib_path}" ] || \ + [ "${client_include_found}" = false ] || [ "${client_lib_found}" = false ]; then log_failure "Could not find cuOpt headers or libraries" if [ -z "${include_path}" ]; then log_failure " Missing: INCLUDE_PATH (searched for cuopt_c.h)" @@ -549,6 +559,12 @@ find_cuopt_libraries() { if [ -z "${lib_path}" ]; then log_failure " Missing: LIBCUOPT_LIBRARY_PATH (searched for libcuopt_mathopt.so)" fi + if [ "${client_include_found}" = false ]; then + log_failure " Missing: client include path (searched for status_codes.h)" + fi + if [ "${client_lib_found}" = false ]; then + log_failure " Missing: client library path (searched for libcuopt_client.so)" + fi return 1 else log_info "Found: INCLUDE_PATH=${include_path}" From 1c64d995c9dc777a987958e8456f2b878908a571 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 2 Oct 2026 13:28:28 -0500 Subject: [PATCH 34/36] fix(cmake): fail loudly on unresolvable external components, scope grpc_server discovery Three issues from CodeRabbit review on #2013, all verified against real CMake configure runs: - CUOPT_FIND_{CLIENT,MATHOPT,ROUTING}_EXTERNALLY silently fell back to a local build when find_package(cuopt) didn't actually provide the requested target, risking an ABI mismatch against the real, separately-installed sibling wheel at runtime. Now a hard configure error (verified: CMake Error at CMakeLists.txt:762 fires as expected). - cuopt_static embeds *_objs directly, which an externally-resolved component doesn't have; combining it with CUOPT_FIND_*_EXTERNALLY would reference a nonexistent target. Now rejected at configure time instead. - python/libcuopt's find_program(cuopt_grpc_server) searched the system PATH unrestricted, so a stray executable left over from an unrelated install could be mistaken for this build's own prebuilt server. Scoped to CMAKE_PREFIX_PATH with NO_DEFAULT_PATH. Co-Authored-By: Claude Sonnet 5 --- cpp/CMakeLists.txt | 17 +++++++++++++++++ python/libcuopt/CMakeLists.txt | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 2df27bc43d..74d3e2b015 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -757,6 +757,17 @@ if((CUOPT_FIND_CLIENT_EXTERNALLY OR CUOPT_FIND_MATHOPT_EXTERNALLY OR CUOPT_FIND_ find_package(cuopt "${RAPIDS_VERSION}" QUIET) endif() +# A requested external component must not silently fall back to a local build. +if(CUOPT_FIND_CLIENT_EXTERNALLY AND NOT TARGET cuopt::client) + message(FATAL_ERROR "CUOPT_FIND_CLIENT_EXTERNALLY=ON but find_package(cuopt) did not provide cuopt::client") +endif() +if(CUOPT_FIND_MATHOPT_EXTERNALLY AND NOT TARGET cuopt::mathopt) + message(FATAL_ERROR "CUOPT_FIND_MATHOPT_EXTERNALLY=ON but find_package(cuopt) did not provide cuopt::mathopt") +endif() +if(CUOPT_FIND_ROUTING_EXTERNALLY AND NOT TARGET cuopt::routing) + message(FATAL_ERROR "CUOPT_FIND_ROUTING_EXTERNALLY=ON but find_package(cuopt) did not provide cuopt::routing") +endif() + # - cuopt_client - CPU-only support library ---------------------------------------------------------- # # Host-side problem representation (parsers, data_model_view, mps_data_model, writers), the @@ -1107,6 +1118,12 @@ endif () # Built for the internal tests, the MIP benchmarks, and for consumers that embed cuOpt into a # single self-contained shared object rather than linking the shared libcuopt (see the Java # classifier JARs). +if ((BUILD_TESTS OR (BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY) OR CUOPT_BUILD_STATIC_LIB) + AND (CUOPT_FIND_CLIENT_EXTERNALLY OR CUOPT_FIND_MATHOPT_EXTERNALLY OR CUOPT_FIND_ROUTING_EXTERNALLY)) + message(FATAL_ERROR "cuopt_static embeds *_objs directly, which an externally-resolved " + "component doesn't have; it is incompatible with CUOPT_FIND_*_EXTERNALLY") +endif() + if (BUILD_TESTS OR (BUILD_MIP_BENCHMARKS AND NOT BUILD_LP_ONLY) OR CUOPT_BUILD_STATIC_LIB) # Embeds the *_objs directly rather than linking the shared component libraries: internal # tests and the Java classifier JAR reach internals the shared libraries don't export. diff --git a/python/libcuopt/CMakeLists.txt b/python/libcuopt/CMakeLists.txt index 577f92b163..bbbbe41908 100644 --- a/python/libcuopt/CMakeLists.txt +++ b/python/libcuopt/CMakeLists.txt @@ -18,9 +18,14 @@ project( # Already installed: nothing to build, the wheel just stages the component. Checking for # the cuopt_grpc_server binary itself, not cuopt_FOUND, matters now that this build installs # the client/mathopt/routing sibling wheels as dependencies -- those alone resolve cuopt_FOUND -# but not a prebuilt cuopt_grpc_server. +# but not a prebuilt cuopt_grpc_server. PATHS+NO_DEFAULT_PATH restricts the search to the same +# install trees find_package(cuopt) resolved, not an unrelated binary elsewhere on PATH. find_package(cuopt "${RAPIDS_VERSION}") -find_program(CUOPT_GRPC_SERVER_PREBUILT cuopt_grpc_server) +find_program(CUOPT_GRPC_SERVER_PREBUILT cuopt_grpc_server + PATHS ${CMAKE_PREFIX_PATH} + PATH_SUFFIXES bin + NO_DEFAULT_PATH +) if(cuopt_FOUND AND CUOPT_GRPC_SERVER_PREBUILT) return() From 4e64e77023be449fe9181857a79b44d3f4375adc Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 2 Oct 2026 13:45:53 -0500 Subject: [PATCH 35/36] fix(cmake): retry find_package(cuopt) per-component, not only when none resolved The guard only called find_package(cuopt) when cuopt::client, cuopt::mathopt, and cuopt::routing were all absent. If an earlier find_package(cuopt) call in this configure (e.g. python/libcuopt's own early check) already resolved one target but not another, the retry was skipped entirely -- so a still-missing requested component could never resolve, and the configure-time FATAL_ERROR guard added for it would fire even though a real find_package(cuopt) attempt might have succeeded. Verified with an isolated CMake probe simulating cuopt::client pre-resolved and CUOPT_FIND_MATHOPT_EXTERNALLY=ON: the old condition skips the retry, the new one correctly retries. Co-Authored-By: Claude Sonnet 5 --- cpp/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 74d3e2b015..6c57fc50fd 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -752,8 +752,12 @@ get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) option(CUOPT_FIND_CLIENT_EXTERNALLY "Resolve cuopt::client via find_package(cuopt) instead of building it" OFF) option(CUOPT_FIND_MATHOPT_EXTERNALLY "Resolve cuopt::mathopt via find_package(cuopt) instead of building it" OFF) option(CUOPT_FIND_ROUTING_EXTERNALLY "Resolve cuopt::routing via find_package(cuopt) instead of building it" OFF) -if((CUOPT_FIND_CLIENT_EXTERNALLY OR CUOPT_FIND_MATHOPT_EXTERNALLY OR CUOPT_FIND_ROUTING_EXTERNALLY) - AND NOT TARGET cuopt::client AND NOT TARGET cuopt::mathopt AND NOT TARGET cuopt::routing) +# A prior find_package(cuopt) call elsewhere in this configure (e.g. python/libcuopt's own +# early check) may have already resolved some but not all requested targets -- retry whenever +# any requested component is still missing, not only when none of them have resolved yet. +if((CUOPT_FIND_CLIENT_EXTERNALLY AND NOT TARGET cuopt::client) OR + (CUOPT_FIND_MATHOPT_EXTERNALLY AND NOT TARGET cuopt::mathopt) OR + (CUOPT_FIND_ROUTING_EXTERNALLY AND NOT TARGET cuopt::routing)) find_package(cuopt "${RAPIDS_VERSION}" QUIET) endif() From 8b280506362e816dfd67a3a0be050134cef389ba Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 2 Oct 2026 13:46:23 -0500 Subject: [PATCH 36/36] style: drop comment from find_package(cuopt) retry fix Co-Authored-By: Claude Sonnet 5 --- cpp/CMakeLists.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6c57fc50fd..6bfa5144e9 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -752,9 +752,6 @@ get_filename_component(CUDSS_MT_LIB_FILE_NAME "${CUDSS_MT_LIB_FILE}" NAME) option(CUOPT_FIND_CLIENT_EXTERNALLY "Resolve cuopt::client via find_package(cuopt) instead of building it" OFF) option(CUOPT_FIND_MATHOPT_EXTERNALLY "Resolve cuopt::mathopt via find_package(cuopt) instead of building it" OFF) option(CUOPT_FIND_ROUTING_EXTERNALLY "Resolve cuopt::routing via find_package(cuopt) instead of building it" OFF) -# A prior find_package(cuopt) call elsewhere in this configure (e.g. python/libcuopt's own -# early check) may have already resolved some but not all requested targets -- retry whenever -# any requested component is still missing, not only when none of them have resolved yet. if((CUOPT_FIND_CLIENT_EXTERNALLY AND NOT TARGET cuopt::client) OR (CUOPT_FIND_MATHOPT_EXTERNALLY AND NOT TARGET cuopt::mathopt) OR (CUOPT_FIND_ROUTING_EXTERNALLY AND NOT TARGET cuopt::routing))