Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions ci/split_package_smoke/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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})
23 changes: 23 additions & 0 deletions ci/split_package_smoke/build_and_run.sh
Original file line number Diff line number Diff line change
@@ -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 <client|mathopt|routing> <CMAKE_PREFIX_PATH>
#
# 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}"
14 changes: 14 additions & 0 deletions ci/split_package_smoke/client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include <cuopt/status_codes.h>

#include <cstdio>

// 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;
}
86 changes: 86 additions & 0 deletions ci/split_package_smoke/env_check.py
Original file line number Diff line number Diff line change
@@ -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 <env_prefix> <expected_name>... Fail unless exactly these cuOpt conda packages are installed
wheel-packages <expected_name>... 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()
36 changes: 36 additions & 0 deletions ci/split_package_smoke/routing.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include <cuopt/routing/data_model_view.hpp>
#include <cuopt/routing/solve.hpp>

#include <raft/core/handle.hpp>
#include <rmm/device_uvector.hpp>

#include <cmath>
#include <cstdio>
#include <vector>

// 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<float> cost = {0, 1, 2, 3, 1, 0, 1, 2, 2, 1, 0, 1, 3, 2, 1, 0};
rmm::device_uvector<float> 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<int, float> model(&handle, 4, 1);
model.add_cost_matrix(d_cost.data());
cuopt::routing::solver_settings_t<int, float> 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;
}
52 changes: 52 additions & 0 deletions ci/test_conda_split_packages.sh
Original file line number Diff line number Diff line change
@@ -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}" \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's scope the set +e's to just the blocks that install the environment, check the packages, and run the smoke tests. Same in the other script.

Suggested change
rapids-mamba-retry create --yes --prefix "${env_dir}" \
set +e
rapids-mamba-retry create --yes --prefix "${env_dir}" \

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 26b6bb4

--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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
conda deactivate
conda deactivate
set -e

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 26b6bb4

set -e
done

rapids-logger "Test script exiting with value: ${EXITCODE}"
exit "${EXITCODE}"
57 changes: 57 additions & 0 deletions ci/test_wheel_split_packages.sh
Original file line number Diff line number Diff line change
@@ -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}" <<CONSTRAINTS
libcuopt-client @ file://$(echo "${CLIENT_WHEELHOUSE}"/libcuopt_client-*.whl)
libcuopt-mathopt-${SUFFIX} @ file://$(echo "${MATHOPT_WHEELHOUSE}"/libcuopt_mathopt_*.whl)
libcuopt-routing-${SUFFIX} @ file://$(echo "${ROUTING_WHEELHOUSE}"/libcuopt_routing_*.whl)
CONSTRAINTS

EXITCODE=0
trap "EXITCODE=1" ERR

for component in "${COMPONENTS[@]}"; do
echo "=== wheel: libcuopt-${component} alone ==="

if [[ "${component}" == "client" ]]; then
package=libcuopt-client
expected=(libcuopt-client)
else
package="libcuopt-${component}-${SUFFIX}"
expected=(libcuopt-client "${package}")
fi

venv="$(mktemp -d)/venv"
set +e
python -m venv "${venv}"
. "${venv}/bin/activate"
pip install --upgrade pip cmake ninja
rapids-pip-retry install --prefer-binary --constraint "${PIP_CONSTRAINT}" "${package}"

python "${REPO_ROOT}/ci/split_package_smoke/env_check.py" wheel-packages "${expected[@]}"
prefixes=$(python "${REPO_ROOT}/ci/split_package_smoke/env_check.py" cmake-prefixes)
bash "${REPO_ROOT}/ci/split_package_smoke/build_and_run.sh" "${component}" "${prefixes}"
deactivate
set -e
done

rapids-logger "Test script exiting with value: ${EXITCODE}"
exit "${EXITCODE}"
Loading