diff --git a/CMakeLists.txt b/CMakeLists.txt index 62843068f6..0646ba0ff1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,8 @@ option(ENABLE_CNPY "Enable cnpy usage" OFF) cmake_dependent_option(ENABLE_ELPA "Enable ELPA eigensolver" ON "ENABLE_MPI" OFF) cmake_dependent_option(ENABLE_LIBRI "Enable LibRI for hybrid functional" OFF "ENABLE_LCAO;ENABLE_MPI" OFF) +cmake_dependent_option(ENABLE_LIBRI_GPU "Enable experimental LibRI CUDA contractions through DDLA" + OFF "ENABLE_LIBRI;USE_CUDA" OFF) cmake_dependent_option(ENABLE_PEXSI "Enable PEXSI for LCAO" OFF "ENABLE_LCAO;ENABLE_MPI" OFF) cmake_dependent_option(ENABLE_MLALGO "Enable the machine learning algorithms" OFF "ENABLE_LCAO;ENABLE_MPI" OFF) @@ -606,9 +608,10 @@ endfunction() if(ENABLE_LIBRI) set_if_higher(CMAKE_CXX_STANDARD 14) + if(ENABLE_LIBRI_GPU) + set_if_higher(CMAKE_CXX_STANDARD 17) + endif() find_package(LibRI REQUIRED) - find_package(LibComm REQUIRED) - find_package(cereal REQUIRED CONFIG) endif() if(ENABLE_LIBXC) diff --git a/cmake/CollectBuildInfoVars.cmake b/cmake/CollectBuildInfoVars.cmake index 740c50ca4b..e72b2cebbc 100644 --- a/cmake/CollectBuildInfoVars.cmake +++ b/cmake/CollectBuildInfoVars.cmake @@ -382,6 +382,7 @@ list(APPEND CMAKE_OPTIONS_LIST " USE_ROCM=${USE_ROCM}") list(APPEND CMAKE_OPTIONS_LIST " ENABLE_LCAO=${ENABLE_LCAO}") list(APPEND CMAKE_OPTIONS_LIST " ENABLE_ELPA=${ENABLE_ELPA}") list(APPEND CMAKE_OPTIONS_LIST " ENABLE_LIBXC=${ENABLE_LIBXC}") +list(APPEND CMAKE_OPTIONS_LIST " ENABLE_LIBRI_GPU=${ENABLE_LIBRI_GPU}") list(APPEND CMAKE_OPTIONS_LIST " ENABLE_MLALGO=${ENABLE_MLALGO}") list(APPEND CMAKE_OPTIONS_LIST " ENABLE_ASAN=${ENABLE_ASAN}") list(APPEND CMAKE_OPTIONS_LIST " BUILD_TESTING=${BUILD_TESTING}") @@ -440,6 +441,7 @@ message(" DeePMD = ${ABACUS_DEEPMD_VERSION}") message(" NEP = ${ABACUS_NEP_VERSION}") message(" Features : Libxc = ${ABACUS_LIBXC_VERSION}") message(" LibRI = ${ABACUS_LIBRI_VERSION}") +message(" LibRI GPU = ${ENABLE_LIBRI_GPU}") message(" DFT-D4 = ${ABACUS_DFTD4_VERSION}") message(" JSON = ${ABACUS_JSON_VERSION}") message(" PEXSI = ${ABACUS_PEXSI_VERSION}") diff --git a/cmake/modules/FindDDLA.cmake b/cmake/modules/FindDDLA.cmake new file mode 100644 index 0000000000..79024169a4 --- /dev/null +++ b/cmake/modules/FindDDLA.cmake @@ -0,0 +1,29 @@ +# CUDA adapter for the DDLA interface currently consumed by LibRI. +# DDLA_ROOT or CMAKE_PREFIX_PATH may name an installation prefix; the explicit +# DDLA_INCLUDE_DIR and DDLA_LIBRARY overrides remain supported. + +find_path(DDLA_INCLUDE_DIR + NAMES ddla/ddla_connector.h + HINTS ${DDLA_ROOT} + PATH_SUFFIXES include) +find_library(DDLA_LIBRARY + NAMES ddla + HINTS ${DDLA_ROOT} + PATH_SUFFIXES lib lib64) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(DDLA + REQUIRED_VARS DDLA_INCLUDE_DIR DDLA_LIBRARY) + +if(DDLA_FOUND AND NOT TARGET DDLA::DDLA) + add_library(DDLA::DDLA UNKNOWN IMPORTED) + set_target_properties(DDLA::DDLA PROPERTIES + IMPORTED_LOCATION "${DDLA_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${DDLA_INCLUDE_DIR}" + INTERFACE_COMPILE_DEFINITIONS DDLA_USE_CUDA + INTERFACE_COMPILE_FEATURES cxx_std_17 + INTERFACE_LINK_LIBRARIES + "MPI::MPI_CXX;CUDA::cudart;CUDA::cublas;CUDA::curand;CUDA::cusolver") +endif() + +mark_as_advanced(DDLA_INCLUDE_DIR DDLA_LIBRARY) diff --git a/cmake/modules/FindLibRI.cmake b/cmake/modules/FindLibRI.cmake index fa7e8833f4..5417d38754 100644 --- a/cmake/modules/FindLibRI.cmake +++ b/cmake/modules/FindLibRI.cmake @@ -4,6 +4,7 @@ # # LIBRI_FOUND - True if LibRI is found. # LIBRI_DIR - Where to find LibRI files. +# LibRI::LibRI - Header-only target, including optional DDLA requirements. find_path(LIBRI_DIR include/RI/version.h @@ -13,6 +14,11 @@ find_path(LIBRI_DIR ) if(NOT LIBRI_DIR) + if(ENABLE_LIBRI_GPU) + message(FATAL_ERROR + "ENABLE_LIBRI_GPU requires an explicitly supplied, patched LIBRI_DIR; " + "see docs/advanced/install.md") + endif() include(FetchContent) FetchContent_Declare( LibRI @@ -26,5 +32,62 @@ endif() include(FindPackageHandleStandardArgs) find_package_handle_standard_args(LibRI DEFAULT_MSG LIBRI_DIR) -# Copy the results to the output variables and target. +if(LibRI_FOUND AND NOT TARGET LibRI::LibRI) + find_package(LibComm REQUIRED) + find_package(cereal REQUIRED CONFIG) + + add_library(LibRI::LibRI INTERFACE IMPORTED) + set_target_properties(LibRI::LibRI PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${LIBRI_DIR}/include;${LIBCOMM_DIR}/include" + INTERFACE_COMPILE_FEATURES cxx_std_14 + INTERFACE_LINK_LIBRARIES "MPI::MPI_CXX;cereal::cereal") + if(ENABLE_OPENMP) + target_link_libraries(LibRI::LibRI INTERFACE OpenMP::OpenMP_CXX) + endif() + # Reuse ABACUS's selected math backend. Special platforms without these + # targets retain their existing numerical libraries in the final link. + foreach(_libri_math_target IN ITEMS LAPACK::LAPACK BLAS::BLAS) + if(TARGET ${_libri_math_target}) + target_link_libraries(LibRI::LibRI INTERFACE ${_libri_math_target}) + endif() + endforeach() + + if(ENABLE_LIBRI_GPU) + find_package(DDLA REQUIRED) + target_compile_definitions(LibRI::LibRI INTERFACE __GPU_RI __DDLA_RI) + target_compile_features(LibRI::LibRI INTERFACE cxx_std_17) + target_link_libraries(LibRI::LibRI INTERFACE DDLA::DDLA) + + include(CMakePushCheckState) + include(CheckCXXSourceCompiles) + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_LIBRARIES LibRI::LibRI) + # Recheck when users change dependency paths or apply compatibility fixes. + unset(LIBRI_GPU_API_WORKS CACHE) + check_cxx_source_compiles([=[ + #include + #include + template void check_gpu() + { + RI::LRI lri; + lri.cal_mode = RI::LRI_Cal_Mode::GPU; + std::map>, RI::Tensor>> result; + lri.cal_loop3({}, result, 1.0); + } + int main() + { + check_gpu(); + check_gpu>(); + } + ]=] LIBRI_GPU_API_WORKS) + cmake_pop_check_state() + if(NOT LIBRI_GPU_API_WORKS) + message(FATAL_ERROR + "LibRI/DDLA GPU compile/link check failed. Supply compatible, patched " + "LibRI and CUDA DDLA installations; see docs/advanced/install.md " + "and the CMake configure log.") + endif() + endif() +endif() + mark_as_advanced(LIBRI_DIR) diff --git a/cmake/modules/FindMKL.cmake b/cmake/modules/FindMKL.cmake index 8d324666bc..d5e4486a7b 100644 --- a/cmake/modules/FindMKL.cmake +++ b/cmake/modules/FindMKL.cmake @@ -241,14 +241,14 @@ if(MKL_FOUND) set(_mkl_base_archives ${MKL_INTERFACE_LIB} ${MKL_THREAD} ${MKL_CORE}) _mkl_link_group(_mkl_base ${_mkl_base_archives}) - add_library(abacus_mkl INTERFACE) - add_library(abacus::mkl ALIAS abacus_mkl) + # Imported interfaces also propagate into dependency compile/link probes. + add_library(abacus::mkl INTERFACE IMPORTED GLOBAL) target_include_directories( - abacus_mkl + abacus::mkl INTERFACE "${MKL_INCLUDE}" "${MKL_FFTW_INCLUDE}") - target_link_libraries(abacus_mkl INTERFACE ${_mkl_base} ${_mkl_runtime}) + target_link_libraries(abacus::mkl INTERFACE ${_mkl_base} ${_mkl_runtime}) if(ENABLE_MPI) add_library(abacus_mkl_scalapack INTERFACE) diff --git a/docs/advanced/install.md b/docs/advanced/install.md index ab1939181b..a4aafa470c 100644 --- a/docs/advanced/install.md +++ b/docs/advanced/install.md @@ -81,6 +81,50 @@ The new EXX implementation directly depends on [LibRI](https://github.com/abacus `ENABLE_LIBCOMM` is deprecated because LibComm is not a standalone ABACUS feature. CMake locates it automatically as a dependency of LibRI. If you prefer using manually downloaded libraries, enable LibRI and provide their locations via `-DLIBRI_DIR=/path/to/LibRI` and `-DLIBCOMM_DIR=/path/to/LibComm`. ``` +Experimental LCAO GPU contractions additionally require `ENABLE_LIBRI_GPU=ON` +(default OFF), `USE_CUDA=ON`, and a CUDA build of DDLA. Like other dependent +options, GPU RI is disabled when its LibRI/CUDA prerequisites are disabled. +INPUT `exx_device` defaults to `cpu`, retaining CPU contractions even with +`device gpu`. To use GPU contractions, set both `device gpu` and `exx_device gpu`. +The latter requires GPU RI to be compiled in. Real-space EXX symmetry reduction +is unsupported by this backend: with `symmetry 1`, use `exx_device cpu` or +`exx_symmetry_realspace 0`. + +The selector applies to the LCAO Kohn-Sham exchange engine. Independent LR and +RDMFT exchange engines retain CPU contractions; LR inherits the selected backend +only when it reuses the Kohn-Sham exchange object. + +With the LibRI version below, all MPI ranks on a node must see the same allocated +GPU list, with at least as many visible GPUs as local ranks. Giving each rank its +own single-GPU `CUDA_VISIBLE_DEVICES` list fails LibRI's GPU-count check when +multiple ranks share a node. For example, on one node allocated GPUs 0 and 1: + +```bash +CUDA_VISIBLE_DEVICES=0,1 mpirun -np 2 abacus +``` + +The tested combination is LibRI `6ce3c8480ae38a085746dc81cc4dbb59b8074430` +and DDLA `v0.0.4` (`9c404ac65148ec1b48977b846f38ba6ef9e134f7`). LibRI requires the +compatibility and force-accumulation fixes in `tests/lcao_hse_gpu/libri-compat.patch`: +normalize `include/RI/global/gpu/GPU_Backend.h` and +`include/RI/ri/LRI-cal_loop3_GPU.hpp` to LF, then run +`git apply --unidiff-zero /path/to/abacus/tests/lcao_hse_gpu/libri-compat.patch` +from the LibRI source directory. GPU mode requires this explicitly supplied +LibRI tree; the default CPU dependency download is unchanged. + +```bash +cmake -B build -DENABLE_LIBRI=ON -DUSE_CUDA=ON -DENABLE_LIBRI_GPU=ON \ + -DLIBRI_DIR=/path/to/patched/LibRI -DDDLA_ROOT=/path/to/DDLA +``` + +`CMAKE_PREFIX_PATH` can also locate DDLA. A configure-time compile/link check +rejects incompatible GPU APIs, but does not validate numerical correctness. +The standalone tests in `tests/lcao_hse_gpu/cmake` additionally accept +`ENABLE_LIBRI_GPU_TESTS=ON` with `ENABLE_LIBRI_GPU=ON` to check real and complex +GPU contractions with signed accumulation. This optional CTest requires one GPU. +This experimental backend has additional GPU memory requirements; its temporary +buffer limit is not a bound on total GPU memory. + ## Build with DFT-D4 support ABACUS can use the external [DFT-D4](https://github.com/dftd4/dftd4) library for Grimme's DFT-D4 dispersion correction. DFT-D4 support is optional and disabled by default. diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 9f7ab0196c..be0a36eef6 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -274,7 +274,7 @@ set(_abacus_feature_libs $<$:PEXSI::PEXSI> $<$:cnpy> $<$:ZLIB::ZLIB> - $<$:cereal::cereal> + $<$:LibRI::LibRI> $<$:Libxc::xc> $<$:PLUMED::plumed>) set(_abacus_feature_include_dirs) @@ -307,12 +307,6 @@ if(ENABLE_CNPY) endif() endif() -if(ENABLE_LIBRI) - list(APPEND _abacus_feature_include_dirs - ${LIBRI_DIR}/include - ${LIBCOMM_DIR}/include) -endif() - if(DEFINED DeePMD_DIR) if(DeePMDC_FOUND) list(APPEND _abacus_feature_libs DeePMD::deepmd_c) @@ -395,6 +389,8 @@ target_compile_options(abacus_compile_requirements INTERFACE foreach(_abacus_feature_target IN ITEMS nlohmann_json::nlohmann_json + LibRI::LibRI + DDLA::DDLA cereal::cereal ELPA::ELPA Libxc::xc diff --git a/source/source_io/module_parameter/input_parameter.h b/source/source_io/module_parameter/input_parameter.h index d86993b63d..d393d60291 100644 --- a/source/source_io/module_parameter/input_parameter.h +++ b/source/source_io/module_parameter/input_parameter.h @@ -628,6 +628,7 @@ struct Input_para ///< the evaluation of Fock exchange double exx_mixing_beta = 1.0; ///< mixing_beta for outer-loop when exx_separate_loop=1 std::string exx_real_number = "default"; ///< exx calculated in real or complex + std::string exx_device = "cpu"; ///< computing device for LCAO EXX tensor contractions double exx_pca_threshold = 0.0001; ///< threshold to screen on-site ABFs in exx double exx_c_threshold = 0.0001; ///< threshold to screen C matrix in exx double exx_v_threshold = 0.1; ///< threshold to screen C matrix in exx diff --git a/source/source_io/module_parameter/read_inp_exx_dftu.cpp b/source/source_io/module_parameter/read_inp_exx_dftu.cpp index bbecf5cace..159787b113 100644 --- a/source/source_io/module_parameter/read_inp_exx_dftu.cpp +++ b/source/source_io/module_parameter/read_inp_exx_dftu.cpp @@ -237,6 +237,48 @@ void ReadInput::item_exx() sync_stringvec(input.exx_fock_lambda, para.input.exx_fock_lambda.size(), ""); this->add_item(item); } + { + Input_Item item("exx_device"); + item.annotation = "computing device for LCAO EXX tensor contractions"; + item.category = "Exact Exchange (LCAO)"; + item.type = "String"; + item.description = R"(Selects the LibRI tensor-contraction device for LCAO Kohn-Sham exact exchange. +* cpu: retain CPU contractions, including when device=gpu. +* gpu: use the experimental GPU backend; requires ENABLE_LIBRI_GPU=ON and device=gpu. +GPU contractions do not support real-space EXX symmetry reduction. With symmetry=1, +use exx_device=cpu or disable exx_symmetry_realspace. +Independent LR and RDMFT exchange engines retain CPU contractions. LR inherits +this selection only when it reuses the Kohn-Sham exchange object. +For GPU contractions, all MPI ranks on a node must see the same allocated GPU +list, with at least as many visible GPUs as local ranks; per-rank single-GPU +masking is unsupported.)"; + item.default_value = "cpu"; + item.set_availability("basis_type==lcao"); + read_sync_string(input.exx_device); + item.check_value = [](const Input_Item& item, const Parameter& para) { + if (para.input.exx_device != "cpu" && para.input.exx_device != "gpu") + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_device must be cpu or gpu"); + } + if (para.input.exx_device == "gpu") + { + if (para.input.basis_type != "lcao" || para.input.device != "gpu") + { + ModuleBase::WARNING_QUIT("ReadInput", "exx_device=gpu requires basis_type=lcao and device=gpu"); + } +#ifndef __GPU_RI + ModuleBase::WARNING_QUIT("ReadInput", "exx_device=gpu requires a build with ENABLE_LIBRI_GPU=ON"); +#endif + if (para.input.symmetry == "1" && para.input.exx_symmetry_realspace) + { + ModuleBase::WARNING_QUIT("ReadInput", + "LibRI GPU contractions do not support real-space EXX symmetry; " + "set exx_device=cpu or exx_symmetry_realspace=0"); + } + } + }; + this->add_item(item); + } { Input_Item item("exx_pca_threshold"); item.annotation = "threshold to screen on-site ABFs in exx"; diff --git a/source/source_io/test_serial/read_input_item_test.cpp b/source/source_io/test_serial/read_input_item_test.cpp index 6107585e73..1e3c694caf 100644 --- a/source/source_io/test_serial/read_input_item_test.cpp +++ b/source/source_io/test_serial/read_input_item_test.cpp @@ -145,6 +145,64 @@ TEST_F(InputTest, PccSelectionAndUnsupportedCombinations) EXPECT_EXIT(it->second.check_value(it->second, param), testing::ExitedWithCode(1), ""); } +TEST_F(InputTest, ExxDevice) +{ + ModuleIO::ReadInput readinput(0); + Parameter param; + auto it = find_label("exx_device", readinput.input_lists); + ASSERT_NE(it, readinput.input_lists.end()); + EXPECT_EQ(param.inp.exx_device, "cpu"); + EXPECT_EQ(it->second.default_value, "cpu"); + + auto& input = TestParameters::input(param); + input.basis_type = "lcao"; + input.device = "gpu"; + input.symmetry = "1"; + input.exx_symmetry_realspace = true; + it->second.check_value(it->second, param); + it->second.get_final_value(it->second, param); + EXPECT_EQ(it->second.final_value.str(), "cpu"); + + const auto expect_rejected = [&](const std::string& message) { + testing::internal::CaptureStdout(); + EXPECT_EXIT(it->second.check_value(it->second, param), ::testing::ExitedWithCode(1), ""); + const std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, testing::HasSubstr(message)); + }; + + it->second.str_values = {"ddla"}; + it->second.read_value(it->second, param); + expect_rejected("exx_device must be cpu or gpu"); + + it->second.str_values = {"gpu"}; + it->second.read_value(it->second, param); + EXPECT_EQ(param.inp.exx_device, "gpu"); + input.basis_type = "pw"; + expect_rejected("exx_device=gpu requires basis_type=lcao and device=gpu"); + input.basis_type = "lcao"; + input.device = "cpu"; + expect_rejected("exx_device=gpu requires basis_type=lcao and device=gpu"); + input.device = "gpu"; + +#ifdef __GPU_RI + expect_rejected("LibRI GPU contractions do not support real-space EXX symmetry"); + input.exx_symmetry_realspace = false; + it->second.check_value(it->second, param); + input.exx_symmetry_realspace = true; + input.symmetry = "0"; + it->second.check_value(it->second, param); +#else + expect_rejected("exx_device=gpu requires a build with ENABLE_LIBRI_GPU=ON"); +#endif + + it->second.str_values = {"cpu"}; + it->second.read_value(it->second, param); + EXPECT_EQ(param.inp.exx_device, "cpu"); + input.symmetry = "1"; + input.exx_symmetry_realspace = true; + it->second.check_value(it->second, param); +} + TEST_F(InputTest, Item_test) { ModuleIO::ReadInput readinput(0); diff --git a/source/source_lcao/module_ri/exx_lri_interface.h b/source/source_lcao/module_ri/exx_lri_interface.h index 8b5bc61cf6..4476d03e9e 100644 --- a/source/source_lcao/module_ri/exx_lri_interface.h +++ b/source/source_lcao/module_ri/exx_lri_interface.h @@ -53,6 +53,16 @@ class Exx_LRI_Interface } Exx_LRI_Interface() = delete; + void set_gpu(const bool use_gpu) + { +#ifdef __GPU_RI + auto& mode = this->exx_ptr->exx_lri.lri.cal_mode; + mode = use_gpu ? RI::LRI_Cal_Mode::GPU : RI::LRI_Cal_Mode::CPU; +#else + (void)use_gpu; +#endif + } + ///// read and write Hexxs using cereal //void write_Hexxs_cereal(const std::string& file_name) const; //void read_Hexxs_cereal(const std::string& file_name); diff --git a/source/source_lcao/setup_exx.cpp b/source/source_lcao/setup_exx.cpp index 0c17b3d2d9..219a22356f 100644 --- a/source/source_lcao/setup_exx.cpp +++ b/source/source_lcao/setup_exx.cpp @@ -50,6 +50,15 @@ void Exx_NAO::before_runner( Exx_Info& exx_info) { #ifdef __EXX + const bool use_gpu = inp.exx_device == "gpu"; + if (this->exd) + { + this->exd->set_gpu(use_gpu); + } + if (this->exc) + { + this->exc->set_gpu(use_gpu); + } if (inp.calculation == "scf" || inp.calculation == "relax" || inp.calculation == "cell-relax" || inp.calculation == "md") { diff --git a/tests/lcao_hse_gpu/cmake/CMakeLists.txt b/tests/lcao_hse_gpu/cmake/CMakeLists.txt new file mode 100644 index 0000000000..3bf7726bbf --- /dev/null +++ b/tests/lcao_hse_gpu/cmake/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.18) +project(libri_consumer_check LANGUAGES CXX) + +# Exercise the dependency target without ABACUS's global includes or link flags. +list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../../cmake/modules") +option(ENABLE_OPENMP "Enable OpenMP" ON) +option(ENABLE_LIBRI_GPU "Check the LibRI CUDA dependency interface" OFF) +option(ENABLE_LIBRI_GPU_TESTS "Run numerical LibRI tests requiring a GPU" OFF) +if(ENABLE_LIBRI_GPU_TESTS AND NOT ENABLE_LIBRI_GPU) + message(FATAL_ERROR "ENABLE_LIBRI_GPU_TESTS requires ENABLE_LIBRI_GPU=ON") +endif() +find_package(MPI REQUIRED COMPONENTS CXX) +if(ENABLE_OPENMP) + find_package(OpenMP REQUIRED COMPONENTS CXX) +endif() +if(MKLROOT OR MKL_ROOT OR DEFINED ENV{MKLROOT}) + find_package(MKL REQUIRED) +else() + find_package(Lapack REQUIRED) +endif() +if(ENABLE_LIBRI_GPU) + find_package(CUDAToolkit REQUIRED) +endif() +find_package(LibRI REQUIRED) + +add_executable(libri_consumer test_libri_consumer.cpp) +target_link_libraries(libri_consumer PRIVATE LibRI::LibRI) +target_compile_definitions(libri_consumer PRIVATE + $<$:EXPECT_LIBRI_GPU>) +enable_testing() +add_test(NAME libri_consumer COMMAND libri_consumer) + +if(ENABLE_LIBRI_GPU_TESTS) + add_executable(libri_gpu_accumulation test_libri_gpu_accumulation.cpp) + target_link_libraries(libri_gpu_accumulation PRIVATE LibRI::LibRI) + add_test(NAME libri_gpu_accumulation + COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 1 ${MPIEXEC_PREFLAGS} + $ ${MPIEXEC_POSTFLAGS}) + set_tests_properties(libri_gpu_accumulation PROPERTIES + LABELS gpu + TIMEOUT 120 + ENVIRONMENT "OMP_NUM_THREADS=1;OPENBLAS_NUM_THREADS=1;MKL_NUM_THREADS=1") +endif() diff --git a/tests/lcao_hse_gpu/cmake/test_libri_consumer.cpp b/tests/lcao_hse_gpu/cmake/test_libri_consumer.cpp new file mode 100644 index 0000000000..6ab8e5a5b0 --- /dev/null +++ b/tests/lcao_hse_gpu/cmake/test_libri_consumer.cpp @@ -0,0 +1,27 @@ +#include + +#if defined(EXPECT_LIBRI_GPU) +#if !defined(__GPU_RI) || !defined(__DDLA_RI) || !defined(DDLA_USE_CUDA) +#error "LibRI GPU usage requirements did not reach the consumer" +#endif +#else +#if defined(__GPU_RI) || defined(__DDLA_RI) || defined(DDLA_USE_CUDA) +#error "CPU LibRI consumer unexpectedly requires a GPU backend" +#endif +#endif + +int main() +{ + RI::LRI lri; +#ifdef EXPECT_LIBRI_GPU + if (lri.cal_mode != RI::LRI_Cal_Mode::CPU) + { + return 1; + } + lri.cal_mode = RI::LRI_Cal_Mode::GPU; +#endif + // The consumer must receive the BLAS dependency through LibRI::LibRI. + const double data[] = {3.0, 4.0}; + const double norm = RI::Blas_Interface::nrm2(2, data); + return norm == 5.0 ? 0 : 1; +} diff --git a/tests/lcao_hse_gpu/cmake/test_libri_gpu_accumulation.cpp b/tests/lcao_hse_gpu/cmake/test_libri_gpu_accumulation.cpp new file mode 100644 index 0000000000..137247c260 --- /dev/null +++ b/tests/lcao_hse_gpu/cmake/test_libri_gpu_accumulation.cpp @@ -0,0 +1,122 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +template +RI::Tensor make_tensor(const RI::Shape_Vector& shape, const T imaginary) +{ + RI::Tensor tensor(shape); + for (std::size_t i = 0; i < tensor.data->size(); ++i) + { + (*tensor.data)[i] = T(0.5 + 0.07 * i) + imaginary * T(0.1 + 0.03 * i); + } + return tensor; +} + +template +void check_accumulation(const char* scalar_name, const T imaginary) +{ + using LRI = RI::LRI; + using Tensors = std::map>>; + const typename LRI::TAC key = {0, {0, 0, 0}}; + const std::map> atoms = {{0, {0.0, 0.0, 0.0}}}; + const std::array, 3> lattice = {{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; + const std::vector labels(RI::Label::array_ab_ab.begin(), RI::Label::array_ab_ab.end()); + LRI lri; + lri.set_parallel(MPI_COMM_WORLD, atoms, lattice, {1, 1, 1}, labels); + for (const auto label: RI::Label::array_ab) + { + const bool coefficient = label == RI::Label::ab::a || label == RI::Label::ab::b; + const RI::Shape_Vector shape = coefficient ? RI::Shape_Vector{2, 2, 2} : RI::Shape_Vector{2, 2}; + Tensors tensors; + tensors[0][key] = make_tensor(shape, imaginary); + lri.set_tensors_map2(tensors, {label}); + } + + const RI::Tensor seed = make_tensor({2, 2}, imaginary); + for (const auto label: labels) + { + Tensors reference; + lri.cal_mode = RI::LRI_Cal_Mode::CPU; + lri.cal_loop3({label}, reference, 1.0); + const RI::Tensor contribution = reference.at(0).at(key).copy(); + const double norm = contribution.norm(2); + if (!std::isfinite(norm) || norm < 1e-8) + { + throw std::runtime_error("Empty or invalid CPU reference contraction"); + } + + for (const auto mode: {RI::LRI_Cal_Mode::CPU, RI::LRI_Cal_Mode::GPU}) + { + lri.cal_mode = mode; + Tensors result; + // Tensor copies share storage unless copy() is used explicitly. + result[0][key] = seed.copy(); + double accumulated_factor = 0.0; + for (const double factor: {-1.0, 1.0, 1.0, -1.0}) + { + lri.cal_loop3({label}, result, factor); + accumulated_factor += factor; + const RI::Tensor& actual = result.at(0).at(key); + if (result.size() != 1 || result.at(0).size() != 1 || actual.shape.size() != 2 || actual.shape[0] != 2 + || actual.shape[1] != 2) + { + throw std::runtime_error("Unexpected contraction output shape"); + } + for (std::size_t i = 0; i < seed.data->size(); ++i) + { + const T expected = (*seed.data)[i] + accumulated_factor * (*contribution.data)[i]; + const double error = std::abs((*actual.data)[i] - expected); + const double tolerance = 1e-11 * (1.0 + std::abs(expected)); + if (!std::isfinite(error) || error > tolerance) + { + std::cerr << scalar_name << " label=" << static_cast(label) + << " mode=" << static_cast(mode) << " factor=" << factor << " element=" << i + << " error=" << error << '\n'; + throw std::runtime_error("Signed accumulation differs from the CPU reference"); + } + } + } + } + } + std::cout << scalar_name << ": all 18 contractions preserve signed accumulation\n"; +} +} // namespace + +int main(int argc, char** argv) +{ + // This standalone dependency test has no ABACUS runtime to initialize MPI. + int provided = 0; + if (MPI_Init_thread(&argc, &argv, MPI_THREAD_MULTIPLE, &provided) != MPI_SUCCESS) + { + return 1; + } + int status = 0; + try + { + const int ranks = RI::MPI_Wrapper::mpi_get_size(MPI_COMM_WORLD); + if (ranks != 1 || provided < MPI_THREAD_MULTIPLE) + { + throw std::runtime_error("This test requires one MPI rank and MPI_THREAD_MULTIPLE"); + } + check_accumulation("real", 0.0); + const std::complex imaginary(0.0, 1.0); + check_accumulation>("complex", imaginary); + } + catch (const std::exception& error) + { + std::cerr << error.what() << '\n'; + status = 1; + } + MPI_Finalize(); + return status; +} diff --git a/tests/lcao_hse_gpu/libri-compat.patch b/tests/lcao_hse_gpu/libri-compat.patch new file mode 100644 index 0000000000..5ad3bcd365 --- /dev/null +++ b/tests/lcao_hse_gpu/libri-compat.patch @@ -0,0 +1,83 @@ +--- a/include/RI/global/gpu/GPU_Backend.h ++++ b/include/RI/global/gpu/GPU_Backend.h +@@ -110 +110 @@ +- ddla::DEVICE_CHECK_RI(ddla::deviceGetDeviceCount(&device_count)); ++ ddla::DEVICE_CHECK(ddla::deviceGetDeviceCount(&device_count)); +@@ -177 +177 @@ +- ddla::DEVICE_CHECK_RI(ddla::deviceMalloc(pointer, count * sizeof(T))); ++ ddla::DEVICE_CHECK(ddla::deviceMalloc(pointer, count * sizeof(T))); +@@ -189 +189 @@ +- ddla::DEVICE_CHECK_RI(ddla::deviceFree(pointer)); ++ ddla::DEVICE_CHECK(ddla::deviceFree(pointer)); +@@ -202 +202 @@ +- ddla::DEVICE_CHECK_RI(deviceMemcpyAsync( ++ ddla::DEVICE_CHECK(deviceMemcpyAsync( +@@ -218 +218 @@ +- ddla::DEVICE_CHECK_RI(deviceMemcpyAsync( ++ ddla::DEVICE_CHECK(deviceMemcpyAsync( +@@ -232 +232 @@ +- ddla::DEVICE_CHECK_RI(deviceMemsetAsync( ++ ddla::DEVICE_CHECK(deviceMemsetAsync( +@@ -242 +242 @@ +- ddla::DEVICE_CHECK_RI( ++ ddla::DEVICE_CHECK( +--- a/include/RI/ri/LRI-cal_loop3_GPU.hpp ++++ b/include/RI/ri/LRI-cal_loop3_GPU.hpp +@@ -205 +205 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -341 +341 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -483 +483 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -623 +623 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -763 +763 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -903 +903 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -1045 +1045 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -1185 +1185 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -1325 +1325 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -1465 +1465 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -1606 +1606 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -1745 +1745 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -1884 +1884 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -2023 +2023 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -2189 +2189 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -2353 +2353 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -2517 +2517 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -2681 +2681 @@ +- rDs_output.download(Ds_result, queue); ++ rDs_output.download(Ds_result_thread, queue); +@@ -2693 +2693,2 @@ +- LRI_Cal_Aux::destroy_lock_result(lock_Ds_result_add_map, Ds_result); ++ LRI_Cal_Aux::destroy_lock(lock_Ds_result_add_map); ++ LRI_Cal_Aux::destroy_Ds_result(Ds_result);