diff --git a/include/RI/physics/symmetry/Filter_Atom_Symmetry.h b/include/RI/physics/symmetry/Filter_Atom_Symmetry.h index 12698ff..77a0f1f 100644 --- a/include/RI/physics/symmetry/Filter_Atom_Symmetry.h +++ b/include/RI/physics/symmetry/Filter_Atom_Symmetry.h @@ -45,6 +45,14 @@ class Filter_Atom_Symmetry: public Filter_Atom> case Label::ab_ab::a1b2_a2b0: case Label::ab_ab::a1b2_a2b1: return !this->symmetry.is_Aa_in_irreducible_sector(A1); + // GPU visits Aa01 before the output Aa2 is known in these cases. + case Label::ab_ab::a0b0_a1b1: + case Label::ab_ab::a0b1_a1b0: + case Label::ab_ab::a0b0_a1b2: + case Label::ab_ab::a0b1_a1b2: + case Label::ab_ab::a0b2_a1b0: + case Label::ab_ab::a0b2_a1b1: + return false; default: throw std::invalid_argument("label "+Label_Tools::get_name(label)+" error in "+std::string(__FILE__)+" line "+std::to_string(__LINE__)); } @@ -62,6 +70,11 @@ class Filter_Atom_Symmetry: public Filter_Atom> case Label::ab_ab::a0b1_a1b2: case Label::ab_ab::a0b2_a1b0: case Label::ab_ab::a0b2_a1b1: + // a01b2_a2b01: Ab01 is the output right atom on GPU. + case Label::ab_ab::a0b2_a2b0: + case Label::ab_ab::a0b2_a2b1: + case Label::ab_ab::a1b2_a2b0: + case Label::ab_ab::a1b2_a2b1: return !this->symmetry.is_Ab_in_irreducible_sector(A1.first); default: throw std::invalid_argument("label "+Label_Tools::get_name(label)+" error in "+std::string(__FILE__)+" line "+std::to_string(__LINE__)); @@ -100,6 +113,14 @@ class Filter_Atom_Symmetry: public Filter_Atom> case Label::ab_ab::a1b0_a2b2: case Label::ab_ab::a1b1_a2b2: return !this->symmetry.in_irreducible_sector(A1, A3); + // GPU output is (Aa2, Ab2) or (Aa2, Ab01); Aa01 is intermediate. + case Label::ab_ab::a0b0_a1b1: + case Label::ab_ab::a0b1_a1b0: + case Label::ab_ab::a0b0_a1b2: + case Label::ab_ab::a0b1_a1b2: + case Label::ab_ab::a0b2_a1b0: + case Label::ab_ab::a0b2_a1b1: + return !this->symmetry.in_irreducible_sector(A2, A3); // a01b2_a2b01: Aa01, Ab01, Aa2 default: return false; diff --git a/include/RI/ri/LRI-cal_loop3_GPU.hpp b/include/RI/ri/LRI-cal_loop3_GPU.hpp index c05794f..d10bee9 100644 --- a/include/RI/ri/LRI-cal_loop3_GPU.hpp +++ b/include/RI/ri/LRI-cal_loop3_GPU.hpp @@ -1554,7 +1554,8 @@ void LRI::cal_loop3_GPU( for (std::size_t ib2 = 0; ib2 < list_Ab2.size(); ++ib2) { const TAC &Ab2 = list_Ab2[ib2]; - if (this->filter_atom->filter_for32(label, Aa01, Ab01, Ab2)) continue; + // The output block is (Aa01, Ab01); Ab2 is only an intermediate atom. + if (this->filter_atom->filter_for32(label, Aa01, Ab2, Ab01)) continue; const Tensor &D_b = Global_Func::find(Ds_b_transpose, Ab01.first, TAC{Ab2.first, (Ab2.second-Ab01.second)%this->period}); if(D_b.empty()) continue; const GPU_Data::Pack &D_mul = rDs_mul.find_2nd(Aa01, Ab2); @@ -1693,7 +1694,7 @@ void LRI::cal_loop3_GPU( for (std::size_t ib2 = 0; ib2 < list_Ab2.size(); ++ib2) { const TAC &Ab2 = list_Ab2[ib2]; - if (this->filter_atom->filter_for32(label, Aa01, Ab01, Ab2)) continue; + if (this->filter_atom->filter_for32(label, Aa01, Ab2, Ab01)) continue; const Tensor &D_b = tools.get_Ds_ab(Label::ab::b, Ab01, Ab2); if(D_b.empty()) continue; const GPU_Data::Pack &D_mul = rDs_mul.find_2nd(Aa01, Ab2); @@ -1832,7 +1833,7 @@ void LRI::cal_loop3_GPU( for (std::size_t ib2 = 0; ib2 < list_Ab2.size(); ++ib2) { const TAC &Ab2 = list_Ab2[ib2]; - if (this->filter_atom->filter_for32(label, Aa01, Ab01, Ab2)) continue; + if (this->filter_atom->filter_for32(label, Aa01, Ab2, Ab01)) continue; const Tensor &D_b = Global_Func::find(Ds_b_transpose, Ab01.first, TAC{Ab2.first, (Ab2.second-Ab01.second)%this->period}); if(D_b.empty()) continue; const GPU_Data::Pack &D_mul = rDs_mul.find_2nd(Aa01, Ab2); @@ -1971,7 +1972,7 @@ void LRI::cal_loop3_GPU( for (std::size_t ib2 = 0; ib2 < list_Ab2.size(); ++ib2) { const TAC &Ab2 = list_Ab2[ib2]; - if (this->filter_atom->filter_for32(label, Aa01, Ab01, Ab2)) continue; + if (this->filter_atom->filter_for32(label, Aa01, Ab2, Ab01)) continue; const Tensor &D_b = tools.get_Ds_ab(Label::ab::b, Ab01, Ab2); if(D_b.empty()) continue; const GPU_Data::Pack &D_mul = rDs_mul.find_2nd(Aa01, Ab2); @@ -2106,10 +2107,11 @@ void LRI::cal_loop3_GPU( const TAC &Aa2 = list_Aa2[ia2]; const Tensor &D_a = Global_Func::find(Ds_a_transpose, Aa01, Aa2); if(D_a.empty()) continue; - if(this->filter_atom->filter_for2(label,Aa01,Aa2)) continue; for(std::size_t ib01=0; ib01filter_atom->filter_for2(label, Aa01, Ab01)) continue; if(this->filter_atom->filter_for32(label, Aa01, Ab01, Aa2)) continue; const Tensor &D_a2b1 = tools.get_Ds_ab(Label::ab::a2b1, Aa2, Ab01); if(D_a2b1.empty()) continue; @@ -2270,10 +2272,11 @@ void LRI::cal_loop3_GPU( const TAC &Aa2 = list_Aa2[ia2]; const Tensor &D_a = Global_Func::find(Ds_a_transpose, Aa01, Aa2); if(D_a.empty()) continue; - if(this->filter_atom->filter_for2(label,Aa01,Aa2)) continue; for(std::size_t ib01=0; ib01filter_atom->filter_for2(label, Aa01, Ab01)) continue; if(this->filter_atom->filter_for32(label, Aa01, Ab01, Aa2)) continue; const Tensor &D_a2b0 = tools.get_Ds_ab(Label::ab::a2b0, Aa2, Ab01); if(D_a2b0.empty()) continue; @@ -2434,10 +2437,11 @@ void LRI::cal_loop3_GPU( const TAC &Aa2 = list_Aa2[ia2]; const Tensor &D_a = tools.get_Ds_ab(Label::ab::a, Aa01, Aa2); if(D_a.empty()) continue; - if(this->filter_atom->filter_for2(label,Aa01,Aa2)) continue; for(std::size_t ib01=0; ib01filter_atom->filter_for2(label, Aa01, Ab01)) continue; if(this->filter_atom->filter_for32(label, Aa01, Ab01, Aa2)) continue; const Tensor &D_a2b1 = tools.get_Ds_ab(Label::ab::a2b1, Aa2, Ab01); if(D_a2b1.empty()) continue; @@ -2598,10 +2602,11 @@ void LRI::cal_loop3_GPU( const TAC &Aa2 = list_Aa2[ia2]; const Tensor &D_a = tools.get_Ds_ab(Label::ab::a, Aa01, Aa2); if(D_a.empty()) continue; - if(this->filter_atom->filter_for2(label,Aa01,Aa2)) continue; for(std::size_t ib01=0; ib01filter_atom->filter_for2(label, Aa01, Ab01)) continue; if(this->filter_atom->filter_for32(label, Aa01, Ab01, Aa2)) continue; const Tensor &D_a2b0 = tools.get_Ds_ab(Label::ab::a2b0, Aa2, Ab01); if(D_a2b0.empty()) continue; diff --git a/include/RI/ri/LRI_Cal_Aux.h b/include/RI/ri/LRI_Cal_Aux.h index 1eae641..c5985c8 100644 --- a/include/RI/ri/LRI_Cal_Aux.h +++ b/include/RI/ri/LRI_Cal_Aux.h @@ -95,7 +95,7 @@ namespace LRI_Cal_Aux if (D_result.empty()) { if(Tdata(1.0)==fac) - D_result = D_add; + D_result = D_add.copy(); else if(Tdata(-1.0)==fac) D_result = -D_add; else diff --git a/include/RI/ri/LRI_k-cal_cvc_mo.hpp b/include/RI/ri/LRI_k-cal_cvc_mo.hpp index d540770..594bd18 100644 --- a/include/RI/ri/LRI_k-cal_cvc_mo.hpp +++ b/include/RI/ri/LRI_k-cal_cvc_mo.hpp @@ -467,7 +467,7 @@ LRI_k::cal_cvc_mo_k_hartree_onthefly( { for (int k1_index=0; k1_index C_mu_ia = Cs_ao_mo_to_Cs_mo(Cs_ao_mo, map_psi, k1, k1, diff --git a/include/RI/ri/LRI_k-cal_hartree.hpp b/include/RI/ri/LRI_k-cal_hartree.hpp index 9705106..7f6063a 100644 --- a/include/RI/ri/LRI_k-cal_hartree.hpp +++ b/include/RI/ri/LRI_k-cal_hartree.hpp @@ -121,7 +121,7 @@ LRI_k::cal_cvcd_k_hartree( #pragma omp for schedule(dynamic) collapse(2) for (int v_index=0; v_index ${MPIEXEC_POSTFLAGS}) +set_tests_properties(lri_symmetry PROPERTIES ENVIRONMENT + "OMP_NUM_THREADS=1;OPENBLAS_NUM_THREADS=1;MKL_NUM_THREADS=1") diff --git a/unittests/gpu_symmetry/README.md b/unittests/gpu_symmetry/README.md new file mode 100644 index 0000000..167f271 --- /dev/null +++ b/unittests/gpu_symmetry/README.md @@ -0,0 +1,35 @@ +# GPU symmetry regression + +Compare all 18 contractions, individually and together, against full CPU +contractions with an independently applied output-sector mask. The fixture uses +two unequal atoms and three periodic cells, real and complex tensors, both CPU +modes, and optionally DDLA/CUDA. It checks unrestricted, restricted and empty +output, as well as seeded signed, zero and fractional accumulation. + +```sh +cmake -S unittests/gpu_symmetry -B build-symmetry \ + -DLIBCOMM_ROOT=/path/to/LibComm -DCEREAL_ROOT=/path/to/cereal \ + -DENABLE_GPU=ON -DDDLA_ROOT=/path/to/DDLA +cmake --build build-symmetry --parallel 2 +ctest --test-dir build-symmetry --output-on-failure +``` + +The GPU test requires one GPU, one MPI rank, and DDLA built for CUDA. Omit +`ENABLE_GPU` and `DDLA_ROOT` to check a CPU-only build. The library headers can +be selected independently with `LIBRI_ROOT`, allowing the same test to reproduce +failures in an earlier revision. + +For OpenMP coverage or an isolated failing contraction: + +```sh +OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 \ + mpirun -np 1 build-symmetry/test_lri_gpu_symmetry +OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 \ + mpirun -np 1 build-symmetry/test_lri_gpu_symmetry 1 gpu real +``` + +Arguments select the contraction index (`0`–`17`, `18` for the combined sum, +or `-1` for all), backend (`cpu`, `gpu`, `all`) and scalar (`real`, `complex`, +`all`). Index 1 is `a1b2_a2b1`: filtering `(Aa01,Aa2)` while summing over `Aa2` +incorrectly removes contributions to the accepted output pair `(Aa01,Ab01)`. +The off-diagonal sector with a nonzero translation detects this error. diff --git a/unittests/gpu_symmetry/test_lri_gpu_symmetry.cpp b/unittests/gpu_symmetry/test_lri_gpu_symmetry.cpp new file mode 100644 index 0000000..233bc34 --- /dev/null +++ b/unittests/gpu_symmetry/test_lri_gpu_symmetry.cpp @@ -0,0 +1,242 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ +using Cell = std::array; +using AtomCell = std::pair; +using Sector = std::map, std::set>; +using Label = RI::Label::ab_ab; + +template +using Tensors = std::map>>; + +template +RI::Tensor make_tensor(const RI::Shape_Vector& shape, int left, int right, + int cell, int label, T imaginary) +{ + RI::Tensor tensor(shape); + for (std::size_t i = 0; i < tensor.data->size(); ++i) + { + // Different atoms and cells must not accidentally produce equal sums. + const double real = 0.2 + 0.07 * left + 0.03 * right + 0.02 * cell + + 0.005 * label + 0.013 * i; + const double imag = 0.04 + 0.02 * left - 0.01 * right + 0.003 * i; + (*tensor.data)[i] = T(real) + imaginary * T(imag); + } + return tensor; +} + +template +Tensors copy_tensors(const Tensors& source) +{ + Tensors result; + for (const auto& left : source) + for (const auto& right : left.second) + result[left.first][right.first] = right.second.copy(); + return result; +} + +template +void compare(const Tensors& actual, const Tensors& expected, + const std::string& context) +{ + if (actual.size() != expected.size()) + throw std::runtime_error(context + ": output atom count differs"); + for (const auto& left : expected) + { + const auto found_left = actual.find(left.first); + if (found_left == actual.end() || found_left->second.size() != left.second.size()) + throw std::runtime_error(context + ": output block count differs"); + for (const auto& right : left.second) + { + const auto found = found_left->second.find(right.first); + if (found == found_left->second.end()) + throw std::runtime_error(context + ": missing output block"); + const auto& value = found->second; + const auto& reference = right.second; + if (value.shape.size() != reference.shape.size() + || !std::equal(value.shape.begin(), value.shape.end(), reference.shape.begin())) + throw std::runtime_error(context + ": output shape differs"); + for (std::size_t i = 0; i < reference.data->size(); ++i) + { + const double error = std::abs((*value.data)[i] - (*reference.data)[i]); + if (!std::isfinite(error) || error > 1e-10 * (1.0 + std::abs((*reference.data)[i]))) + { + std::cerr << context << " atoms=" << left.first << ',' << right.first.first + << " cell=" << right.first.second[0] << " element=" << i + << " error=" << error << '\n'; + throw std::runtime_error(context + ": contraction differs from masked CPU reference"); + } + } + } + } +} + +template +void check(const std::string& scalar, T imaginary, int selected, const std::string& backend) +{ + RI::LRI lri; + const std::vector