diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ddd1abf706..1e640721e7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -178,6 +178,42 @@ jobs: script-env-secret-3-key: CUOPT_AWS_SECRET_ACCESS_KEY script-env-secret-3-value: ${{ secrets.CUOPT_AWS_SECRET_ACCESS_KEY }} + # Installs each libcuopt-* package alone and builds and runs a C++ binary against it, on the + # latest amd64 CUDA and Python only. + conda-split-package-tests: + if: ${{ inputs.build_type == 'nightly' }} + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@main + with: + build_type: ${{ inputs.build_type }} + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + script: ci/test_conda_split_packages.sh + matrix_filter: 'map(select(.ARCH == "amd64")) | sort_by(.CUDA_VER, .PY_VER) | [last]' + + wheel-split-package-tests: + if: ${{ inputs.build_type == 'nightly' }} + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@main + with: + build_type: ${{ inputs.build_type }} + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + script: ci/test_wheel_split_packages.sh + matrix_filter: 'map(select(.ARCH == "amd64")) | sort_by(.CUDA_VER, .PY_VER) | [last]' + conda-notebook-tests: permissions: actions: read @@ -223,6 +259,8 @@ jobs: - java-static-test - wheel-tests-cuopt - wheel-tests-cuopt-server + - conda-split-package-tests + - wheel-split-package-tests - conda-notebook-tests - multi-gpu-cpp-tests uses: ./.github/workflows/nightly-summary.yaml diff --git a/ci/split_package_smoke/CMakeLists.txt b/ci/split_package_smoke/CMakeLists.txt new file mode 100644 index 0000000000..db6249e70b --- /dev/null +++ b/ci/split_package_smoke/CMakeLists.txt @@ -0,0 +1,33 @@ +# cmake-format: off +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# cmake-format: on + +cmake_minimum_required(VERSION 3.30) +project(cuopt_split_package_smoke LANGUAGES C CXX) + +set(COMPONENT "" CACHE STRING "cuOpt component to link: client, mathopt or routing") +if(NOT COMPONENT MATCHES "^(client|mathopt|routing)$") + message(FATAL_ERROR "COMPONENT must be client, mathopt or routing, got '${COMPONENT}'") +endif() + +# Wheels install into lib64, which CMake skips on Debian-derived distros. +set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS ON) + +find_package(cuopt REQUIRED) +if(NOT TARGET cuopt::${COMPONENT}) + message(FATAL_ERROR "cuopt::${COMPONENT} was not found in this installation") +endif() + +if(COMPONENT STREQUAL "mathopt") + set(SMOKE_SOURCE ${CMAKE_CURRENT_LIST_DIR}/../../docs/cuopt/source/cuopt-c/convex/examples/simple_lp_example.c) +else() + set(SMOKE_SOURCE ${COMPONENT}.cpp) +endif() + +add_executable(smoke_${COMPONENT} ${SMOKE_SOURCE}) +target_compile_features(smoke_${COMPONENT} PRIVATE cxx_std_20) +# Keep the component in the link even if the program uses none of it, so a library that cannot +# load fails at startup. +target_link_options(smoke_${COMPONENT} PRIVATE LINKER:--no-as-needed) +target_link_libraries(smoke_${COMPONENT} PRIVATE cuopt::${COMPONENT}) diff --git a/ci/split_package_smoke/build_and_run.sh b/ci/split_package_smoke/build_and_run.sh new file mode 100755 index 0000000000..c1e745bea8 --- /dev/null +++ b/ci/split_package_smoke/build_and_run.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Usage: build_and_run.sh +# +# Builds the consumer for one component against whatever is on CMAKE_PREFIX_PATH, then runs it. + +set -euo pipefail + +component=$1 +prefix_path=$2 +src="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +build="$(mktemp -d)/${component}" + +cmake -S "${src}" -B "${build}" -GNinja \ + -DCOMPONENT="${component}" \ + -DCMAKE_PREFIX_PATH="${prefix_path}" +cmake --build "${build}" + +cd "${build}" +"./smoke_${component}" diff --git a/ci/split_package_smoke/client.cpp b/ci/split_package_smoke/client.cpp new file mode 100644 index 0000000000..a43c57b00a --- /dev/null +++ b/ci/split_package_smoke/client.cpp @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include + +// The client's installed headers expose almost nothing to call, so this checks that the library +// and every shared library it links resolve when the binary starts. +int main() +{ + std::printf("CUOPT_SUCCESS=%d\n", CUOPT_SUCCESS); + return CUOPT_SUCCESS; +} diff --git a/ci/split_package_smoke/env_check.py b/ci/split_package_smoke/env_check.py new file mode 100755 index 0000000000..5470bbb93f --- /dev/null +++ b/ci/split_package_smoke/env_check.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Helpers for the split-package smoke tests, called from the ci/test_*_split_packages.sh scripts. + +Commands: + conda-packages ... Fail unless exactly these cuOpt conda packages are installed + wheel-packages ... Fail unless exactly these libcuopt wheels are installed + cmake-prefixes Print the CMAKE_PREFIX_PATH of the installed wheels +""" + +import importlib +import importlib.metadata as metadata +import json +import os +import subprocess +import sys + + +def installed_conda_packages(prefix): + """Return the names of the cuOpt packages installed in the conda environment at prefix.""" + out = subprocess.run( + [ + os.environ.get("CONDA_EXE", "conda"), + "list", + "--prefix", + prefix, + "--json", + ], + check=True, + capture_output=True, + text=True, + ).stdout + return { + p["name"] + for p in json.loads(out) + if p["name"].startswith(("libcuopt", "cuopt")) + } + + +def installed_wheels(): + """Return the names of the libcuopt wheels installed in the current environment.""" + names = ( + d.metadata["Name"].lower().replace("_", "-") + for d in metadata.distributions() + ) + return {n for n in names if n.startswith("libcuopt")} + + +def check(installed, expected): + """Exit with an error unless the installed package names are exactly the expected ones.""" + if installed != set(expected): + print( + f"installed: {sorted(installed)}, expected: {sorted(expected)}", + file=sys.stderr, + ) + sys.exit(1) + + +def cmake_prefixes(): + """Return the CMake prefix paths of the installed wheels, joined with ";".""" + return ";".join( + os.path.dirname( + importlib.import_module(ep.value.split(":")[0]).__file__ + ) + for ep in metadata.entry_points(group="cmake.prefix") + ) + + +def main(): + """Run the command named in argv.""" + command, args = sys.argv[1], sys.argv[2:] + if command == "conda-packages": + check(installed_conda_packages(args[0]), args[1:]) + elif command == "wheel-packages": + check(installed_wheels(), args) + elif command == "cmake-prefixes": + print(cmake_prefixes()) + else: + sys.exit(__doc__) + + +if __name__ == "__main__": + main() diff --git a/ci/split_package_smoke/routing.cpp b/ci/split_package_smoke/routing.cpp new file mode 100644 index 0000000000..d6bfcd9b4c --- /dev/null +++ b/ci/split_package_smoke/routing.cpp @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include +#include + +#include +#include +#include + +// Four locations on a line, one vehicle: the best tour costs 1 + 1 + 1 + 3. +int main() +{ + raft::handle_t handle; + auto stream = handle.get_stream(); + std::vector cost = {0, 1, 2, 3, 1, 0, 1, 2, 2, 1, 0, 1, 3, 2, 1, 0}; + rmm::device_uvector d_cost(cost.size(), stream); + raft::update_device(d_cost.data(), cost.data(), cost.size(), stream); + handle.sync_stream(); + + cuopt::routing::data_model_view_t model(&handle, 4, 1); + model.add_cost_matrix(d_cost.data()); + cuopt::routing::solver_settings_t settings; + settings.set_time_limit(1); + + auto solution = cuopt::routing::solve(model, settings); + std::printf( + "%s objective=%f\n", solution.get_status_string().c_str(), solution.get_total_objective()); + return solution.get_status() == cuopt::routing::solution_status_t::SUCCESS && + std::fabs(solution.get_total_objective() - 6.0) < 1e-3 + ? 0 + : 1; +} diff --git a/ci/test_conda_split_packages.sh b/ci/test_conda_split_packages.sh new file mode 100755 index 0000000000..63b345d826 --- /dev/null +++ b/ci/test_conda_split_packages.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Installs each libcuopt-* conda package on its own, in a fresh environment, and builds and runs a +# C++ binary against it, to catch a package that only works because another one happened to be +# installed. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMPONENTS=(client mathopt routing) + +. "$(conda info --base)/etc/profile.d/conda.sh" + +conda config --set channel_priority strict +CPP_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") +RAPIDS_CHANNEL=rapidsai-nightly +rapids-is-release-build && RAPIDS_CHANNEL=rapidsai +CUDA_VERSION=${RAPIDS_CUDA_VERSION%.*} + +EXITCODE=0 +trap "EXITCODE=1" ERR + +for component in "${COMPONENTS[@]}"; do + echo "=== conda: libcuopt-${component} alone ===" + package="libcuopt-${component}" + expected=(libcuopt-client "${package}") + if [[ "${component}" == "client" ]]; then + expected=(libcuopt-client) + fi + + env_dir="$(mktemp -d)/env" + set +e + rapids-mamba-retry create --yes --prefix "${env_dir}" \ + --channel "${CPP_CHANNEL}" --channel "${RAPIDS_CHANNEL}" --channel conda-forge --channel nvidia \ + "${package}" "cuda-version=${CUDA_VERSION}" \ + cmake ninja cxx-compiler cuda-nvcc cuda-cudart-dev + + python "${REPO_ROOT}/ci/split_package_smoke/env_check.py" conda-packages "${env_dir}" "${expected[@]}" + + set +u + conda activate "${env_dir}" + set -u + bash "${REPO_ROOT}/ci/split_package_smoke/build_and_run.sh" "${component}" "${env_dir}" + conda deactivate + set -e +done + +rapids-logger "Test script exiting with value: ${EXITCODE}" +exit "${EXITCODE}" diff --git a/ci/test_wheel_split_packages.sh b/ci/test_wheel_split_packages.sh new file mode 100755 index 0000000000..fdc912a0b3 --- /dev/null +++ b/ci/test_wheel_split_packages.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Installs each libcuopt-* wheel on its own, in a fresh venv, and builds and runs a C++ binary +# against it, to catch a wheel that only works because another one happened to be installed. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMPONENTS=(client mathopt routing) + +source rapids-init-pip +bash "${REPO_ROOT}/ci/utils/install_openssl3_runtime.sh" + +SUFFIX="$(rapids-wheel-ctk-name-gen "${RAPIDS_CUDA_VERSION}")" +CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_client cuopt)") +MATHOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_mathopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") +ROUTING_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt_routing cuopt --cuda "$RAPIDS_CUDA_VERSION")") + +cat > "${PIP_CONSTRAINT}" <