diff --git a/.github/workflows/build_images.yaml b/.github/workflows/build_images.yaml index 4b7881e806..dd9a0735e0 100644 --- a/.github/workflows/build_images.yaml +++ b/.github/workflows/build_images.yaml @@ -61,6 +61,16 @@ jobs: run: | git rev-parse HEAD > ./ci/docker/context/COMMIT_SHA git log -n1 --pretty='%ct' > ./ci/docker/context/COMMIT_TIME + # cpp/include and cpp/src aren't in the pip wheel; cuopt_jni.cpp needs both (the latter for + # internal headers like pdlp/cuopt_c_internal.hpp) to build the JNI library in-image. + - name: Stage Java sources for the in-image dynamic build + run: | + mkdir -p ./ci/docker/context/repo + cp -r ./java ./ci/docker/context/repo/java + mkdir -p ./ci/docker/context/repo/cpp + cp -r ./cpp/include ./ci/docker/context/repo/cpp/include + cp -r ./cpp/src ./ci/docker/context/repo/cpp/src + cp ./ci/docker/build_java_dynamic.sh ./ci/docker/context/build_java_dynamic.sh - name: Login to NGC uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: diff --git a/ci/docker/Dockerfile b/ci/docker/Dockerfile index 5c6e4c3b39..8bfa48ed0c 100644 --- a/ci/docker/Dockerfile +++ b/ci/docker/Dockerfile @@ -89,10 +89,37 @@ RUN \ && apt-get purge -y --autoremove gcc \ && rm -rf /var/lib/apt/lists/* +# Own stage so the build toolchain doesn't persist into cuopt-final. See build_java_dynamic.sh. +FROM install-env AS java-build + +ARG PYTHON_SHORT_VER + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + openjdk-17-jdk \ + build-essential \ + cmake \ + && rm -rf /var/lib/apt/lists/* + +# CMake's find_package(CUDAToolkit REQUIRED) needs nvcc on PATH; reuse cuda-headers instead of +# installing the toolkit again via apt. +COPY --from=cuda-headers /usr/local/cuda /usr/local/cuda +ENV PATH="/usr/local/cuda/bin:${PATH}" + +COPY ./repo /build/repo +COPY ./build_java_dynamic.sh /build/build_java_dynamic.sh +RUN bash /build/build_java_dynamic.sh /build/repo \ + "/usr/local/lib/python${PYTHON_SHORT_VER}/dist-packages/libcuopt" /opt/cuopt/java + FROM install-env AS cuopt-final ARG PYTHON_SHORT_VER +# Headless JRE to run the Java bindings; version matches java/cuopt/pom.xml's maven.compiler.release. +RUN apt-get update \ + && apt-get install -y --no-install-recommends openjdk-17-jre-headless \ + && rm -rf /var/lib/apt/lists/* + # Make cuopt_grpc_server, cuopt_cli, and shared libraries available to all processes # (profile.d scripts are only sourced by login shells; ENV works for all containers) ENV PATH="/usr/local/cuda/bin:/usr/bin:/usr/local/bin:/usr/local/nvidia/bin/:/usr/local/lib/python${PYTHON_SHORT_VER}/dist-packages/libcuopt/bin:${PATH}" @@ -119,6 +146,8 @@ COPY --from=cuda-libs /usr/local/cuda/lib64/libnvJitLink* /usr/local/cuda/lib64/ # Copy CUDA headers needed for runtime compilation (e.g., CuPy NVRTC). COPY --from=cuda-headers /usr/local/cuda/include/ /usr/local/cuda/include/ +COPY --from=java-build /opt/cuopt/java /opt/cuopt/java + # Entrypoint supports server selection: # Default: Python REST server # CUOPT_SERVER_TYPE=grpc: gRPC server (uses CUOPT_SERVER_PORT, CUOPT_GPU_COUNT) diff --git a/ci/docker/Dockerfile.ubi b/ci/docker/Dockerfile.ubi index 5668d7864f..fb208ca5a8 100644 --- a/ci/docker/Dockerfile.ubi +++ b/ci/docker/Dockerfile.ubi @@ -61,8 +61,27 @@ RUN \ dnf clean all && \ ln -sf /usr/bin/python3.14 /usr/bin/python +# Own stage so the build toolchain doesn't persist into cuopt-final. See build_java_dynamic.sh. +FROM install-env AS java-build + +# No java-17-openjdk on UBI10 AppStream (only 21, 25); 21 satisfies the JDK 17+ requirement. +RUN dnf install -y java-21-openjdk-devel cmake gcc-c++ make && dnf clean all + +# CMake's find_package(CUDAToolkit REQUIRED) needs nvcc on PATH; reuse cuda-headers instead of +# installing the toolkit again via dnf. +COPY --from=cuda-headers /usr/local/cuda /usr/local/cuda +ENV PATH="/usr/local/cuda/bin:${PATH}" + +COPY ./repo /build/repo +COPY ./build_java_dynamic.sh /build/build_java_dynamic.sh +RUN bash /build/build_java_dynamic.sh /build/repo \ + /usr/local/lib64/python3.14/site-packages/libcuopt /opt/cuopt/java + FROM install-env AS cuopt-final +# Headless JRE to run the Java bindings; version matches java/cuopt/pom.xml's maven.compiler.release. +RUN dnf install -y java-21-openjdk-headless && dnf clean all + # Register python via alternatives so the system alternatives database is # consistent with the /usr/bin/python symlink created in install-env. # NOTE: do NOT register python3 → python3.14 here: /usr/bin/dnf uses @@ -110,6 +129,8 @@ COPY --from=cuda-libs /usr/local/cuda/lib64/libnvJitLink* /usr/local/cuda/lib64/ # Copy CUDA headers needed for runtime compilation (e.g., CuPy NVRTC) COPY --from=cuda-headers /usr/local/cuda/include/ /usr/local/cuda/include/ +COPY --from=java-build /opt/cuopt/java /opt/cuopt/java + COPY ./entrypoint.sh /opt/cuopt/entrypoint.sh ENTRYPOINT ["/opt/cuopt/entrypoint.sh"] CMD ["python", "-m", "cuopt_server.cuopt_service"] diff --git a/ci/docker/build_java_dynamic.sh b/ci/docker/build_java_dynamic.sh new file mode 100644 index 0000000000..d94a77660a --- /dev/null +++ b/ci/docker/build_java_dynamic.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Builds libcuopt_jni.so against the pip-installed libcuopt.so and compiles the Java classes with +# javac (java/cuopt/pom.xml has no non-test dependency, so Maven isn't needed). Writes cuopt.jar +# and libcuopt_jni.so to OUT_DIR. + +set -euo pipefail + +REPO_ROOT="${1:?missing repo root}" +CUOPT_SITE_PACKAGES="${2:?missing path to the pip-installed libcuopt package, e.g. /usr/local/lib/python3.14/dist-packages/libcuopt}" +OUT_DIR="${3:?missing output directory}" + +if [[ ! -f "${CUOPT_SITE_PACKAGES}/lib64/libcuopt.so" ]]; then + echo "libcuopt.so not found under ${CUOPT_SITE_PACKAGES}/lib64; is libcuopt pip-installed yet?" >&2 + exit 1 +fi + +# libcuopt.so resolves its own dependencies (TBB, NCCL, cuDSS, rmm, rapids_logger, cublas, ...) +# via RPATH, so cuopt_jni.so only needs to find libcuopt.so itself -- CUOPT_RUNTIME_LIBRARY_DIR +# bakes that into its RPATH too, no LD_LIBRARY_PATH needed. +export CUOPT_PREFIX="${CUOPT_SITE_PACKAGES}" +export CUOPT_LIBRARY="${CUOPT_SITE_PACKAGES}/lib64/libcuopt.so" +export CUOPT_RUNTIME_LIBRARY_DIR="${CUOPT_SITE_PACKAGES}/lib64" +# rmm and rapids_logger are separate pip packages with their own include dirs (unlike conda, +# where CUOPT_PREFIX/include/rapids covers all three). +PIP_SITE_PACKAGES="$(dirname "${CUOPT_SITE_PACKAGES}")" +export CUOPT_EXTRA_INCLUDE_DIRS="${REPO_ROOT}/cpp/include;${REPO_ROOT}/cpp/src;${PIP_SITE_PACKAGES}/librmm/include;${PIP_SITE_PACKAGES}/rapids_logger/include" +export CUOPT_JAVA_NATIVE_BUILD_DIR="${REPO_ROOT}/java/cuopt/build/native" + +cd "${REPO_ROOT}" +bash java/cuopt/scripts/build_native.sh + +GEN_SRC_DIR="${REPO_ROOT}/java/cuopt/target/generated-sources/cuopt" +bash java/cuopt/scripts/generate_constants.sh \ + "${REPO_ROOT}/cpp/include/cuopt/mathematical_optimization/constants.h" \ + "${GEN_SRC_DIR}" + +CLASSES_DIR="$(mktemp -d)" +mapfile -t JAVA_SOURCES < <(find "${REPO_ROOT}/java/cuopt/src/main/java" "${GEN_SRC_DIR}" -name '*.java') +javac -d "${CLASSES_DIR}" --release 17 "${JAVA_SOURCES[@]}" + +mkdir -p "${OUT_DIR}" +jar cf "${OUT_DIR}/cuopt.jar" -C "${CLASSES_DIR}" . +cp "${CUOPT_JAVA_NATIVE_BUILD_DIR}/libcuopt_jni.so" "${OUT_DIR}/" +rm -rf "${CLASSES_DIR}" + +echo "Wrote ${OUT_DIR}/cuopt.jar and ${OUT_DIR}/libcuopt_jni.so"