diff --git a/.github/workflows/python_wheels.yml b/.github/workflows/python_wheels.yml index e9521d4475b1..cd611f46371f 100644 --- a/.github/workflows/python_wheels.yml +++ b/.github/workflows/python_wheels.yml @@ -28,6 +28,10 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: submodules: 'true' + - name: Set up homebrew + uses: Homebrew/actions/setup-homebrew@68fa6aeb1ccb0596d311f2b34ec74ec21ee68e54 + - name: Install libomp + run: brew install libomp - uses: conda-incubator/setup-miniconda@a4260408e20b96e80095f42ff7f1a15b27dd94ca # v3.0.4 with: miniforge-variant: Mambaforge diff --git a/CMakeLists.txt b/CMakeLists.txt index 2eb0c902bfc1..c4ca82937db7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -234,28 +234,17 @@ endif() find_package(Threads REQUIRED) +# -- OpenMP +include(cmake/FindOpenMPMacOS.cmake) if(USE_OPENMP) if(APPLE) - find_package(OpenMP) - if(NOT OpenMP_FOUND) - # Try again with extra path info; required for libomp 15+ from Homebrew - execute_process(COMMAND brew --prefix libomp - OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX - OUTPUT_STRIP_TRAILING_WHITESPACE) - set(OpenMP_C_FLAGS - "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") - set(OpenMP_CXX_FLAGS - "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") - set(OpenMP_C_LIB_NAMES omp) - set(OpenMP_CXX_LIB_NAMES omp) - set(OpenMP_omp_LIBRARY ${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib) - find_package(OpenMP REQUIRED) - endif() + find_openmp_macos() else() find_package(OpenMP REQUIRED) endif() endif() -#Add for IBM i + +# Add for IBM i if(${CMAKE_SYSTEM_NAME} MATCHES "OS400") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") set(CMAKE_CXX_ARCHIVE_CREATE " -X64 qc ") @@ -381,6 +370,10 @@ if(JVM_BINDINGS) xgboost_target_defs(xgboost4j) endif() +if(USE_OPENMP AND APPLE) + patch_openmp_path_macos(xgboost libxgboost) +endif() + if(KEEP_BUILD_ARTIFACTS_IN_BINARY_DIR) set_output_directory(xgboost ${xgboost_BINARY_DIR}/lib) else() diff --git a/cmake/FindOpenMPMacOS.cmake b/cmake/FindOpenMPMacOS.cmake new file mode 100644 index 000000000000..416a9b8cf1bc --- /dev/null +++ b/cmake/FindOpenMPMacOS.cmake @@ -0,0 +1,124 @@ +# Find OpenMP library on MacOS +# Automatically handle locating libomp from the Homebrew package manager + +# lint_cmake: -package/consistency + +macro(find_openmp_macos) + if(NOT APPLE) + message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}() must only be used on MacOS") + endif() + find_package(OpenMP) + if(NOT OpenMP_FOUND) + # Try again with extra path info. This step is required for libomp 15+ from Homebrew, + # as libomp 15.0+ from brew is keg-only + # See https://github.com/Homebrew/homebrew-core/issues/112107#issuecomment-1278042927. + execute_process(COMMAND brew --prefix libomp + OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE) + set(OpenMP_C_FLAGS + "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") + set(OpenMP_CXX_FLAGS + "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") + set(OpenMP_C_LIB_NAMES omp) + set(OpenMP_CXX_LIB_NAMES omp) + set(OpenMP_omp_LIBRARY ${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib) + find_package(OpenMP REQUIRED) + endif() +endmacro() + +# Patch libxgboost.dylib so that it depends on @rpath/libomp.dylib instead of +# /opt/homebrew/opt/libomp/lib/libomp.dylib or other hard-coded paths. +# Doing so enables XGBoost to interoperate with multiple kinds of OpenMP +# libraries. See https://github.com/microsoft/LightGBM/pull/6391 for detailed +# explanation. Adapted from https://github.com/microsoft/LightGBM/pull/6391 +# by James Lamb. +# MacOS only. +function(patch_openmp_path_macos target target_default_output_name) + if(NOT APPLE) + message(FATAL_ERROR "${CMAKE_CURRENT_FUNCTION}() must only be used on MacOS") + endif() + # Get path to libomp found at build time + get_target_property( + __OpenMP_LIBRARY_LOCATION + OpenMP::OpenMP_CXX + INTERFACE_LINK_LIBRARIES + ) + # Get the base name of the OpenMP lib + # Usually: libomp.dylib, libgomp.dylib, or libiomp.dylib + get_filename_component( + __OpenMP_LIBRARY_NAME + ${__OpenMP_LIBRARY_LOCATION} + NAME + ) + # Get the directory containing the OpenMP lib + get_filename_component( + __OpenMP_LIBRARY_DIR + ${__OpenMP_LIBRARY_LOCATION} + DIRECTORY + ) + # Get the name of the XGBoost lib, e.g. libxgboost + get_target_property( + __LIBXGBOOST_OUTPUT_NAME + ${target} + OUTPUT_NAME + ) + if(NOT __LIBXGBOOST_OUTPUT_NAME) + set(__LIBXGBOOST_OUTPUT_NAME "${target_default_output_name}") + endif() + + # Get the file name of the XGBoost lib, e.g. libxgboost.dylib + if(CMAKE_SHARED_LIBRARY_SUFFIX_CXX) + set( + __LIBXGBOOST_FILENAME_${target} "${__LIBXGBOOST_OUTPUT_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX_CXX}" + CACHE INTERNAL "Shared library filename ${target}" + ) + else() + set( + __LIBXGBOOST_FILENAME_${target} "${__LIBXGBOOST_OUTPUT_NAME}.dylib" + CACHE INTERNAL "Shared library filename ${target}" + ) + endif() + + message(STATUS "Creating shared lib for target ${target}: ${__LIBXGBOOST_FILENAME_${target}}") + + # Override the absolute path to OpenMP with a relative one using @rpath. + # + # This also ensures that if a libomp.dylib has already been loaded, it'll just use that. + if(KEEP_BUILD_ARTIFACTS_IN_BINARY_DIR) + set(__LIB_DIR ${xgboost_BINARY_DIR}/lib) + else() + set(__LIB_DIR ${xgboost_SOURCE_DIR}/lib) + endif() + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND + install_name_tool + -change + ${__OpenMP_LIBRARY_LOCATION} + "@rpath/${__OpenMP_LIBRARY_NAME}" + "${__LIBXGBOOST_FILENAME_${target}}" + WORKING_DIRECTORY ${__LIB_DIR} + ) + message(STATUS + "${__LIBXGBOOST_FILENAME_${target}}: " + "Replacing hard-coded OpenMP install_name with '@rpath/${__OpenMP_LIBRARY_NAME}'..." + ) + # Add RPATH entries to ensure the loader looks in the following, in the following order: + # + # - /opt/homebrew/opt/libomp/lib (where 'brew install' / 'brew link' puts libomp.dylib) + # - ${__OpenMP_LIBRARY_DIR} (wherever find_package(OpenMP) found OpenMP at build time) + # + # Note: This list will only be used if libomp.dylib isn't already loaded into memory. + # So Conda users will likely use ${CONDA_PREFIX}/libomp.dylib + execute_process(COMMAND brew --prefix libomp + OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE) + set_target_properties( + ${target} + PROPERTIES + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "${HOMEBREW_LIBOMP_PREFIX}/lib;${__OpenMP_LIBRARY_DIR}" + INSTALL_RPATH_USE_LINK_PATH FALSE + ) +endfunction() diff --git a/jvm-packages/CMakeLists.txt b/jvm-packages/CMakeLists.txt index 36ed61a6b063..8c20eb135a9b 100644 --- a/jvm-packages/CMakeLists.txt +++ b/jvm-packages/CMakeLists.txt @@ -25,3 +25,8 @@ target_include_directories(xgboost4j ${PROJECT_SOURCE_DIR}/rabit/include) set_output_directory(xgboost4j ${PROJECT_SOURCE_DIR}/lib) + +# MacOS: Patch libxgboost4j.dylib to use @rpath/libomp.dylib +if(USE_OPENMP AND APPLE) + patch_openmp_path_macos(xgboost4j libxgboost4j) +endif() diff --git a/tests/buildkite/test-macos-m1-clang11.sh b/tests/buildkite/test-macos-m1-clang11.sh index 401701b42223..a3f1eab6c589 100755 --- a/tests/buildkite/test-macos-m1-clang11.sh +++ b/tests/buildkite/test-macos-m1-clang11.sh @@ -18,10 +18,11 @@ set -x mkdir build pushd build export JAVA_HOME=$(/usr/libexec/java_home) -cmake .. -GNinja -DJVM_BINDINGS=ON -DUSE_OPENMP=OFF -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 +cmake .. -GNinja -DJVM_BINDINGS=ON -DUSE_OPENMP=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 ninja -v popd rm -rf build +otool -L lib/libxgboost.dylib set +x echo "--- Upload Python wheel" diff --git a/tests/ci_build/build_python_wheels.sh b/tests/ci_build/build_python_wheels.sh index 03a9bc1d6026..8d18dfca51eb 100644 --- a/tests/ci_build/build_python_wheels.sh +++ b/tests/ci_build/build_python_wheels.sh @@ -11,33 +11,19 @@ fi platform_id=$1 commit_id=$2 -# Bundle libomp 11.1.0 when targeting MacOS. -# This is a workaround in order to prevent segfaults when running inside a Conda environment. -# See https://github.com/dmlc/xgboost/issues/7039#issuecomment-1025125003 for more context. -# The workaround is also used by the scikit-learn project. if [[ "$platform_id" == macosx_* ]]; then - # Make sure to use a libomp version binary compatible with the oldest - # supported version of the macos SDK as libomp will be vendored into the - # XGBoost wheels for MacOS. - if [[ "$platform_id" == macosx_arm64 ]]; then # MacOS, Apple Silicon - # arm64 builds must cross compile because CI is on x64 - # cibuildwheel will take care of cross-compilation. wheel_tag=macosx_12_0_arm64 cpython_ver=39 cibw_archs=arm64 export MACOSX_DEPLOYMENT_TARGET=12.0 - #OPENMP_URL="https://anaconda.org/conda-forge/llvm-openmp/11.1.0/download/osx-arm64/llvm-openmp-11.1.0-hf3c4609_1.tar.bz2" - OPENMP_URL="https://xgboost-ci-jenkins-artifacts.s3.us-west-2.amazonaws.com/llvm-openmp-11.1.0-hf3c4609_1-osx-arm64.tar.bz2" elif [[ "$platform_id" == macosx_x86_64 ]]; then # MacOS, Intel wheel_tag=macosx_10_15_x86_64.macosx_11_0_x86_64.macosx_12_0_x86_64 cpython_ver=39 cibw_archs=x86_64 export MACOSX_DEPLOYMENT_TARGET=10.15 - #OPENMP_URL="https://anaconda.org/conda-forge/llvm-openmp/11.1.0/download/osx-64/llvm-openmp-11.1.0-hda6cdc1_1.tar.bz2" - OPENMP_URL="https://xgboost-ci-jenkins-artifacts.s3.us-west-2.amazonaws.com/llvm-openmp-11.1.0-hda6cdc1_1-osx-64.tar.bz2" else echo "Platform not supported: $platform_id" exit 3 @@ -48,26 +34,23 @@ if [[ "$platform_id" == macosx_* ]]; then export CIBW_ENVIRONMENT=${setup_env_var} export CIBW_TEST_SKIP='*-macosx_arm64' export CIBW_BUILD_VERBOSITY=3 - - mamba create -n build $OPENMP_URL - PREFIX="$HOME/miniconda3/envs/build" - - # Set up build flags for cibuildwheel - # This is needed to bundle libomp lib we downloaded earlier - export CC=/usr/bin/clang - export CXX=/usr/bin/clang++ - export CPPFLAGS="$CPPFLAGS -Xpreprocessor -fopenmp" - export CFLAGS="$CFLAGS -I$PREFIX/include" - export CXXFLAGS="$CXXFLAGS -I$PREFIX/include" - export LDFLAGS="$LDFLAGS -Wl,-rpath,$PREFIX/lib -L$PREFIX/lib -lomp" else echo "Platform not supported: $platform_id" exit 2 fi +# Tell delocate-wheel to not vendor libomp.dylib into the wheel" +export CIBW_REPAIR_WHEEL_COMMAND_MACOS="delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} --exclude libomp.dylib" + python -m pip install cibuildwheel python -m cibuildwheel python-package --output-dir wheelhouse python tests/ci_build/rename_whl.py \ --wheel-path wheelhouse/*.whl \ --commit-hash ${commit_id} \ --platform-tag ${wheel_tag} + +# List dependencies of libxgboost.dylib +mkdir tmp +unzip -j wheelhouse/xgboost-*.whl xgboost/lib/libxgboost.dylib -d tmp +otool -L tmp/libxgboost.dylib +rm -rf tmp