From a475a16d03061210e6456838fd98786812c56771 Mon Sep 17 00:00:00 2001 From: Paul Kuberry Date: Thu, 23 Sep 2021 07:51:21 -0600 Subject: [PATCH 01/40] Switch compressed row radius neighbor search to use ArborX --- CMakeLists.txt | 2 + pycompadre/pycompadre.cpp | 2 - src/CMakeLists.txt | 2 + src/Compadre_PointCloudSearch.hpp | 947 ++++++++++++++---------------- 4 files changed, 440 insertions(+), 513 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 196145439..78c2e58cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,6 +351,8 @@ else() # Raw CMake Project set(Compadre_EXT_DEPS Kokkos KokkosKernels) endif() + option(ARBORX_ENABLE_MPI "" "${Compadre_USE_MPI}") + add_subdirectory(arborx) #MPI (Not really needed, only used so that if a kokkos-tool was built with MPI, it won't segfault) diff --git a/pycompadre/pycompadre.cpp b/pycompadre/pycompadre.cpp index 8c448a694..d2fbfb013 100644 --- a/pycompadre/pycompadre.cpp +++ b/pycompadre/pycompadre.cpp @@ -35,8 +35,6 @@ class ParticleHelper { Compadre::GMLS* gmls_object; Compadre::NeighborLists* nl; - typedef nanoflann::KDTreeSingleIndexAdaptor >, - Compadre::PointCloudSearch, 3> tree_type; std::shared_ptr > point_cloud_search; double_2d_view_type _source_coords; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 93aac5f7c..03102ae3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,6 +55,8 @@ elseif (KOKKOSKERNELS_BUILT_FOR_USER) # Kokkos-Kernels built for user by this pr target_link_libraries(compadre PUBLIC Kokkos::kokkoskernels) endif() +target_link_libraries(compadre PUBLIC ArborX::ArborX) + # link to MPI if (Compadre_USE_MPI) if (MPI_CXX_INCLUDE_PATH) diff --git a/src/Compadre_PointCloudSearch.hpp b/src/Compadre_PointCloudSearch.hpp index 9d6f85448..db809c0c9 100644 --- a/src/Compadre_PointCloudSearch.hpp +++ b/src/Compadre_PointCloudSearch.hpp @@ -3,124 +3,13 @@ #include "Compadre_Typedefs.hpp" #include "Compadre_NeighborLists.hpp" -#include "nanoflann.hpp" #include -#include +#include -namespace Compadre { - -//class sort_indices -//{ -// private: -// double* mparr; -// public: -// sort_indices(double* parr) : mparr(parr) {} -// bool operator()(int i, int j) const { return mparr[i] -class RadiusResultSet { - - public: - - typedef _DistanceType DistanceType; - typedef _IndexType IndexType; - - const DistanceType radius; - IndexType count; - DistanceType* r_dist; - IndexType* i_dist; - const IndexType max_size; - - RadiusResultSet( - DistanceType radius_, - DistanceType* r_dist_, IndexType* i_dist_, const IndexType max_size_) - : radius(radius_), count(0), r_dist(r_dist_), i_dist(i_dist_), max_size(max_size_) { - init(); - } - - void init() {} - - void clear() { count = 0; } - - size_t size() const { return count; } - - bool full() const { return true; } - - bool addPoint(DistanceType dist, IndexType index) { - - if (dist < radius) { - // would throw an exception here if count>=max_size, but this code is - // called inside of a parallel region so only an abort is possible, - // but this situation is recoverable - // - // instead, we increase count, but there is nowhere to store neighbors - // since we are working with pre-allocated space - // this will be discovered after returning from the search by comparing - // the count against the pre-allocate space dimensions - if (count worst_item() const { - // just to verify this isn't ever called - compadre_kernel_assert_release(false && "worst_item() should not be called."); - } - - void sort() { - // puts closest neighbor as the first entry in the neighbor list - // leaves the rest unsorted - - if (count > 0) { - - // alternate sort for every entry, not currently being used - //int indices[count]; - //for (int i=0; i tmp_indices(count); - //std::vector tmp_r(count); - //for (int i=0; i::max(); - IndexType best_distance_index = 0; - int best_index = -1; - for (IndexType i=0; i +using BVH = ArborX::BoundingVolumeHierarchy; +namespace Compadre { //! PointCloudSearch generates neighbor lists and window sizes for each target site /*! @@ -142,15 +31,6 @@ class RadiusResultSet { template class PointCloudSearch { - public: - - typedef nanoflann::KDTreeSingleIndexAdaptor >, - PointCloudSearch, 1> tree_type_1d; - typedef nanoflann::KDTreeSingleIndexAdaptor >, - PointCloudSearch, 2> tree_type_2d; - typedef nanoflann::KDTreeSingleIndexAdaptor >, - PointCloudSearch, 3> tree_type_3d; - protected: //! source site coordinates @@ -158,9 +38,7 @@ class PointCloudSearch { local_index_type _dim; local_index_type _max_leaf; - std::shared_ptr _tree_1d; - std::shared_ptr _tree_2d; - std::shared_ptr _tree_3d; + BVH _tree; public: @@ -171,6 +49,23 @@ class PointCloudSearch { _max_leaf((max_leaf < 0) ? 10 : max_leaf) { compadre_assert_release((Kokkos::SpaceAccessibility::accessible==1) && "Views passed to PointCloudSearch at construction should be accessible from the host."); + + { //TODO: Add check here to see if _tree is populated + // if not, populate + Kokkos::View cloud("point_cloud", _src_pts_view.extent(0)); + Kokkos::parallel_for(_src_pts_view.extent(0), KOKKOS_LAMBDA(int i) { + if (_dim==1) { + cloud(i) = {{_src_pts_view(i,0),0,0}}; + } + if (_dim==2) { + cloud(i) = {{_src_pts_view(i,0),_src_pts_view(i,1),0}}; + } + if (_dim==3) { + cloud(i) = {{_src_pts_view(i,0),_src_pts_view(i,1),_src_pts_view(i,2)}}; + } + }); + _tree = BVH(device_execution_space(), cloud); + } }; ~PointCloudSearch() {}; @@ -204,19 +99,31 @@ class PointCloudSearch { } - void generateKDTree() { - if (_dim==1) { - _tree_1d = std::make_shared(1, *this, nanoflann::KDTreeSingleIndexAdaptorParams(_max_leaf)); - _tree_1d->buildIndex(); - } else if (_dim==2) { - _tree_2d = std::make_shared(2, *this, nanoflann::KDTreeSingleIndexAdaptorParams(_max_leaf)); - _tree_2d->buildIndex(); - } else if (_dim==3) { - _tree_3d = std::make_shared(3, *this, nanoflann::KDTreeSingleIndexAdaptorParams(_max_leaf)); - _tree_3d->buildIndex(); + //! Returns the distance between a point and a source site, given its index + inline double kdtreeDistance(const double* queryPt, const int idx) const { + + double distance = 0; + for (int i=0; i<_dim; ++i) { + distance += (_src_pts_view(idx,i)-queryPt[i])*(_src_pts_view(idx,i)-queryPt[i]); } + return std::sqrt(distance); + } + //void generateKDTree() { + //if (_dim==1) { + // //_tree_1d = std::make_shared(1, *this, nanoflann::KDTreeSingleIndexAdaptorParams(_max_leaf)); + // //_tree_1d->buildIndex(); + // _tree_1d = std::make_shared();//(1, *this, nanoflann::KDTreeSingleIndexAdaptorParams(_max_leaf)); + //} else if (_dim==2) { + // _tree_2d = std::make_shared();//std::make_shared(2, *this, nanoflann::KDTreeSingleIndexAdaptorParams(_max_leaf)); + // _tree_2d->buildIndex(); + //} else if (_dim==3) { + // _tree_3d = std::make_shared();//std::make_shared(3, *this, nanoflann::KDTreeSingleIndexAdaptorParams(_max_leaf)); + // //_tree_3d->buildIndex(); + //} + //} + /*! \brief Generates neighbor lists of 2D view by performing a radius search where the radius to be searched is in the epsilons view. If uniform_radius is given, then this overrides the epsilons view radii sizes. @@ -233,121 +140,124 @@ class PointCloudSearch { neighbor_lists_view_type neighbor_lists, epsilons_view_type epsilons, const double uniform_radius = 0.0, double max_search_radius = 0.0) { - // function does not populate epsilons, they must be prepopulated + //// function does not populate epsilons, they must be prepopulated - compadre_assert_release((Kokkos::SpaceAccessibility::accessible==1) && - "Target coordinates view passed to generate2DNeighborListsFromRadiusSearch should be accessible from the host."); - compadre_assert_release((((int)trg_pts_view.extent(1))>=_dim) && - "Target coordinates view passed to generate2DNeighborListsFromRadiusSearch must have \ - second dimension as large as _dim."); - compadre_assert_release((Kokkos::SpaceAccessibility::accessible==1) && - "Views passed to generate2DNeighborListsFromRadiusSearch should be accessible from the host."); - compadre_assert_release((Kokkos::SpaceAccessibility::accessible==1) && - "Views passed to generate2DNeighborListsFromRadiusSearch should be accessible from the host."); + //compadre_assert_release((Kokkos::SpaceAccessibility::accessible==1) && + // "Target coordinates view passed to generate2DNeighborListsFromRadiusSearch should be accessible from the host."); + //compadre_assert_release((((int)trg_pts_view.extent(1))>=_dim) && + // "Target coordinates view passed to generate2DNeighborListsFromRadiusSearch must have \ + // second dimension as large as _dim."); + //compadre_assert_release((Kokkos::SpaceAccessibility::accessible==1) && + // "Views passed to generate2DNeighborListsFromRadiusSearch should be accessible from the host."); + //compadre_assert_release((Kokkos::SpaceAccessibility::accessible==1) && + // "Views passed to generate2DNeighborListsFromRadiusSearch should be accessible from the host."); - // loop size - const int num_target_sites = trg_pts_view.extent(0); + //// loop size + //const int num_target_sites = trg_pts_view.extent(0); - if ((!_tree_1d && _dim==1) || (!_tree_2d && _dim==2) || (!_tree_3d && _dim==3)) { - this->generateKDTree(); - } + ////if ((!_tree_1d && _dim==1) || (!_tree_2d && _dim==2) || (!_tree_3d && _dim==3)) { + //// this->generateKDTree(); + ////} - // check neighbor lists and epsilons view sizes - compadre_assert_release((neighbor_lists.extent(0)==(size_t)num_target_sites - && neighbor_lists.extent(1)>=1) - && "neighbor lists View does not have large enough dimensions"); - compadre_assert_release((neighbor_lists_view_type::rank==2) && "neighbor_lists must be a 2D Kokkos view."); + //// check neighbor lists and epsilons view sizes + //compadre_assert_release((neighbor_lists.extent(0)==(size_t)num_target_sites + // && neighbor_lists.extent(1)>=1) + // && "neighbor lists View does not have large enough dimensions"); + //compadre_assert_release((neighbor_lists_view_type::rank==2) && "neighbor_lists must be a 2D Kokkos view."); - compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) - && "epsilons View does not have the correct dimension"); + //compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) + // && "epsilons View does not have the correct dimension"); - typedef Kokkos::View > - scratch_double_view; + //typedef Kokkos::View > + // scratch_double_view; - typedef Kokkos::View > - scratch_int_view; + //typedef Kokkos::View > + // scratch_int_view; - // determine scratch space size needed - int team_scratch_size = 0; - team_scratch_size += scratch_double_view::shmem_size(neighbor_lists.extent(1)); // distances - team_scratch_size += scratch_int_view::shmem_size(neighbor_lists.extent(1)); // indices - team_scratch_size += scratch_double_view::shmem_size(_dim); // target coordinate - - // maximum number of neighbors found over all target sites' neighborhoods - size_t max_num_neighbors = 0; - // part 2. do radius search using window size from knn search - // each row of neighbor lists is a neighbor list for the target site corresponding to that row - Kokkos::parallel_reduce("radius search", host_team_policy(num_target_sites, Kokkos::AUTO) - .set_scratch_size(0 /*shared memory level*/, Kokkos::PerTeam(team_scratch_size)), - KOKKOS_LAMBDA(const host_member_type& teamMember, size_t& t_max_num_neighbors) { + //// determine scratch space size needed + //int team_scratch_size = 0; + //team_scratch_size += scratch_double_view::shmem_size(neighbor_lists.extent(1)); // distances + //team_scratch_size += scratch_int_view::shmem_size(neighbor_lists.extent(1)); // indices + //team_scratch_size += scratch_double_view::shmem_size(_dim); // target coordinate - // make unmanaged scratch views - scratch_double_view neighbor_distances(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); - scratch_int_view neighbor_indices(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); - scratch_double_view this_target_coord(teamMember.team_scratch(0 /*shared memory*/), _dim); + //// maximum number of neighbors found over all target sites' neighborhoods + //size_t max_num_neighbors = 0; + //// part 2. do radius search using window size from knn search + //// each row of neighbor lists is a neighbor list for the target site corresponding to that row + //Kokkos::parallel_reduce("radius search", host_team_policy(num_target_sites, Kokkos::AUTO) + // .set_scratch_size(0 /*shared memory level*/, Kokkos::PerTeam(team_scratch_size)), + // KOKKOS_LAMBDA(const host_member_type& teamMember, size_t& t_max_num_neighbors) { - size_t neighbors_found = 0; + // // make unmanaged scratch views + // scratch_double_view neighbor_distances(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); + // scratch_int_view neighbor_indices(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); + // scratch_double_view this_target_coord(teamMember.team_scratch(0 /*shared memory*/), _dim); - const int i = teamMember.league_rank(); + // size_t neighbors_found = 0; - // set epsilons if radius is specified - if (uniform_radius > 0) epsilons(i) = uniform_radius; + // const int i = teamMember.league_rank(); - // needs furthest neighbor's distance for next portion - compadre_kernel_assert_release((epsilons(i)<=max_search_radius || max_search_radius==0) && "max_search_radius given (generally derived from the size of a halo region), and search radius needed would exceed this max_search_radius."); + // // set epsilons if radius is specified + // if (uniform_radius > 0) epsilons(i) = uniform_radius; - Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, neighbor_lists.extent(1)), [&](const int j) { - neighbor_indices(j) = 0; - neighbor_distances(j) = -1.0; - }); - teamMember.team_barrier(); + // // needs furthest neighbor's distance for next portion + // compadre_kernel_assert_release((epsilons(i)<=max_search_radius || max_search_radius==0) && "max_search_radius given (generally derived from the size of a halo region), and search radius needed would exceed this max_search_radius."); - Kokkos::single(Kokkos::PerTeam(teamMember), [&] () { - // target_coords is LayoutLeft on device and its HostMirror, so giving a pointer to - // this data would lead to a wrong result if the device is a GPU + // Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, neighbor_lists.extent(1)), [&](const int j) { + // neighbor_indices(j) = 0; + // neighbor_distances(j) = -1.0; + // }); + // teamMember.team_barrier(); - for (int j=0; j<_dim; ++j) { - this_target_coord(j) = trg_pts_view(i,j); - } + // Kokkos::single(Kokkos::PerTeam(teamMember), [&] () { + // // target_coords is LayoutLeft on device and its HostMirror, so giving a pointer to + // // this data would lead to a wrong result if the device is a GPU - nanoflann::SearchParams sp; // default parameters - Compadre::RadiusResultSet rrs(epsilons(i)*epsilons(i), neighbor_distances.data(), neighbor_indices.data(), neighbor_lists.extent(1)); - if (_dim==1) { - neighbors_found = _tree_1d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - } else if (_dim==2) { - neighbors_found = _tree_2d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - } else if (_dim==3) { - neighbors_found = _tree_3d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - } + // for (int j=0; j<_dim; ++j) { + // this_target_coord(j) = trg_pts_view(i,j); + // } - t_max_num_neighbors = (neighbors_found > t_max_num_neighbors) ? neighbors_found : t_max_num_neighbors; - - // the number of neighbors is stored in column zero of the neighbor lists 2D array - neighbor_lists(i,0) = neighbors_found; + // nanoflann::SearchParams sp; // default parameters + // Compadre::RadiusResultSet rrs(epsilons(i)*epsilons(i), neighbor_distances.data(), neighbor_indices.data(), neighbor_lists.extent(1)); + // if (_dim==1) { + // neighbors_found = _tree_1d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - // epsilons already scaled and then set by search radius - }); - teamMember.team_barrier(); - - // loop_bound so that we don't write into memory we don't have allocated - int loop_bound = (neighbors_found < neighbor_lists.extent(1)-1) ? neighbors_found : neighbor_lists.extent(1)-1; - // loop over each neighbor index and fill with a value - if (!is_dry_run) { - Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, loop_bound), [&](const int j) { - // cast to an whatever data type the 2D array of neighbor lists is using - neighbor_lists(i,j+1) = static_cast::type>::type>(neighbor_indices(j)); - }); - teamMember.team_barrier(); - } + // } else if (_dim==2) { + // neighbors_found = _tree_2d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - }, Kokkos::Max(max_num_neighbors) ); - Kokkos::fence(); + // } else if (_dim==3) { + // neighbors_found = _tree_3d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - // check if max_num_neighbors will fit onto pre-allocated space - compadre_assert_release((neighbor_lists.extent(1) >= (max_num_neighbors+1) || is_dry_run) - && "neighbor_lists does not contain enough columns for the maximum number of neighbors needing to be stored."); + // } - return max_num_neighbors; + // t_max_num_neighbors = (neighbors_found > t_max_num_neighbors) ? neighbors_found : t_max_num_neighbors; + // + // // the number of neighbors is stored in column zero of the neighbor lists 2D array + // neighbor_lists(i,0) = neighbors_found; + + // // epsilons already scaled and then set by search radius + // }); + // teamMember.team_barrier(); + + // // loop_bound so that we don't write into memory we don't have allocated + // int loop_bound = (neighbors_found < neighbor_lists.extent(1)-1) ? neighbors_found : neighbor_lists.extent(1)-1; + // // loop over each neighbor index and fill with a value + // if (!is_dry_run) { + // Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, loop_bound), [&](const int j) { + // // cast to an whatever data type the 2D array of neighbor lists is using + // neighbor_lists(i,j+1) = static_cast::type>::type>(neighbor_indices(j)); + // }); + // teamMember.team_barrier(); + // } + + //}, Kokkos::Max(max_num_neighbors) ); + //Kokkos::fence(); + + //// check if max_num_neighbors will fit onto pre-allocated space + //compadre_assert_release((neighbor_lists.extent(1) >= (max_num_neighbors+1) || is_dry_run) + // && "neighbor_lists does not contain enough columns for the maximum number of neighbors needing to be stored."); + + return 0;//max_num_neighbors; } /*! \brief Generates compressed row neighbor lists by performing a radius search @@ -382,14 +292,68 @@ class PointCloudSearch { // loop size const int num_target_sites = trg_pts_view.extent(0); - if ((!_tree_1d && _dim==1) || (!_tree_2d && _dim==2) || (!_tree_3d && _dim==3)) { - this->generateKDTree(); - } - compadre_assert_release((number_of_neighbors_list.extent(0)==(size_t)num_target_sites) && "number_of_neighbors_list or neighbor lists View does not have large enough dimensions"); compadre_assert_release((neighbor_lists_view_type::rank==1) && "neighbor_lists must be a 1D Kokkos view."); + compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) + && "epsilons View does not have the correct dimension"); + + typedef Kokkos::View > + scratch_double_view; + + // build target points into a cloud of sphere with radius given as epsilons(i) + Kokkos::View cloud("trg_point_cloud", trg_pts_view.extent(0)); + Kokkos::parallel_for(num_target_sites, KOKKOS_LAMBDA(int i) { + compadre_kernel_assert_release((epsilons(i)<=max_search_radius || max_search_radius==0) && "max_search_radius given (generally derived from the size of a halo region), and search radius needed would exceed this max_search_radius."); + if (uniform_radius > 0) epsilons(i) = uniform_radius; + if (_dim==1) { + cloud(i) = {{{trg_pts_view(i,0),0,0}}, epsilons(i)}; + } + if (_dim==2) { + cloud(i) = {{{trg_pts_view(i,0),trg_pts_view(i,1),0}}, epsilons(i)}; + } + if (_dim==3) { + cloud(i) = {{{trg_pts_view(i,0),trg_pts_view(i,1),trg_pts_view(i,2)}}, epsilons(i)}; + } + }); + Kokkos::fence(); + + // build queries + Kokkos::View *, device_memory_space> + queries("queries", num_target_sites); + Kokkos::parallel_for(Kokkos::RangePolicy(0, num_target_sites), + KOKKOS_LAMBDA(int i) { + queries(i) = ArborX::intersects(cloud(i)); + }); + Kokkos::fence(); + + // perform tree search + Kokkos::View values("values", 0); + Kokkos::View offsets("offsets", 0); + _tree.query(device_execution_space(), queries, values, offsets); + + + // set number of neighbors list (how many neighbors for each target site) based + // on results on tree query + Kokkos::parallel_for(Kokkos::RangePolicy(0, num_target_sites), + KOKKOS_LAMBDA(int i) { + size_t neighbors_found_for_target_i = 0; + if (i==num_target_sites-1) { + neighbors_found_for_target_i = values.extent(0) - offsets(i); + } else { + neighbors_found_for_target_i = offsets(i+1) - offsets(i); + } + if (is_dry_run || uniform_radius!=0.0) { + number_of_neighbors_list(i) = neighbors_found_for_target_i; + } else { + compadre_kernel_assert_debug((neighbors_found==(size_t)number_of_neighbors_list(i)) + && "Number of neighbors found changed since dry-run."); + } + }); + Kokkos::fence(); + + // build a row offsets object (only if not a dry run) typedef Kokkos::View row_offsets_view_type; row_offsets_view_type row_offsets; @@ -405,92 +369,53 @@ class PointCloudSearch { Kokkos::fence(); } - compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) - && "epsilons View does not have the correct dimension"); - - typedef Kokkos::View > - scratch_double_view; - - typedef Kokkos::View > - scratch_int_view; - - // determine scratch space size needed + // sort to put closest neighbor first (other neighbors are not affected) int team_scratch_size = 0; - team_scratch_size += scratch_double_view::shmem_size(max_neighbor_list_row_storage_size); // distances - team_scratch_size += scratch_int_view::shmem_size(max_neighbor_list_row_storage_size); // indices - team_scratch_size += scratch_double_view::shmem_size(_dim); // target coordinate - - // part 2. do radius search using window size from knn search - // each row of neighbor lists is a neighbor list for the target site corresponding to that row + team_scratch_size += scratch_double_view::shmem_size(trg_pts_view.extent(1)); // distances Kokkos::parallel_for("radius search", host_team_policy(num_target_sites, Kokkos::AUTO) .set_scratch_size(0 /*shared memory level*/, Kokkos::PerTeam(team_scratch_size)), KOKKOS_LAMBDA(const host_member_type& teamMember) { - // make unmanaged scratch views - scratch_double_view neighbor_distances(teamMember.team_scratch(0 /*shared memory*/), max_neighbor_list_row_storage_size); - scratch_int_view neighbor_indices(teamMember.team_scratch(0 /*shared memory*/), max_neighbor_list_row_storage_size); - scratch_double_view this_target_coord(teamMember.team_scratch(0 /*shared memory*/), _dim); - - size_t neighbors_found = 0; - const int i = teamMember.league_rank(); - // set epsilons if radius is specified - if (uniform_radius > 0) epsilons(i) = uniform_radius; - - // needs furthest neighbor's distance for next portion - compadre_kernel_assert_release((epsilons(i)<=max_search_radius || max_search_radius==0) && "max_search_radius given (generally derived from the size of a halo region), and search radius needed would exceed this max_search_radius."); - - Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, max_neighbor_list_row_storage_size), [&](const int j) { - neighbor_indices(j) = 0; - neighbor_distances(j) = -1.0; - }); - teamMember.team_barrier(); - - Kokkos::single(Kokkos::PerTeam(teamMember), [&] () { - // target_coords is LayoutLeft on device and its HostMirror, so giving a pointer to - // this data would lead to a wrong result if the device is a GPU - - for (int j=0; j<_dim; ++j) { - this_target_coord(j) = trg_pts_view(i,j); - } + // make unmanaged scratch views + scratch_double_view trg_pt(teamMember.team_scratch(0 /*shared memory*/), trg_pts_view.extent(1)); + for (int j=0; j rrs(epsilons(i)*epsilons(i), neighbor_distances.data(), neighbor_indices.data(), max_neighbor_list_row_storage_size); - if (_dim==1) { - neighbors_found = _tree_1d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - } else if (_dim==2) { - neighbors_found = _tree_2d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - } else if (_dim==3) { - neighbors_found = _tree_3d->template radiusSearchCustomCallback >(this_target_coord.data(), rrs, sp) ; - } - - // we check that neighbors found doesn't differ from dry-run or we store neighbors_found - // no check that neighbors found stay the same if uniform_radius specified (!=0) - if (is_dry_run || uniform_radius!=0.0) { - number_of_neighbors_list(i) = neighbors_found; - } else { - compadre_kernel_assert_debug((neighbors_found==(size_t)number_of_neighbors_list(i)) - && "Number of neighbors found changed since dry-run."); + // find swap index + int min_distance_ind = 0; + double first_neighbor_distance = kdtreeDistance(trg_pt.data(), values(offsets(i))); + for (int j=1; j::type>::type>(neighbor_indices(j)); - }); - teamMember.team_barrier(); } + // do the swap + if (min_distance_ind != 0) { + int tmp = values(offsets(i)); + values(offsets(i)) = values(offsets(i)+min_distance_ind); + values(offsets(i)+min_distance_ind) = tmp; + } }); Kokkos::fence(); + + // copy neighbor list values over from tree query + if (!is_dry_run) { + Kokkos::parallel_for(Kokkos::RangePolicy(0, num_target_sites), + KOKKOS_LAMBDA(int i) { + for (int j=0; jgenerateKDTree(); - } - Kokkos::fence(); - - compadre_assert_release((num_target_sites==0 || // sizes don't matter when there are no targets - (neighbor_lists.extent(0)==(size_t)num_target_sites - && neighbor_lists.extent(1)>=(size_t)(neighbors_needed+1))) - && "neighbor lists View does not have large enough dimensions"); - compadre_assert_release((neighbor_lists_view_type::rank==2) && "neighbor_lists must be a 2D Kokkos view."); - - compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) - && "epsilons View does not have the correct dimension"); - - typedef Kokkos::View > - scratch_double_view; - - typedef Kokkos::View > - scratch_int_view; - - // determine scratch space size needed - int team_scratch_size = 0; - team_scratch_size += scratch_double_view::shmem_size(neighbor_lists.extent(1)); // distances - team_scratch_size += scratch_int_view::shmem_size(neighbor_lists.extent(1)); // indices - team_scratch_size += scratch_double_view::shmem_size(_dim); // target coordinate - - // minimum number of neighbors found over all target sites' neighborhoods - size_t min_num_neighbors = 0; + //if ((!_tree_1d && _dim==1) || (!_tree_2d && _dim==2) || (!_tree_3d && _dim==3)) { + // this->generateKDTree(); + //} + //Kokkos::fence(); + + //compadre_assert_release((num_target_sites==0 || // sizes don't matter when there are no targets + // (neighbor_lists.extent(0)==(size_t)num_target_sites + // && neighbor_lists.extent(1)>=(size_t)(neighbors_needed+1))) + // && "neighbor lists View does not have large enough dimensions"); + //compadre_assert_release((neighbor_lists_view_type::rank==2) && "neighbor_lists must be a 2D Kokkos view."); + + //compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) + // && "epsilons View does not have the correct dimension"); + + //typedef Kokkos::View > + // scratch_double_view; + + //typedef Kokkos::View > + // scratch_int_view; + + //// determine scratch space size needed + //int team_scratch_size = 0; + //team_scratch_size += scratch_double_view::shmem_size(neighbor_lists.extent(1)); // distances + //team_scratch_size += scratch_int_view::shmem_size(neighbor_lists.extent(1)); // indices + //team_scratch_size += scratch_double_view::shmem_size(_dim); // target coordinate + + //// minimum number of neighbors found over all target sites' neighborhoods + //size_t min_num_neighbors = 0; + //// + //// part 1. do knn search for neighbors needed for unisolvency + //// each row of neighbor lists is a neighbor list for the target site corresponding to that row + //// + //// as long as neighbor_lists can hold the number of neighbors_needed, we don't need to check + //// that the maximum number of neighbors will fit into neighbor_lists + //// + //Kokkos::parallel_reduce("knn search", host_team_policy(num_target_sites, Kokkos::AUTO) + // .set_scratch_size(0 /*shared memory level*/, Kokkos::PerTeam(team_scratch_size)), + // KOKKOS_LAMBDA(const host_member_type& teamMember, size_t& t_min_num_neighbors) { + + // // make unmanaged scratch views + // scratch_double_view neighbor_distances(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); + // scratch_int_view neighbor_indices(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); + // scratch_double_view this_target_coord(teamMember.team_scratch(0 /*shared memory*/), _dim); + + // size_t neighbors_found = 0; + + // const int i = teamMember.league_rank(); + + // Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, neighbor_lists.extent(1)), [=](const int j) { + // neighbor_indices(j) = 0; + // neighbor_distances(j) = -1.0; + // }); // - // part 1. do knn search for neighbors needed for unisolvency - // each row of neighbor lists is a neighbor list for the target site corresponding to that row + // teamMember.team_barrier(); + // Kokkos::single(Kokkos::PerTeam(teamMember), [&] () { + // // target_coords is LayoutLeft on device and its HostMirror, so giving a pointer to + // // this data would lead to a wrong result if the device is a GPU + + // for (int j=0; j<_dim; ++j) { + // this_target_coord(j) = trg_pts_view(i,j); + // } + + // if (_dim==1) { + // neighbors_found = _tree_1d->knnSearch(this_target_coord.data(), neighbors_needed, + // neighbor_indices.data(), neighbor_distances.data()) ; + // } else if (_dim==2) { + // neighbors_found = _tree_2d->knnSearch(this_target_coord.data(), neighbors_needed, + // neighbor_indices.data(), neighbor_distances.data()) ; + // } else if (_dim==3) { + // neighbors_found = _tree_3d->knnSearch(this_target_coord.data(), neighbors_needed, + // neighbor_indices.data(), neighbor_distances.data()) ; + // } + + // // get minimum number of neighbors found over all target sites' neighborhoods + // t_min_num_neighbors = (neighbors_found < t_min_num_neighbors) ? neighbors_found : t_min_num_neighbors; // - // as long as neighbor_lists can hold the number of neighbors_needed, we don't need to check - // that the maximum number of neighbors will fit into neighbor_lists + // // scale by epsilon_multiplier to window from location where the last neighbor was found + // epsilons(i) = (neighbor_distances(neighbors_found-1) > 0) ? + // std::sqrt(neighbor_distances(neighbors_found-1))*epsilon_multiplier : 1e-14*epsilon_multiplier; + // // the only time the second case using 1e-14 is used is when either zero neighbors or exactly one + // // neighbor (neighbor is target site) is found. when the follow on radius search is conducted, the one + // // neighbor (target site) will not be found if left at 0, so any positive amount will do, however 1e-14 + // // should is small enough to ensure that other neighbors are not found + + // // needs furthest neighbor's distance for next portion + // compadre_kernel_assert_release((neighbors_found(min_num_neighbors) ); + //Kokkos::fence(); // - Kokkos::parallel_reduce("knn search", host_team_policy(num_target_sites, Kokkos::AUTO) - .set_scratch_size(0 /*shared memory level*/, Kokkos::PerTeam(team_scratch_size)), - KOKKOS_LAMBDA(const host_member_type& teamMember, size_t& t_min_num_neighbors) { - - // make unmanaged scratch views - scratch_double_view neighbor_distances(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); - scratch_int_view neighbor_indices(teamMember.team_scratch(0 /*shared memory*/), neighbor_lists.extent(1)); - scratch_double_view this_target_coord(teamMember.team_scratch(0 /*shared memory*/), _dim); - - size_t neighbors_found = 0; - - const int i = teamMember.league_rank(); - - Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, neighbor_lists.extent(1)), [=](const int j) { - neighbor_indices(j) = 0; - neighbor_distances(j) = -1.0; - }); - - teamMember.team_barrier(); - Kokkos::single(Kokkos::PerTeam(teamMember), [&] () { - // target_coords is LayoutLeft on device and its HostMirror, so giving a pointer to - // this data would lead to a wrong result if the device is a GPU - - for (int j=0; j<_dim; ++j) { - this_target_coord(j) = trg_pts_view(i,j); - } + //// if no target sites, then min_num_neighbors is set to neighbors_needed + //// which also avoids min_num_neighbors being improperly set by min reduction + //if (num_target_sites==0) min_num_neighbors = neighbors_needed; - if (_dim==1) { - neighbors_found = _tree_1d->knnSearch(this_target_coord.data(), neighbors_needed, - neighbor_indices.data(), neighbor_distances.data()) ; - } else if (_dim==2) { - neighbors_found = _tree_2d->knnSearch(this_target_coord.data(), neighbors_needed, - neighbor_indices.data(), neighbor_distances.data()) ; - } else if (_dim==3) { - neighbors_found = _tree_3d->knnSearch(this_target_coord.data(), neighbors_needed, - neighbor_indices.data(), neighbor_distances.data()) ; - } + //// Next, check that we found the neighbors_needed number that we require for unisolvency + //compadre_assert_release((num_target_sites==0 || (min_num_neighbors>=(size_t)neighbors_needed)) + // && "Neighbor search failed to find number of neighbors needed for unisolvency."); + // + //// call a radius search using values now stored in epsilons + //size_t max_num_neighbors = generate2DNeighborListsFromRadiusSearch(is_dry_run, trg_pts_view, neighbor_lists, + // epsilons, 0.0 /*don't set uniform radius*/, max_search_radius); - // get minimum number of neighbors found over all target sites' neighborhoods - t_min_num_neighbors = (neighbors_found < t_min_num_neighbors) ? neighbors_found : t_min_num_neighbors; - - // scale by epsilon_multiplier to window from location where the last neighbor was found - epsilons(i) = (neighbor_distances(neighbors_found-1) > 0) ? - std::sqrt(neighbor_distances(neighbors_found-1))*epsilon_multiplier : 1e-14*epsilon_multiplier; - // the only time the second case using 1e-14 is used is when either zero neighbors or exactly one - // neighbor (neighbor is target site) is found. when the follow on radius search is conducted, the one - // neighbor (target site) will not be found if left at 0, so any positive amount will do, however 1e-14 - // should is small enough to ensure that other neighbors are not found - - // needs furthest neighbor's distance for next portion - compadre_kernel_assert_release((neighbors_found(min_num_neighbors) ); - Kokkos::fence(); - - // if no target sites, then min_num_neighbors is set to neighbors_needed - // which also avoids min_num_neighbors being improperly set by min reduction - if (num_target_sites==0) min_num_neighbors = neighbors_needed; - - // Next, check that we found the neighbors_needed number that we require for unisolvency - compadre_assert_release((num_target_sites==0 || (min_num_neighbors>=(size_t)neighbors_needed)) - && "Neighbor search failed to find number of neighbors needed for unisolvency."); - - // call a radius search using values now stored in epsilons - size_t max_num_neighbors = generate2DNeighborListsFromRadiusSearch(is_dry_run, trg_pts_view, neighbor_lists, - epsilons, 0.0 /*don't set uniform radius*/, max_search_radius); - - return max_num_neighbors; + return 0;//max_num_neighbors; } /*! \brief Generates compressed row neighbor lists by performing a k-nearest neighbor search @@ -669,114 +594,114 @@ class PointCloudSearch { // loop size const int num_target_sites = trg_pts_view.extent(0); - if ((!_tree_1d && _dim==1) || (!_tree_2d && _dim==2) || (!_tree_3d && _dim==3)) { - this->generateKDTree(); - } - Kokkos::fence(); - - compadre_assert_release((number_of_neighbors_list.extent(0)==(size_t)num_target_sites ) - && "number_of_neighbors_list or neighbor lists View does not have large enough dimensions"); - compadre_assert_release((neighbor_lists_view_type::rank==1) && "neighbor_lists must be a 1D Kokkos view."); - - // if dry-run, neighbors_needed, else max over previous dry-run - int max_neighbor_list_row_storage_size = neighbors_needed; - if (!is_dry_run) { - auto nla = CreateNeighborLists(neighbor_lists, number_of_neighbors_list); - max_neighbor_list_row_storage_size = nla.getMaxNumNeighbors(); - } - - compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) - && "epsilons View does not have the correct dimension"); - - typedef Kokkos::View > - scratch_double_view; + //if ((!_tree_1d && _dim==1) || (!_tree_2d && _dim==2) || (!_tree_3d && _dim==3)) { + // this->generateKDTree(); + //} + //Kokkos::fence(); - typedef Kokkos::View > - scratch_int_view; + //compadre_assert_release((number_of_neighbors_list.extent(0)==(size_t)num_target_sites ) + // && "number_of_neighbors_list or neighbor lists View does not have large enough dimensions"); + //compadre_assert_release((neighbor_lists_view_type::rank==1) && "neighbor_lists must be a 1D Kokkos view."); - // determine scratch space size needed - int team_scratch_size = 0; - team_scratch_size += scratch_double_view::shmem_size(max_neighbor_list_row_storage_size); // distances - team_scratch_size += scratch_int_view::shmem_size(max_neighbor_list_row_storage_size); // indices - team_scratch_size += scratch_double_view::shmem_size(_dim); // target coordinate + //// if dry-run, neighbors_needed, else max over previous dry-run + //int max_neighbor_list_row_storage_size = neighbors_needed; + //if (!is_dry_run) { + // auto nla = CreateNeighborLists(neighbor_lists, number_of_neighbors_list); + // max_neighbor_list_row_storage_size = nla.getMaxNumNeighbors(); + //} - // minimum number of neighbors found over all target sites' neighborhoods - size_t min_num_neighbors = 0; + //compadre_assert_release((epsilons.extent(0)==(size_t)num_target_sites) + // && "epsilons View does not have the correct dimension"); + + //typedef Kokkos::View > + // scratch_double_view; + + //typedef Kokkos::View > + // scratch_int_view; + + //// determine scratch space size needed + //int team_scratch_size = 0; + //team_scratch_size += scratch_double_view::shmem_size(max_neighbor_list_row_storage_size); // distances + //team_scratch_size += scratch_int_view::shmem_size(max_neighbor_list_row_storage_size); // indices + //team_scratch_size += scratch_double_view::shmem_size(_dim); // target coordinate + + //// minimum number of neighbors found over all target sites' neighborhoods + //size_t min_num_neighbors = 0; + //// + //// part 1. do knn search for neighbors needed for unisolvency + //// each row of neighbor lists is a neighbor list for the target site corresponding to that row + //// + //// as long as neighbor_lists can hold the number of neighbors_needed, we don't need to check + //// that the maximum number of neighbors will fit into neighbor_lists + //// + //Kokkos::parallel_reduce("knn search", host_team_policy(num_target_sites, Kokkos::AUTO) + // .set_scratch_size(0 /*shared memory level*/, Kokkos::PerTeam(team_scratch_size)), + // KOKKOS_LAMBDA(const host_member_type& teamMember, size_t& t_min_num_neighbors) { + + // // make unmanaged scratch views + // scratch_double_view neighbor_distances(teamMember.team_scratch(0 /*shared memory*/), max_neighbor_list_row_storage_size); + // scratch_int_view neighbor_indices(teamMember.team_scratch(0 /*shared memory*/), max_neighbor_list_row_storage_size); + // scratch_double_view this_target_coord(teamMember.team_scratch(0 /*shared memory*/), _dim); + + // size_t neighbors_found = 0; + + // const int i = teamMember.league_rank(); + + // Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, max_neighbor_list_row_storage_size), [=](const int j) { + // neighbor_indices(j) = 0; + // neighbor_distances(j) = -1.0; + // }); // - // part 1. do knn search for neighbors needed for unisolvency - // each row of neighbor lists is a neighbor list for the target site corresponding to that row + // teamMember.team_barrier(); + // Kokkos::single(Kokkos::PerTeam(teamMember), [&] () { + // // target_coords is LayoutLeft on device and its HostMirror, so giving a pointer to + // // this data would lead to a wrong result if the device is a GPU + + // for (int j=0; j<_dim; ++j) { + // this_target_coord(j) = trg_pts_view(i,j); + // } + + // if (_dim==1) { + // neighbors_found = _tree_1d->knnSearch(this_target_coord.data(), neighbors_needed, + // neighbor_indices.data(), neighbor_distances.data()) ; + // } else if (_dim==2) { + // neighbors_found = _tree_2d->knnSearch(this_target_coord.data(), neighbors_needed, + // neighbor_indices.data(), neighbor_distances.data()) ; + // } else if (_dim==3) { + // neighbors_found = _tree_3d->knnSearch(this_target_coord.data(), neighbors_needed, + // neighbor_indices.data(), neighbor_distances.data()) ; + // } + + // // get minimum number of neighbors found over all target sites' neighborhoods + // t_min_num_neighbors = (neighbors_found < t_min_num_neighbors) ? neighbors_found : t_min_num_neighbors; // - // as long as neighbor_lists can hold the number of neighbors_needed, we don't need to check - // that the maximum number of neighbors will fit into neighbor_lists + // // scale by epsilon_multiplier to window from location where the last neighbor was found + // epsilons(i) = (neighbor_distances(neighbors_found-1) > 0) ? + // std::sqrt(neighbor_distances(neighbors_found-1))*epsilon_multiplier : 1e-14*epsilon_multiplier; + // // the only time the second case using 1e-14 is used is when either zero neighbors or exactly one + // // neighbor (neighbor is target site) is found. when the follow on radius search is conducted, the one + // // neighbor (target site) will not be found if left at 0, so any positive amount will do, however 1e-14 + // // should is small enough to ensure that other neighbors are not found + + // compadre_kernel_assert_release((epsilons(i)<=max_search_radius || max_search_radius==0 || is_dry_run) + // && "max_search_radius given (generally derived from the size of a halo region), \ + // and search radius needed would exceed this max_search_radius."); + // // neighbor_distances stores squared distances from neighbor to target, as returned by nanoflann + // }); + //}, Kokkos::Min(min_num_neighbors) ); + //Kokkos::fence(); // - Kokkos::parallel_reduce("knn search", host_team_policy(num_target_sites, Kokkos::AUTO) - .set_scratch_size(0 /*shared memory level*/, Kokkos::PerTeam(team_scratch_size)), - KOKKOS_LAMBDA(const host_member_type& teamMember, size_t& t_min_num_neighbors) { - - // make unmanaged scratch views - scratch_double_view neighbor_distances(teamMember.team_scratch(0 /*shared memory*/), max_neighbor_list_row_storage_size); - scratch_int_view neighbor_indices(teamMember.team_scratch(0 /*shared memory*/), max_neighbor_list_row_storage_size); - scratch_double_view this_target_coord(teamMember.team_scratch(0 /*shared memory*/), _dim); + //// if no target sites, then min_num_neighbors is set to neighbors_needed + //// which also avoids min_num_neighbors being improperly set by min reduction + //if (num_target_sites==0) min_num_neighbors = neighbors_needed; - size_t neighbors_found = 0; - - const int i = teamMember.league_rank(); - - Kokkos::parallel_for(Kokkos::TeamThreadRange(teamMember, max_neighbor_list_row_storage_size), [=](const int j) { - neighbor_indices(j) = 0; - neighbor_distances(j) = -1.0; - }); - - teamMember.team_barrier(); - Kokkos::single(Kokkos::PerTeam(teamMember), [&] () { - // target_coords is LayoutLeft on device and its HostMirror, so giving a pointer to - // this data would lead to a wrong result if the device is a GPU - - for (int j=0; j<_dim; ++j) { - this_target_coord(j) = trg_pts_view(i,j); - } - - if (_dim==1) { - neighbors_found = _tree_1d->knnSearch(this_target_coord.data(), neighbors_needed, - neighbor_indices.data(), neighbor_distances.data()) ; - } else if (_dim==2) { - neighbors_found = _tree_2d->knnSearch(this_target_coord.data(), neighbors_needed, - neighbor_indices.data(), neighbor_distances.data()) ; - } else if (_dim==3) { - neighbors_found = _tree_3d->knnSearch(this_target_coord.data(), neighbors_needed, - neighbor_indices.data(), neighbor_distances.data()) ; - } - - // get minimum number of neighbors found over all target sites' neighborhoods - t_min_num_neighbors = (neighbors_found < t_min_num_neighbors) ? neighbors_found : t_min_num_neighbors; - - // scale by epsilon_multiplier to window from location where the last neighbor was found - epsilons(i) = (neighbor_distances(neighbors_found-1) > 0) ? - std::sqrt(neighbor_distances(neighbors_found-1))*epsilon_multiplier : 1e-14*epsilon_multiplier; - // the only time the second case using 1e-14 is used is when either zero neighbors or exactly one - // neighbor (neighbor is target site) is found. when the follow on radius search is conducted, the one - // neighbor (target site) will not be found if left at 0, so any positive amount will do, however 1e-14 - // should is small enough to ensure that other neighbors are not found - - compadre_kernel_assert_release((epsilons(i)<=max_search_radius || max_search_radius==0 || is_dry_run) - && "max_search_radius given (generally derived from the size of a halo region), \ - and search radius needed would exceed this max_search_radius."); - // neighbor_distances stores squared distances from neighbor to target, as returned by nanoflann - }); - }, Kokkos::Min(min_num_neighbors) ); - Kokkos::fence(); - - // if no target sites, then min_num_neighbors is set to neighbors_needed - // which also avoids min_num_neighbors being improperly set by min reduction - if (num_target_sites==0) min_num_neighbors = neighbors_needed; - - // Next, check that we found the neighbors_needed number that we require for unisolvency - compadre_assert_release((num_target_sites==0 || (min_num_neighbors>=(size_t)neighbors_needed)) - && "Neighbor search failed to find number of neighbors needed for unisolvency."); - - // call a radius search using values now stored in epsilons - generateCRNeighborListsFromRadiusSearch(is_dry_run, trg_pts_view, neighbor_lists, - number_of_neighbors_list, epsilons, 0.0 /*don't set uniform radius*/, max_search_radius); + //// Next, check that we found the neighbors_needed number that we require for unisolvency + //compadre_assert_release((num_target_sites==0 || (min_num_neighbors>=(size_t)neighbors_needed)) + // && "Neighbor search failed to find number of neighbors needed for unisolvency."); + // + //// call a radius search using values now stored in epsilons + //generateCRNeighborListsFromRadiusSearch(is_dry_run, trg_pts_view, neighbor_lists, + // number_of_neighbors_list, epsilons, 0.0 /*don't set uniform radius*/, max_search_radius); auto nla = CreateNeighborLists(number_of_neighbors_list); return nla.getTotalNeighborsOverAllListsHost(); From 9d6bb957d23c04eab2a2124c41cf87166b2222ff Mon Sep 17 00:00:00 2001 From: Paul Kuberry Date: Thu, 23 Sep 2021 07:52:35 -0600 Subject: [PATCH 02/40] Add ArborX to arborx folder at SHA 3ff6ce508f0fee7 --- arborx/.clang-format | 22 + arborx/.clang-format-ignore | 0 arborx/.clang-tidy | 109 +++ arborx/.gitattributes | 7 + arborx/.github/workflows/gitlab-ci.yml | 21 + arborx/.gitignore | 4 + arborx/.gitlab-ci.yml | 147 +++ arborx/.jenkins | 462 +++++++++ arborx/.mailmap | 2 + arborx/CMakeLists.txt | 171 ++++ arborx/CONTRIBUTING.md | 12 + arborx/LICENSE | 27 + arborx/README.md | 57 ++ arborx/benchmarks/CMakeLists.txt | 7 + arborx/benchmarks/bvh_driver/CMakeLists.txt | 16 + .../bvh_driver/benchmark_registration.hpp | 441 +++++++++ arborx/benchmarks/bvh_driver/bvh_driver.cpp | 295 ++++++ .../distributed_tree_driver/CMakeLists.txt | 4 + .../distributed_tree_driver.cpp | 637 ++++++++++++ .../execution_space_instances/CMakeLists.txt | 4 + .../execution_space_instances_driver.cpp | 225 +++++ .../benchmarks/point_clouds/point_clouds.hpp | 216 ++++ arborx/cmake/ArborXConfig.cmake.in | 26 + arborx/cmake/ArborXSettings.cmake.in | 4 + arborx/cmake/SetupVersion.cmake | 30 + arborx/docker/.env | 2 + arborx/docker/Dockerfile | 145 +++ arborx/docker/Dockerfile.hipcc | 110 +++ arborx/docker/Dockerfile.pgi | 122 +++ arborx/docker/Dockerfile.sycl | 132 +++ arborx/docker/README.md | 18 + arborx/docker/docker-compose.yml | 20 + arborx/docs/LICENSE.ECL | 36 + arborx/docs/logos/arborx_logo_v1.0.png | Bin 0 -> 79458 bytes arborx/docs/logos/arborx_logo_v1.0.svg | 357 +++++++ arborx/docs/logos/arborx_logo_v1.0_nobg.png | Bin 0 -> 81640 bytes arborx/examples/CMakeLists.txt | 20 + arborx/examples/access_traits/CMakeLists.txt | 9 + .../example_cuda_access_traits.cpp | 98 ++ .../example_host_access_traits.cpp | 52 + arborx/examples/brute_force/CMakeLists.txt | 3 + arborx/examples/brute_force/brute_force.cpp | 124 +++ arborx/examples/callback/CMakeLists.txt | 3 + arborx/examples/callback/example_callback.cpp | 145 +++ .../dbscan/ArborX_DBSCANVerification.hpp | 320 ++++++ arborx/examples/dbscan/CMakeLists.txt | 11 + arborx/examples/dbscan/README.md | 62 ++ arborx/examples/dbscan/converter.cpp | 472 +++++++++ arborx/examples/dbscan/dbscan.cpp | 504 ++++++++++ arborx/examples/dbscan/input.txt | 9 + arborx/examples/raytracing/CMakeLists.txt | 3 + .../raytracing/example_raytracing.cpp | 200 ++++ .../simple_intersection/CMakeLists.txt | 3 + .../example_intersection.cpp | 168 ++++ arborx/examples/viz/CMakeLists.txt | 7 + arborx/examples/viz/arborx_query_sort.py | 121 +++ arborx/examples/viz/leaf_cloud.txt | 59 ++ arborx/examples/viz/requirements.txt | 3 + arborx/examples/viz/tree_visualization.cpp | 182 ++++ arborx/scripts/benchmark.py | 101 ++ arborx/scripts/benchmark_plot.py | 199 ++++ arborx/scripts/check_format_cpp.sh | 75 ++ arborx/scripts/docker_cmake | 25 + arborx/scripts/requirements.txt | 3 + arborx/src/ArborX.hpp | 28 + arborx/src/ArborX_BruteForce.hpp | 125 +++ arborx/src/ArborX_Config.hpp.in | 20 + arborx/src/ArborX_CrsGraphWrapper.hpp | 46 + arborx/src/ArborX_DBSCAN.hpp | 502 ++++++++++ arborx/src/ArborX_DistributedSearchTree.hpp | 25 + arborx/src/ArborX_DistributedTree.hpp | 225 +++++ arborx/src/ArborX_LinearBVH.hpp | 329 +++++++ arborx/src/ArborX_Version.hpp.in | 28 + arborx/src/details/ArborX_AccessTraits.hpp | 192 ++++ arborx/src/details/ArborX_Box.hpp | 120 +++ arborx/src/details/ArborX_Callbacks.hpp | 212 ++++ .../src/details/ArborX_DetailsAlgorithms.hpp | 259 +++++ .../details/ArborX_DetailsBatchedQueries.hpp | 188 ++++ .../details/ArborX_DetailsBruteForceImpl.hpp | 152 +++ arborx/src/details/ArborX_DetailsConcepts.hpp | 77 ++ .../src/details/ArborX_DetailsContainers.hpp | 106 ++ .../ArborX_DetailsCrsGraphWrapperImpl.hpp | 475 +++++++++ .../ArborX_DetailsDistributedTreeImpl.hpp | 921 ++++++++++++++++++ .../src/details/ArborX_DetailsDistributor.hpp | 425 ++++++++ arborx/src/details/ArborX_DetailsFDBSCAN.hpp | 100 ++ .../details/ArborX_DetailsFDBSCANDenseBox.hpp | 390 ++++++++ .../ArborX_DetailsHappyTreeFriends.hpp | 61 ++ arborx/src/details/ArborX_DetailsHeap.hpp | 129 +++ ...rX_DetailsKokkosExtAccessibilityTraits.hpp | 50 + ...rborX_DetailsKokkosExtArithmeticTraits.hpp | 72 ++ .../ArborX_DetailsKokkosExtMathFunctions.hpp | 41 + ...rborX_DetailsKokkosExtMinMaxOperations.hpp | 36 + ...rX_DetailsKokkosExtScopedProfileRegion.hpp | 37 + .../src/details/ArborX_DetailsMortonCode.hpp | 60 ++ arborx/src/details/ArborX_DetailsNode.hpp | 119 +++ .../ArborX_DetailsOperatorFunctionObjects.hpp | 45 + .../details/ArborX_DetailsPermutedData.hpp | 67 ++ .../details/ArborX_DetailsPriorityQueue.hpp | 113 +++ .../src/details/ArborX_DetailsSortUtils.hpp | 281 ++++++ arborx/src/details/ArborX_DetailsStack.hpp | 73 ++ arborx/src/details/ArborX_DetailsTags.hpp | 57 ++ .../ArborX_DetailsTreeConstruction.hpp | 416 ++++++++ .../details/ArborX_DetailsTreeTraversal.hpp | 473 +++++++++ .../ArborX_DetailsTreeVisualization.hpp | 242 +++++ .../src/details/ArborX_DetailsUnionFind.hpp | 189 ++++ arborx/src/details/ArborX_DetailsUtils.hpp | 578 +++++++++++ arborx/src/details/ArborX_Exception.hpp | 40 + arborx/src/details/ArborX_KDOP.hpp | 316 ++++++ arborx/src/details/ArborX_Point.hpp | 74 ++ arborx/src/details/ArborX_Predicates.hpp | 143 +++ arborx/src/details/ArborX_Ray.hpp | 286 ++++++ arborx/src/details/ArborX_Sphere.hpp | 47 + arborx/src/details/ArborX_TraversalPolicy.hpp | 52 + arborx/test/ArborXTest_TreeTypeTraits.hpp | 85 ++ arborx/test/ArborX_BoostGeometryAdapters.hpp | 97 ++ arborx/test/ArborX_BoostRTreeHelpers.hpp | 337 +++++++ arborx/test/ArborX_BoostRangeAdapters.hpp | 135 +++ arborx/test/ArborX_EnableDeviceTypes.hpp.in | 21 + arborx/test/ArborX_EnableViewComparison.hpp | 71 ++ .../test/BoostTest_CUDA_clang_workarounds.hpp | 24 + arborx/test/CMakeLists.txt | 186 ++++ arborx/test/Search_UnitTestHelpers.hpp | 276 ++++++ .../boost_ext/CompressedStorageComparison.hpp | 110 +++ .../test/boost_ext/KokkosPairComparison.hpp | 46 + arborx/test/boost_ext/TupleComparison.hpp | 62 ++ .../headers_self_contained/CMakeLists.txt | 15 + .../test/headers_self_contained/tstHeader.cpp | 27 + arborx/test/tstBoostGeometryAdapters.cpp | 241 +++++ arborx/test/tstBoostRangeAdapters.cpp | 78 ++ arborx/test/tstCompileOnlyAccessTraits.cpp | 87 ++ arborx/test/tstCompileOnlyCallbacks.cpp | 150 +++ arborx/test/tstCompileOnlyConcepts.cpp | 23 + arborx/test/tstCompileOnlyMain.cpp | 12 + .../test/tstCompileOnlyTypeRequirements.cpp | 64 ++ arborx/test/tstContainerAdaptors.cpp | 73 ++ arborx/test/tstDBSCAN.cpp | 270 +++++ arborx/test/tstDetailsAlgorithms.cpp | 234 +++++ arborx/test/tstDetailsBatchedQueries.cpp | 69 ++ arborx/test/tstDetailsCrsGraphWrapperImpl.cpp | 95 ++ arborx/test/tstDetailsDistributedTreeImpl.cpp | 418 ++++++++ arborx/test/tstDetailsTreeConstruction.cpp | 294 ++++++ arborx/test/tstDetailsUtils.cpp | 268 +++++ arborx/test/tstDistributedTree.cpp | 693 +++++++++++++ arborx/test/tstException.cpp | 37 + arborx/test/tstHeapOperations.cpp | 208 ++++ arborx/test/tstKDOP.cpp | 143 +++ arborx/test/tstKokkosToolsAnnotations.cpp | 249 +++++ .../tstKokkosToolsDistributedAnnotations.cpp | 197 ++++ arborx/test/tstPriorityQueueMiscellaneous.cpp | 144 +++ arborx/test/tstQueryTreeCallbacks.cpp | 343 +++++++ .../test/tstQueryTreeComparisonWithBoost.cpp | 269 +++++ arborx/test/tstQueryTreeDegenerate.cpp | 403 ++++++++ arborx/test/tstQueryTreeIntersectsKDOP.cpp | 82 ++ .../test/tstQueryTreeManufacturedSolution.cpp | 257 +++++ arborx/test/tstQueryTreeTraversalPolicy.cpp | 177 ++++ arborx/test/tstRay.cpp | 259 +++++ arborx/test/tstScopedProfileRegion.cpp | 97 ++ arborx/test/tstSequenceContainers.cpp | 146 +++ arborx/test/utf_main.cpp | 45 + 159 files changed, 23528 insertions(+) create mode 100644 arborx/.clang-format create mode 100644 arborx/.clang-format-ignore create mode 100644 arborx/.clang-tidy create mode 100644 arborx/.gitattributes create mode 100644 arborx/.github/workflows/gitlab-ci.yml create mode 100644 arborx/.gitignore create mode 100644 arborx/.gitlab-ci.yml create mode 100644 arborx/.jenkins create mode 100644 arborx/.mailmap create mode 100644 arborx/CMakeLists.txt create mode 100644 arborx/CONTRIBUTING.md create mode 100644 arborx/LICENSE create mode 100644 arborx/README.md create mode 100644 arborx/benchmarks/CMakeLists.txt create mode 100644 arborx/benchmarks/bvh_driver/CMakeLists.txt create mode 100644 arborx/benchmarks/bvh_driver/benchmark_registration.hpp create mode 100644 arborx/benchmarks/bvh_driver/bvh_driver.cpp create mode 100644 arborx/benchmarks/distributed_tree_driver/CMakeLists.txt create mode 100644 arborx/benchmarks/distributed_tree_driver/distributed_tree_driver.cpp create mode 100644 arborx/benchmarks/execution_space_instances/CMakeLists.txt create mode 100644 arborx/benchmarks/execution_space_instances/execution_space_instances_driver.cpp create mode 100644 arborx/benchmarks/point_clouds/point_clouds.hpp create mode 100644 arborx/cmake/ArborXConfig.cmake.in create mode 100644 arborx/cmake/ArborXSettings.cmake.in create mode 100644 arborx/cmake/SetupVersion.cmake create mode 100644 arborx/docker/.env create mode 100644 arborx/docker/Dockerfile create mode 100644 arborx/docker/Dockerfile.hipcc create mode 100644 arborx/docker/Dockerfile.pgi create mode 100644 arborx/docker/Dockerfile.sycl create mode 100644 arborx/docker/README.md create mode 100644 arborx/docker/docker-compose.yml create mode 100644 arborx/docs/LICENSE.ECL create mode 100644 arborx/docs/logos/arborx_logo_v1.0.png create mode 100644 arborx/docs/logos/arborx_logo_v1.0.svg create mode 100644 arborx/docs/logos/arborx_logo_v1.0_nobg.png create mode 100644 arborx/examples/CMakeLists.txt create mode 100644 arborx/examples/access_traits/CMakeLists.txt create mode 100644 arborx/examples/access_traits/example_cuda_access_traits.cpp create mode 100644 arborx/examples/access_traits/example_host_access_traits.cpp create mode 100644 arborx/examples/brute_force/CMakeLists.txt create mode 100644 arborx/examples/brute_force/brute_force.cpp create mode 100644 arborx/examples/callback/CMakeLists.txt create mode 100644 arborx/examples/callback/example_callback.cpp create mode 100644 arborx/examples/dbscan/ArborX_DBSCANVerification.hpp create mode 100644 arborx/examples/dbscan/CMakeLists.txt create mode 100644 arborx/examples/dbscan/README.md create mode 100644 arborx/examples/dbscan/converter.cpp create mode 100644 arborx/examples/dbscan/dbscan.cpp create mode 100644 arborx/examples/dbscan/input.txt create mode 100644 arborx/examples/raytracing/CMakeLists.txt create mode 100644 arborx/examples/raytracing/example_raytracing.cpp create mode 100644 arborx/examples/simple_intersection/CMakeLists.txt create mode 100644 arborx/examples/simple_intersection/example_intersection.cpp create mode 100644 arborx/examples/viz/CMakeLists.txt create mode 100755 arborx/examples/viz/arborx_query_sort.py create mode 100644 arborx/examples/viz/leaf_cloud.txt create mode 100644 arborx/examples/viz/requirements.txt create mode 100644 arborx/examples/viz/tree_visualization.cpp create mode 100644 arborx/scripts/benchmark.py create mode 100755 arborx/scripts/benchmark_plot.py create mode 100755 arborx/scripts/check_format_cpp.sh create mode 100644 arborx/scripts/docker_cmake create mode 100644 arborx/scripts/requirements.txt create mode 100644 arborx/src/ArborX.hpp create mode 100644 arborx/src/ArborX_BruteForce.hpp create mode 100644 arborx/src/ArborX_Config.hpp.in create mode 100644 arborx/src/ArborX_CrsGraphWrapper.hpp create mode 100644 arborx/src/ArborX_DBSCAN.hpp create mode 100644 arborx/src/ArborX_DistributedSearchTree.hpp create mode 100644 arborx/src/ArborX_DistributedTree.hpp create mode 100644 arborx/src/ArborX_LinearBVH.hpp create mode 100644 arborx/src/ArborX_Version.hpp.in create mode 100644 arborx/src/details/ArborX_AccessTraits.hpp create mode 100644 arborx/src/details/ArborX_Box.hpp create mode 100644 arborx/src/details/ArborX_Callbacks.hpp create mode 100644 arborx/src/details/ArborX_DetailsAlgorithms.hpp create mode 100644 arborx/src/details/ArborX_DetailsBatchedQueries.hpp create mode 100644 arborx/src/details/ArborX_DetailsBruteForceImpl.hpp create mode 100644 arborx/src/details/ArborX_DetailsConcepts.hpp create mode 100644 arborx/src/details/ArborX_DetailsContainers.hpp create mode 100644 arborx/src/details/ArborX_DetailsCrsGraphWrapperImpl.hpp create mode 100644 arborx/src/details/ArborX_DetailsDistributedTreeImpl.hpp create mode 100644 arborx/src/details/ArborX_DetailsDistributor.hpp create mode 100644 arborx/src/details/ArborX_DetailsFDBSCAN.hpp create mode 100644 arborx/src/details/ArborX_DetailsFDBSCANDenseBox.hpp create mode 100644 arborx/src/details/ArborX_DetailsHappyTreeFriends.hpp create mode 100644 arborx/src/details/ArborX_DetailsHeap.hpp create mode 100644 arborx/src/details/ArborX_DetailsKokkosExtAccessibilityTraits.hpp create mode 100644 arborx/src/details/ArborX_DetailsKokkosExtArithmeticTraits.hpp create mode 100644 arborx/src/details/ArborX_DetailsKokkosExtMathFunctions.hpp create mode 100644 arborx/src/details/ArborX_DetailsKokkosExtMinMaxOperations.hpp create mode 100644 arborx/src/details/ArborX_DetailsKokkosExtScopedProfileRegion.hpp create mode 100644 arborx/src/details/ArborX_DetailsMortonCode.hpp create mode 100644 arborx/src/details/ArborX_DetailsNode.hpp create mode 100644 arborx/src/details/ArborX_DetailsOperatorFunctionObjects.hpp create mode 100644 arborx/src/details/ArborX_DetailsPermutedData.hpp create mode 100644 arborx/src/details/ArborX_DetailsPriorityQueue.hpp create mode 100644 arborx/src/details/ArborX_DetailsSortUtils.hpp create mode 100644 arborx/src/details/ArborX_DetailsStack.hpp create mode 100644 arborx/src/details/ArborX_DetailsTags.hpp create mode 100644 arborx/src/details/ArborX_DetailsTreeConstruction.hpp create mode 100644 arborx/src/details/ArborX_DetailsTreeTraversal.hpp create mode 100644 arborx/src/details/ArborX_DetailsTreeVisualization.hpp create mode 100644 arborx/src/details/ArborX_DetailsUnionFind.hpp create mode 100644 arborx/src/details/ArborX_DetailsUtils.hpp create mode 100644 arborx/src/details/ArborX_Exception.hpp create mode 100644 arborx/src/details/ArborX_KDOP.hpp create mode 100644 arborx/src/details/ArborX_Point.hpp create mode 100644 arborx/src/details/ArborX_Predicates.hpp create mode 100644 arborx/src/details/ArborX_Ray.hpp create mode 100644 arborx/src/details/ArborX_Sphere.hpp create mode 100644 arborx/src/details/ArborX_TraversalPolicy.hpp create mode 100644 arborx/test/ArborXTest_TreeTypeTraits.hpp create mode 100644 arborx/test/ArborX_BoostGeometryAdapters.hpp create mode 100644 arborx/test/ArborX_BoostRTreeHelpers.hpp create mode 100644 arborx/test/ArborX_BoostRangeAdapters.hpp create mode 100644 arborx/test/ArborX_EnableDeviceTypes.hpp.in create mode 100644 arborx/test/ArborX_EnableViewComparison.hpp create mode 100644 arborx/test/BoostTest_CUDA_clang_workarounds.hpp create mode 100644 arborx/test/CMakeLists.txt create mode 100644 arborx/test/Search_UnitTestHelpers.hpp create mode 100644 arborx/test/boost_ext/CompressedStorageComparison.hpp create mode 100644 arborx/test/boost_ext/KokkosPairComparison.hpp create mode 100644 arborx/test/boost_ext/TupleComparison.hpp create mode 100644 arborx/test/headers_self_contained/CMakeLists.txt create mode 100644 arborx/test/headers_self_contained/tstHeader.cpp create mode 100644 arborx/test/tstBoostGeometryAdapters.cpp create mode 100644 arborx/test/tstBoostRangeAdapters.cpp create mode 100644 arborx/test/tstCompileOnlyAccessTraits.cpp create mode 100644 arborx/test/tstCompileOnlyCallbacks.cpp create mode 100644 arborx/test/tstCompileOnlyConcepts.cpp create mode 100644 arborx/test/tstCompileOnlyMain.cpp create mode 100644 arborx/test/tstCompileOnlyTypeRequirements.cpp create mode 100644 arborx/test/tstContainerAdaptors.cpp create mode 100644 arborx/test/tstDBSCAN.cpp create mode 100644 arborx/test/tstDetailsAlgorithms.cpp create mode 100644 arborx/test/tstDetailsBatchedQueries.cpp create mode 100644 arborx/test/tstDetailsCrsGraphWrapperImpl.cpp create mode 100644 arborx/test/tstDetailsDistributedTreeImpl.cpp create mode 100644 arborx/test/tstDetailsTreeConstruction.cpp create mode 100644 arborx/test/tstDetailsUtils.cpp create mode 100644 arborx/test/tstDistributedTree.cpp create mode 100644 arborx/test/tstException.cpp create mode 100644 arborx/test/tstHeapOperations.cpp create mode 100644 arborx/test/tstKDOP.cpp create mode 100644 arborx/test/tstKokkosToolsAnnotations.cpp create mode 100644 arborx/test/tstKokkosToolsDistributedAnnotations.cpp create mode 100644 arborx/test/tstPriorityQueueMiscellaneous.cpp create mode 100644 arborx/test/tstQueryTreeCallbacks.cpp create mode 100644 arborx/test/tstQueryTreeComparisonWithBoost.cpp create mode 100644 arborx/test/tstQueryTreeDegenerate.cpp create mode 100644 arborx/test/tstQueryTreeIntersectsKDOP.cpp create mode 100644 arborx/test/tstQueryTreeManufacturedSolution.cpp create mode 100644 arborx/test/tstQueryTreeTraversalPolicy.cpp create mode 100644 arborx/test/tstRay.cpp create mode 100644 arborx/test/tstScopedProfileRegion.cpp create mode 100644 arborx/test/tstSequenceContainers.cpp create mode 100644 arborx/test/utf_main.cpp diff --git a/arborx/.clang-format b/arborx/.clang-format new file mode 100644 index 000000000..a77b42069 --- /dev/null +++ b/arborx/.clang-format @@ -0,0 +1,22 @@ +BasedOnStyle: LLVM +--- +AlwaysBreakTemplateDeclarations: true +BreakBeforeBraces: Allman +BreakConstructorInitializersBeforeComma: true +IncludeBlocks: Regroup +Language: Cpp +IncludeCategories: +# arborx first + - Regex: "ArborX_Config.hpp" + Priority: -1 + - Regex: "ArborX*" + Priority: 1 +# Then Kokkos + - Regex: "Kokkos*" + Priority: 2 +# Then boost + - Regex: "boost*" + Priority: 3 +# Finally the standard library + - Regex: "<[a-z_]+>" + Priority: 10 diff --git a/arborx/.clang-format-ignore b/arborx/.clang-format-ignore new file mode 100644 index 000000000..e69de29bb diff --git a/arborx/.clang-tidy b/arborx/.clang-tidy new file mode 100644 index 000000000..ecd9abb7c --- /dev/null +++ b/arborx/.clang-tidy @@ -0,0 +1,109 @@ +--- +Checks: 'clang-diagnostic-*,clang-analyzer-*,-*,performance-*,-performance-inefficient-string-concatenation,mpi-*,modernize-*,-modernize-pass-by-value,-modernize-use-trailing-return-type,-modernize-avoid-c-arrays,readability-*,-readability-braces-around-statements,-readability-named-parameter,-readability-magic-numbers,-readability-uppercase-literal-suffix' +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +AnalyzeTemporaryDtors: false +FormatStyle: none +CheckOptions: + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-make-shared.IgnoreMacros + value: '1' + - key: modernize-make-shared.IncludeStyle + value: '0' + - key: modernize-make-shared.MakeSmartPtrFunction + value: 'std::make_shared' + - key: modernize-make-shared.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-make-unique.IgnoreMacros + value: '1' + - key: modernize-make-unique.IncludeStyle + value: '0' + - key: modernize-make-unique.MakeSmartPtrFunction + value: 'std::make_unique' + - key: modernize-make-unique.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-raw-string-literal.ReplaceShorterLiterals + value: '0' + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-replace-random-shuffle.IncludeStyle + value: llvm + - key: modernize-use-auto.RemoveStars + value: '0' + - key: modernize-use-default-member-init.IgnoreMacros + value: '1' + - key: modernize-use-default-member-init.UseAssignment + value: '0' + - key: modernize-use-emplace.ContainersWithPushBack + value: '::std::vector;::std::list;::std::deque' + - key: modernize-use-emplace.SmartPointers + value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: modernize-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' + - key: modernize-use-emplace.TupleTypes + value: '::std::pair;::std::tuple' + - key: modernize-use-equals-default.IgnoreMacros + value: '1' + - key: modernize-use-noexcept.ReplacementString + value: '' + - key: modernize-use-noexcept.UseNoexceptFalse + value: '1' + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: modernize-use-transparent-functors.SafeMode + value: '0' + - key: modernize-use-using.IgnoreMacros + value: '1' + - key: performance-faster-string-find.StringLikeClasses + value: 'std::basic_string' + - key: performance-for-range-copy.WarnOnAllAutoCopies + value: '0' + - key: performance-inefficient-vector-operation.VectorLikeClasses + value: '::std::vector' + - key: performance-move-const-arg.CheckTriviallyCopyableMove + value: '0' + - key: performance-move-constructor-init.IncludeStyle + value: llvm + - key: performance-type-promotion-in-math-fn.IncludeStyle + value: llvm + - key: performance-unnecessary-value-param.IncludeStyle + value: llvm + - key: readability-function-size.BranchThreshold + value: '4294967295' + - key: readability-function-size.LineThreshold + value: '4294967295' + - key: readability-function-size.NestingThreshold + value: '4294967295' + - key: readability-function-size.ParameterThreshold + value: '4294967295' + - key: readability-function-size.StatementThreshold + value: '800' + - key: readability-identifier-naming.IgnoreFailedSplit + value: '0' + - key: readability-implicit-bool-conversion.AllowIntegerConditions + value: '0' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: '0' + - key: readability-simplify-boolean-expr.ChainedConditionalAssignment + value: '0' + - key: readability-simplify-boolean-expr.ChainedConditionalReturn + value: '0' + - key: readability-static-accessed-through-instance.NameSpecifierNestingThreshold + value: '3' +... + diff --git a/arborx/.gitattributes b/arborx/.gitattributes new file mode 100644 index 000000000..d05b3a821 --- /dev/null +++ b/arborx/.gitattributes @@ -0,0 +1,7 @@ +docker/ export-ignore +.clang* export-ignore +.git* export-ignore +.jenkins export-ignore +.mailmap export-ignore +.travis.yml export-ignore +*.svg binary diff --git a/arborx/.github/workflows/gitlab-ci.yml b/arborx/.github/workflows/gitlab-ci.yml new file mode 100644 index 000000000..5340536cb --- /dev/null +++ b/arborx/.github/workflows/gitlab-ci.yml @@ -0,0 +1,21 @@ +name: Mirror and run GitLab CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v1 + - name: Trigger GitLab CI + uses: masterleinad/gitlab-mirror-and-ci-action@master + with: + args: "https://code.ornl.gov/ecpcitest/alexa/" + env: + GITLAB_HOSTNAME: "code.ornl.gov" + GITLAB_USERNAME: ${{ secrets.GITLAB_USER }} + GITLAB_PASSWORD: ${{ secrets.GITLAB_PASSWORD }} + GITLAB_PROJECT_ID: "6927" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_JOB_NAME: "CompareResults" + ARTIFACT_NAME: "regression" diff --git a/arborx/.gitignore b/arborx/.gitignore new file mode 100644 index 000000000..48439bce0 --- /dev/null +++ b/arborx/.gitignore @@ -0,0 +1,4 @@ +*~ +*.swp +.#* +/build* diff --git a/arborx/.gitlab-ci.yml b/arborx/.gitlab-ci.yml new file mode 100644 index 000000000..fcf63137a --- /dev/null +++ b/arborx/.gitlab-ci.yml @@ -0,0 +1,147 @@ +# see https://docs.gitlab.com/ce/ci/yaml/README.html for all available options + +variables: + SCHEDULER_PARAMETERS: "-J ArborX_CI -W 1:00 -nnodes 1 -P CSC333 -alloc_flags smt1" + +stages: + - buildDependencies + - buildArborX + - runBenchmarks + - compare + +.LoadModules: + before_script: + - module load gcc/7.4.0 cuda/10.1.243 cmake/3.18.2 git/2.20.1 spectrum-mpi/10.3.1.2-20200121 + +.BuildBoost: + extends: .LoadModules + stage: buildDependencies + script: + - BOOST_VERSION=1.67.0 && + BOOST_VERSION_UNDERSCORE=$(echo "$BOOST_VERSION" | sed -e "s/\./_/g") && + BOOST_URL=https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION}/source && + BOOST_ARCHIVE=boost_${BOOST_VERSION_UNDERSCORE}.tar.bz2 && + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE} && + mkdir -p boost && + tar -xf ${BOOST_ARCHIVE} -C boost --strip-components=1 && + cd boost && + ./bootstrap.sh --prefix=/ccsopen/proj/csc333/boost.install && + ./b2 -j8 variant=release cxxflags=-w install + tags: + - nobatch + +.BuildBenchmark: + extends: .LoadModules + stage: buildDependencies + script: + - git clone https://github.com/google/benchmark.git -b v1.4.1 && + cd benchmark && + git clone https://github.com/google/googletest.git -b release-1.8.1 && + mkdir build && cd build && + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++ -DBENCHMARK_ENABLE_TESTING=OFF -DCMAKE_INSTALL_PREFIX=/ccsopen/proj/csc333/benchmark.install .. && + make && make install + tags: + - nobatch + +.BuildKokkos: + extends: .LoadModules + stage: buildDependencies + script: + - git clone --depth=1 --branch 3.1.00 https://github.com/kokkos/kokkos.git && + cd kokkos && + mkdir build && + cd build && + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ccsopen/proj/csc333/kokkos.install -DCMAKE_CXX_COMPILER=${CI_PROJECT_DIR}/kokkos/bin/nvcc_wrapper -DCMAKE_CXX_EXTENSIONS=OFF -DKokkos_ENABLE_SERIAL=ON -DKokkos_ENABLE_OPENMP=ON -DKokkos_ENABLE_CUDA=ON -DKokkos_ENABLE_CUDA_LAMBDA=ON -DKokkos_ARCH_POWER9=ON -DKokkos_ARCH_VOLTA70=ON .. && + make && + make install + tags: + - nobatch + +BuildArborXBranch: + extends: .LoadModules + stage: buildArborX + script: + - git fetch + - export BRANCH_HASH=`git rev-parse HEAD` + - git merge origin/master + - mkdir build && cd build && + cmake -DCMAKE_PREFIX_PATH="/ccsopen/proj/csc333/kokkos.install;/ccsopen/proj/csc333/benchmark.install;/ccsopen/proj/csc333/boost.install" + -DCMAKE_CXX_COMPILER=/ccsopen/proj/csc333/kokkos.install/bin/nvcc_wrapper + -DARBORX_ENABLE_MPI=ON + -DARBORX_ENABLE_TESTS=OFF + -DARBORX_ENABLE_BENCHMARKS=ON + -DARBORX_PERFORMANCE_TESTING=ON .. && + make ArborX_BoundingVolumeHierarchy.exe + - cp ./benchmarks/bvh_driver/ArborX_BoundingVolumeHierarchy.exe /ccsopen/proj/csc333/ArborX_BoundingVolumeHierarchyBranch${BRANCH_HASH}.exe + - echo export BRANCH_HASH=${BRANCH_HASH} > ${CI_PROJECT_DIR}/branch_hash + tags: + - nobatch + artifacts: + paths: + - ${CI_PROJECT_DIR}/branch_hash + +BuildArborXMaster: + extends: .LoadModules + stage: buildArborX + script: + - git fetch + - export BRANCH_HASH=`git rev-parse HEAD` + - git worktree add -f ${CI_PROJECT_DIR}/arborx-master origin/master + - cd ${CI_PROJECT_DIR}/arborx-master + - mkdir build_master && cd build_master && + cmake -DCMAKE_PREFIX_PATH="/ccsopen/proj/csc333/kokkos.install;/ccsopen/proj/csc333/benchmark.install;/ccsopen/proj/csc333/boost.install" + -DCMAKE_CXX_COMPILER=/ccsopen/proj/csc333/kokkos.install/bin/nvcc_wrapper + -DARBORX_ENABLE_MPI=ON + -DARBORX_ENABLE_TESTS=OFF + -DARBORX_ENABLE_BENCHMARKS=ON + -DARBORX_PERFORMANCE_TESTING=ON .. && + make ArborX_BoundingVolumeHierarchy.exe + - cp ./benchmarks/bvh_driver/ArborX_BoundingVolumeHierarchy.exe /ccsopen/proj/csc333/ArborX_BoundingVolumeHierarchyMaster${BRANCH_HASH}.exe + tags: + - nobatch + +RunBenchmarks: + extends: .LoadModules + stage: runBenchmarks + script: + - source ${CI_PROJECT_DIR}/branch_hash + - export OMP_PROC_BIND=spread + - export OMP_PLACES=threads + - export JSRUN_OPTIONS="-n 1 -a 1 -c 42 -g 1 -r 1 -l CPU-CPU -d packed -b packed:42" + - export BENCHMARK_OPTIONS="--benchmark_repetitions=15 + --exact-spec serial/1000/1000/10/1/0/0/2 + --exact-spec serial/10000/10000/10/1/0/0/2 + --exact-spec serial/100000/100000/10/1/0/0/2 + --exact-spec serial/1000/1000/10/1/0/1/3 + --exact-spec serial/10000/10000/10/1/0/1/3 + --exact-spec serial/100000/100000/10/1/0/1/3 + --exact-spec openmp/1000/1000/10/1/0/0/2 + --exact-spec openmp/10000/10000/10/1/0/0/2 + --exact-spec openmp/100000/100000/10/1/0/0/2 + --exact-spec openmp/1000/1000/10/1/0/1/3 + --exact-spec openmp/10000/10000/10/1/0/1/3 + --exact-spec openmp/100000/100000/10/1/0/1/3 + --exact-spec cuda/10000/10000/10/1/0/0/2 + --exact-spec cuda/100000/100000/10/1/0/0/2 + --exact-spec cuda/1000000/1000000/10/1/0/0/2 + --exact-spec cuda/10000/10000/10/1/0/1/3 + --exact-spec cuda/100000/100000/10/1/0/1/3 + --exact-spec cuda/1000000/1000000/10/1/0/1/3" + - jsrun ${JSRUN_OPTIONS} /ccsopen/proj/csc333/ArborX_BoundingVolumeHierarchyBranch${BRANCH_HASH}.exe ${BENCHMARK_OPTIONS} --benchmark_out_format=json --benchmark_out=/ccsopen/proj/csc333/arborx-branch${BRANCH_HASH}.json + - jsrun ${JSRUN_OPTIONS} /ccsopen/proj/csc333/ArborX_BoundingVolumeHierarchyMaster${BRANCH_HASH}.exe ${BENCHMARK_OPTIONS} --benchmark_out_format=json --benchmark_out=/ccsopen/proj/csc333/arborx-master${BRANCH_HASH}.json + - rm /ccsopen/proj/csc333/ArborX_BoundingVolumeHierarchyBranch${BRANCH_HASH}.exe /ccsopen/proj/csc333/ArborX_BoundingVolumeHierarchyMaster${BRANCH_HASH}.exe + tags: + - batch + +CompareResults: + stage: compare + script: + - module load python/3.6.6-anaconda3-5.3.0 + - source ${CI_PROJECT_DIR}/branch_hash + - /ccsopen/proj/csc333/tools/compare.py benchmarks /ccsopen/proj/csc333/arborx-master${BRANCH_HASH}.json /ccsopen/proj/csc333/arborx-branch${BRANCH_HASH}.json | grep "median" | tee ${CI_PROJECT_DIR}/regression${CI_PIPELINE_ID} + - rm /ccsopen/proj/csc333/arborx-branch${BRANCH_HASH}.json /ccsopen/proj/csc333/arborx-master${BRANCH_HASH}.json + tags: + - nobatch + artifacts: + paths: + - ${CI_PROJECT_DIR}/regression${CI_PIPELINE_ID} diff --git a/arborx/.jenkins b/arborx/.jenkins new file mode 100644 index 000000000..2a0741629 --- /dev/null +++ b/arborx/.jenkins @@ -0,0 +1,462 @@ +pipeline { + triggers { + issueCommentTrigger('.*test this please.*') + } + agent none + + environment { + CCACHE_DIR = '/tmp/ccache' + CCACHE_MAXSIZE = '10G' + ARBORX_DIR = '/opt/arborx' + BENCHMARK_COLOR = 'no' + BOOST_TEST_COLOR_OUTPUT = 'no' + CTEST_OPTIONS = '--timeout 180 --no-compress-output -T Test --test-output-size-passed=65536 --test-output-size-failed=1048576' + OMP_NUM_THREADS = 8 + OMP_PLACES = 'threads' + OMP_PROC_BIND = 'spread' + } + stages { + + stage("Style") { + agent { + docker { + // arbitrary image that has clang-format version 7.0 + image "dalg24/arborx_base:19.04.0-cuda-9.2" + label 'docker' + } + } + steps { + sh './scripts/check_format_cpp.sh' + } + } + + stage('Build') { + parallel { + stage('CUDA-9.2-NVCC-CUDA-AWARE-MPI') { + agent { + dockerfile { + filename "Dockerfile" + dir "docker" + additionalBuildArgs '--build-arg BASE=nvidia/cuda:9.2-devel-ubuntu18.04 --build-arg KOKKOS_OPTIONS="-DCMAKE_CXX_EXTENSIONS=OFF -DKokkos_ENABLE_SERIAL=ON -DKokkos_ENABLE_CUDA=ON -DKokkos_ENABLE_CUDA_LAMBDA=ON -DKokkos_ARCH_VOLTA70=ON" --build-arg CUDA_AWARE_MPI=1' + args '-v /tmp/ccache:/tmp/ccache --env NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES}' + label 'NVIDIA_Tesla_V100-PCIE-32GB && nvidia-docker' + } + } + steps { + sh 'ccache --zero-stats' + sh 'rm -rf build && mkdir -p build' + dir('build') { + sh ''' + cmake \ + -D CMAKE_INSTALL_PREFIX=$ARBORX_DIR \ + -D CMAKE_BUILD_TYPE=Debug \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=$KOKKOS_DIR/bin/nvcc_wrapper \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_CXX_FLAGS="-Wpedantic -Wall -Wextra" \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BOOST_DIR;$BENCHMARK_DIR" \ + -D ARBORX_ENABLE_MPI=ON \ + -D MPIEXEC_PREFLAGS="--allow-run-as-root" \ + -D MPIEXEC_MAX_NUMPROCS=4 \ + -D ARBORX_USE_CUDA_AWARE_MPI=ON \ + -D ARBORX_ENABLE_TESTS=ON \ + -D ARBORX_ENABLE_EXAMPLES=ON \ + -D ARBORX_ENABLE_BENCHMARKS=ON \ + .. + ''' + sh 'make -j8 VERBOSE=1' + sh 'ctest $CTEST_OPTIONS' + } + } + post { + always { + sh 'ccache --show-stats' + xunit reduceLog: false, tools:[CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'build/Testing/**/Test.xml', skipNoTestFiles: false, stopProcessingIfError: true)] + } + success { + sh 'cd build && make install' + sh 'rm -rf test_install && mkdir -p test_install' + dir('test_install') { + sh 'cp -r ../examples .' + sh ''' + cmake \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=$KOKKOS_DIR/bin/nvcc_wrapper \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$ARBORX_DIR" \ + examples \ + ''' + sh 'make VERBOSE=1' + sh 'make test' + } + } + } + } + stage('CUDA-10.2-NVCC') { + agent { + dockerfile { + filename "Dockerfile" + dir "docker" + additionalBuildArgs '--build-arg BASE=nvidia/cuda:10.2-devel --build-arg KOKKOS_OPTIONS="-DCMAKE_CXX_EXTENSIONS=OFF -DKokkos_ENABLE_SERIAL=ON -DKokkos_ENABLE_OPENMP=ON -DKokkos_ENABLE_CUDA=ON -DKokkos_ENABLE_CUDA_LAMBDA=ON -DKokkos_ARCH_VOLTA70=ON"' + args '-v /tmp/ccache:/tmp/ccache --env NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES}' + label 'NVIDIA_Tesla_V100-PCIE-32GB && nvidia-docker' + } + } + steps { + sh 'ccache --zero-stats' + sh 'rm -rf build && mkdir -p build' + dir('build') { + sh ''' + cmake \ + -D CMAKE_INSTALL_PREFIX=$ARBORX_DIR \ + -D CMAKE_BUILD_TYPE=Debug \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=$KOKKOS_DIR/bin/nvcc_wrapper \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_CXX_FLAGS="-Wpedantic -Wall -Wextra" \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BOOST_DIR;$BENCHMARK_DIR" \ + -D ARBORX_ENABLE_MPI=ON \ + -D MPIEXEC_PREFLAGS="--allow-run-as-root" \ + -D MPIEXEC_MAX_NUMPROCS=4 \ + -D ARBORX_ENABLE_TESTS=ON \ + -D ARBORX_ENABLE_EXAMPLES=ON \ + -D ARBORX_ENABLE_BENCHMARKS=ON \ + .. + ''' + sh 'make -j8 VERBOSE=1' + sh 'ctest $CTEST_OPTIONS' + } + } + post { + always { + sh 'ccache --show-stats' + xunit reduceLog: false, tools:[CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'build/Testing/**/Test.xml', skipNoTestFiles: false, stopProcessingIfError: true)] + } + success { + sh 'cd build && make install' + sh 'rm -rf test_install && mkdir -p test_install' + dir('test_install') { + sh 'cp -r ../examples .' + sh ''' + cmake \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=$KOKKOS_DIR/bin/nvcc_wrapper \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$ARBORX_DIR" \ + examples \ + ''' + sh 'make VERBOSE=1' + sh 'make test' + } + } + } + } + stage('CUDA-10.0-Clang') { + agent { + dockerfile { + filename "Dockerfile" + dir "docker" + additionalBuildArgs '--build-arg BASE=nvidia/cuda:10.0-devel-ubuntu18.04 --build-arg KOKKOS_OPTIONS="-DCMAKE_CXX_EXTENSIONS=OFF -DCMAKE_CXX_COMPILER=clang++ -DKokkos_ENABLE_PTHREAD=ON -DKokkos_ENABLE_CUDA=ON -DKokkos_ENABLE_CUDA_LAMBDA=ON -DKokkos_ARCH_VOLTA70=ON"' + args '-v /tmp/ccache:/tmp/ccache --env NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES}' + label 'NVIDIA_Tesla_V100-PCIE-32GB && nvidia-docker' + } + } + steps { + sh 'ccache --zero-stats' + sh 'rm -rf build && mkdir -p build' + dir('build') { + sh ''' + cmake \ + -D CMAKE_INSTALL_PREFIX=$ARBORX_DIR \ + -D CMAKE_BUILD_TYPE=Debug \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=clang++ \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_CXX_FLAGS="-Wpedantic -Wall -Wextra" \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BOOST_DIR;$BENCHMARK_DIR" \ + -D ARBORX_ENABLE_MPI=OFF \ + -D ARBORX_ENABLE_TESTS=ON \ + -D ARBORX_ENABLE_EXAMPLES=ON \ + -D ARBORX_ENABLE_BENCHMARKS=ON \ + .. + ''' + sh 'make -j8 VERBOSE=1' + sh 'ctest $CTEST_OPTIONS' + } + } + post { + always { + sh 'ccache --show-stats' + xunit reduceLog: false, tools:[CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'build/Testing/**/Test.xml', skipNoTestFiles: false, stopProcessingIfError: true)] + } + success { + sh 'cd build && make install' + sh 'rm -rf test_install && mkdir -p test_install' + dir('test_install') { + sh 'cp -r ../examples .' + sh ''' + cmake \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=clang++ \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$ARBORX_DIR" \ + examples \ + ''' + sh 'make VERBOSE=1' + sh 'make test' + } + } + } + } + + stage('Clang') { + agent { + dockerfile { + filename "Dockerfile" + dir "docker" + additionalBuildArgs '--build-arg BASE=ubuntu:18.04 --build-arg KOKKOS_OPTIONS="-DCMAKE_CXX_EXTENSIONS=OFF -DKokkos_ENABLE_OPENMP=ON -DCMAKE_CXX_COMPILER=clang++"' + args '-v /tmp/ccache:/tmp/ccache' + label 'docker' + } + } + steps { + sh 'ccache --zero-stats' + sh 'rm -rf build && mkdir -p build' + dir('build') { + sh ''' + cmake \ + -D CMAKE_INSTALL_PREFIX=$ARBORX_DIR \ + -D CMAKE_BUILD_TYPE=Debug \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=clang++ \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_CXX_FLAGS="-Wpedantic -Wall -Wextra" \ + -D CMAKE_CXX_CLANG_TIDY="$LLVM_DIR/bin/clang-tidy" \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BOOST_DIR;$BENCHMARK_DIR" \ + -D ARBORX_ENABLE_MPI=ON \ + -D MPIEXEC_PREFLAGS="--allow-run-as-root" \ + -D MPIEXEC_MAX_NUMPROCS=4 \ + -D ARBORX_ENABLE_TESTS=ON \ + -D ARBORX_ENABLE_EXAMPLES=ON \ + -D ARBORX_ENABLE_BENCHMARKS=ON \ + .. + ''' + sh 'make -j8 VERBOSE=1' + sh 'ctest $CTEST_OPTIONS' + } + } + post { + always { + sh 'ccache --show-stats' + xunit reduceLog: false, tools:[CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'build/Testing/**/Test.xml', skipNoTestFiles: false, stopProcessingIfError: true)] + } + success { + sh 'cd build && make install' + sh 'rm -rf test_install && mkdir -p test_install' + dir('test_install') { + sh 'cp -r ../examples .' + sh ''' + cmake \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=clang++ \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$ARBORX_DIR" \ + examples \ + ''' + sh 'make VERBOSE=1' + sh 'make test' + } + } + } + } + + stage('PGI') { + agent { + dockerfile { + filename "Dockerfile.pgi" + dir "docker" + args '-v /tmp/ccache:/tmp/ccache' + label 'docker' + } + } + steps { + sh 'ccache --zero-stats' + sh 'rm -rf build && mkdir -p build' + dir('build') { + sh ''' + cmake \ + -D CMAKE_INSTALL_PREFIX=$ARBORX_DIR \ + -D CMAKE_BUILD_TYPE=Debug \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=pgc++ \ + -D CMAKE_CXX_FLAGS="-Minform=inform" \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BOOST_DIR;$BENCHMARK_DIR" \ + -D ARBORX_ENABLE_MPI=ON \ + -D MPIEXEC_PREFLAGS="--allow-run-as-root" \ + -D MPIEXEC_MAX_NUMPROCS=4 \ + -D ARBORX_ENABLE_TESTS=ON \ + -D ARBORX_ENABLE_EXAMPLES=ON \ + -D ARBORX_ENABLE_BENCHMARKS=ON \ + .. + ''' + sh 'make -j8 VERBOSE=1' + sh 'ctest $CTEST_OPTIONS' + } + } + post { + always { + sh 'ccache --show-stats' + xunit reduceLog: false, tools:[CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'build/Testing/**/Test.xml', skipNoTestFiles: false, stopProcessingIfError: true)] + } + success { + sh 'cd build && make install' + sh 'rm -rf test_install && mkdir -p test_install' + dir('test_install') { + sh 'cp -r ../examples .' + sh ''' + cmake \ + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -D CMAKE_CXX_COMPILER=pgc++ \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$ARBORX_DIR" \ + examples \ + ''' + sh 'make VERBOSE=1' + sh 'make test' + } + } + } + } + + stage('HIP-4.2') { + agent { + dockerfile { + filename "Dockerfile.hipcc" + dir "docker" + additionalBuildArgs '--build-arg BASE=rocm/dev-ubuntu-20.04:4.2 --build-arg KOKKOS_ARCH=${KOKKOS_ARCH}' + args '-v /tmp/ccache.kokkos:/tmp/ccache --device=/dev/kfd --device=/dev/dri --security-opt seccomp=unconfined --group-add video --env HIP_VISIBLE_DEVICES=${HIP_VISIBLE_DEVICES}' + label 'rocm-docker && vega' + } + } + steps { + sh 'ccache --zero-stats' + sh 'rm -rf build && mkdir -p build' + dir('build') { + sh ''' + cmake \ + -D CMAKE_INSTALL_PREFIX=$ARBORX_DIR \ + -D CMAKE_BUILD_TYPE=Debug \ + -D CMAKE_CXX_COMPILER=hipcc \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_CXX_FLAGS="-DNDEBUG -Wpedantic -Wall -Wextra" \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BOOST_DIR;$BENCHMARK_DIR" \ + -D ARBORX_ENABLE_MPI=ON \ + -D MPIEXEC_PREFLAGS="--allow-run-as-root" \ + -D MPIEXEC_MAX_NUMPROCS=4 \ + -D ARBORX_ENABLE_TESTS=ON \ + -D ARBORX_ENABLE_EXAMPLES=ON \ + -D ARBORX_ENABLE_BENCHMARKS=ON \ + .. + ''' + sh 'make -j8 VERBOSE=1' + sh 'ctest $CTEST_OPTIONS' + } + } + post { + always { + sh 'ccache --show-stats' + xunit reduceLog: false, tools:[CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'build/Testing/**/Test.xml', skipNoTestFiles: false, stopProcessingIfError: true)] + } + success { + sh 'cd build && make install' + sh 'rm -rf test_install && mkdir -p test_install' + dir('test_install') { + sh 'cp -r ../examples .' + sh ''' + cmake \ + -D CMAKE_CXX_COMPILER=hipcc \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_BUILD_TYPE=RelWithDebInfo \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$ARBORX_DIR" \ + examples \ + ''' + sh 'make VERBOSE=1' + sh 'ctest --output-on-failure' + } + } + } + } + + stage('SYCL') { + agent { + dockerfile { + filename "Dockerfile.sycl" + dir "docker" + args '-v /tmp/ccache.kokkos:/tmp/ccache' + label 'NVIDIA_Tesla_V100-PCIE-32GB && nvidia-docker' + } + } + steps { + sh 'ccache --zero-stats' + sh 'rm -rf build && mkdir -p build' + dir('build') { + sh ''' + cmake \ + -D CMAKE_INSTALL_PREFIX=$ARBORX_DIR \ + -D CMAKE_BUILD_TYPE=Release \ + -D CMAKE_CXX_COMPILER=clang++ \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_CXX_FLAGS="-Wpedantic -Wall -Wextra -Wno-unknown-cuda-version -fsycl -fsycl-targets=nvptx64-nvidia-cuda-sycldevice" \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BOOST_DIR;$BENCHMARK_DIR;$ONE_DPL_DIR" \ + -D ARBORX_ENABLE_MPI=ON \ + -D MPIEXEC_PREFLAGS="--allow-run-as-root" \ + -D MPIEXEC_MAX_NUMPROCS=4 \ + -D ARBORX_ENABLE_TESTS=ON \ + -D ARBORX_ENABLE_EXAMPLES=ON \ + -D ARBORX_ENABLE_BENCHMARKS=ON \ + -D ARBORX_ENABLE_ONEDPL=ON \ + .. + ''' + sh 'make -j8 VERBOSE=1' + sh 'ctest $CTEST_OPTIONS' + } + } + post { + always { + sh 'ccache --show-stats' + xunit reduceLog: false, tools:[CTest(deleteOutputFiles: true, failIfNotNew: true, pattern: 'build/Testing/**/Test.xml', skipNoTestFiles: false, stopProcessingIfError: true)] + } + success { + sh 'cd build && make install' + sh 'rm -rf test_install && mkdir -p test_install' + dir('test_install') { + sh 'cp -r ../examples .' + sh ''' + cmake \ + -D CMAKE_BUILD_TYPE=Release \ + -D CMAKE_CXX_COMPILER=clang++ \ + -D CMAKE_CXX_EXTENSIONS=OFF \ + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$ARBORX_DIR;$ONE_DPL_DIR" \ + examples \ + ''' + sh 'make VERBOSE=1' + sh 'make test' + } + } + } + } + } + } + } + post { + always { + node('docker') { + recordIssues( + enabledForFailure: true, + tools: [cmake(), gcc(), clang(), clangTidy()], + qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]], + filters: [excludeFile('/usr/local/cuda.*'), excludeCategory('#pragma-messages')] + ) + } + } + } +} diff --git a/arborx/.mailmap b/arborx/.mailmap new file mode 100644 index 000000000..802d8ebba --- /dev/null +++ b/arborx/.mailmap @@ -0,0 +1,2 @@ +Damien L-G +Wenjun Ge diff --git a/arborx/CMakeLists.txt b/arborx/CMakeLists.txt new file mode 100644 index 000000000..3181f1f93 --- /dev/null +++ b/arborx/CMakeLists.txt @@ -0,0 +1,171 @@ +cmake_minimum_required(VERSION 3.16) +project(ArborX CXX) + +#find_package(Kokkos 3.1 REQUIRED) +if(Kokkos_ENABLE_CUDA) + kokkos_check(OPTIONS CUDA_LAMBDA) +endif() + +add_library(ArborX INTERFACE) +target_link_libraries(ArborX INTERFACE Kokkos::kokkos) +set_target_properties(ArborX PROPERTIES INTERFACE_COMPILE_FEATURES cxx_std_14) +# As all executables using ArborX depend on it, depending on record_hash allows +# updating hash each time executable is rebuilt, including when called from +# within a subdirectory. +add_dependencies(ArborX record_hash) + +include(CMakeDependentOption) +cmake_dependent_option(ARBORX_ENABLE_ROCTHRUST "Enable rocThrust support" ON "Kokkos_ENABLE_HIP" OFF) +if(Kokkos_ENABLE_HIP AND ARBORX_ENABLE_ROCTHRUST) + # Require at least rocThrust-2.10.5 (that comes with ROCm 3.9) because + # rocPRIM dependency is not set properly in exported configuration for + # earlier versions + find_package(rocthrust 2.10.5 REQUIRED CONFIG) + target_link_libraries(ArborX INTERFACE roc::rocthrust) +endif() + +if(Kokkos_ENABLE_HIP AND NOT ARBORX_ENABLE_ROCTHRUST) + message(WARNING "rocThrust is NOT enabled.\nThis will negatively impact performance on AMD GPUs.") +endif() + +cmake_dependent_option(ARBORX_ENABLE_ONEDPL "Enable oneDPL support" ON "Kokkos_ENABLE_SYCL" OFF) +if(Kokkos_ENABLE_SYCL AND ARBORX_ENABLE_ONEDPL) + find_package(oneDPL REQUIRED) + target_link_libraries(ArborX INTERFACE oneDPL) +endif() + +# Refer to the alias target in examples and benchmarks so they can be built +# against an installed ArborX +add_library(ArborX::ArborX ALIAS ArborX) + +option(ARBORX_ENABLE_MPI "Enable MPI support" OFF) +if(ARBORX_ENABLE_MPI) + find_package(MPI REQUIRED) + target_link_libraries(ArborX INTERFACE MPI::MPI_CXX) +endif() +cmake_dependent_option(ARBORX_USE_CUDA_AWARE_MPI + "Allow using device data in MPI communication" + OFF "ARBORX_ENABLE_MPI" OFF) + +target_include_directories(ArborX INTERFACE + $ + $ + $ + $ + $ +) + +install(TARGETS ArborX + EXPORT ArborXTargets + ARCHIVE LIBRARY PUBLIC_HEADER +) + +install(EXPORT ArborXTargets + NAMESPACE ArborX:: + DESTINATION lib/cmake/ArborX +) + +set(ARBORX_VERSION_STRING "1.1 (dev)") + +# Make sure that the git hash in ArborX_Version.hpp is considered to be always +# out of date, and thus is updated every recompile. +add_custom_target( + record_hash ALL VERBATIM + COMMAND ${CMAKE_COMMAND} + -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} + -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} + -DARBORX_VERSION_STRING=${ARBORX_VERSION_STRING} + -P cmake/SetupVersion.cmake + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +# Also run the record_hash command during configuration stage to have a visible +# ArborX_Version.hpp at all times. +execute_process( + COMMAND ${CMAKE_COMMAND} + -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} + -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} + -DARBORX_VERSION_STRING=${ARBORX_VERSION_STRING} + -P cmake/SetupVersion.cmake + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/ArborX_Config.hpp.in + ${CMAKE_CURRENT_BINARY_DIR}/include/ArborX_Config.hpp) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/ArborXSettings.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/ArborXSettings.cmake + @ONLY) + +include(CMakePackageConfigHelpers) +configure_package_config_file(cmake/ArborXConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/ArborXConfig.cmake + INSTALL_DESTINATION lib/cmake/ArborX +) +write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/ArborXConfigVersion.cmake + VERSION ${ARBORX_VERSION_STRING} + COMPATIBILITY SameMajorVersion +) +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/ArborXConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/ArborXConfigVersion.cmake + ${CMAKE_CURRENT_BINARY_DIR}/ArborXSettings.cmake + DESTINATION lib/cmake/ArborX ) + +if(ARBORX_ENABLE_MPI) + install(DIRECTORY ${PROJECT_SOURCE_DIR}/src/ DESTINATION include + FILES_MATCHING PATTERN "*.hpp") +else() + install(DIRECTORY ${PROJECT_SOURCE_DIR}/src/ DESTINATION include + FILES_MATCHING PATTERN "*.hpp" + PATTERN "*Distribut*" EXCLUDE) +endif() +install(DIRECTORY ${PROJECT_BINARY_DIR}/include/ DESTINATION include + FILES_MATCHING PATTERN "*.hpp") + +if (NOT CMAKE_BUILD_TYPE) + set(default_build_type "RelWithDebInfo") + message(STATUS "Setting build type to '${default_build_type}' as none was specified.") + set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING + "Choose the type of build, options are: Debug, Release, RelWithDebInfo and MinSizeRel." + FORCE) +endif() + +option(ARBORX_ENABLE_TESTS "Enable tests" OFF) +option(ARBORX_ENABLE_EXAMPLES "Enable examples" OFF) +option(ARBORX_ENABLE_BENCHMARKS "Enable benchmarks" OFF) + +if(${ARBORX_ENABLE_TESTS} OR ${ARBORX_ENABLE_EXAMPLES}) + enable_testing() +endif() + +cmake_dependent_option(ARBORX_ENABLE_HEADER_SELF_CONTAINMENT_TESTS "Enable header self-containment unit tests" ON "ARBORX_ENABLE_TESTS" OFF) +if(ARBORX_ENABLE_HEADER_SELF_CONTAINMENT_TESTS) + # Globbing all the header filenames to test for self-containment and presence of header guards + file(GLOB_RECURSE ArborX_HEADERS RELATIVE ${CMAKE_SOURCE_DIR}/src src/*.hpp) + # Findout what headers are using macros defined in ArborX_Config.hpp + file(STRINGS src/ArborX_Config.hpp.in ArborX_DEFINITIONS REGEX "define ARBORX_") + foreach(_definition ${ArborX_DEFINITIONS}) + string(REGEX REPLACE "(#define |#cmakedefine )" "" _macro ${_definition}) + list(APPEND ArborX_MACROS ${_macro}) + endforeach() + foreach(_file ${ArborX_HEADERS}) + foreach(_macro ${ArborX_MACROS}) + file(STRINGS src/${_file} _includes_mpi REGEX "mpi.h") + if(_includes_mpi) + list(APPEND ArborX_HEADERS_MUST_ENABLE_MPI ${_file}) + endif() + file(STRINGS src/${_file} _has_macro REGEX "${_macro}") + if(_has_macro) + list(APPEND ArborX_HEADERS_MUST_INCLUDE_CONFIG_HPP ${_file}) + continue() + endif() + endforeach() + endforeach() +endif() +if(ARBORX_ENABLE_TESTS) + add_subdirectory(test) +endif() +if(ARBORX_ENABLE_EXAMPLES) + add_subdirectory(examples) +endif() +if(ARBORX_ENABLE_BENCHMARKS) + add_subdirectory(benchmarks) +endif() diff --git a/arborx/CONTRIBUTING.md b/arborx/CONTRIBUTING.md new file mode 100644 index 000000000..ab273435c --- /dev/null +++ b/arborx/CONTRIBUTING.md @@ -0,0 +1,12 @@ +Contributing +------------ + +Contributing to ArborX is easy. Just send us a [pull +request](https://help.github.com/articles/using-pull-requests/). When you send +your request, make `master` the destination branch on the [ArborX +repository](https://github.com/ArborX/ArborX). Please allow edits from +maintainers in the pull request. + +Your pull request must pass ArborX's tests. It is also desired that it follows +the coding style used in ArborX (see +[wiki](https://github.com/arborx/ArborX/wiki/CodeStyle)). diff --git a/arborx/LICENSE b/arborx/LICENSE new file mode 100644 index 000000000..7d80a097f --- /dev/null +++ b/arborx/LICENSE @@ -0,0 +1,27 @@ +BSD 3-Clause License + +Copyright 2017-2021 the ArborX authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/arborx/README.md b/arborx/README.md new file mode 100644 index 000000000..79f4432e7 --- /dev/null +++ b/arborx/README.md @@ -0,0 +1,57 @@ + + +ArborX +====== +ArborX is an open-source library designed to provide performance portable +algorithms for geometric search, similarly to nanoflann and Boost Geometry. + +Installation +------------ +The installation instructions can be found [here](https://github.com/arborx/ArborX/wiki/Build). + +Using ArborX +------------ +The interface is described [here](https://github.com/arborx/ArborX/wiki/ArborX%3A%3ABoundingVolumeHierarchy). + +Examples +-------- +Examples can be found in the [examples directory](https://github.com/arborx/ArborX/examples). + +Questions, Bug Reporting, and Issue Tracking +-------------------------------------------- +Questions, bug reporting and issue tracking are provided by GitHub. Please +report all bugs by creating a new issue with the "bug" tag. You can ask +questions by creating a new issue with the "question" tag. + +Contributing +------------ +We encourage you to contribute to ArborX! Please check out the +[guidelines](CONTRIBUTING.md) about how to proceed. + +Citing ArborX +------------- +If you publish work which mentions ArborX, please cite the following paper: + +```BibTeX +@article{arborx2020, + author = {Lebrun-Grandi\'{e}, D. and Prokopenko, A. and Turcksin, B. and Slattery, S. R.}, + title = {{ArborX}: A Performance Portable Geometric Search Library}, + year = {2020}, + issue_date = {December 2020}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, + volume = {47}, + number = {1}, + issn = {0098-3500}, + url = {https://doi.org/10.1145/3412558}, + doi = {10.1145/3412558}, + journal = {ACM Trans. Math. Softw.}, + month = dec, + articleno = {2}, + numpages = {15} +} +``` + +License +------- +ArborX has a [BSD 3-clause open-source license](LICENSE). diff --git a/arborx/benchmarks/CMakeLists.txt b/arborx/benchmarks/CMakeLists.txt new file mode 100644 index 000000000..58796651a --- /dev/null +++ b/arborx/benchmarks/CMakeLists.txt @@ -0,0 +1,7 @@ +find_package(Boost 1.56.0 REQUIRED COMPONENTS program_options) + +add_subdirectory(bvh_driver) +add_subdirectory(execution_space_instances) +if (ARBORX_ENABLE_MPI) + add_subdirectory(distributed_tree_driver) +endif() diff --git a/arborx/benchmarks/bvh_driver/CMakeLists.txt b/arborx/benchmarks/bvh_driver/CMakeLists.txt new file mode 100644 index 000000000..2fcd9a43f --- /dev/null +++ b/arborx/benchmarks/bvh_driver/CMakeLists.txt @@ -0,0 +1,16 @@ +set(POINT_CLOUDS_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/benchmarks/point_clouds) +set(UNIT_TESTS_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/test) + +# We require version 1.4.0 or higher but the format used by Google benchmark is +# wrong and thus, we cannot check the version during the configuration step. +find_package(benchmark REQUIRED) + +find_package(Threads REQUIRED) + +add_executable(ArborX_BoundingVolumeHierarchy.exe bvh_driver.cpp) +target_link_libraries(ArborX_BoundingVolumeHierarchy.exe ArborX::ArborX benchmark::benchmark Boost::program_options Threads::Threads) +target_include_directories(ArborX_BoundingVolumeHierarchy.exe PRIVATE ${POINT_CLOUDS_INCLUDE_DIR} ${UNIT_TESTS_INCLUDE_DIR}) +add_test(NAME ArborX_BoundingVolumeHierarchy_Benchmark COMMAND ./ArborX_BoundingVolumeHierarchy.exe --buffer=0 --benchmark_color=true) +if(ARBORX_PERFORMANCE_TESTING) + target_compile_definitions(ArborX_BoundingVolumeHierarchy.exe PRIVATE ARBORX_PERFORMANCE_TESTING) +endif() diff --git a/arborx/benchmarks/bvh_driver/benchmark_registration.hpp b/arborx/benchmarks/bvh_driver/benchmark_registration.hpp new file mode 100644 index 000000000..eceb38158 --- /dev/null +++ b/arborx/benchmarks/bvh_driver/benchmark_registration.hpp @@ -0,0 +1,441 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef BENCHMARK_REGISTRATION_HPP +#define BENCHMARK_REGISTRATION_HPP + +#include +#include + +#include + +#include +#include // cbrt + +#include +#include + +struct Spec +{ + std::string backends; + int n_values; + int n_queries; + int n_neighbors; + bool sort_predicates; + int buffer_size; + PointCloudType source_point_cloud_type; + PointCloudType target_point_cloud_type; + + Spec() = default; + Spec(std::string const &spec_string) + { + std::istringstream ss(spec_string); + std::string token; + + // clang-format off + getline(ss, token, '/'); backends = token; + getline(ss, token, '/'); n_values = std::stoi(token); + getline(ss, token, '/'); n_queries = std::stoi(token); + getline(ss, token, '/'); n_neighbors = std::stoi(token); + getline(ss, token, '/'); sort_predicates = static_cast(std::stoi(token)); + getline(ss, token, '/'); buffer_size = std::stoi(token); + getline(ss, token, '/'); source_point_cloud_type = static_cast(std::stoi(token)); + getline(ss, token, '/'); target_point_cloud_type = static_cast(std::stoi(token)); + // clang-format on + + if (!(backends == "all" || backends == "serial" || backends == "openmp" || + backends == "threads" || backends == "cuda" || backends == "rtree" || + backends == "hip" || backends == "sycl" || + backends == "openmptarget")) + throw std::runtime_error("Backend " + backends + " invalid!"); + } + + std::string create_label_construction(std::string const &tree_name) const + { + std::string s = std::string("BM_construction<") + tree_name + ">"; + for (auto const &var : + {n_values, static_cast(source_point_cloud_type)}) + s += "/" + std::to_string(var); + return s; + } + + std::string create_label_radius_search(std::string const &tree_name, + std::string const &flavor = "") const + { + std::string s = std::string("BM_radius_") + + (flavor.empty() ? "" : flavor + "_") + "search<" + + tree_name + ">"; + for (auto const &var : + {n_values, n_queries, n_neighbors, static_cast(sort_predicates), + buffer_size, static_cast(source_point_cloud_type), + static_cast(target_point_cloud_type)}) + s += "/" + std::to_string(var); + return s; + }; + + std::string create_label_knn_search(std::string const &tree_name, + std::string const &flavor = "") const + { + std::string s = std::string("BM_knn_") + + (flavor.empty() ? "" : flavor + "_") + "search<" + + tree_name + ">"; + for (auto const &var : + {n_values, n_queries, n_neighbors, static_cast(sort_predicates), + static_cast(source_point_cloud_type), + static_cast(target_point_cloud_type)}) + s += "/" + std::to_string(var); + return s; + }; +}; + +template +Kokkos::View +constructPoints(int n_values, PointCloudType point_cloud_type) +{ + Kokkos::View random_points( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "random_points"), + n_values); + // Generate random points uniformly distributed within a box. The edge + // length of the box chosen such that object density (here objects will be + // boxes 2x2x2 centered around a random point) will remain constant as + // problem size is changed. + auto const a = std::cbrt(n_values); + generatePointCloud(point_cloud_type, a, random_points); + + return random_points; +} + +template +Kokkos::View +makeSpatialQueries(int n_values, int n_queries, int n_neighbors, + PointCloudType target_point_cloud_type) +{ + Kokkos::View random_points( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "random_points"), + n_queries); + auto const a = std::cbrt(n_values); + generatePointCloud(target_point_cloud_type, a, random_points); + + Kokkos::View + queries(Kokkos::view_alloc(Kokkos::WithoutInitializing, "queries"), + n_queries); + // Radius is computed so that the number of results per query for a uniformly + // distributed points in a [-a,a]^3 box is approximately n_neighbors. + // Calculation: n_values*(4/3*M_PI*r^3)/(2a)^3 = n_neighbors + double const r = std::cbrt(static_cast(n_neighbors) * 6. / M_PI); + using ExecutionSpace = typename DeviceType::execution_space; + Kokkos::parallel_for( + "bvh_driver:setup_radius_search_queries", + Kokkos::RangePolicy(0, n_queries), KOKKOS_LAMBDA(int i) { + queries(i) = ArborX::intersects(ArborX::Sphere{random_points(i), r}); + }); + return queries; +} + +template +Kokkos::View *, DeviceType> +makeNearestQueries(int n_values, int n_queries, int n_neighbors, + PointCloudType target_point_cloud_type) +{ + Kokkos::View random_points( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "random_points"), + n_queries); + auto const a = std::cbrt(n_values); + generatePointCloud(target_point_cloud_type, a, random_points); + + Kokkos::View *, DeviceType> queries( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "queries"), n_queries); + using ExecutionSpace = typename DeviceType::execution_space; + Kokkos::parallel_for( + "bvh_driver:setup_knn_search_queries", + Kokkos::RangePolicy(0, n_queries), KOKKOS_LAMBDA(int i) { + queries(i) = + ArborX::nearest(random_points(i), n_neighbors); + }); + return queries; +} + +template +struct QueriesWithIndex +{ + Queries _queries; +}; + +template +struct ArborX::AccessTraits, ArborX::PredicatesTag> +{ + using memory_space = typename Queries::memory_space; + static size_t size(QueriesWithIndex const &q) + { + return q._queries.extent(0); + } + static KOKKOS_FUNCTION auto get(QueriesWithIndex const &q, size_t i) + { + return attach(q._queries(i), (int)i); + } +}; + +template +struct CountCallback +{ + Kokkos::View count_; + + template + KOKKOS_FUNCTION void operator()(Query const &query, int) const + { + auto const i = ArborX::getData(query); + Kokkos::atomic_fetch_add(&count_(i), 1); + } +}; + +template +void BM_construction(benchmark::State &state, Spec const &spec) +{ + using DeviceType = + Kokkos::Device; + + auto const points = + constructPoints(spec.n_values, spec.source_point_cloud_type); + + ExecutionSpace exec_space; + + for (auto _ : state) + { + exec_space.fence(); + auto const start = std::chrono::high_resolution_clock::now(); + + TreeType index(exec_space, points); + + exec_space.fence(); + auto const end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed_seconds = end - start; + state.SetIterationTime(elapsed_seconds.count()); + } + // In Benchmark 1.5.0, it could be rewritten as + // state.counters["rate"] = benchmark::Counter( + // spec.n_values, benchmark::Counter::kIsIterationInvariantRate); + // Benchmark 1.4 does not support kIsIterationInvariantRate, however. + state.counters["rate"] = benchmark::Counter( + spec.n_values * state.iterations(), benchmark::Counter::kIsRate); +} + +template +void BM_radius_search(benchmark::State &state, Spec const &spec) +{ + using DeviceType = + Kokkos::Device; + + ExecutionSpace exec_space; + + TreeType index(exec_space, constructPoints( + spec.n_values, spec.source_point_cloud_type)); + auto const queries = makeSpatialQueries( + spec.n_values, spec.n_queries, spec.n_neighbors, + spec.target_point_cloud_type); + + for (auto _ : state) + { + Kokkos::View offset("offset", 0); + Kokkos::View indices("indices", 0); + + exec_space.fence(); + auto const start = std::chrono::high_resolution_clock::now(); + + ArborX::query(index, exec_space, queries, indices, offset, + ArborX::Experimental::TraversalPolicy() + .setPredicateSorting(spec.sort_predicates) + .setBufferSize(spec.buffer_size)); + + exec_space.fence(); + auto const end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed_seconds = end - start; + state.SetIterationTime(elapsed_seconds.count()); + } + state.counters["rate"] = benchmark::Counter( + spec.n_queries * state.iterations(), benchmark::Counter::kIsRate); +} + +template +void BM_radius_callback_search(benchmark::State &state, Spec const &spec) +{ + using DeviceType = + Kokkos::Device; + + ExecutionSpace exec_space; + + TreeType index( + ExecutionSpace{}, + constructPoints(spec.n_values, spec.source_point_cloud_type)); + auto const queries_no_index = makeSpatialQueries( + spec.n_values, spec.n_queries, spec.n_neighbors, + spec.target_point_cloud_type); + QueriesWithIndex queries{queries_no_index}; + + for (auto _ : state) + { + Kokkos::View num_neigh("Testing::num_neigh", + spec.n_queries); + CountCallback callback{num_neigh}; + + exec_space.fence(); + auto const start = std::chrono::high_resolution_clock::now(); + + index.query(exec_space, queries, callback, + ArborX::Experimental::TraversalPolicy().setPredicateSorting( + spec.sort_predicates)); + + exec_space.fence(); + auto const end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed_seconds = end - start; + state.SetIterationTime(elapsed_seconds.count()); + } + state.counters["rate"] = benchmark::Counter( + spec.n_queries * state.iterations(), benchmark::Counter::kIsRate); +} + +template +void BM_knn_search(benchmark::State &state, Spec const &spec) +{ + using DeviceType = + Kokkos::Device; + + ExecutionSpace exec_space; + + TreeType index(exec_space, constructPoints( + spec.n_values, spec.source_point_cloud_type)); + auto const queries = makeNearestQueries( + spec.n_values, spec.n_queries, spec.n_neighbors, + spec.target_point_cloud_type); + + for (auto _ : state) + { + Kokkos::View offset("offset", 0); + Kokkos::View indices("indices", 0); + + exec_space.fence(); + auto const start = std::chrono::high_resolution_clock::now(); + + ArborX::query(index, exec_space, queries, indices, offset, + ArborX::Experimental::TraversalPolicy().setPredicateSorting( + spec.sort_predicates)); + + exec_space.fence(); + auto const end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed_seconds = end - start; + state.SetIterationTime(elapsed_seconds.count()); + } + state.counters["rate"] = benchmark::Counter( + spec.n_queries * state.iterations(), benchmark::Counter::kIsRate); +} + +template +void BM_knn_callback_search(benchmark::State &state, Spec const &spec) +{ + using DeviceType = + Kokkos::Device; + + ExecutionSpace exec_space; + + TreeType index(exec_space, constructPoints( + spec.n_values, spec.source_point_cloud_type)); + auto const queries_no_index = makeNearestQueries( + spec.n_values, spec.n_queries, spec.n_neighbors, + spec.target_point_cloud_type); + QueriesWithIndex queries{queries_no_index}; + + for (auto _ : state) + { + Kokkos::View num_neigh("Testing::num_neigh", + spec.n_queries); + CountCallback callback{num_neigh}; + + exec_space.fence(); + auto const start = std::chrono::high_resolution_clock::now(); + + index.query(exec_space, queries, callback, + ArborX::Experimental::TraversalPolicy().setPredicateSorting( + spec.sort_predicates)); + + exec_space.fence(); + auto const end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed_seconds = end - start; + state.SetIterationTime(elapsed_seconds.count()); + } + state.counters["rate"] = benchmark::Counter( + spec.n_queries * state.iterations(), benchmark::Counter::kIsRate); +} + +template +void register_benchmark_construction(Spec const &spec, + std::string const &description) +{ + benchmark::RegisterBenchmark( + spec.create_label_construction(description).c_str(), + [=](benchmark::State &state) { + BM_construction(state, spec); + }) + ->UseManualTime() + ->Unit(benchmark::kMicrosecond); +} + +template +void register_benchmark_spatial_query_no_callback( + Spec const &spec, std::string const &description) +{ + benchmark::RegisterBenchmark( + spec.create_label_radius_search(description).c_str(), + [=](benchmark::State &state) { + BM_radius_search(state, spec); + }) + ->UseManualTime() + ->Unit(benchmark::kMicrosecond); +} + +template +void register_benchmark_spatial_query_callback(Spec const &spec, + std::string const &description) +{ + benchmark::RegisterBenchmark( + spec.create_label_radius_search(description, "callback").c_str(), + [=](benchmark::State &state) { + BM_radius_callback_search(state, spec); + }) + ->UseManualTime() + ->Unit(benchmark::kMicrosecond); +} + +template +void register_benchmark_nearest_query_no_callback( + Spec const &spec, std::string const &description) +{ + benchmark::RegisterBenchmark( + spec.create_label_knn_search(description).c_str(), + [=](benchmark::State &state) { + BM_knn_search(state, spec); + }) + ->UseManualTime() + ->Unit(benchmark::kMicrosecond); +} + +template +void register_benchmark_nearest_query_callback(Spec const &spec, + std::string const &description) +{ + benchmark::RegisterBenchmark( + spec.create_label_knn_search(description, "callback").c_str(), + [=](benchmark::State &state) { + BM_knn_callback_search(state, spec); + }) + ->UseManualTime() + ->Unit(benchmark::kMicrosecond); +} + +#endif diff --git a/arborx/benchmarks/bvh_driver/bvh_driver.cpp b/arborx/benchmarks/bvh_driver/bvh_driver.cpp new file mode 100644 index 000000000..ce29a42dc --- /dev/null +++ b/arborx/benchmarks/bvh_driver/bvh_driver.cpp @@ -0,0 +1,295 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include +#include +#include + +#include + +#include + +#include + +#include "benchmark_registration.hpp" + +#ifdef ARBORX_PERFORMANCE_TESTING +#include +#endif + +#include + +template +struct BenchmarkRegistration +{ + BenchmarkRegistration(Spec const &, std::string const &) {} +}; + +template +struct BenchmarkRegistration> +{ + using TreeType = ArborX::BVH; + BenchmarkRegistration(Spec const &spec, std::string const &description) + { + register_benchmark_construction(spec, + description); + register_benchmark_spatial_query_no_callback( + spec, description); + register_benchmark_spatial_query_callback( + spec, description); + register_benchmark_nearest_query_no_callback( + spec, description); + register_benchmark_nearest_query_callback( + spec, description); + } +}; + +template +struct BenchmarkRegistration> +{ + using TreeType = BoostExt::RTree; + BenchmarkRegistration(Spec const &spec, std::string const &description) + { + register_benchmark_construction(spec, + description); + register_benchmark_spatial_query_no_callback( + spec, description); + register_benchmark_nearest_query_no_callback( + spec, description); + } +}; + +template +using BVHBenchmarkRegistration = + BenchmarkRegistration>; +void register_bvh_benchmarks(Spec const &spec) +{ +#ifdef KOKKOS_ENABLE_SERIAL + if (spec.backends == "all" || spec.backends == "serial") + BVHBenchmarkRegistration(spec, "ArborX::BVH"); +#else + if (spec.backends == "serial") + throw std::runtime_error("Serial backend not available!"); +#endif + +#ifdef KOKKOS_ENABLE_OPENMP + if (spec.backends == "all" || spec.backends == "openmp") + BVHBenchmarkRegistration(spec, "ArborX::BVH"); +#else + if (spec.backends == "openmp") + throw std::runtime_error("OpenMP backend not available!"); +#endif + +#ifdef KOKKOS_ENABLE_THREADS + if (spec.backends == "all" || spec.backends == "threads") + BVHBenchmarkRegistration(spec, "ArborX::BVH"); +#else + if (spec.backends == "threads") + throw std::runtime_error("Threads backend not available!"); +#endif + +#ifdef KOKKOS_ENABLE_CUDA + if (spec.backends == "all" || spec.backends == "cuda") + BVHBenchmarkRegistration(spec, "ArborX::BVH"); +#else + if (spec.backends == "cuda") + throw std::runtime_error("CUDA backend not available!"); +#endif + +#ifdef KOKKOS_ENABLE_HIP + if (spec.backends == "all" || spec.backends == "hip") + BVHBenchmarkRegistration(spec, + "ArborX::BVH"); +#else + if (spec.backends == "hip") + throw std::runtime_error("HIP backend not available!"); +#endif + +#ifdef KOKKOS_ENABLE_OPENMPTARGET + if (spec.backends == "all" || spec.backends == "openmptarget") + BVHBenchmarkRegistration( + spec, "ArborX::BVH"); +#else + if (spec.backends == "openmptarget") + throw std::runtime_error("OpenMPTarget backend not available!"); +#endif + +#ifdef KOKKOS_ENABLE_SYCL + if (spec.backends == "all" || spec.backends == "sycl") + BVHBenchmarkRegistration(spec, + "ArborX::BVH"); +#else + if (spec.backends == "sycl") + throw std::runtime_error("SYCL backend not available!"); +#endif +} + +void register_boostrtree_benchmarks(Spec const &spec) +{ +#ifdef KOKKOS_ENABLE_SERIAL + if (spec.backends == "all" || spec.backends == "rtree") + BenchmarkRegistration>( + spec, "BoostRTree"); +#else + std::ignore = spec; +#endif +} + +// NOTE Motivation for this class that stores the argument count and values is +// I could not figure out how to make the parser consume arguments with +// Boost.Program_options +// Benchmark removes its own arguments from the command line arguments. This +// means, that by virtue of returning references to internal data members in +// argc() and argv() function, it will necessarily modify the members. It will +// decrease _argc, and "reduce" _argv data. Hence, we must keep a copy of _argv +// that is not modified from the outside to release memory in the destructor +// correctly. +class CmdLineArgs +{ +private: + int _argc; + std::vector _argv; + std::vector _owner_ptrs; + +public: + CmdLineArgs(std::vector const &args, char const *exe) + : _argc(args.size() + 1) + , _owner_ptrs{new char[std::strlen(exe) + 1]} + { + std::strcpy(_owner_ptrs[0], exe); + _owner_ptrs.reserve(_argc); + for (auto const &s : args) + { + _owner_ptrs.push_back(new char[s.size() + 1]); + std::strcpy(_owner_ptrs.back(), s.c_str()); + } + _argv = _owner_ptrs; + } + + ~CmdLineArgs() + { + for (auto *p : _owner_ptrs) + { + delete[] p; + } + } + + int &argc() { return _argc; } + + char **argv() { return _argv.data(); } +}; + +int main(int argc, char *argv[]) +{ +#ifdef ARBORX_PERFORMANCE_TESTING + MPI_Init(&argc, &argv); +#endif + Kokkos::initialize(argc, argv); + + namespace bpo = boost::program_options; + bpo::options_description desc("Allowed options"); + Spec single_spec; + std::string source_pt_cloud; + std::string target_pt_cloud; + std::vector exact_specs; + // clang-format off + desc.add_options() + ( "help", "produce help message" ) + ( "values", bpo::value(&single_spec.n_values)->default_value(50000), "number of indexable values (source)" ) + ( "queries", bpo::value(&single_spec.n_queries)->default_value(20000), "number of queries (target)" ) + ( "predicate-sort", bpo::value(&single_spec.sort_predicates)->default_value(true), "sort predicates" ) + ( "neighbors", bpo::value(&single_spec.n_neighbors)->default_value(10), "desired number of results per query" ) + ( "buffer", bpo::value(&single_spec.buffer_size)->default_value(0), "size for buffer optimization in radius search" ) + ( "source-point-cloud-type", bpo::value(&source_pt_cloud)->default_value("filled_box"), "shape of the source point cloud" ) + ( "target-point-cloud-type", bpo::value(&target_pt_cloud)->default_value("filled_box"), "shape of the target point cloud" ) + ( "exact-spec", bpo::value>(&exact_specs)->multitoken(), "exact specification (can be specified multiple times for batch)" ) + ; + // clang-format on + bpo::variables_map vm; + bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) + .options(desc) + .allow_unregistered() + .run(); + bpo::store(parsed, vm); + CmdLineArgs pass_further{ + bpo::collect_unrecognized(parsed.options, bpo::include_positional), + argv[0]}; + bpo::notify(vm); + + std::cout << "ArborX version: " << ArborX::version() << std::endl; + std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; + + if (vm.count("help") > 0) + { + // Full list of options consists of Kokkos + Boost.Program_options + + // Google Benchmark and we still need to call benchmark::Initialize() to + // get those printed to the standard output. + std::cout << desc << "\n"; + int ac = 2; + char *av[] = {(char *)"ignored", (char *)"--help"}; + // benchmark::Initialize() calls exit(0) when `--help` so register + // Kokkos::finalize() to be called on normal program termination. + std::atexit(Kokkos::finalize); + benchmark::Initialize(&ac, av); + return 1; + } + + if (vm.count("exact-spec") > 0) + { + for (std::string option : + {"values", "queries", "predicate-sort", "neighbors", "buffer", + "source-point-cloud-type", "target-point-cloud-type"}) + { + if (!vm[option].defaulted()) + { + std::cout << "Conflicting options: 'exact-spec' and '" << option + << "', exiting..." << std::endl; + return EXIT_FAILURE; + } + } + } + + benchmark::Initialize(&pass_further.argc(), pass_further.argv()); + // Throw if some of the arguments have not been recognized. + std::ignore = + bpo::command_line_parser(pass_further.argc(), pass_further.argv()) + .options(bpo::options_description("")) + .run(); + + std::vector specs; + specs.reserve(exact_specs.size()); + for (auto const &spec_string : exact_specs) + specs.emplace_back(spec_string); + + if (vm.count("exact-spec") == 0) + { + single_spec.backends = "all"; + single_spec.source_point_cloud_type = to_point_cloud_enum(source_pt_cloud); + single_spec.target_point_cloud_type = to_point_cloud_enum(target_pt_cloud); + specs.push_back(single_spec); + } + + for (auto const &spec : specs) + { + register_bvh_benchmarks(spec); + register_boostrtree_benchmarks(spec); + } + + benchmark::RunSpecifiedBenchmarks(); + + Kokkos::finalize(); +#ifdef ARBORX_PERFORMANCE_TESTING + MPI_Finalize(); +#endif + + return EXIT_SUCCESS; +} diff --git a/arborx/benchmarks/distributed_tree_driver/CMakeLists.txt b/arborx/benchmarks/distributed_tree_driver/CMakeLists.txt new file mode 100644 index 000000000..f0ccc61b8 --- /dev/null +++ b/arborx/benchmarks/distributed_tree_driver/CMakeLists.txt @@ -0,0 +1,4 @@ +add_executable(ArborX_DistributedTree_Benchmark.exe distributed_tree_driver.cpp) +target_link_libraries(ArborX_DistributedTree_Benchmark.exe ArborX::ArborX Boost::program_options) +target_include_directories(ArborX_DistributedTree_Benchmark.exe PRIVATE ${CMAKE_SOURCE_DIR}/benchmarks/point_clouds) +add_test(NAME ArborX_DistributedTree_Benchmark COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} ${MPIEXEC_MAX_NUMPROCS} ${MPIEXEC_PREFLAGS} ./ArborX_DistributedTree_Benchmark.exe ${MPIEXEC_POSTFLAGS}) diff --git a/arborx/benchmarks/distributed_tree_driver/distributed_tree_driver.cpp b/arborx/benchmarks/distributed_tree_driver/distributed_tree_driver.cpp new file mode 100644 index 000000000..150b88100 --- /dev/null +++ b/arborx/benchmarks/distributed_tree_driver/distributed_tree_driver.cpp @@ -0,0 +1,637 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include +#include + +#include + +#include + +#include +#include +#include // sqrt, cbrt +#include +#include +#include +#include +#include + +#include + +struct HelpPrinted +{ +}; + +// The TimeMonitor class can be used to measure for a series of events, i.e. it +// represents a set of timers of type Timer. It is a poor man's drop-in +// replacement for Teuchos::TimeMonitor +class TimeMonitor +{ + using container_type = std::vector>; + using entry_reference_type = container_type::reference; + container_type _data; + +public: + class Timer + { + entry_reference_type _entry; + bool _started; + std::chrono::high_resolution_clock::time_point _tick; + + public: + Timer(entry_reference_type ref) + : _entry{ref} + , _started{false} + { + } + void start() + { + assert(!_started); + _tick = std::chrono::high_resolution_clock::now(); + _started = true; + } + void stop() + { + assert(_started); + std::chrono::duration duration = + std::chrono::high_resolution_clock::now() - _tick; + // NOTE I have put much thought into whether we should use the + // operator+= and keep track of how many times the timer was + // restarted. To be honest I have not even looked was the original + // TimeMonitor behavior is :) + _entry.second = duration.count(); + _started = false; + } + }; + // NOTE Original code had the pointer semantics. Can change in the future. + // The smart pointer is a distraction. The main problem here is that the + // reference stored by the timer is invalidated if the time monitor gets + // out of scope. + std::unique_ptr getNewTimer(std::string name) + { + // FIXME Consider searching whether there already is an entry with the + // same name. + _data.emplace_back(std::move(name), 0.); + return std::make_unique(_data.back()); + } + + void summarize(MPI_Comm comm, std::ostream &os = std::cout) + { + int comm_size; + MPI_Comm_size(comm, &comm_size); + int comm_rank; + MPI_Comm_rank(comm, &comm_rank); + int n_timers = _data.size(); + + os << std::left << std::scientific; + + // Initialize with length of "Timer Name" + std::string const timer_name = "Timer Name"; + std::size_t const max_section_length = std::accumulate( + _data.begin(), _data.end(), timer_name.size(), + [](std::size_t current_max, entry_reference_type section) { + return std::max(current_max, section.first.size()); + }); + + if (comm_size == 1) + { + std::string const header_without_timer_name = " | GlobalTime"; + std::stringstream dummy_string_stream; + dummy_string_stream << std::setprecision(os.precision()) + << std::scientific << " | " << 1.; + int const header_width = + max_section_length + std::max(header_without_timer_name.size(), + dummy_string_stream.str().size()); + + os << std::string(header_width, '=') << "\n\n"; + os << "TimeMonitor results over 1 processor\n\n"; + os << std::setw(max_section_length) << timer_name + << header_without_timer_name << '\n'; + os << std::string(header_width, '-') << '\n'; + for (int i = 0; i < n_timers; ++i) + { + os << std::setw(max_section_length) << _data[i].first << " | " + << _data[i].second << '\n'; + } + os << std::string(header_width, '=') << '\n'; + return; + } + std::vector all_entries(comm_size * n_timers); + std::transform( + _data.begin(), _data.end(), all_entries.begin() + comm_rank * n_timers, + [](std::pair const &x) { return x.second; }); + // FIXME No guarantee that all processors have the same timers! + MPI_Allgather(MPI_IN_PLACE, 0, MPI_DATATYPE_NULL, all_entries.data(), + n_timers, MPI_DOUBLE, comm); + std::string const header_without_timer_name = + " | MinOverProcs | MeanOverProcs | MaxOverProcs"; + if (comm_rank == 0) + { + os << std::string(max_section_length + header_without_timer_name.size(), + '=') + << "\n\n"; + os << "TimeMonitor results over " << comm_size << " processors\n"; + os << std::setw(max_section_length) << timer_name + << header_without_timer_name << '\n'; + os << std::string(max_section_length + header_without_timer_name.size(), + '-') + << '\n'; + } + std::vector tmp(comm_size); + for (int i = 0; i < n_timers; ++i) + { + for (int j = 0; j < comm_size; ++j) + { + tmp[j] = all_entries[j * n_timers + i]; + } + auto min = *std::min_element(tmp.begin(), tmp.end()); + auto max = *std::max_element(tmp.begin(), tmp.end()); + auto mean = std::accumulate(tmp.begin(), tmp.end(), 0.) / comm_size; + if (comm_rank == 0) + { + os << std::setw(max_section_length) << _data[i].first << " | " << min + << " | " << mean << " | " << max << '\n'; + } + } + if (comm_rank == 0) + { + os << std::string(max_section_length + header_without_timer_name.size(), + '=') + << '\n'; + } + } +}; + +template +struct NearestNeighborsSearches +{ + Kokkos::View points; + int k; +}; +template +struct RadiusSearches +{ + Kokkos::View points; + double radius; +}; + +template +struct ArborX::AccessTraits, ArborX::PredicatesTag> +{ + using memory_space = typename DeviceType::memory_space; + static KOKKOS_FUNCTION std::size_t + size(RadiusSearches const &pred) + { + return pred.points.extent(0); + } + static KOKKOS_FUNCTION auto get(RadiusSearches const &pred, + std::size_t i) + { + return ArborX::intersects(ArborX::Sphere{pred.points(i), pred.radius}); + } +}; + +template +struct ArborX::AccessTraits, + ArborX::PredicatesTag> +{ + using memory_space = typename DeviceType::memory_space; + static KOKKOS_FUNCTION std::size_t + size(NearestNeighborsSearches const &pred) + { + return pred.points.extent(0); + } + static KOKKOS_FUNCTION auto + get(NearestNeighborsSearches const &pred, std::size_t i) + { + return ArborX::nearest(pred.points(i), pred.k); + } +}; + +namespace bpo = boost::program_options; + +template +int main_(std::vector const &args, const MPI_Comm comm) +{ + TimeMonitor time_monitor; + + using DeviceType = typename NO::device_type; + using ExecutionSpace = typename DeviceType::execution_space; + using MemorySpace = typename DeviceType::memory_space; + + int n_values; + int n_queries; + int n_neighbors; + double shift; + int partition_dim; + bool perform_knn_search = true; + bool perform_radius_search = true; + bool shift_queries = false; + + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "produce help message" ) + ( "values", bpo::value(&n_values)->default_value(20000), "Number of indexable values (source) per MPI rank." ) + ( "queries", bpo::value(&n_queries)->default_value(5000), "Number of queries (target) per MPI rank." ) + ( "neighbors", bpo::value(&n_neighbors)->default_value(10), "Desired number of results per query." ) + ( "shift", bpo::value(&shift)->default_value(1.), "Shift of the point clouds. '0' means the clouds are built " + "at the same place, while '1' places the clouds next to each" + "other. Negative values and values larger than one " + "mean that the clouds are separated." ) + ( "partition_dim", bpo::value(&partition_dim)->default_value(3), "Number of dimension used by the partitioning of the global " + "point cloud. 1 -> local clouds are aligned on a line, 2 -> " + "local clouds form a board, 3 -> local clouds form a box." ) + ( "do-not-perform-knn-search", "skip kNN search" ) + ( "do-not-perform-radius-search", "skip radius search" ) + ( "shift-queries" , "By default, points are reused for the queries. Enabling this option shrinks the local box queries are created " + "in to a third of its size and moves it to the center of the global box. The result is a huge imbalance for the " + "number of queries that need to be processed by each processor.") + ; + // clang-format on + bpo::variables_map vm; + bpo::store(bpo::command_line_parser(args).options(desc).run(), vm); + bpo::notify(vm); + + int comm_rank; + MPI_Comm_rank(comm, &comm_rank); + int comm_size; + MPI_Comm_size(comm, &comm_size); + + if (vm.count("help") > 0) + { + if (comm_rank == 0) + std::cout << desc << '\n'; + throw HelpPrinted(); + } + + if (vm.count("do-not-perform-knn-search") > 0) + perform_knn_search = false; + if (vm.count("do-not-perform-radius-search") > 0) + perform_radius_search = false; + if (vm.count("shift-queries") > 0) + shift_queries = true; + + if (comm_rank == 0) + { + std::cout << std::boolalpha; + std::cout << "\nRunning with arguments:\n" + << "perform knn search : " << perform_knn_search << '\n' + << "perform radius search : " << perform_radius_search << '\n' + << "#points/MPI process : " << n_values << '\n' + << "#queries/MPI process : " << n_queries << '\n' + << "size of shift : " << shift << '\n' + << "dimension : " << partition_dim << '\n' + << "shift-queries : " << shift_queries << '\n' + << '\n'; + } + + Kokkos::View random_values( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "Testing::values"), + n_values); + Kokkos::View random_queries( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "Testing::queries"), + n_queries); + { + double a = 0.; + double offset_x = 0.; + double offset_y = 0.; + double offset_z = 0.; + int i_max = 0; + // Change the geometry of the problem. In 1D, all the point clouds are + // aligned on a line. In 2D, the point clouds create a board and in 3D, + // they create a box. + switch (partition_dim) + { + case 1: + { + i_max = comm_size; + offset_x = 2 * shift * comm_rank; + a = n_values; + + break; + } + case 2: + { + i_max = std::ceil(std::sqrt(comm_size)); + int i = comm_rank % i_max; + int j = comm_rank / i_max; + offset_x = 2 * shift * i; + offset_y = 2 * shift * j; + a = std::sqrt(n_values); + + break; + } + case 3: + { + i_max = std::ceil(std::cbrt(comm_size)); + int j_max = i_max; + int i = comm_rank % i_max; + int j = (comm_rank / i_max) % j_max; + int k = comm_rank / (i_max * j_max); + offset_x = 2 * shift * i; + offset_y = 2 * shift * j; + offset_z = 2 * shift * k; + a = std::cbrt(n_values); + + break; + } + default: + { + throw std::runtime_error("partition_dim should be 1, 2, or 3"); + } + } + + // Generate random points uniformly distributed within a box. + std::uniform_real_distribution distribution(-1., 1.); + std::default_random_engine generator; + auto random = [&distribution, &generator]() { + return distribution(generator); + }; + + // The boxes in which the points are placed have side length two, centered + // around offset_[xyz] and scaled by a. + Kokkos::View random_points( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "Testing::points"), + std::max(n_values, n_queries)); + auto random_points_host = Kokkos::create_mirror_view(random_points); + for (int i = 0; i < random_points.extent_int(0); ++i) + random_points_host(i) = { + {a * (offset_x + random()), + a * (offset_y + random()) * (partition_dim > 1), + a * (offset_z + random()) * (partition_dim > 2)}}; + Kokkos::deep_copy(random_points, random_points_host); + + Kokkos::deep_copy( + random_values, + Kokkos::subview(random_points, Kokkos::pair(0, n_values))); + + if (!shift_queries) + { + // By default, random points are "reused" between building the tree and + // performing queries. + Kokkos::deep_copy( + random_queries, + Kokkos::subview(random_points, Kokkos::pair(0, n_queries))); + } + else + { + // For the queries, we shrink the global box by a factor three, and + // move it by a third of the global size towards the global center. + auto random_queries_host = Kokkos::create_mirror_view(random_queries); + + int const max_offset = 2 * shift * i_max; + for (int i = 0; i < n_queries; ++i) + random_queries_host(i) = { + {a * ((offset_x + random()) / 3 + max_offset / 3), + a * ((offset_y + random()) / 3 + max_offset / 3) * + (partition_dim > 1), + a * ((offset_z + random()) / 3 + max_offset / 3) * + (partition_dim > 2)}}; + Kokkos::deep_copy(random_queries, random_queries_host); + } + } + + Kokkos::View bounding_boxes( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "Testing::bounding_boxes"), + n_values); + Kokkos::parallel_for("bvh_driver:construct_bounding_boxes", + Kokkos::RangePolicy(0, n_values), + KOKKOS_LAMBDA(int i) { + double const x = random_values(i)[0]; + double const y = random_values(i)[1]; + double const z = random_values(i)[2]; + bounding_boxes(i) = {{{x - 1., y - 1., z - 1.}}, + {{x + 1., y + 1., z + 1.}}}; + }); + + auto construction = time_monitor.getNewTimer("construction"); + MPI_Barrier(comm); + construction->start(); + ArborX::DistributedTree distributed_tree(comm, ExecutionSpace{}, + bounding_boxes); + construction->stop(); + + std::ostream &os = std::cout; + if (comm_rank == 0) + os << "construction done\n"; + + using PairIndexRank = Kokkos::pair; + + if (perform_knn_search) + { + Kokkos::View offsets("Testing::offsets", 0); + Kokkos::View values("Testing::values", 0); + + auto knn = time_monitor.getNewTimer("knn"); + MPI_Barrier(comm); + knn->start(); + distributed_tree.query( + ExecutionSpace{}, + NearestNeighborsSearches{random_queries, n_neighbors}, + values, offsets); + knn->stop(); + + if (comm_rank == 0) + os << "knn done\n"; + } + + if (perform_radius_search) + { + // Radius is computed so that the number of results per query for a + // uniformly distributed primitives in a [-a,a]^d box is approximately + // n_neighbors. The primivites are boxes and not points. Thus, the radius + // we would have chosen for the case of point primitives has to be adjusted + // to account for box-box interaction. The radius is decreased by an + // average of the lengths of a half-edge and a half-diagonal to account for + // that (approximately). An exact calculation would require computing + // an integral. + double r = 0.; + switch (partition_dim) + { + case 1: + // Derivation (first term): n_values*(2*r)/(2a) = n_neighbors + r = static_cast(n_neighbors) - 1.; + break; + case 2: + // Derivation (first term): n_values*(M_PI*r^2)/(2a)^2 = n_neighbors + r = std::sqrt(static_cast(n_neighbors) * 4. / M_PI) - + (1. + std::sqrt(2.)) / 2; + break; + case 3: + // Derivation (first term): n_values*(4/3*M_PI*r^3)/(2a)^3 = n_neighbors + r = std::cbrt(static_cast(n_neighbors) * 6. / M_PI) - + (1. + std::cbrt(3.)) / 2; + break; + } + + Kokkos::View offsets("Testing::offsets", 0); + Kokkos::View values("Testing::values", 0); + + auto radius = time_monitor.getNewTimer("radius"); + MPI_Barrier(comm); + radius->start(); + distributed_tree.query(ExecutionSpace{}, + RadiusSearches{random_queries, r}, + values, offsets); + radius->stop(); + + if (comm_rank == 0) + os << "radius done\n"; + } + time_monitor.summarize(comm); + + return 0; +} + +int main(int argc, char *argv[]) +{ + MPI_Init(&argc, &argv); + + MPI_Comm const comm = MPI_COMM_WORLD; + int comm_rank; + MPI_Comm_rank(comm, &comm_rank); + if (comm_rank == 0) + { + std::cout << "ArborX version: " << ArborX::version() << std::endl; + std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; + } + + // Strip "--help" and "--kokkos-help" from the flags passed to Kokkos if we + // are not on MPI rank 0 to prevent Kokkos from printing the help message + // multiply. + if (comm_rank != 0) + { + auto *help_it = std::find_if(argv, argv + argc, [](std::string const &x) { + return x == "--help" || x == "--kokkos-help"; + }); + if (help_it != argv + argc) + { + std::swap(*help_it, *(argv + argc - 1)); + --argc; + } + } + Kokkos::initialize(argc, argv); + + bool success = true; + + try + { + std::string node; + // NOTE Lame trick to get a valid default value +#if defined(KOKKOS_ENABLE_HIP) + node = "hip"; +#elif defined(KOKKOS_ENABLE_CUDA) + node = "cuda"; +#elif defined(KOKKOS_ENABLE_OPENMP) + node = "openmp"; +#elif defined(KOKKOS_ENABLE_THREADS) + node = "threads"; +#elif defined(KOKKOS_ENABLE_SERIAL) + node = "serial"; +#endif + bpo::options_description desc("Parallel setting:"); + desc.add_options()("node", bpo::value(&node), + "node type (serial | openmp | threads | cuda)"); + bpo::variables_map vm; + bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) + .options(desc) + .allow_unregistered() + .run(); + bpo::store(parsed, vm); + std::vector pass_further = + bpo::collect_unrecognized(parsed.options, bpo::include_positional); + bpo::notify(vm); + + if (comm_rank == 0 && std::find_if(pass_further.begin(), pass_further.end(), + [](std::string const &x) { + return x == "--help"; + }) != pass_further.end()) + { + std::cout << desc << '\n'; + } + + if (node != "serial" && node != "openmp" && node != "cuda" && + node != "threads" && node != "hip") + throw std::runtime_error("Unrecognized node type: \"" + node + "\""); + + if (node == "serial") + { +#ifdef KOKKOS_ENABLE_SERIAL + using Node = Kokkos::Serial; + main_(pass_further, comm); +#else + throw std::runtime_error("Serial node type is disabled"); +#endif + } + if (node == "openmp") + { +#ifdef KOKKOS_ENABLE_OPENMP + using Node = Kokkos::OpenMP; + main_(pass_further, comm); +#else + throw std::runtime_error("OpenMP node type is disabled"); +#endif + } + if (node == "threads") + { +#ifdef KOKKOS_ENABLE_THREADS + using Node = Kokkos::Threads; + main_(pass_further, comm); +#else + throw std::runtime_error("Threads node type is disabled"); +#endif + } + if (node == "cuda") + { +#ifdef KOKKOS_ENABLE_CUDA + using Node = Kokkos::Device; + main_(pass_further, comm); +#else + throw std::runtime_error("CUDA node type is disabled"); +#endif + } + if (node == "hip") + { +#ifdef KOKKOS_ENABLE_HIP + using Node = Kokkos::Device; + main_(pass_further, comm); +#else + throw std::runtime_error("HIP node type is disabled"); +#endif + } + } + catch (HelpPrinted const &) + { + // Do nothing, it was a successful run. Just clean up things below. + } + catch (std::exception const &e) + { + std::cerr << "processor " << comm_rank + << " caught a std::exception: " << e.what() << '\n'; + success = false; + } + catch (...) + { + std::cerr << "processor " << comm_rank + << " caught some kind of exception\n"; + success = false; + } + + Kokkos::finalize(); + + MPI_Finalize(); + + return (success ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/arborx/benchmarks/execution_space_instances/CMakeLists.txt b/arborx/benchmarks/execution_space_instances/CMakeLists.txt new file mode 100644 index 000000000..2034009d7 --- /dev/null +++ b/arborx/benchmarks/execution_space_instances/CMakeLists.txt @@ -0,0 +1,4 @@ +add_executable(ArborX_ExecutionSpaces_Benchmark.exe execution_space_instances_driver.cpp) +target_link_libraries(ArborX_ExecutionSpaces_Benchmark.exe ArborX::ArborX Boost::program_options) +target_include_directories(ArborX_ExecutionSpaces_Benchmark.exe PRIVATE ${CMAKE_SOURCE_DIR}/benchmarks/point_clouds) +add_test(NAME ArborX_ExecutionSpaces_Benchmark COMMAND ./ArborX_ExecutionSpaces_Benchmark.exe) diff --git a/arborx/benchmarks/execution_space_instances/execution_space_instances_driver.cpp b/arborx/benchmarks/execution_space_instances/execution_space_instances_driver.cpp new file mode 100644 index 000000000..99ede0097 --- /dev/null +++ b/arborx/benchmarks/execution_space_instances/execution_space_instances_driver.cpp @@ -0,0 +1,225 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include +#include + +#include + +#include + +#include +#include +#include // sqrt, cbrt +#include +#include +#include +#include +#include + +template +class InstanceManager +{ +public: + InstanceManager(int const n_instances) { _instances.resize(n_instances); } + const std::vector &get_instances() const + { + return _instances; + } + +private: + std::vector _instances; +}; + +#ifdef KOKKOS_ENABLE_CUDA +template <> +class InstanceManager +{ +public: + InstanceManager(int const n_instances) + { + _streams.resize(n_instances); + _instances.reserve(n_instances); + for (int i = 0; i < n_instances; ++i) + { + cudaStreamCreate(&_streams[i]); + _instances.emplace_back(_streams[i]); + } + } + + ~InstanceManager() + { + for (unsigned int i = 0; i < _streams.size(); ++i) + cudaStreamDestroy(_streams[i]); + } + + const std::vector &get_instances() const { return _instances; } + +private: + std::vector _instances; + std::vector _streams; +}; +#endif + +template +struct CountCallback +{ + Kokkos::View _counts; + + template + KOKKOS_FUNCTION void operator()(Query const &query, int) const + { + auto const i = ArborX::getData(query); + Kokkos::atomic_fetch_add(&_counts(i), 1); + } +}; + +int main(int argc, char *argv[]) +{ + using ExecutionSpace = Kokkos::DefaultExecutionSpace; + using MemorySpace = typename ExecutionSpace::memory_space; + + Kokkos::ScopeGuard guard(argc, argv); + + std::cout << "ArborX version: " << ArborX::version() << std::endl; + std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; + + namespace bpo = boost::program_options; + + int num_exec_spaces; + int num_primitives; + int num_problems; + int num_predicates; + + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "produce help message" ) + ( "num-spaces", bpo::value(&num_exec_spaces)->default_value(1), "Number of execution space instances." ) + ( "num-problems", bpo::value(&num_problems)->default_value(1), "Number of subproblems." ) + ( "values", bpo::value(&num_primitives)->default_value(20000), "Number of indexable values (source) per subproblem." ) + ( "queries", bpo::value(&num_predicates)->default_value(5000), "Number of queries (target) per subproblem." ) + ; + // clang-format on + bpo::variables_map vm; + bpo::store(bpo::command_line_parser(argc, argv).options(desc).run(), vm); + bpo::notify(vm); + + float const r = 0.1f; + float const shift = 2.f; + + if (vm.count("help") > 0) + { + std::cout << desc << '\n'; + return 1; + } + + std::cout << std::boolalpha; + std::cout << "\nRunning with arguments:" + << "\nnumber of execution space instances : " << num_exec_spaces + << "\nnumber of problems : " << num_problems + << "\n#points/problem : " << num_primitives + << "\n#queries/problem : " << num_predicates + << '\n'; + + // Generate random points uniformly distributed within a box. + std::uniform_real_distribution distribution(-1., 1.); + std::default_random_engine generator; + auto random = [&distribution, &generator]() { + return distribution(generator); + }; + + Kokkos::View primitives( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "primitives"), + num_primitives * num_problems); + Kokkos::View + predicates(Kokkos::view_alloc(Kokkos::WithoutInitializing, "predicates"), + num_predicates * num_problems); + for (int p = 0; p < num_problems; ++p) + { + // Points are placed in a box [offset_x-1, offset_x+1] x [-1, 1] x [-1, 1] + float offset_x = p * shift; + + Kokkos::View points( + "points", std::max(num_primitives, num_predicates)); + auto points_host = Kokkos::create_mirror_view(points); + for (int i = 0; i < (int)points.extent(0); ++i) + points_host(i) = {offset_x + random(), random(), random()}; + Kokkos::deep_copy(points, points_host); + + Kokkos::deep_copy( + Kokkos::subview( + primitives, + Kokkos::make_pair(p * num_primitives, (p + 1) * num_primitives)), + Kokkos::subview(points, Kokkos::make_pair(0, num_primitives))); + Kokkos::parallel_for( + "construct_predicates", + Kokkos::RangePolicy( + ExecutionSpace{}, p * num_predicates, (p + 1) * num_predicates), + KOKKOS_LAMBDA(int i) { + predicates(i) = + ArborX::attach(ArborX::intersects(ArborX::Sphere{ + points(i - p * num_predicates), r}), + i); + }); + } + + InstanceManager instance_manager(num_exec_spaces); + auto const &instances = instance_manager.get_instances(); + + std::vector> trees; + for (int p = 0; p < num_problems; ++p) + { + auto const &exec_space = instances[p % num_exec_spaces]; + + trees.emplace_back( + exec_space, Kokkos::subview(primitives, Kokkos::pair( + p * num_primitives, + (p + 1) * num_primitives))); + } + ArborX::BVH tree(instances[0], primitives); + + Kokkos::View counts("counts", + num_predicates * num_problems); + + Kokkos::fence(); + Kokkos::Timer query_time; + query_time.reset(); + for (int p = 0; p < num_problems; ++p) + { + auto const &exec_space = instances[p % num_exec_spaces]; + + trees[p].query( + exec_space, + Kokkos::subview(predicates, + Kokkos::pair(p * num_predicates, + (p + 1) * num_predicates)), + CountCallback{counts}, + ArborX::Experimental::TraversalPolicy().setPredicateSorting(false)); + // ArborX::Experimental::TraversalPolicy().setPredicateSorting(true)); + } + Kokkos::fence(); + std::cout << "Time multiple(s): " << query_time.seconds() << '\n'; + + Kokkos::deep_copy(counts, 0); + query_time.reset(); + + tree.query( + instances[0], predicates, CountCallback{counts}, + ArborX::Experimental::TraversalPolicy().setPredicateSorting(false)); + + Kokkos::fence(); + std::cout << "Time single(s): " << query_time.seconds() << '\n'; + + return EXIT_SUCCESS; +} diff --git a/arborx/benchmarks/point_clouds/point_clouds.hpp b/arborx/benchmarks/point_clouds/point_clouds.hpp new file mode 100644 index 000000000..340f25f72 --- /dev/null +++ b/arborx/benchmarks/point_clouds/point_clouds.hpp @@ -0,0 +1,216 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_POINT_CLOUDS_HPP +#define ARBORX_POINT_CLOUDS_HPP + +#include +#include +#include + +#include + +#include +#include + +enum class PointCloudType +{ + filled_box, + hollow_box, + filled_sphere, + hollow_sphere +}; + +inline PointCloudType to_point_cloud_enum(std::string const &str) +{ + if (str == "filled_box") + return PointCloudType::filled_box; + if (str == "hollow_box") + return PointCloudType::hollow_box; + if (str == "filled_sphere") + return PointCloudType::filled_sphere; + if (str == "hollow_sphere") + return PointCloudType::hollow_sphere; + throw std::runtime_error(str + + " doesn't correspond to any known PointCloudType!"); +} + +template +void filledBoxCloud( + double const half_edge, + Kokkos::View random_points) +{ + static_assert( + KokkosExt::is_accessible_from_host::value, + "The View should be accessible on the Host"); + std::uniform_real_distribution distribution(-half_edge, half_edge); + std::default_random_engine generator; + auto random = [&distribution, &generator]() { + return distribution(generator); + }; + unsigned int const n = random_points.extent(0); + for (unsigned int i = 0; i < n; ++i) + random_points(i) = {{random(), random(), random()}}; +} + +template +void hollowBoxCloud( + double const half_edge, + Kokkos::View random_points) +{ + static_assert( + KokkosExt::is_accessible_from_host::value, + "The View should be accessible on the Host"); + std::uniform_real_distribution distribution(-half_edge, half_edge); + std::default_random_engine generator; + auto random = [&distribution, &generator]() { + return distribution(generator); + }; + unsigned int const n = random_points.extent(0); + for (unsigned int i = 0; i < n; ++i) + { + unsigned int face = i % 6; + switch (face) + { + case 0: + { + random_points(i) = {{-half_edge, random(), random()}}; + + break; + } + case 1: + { + random_points(i) = {{half_edge, random(), random()}}; + + break; + } + case 2: + { + random_points(i) = {{random(), -half_edge, random()}}; + + break; + } + case 3: + { + random_points(i) = {{random(), half_edge, random()}}; + + break; + } + case 4: + { + random_points(i) = {{random(), random(), -half_edge}}; + + break; + } + case 5: + { + random_points(i) = {{random(), random(), half_edge}}; + + break; + } + default: + { + throw std::runtime_error("Your compiler is broken"); + } + } + } +} + +template +void filledSphereCloud( + double const radius, + Kokkos::View random_points) +{ + static_assert( + KokkosExt::is_accessible_from_host::value, + "The View should be accessible on the Host"); + std::default_random_engine generator; + + std::uniform_real_distribution distribution(-radius, radius); + auto random = [&distribution, &generator]() { + return distribution(generator); + }; + + unsigned int const n = random_points.extent(0); + for (unsigned int i = 0; i < n; ++i) + { + bool point_accepted = false; + while (!point_accepted) + { + double const x = random(); + double const y = random(); + double const z = random(); + + // Only accept points that are in the sphere + if (std::sqrt(x * x + y * y + z * z) <= radius) + { + random_points(i) = {{x, y, z}}; + point_accepted = true; + } + } + } +} + +template +void hollowSphereCloud( + double const radius, + Kokkos::View random_points) +{ + static_assert( + KokkosExt::is_accessible_from_host::value, + "The View should be accessible on the Host"); + std::default_random_engine generator; + + std::uniform_real_distribution distribution(-1., 1.); + auto random = [&distribution, &generator]() { + return distribution(generator); + }; + + unsigned int const n = random_points.extent(0); + for (unsigned int i = 0; i < n; ++i) + { + double const x = random(); + double const y = random(); + double const z = random(); + double const norm = std::sqrt(x * x + y * y + z * z); + + random_points(i) = { + {radius * x / norm, radius * y / norm, radius * z / norm}}; + } +} + +template +void generatePointCloud(PointCloudType const point_cloud_type, + double const length, + Kokkos::View random_points) +{ + auto random_points_host = Kokkos::create_mirror_view(random_points); + switch (point_cloud_type) + { + case PointCloudType::filled_box: + filledBoxCloud(length, random_points_host); + break; + case PointCloudType::hollow_box: + hollowBoxCloud(length, random_points_host); + break; + case PointCloudType::filled_sphere: + filledSphereCloud(length, random_points_host); + break; + case PointCloudType::hollow_sphere: + hollowSphereCloud(length, random_points_host); + break; + default: + throw ArborX::SearchException("not implemented"); + } + Kokkos::deep_copy(random_points, random_points_host); +} + +#endif diff --git a/arborx/cmake/ArborXConfig.cmake.in b/arborx/cmake/ArborXConfig.cmake.in new file mode 100644 index 000000000..3702eecdc --- /dev/null +++ b/arborx/cmake/ArborXConfig.cmake.in @@ -0,0 +1,26 @@ +@PACKAGE_INIT@ + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_MODULE_PATH}) + +include(CMakeFindDependencyMacro) + +find_package(Kokkos QUIET) +if(NOT Kokkos_FOUND) + # If Kokkos was not found, try to use Kokkos used when building ArborX + set(Kokkos_DIR @Kokkos_DIR@) + find_dependency(Kokkos) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/ArborXSettings.cmake") +if(Kokkos_ENABLE_HIP AND ARBORX_ENABLE_ROCTHRUST) + find_dependency(rocthrust) +endif() +if(Kokkos_ENABLE_SYCL AND ARBORX_ENABLE_ONEDPL) + find_dependency(oneDPL) +endif() +if(ARBORX_ENABLE_MPI) + find_dependency(MPI) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/ArborXTargets.cmake") +check_required_components(ArborX) diff --git a/arborx/cmake/ArborXSettings.cmake.in b/arborx/cmake/ArborXSettings.cmake.in new file mode 100644 index 000000000..6a706358f --- /dev/null +++ b/arborx/cmake/ArborXSettings.cmake.in @@ -0,0 +1,4 @@ +set(ARBORX_ENABLE_ROCTHRUST @ARBORX_ENABLE_ROCTHRUST@) +set(ARBORX_ENABLE_ONEDPL @ARBORX_ENABLE_ONEDPL@) +set(ARBORX_ENABLE_MPI @ARBORX_ENABLE_MPI@) +set(ARBORX_USE_CUDA_AWARE_MPI @ARBORX_USE_CUDA_AWARE_MPI@) diff --git a/arborx/cmake/SetupVersion.cmake b/arborx/cmake/SetupVersion.cmake new file mode 100644 index 000000000..248813a46 --- /dev/null +++ b/arborx/cmake/SetupVersion.cmake @@ -0,0 +1,30 @@ +##/**************************************************************************** +## * Copyright (c) 2017-2021 by the ArborX authors * +## * All rights reserved. * +## * * +## * This file is part of the ArborX library. ArborX is * +## * distributed under a BSD 3-clause license. For the licensing terms see * +## * the LICENSE file in the top-level directory. * +## * * +## * SPDX-License-Identifier: BSD-3-Clause * +## ****************************************************************************/ + +# This CMake script makes sure to store the current git hash in +# ArborX_Version.hpp each time we recompile. It is important that this script +# is called by through a target created by add_custom_target so that it is +# always considered to be out-of-date. + +SET(ARBORX_GIT_COMMIT_HASH "No hash available") + +IF(EXISTS ${SOURCE_DIR}/.git) + FIND_PACKAGE(Git QUIET) + IF(GIT_FOUND) + EXECUTE_PROCESS( + COMMAND ${GIT_EXECUTABLE} log --pretty=format:%h -n 1 + OUTPUT_VARIABLE ARBORX_GIT_COMMIT_HASH) + ENDIF() +ENDIF() +MESSAGE(STATUS "ArborX hash = '${ARBORX_GIT_COMMIT_HASH}'") + +configure_file(${SOURCE_DIR}/src/ArborX_Version.hpp.in + ${BINARY_DIR}/include/ArborX_Version.hpp) diff --git a/arborx/docker/.env b/arborx/docker/.env new file mode 100644 index 000000000..c4798d0a6 --- /dev/null +++ b/arborx/docker/.env @@ -0,0 +1,2 @@ +CONTAINER_ARBORX_DIR=/scratch +CONTAINER_CCACHE_DIR=/tmp/ccache diff --git a/arborx/docker/Dockerfile b/arborx/docker/Dockerfile new file mode 100644 index 000000000..9861c9ff6 --- /dev/null +++ b/arborx/docker/Dockerfile @@ -0,0 +1,145 @@ +ARG BASE=nvidia/cuda:10.1-devel +FROM $BASE + +ARG NPROCS=4 + +RUN apt-get update && apt-get install -y \ + build-essential \ + bc \ + curl \ + git \ + wget \ + jq \ + vim \ + lcov \ + ccache \ + gdb \ + ninja-build \ + libbz2-dev \ + libicu-dev \ + python-dev \ + autotools-dev \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN KEYDUMP_URL=https://cloud.cees.ornl.gov/download && \ + KEYDUMP_FILE=keydump && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE} && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE}.sig && \ + gpg --import ${KEYDUMP_FILE} && \ + gpg --verify ${KEYDUMP_FILE}.sig ${KEYDUMP_FILE} && \ + rm ${KEYDUMP_FILE}* + +# Install CMake +ENV CMAKE_DIR=/opt/cmake +RUN CMAKE_VERSION=3.16.9 && \ + CMAKE_KEY=2D2CEF1034921684 && \ + CMAKE_URL=https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION} && \ + CMAKE_SCRIPT=cmake-${CMAKE_VERSION}-Linux-x86_64.sh && \ + CMAKE_SHA256=cmake-${CMAKE_VERSION}-SHA-256.txt && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256} && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256}.asc && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SCRIPT} && \ + gpg --verify ${CMAKE_SHA256}.asc ${CMAKE_SHA256} && \ + grep ${CMAKE_SCRIPT} ${CMAKE_SHA256} | sha256sum --check && \ + mkdir -p ${CMAKE_DIR} && \ + sh ${CMAKE_SCRIPT} --skip-license --prefix=${CMAKE_DIR} && \ + rm cmake* +ENV PATH=${CMAKE_DIR}/bin:$PATH + +# Install Clang/LLVM +ENV LLVM_DIR=/opt/llvm +RUN LLVM_VERSION=10.0.0 && \ + LLVM_KEY="86419D8A 345AD05D" && \ + LLVM_URL=https://github.com/llvm/llvm-project/releases/download/llvmorg-${LLVM_VERSION}/clang+llvm-${LLVM_VERSION}-x86_64-linux-gnu-ubuntu-18.04.tar.xz && \ + LLVM_ARCHIVE=llvm-${LLVM_VERSION}.tar.xz && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${LLVM_URL} --output-document=${LLVM_ARCHIVE} && \ + wget --quiet ${LLVM_URL}.sig --output-document=${LLVM_ARCHIVE}.sig && \ + gpg --verify ${LLVM_ARCHIVE}.sig ${LLVM_ARCHIVE} && \ + mkdir -p ${LLVM_DIR} && \ + tar -xvf ${LLVM_ARCHIVE} -C ${LLVM_DIR} --strip-components=1 && \ + echo "${LLVM_DIR}/lib" > /etc/ld.so.conf.d/llvm.conf && ldconfig && \ + rm -rf ${SCRATCH_DIR} +ENV PATH=${LLVM_DIR}/bin:$PATH + +# Install OpenMPI +ARG CUDA_AWARE_MPI +ENV OPENMPI_DIR=/opt/openmpi +RUN OPENMPI_VERSION=4.0.3 && \ + OPENMPI_VERSION_SHORT=$(echo "$OPENMPI_VERSION" | cut -d. -f1,2) && \ + OPENMPI_SHA1=d958454e32da2c86dd32b7d557cf9a401f0a08d3 && \ + OPENMPI_URL=https://download.open-mpi.org/release/open-mpi/v${OPENMPI_VERSION_SHORT}/openmpi-${OPENMPI_VERSION}.tar.bz2 && \ + OPENMPI_ARCHIVE=openmpi-${OPENMPI_VERSION}.tar.bz2 && \ + CUDA_OPTIONS=${CUDA_AWARE_MPI:+--with-cuda} && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${OPENMPI_URL} --output-document=${OPENMPI_ARCHIVE} && \ + echo "${OPENMPI_SHA1} ${OPENMPI_ARCHIVE}" | sha1sum -c && \ + mkdir -p openmpi && \ + tar -xf ${OPENMPI_ARCHIVE} -C openmpi --strip-components=1 && \ + mkdir -p build && cd build && \ + ../openmpi/configure --prefix=${OPENMPI_DIR} ${CUDA_OPTIONS} CFLAGS=-w && \ + make -j${NPROCS} install && \ + rm -rf ${SCRATCH_DIR} +ENV PATH=${OPENMPI_DIR}/bin:$PATH + +# Install Boost +ENV BOOST_DIR=/opt/boost +RUN BOOST_VERSION=1.75.0 && \ + BOOST_VERSION_UNDERSCORE=$(echo "$BOOST_VERSION" | sed -e "s/\./_/g") && \ + BOOST_KEY=379CE192D401AB61 && \ + BOOST_URL=https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION}/source && \ + BOOST_ARCHIVE=boost_${BOOST_VERSION_UNDERSCORE}.tar.bz2 && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.asc && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json.asc && \ + gpg --verify ${BOOST_ARCHIVE}.json.asc ${BOOST_ARCHIVE}.json && \ + gpg --verify ${BOOST_ARCHIVE}.asc ${BOOST_ARCHIVE} && \ + cat ${BOOST_ARCHIVE}.json | jq -r '. | .sha256 + " " + .file' | sha256sum --check && \ + mkdir -p boost && \ + tar -xf ${BOOST_ARCHIVE} -C boost --strip-components=1 && \ + cd boost && \ + CXXFLAGS="-w" ./bootstrap.sh \ + --prefix=${BOOST_DIR} \ + && \ + echo "using mpi ;" >> project-config.jam && \ + ./b2 -j${NPROCS} \ + hardcode-dll-paths=true dll-path=${BOOST_DIR}/lib \ + link=shared \ + variant=release \ + cxxflags=-w \ + install \ + && \ + rm -rf ${SCRATCH_DIR} + +# Install Google Benchmark support library +ENV BENCHMARK_DIR=/opt/benchmark +RUN SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + git clone https://github.com/google/benchmark.git -b v1.4.1 && \ + cd benchmark && \ + mkdir build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${BENCHMARK_DIR} -D BENCHMARK_ENABLE_TESTING=OFF .. && \ + make -j${NPROCS} && make install && \ + rm -rf ${SCRATCH_DIR} + +# Workaround for Kokkos to find libcudart +ENV LD_LIBRARY_PATH=/usr/local/cuda/targets/x86_64-linux/lib:${LD_LIBRARY_PATH} + +# Install Kokkos +ARG KOKKOS_VERSION=3.1.00 +ARG KOKKOS_OPTIONS="-DKokkos_ENABLE_SERIAL=ON -DKokkos_ENABLE_OPENMP=ON -DKokkos_ENABLE_CUDA=ON -DKokkos_ENABLE_CUDA_LAMBDA=ON" +ENV KOKKOS_DIR=/opt/kokkos +RUN KOKKOS_URL=https://github.com/kokkos/kokkos/archive/${KOKKOS_VERSION}.tar.gz && \ + KOKKOS_ARCHIVE=kokkos-${KOKKOS_HASH}.tar.gz && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${KOKKOS_URL} --output-document=${KOKKOS_ARCHIVE} && \ + mkdir -p kokkos && \ + tar -xf ${KOKKOS_ARCHIVE} -C kokkos --strip-components=1 && \ + cd kokkos && \ + mkdir -p build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${KOKKOS_DIR} -D CMAKE_CXX_COMPILER=/scratch/kokkos/bin/nvcc_wrapper ${KOKKOS_OPTIONS} .. && \ + make -j${NPROCS} install && \ + rm -rf ${SCRATCH_DIR} diff --git a/arborx/docker/Dockerfile.hipcc b/arborx/docker/Dockerfile.hipcc new file mode 100644 index 000000000..2d22a448c --- /dev/null +++ b/arborx/docker/Dockerfile.hipcc @@ -0,0 +1,110 @@ +ARG BASE=rocm/dev-ubuntu-20.04:4.2 +FROM $BASE + +ARG NPROCS=4 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -yq \ + build-essential \ + bc \ + curl \ + git \ + kmod \ + wget \ + jq \ + vim \ + gdb \ + ccache \ + libbz2-dev \ + libicu-dev \ + python-dev \ + autotools-dev \ + libopenmpi-dev \ + rocthrust \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +ENV PATH=/opt/rocm/bin:$PATH + +RUN KEYDUMP_URL=https://cloud.cees.ornl.gov/download && \ + KEYDUMP_FILE=keydump && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE} && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE}.sig && \ + gpg --import ${KEYDUMP_FILE} && \ + gpg --verify ${KEYDUMP_FILE}.sig ${KEYDUMP_FILE} && \ + rm ${KEYDUMP_FILE}* + +# Install CMake +ENV CMAKE_DIR=/opt/cmake +RUN CMAKE_VERSION=3.18.5 && \ + CMAKE_KEY=2D2CEF1034921684 && \ + CMAKE_URL=https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION} && \ + CMAKE_SCRIPT=cmake-${CMAKE_VERSION}-Linux-x86_64.sh && \ + CMAKE_SHA256=cmake-${CMAKE_VERSION}-SHA-256.txt && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256} && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256}.asc && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SCRIPT} && \ + gpg --verify ${CMAKE_SHA256}.asc ${CMAKE_SHA256} && \ + grep ${CMAKE_SCRIPT} ${CMAKE_SHA256} | sha256sum --check && \ + mkdir -p ${CMAKE_DIR} && \ + sh ${CMAKE_SCRIPT} --skip-license --prefix=${CMAKE_DIR} && \ + rm cmake* +ENV PATH=${CMAKE_DIR}/bin:$PATH + +# Install Boost +ENV BOOST_DIR=/opt/boost +RUN BOOST_VERSION=1.72.0 && \ + BOOST_VERSION_UNDERSCORE=$(echo "$BOOST_VERSION" | sed -e "s/\./_/g") && \ + BOOST_KEY=379CE192D401AB61 && \ + BOOST_URL=https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION}/source && \ + BOOST_ARCHIVE=boost_${BOOST_VERSION_UNDERSCORE}.tar.bz2 && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.asc && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json.asc && \ + gpg --verify ${BOOST_ARCHIVE}.json.asc ${BOOST_ARCHIVE}.json && \ + gpg --verify ${BOOST_ARCHIVE}.asc ${BOOST_ARCHIVE} && \ + cat ${BOOST_ARCHIVE}.json | jq -r '. | .sha256 + " " + .file' | sha256sum --check && \ + mkdir -p boost && \ + tar -xf ${BOOST_ARCHIVE} -C boost --strip-components=1 && \ + cd boost && \ + CXXFLAGS="-w" ./bootstrap.sh \ + --prefix=${BOOST_DIR} \ + && \ + ./b2 -j${NPROCS} \ + hardcode-dll-paths=true dll-path=${BOOST_DIR}/lib \ + link=shared \ + variant=release \ + cxxflags=-w \ + install \ + && \ + rm -rf ${SCRATCH_DIR} + +# Install Google Benchmark support library +ENV BENCHMARK_DIR=/opt/benchmark +RUN SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + git clone https://github.com/google/benchmark.git -b v1.5.0 && \ + cd benchmark && \ + mkdir build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${BENCHMARK_DIR} -D BENCHMARK_ENABLE_TESTING=OFF .. && \ + make -j${NPROCS} && make install && \ + rm -rf ${SCRATCH_DIR} + +# Install Kokkos +ARG KOKKOS_VERSION=3.3.01 +ARG KOKKOS_ARCH=Kokkos_ARCH_VEGA906 +ARG KOKKOS_OPTIONS="-DKokkos_ENABLE_HIP=ON -D${KOKKOS_ARCH}=ON" +ENV KOKKOS_DIR=/opt/kokkos +RUN KOKKOS_URL=https://github.com/kokkos/kokkos/archive/${KOKKOS_VERSION}.tar.gz && \ + KOKKOS_ARCHIVE=kokkos-${KOKKOS_HASH}.tar.gz && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${KOKKOS_URL} --output-document=${KOKKOS_ARCHIVE} && \ + mkdir -p kokkos && \ + tar -xf ${KOKKOS_ARCHIVE} -C kokkos --strip-components=1 && \ + cd kokkos && \ + mkdir -p build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${KOKKOS_DIR} -D CMAKE_CXX_COMPILER=hipcc ${KOKKOS_OPTIONS} .. && \ + make -j${NPROCS} install && \ + rm -rf ${SCRATCH_DIR} diff --git a/arborx/docker/Dockerfile.pgi b/arborx/docker/Dockerfile.pgi new file mode 100644 index 000000000..4b3025489 --- /dev/null +++ b/arborx/docker/Dockerfile.pgi @@ -0,0 +1,122 @@ +# Installing PGI needs to be done interactively to accept the license. The +# installation of the compiler and MPI is done using the default options. +# During the installation the PGI compiler, multiple versions of the CUDA +# toolkit are gettin installed. This unnecessarily increases the size of +# the image, so before the image is created all but the latest CUDA toolkit +# are removed from the container. The image is built using the ubuntu:18.04 image and +# the compiler is downloaded from https://www.pgroup.com/products/community.html +FROM rombur/pgi:19.04 + +ARG NPROCS=8 +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + build-essential \ + bc \ + curl \ + git \ + wget \ + jq \ + ccache \ + ninja-build \ + libbz2-dev \ + libicu-dev \ + autotools-dev \ + environment-modules \ + tcl \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN KEYDUMP_URL=https://cloud.cees.ornl.gov/download && \ + KEYDUMP_FILE=keydump && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE} && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE}.sig && \ + gpg --import ${KEYDUMP_FILE} && \ + gpg --verify ${KEYDUMP_FILE}.sig ${KEYDUMP_FILE} && \ + rm ${KEYDUMP_FILE}* + +# Install CMake +ENV CMAKE_DIR=/opt/cmake +RUN CMAKE_VERSION=3.16.9 && \ + CMAKE_KEY=2D2CEF1034921684 && \ + CMAKE_URL=https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION} && \ + CMAKE_SCRIPT=cmake-${CMAKE_VERSION}-Linux-x86_64.sh && \ + CMAKE_SHA256=cmake-${CMAKE_VERSION}-SHA-256.txt && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256} && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256}.asc && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SCRIPT} && \ + gpg --verify ${CMAKE_SHA256}.asc ${CMAKE_SHA256} && \ + grep ${CMAKE_SCRIPT} ${CMAKE_SHA256} | sha256sum --check && \ + mkdir -p ${CMAKE_DIR} && \ + sh ${CMAKE_SCRIPT} --skip-license --prefix=${CMAKE_DIR} && \ + rm cmake* +ENV PATH=${CMAKE_DIR}/bin:$PATH + +# Install Boost +ENV BOOST_DIR=/opt/boost +RUN BOOST_VERSION=1.71.0 && \ + BOOST_VERSION_UNDERSCORE=$(echo "$BOOST_VERSION" | sed -e "s/\./_/g") && \ + BOOST_KEY=379CE192D401AB61 && \ + BOOST_URL=https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION}/source && \ + BOOST_ARCHIVE=boost_${BOOST_VERSION_UNDERSCORE}.tar.bz2 && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.asc && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json.asc && \ + gpg --verify ${BOOST_ARCHIVE}.json.asc ${BOOST_ARCHIVE}.json && \ + gpg --verify ${BOOST_ARCHIVE}.asc ${BOOST_ARCHIVE} && \ + cat ${BOOST_ARCHIVE}.json | jq -r '. | .sha256 + " " + .file' | sha256sum --check && \ + mkdir -p boost && \ + tar -xf ${BOOST_ARCHIVE} -C boost --strip-components=1 && \ + cd boost && \ + CXXFLAGS="-w" ./bootstrap.sh \ + --prefix=${BOOST_DIR} \ + && \ + echo "using mpi ;" >> project-config.jam && \ + ./b2 -j${NPROCS} \ + hardcode-dll-paths=true dll-path=${BOOST_DIR}/lib \ + link=shared \ + variant=release \ + cxxflags=-w \ + install \ + && \ + rm -rf ${SCRATCH_DIR} + +# Install Google Benchmark support library +ENV BENCHMARK_DIR=/opt/benchmark +RUN SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + git clone https://github.com/google/benchmark.git -b v1.4.1 && \ + cd benchmark && \ + mkdir build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${BENCHMARK_DIR} -D BENCHMARK_ENABLE_TESTING=OFF .. && \ + make -j${NPROCS} && make install && \ + rm -rf ${SCRATCH_DIR} + +# Install Kokkos +ARG KOKKOS_VERSION=3.1.00 +ARG KOKKOS_OPTIONS="-DKokkos_ENABLE_SERIAL=ON -DKokkos_ENABLE_OPENMP=ON -DKokkos_ENABLE_DEPRECATED_CODE=OFF -DKokkos_ENABLE_PROFILING=OFF -DKokkos_ENABLE_LIBDL=OFF" +ENV KOKKOS_DIR=/opt/kokkos +RUN KOKKOS_URL=https://github.com/kokkos/kokkos/archive/${KOKKOS_VERSION}.tar.gz && \ + KOKKOS_ARCHIVE=kokkos-${KOKKOS_HASH}.tar.gz && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${KOKKOS_URL} --output-document=${KOKKOS_ARCHIVE} && \ + mkdir -p kokkos && \ + tar -xf ${KOKKOS_ARCHIVE} -C kokkos --strip-components=1 && \ + cd kokkos && \ + mkdir -p build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${KOKKOS_DIR} -D CMAKE_CXX_COMPILER=/opt/pgi/linux86-64-llvm/19.4/bin/pgc++ ${KOKKOS_OPTIONS} .. && \ + make -j${NPROCS} install && \ + rm -rf ${SCRATCH_DIR} + +# Set the different paths +ENV PGI_VERSION=19.4 +ENV LD_LIBRARY_PATH=/opt/pgi/linux86-64-llvm/2019/mpi/openmpi-3.1.3/lib:/opt/pgi/linux86-64-llvm/${PGI_VERSION}/lib:$LD_LIBRARY_PATH +ENV CXX=/opt/pgi/linux86-64-llvm/${PGI_VERSION}/bin/pgc++ +ENV F90=/opt/pgi/linux86-64-llvm/${PGI_VERSION}/bin/pgf90 +ENV PGI=/opt/pgi +ENV FC=/opt/pgi/linux86-64-llvm/${PGI_VERSION}/bin/pgfortran +ENV F77=/opt/pgi/linux86-64-llvm/${PGI_VERSION}/bin/pgf77 +ENV PATH=/opt/pgi/linux86-64-llvm/2019/mpi/openmpi-3.1.3/bin:/opt/pgi/linux86-64-llvm/${PGI_VERSION}/bin:/trash:/opt/cmake/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ENV CC=/opt/pgi/linux86-64-llvm/${PGI_VERSION}/bin/pgcc diff --git a/arborx/docker/Dockerfile.sycl b/arborx/docker/Dockerfile.sycl new file mode 100644 index 000000000..76668985f --- /dev/null +++ b/arborx/docker/Dockerfile.sycl @@ -0,0 +1,132 @@ +ARG BASE=nvidia/cuda:10.2-devel +FROM $BASE + +ARG NPROCS=4 + +RUN apt-get update && apt-get install -y \ + bc \ + wget \ + ccache \ + ninja-build \ + python3 \ + git \ + vim \ + jq \ + libopenmpi-dev \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN KEYDUMP_URL=https://cloud.cees.ornl.gov/download && \ + KEYDUMP_FILE=keydump && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE} && \ + wget --quiet ${KEYDUMP_URL}/${KEYDUMP_FILE}.sig && \ + gpg --import ${KEYDUMP_FILE} && \ + gpg --verify ${KEYDUMP_FILE}.sig ${KEYDUMP_FILE} && \ + rm ${KEYDUMP_FILE}* + +ARG CMAKE_VERSION=3.19.0 +ENV CMAKE_DIR=/opt/cmake +RUN CMAKE_KEY=2D2CEF1034921684 && \ + CMAKE_URL=https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION} && \ + CMAKE_SCRIPT=cmake-${CMAKE_VERSION}-Linux-x86_64.sh && \ + CMAKE_SHA256=cmake-${CMAKE_VERSION}-SHA-256.txt && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256} && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SHA256}.asc && \ + wget --quiet ${CMAKE_URL}/${CMAKE_SCRIPT} && \ + gpg --verify ${CMAKE_SHA256}.asc ${CMAKE_SHA256} && \ + grep ${CMAKE_SCRIPT} ${CMAKE_SHA256} | sha256sum --check && \ + mkdir -p ${CMAKE_DIR} && \ + sh ${CMAKE_SCRIPT} --skip-license --prefix=${CMAKE_DIR} && \ + rm cmake* +ENV PATH=${CMAKE_DIR}/bin:$PATH + +ENV SYCL_DIR=/opt/sycl +RUN SYCL_VERSION=20210311 && \ + SYCL_URL=https://github.com/intel/llvm/archive/sycl-nightly && \ + SYCL_ARCHIVE=${SYCL_VERSION}.tar.gz && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${SYCL_URL}/${SYCL_ARCHIVE} && \ + mkdir llvm && \ + tar -xf ${SYCL_ARCHIVE} -C llvm --strip-components=1 && \ + cd llvm && \ + python3 buildbot/configure.py --cuda && \ + python3 buildbot/compile.py && \ + mkdir -p ${SYCL_DIR} && \ + mv ${SCRATCH_DIR}/llvm/build/install/* ${SYCL_DIR} && \ + echo "${SYCL_DIR}/lib" > /etc/ld.so.conf.d/sycl.conf && ldconfig && \ + rm -rf ${SCRATCH_DIR} +ENV PATH=${SYCL_DIR}/bin:$PATH + +# Install oneDPL +ENV ONE_DPL_DIR=/opt/onedpl +RUN ONE_DPL_VERSION=oneDPL-2021.3.0-release && \ + ONE_DPL_URL=https://github.com/oneapi-src/oneDPL/archive && \ + ONE_DPL_ARCHIVE=${ONE_DPL_VERSION}.tar.gz && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${ONE_DPL_URL}/${ONE_DPL_ARCHIVE} && \ + mkdir onedpl && \ + tar -xf ${ONE_DPL_ARCHIVE} -C onedpl --strip-components=1 && cd onedpl && \ + echo 'install(CODE "set(OUTPUT_DIR \"${CMAKE_INSTALL_PREFIX}/lib/cmake/oneDPL\")")' >> CMakeLists.txt && \ + echo 'install(SCRIPT cmake/scripts/generate_config.cmake)' >> CMakeLists.txt && \ + echo 'install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include DESTINATION linux)' >> CMakeLists.txt && \ + mkdir build && cd build && \ + cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-w" -DCMAKE_INSTALL_PREFIX=${ONE_DPL_DIR} -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=TRUE -DONEDPL_BACKEND="dpcpp_only" .. && \ + make -j${NPROCS} install && \ + rm -rf ${SCRATCH_DIR} + +# Install Boost +ENV BOOST_DIR=/opt/boost +RUN BOOST_VERSION=1.72.0 && \ + BOOST_VERSION_UNDERSCORE=$(echo "$BOOST_VERSION" | sed -e "s/\./_/g") && \ + BOOST_KEY=379CE192D401AB61 && \ + BOOST_URL=https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION}/source && \ + BOOST_ARCHIVE=boost_${BOOST_VERSION_UNDERSCORE}.tar.bz2 && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE} && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.asc && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json && \ + wget --quiet ${BOOST_URL}/${BOOST_ARCHIVE}.json.asc && \ + gpg --verify ${BOOST_ARCHIVE}.json.asc ${BOOST_ARCHIVE}.json && \ + gpg --verify ${BOOST_ARCHIVE}.asc ${BOOST_ARCHIVE} && \ + cat ${BOOST_ARCHIVE}.json | jq -r '. | .sha256 + " " + .file' | sha256sum --check && \ + mkdir -p boost && \ + tar -xf ${BOOST_ARCHIVE} -C boost --strip-components=1 && \ + cd boost && \ + CXXFLAGS="-w" ./bootstrap.sh \ + --prefix=${BOOST_DIR} \ + && \ + ./b2 -j${NPROCS} \ + hardcode-dll-paths=true dll-path=${BOOST_DIR}/lib \ + link=shared \ + variant=release \ + cxxflags=-w \ + install \ + && \ + rm -rf ${SCRATCH_DIR} + +# Install Google Benchmark support library +ENV BENCHMARK_DIR=/opt/benchmark +RUN SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + git clone https://github.com/google/benchmark.git -b v1.5.0 && \ + cd benchmark && \ + mkdir build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${BENCHMARK_DIR} -D BENCHMARK_ENABLE_TESTING=OFF .. && \ + make -j${NPROCS} && make install && \ + rm -rf ${SCRATCH_DIR} + +# Install Kokkos +ARG KOKKOS_VERSION=3.4.00 +ARG KOKKOS_OPTIONS="-DKokkos_ENABLE_SYCL=ON -DCMAKE_CXX_FLAGS=-Wno-unknown-cuda-version -DKokkos_ENABLE_UNSUPPORTED_ARCHS=ON -DKokkos_ARCH_VOLTA70=ON -DCMAKE_CXX_STANDARD=17" +ENV KOKKOS_DIR=/opt/kokkos +RUN KOKKOS_URL=https://github.com/kokkos/kokkos/archive/${KOKKOS_VERSION}.tar.gz && \ + KOKKOS_ARCHIVE=kokkos-${KOKKOS_HASH}.tar.gz && \ + SCRATCH_DIR=/scratch && mkdir -p ${SCRATCH_DIR} && cd ${SCRATCH_DIR} && \ + wget --quiet ${KOKKOS_URL} --output-document=${KOKKOS_ARCHIVE} && \ + mkdir -p kokkos && \ + tar -xf ${KOKKOS_ARCHIVE} -C kokkos --strip-components=1 && \ + cd kokkos && \ + mkdir -p build && cd build && \ + cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=${KOKKOS_DIR} -D CMAKE_CXX_COMPILER=clang++ ${KOKKOS_OPTIONS} .. && \ + make -j${NPROCS} install && \ + rm -rf ${SCRATCH_DIR} diff --git a/arborx/docker/README.md b/arborx/docker/README.md new file mode 100644 index 000000000..66b82173c --- /dev/null +++ b/arborx/docker/README.md @@ -0,0 +1,18 @@ +Consider setting the `COMPOSE_PROJECT_NAME` environment variable or providing it +in the environment file. Its value will be prepended along with the service +name to the container on start up. + + +You may use multiple compose files to customize your container. For instance, you +could reproduce the configuration from one of the automated builds by providing +the following a `docker-compose.override.yml` file: +``` +version: '3' +services: + arborx_dev: + build: + args: + - BASE=nvidia/cuda:10.1-devel + - KOKKOS_VERSION=2.8.00 + - KOKKOS_OPTIONS=--cxxstandard=c++14 --with-serial --with-openmp --with-options=disable_deprecated_code --with-cuda --with-cuda-options=enable_lambda --arch=SNB,Volta70 +``` diff --git a/arborx/docker/docker-compose.yml b/arborx/docker/docker-compose.yml new file mode 100644 index 000000000..3487fb773 --- /dev/null +++ b/arborx/docker/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' +services: + arborx_dev: + build: + context: . + volumes: + - ..:${CONTAINER_ARBORX_DIR} + - ccache_dir:${CONTAINER_CCACHE_DIR} + working_dir: ${CONTAINER_ARBORX_DIR} + environment: + - TERM=xterm + - ARBORX_DIR=${CONTAINER_ARBORX_DIR} + - CCACHE_DIR=${CONTAINER_CCACHE_DIR} + - CCACHE_MAXSIZE=10G + command: tail -f /dev/null + security_opt: + - seccomp:unconfined + network_mode: host +volumes: + ccache_dir: diff --git a/arborx/docs/LICENSE.ECL b/arborx/docs/LICENSE.ECL new file mode 100644 index 000000000..82de69c15 --- /dev/null +++ b/arborx/docs/LICENSE.ECL @@ -0,0 +1,36 @@ +/* +ECL-CC code: ECL-CC is a connected components algorithm. The CUDA +implementation thereof is very fast. It operates on graphs stored in +binary CSR format. + +Copyright (c) 2017, Texas State University. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted for academic, research, experimental, or personal use provided +that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions, and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Texas State University nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +For all other uses, please contact the Office for Commercialization and Industry +Relations at Texas State University . + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Authors: Jayadharini Jaiganesh and Martin Burtscher +*/ diff --git a/arborx/docs/logos/arborx_logo_v1.0.png b/arborx/docs/logos/arborx_logo_v1.0.png new file mode 100644 index 0000000000000000000000000000000000000000..d52b1c3f80f4495fe16ae5e924bf5c2dc19f7925 GIT binary patch literal 79458 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%0A!Yz6b*THf_m-0<@ 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 U9;;Ys#lmN(YpO%ka=!b205R3q4FCWD literal 0 HcmV?d00001 diff --git a/arborx/docs/logos/arborx_logo_v1.0.svg b/arborx/docs/logos/arborx_logo_v1.0.svg new file mode 100644 index 000000000..5c94a0b19 --- /dev/null +++ b/arborx/docs/logos/arborx_logo_v1.0.svg @@ -0,0 +1,357 @@ + + + + + + + + + + + + + + + diff --git a/arborx/docs/logos/arborx_logo_v1.0_nobg.png b/arborx/docs/logos/arborx_logo_v1.0_nobg.png new file mode 100644 index 0000000000000000000000000000000000000000..7769f01366736943aa400145cecca0622cbfb187 GIT binary patch literal 81640 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

W_Um~R=Q4=r5GPfWaTWA1B1+0a}v!ZNz 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 + +#include + +#include +#include +#include + +struct PointCloud +{ + float *d_x; + float *d_y; + float *d_z; + int N; +}; + +struct Spheres +{ + float *d_x; + float *d_y; + float *d_z; + float *d_r; + int N; +}; + +template <> +struct ArborX::AccessTraits +{ + static KOKKOS_FUNCTION std::size_t size(PointCloud const &cloud) + { + return cloud.N; + } + static KOKKOS_FUNCTION ArborX::Point get(PointCloud const &cloud, + std::size_t i) + { + return {{cloud.d_x[i], cloud.d_y[i], cloud.d_z[i]}}; + } + using memory_space = Kokkos::CudaSpace; +}; + +template <> +struct ArborX::AccessTraits +{ + static KOKKOS_FUNCTION std::size_t size(Spheres const &d) { return d.N; } + static KOKKOS_FUNCTION auto get(Spheres const &d, std::size_t i) + { + return ArborX::intersects( + ArborX::Sphere{{{d.d_x[i], d.d_y[i], d.d_z[i]}}, d.d_r[i]}); + } + using memory_space = Kokkos::CudaSpace; +}; + +int main(int argc, char *argv[]) +{ + Kokkos::ScopeGuard guard(argc, argv); + + constexpr std::size_t N = 10; + std::array a; + + float *d_a; + cudaMalloc(&d_a, sizeof(a)); + + std::iota(std::begin(a), std::end(a), 1.0); + + cudaStream_t stream; + cudaStreamCreate(&stream); + cudaMemcpyAsync(d_a, a.data(), sizeof(a), cudaMemcpyHostToDevice, stream); + + Kokkos::Cuda cuda{stream}; + ArborX::BVH bvh{cuda, PointCloud{d_a, d_a, d_a, N}}; + + Kokkos::View indices("indices", 0); + Kokkos::View offset("offset", 0); + ArborX::query(bvh, cuda, Spheres{d_a, d_a, d_a, d_a, N}, indices, offset); + + Kokkos::parallel_for(Kokkos::RangePolicy(cuda, 0, N), + KOKKOS_LAMBDA(int i) { + for (int j = offset(i); j < offset(i + 1); ++j) + { + printf("%i %i\n", i, indices(j)); + } + }); + + cudaStreamDestroy(stream); + + return 0; +} diff --git a/arborx/examples/access_traits/example_host_access_traits.cpp b/arborx/examples/access_traits/example_host_access_traits.cpp new file mode 100644 index 000000000..b2a1eb06b --- /dev/null +++ b/arborx/examples/access_traits/example_host_access_traits.cpp @@ -0,0 +1,52 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include + +#include + +#include +#include +#include + +template +struct ArborX::AccessTraits, Tag> +{ + static std::size_t size(std::vector const &v) { return v.size(); } + static T const &get(std::vector const &v, std::size_t i) { return v[i]; } + using memory_space = Kokkos::HostSpace; +}; + +int main(int argc, char *argv[]) +{ + Kokkos::ScopeGuard guard(argc, argv); + + std::vector points; + // Fill vector with random points in [-1, 1]^3 + std::uniform_real_distribution dis{-1., 1.}; + std::default_random_engine gen; + auto rd = [&]() { return dis(gen); }; + std::generate_n(std::back_inserter(points), 100, [&]() { + return ArborX::Point{rd(), rd(), rd()}; + }); + + // Pass directly the vector of points to use the access traits defined above + ArborX::BVH bvh{Kokkos::DefaultHostExecutionSpace{}, + points}; + + // As a supported alternative, wrap the vector in an unmanaged View + bvh = ArborX::BVH{ + Kokkos::DefaultHostExecutionSpace{}, + Kokkos::View{ + points.data(), points.size()}}; + + return 0; +} diff --git a/arborx/examples/brute_force/CMakeLists.txt b/arborx/examples/brute_force/CMakeLists.txt new file mode 100644 index 000000000..c2ab407e6 --- /dev/null +++ b/arborx/examples/brute_force/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(ArborX_BruteForce.exe brute_force.cpp) +target_link_libraries(ArborX_BruteForce.exe ArborX::ArborX Boost::program_options) +add_test(NAME ArborX_BruteForce_Example COMMAND ./ArborX_BruteForce.exe) diff --git a/arborx/examples/brute_force/brute_force.cpp b/arborx/examples/brute_force/brute_force.cpp new file mode 100644 index 000000000..1b03d2d1f --- /dev/null +++ b/arborx/examples/brute_force/brute_force.cpp @@ -0,0 +1,124 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include +#include + +#include + +#include + +struct Dummy +{ + int count; +}; + +using ExecutionSpace = Kokkos::DefaultExecutionSpace; +using MemorySpace = ExecutionSpace::memory_space; + +template <> +struct ArborX::AccessTraits +{ + using memory_space = MemorySpace; + using size_type = typename MemorySpace::size_type; + static KOKKOS_FUNCTION size_type size(Dummy const &d) { return d.count; } + static KOKKOS_FUNCTION Point get(Dummy const &, size_type i) + { + return {{(float)i, (float)i, (float)i}}; + } +}; + +template <> +struct ArborX::AccessTraits +{ + using memory_space = MemorySpace; + using size_type = typename MemorySpace::size_type; + static KOKKOS_FUNCTION size_type size(Dummy const &d) { return d.count; } + static KOKKOS_FUNCTION auto get(Dummy const &, size_type i) + { + return attach( + intersects(Sphere{{{(float)i, (float)i, (float)i}}, (float)i}), i); + } +}; + +int main(int argc, char *argv[]) +{ + Kokkos::ScopeGuard guard(argc, argv); + + int nqueries; + int nprimitives; + int nrepeats; + namespace bpo = boost::program_options; + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "help message" ) + ( "predicates", bpo::value(&nqueries)->default_value(5), "number of predicates" ) + ( "primitives", bpo::value(&nprimitives)->default_value(5), "number of primitives" ) + ( "iterations", bpo::value(&nrepeats)->default_value(1), "number of iterations" ) + ; + // clang-format on + bpo::variables_map vm; + bpo::store(bpo::command_line_parser(argc, argv).options(desc).run(), vm); + bpo::notify(vm); + + if (vm.count("help") > 0) + { + std::cout << desc << '\n'; + return 1; + } + printf("Primitives: %d\n", nprimitives); + printf("Predicates: %d\n", nqueries); + printf("Iterations: %d\n", nrepeats); + + ARBORX_ASSERT(nprimitives > 0); + ARBORX_ASSERT(nqueries > 0); + + ExecutionSpace space{}; + Dummy primitives{nprimitives}; + Dummy predicates{nqueries}; + + for (int i = 0; i < nrepeats; i++) + { + unsigned int out_count; + { + Kokkos::Timer timer; + ArborX::BoundingVolumeHierarchy bvh{space, primitives}; + + Kokkos::View indices("indices_ref", 0); + Kokkos::View offset("offset_ref", 0); + bvh.query(space, predicates, indices, offset); + + space.fence(); + double time = timer.seconds(); + if (i == 0) + printf("Collisions: %.5f\n", + (float)(indices.extent(0)) / (nprimitives * nqueries)); + printf("Time BVH: %lf\n", time); + out_count = indices.extent(0); + } + + { + Kokkos::Timer timer; + ArborX::BruteForce brute{space, primitives}; + + Kokkos::View indices("indices", 0); + Kokkos::View offset("offset", 0); + brute.query(space, predicates, indices, offset); + + space.fence(); + double time = timer.seconds(); + printf("Time BF: %lf\n", time); + ARBORX_ASSERT(out_count == indices.extent(0)); + } + } + return 0; +} diff --git a/arborx/examples/callback/CMakeLists.txt b/arborx/examples/callback/CMakeLists.txt new file mode 100644 index 000000000..c0338142f --- /dev/null +++ b/arborx/examples/callback/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(ArborX_Callback.exe example_callback.cpp) +target_link_libraries(ArborX_Callback.exe ArborX::ArborX) +add_test(NAME ArborX_Callback_Example COMMAND ./ArborX_Callback.exe) diff --git a/arborx/examples/callback/example_callback.cpp b/arborx/examples/callback/example_callback.cpp new file mode 100644 index 000000000..2bd3234cb --- /dev/null +++ b/arborx/examples/callback/example_callback.cpp @@ -0,0 +1,145 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include + +#include + +#include +#include +#include + +using ExecutionSpace = Kokkos::DefaultExecutionSpace; +using MemorySpace = ExecutionSpace::memory_space; + +struct FirstOctant +{ +}; + +struct NearestToOrigin +{ + int k; +}; + +template <> +struct ArborX::AccessTraits +{ + static KOKKOS_FUNCTION std::size_t size(FirstOctant) { return 1; } + static KOKKOS_FUNCTION auto get(FirstOctant, std::size_t) + { + return ArborX::intersects(ArborX::Box{{{0, 0, 0}}, {{1, 1, 1}}}); + } + using memory_space = MemorySpace; +}; + +template <> +struct ArborX::AccessTraits +{ + static KOKKOS_FUNCTION std::size_t size(NearestToOrigin) { return 1; } + static KOKKOS_FUNCTION auto get(NearestToOrigin d, std::size_t) + { + return ArborX::nearest(ArborX::Point{0, 0, 0}, d.k); + } + using memory_space = MemorySpace; +}; + +struct PrintfCallback +{ + template + KOKKOS_FUNCTION void operator()(Predicate, int primitive, + OutputFunctor const &out) const + { +#ifndef __SYCL_DEVICE_ONLY__ + printf("Found %d from functor\n", primitive); +#endif + out(primitive); + } +}; + +int main(int argc, char *argv[]) +{ + Kokkos::ScopeGuard guard(argc, argv); + + int const n = 100; + std::vector points; + // Fill vector with random points in [-1, 1]^3 + std::uniform_real_distribution dis{-1., 1.}; + std::default_random_engine gen; + auto rd = [&]() { return dis(gen); }; + std::generate_n(std::back_inserter(points), n, [&]() { + return ArborX::Point{rd(), rd(), rd()}; + }); + + ArborX::BVH bvh{ + ExecutionSpace{}, + Kokkos::create_mirror_view_and_copy( + MemorySpace{}, + Kokkos::View(points.data(), points.size()))}; + + { + Kokkos::View values("values", 0); + Kokkos::View offsets("offsets", 0); + ArborX::query(bvh, ExecutionSpace{}, FirstOctant{}, PrintfCallback{}, + values, offsets); +#ifndef __NVCC__ + ArborX::query(bvh, ExecutionSpace{}, FirstOctant{}, + KOKKOS_LAMBDA(auto /*predicate*/, int primitive, + auto /*output_functor*/) { +#ifndef __SYCL_DEVICE_ONLY__ + printf("Found %d from generic lambda\n", primitive); +#else + (void)primitive; +#endif + }, + values, offsets); +#endif + } + + { + int const k = 10; + Kokkos::View values("values", 0); + Kokkos::View offsets("offsets", 0); + ArborX::query(bvh, ExecutionSpace{}, NearestToOrigin{k}, PrintfCallback{}, + values, offsets); +#ifndef __NVCC__ + ArborX::query(bvh, ExecutionSpace{}, NearestToOrigin{k}, + KOKKOS_LAMBDA(auto /*predicate*/, int primitive, + auto /*output_functor*/) { +#ifndef __SYCL_DEVICE_ONLY__ + printf("Found %d from generic lambda\n", primitive); +#else + (void)primitive; +#endif + }, + values, offsets); +#endif + } + + { + // EXPERIMENTAL + Kokkos::View> c( + "counter"); + +#ifndef __NVCC__ + bvh.query(ExecutionSpace{}, FirstOctant{}, + KOKKOS_LAMBDA(auto /*predicate*/, int j) { +#ifndef __SYCL_DEVICE_ONLY__ + printf("%d %d %d\n", ++c(), -1, j); +#else + (void)j; +#endif + }); +#endif + } + + return 0; +} diff --git a/arborx/examples/dbscan/ArborX_DBSCANVerification.hpp b/arborx/examples/dbscan/ArborX_DBSCANVerification.hpp new file mode 100644 index 000000000..d8f8e1437 --- /dev/null +++ b/arborx/examples/dbscan/ArborX_DBSCANVerification.hpp @@ -0,0 +1,320 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_DETAILSDBSCANVERIFICATION_HPP +#define ARBORX_DETAILSDBSCANVERIFICATION_HPP + +#include +#include + +#include + +#include +#include + +namespace ArborX +{ +namespace Details +{ + +// Check that core points have nonnegative indices +template +bool verifyCorePointsNonnegativeIndex(ExecutionSpace const &exec_space, + IndicesView /*indices*/, + OffsetView offset, LabelsView labels, + int core_min_size) +{ + int n = labels.size(); + + int num_incorrect = 0; + Kokkos::parallel_reduce( + "ArborX::DBSCAN::verify_core_points_nonnegative", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int i, int &update) { + bool self_is_core_point = (offset(i + 1) - offset(i) >= core_min_size); + if (self_is_core_point && labels(i) < 0) + { +#ifndef __SYCL_DEVICE_ONLY__ + printf("Core point is marked as noise: %d [%d]\n", i, labels(i)); +#endif + update++; + } + }, + num_incorrect); + return (num_incorrect == 0); +} + +// Check that connected core points have same cluster indices +template +bool verifyConnectedCorePointsShareIndex(ExecutionSpace const &exec_space, + IndicesView indices, OffsetView offset, + LabelsView labels, int core_min_size) +{ + int n = labels.size(); + + int num_incorrect = 0; + Kokkos::parallel_reduce( + "ArborX::DBSCAN::verify_connected_core_points", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int i, int &update) { + bool self_is_core_point = (offset(i + 1) - offset(i) >= core_min_size); + if (self_is_core_point) + { + for (int jj = offset(i); jj < offset(i + 1); ++jj) + { + int j = indices(jj); + bool neigh_is_core_point = + (offset(j + 1) - offset(j) >= core_min_size); + + if (neigh_is_core_point && labels(i) != labels(j)) + { +#ifndef __SYCL_DEVICE_ONLY__ + printf("Connected cores do not belong to the same cluster: " + "%d [%d] -> %d [%d]\n", + i, labels(i), j, labels(j)); +#endif + update++; + } + } + } + }, + num_incorrect); + return (num_incorrect == 0); +} + +// Check that border points share index with at least one core point, and +// that noise points have index -1 +template +bool verifyBorderAndNoisePoints(ExecutionSpace const &exec_space, + IndicesView indices, OffsetView offset, + LabelsView labels, int core_min_size) +{ + int n = labels.size(); + + int num_incorrect = 0; + Kokkos::parallel_reduce( + "ArborX::DBSCAN::verify_connected_border_points", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int i, int &update) { + bool self_is_core_point = (offset(i + 1) - offset(i) >= core_min_size); + if (!self_is_core_point) + { + bool is_border = false; + bool have_shared_core = false; + for (int jj = offset(i); jj < offset(i + 1); ++jj) + { + int j = indices(jj); + bool neigh_is_core_point = + (offset(j + 1) - offset(j) >= core_min_size); + + if (neigh_is_core_point) + { + is_border = true; + if (labels(i) == labels(j)) + { + have_shared_core = true; + break; + } + } + } + + // Border point must be connected to a core point + if (is_border && !have_shared_core) + { +#ifndef __SYCL_DEVICE_ONLY__ + printf("Border point does not belong to a cluster: %d [%d]\n", i, + labels(i)); +#endif + update++; + } + // Noise points must have index -1 + if (!is_border && labels(i) != -1) + { +#ifndef __SYCL_DEVICE_ONLY__ + printf("Noise point does not have index -1: %d [%d]\n", i, + labels(i)); +#endif + update++; + } + } + }, + num_incorrect); + return (num_incorrect == 0); +} + +// Check that cluster indices are unique +template +bool verifyClustersAreUnique(ExecutionSpace const &exec_space, + IndicesView indices, OffsetView offset, + LabelsView labels, int core_min_size) +{ + int n = labels.size(); + + // FIXME we don't want to modify the labels view in this check. What we + // want here is to create a view on the host, and deep_copy into it. + // create_mirror_view_and_copy won't work, because it is a no-op if labels + // is already on the host. + decltype(Kokkos::create_mirror_view(Kokkos::HostSpace{}, + std::declval())) + labels_host(Kokkos::view_alloc(Kokkos::WithoutInitializing, + "ArborX::DBSCAN::labels_host"), + labels.size()); + Kokkos::deep_copy(exec_space, labels_host, labels); + auto offset_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, offset); + auto indices_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, indices); + + auto is_core_point = [&](int i) { + return offset_host(i + 1) - offset_host(i) >= core_min_size; + }; + + // Remove all border points from consideration (noise points are already -1) + // The idea is that this way if labels were bridged through a border + // point, we will count them as separate labels but with a shared cluster + // index, which will fail the unique labels check + for (int i = 0; i < n; ++i) + { + if (!is_core_point(i)) + { + for (int jj = offset_host(i); jj < offset_host(i + 1); ++jj) + { + int j = indices_host(jj); + if (is_core_point(j)) + { + // The point is a border point + labels_host(i) = -1; + break; + } + } + } + } + + // Record all unique cluster indices + std::set unique_cluster_indices; + for (int i = 0; i < n; ++i) + if (labels_host(i) != -1) + unique_cluster_indices.insert(labels_host(i)); + auto num_unique_cluster_indices = unique_cluster_indices.size(); + + // Record all cluster indices, assigning a unique index to each (which is + // different from the original cluster index). This will only use noise and + // core points (see above). + unsigned int num_clusters = 0; + std::set cluster_sets; + for (int i = 0; i < n; ++i) + { + if (labels_host(i) >= 0) + { + auto id = labels_host(i); + cluster_sets.insert(id); + num_clusters++; + + // DFS search + std::stack stack; + stack.push(i); + while (!stack.empty()) + { + auto k = stack.top(); + stack.pop(); + if (labels_host(k) >= 0) + { + labels_host(k) = -1; + for (int jj = offset_host(k); jj < offset_host(k + 1); ++jj) + { + int j = indices_host(jj); + if (is_core_point(j) || (labels_host(j) == id)) + stack.push(j); + } + } + } + } + } + if (cluster_sets.size() != num_unique_cluster_indices) + { + std::cerr << "Number of components does not match" << std::endl; + return false; + } + if (num_clusters != num_unique_cluster_indices) + { + std::cerr << "Cluster IDs are not unique" << std::endl; + return false; + } + + return true; +} + +template +bool verifyClusters(ExecutionSpace const &exec_space, IndicesView indices, + OffsetView offset, LabelsView labels, int core_min_size) +{ + int n = labels.size(); + if ((int)offset.size() != n + 1 || + ArborX::lastElement(offset) != (int)indices.size()) + return false; + + using Verify = bool (*)(ExecutionSpace const &, IndicesView, OffsetView, + LabelsView, int); + + for (auto verify : {static_cast(verifyCorePointsNonnegativeIndex), + static_cast(verifyConnectedCorePointsShareIndex), + static_cast(verifyBorderAndNoisePoints), + static_cast(verifyClustersAreUnique)}) + { + if (!verify(exec_space, indices, offset, labels, core_min_size)) + return false; + } + + return true; +} + +template +bool verifyDBSCAN(ExecutionSpace exec_space, Primitives const &primitives, + float eps, int core_min_size, LabelsView const &labels) +{ + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::verify"); + + static_assert(Kokkos::is_view{}, ""); + + using Access = AccessTraits; + using MemorySpace = typename Access::memory_space; + + static_assert(std::is_same{}, ""); + static_assert(std::is_same{}, + ""); + + ARBORX_ASSERT(eps > 0); + ARBORX_ASSERT(core_min_size >= 2); + + ArborX::BVH bvh(exec_space, primitives); + + auto const predicates = + Details::PrimitivesWithRadius{primitives, eps}; + + Kokkos::View indices("ArborX::DBSCAN::indices", 0); + Kokkos::View offset("ArborX::DBSCAN::offset", 0); + ArborX::query(bvh, exec_space, predicates, indices, offset); + + auto passed = Details::verifyClusters(exec_space, indices, offset, labels, + core_min_size); + Kokkos::Profiling::popRegion(); + + return passed; +} +} // namespace Details +} // namespace ArborX + +#endif diff --git a/arborx/examples/dbscan/CMakeLists.txt b/arborx/examples/dbscan/CMakeLists.txt new file mode 100644 index 000000000..6449e7261 --- /dev/null +++ b/arborx/examples/dbscan/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(ArborX_DBSCAN.exe dbscan.cpp) +target_include_directories(ArborX_DBSCAN.exe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(ArborX_DBSCAN.exe ArborX::ArborX Boost::program_options) + +add_executable(ArborX_DataConverter.exe converter.cpp) +target_compile_features(ArborX_DataConverter.exe PRIVATE cxx_std_14) +target_link_libraries(ArborX_DataConverter.exe Boost::program_options) + +set(input_file "input.txt") +add_test(NAME ArborX_DBSCAN_Example COMMAND ./ArborX_DBSCAN.exe --filename=${input_file} --eps=1.4 --verify) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${input_file} ${CMAKE_CURRENT_BINARY_DIR}/${input_file} COPYONLY) diff --git a/arborx/examples/dbscan/README.md b/arborx/examples/dbscan/README.md new file mode 100644 index 000000000..683e6a4ce --- /dev/null +++ b/arborx/examples/dbscan/README.md @@ -0,0 +1,62 @@ +# Algorithm + +This example considers the DBSCAN algorithm [1]. DBSCAN algorithm computes +clusters based on two parameters, `min_pts` (number of neighbors required to be +considered a core point) and `eps` (radius). + +An example of an application requiring a solution to such problem is computing +halos in a cosmology simulation. Here, points correspond to points in the +universe, `eps` is called linking length, `min_pts = 1` and clusters are called +halos, or Friends-of-Friends (FoF). + +A straightforward approach to compute halos, for example, would have one +compute the connectivity graph explicitly through ArborX spatial query search, +and then run a CC algorithm, such as ECL-CC ([2]), on that graph. The +implemented approach is an improvement over straightforward approach. Instead +of constructing the graph explicitly, it uses a combination of the callback +mechanism in ArborX with the `compute1` routine of the CC algorithm in [2] to +construct CC in a single tree query. A general DBSCAN algorithm is implemented +in a similar fashion, with a main distinction being that the number of +neighbors is pre-computed. Thus, it is expected that the `min_pts > 1` +algorithm is twice slower compared to `min_pts = 1` case. + +[1] Ester, Kriegel, Sander, Xu. "A density-based algorithm for discovering +clusters in large spatial databases with noise". In Proceedings of the Second +International Conference on Knowledge Discovery and Data Mining, pp. 226-231. +1996. + +[2] Jaiganesh, Burtscher. "A high-performance connected +components implementation for GPUs." In Proceedings of the 27th International +Symposium on High-Performance Parallel and Distributed Computing, pp. 92-104. +2018. + +# Input + +The example conrols its input through command-line options: +- `--filename` + The data is expected to be provided as an argument to the `--filename` + option. The data is in the format `[number of points, X-coordinates, + Y-coordinates, Z-coordinates]`. +- `--binary` + Indicator whether the data provided through `--filename` option is text or + binary. If the data is binary, it is expected that number of points is an + 4-byte integer, and each coordinate is a 4-byte floating point number. +- `--core-min-size` + `min_pts` parameter of the DBSCAN algorithm +- `--eps` + `eps` parameter of the DBSCAN algorithm +- `--cluster-min-size` + Further post-processing to filter out clusters under certain size +- `--verify` + Internal check switch to verify clusters. This options is significantly more + expected, as it explicitly computes the graph. This may also mean that it + will run out of memory on GPU even if the DBSCAN algorithm itself does not. + +# Output + +The example produces clusters in CSR (compressed sparse storage) format +consisting of two arrays `(cluster_indices, cluster_offset)`, with indices for +a cluster `i` being entries +`cluster_indices(cluster_offset(i):cluster_offset(i+1)`. A simple +postprocessing step that calculates the sizes and centers of each cluster is +then performed. diff --git a/arborx/examples/dbscan/converter.cpp b/arborx/examples/dbscan/converter.cpp new file mode 100644 index 000000000..53d7f4572 --- /dev/null +++ b/arborx/examples/dbscan/converter.cpp @@ -0,0 +1,472 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Points +{ +private: + std::vector> _data; + +public: + Points(int dim, int num_points = 0) + { + _data.resize(dim); + for (int i = 0; i < dim; ++i) + _data[i].resize(num_points); + } + + int dimension() const { return _data.size(); } + + int size() const + { + assert(dimension() > 0); + return _data[0].size(); + } + + std::vector &operator[](int d) + { + assert(d < dimension()); + return _data[d]; + } + + std::vector const &operator[](int d) const + { + assert(d < dimension()); + return _data[d]; + } +}; + +auto loadHACCData(std::string const &filename) +{ + std::cout << "Assuming HACC data.\n"; + std::cout << "Reading in \"" << filename << "\" in binary mode..."; + std::cout.flush(); + + std::ifstream input(filename, std::ifstream::binary); + if (!input.good()) + throw std::runtime_error("Cannot open file"); + + int num_points = 0; + input.read(reinterpret_cast(&num_points), sizeof(int)); + + Points points(3, num_points); + input.read(reinterpret_cast(points[0].data()), + num_points * sizeof(float)); + input.read(reinterpret_cast(points[1].data()), + num_points * sizeof(float)); + input.read(reinterpret_cast(points[2].data()), + num_points * sizeof(float)); + input.close(); + std::cout << "done\nRead in " << num_points << " points" << std::endl; + + return points; +} + +// Next Generation Simulation (NGSIM) Vehicle Trajectories data reader. +// +// NGSIM data consists of vehicle trajectory data collected by NGSIM +// researchers on three highways in Los Angeles, CA, Emeryville, CA, and +// Atlanta, GA. The trajectory data have been transcribed for every vehicle +// from the footage of video cameras using NGVIDEO. +// +// The data was used in Mustafa et al "An experimental comparison of GPU +// techniques for DBSCAN clustering", IEEE International Conference on Big Data, +// 2019. +// +// The data can be found at +// https://catalog.data.gov/dataset/next-generation-simulation-ngsim-vehicle-trajectories-and-supporting-data +// (direct link +// https://data.transportation.gov/api/views/8ect-6jqj/rows.csv?accessType=DOWNLOAD). +// +// Among other attributes, each data points has a timestamp, vehicle ID, local +// orad coordinates, global coordinates, vehicle length, width, velocity and +// acceleration. +// +// The code here is different from the source code for the Mustafa2019 paper. +// In that codebase, they seem to have a filtered file that only contains the +// global coordinates and not all the other data fields. +auto loadNGSIMData(std::string const &filename) +{ + std::cout << "Assuming NGSIM data.\n"; + std::cout << "Reading in \"" << filename << "\" in text mode..."; + std::cout.flush(); + + std::ifstream file(filename); + if (!file.good()) + throw std::runtime_error("Cannot open file"); + + std::string thisWord; + std::string line; + + Points points(2); + + // ignore first line that contains the descriptions + int n_points = 0; + getline(file, thisWord); + while (file.good()) + { + if (!getline(file, line)) + break; + + std::stringstream ss(line); + // GVehicle_ID,Frame_ID,Total_Frames,Global_Time,Local_X,Local_Y + for (int i = 0; i < 6; ++i) + getline(ss, thisWord, ','); + // Global_X,Global_Y + getline(ss, thisWord, ','); + float longitude = stof(thisWord); + getline(ss, thisWord, ','); + float latitude = stof(thisWord); + points[0].emplace_back(longitude); + points[1].emplace_back(latitude); + // v_length,v_Width,v_Class,v_Vel,v_Acc,Lane_ID,O_Zone,D_Zone,Int_ID,Section_ID,Direction,Movement,Preceding,Following,Space_Headway,Time_Headway,Location + for (int i = 0; i < 16; ++i) + getline(ss, thisWord, ','); + getline(ss, thisWord, ','); + ++n_points; + } + std::cout << "done\nRead in " << n_points << " points" << std::endl; + return points; +} + +// Taxi Service Trajectory Prediction Challenge data reader. +// +// The data consists of the trajectories of 442 taxis running in the city of +// Porto, Portugal, over the period of one year. This is a dataset with over +// 1,710,000+ trajectories with 81,000,000+ points in total. +// +// The data can be found at +// https://archive.ics.uci.edu/ml/datasets/Taxi+Service+Trajectory+-+Prediction+Challenge,+ECML+PKDD+2015 +// (direct link +// https://archive.ics.uci.edu/ml/machine-learning-databases/00339/train.csv.zip). +// +// Every data point in this dataset has, besides longitude and latitude values, +// a unique identifier for each taxi trip, taxi ID, timestamp, and user +// information. +auto loadTaxiPortoData(std::string const &filename) +{ + std::cout << "Assuming TaxiPorto data.\n"; + std::cout << "Reading in \"" << filename << "\" in text mode..."; + std::cout.flush(); + + FILE *fp_data = fopen(filename.c_str(), "rb"); + if (fp_data == nullptr) + throw std::runtime_error("Cannot open file"); + char line[100000]; + + // This function reads and segments trajectories in dataset in the following + // format: The first line indicates number of variables per point (I'm + // ignoring that and assuming 2) The second line indicates total trajectories + // in file (I'm ignoring that and observing how many are there by reading + // them). All lines that follow contains a trajectory separated by new line. + // The first number in the trajectory is the number of points followed by + // location points separated by spaces + + std::vector longitudes; + std::vector latitudes; + + int lineNo = -1; + int wordNo = 0; + int lonlatno = 100; + + float thisWord; + while (fgets(line, sizeof(line), fp_data) != nullptr) + { + if (lineNo > -1) + { + char *pch; + char *end_str; + wordNo = 0; + lonlatno = 0; + pch = strtok_r(line, "\"[", &end_str); + while (pch != nullptr) + { + if (wordNo > 0) + { + char *pch2; + char *end_str2; + + pch2 = strtok_r(pch, ",", &end_str2); + + if (strcmp(pch2, "]") < 0 && lonlatno < 255) + { + + thisWord = atof(pch2); + + if (thisWord != 0.00000) + { + if (thisWord > -9 && thisWord < -7) + { + longitudes.push_back(thisWord); + // printf("lon %f",thisWord); + pch2 = strtok_r(nullptr, ",", &end_str2); + thisWord = atof(pch2); + if (thisWord < 42 && thisWord > 40) + { + latitudes.push_back(thisWord); + // printf(" lat %f\n",thisWord); + + lonlatno++; + } + else + { + longitudes.pop_back(); + } + } + } + } + } + pch = strtok_r(nullptr, "[", &end_str); + wordNo++; + } + // printf("num lonlat were %d x 2\n",lonlatno); + } + lineNo++; + if (lonlatno <= 0) + { + lineNo--; + } + + // printf("Line %d\n",lineNo); + } + fclose(fp_data); + + int num_points = longitudes.size(); + assert(longitudes.size() == latitudes.size()); + + Points points(2, num_points); + std::copy(longitudes.begin(), longitudes.end(), points[0].begin()); + std::copy(latitudes.begin(), latitudes.end(), points[1].begin()); + + std::cout << "done\nRead in " << num_points << " points" << std::endl; + + return points; +} + +// 3D Road Network data reader. +// +// The data consists of more than 400,000 points from the road network of North +// Jutland in Denmark. +// +// The data can be found at +// https://archive.ics.uci.edu/ml/datasets/3D+Road+Network+(North+Jutland,+Denmark) +// (direct link +// https://archive.ics.uci.edu/ml/machine-learning-databases/00246/3D_spatial_network.txt). +// +// Each data point contains its ID, longitude, latitude, and altitude. +auto load3DRoadNetworkData(std::string const &filename) +{ + std::cout << "Assuming 3DRoadNetwork data.\n"; + std::cout << "Reading in \"" << filename << "\" in text mode..."; + std::cout.flush(); + + std::ifstream file(filename); + assert(file.good()); + if (!file.good()) + throw std::runtime_error("Cannot open file"); + + Points points(2); + + std::string thisWord; + while (file.good()) + { + getline(file, thisWord, ','); + getline(file, thisWord, ','); + float longitude = stof(thisWord); + getline(file, thisWord, ','); + float latitude = stof(thisWord); + points[0].emplace_back(longitude); + points[1].emplace_back(latitude); + } + // In Mustafa2019 they discarded the last item read but it's not quite clear + // if/why this was necessary. + // lon_ptr.pop_back(); + // lat_ptr.pop_back(); + std::cout << "done\nRead in " << points.size() << " points" << std::endl; + + return points; +} + +// SW data reader. +// +// SW data consists of ionospheric total electron content datasets collected by +// GPS receivers. +// +// Data preprocessing described in Pankratius et al "GPS Data Processing for +// Scientific Studies of the Earth’s Atmosphere and Near-Space Environment". +// Springer International Publishing, 2015, pp. 1–12. +// +// The data was used in Gowanlock et al "Clustering Throughput Optimization on +// the GPU", IPDPS, 2017, pp. 832-841. +// +// The data is available at +// ftp://gemini.haystack.mit.edu/pub/informatics/dbscandat.zip +// +// The data file is a text file. Each line contains three floating point +// numbers separated by ','. The fields corespond to longitude, latitude, and +// total electron content (TEC). The TEC field is unused in the Gowanlock's +// paper, as according to the author: +// because in the application scenario of monitoring space weather, we +// typically first selected the data points based on TEC, and then cluster +// the positions of the points +auto loadSWData(std::string const &filename) +{ + std::cout << "Assuming SW data.\n"; + std::cout << "Reading in \"" << filename << "\" in text mode..."; + std::cout.flush(); + + std::ifstream input; + input.open(filename); + if (!input.good()) + throw std::runtime_error("Cannot open file"); + + Points points(2); + while (input.good()) + { + std::string line; + if (!std::getline(input, line)) + break; + std::istringstream line_stream(line); + + std::string word; + std::getline(line_stream, word, ','); // longitude field + float longitude = std::stof(word); + std::getline(line_stream, word, ','); // latitude field + float latitude = std::stof(word); + std::getline(line_stream, word, ','); // TEC field (ignored) + + points[0].emplace_back(longitude); + points[1].emplace_back(latitude); + } + input.close(); + std::cout << "done\nRead in " << points.size() << " points" << std::endl; + + return points; +} + +// Gaia data reader. +// +// Gaia catalog (data release 2) contains 1.69 billion points +// +// Scientific Studies of the Earth’s Atmosphere and Near-Space Environment". +// Springer International Publishing, 2015, pp. 1–12. +// +// The data was used in Gowanlock "Hybrid CPU/GPU Clustering in Shared Memory +// on the Billion Point Scale", 2019 +// +// The data is available at +// https://rcdata.nau.edu/gowanlock_lab/datasets/ICS19_data/gaia_dr2_ra_dec_50M.txt. +// +// The data file is a text file. Each line contains two floating point +// numbers separated by ','. The fields corespond to longitude, latitude. +auto loadGaiaData(std::string const &filename) +{ + std::cout << "Assuming Gaia data.\n"; + std::cout << "Reading in \"" << filename << "\" in text mode..."; + std::cout.flush(); + + std::ifstream input; + input.open(filename); + if (!input.good()) + throw std::runtime_error("Cannot open file"); + + Points points(2); + while (input.good()) + { + std::string line; + if (!std::getline(input, line)) + break; + std::istringstream line_stream(line); + + std::string word; + std::getline(line_stream, word, ','); // longitude field + float longitude = std::stof(word); + std::getline(line_stream, word, ','); // latitude field + float latitude = std::stof(word); + + points[0].emplace_back(longitude); + points[1].emplace_back(latitude); + } + input.close(); + std::cout << "done\nRead in " << points.size() << " points" << std::endl; + + return points; +} + +auto loadData(std::string const &filename, std::string const &reader_type) +{ + if (reader_type == "hacc") + return loadHACCData(filename); + if (reader_type == "ngsim") + return loadNGSIMData(filename); + if (reader_type == "taxiporto") + return loadTaxiPortoData(filename); + if (reader_type == "3droad") + return load3DRoadNetworkData(filename); + if (reader_type == "sw") + return loadSWData(filename); + if (reader_type == "gaia") + return loadGaiaData(filename); + + throw std::runtime_error("Unknown reader type: \"" + reader_type + "\""); +} + +int main(int argc, char *argv[]) +{ + namespace bpo = boost::program_options; + + std::string input_file; + std::string output_file; + std::string reader; + + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "help message" ) + ( "input", bpo::value(&input_file), "file containing data" ) + ( "output", bpo::value(&output_file), "file to contain the results" ) + ( "reader", bpo::value(&reader), "reader type" ) + ; + // clang-format on + bpo::variables_map vm; + bpo::store(bpo::command_line_parser(argc, argv).options(desc).run(), vm); + bpo::notify(vm); + + if (vm.count("help") > 0) + { + std::cout << desc << '\n'; + return 1; + } + + auto points = loadData(input_file, reader); + int n = points.size(); + int dim = points.dimension(); + + std::ofstream out(output_file, std::ofstream::binary); + out.write((char *)&n, sizeof(int)); + out.write((char *)&dim, sizeof(int)); + for (int i = 0; i < n; ++i) + for (int d = 0; d < dim; ++d) + out.write((char *)(&points[d][i]), sizeof(float)); + + return EXIT_SUCCESS; +} diff --git a/arborx/examples/dbscan/dbscan.cpp b/arborx/examples/dbscan/dbscan.cpp new file mode 100644 index 000000000..a79e5bba2 --- /dev/null +++ b/arborx/examples/dbscan/dbscan.cpp @@ -0,0 +1,504 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include +#include +#include +#include // Less +#include + +#include + +#include + +#include + +std::vector loadData(std::string const &filename, + bool binary = true, int max_num_points = -1) +{ + std::cout << "Reading in \"" << filename << "\" in " + << (binary ? "binary" : "text") << " mode..."; + std::cout.flush(); + + std::ifstream input; + if (!binary) + input.open(filename); + else + input.open(filename, std::ifstream::binary); + ARBORX_ASSERT(input.good()); + + std::vector v; + + int num_points = 0; + int dim = 0; + if (!binary) + { + input >> num_points; + input >> dim; + } + else + { + input.read(reinterpret_cast(&num_points), sizeof(int)); + input.read(reinterpret_cast(&dim), sizeof(int)); + } + + // For now, only allow reading in 2D or 3D data. Will relax in the future. + ARBORX_ASSERT(dim == 2 || dim == 3); + + if (max_num_points > 0 && max_num_points < num_points) + num_points = max_num_points; + + if (!binary) + { + v.reserve(num_points); + + auto it = std::istream_iterator(input); + auto read_point = [&it, dim]() { + float xyz[3] = {0.f, 0.f, 0.f}; + for (int i = 0; i < dim; ++i) + xyz[i] = *it++; + return ArborX::Point{xyz[0], xyz[1], xyz[2]}; + }; + std::generate_n(std::back_inserter(v), num_points, read_point); + } + else + { + v.resize(num_points); + + if (dim == 3) + { + // Can directly read into ArborX::Point + input.read(reinterpret_cast(v.data()), + num_points * sizeof(ArborX::Point)); + } + else + { + std::vector aux(num_points * dim); + input.read(reinterpret_cast(aux.data()), + aux.size() * sizeof(float)); + + for (int i = 0; i < num_points; ++i) + { + ArborX::Point p{0.f, 0.f, 0.f}; + for (int d = 0; d < dim; ++d) + p[d] = aux[i * dim + d]; + v[i] = p; + } + } + } + input.close(); + std::cout << "done\nRead in " << num_points << " " << dim << "D points" + << std::endl; + + return v; +} + +std::vector sampleData(std::vector const &data, + int num_samples) +{ + std::vector sampled_data(num_samples); + + // Knuth algorithm + auto const N = (int)data.size(); + auto const M = num_samples; + for (int in = 0, im = 0; in < N && im < M; ++in) + { + int rn = N - in; + int rm = M - im; + if (rand() % rn < rm) + sampled_data[im++] = data[in]; + } + return sampled_data; +} + +template +void writeLabelsData(std::string const &filename, + Kokkos::View labels) +{ + std::ofstream out(filename, std::ofstream::binary); + ARBORX_ASSERT(out.good()); + + auto labels_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, labels); + + int n = labels_host.size(); + out.write((char *)&n, sizeof(int)); + out.write((char *)labels_host.data(), sizeof(int) * n); +} + +template +auto vec2view(std::vector const &in, std::string const &label = "") +{ + Kokkos::View out( + Kokkos::view_alloc(label, Kokkos::WithoutInitializing), in.size()); + Kokkos::deep_copy(out, Kokkos::View>{ + in.data(), in.size()}); + return out; +} + +template +void sortAndFilterClusters(ExecutionSpace const &exec_space, + LabelsView const &labels, + ClusterIndicesView &cluster_indices, + ClusterOffsetView &cluster_offset, + int cluster_min_size = 1) +{ + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::sortAndFilterClusters"); + + static_assert(Kokkos::is_view{}, ""); + static_assert(Kokkos::is_view{}, ""); + static_assert(Kokkos::is_view{}, ""); + + using MemorySpace = typename LabelsView::memory_space; + + static_assert(std::is_same{}, ""); + static_assert(std::is_same{}, + ""); + static_assert(std::is_same{}, + ""); + + static_assert(std::is_same{}, + ""); + static_assert( + std::is_same{}, + ""); + static_assert( + std::is_same{}, + ""); + + ARBORX_ASSERT(cluster_min_size >= 1); + + int const n = labels.extent_int(0); + + Kokkos::View cluster_sizes( + "ArborX::DBSCAN::cluster_sizes", n); + Kokkos::parallel_for("ArborX::DBSCAN::compute_cluster_sizes", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int const i) { + // Ignore noise points + if (labels(i) < 0) + return; + + Kokkos::atomic_fetch_add(&cluster_sizes(labels(i)), 1); + }); + + // This kernel serves dual purpose: + // - it constructs an offset array through exclusive prefix sum, with a + // caveat that small clusters (of size < cluster_min_size) are filtered out + // - it creates a mapping from a cluster index into the cluster's position in + // the offset array + // We reuse the cluster_sizes array for the second, creating a new alias for + // it for clarity. + auto &map_cluster_to_offset_position = cluster_sizes; + int constexpr IGNORED_CLUSTER = -1; + int num_clusters; + ArborX::reallocWithoutInitializing(cluster_offset, n + 1); + Kokkos::parallel_scan( + "ArborX::DBSCAN::compute_cluster_offset_with_filter", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int const i, int &update, bool final_pass) { + bool is_cluster_too_small = (cluster_sizes(i) < cluster_min_size); + if (!is_cluster_too_small) + { + if (final_pass) + { + cluster_offset(update) = cluster_sizes(i); + map_cluster_to_offset_position(i) = update; + } + ++update; + } + else + { + if (final_pass) + map_cluster_to_offset_position(i) = IGNORED_CLUSTER; + } + }, + num_clusters); + Kokkos::resize(Kokkos::WithoutInitializing, cluster_offset, num_clusters + 1); + ArborX::exclusivePrefixSum(exec_space, cluster_offset); + + auto cluster_starts = ArborX::clone(exec_space, cluster_offset); + ArborX::reallocWithoutInitializing(cluster_indices, + ArborX::lastElement(cluster_offset)); + Kokkos::parallel_for("ArborX::DBSCAN::compute_cluster_indices", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int const i) { + // Ignore noise points + if (labels(i) < 0) + return; + + auto offset_pos = + map_cluster_to_offset_position(labels(i)); + if (offset_pos != IGNORED_CLUSTER) + { + auto position = Kokkos::atomic_fetch_add( + &cluster_starts(offset_pos), 1); + cluster_indices(position) = i; + } + }); + + Kokkos::Profiling::popRegion(); +} + +template +void printClusterSizesAndCenters(ExecutionSpace const &exec_space, + Primitives const &primitives, + ClusterIndicesView &cluster_indices, + ClusterOffsetView &cluster_offset) +{ + auto const num_clusters = static_cast(cluster_offset.size()) - 1; + + using MemorySpace = typename ClusterIndicesView::memory_space; + + Kokkos::View cluster_centers( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "Testing::centers"), + num_clusters); + Kokkos::parallel_for( + "Testing::compute_centers", + Kokkos::RangePolicy(exec_space, 0, num_clusters), + KOKKOS_LAMBDA(int const i) { + // The only reason we sort indices here is for reproducibility. + // Current DBSCAN algorithm does not guarantee that the indices + // corresponding to the same cluster are going to appear in the same + // order from run to run. Using sorted indices, we explicitly + // guarantee the same summation order when computing cluster centers. + + auto *cluster_start = cluster_indices.data() + cluster_offset(i); + auto cluster_size = cluster_offset(i + 1) - cluster_offset(i); + + // Sort cluster indices in ascending order. This uses heap for + // sorting, only because there is no other convenient utility that + // could sort within a kernel. + ArborX::Details::makeHeap(cluster_start, cluster_start + cluster_size, + ArborX::Details::Less()); + ArborX::Details::sortHeap(cluster_start, cluster_start + cluster_size, + ArborX::Details::Less()); + + // Compute cluster centers + ArborX::Point cluster_center{0.f, 0.f, 0.f}; + for (int j = cluster_offset(i); j < cluster_offset(i + 1); j++) + { + auto const &cluster_point = primitives(cluster_indices(j)); + // NOTE The explicit casts below are intended to silence warnings + // about narrowing conversion from 'int' to 'float'. A potential + // accuracy issue here is that 'float' can represent all integer + // values in the range [-2^23, 2^23] but 'int' can actually represent + // values in the range [-2^31, 2^31-1]. However, we ignore it for + // now. + cluster_center[0] += + cluster_point[0] / static_cast(cluster_size); + cluster_center[1] += + cluster_point[1] / static_cast(cluster_size); + cluster_center[2] += + cluster_point[2] / static_cast(cluster_size); + } + cluster_centers(i) = cluster_center; + }); + + auto cluster_offset_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, cluster_offset); + auto cluster_centers_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, cluster_centers); + for (int i = 0; i < num_clusters; i++) + { + int cluster_size = cluster_offset_host(i + 1) - cluster_offset_host(i); + + // This is HACC specific filtering. It is only interested in the clusters + // with centers in [0,64]^3 domain. + auto const &cluster_center = cluster_centers_host(i); + if (cluster_center[0] >= 0 && cluster_center[1] >= 0 && + cluster_center[2] >= 0 && cluster_center[0] < 64 && + cluster_center[1] < 64 && cluster_center[2] < 64) + { + printf("%d %e %e %e\n", cluster_size, cluster_center[0], + cluster_center[1], cluster_center[2]); + } + } +} + +namespace ArborX +{ +namespace DBSCAN +{ +// This function is required for Boost program_options to be able to use the +// Implementation enum. +std::istream &operator>>(std::istream &in, Implementation &implementation) +{ + std::string impl_string; + in >> impl_string; + + if (impl_string == "fdbscan") + implementation = ArborX::DBSCAN::Implementation::FDBSCAN; + else if (impl_string == "fdbscan-densebox") + implementation = ArborX::DBSCAN::Implementation::FDBSCAN_DenseBox; + else + in.setstate(std::ios_base::failbit); + + return in; +} + +// This function is required for Boost program_options to use Implementation +// enum as the default_value(). +std::ostream &operator<<(std::ostream &out, + Implementation const &implementation) +{ + switch (implementation) + { + case ArborX::DBSCAN::Implementation::FDBSCAN: + out << "fdbscan"; + break; + case ArborX::DBSCAN::Implementation::FDBSCAN_DenseBox: + out << "fdbscan-densebox"; + break; + } + return out; +} +} // namespace DBSCAN +} // namespace ArborX + +int main(int argc, char *argv[]) +{ + using ExecutionSpace = Kokkos::DefaultExecutionSpace; + using MemorySpace = typename ExecutionSpace::memory_space; + + Kokkos::ScopeGuard guard(argc, argv); + + std::cout << "ArborX version : " << ArborX::version() << std::endl; + std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; + + namespace bpo = boost::program_options; + using ArborX::DBSCAN::Implementation; + + std::string filename; + bool binary; + bool verify; + bool print_dbscan_timers; + bool print_sizes_centers; + float eps; + int cluster_min_size; + int core_min_size; + int max_num_points; + int num_samples; + std::string filename_labels; + Implementation implementation; + + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "help message" ) + ( "filename", bpo::value(&filename), "filename containing data" ) + ( "binary", bpo::bool_switch(&binary)->default_value(false), "binary file indicator") + ( "max-num-points", bpo::value(&max_num_points)->default_value(-1), "max number of points to read in") + ( "eps", bpo::value(&eps), "DBSCAN eps" ) + ( "cluster-min-size", bpo::value(&cluster_min_size)->default_value(1), "minimum cluster size") + ( "core-min-size", bpo::value(&core_min_size)->default_value(2), "DBSCAN min_pts") + ( "verify", bpo::bool_switch(&verify)->default_value(false), "verify connected components") + ( "samples", bpo::value(&num_samples)->default_value(-1), "number of samples" ) + ( "labels", bpo::value(&filename_labels)->default_value(""), "clutering results output" ) + ( "print-dbscan-timers", bpo::bool_switch(&print_dbscan_timers)->default_value(false), "print dbscan timers") + ( "output-sizes-and-centers", bpo::bool_switch(&print_sizes_centers)->default_value(false), "print cluster sizes and centers") + ( "impl", bpo::value(&implementation)->default_value(Implementation::FDBSCAN), R"(implementation ("fdbscan" or "fdbscan-densebox"))") + ; + // clang-format on + bpo::variables_map vm; + bpo::store(bpo::command_line_parser(argc, argv).options(desc).run(), vm); + bpo::notify(vm); + + if (vm.count("help") > 0) + { + std::cout << desc << '\n'; + return 1; + } + + std::stringstream ss; + ss << implementation; + + // Print out the runtime parameters + printf("eps : %f\n", eps); + printf("minpts : %d\n", core_min_size); + printf("cluster min size : %d\n", cluster_min_size); + printf("filename : %s [%s, max_pts = %d]\n", filename.c_str(), + (binary ? "binary" : "text"), max_num_points); + printf("filename [labels] : %s [binary]\n", filename_labels.c_str()); + printf("implementation : %s\n", ss.str().c_str()); + printf("samples : %d\n", num_samples); + printf("verify : %s\n", (verify ? "true" : "false")); + printf("print timers : %s\n", (print_dbscan_timers ? "true" : "false")); + printf("output centers : %s\n", (print_sizes_centers ? "true" : "false")); + + // read in data + std::vector data = loadData(filename, binary, max_num_points); + if (num_samples > 0 && num_samples < (int)data.size()) + data = sampleData(data, num_samples); + auto const primitives = vec2view(data, "primitives"); + + ExecutionSpace exec_space; + + Kokkos::Timer timer_total; + Kokkos::Timer timer; + std::map elapsed; + + bool const verbose = print_dbscan_timers; + auto timer_start = [&exec_space, verbose](Kokkos::Timer &timer) { + if (verbose) + exec_space.fence(); + timer.reset(); + }; + auto timer_seconds = [&exec_space, verbose](Kokkos::Timer const &timer) { + if (verbose) + exec_space.fence(); + return timer.seconds(); + }; + + timer_start(timer_total); + + auto labels = ArborX::dbscan(exec_space, primitives, eps, core_min_size, + ArborX::DBSCAN::Parameters() + .setPrintTimers(print_dbscan_timers) + .setImplementation(implementation)); + + timer_start(timer); + Kokkos::View cluster_indices("Testing::cluster_indices", + 0); + Kokkos::View cluster_offset("Testing::cluster_offset", 0); + sortAndFilterClusters(exec_space, labels, cluster_indices, cluster_offset, + cluster_min_size); + elapsed["cluster"] = timer_seconds(timer); + elapsed["total"] = timer_seconds(timer_total); + + printf("-- postprocess : %10.3f\n", elapsed["cluster"]); + printf("total time : %10.3f\n", elapsed["total"]); + + int num_clusters = cluster_offset.size() - 1; + int num_cluster_points = cluster_indices.size(); + printf("\n#clusters : %d\n", num_clusters); + printf("#cluster points : %d [%.2f%%]\n", num_cluster_points, + (100.f * num_cluster_points / data.size())); + + if (verify) + { + auto passed = ArborX::Details::verifyDBSCAN(exec_space, primitives, eps, + core_min_size, labels); + printf("Verification %s\n", (passed ? "passed" : "failed")); + } + + if (!filename_labels.empty()) + writeLabelsData(filename_labels, labels); + + if (print_sizes_centers) + printClusterSizesAndCenters(exec_space, primitives, cluster_indices, + cluster_offset); + + return EXIT_SUCCESS; +} diff --git a/arborx/examples/dbscan/input.txt b/arborx/examples/dbscan/input.txt new file mode 100644 index 000000000..f04abedd9 --- /dev/null +++ b/arborx/examples/dbscan/input.txt @@ -0,0 +1,9 @@ +8 2 +3 2 +0 0 +0 1 +1 1 +1 0 +2 2 +2 3 +3 3 diff --git a/arborx/examples/raytracing/CMakeLists.txt b/arborx/examples/raytracing/CMakeLists.txt new file mode 100644 index 000000000..27e6e6ce0 --- /dev/null +++ b/arborx/examples/raytracing/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(ArborX_RayTracing.exe example_raytracing.cpp) +target_link_libraries(ArborX_RayTracing.exe ArborX::ArborX Boost::program_options) +add_test(NAME ArborX_RayTracing_Example COMMAND ./ArborX_RayTracing.exe --spheres=1000 --rays=50000 --L=200.0) diff --git a/arborx/examples/raytracing/example_raytracing.cpp b/arborx/examples/raytracing/example_raytracing.cpp new file mode 100644 index 000000000..e435d5f32 --- /dev/null +++ b/arborx/examples/raytracing/example_raytracing.cpp @@ -0,0 +1,200 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include +#include +#include + +#include + +#include + +#include +#include + +template +struct SpheresToBoxes +{ + Kokkos::View _spheres; +}; + +template +struct ArborX::AccessTraits, ArborX::PrimitivesTag> +{ + using memory_space = MemorySpace; + + KOKKOS_FUNCTION static std::size_t + size(const SpheresToBoxes &stob) + { + return stob._spheres.extent(0); + } + KOKKOS_FUNCTION static ArborX::Box + get(SpheresToBoxes const &stobs, std::size_t const i) + { + auto const &sphere = stobs._spheres(i); + auto const &c = sphere.centroid(); + auto const r = sphere.radius(); + return {{c[0] - r, c[1] - r, c[2] - r}, {c[0] + r, c[1] + r, c[2] + r}}; + } +}; + +template +struct Rays +{ + Kokkos::View _rays; +}; + +template +struct ArborX::AccessTraits, ArborX::PredicatesTag> +{ + using memory_space = MemorySpace; + + KOKKOS_FUNCTION static std::size_t size(const Rays &rays) + { + return rays._rays.extent(0); + } + KOKKOS_FUNCTION static auto get(Rays const &rays, std::size_t i) + { + return attach(intersects(rays._rays(i)), (int)i); + } +}; + +template +struct AccumRaySphereInterDist +{ + Kokkos::View _spheres; + Kokkos::View _accumulator; + + template + KOKKOS_FUNCTION void operator()(Predicate const &predicate, + int const primitive_index) const + { + auto const &ray = ArborX::getGeometry(predicate); + auto const &sphere = _spheres(primitive_index); + + float const length = overlapDistance(ray, sphere); + int const i = getData(predicate); + + Kokkos::atomic_fetch_add(&_accumulator(i), length); + } +}; + +int main(int argc, char *argv[]) +{ + using ExecutionSpace = Kokkos::DefaultExecutionSpace; + using MemorySpace = ExecutionSpace::memory_space; + + Kokkos::ScopeGuard guard(argc, argv); + + namespace bpo = boost::program_options; + + int num_spheres; + int num_rays; + float L; + + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "help message" ) + ("spheres", bpo::value(&num_spheres)->default_value(100), "number of spheres") + ("rays", bpo::value(&num_rays)->default_value(10000), "number of rays") + ("L", bpo::value(&L)->default_value(100.0), "size of the domain") + ; + // clang-format on + bpo::variables_map vm; + bpo::store(bpo::command_line_parser(argc, argv).options(desc).run(), vm); + bpo::notify(vm); + + if (vm.count("help") > 0) + { + std::cout << desc << '\n'; + return 1; + } + + std::cout << "ArborX version: " << ArborX::version() << std::endl; + std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; + + std::uniform_real_distribution uniform{0.0, 1.0}; + std::default_random_engine gen; + auto rand_uniform = [&]() { return uniform(gen); }; + + // Random parameters for Gaussian distribution of radii + const float mu_R = 1.0; + const float sigma_R = mu_R / 3.0; + + std::normal_distribution<> normal{mu_R, sigma_R}; + auto rand_normal = [&]() { return std::max(normal(gen), 0.0); }; + + // Construct spheres + // + // The centers of spheres are uniformly sampling the domain. The radii of + // spheres have Gaussian (mu_R, sigma_R) sampling. + Kokkos::View spheres( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "spheres"), num_spheres); + auto spheres_host = Kokkos::create_mirror_view(spheres); + for (int i = 0; i < num_spheres; ++i) + { + spheres_host(i) = { + {rand_uniform() * L, rand_uniform() * L, rand_uniform() * L}, + rand_normal()}; + } + Kokkos::deep_copy(spheres, spheres_host); + + // Construct rays + // + // The origins of rays are uniformly sampling the bottom surface of the + // domain. The direction vectors are uniformly sampling of a cosine-weighted + // hemisphere, It requires expressing the direction vector in the spherical + // coordinates as: + // {sinpolar * cosazimuth, sinpolar * sinazimuth, cospolar} + // A detailed description can be found in the slides here (slide 47): + // https://cg.informatik.uni-freiburg.de/course_notes/graphics2_08_renderingEquation.pdf + Kokkos::View rays( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "rays"), num_rays); + auto rays_host = Kokkos::create_mirror_view(rays); + + for (int i = 0; i < num_rays; ++i) + { + float xi_1 = rand_uniform(); + float xi_2 = rand_uniform(); + + rays_host(i) = {ArborX::Point{rand_uniform() * L, rand_uniform() * L, 0.f}, + ArborX::Experimental::Vector{ + float(std::cos(2 * M_PI * xi_2) * std::sqrt(xi_1)), + float(std::sin(2 * M_PI * xi_2) * std::sqrt(xi_1)), + std::sqrt(1.f - xi_1)}}; + } + Kokkos::deep_copy(rays, rays_host); + + Kokkos::Timer timer; + + ExecutionSpace exec_space{}; + + exec_space.fence(); + timer.reset(); + ArborX::BVH bvh{exec_space, + SpheresToBoxes{spheres}}; + + Kokkos::View accumulator("accumulator", num_rays); + bvh.query(exec_space, Rays{rays}, + AccumRaySphereInterDist{spheres, accumulator}); + exec_space.fence(); + auto time = timer.seconds(); + + auto accumulator_avg = + ArborX::accumulate(exec_space, accumulator, 0.f) / num_rays; + + printf("time : %.3f [%.3fM ray/sec]\n", time, + num_rays / (1000000 * time)); + printf("ray avg : %.3f\n", accumulator_avg); + + return EXIT_SUCCESS; +} diff --git a/arborx/examples/simple_intersection/CMakeLists.txt b/arborx/examples/simple_intersection/CMakeLists.txt new file mode 100644 index 000000000..5b76eee46 --- /dev/null +++ b/arborx/examples/simple_intersection/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(ArborX_Intersection.exe example_intersection.cpp) +target_link_libraries(ArborX_Intersection.exe ArborX::ArborX) +add_test(NAME ArborX_Intersection_Example COMMAND ./ArborX_Intersection.exe) diff --git a/arborx/examples/simple_intersection/example_intersection.cpp b/arborx/examples/simple_intersection/example_intersection.cpp new file mode 100644 index 000000000..e30aad98b --- /dev/null +++ b/arborx/examples/simple_intersection/example_intersection.cpp @@ -0,0 +1,168 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include + +#include + +// Perform intersection queries using the same objects for the queries as the +// objects used in BVH construction that are located on a regular spaced +// three-dimensional grid. +// Each box will only intersect with itself. +// +// i-2 i-1 i i+1 +// +// o o o o j+1 +// --- +// o o | x | o j +// --- +// o o o o j-1 +// +// o o o o j-2 +// + +template +class Boxes +{ +public: + // Create non-intersecting boxes on a 3D cartesian grid + // used both for queries and predicates. + Boxes(typename DeviceType::execution_space const &execution_space) + { + float Lx = 100.0; + float Ly = 100.0; + float Lz = 100.0; + int nx = 11; + int ny = 11; + int nz = 11; + int n = nx * ny * nz; + float hx = Lx / (nx - 1); + float hy = Ly / (ny - 1); + float hz = Lz / (nz - 1); + + auto index = [nx, ny](int i, int j, int k) { + return i + j * nx + k * (nx * ny); + }; + + _boxes = Kokkos::View( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "boxes"), n); + auto boxes_host = Kokkos::create_mirror_view(_boxes); + + for (int i = 0; i < nx; ++i) + for (int j = 0; j < ny; ++j) + for (int k = 0; k < nz; ++k) + { + ArborX::Point p_lower{ + {(i - .25) * hx, (j - .25) * hy, (k - .25) * hz}}; + ArborX::Point p_upper{ + {(i + .25) * hx, (j + .25) * hy, (k + .25) * hz}}; + boxes_host[index(i, j, k)] = {p_lower, p_upper}; + } + Kokkos::deep_copy(execution_space, _boxes, boxes_host); + } + + // Return the number of boxes. + KOKKOS_FUNCTION int size() const { return _boxes.size(); } + + // Return the box with index i. + KOKKOS_FUNCTION const ArborX::Box &get_box(int i) const { return _boxes(i); } + +private: + Kokkos::View _boxes; +}; + +// For creating the bounding volume hierarchy given a Boxes object, we +// need to define the memory space, how to get the total number of objects, +// and how to access a specific box. Since there are corresponding functions in +// the Boxes class, we just resort to them. +template +struct ArborX::AccessTraits, ArborX::PrimitivesTag> +{ + using memory_space = typename DeviceType::memory_space; + static KOKKOS_FUNCTION int size(Boxes const &boxes) + { + return boxes.size(); + } + static KOKKOS_FUNCTION auto get(Boxes const &boxes, int i) + { + return boxes.get_box(i); + } +}; + +// For performing the queries given a Boxes object, we need to define memory +// space, how to get the total number of queries, and what the query with index +// i should look like. Since we are using self-intersection (which boxes +// intersect with the given one), the functions here very much look like the +// ones in ArborX::AccessTraits, ArborX::PrimitivesTag>. +template +struct ArborX::AccessTraits, ArborX::PredicatesTag> +{ + using memory_space = typename DeviceType::memory_space; + static KOKKOS_FUNCTION int size(Boxes const &boxes) + { + return boxes.size(); + } + static KOKKOS_FUNCTION auto get(Boxes const &boxes, int i) + { + return intersects(boxes.get_box(i)); + } +}; + +// Now that we have encapsulated the objects and queries to be used within the +// Boxes class, we can continue with performing the actual search. +int main() +{ + Kokkos::initialize(); + { + using ExecutionSpace = Kokkos::DefaultExecutionSpace; + using MemorySpace = typename ExecutionSpace::memory_space; + using DeviceType = Kokkos::Device; + ExecutionSpace execution_space; + + std::cout << "Create grid with bounding boxes" << '\n'; + Boxes boxes(execution_space); + std::cout << "Bounding boxes set up." << '\n'; + + std::cout << "Creating BVH tree." << '\n'; + ArborX::BVH const tree(execution_space, boxes); + std::cout << "BVH tree set up." << '\n'; + + std::cout << "Starting the queries." << '\n'; + // The query will resize indices and offsets accordingly + Kokkos::View indices("indices", 0); + Kokkos::View offsets("offsets", 0); + + ArborX::query(tree, execution_space, boxes, indices, offsets); + std::cout << "Queries done." << '\n'; + + std::cout << "Starting checking results." << '\n'; + auto offsets_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, offsets); + auto indices_host = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, indices); + + unsigned int const n = boxes.size(); + if (offsets_host.size() != n + 1) + Kokkos::abort("Wrong dimensions for the offsets View!\n"); + for (int i = 0; i < static_cast(n + 1); ++i) + if (offsets_host(i) != i) + Kokkos::abort("Wrong entry in the offsets View!\n"); + + if (indices_host.size() != n) + Kokkos::abort("Wrong dimensions for the indices View!\n"); + for (int i = 0; i < static_cast(n); ++i) + if (indices_host(i) != i) + Kokkos::abort("Wrong entry in the indices View!\n"); + std::cout << "Checking results successful." << '\n'; + } + + Kokkos::finalize(); +} diff --git a/arborx/examples/viz/CMakeLists.txt b/arborx/examples/viz/CMakeLists.txt new file mode 100644 index 000000000..0c637903f --- /dev/null +++ b/arborx/examples/viz/CMakeLists.txt @@ -0,0 +1,7 @@ +if(Kokkos_ENABLE_SERIAL) + add_executable(ArborX_TreeViz.exe tree_visualization.cpp) + target_link_libraries(ArborX_TreeViz.exe ArborX::ArborX Boost::program_options) + add_test(NAME ArborX_TreeViz_Example COMMAND ./ArborX_TreeViz.exe) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/leaf_cloud.txt ${CMAKE_CURRENT_BINARY_DIR}/leaf_cloud.txt COPYONLY) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/arborx_query_sort.py ${CMAKE_CURRENT_BINARY_DIR}/arborx_query_sort.py COPYONLY) +ENDIF() diff --git a/arborx/examples/viz/arborx_query_sort.py b/arborx/examples/viz/arborx_query_sort.py new file mode 100755 index 000000000..5ab86b2ba --- /dev/null +++ b/arborx/examples/viz/arborx_query_sort.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""arborx_query_sort.py + +Usage: + arborx_query_sort.py -a ALGO -p PREFIX [-d] [-o OUTPUT_FILE] + arborx_query_sort.py (-h | --help) + +Options: + -h --help Show this screen. + -a ALGO --algo=ALGO Query order ['untouched', 'shuffled', 'sorted'] + -d --display Display mode + -o FILE --output-file=FILE Output file [default: plot.png] + -p PREFIX --prefix=PREFIX Input file prefix +""" +from docopt import docopt +import subprocess +import numpy +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap + + +def read_traversal(filename): + nearest_traversal = [] + replace = { + '[internal]': '[style=filled, color=black]', + '[leaf]': '[style=filled, color=white]', + '[result]': '[style=filled, color=black]', + } + with open(filename) as fin: + def is_commented(line): + return line.lstrip(' ').startswith('//') + + def is_edge(line): + return line.count('->') > 0 + + for line in fin: + for old, new in replace.items(): + line = line.replace(old, new) + line = line.rstrip('\n') + if line and not is_commented(line) and not is_edge(line): + nearest_traversal.append(line) + return nearest_traversal + + +def read_tree(filename): + tree = [] + replace = { + '[internal]': '[style=filled, color=red]', + '[leaf]': '[style=filled, color=green]', + '[edge]': '', + '[pendant]': '', + } + with open(filename) as fin: + for line in fin: + line = line.rstrip('\n') + for old, new in replace.items(): + line = line.replace(old, new) + tree.append(line) + tree.insert(0, 'digraph g {') + tree.insert(1, ' root = i0;') + tree.append('}') + return tree + + +def append_traversal_to_tree(tree, traversal): + for line in traversal: + tree.insert(-1, line) + return tree + + +if __name__ == '__main__': + # Process input + options = docopt(__doc__) + + algo = options['--algo'] + display = options['--display'] + prefix = options['--prefix'] + output_file = options['--output-file'] + + matrix = {} + + n = int(subprocess.check_output('ls -1 ' + prefix + '*' + algo + '* ' + '| wc -l', shell=True)) + assert n > 0, 'Could not find any matching files' + + matrix = numpy.zeros((n, n-1)) + for i in range(n): + with open(prefix + '%s_%s_nearest_traversal.dot.m4' + % (algo, i)) as fin: + def is_commented(line): + return line.lstrip(' ').startswith('//') + + def is_edge(line): + return line.count('->') > 0 + + count = 0 + for line in fin: + if line and not is_commented(line) and not is_edge(line): + count += 1 + line = line.lstrip(' ') + entry = int(line[1:line.find(' ')]) + if line[0] is 'i': + matrix[i, entry] = 1 + elif line[0] is 'l': + # Uncomment for full matrix with leaves + # matrix[i, n-1 + entry] = 1 + continue + else: + raise RuntimeError() + + cmap = ListedColormap(['w', 'r', 'k']) + + plt.matshow(matrix, cmap=cmap) + plt.tight_layout() + + if display: + plt.show() + else: + if output_file == "": + output_file = algo + '.png' + plt.savefig(output_file, bbox_inches='tight') diff --git a/arborx/examples/viz/leaf_cloud.txt b/arborx/examples/viz/leaf_cloud.txt new file mode 100644 index 000000000..d2e220d11 --- /dev/null +++ b/arborx/examples/viz/leaf_cloud.txt @@ -0,0 +1,59 @@ +58 +0.5 0.1 0 +0.5 0 0 +-0.2 0.4 0 +-0.3 0.4 0 +-0.5 0.6 0 +-0.4 0.6 0 +-0.9 0.8 0 +-2 0.8 0 +-2.4 1 0 +-2.3 1.2 0 +-1.9 1.3 0 +-1.1 1.3 0 +-1.1 1.6 0 +-1.9 1.6 0 +-2.4 1.6 0 +-2.6 1.8 0 +-2.4 2 0 +-2.7 2.3 0 +-2.6 2.5 0 +-1.7 2.3 0 +-1.6 2.5 0 +-1.7 2.7 0 +-2.6 3 0 +-2.5 3.3 0 +-2.1 3.2 0 +-2 3.3 0 +-2.2 3.6 0 +-1.9 3.8 0 +-1.4 3.4 0 +-1.1 3.7 0 +-0.95 3.7 0 +-1 2.9 0 +-0.9 2.7 0 +-0.5 3.3 0 +-0.3 3.5 0 +-0.1 3.4 0 +-0.15 3.2 0 +0.05 3.1 0 +0.2 3.15 0 +0.35 3.1 0 +0.15 2.9 0 +-0.1 2.7 0 +-0.2 2.5 0 +-0.25 2.2 0 +0.25 2.5 0 +0.45 2.5 0 +0.5 2.3 0 +0.1 2 0 +0.2 1.7 0 +0.8 2 0 +1.1 1.9 0 +1.1 1.5 0 +0.1 1.2 0 +-0.5 1 0 +-2.2 1.7 0 +-2.3 2.2 0 +-0.5 2.2 0 +-0 1.7 0 diff --git a/arborx/examples/viz/requirements.txt b/arborx/examples/viz/requirements.txt new file mode 100644 index 000000000..c45223e74 --- /dev/null +++ b/arborx/examples/viz/requirements.txt @@ -0,0 +1,3 @@ +docopt +matplotlib +numpy diff --git a/arborx/examples/viz/tree_visualization.cpp b/arborx/examples/viz/tree_visualization.cpp new file mode 100644 index 000000000..6f5ca14d5 --- /dev/null +++ b/arborx/examples/viz/tree_visualization.cpp @@ -0,0 +1,182 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +// FIXME: this is a temporary place for loadPointCloud and writePointCloud +// Right now, only loadPointCloud is being used, and only in this example +template +void loadPointCloud(std::string const &filename, + Kokkos::View &random_points) +{ + std::ifstream file(filename); + if (file.is_open()) + { + int size = -1; + file >> size; + ARBORX_ASSERT(size > 0); + Kokkos::realloc(random_points, size); + auto random_points_host = Kokkos::create_mirror_view(random_points); + for (int i = 0; i < size; ++i) + for (int j = 0; j < 3; ++j) + file >> random_points(i)[j]; + Kokkos::deep_copy(random_points, random_points_host); + } + else + { + throw std::runtime_error("Cannot open file"); + } +} + +template +void writePointCloud( + Kokkos::View random_points, + std::string const &filename) + +{ + static_assert( + KokkosExt::is_accessible_from_host::value, + "The View should be accessible on the Host"); + std::ofstream file(filename); + if (file.is_open()) + { + unsigned int const n = random_points.extent(0); + for (unsigned int i = 0; i < n; ++i) + file << random_points(i)[0] << " " << random_points(i)[1] << " " + << random_points(i)[2] << "\n"; + file.close(); + } +} + +template +void printPointCloud(View points, std::ostream &os) +{ + auto const n = points.extent_int(0); + for (int i = 0; i < n; ++i) + os << "\\node[leaf] at (" << points(i)[0] << "," << points(i)[1] + << ") {\\textbullet};\n"; +} + +void viz(std::string const &prefix, std::string const &infile, int n_neighbors) +{ + using ExecutionSpace = Kokkos::DefaultHostExecutionSpace; + using DeviceType = ExecutionSpace::device_type; + Kokkos::View points("points", 0); + loadPointCloud(infile, points); + + ArborX::BVH bvh{ExecutionSpace{}, points}; + + using TreeVisualization = + typename ArborX::Details::TreeVisualization; + using TikZVisitor = typename TreeVisualization::TikZVisitor; + using GraphvizVisitor = typename TreeVisualization::GraphvizVisitor; + + int const n_queries = bvh.size(); + if (n_neighbors < 0) + n_neighbors = bvh.size(); + Kokkos::View *, DeviceType> queries("queries", + n_queries); + Kokkos::parallel_for(Kokkos::RangePolicy(0, n_queries), + KOKKOS_LAMBDA(int i) { + queries(i) = ArborX::nearest(points(i), n_neighbors); + }); + + auto performQueries = [&bvh, &queries](std::string const &p, + std::string const &s) { + std::ofstream fout; + for (int i = 0; i < queries.extent_int(0); ++i) + { + std::string const fname = p + std::to_string(i) + s; + fout.open(fname, std::fstream::out); + TreeVisualization::visit(bvh, queries(i), GraphvizVisitor{fout}); + fout.close(); + } + }; + + std::fstream fout; + + // Print the point cloud + fout.open(prefix + "points.tex", std::fstream::out); + printPointCloud(points, fout); + fout.close(); + + // Print the bounding volume hierarchy + fout.open(prefix + "bounding_volumes.tex", std::fstream::out); + TreeVisualization::visitAllIterative(bvh, TikZVisitor{fout}); + fout.close(); + + // Print the entire tree + fout.open(prefix + "tree_all_nodes_and_edges.dot.m4", std::fstream::out); + TreeVisualization::visitAllIterative(bvh, GraphvizVisitor{fout}); + fout.close(); + + std::string const suffix = "_nearest_traversal.dot.m4"; + performQueries(prefix + "untouched_", suffix); + + // Shuffle the queries + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(queries.data(), queries.data() + queries.size(), g); + performQueries(prefix + "shuffled_", suffix); + + // Sort them + auto permute = + ArborX::Details::BatchedQueries::sortQueriesAlongZOrderCurve( + ExecutionSpace{}, bvh.bounds(), queries); + queries = ArborX::Details::BatchedQueries::applyPermutation( + ExecutionSpace{}, permute, queries); + performQueries(prefix + "sorted_", suffix); +} + +int main(int argc, char *argv[]) +{ + Kokkos::InitArguments args; + args.disable_warnings = true; + Kokkos::ScopeGuard guard(args); + + std::string prefix; + std::string infile; + int n_neighbors; + boost::program_options::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "produce help message" ) + ( "prefix", boost::program_options::value (&prefix)->default_value("viz_"), "set prefix for output files" ) + ( "infile", boost::program_options::value (&infile)->default_value("leaf_cloud.txt"), "set input point cloud file" ) + ( "neighbors", boost::program_options::value (&n_neighbors)->default_value(5), "set the number of neighbors to search for (negative value means all)" ) + ; + // clang-format on + + boost::program_options::variables_map vm; + boost::program_options::store( + boost::program_options::parse_command_line(argc, argv, desc), vm); + boost::program_options::notify(vm); + + if (vm.count("help") > 0) + { + std::cout << desc << "\n"; + return 1; + } + + viz(prefix, infile, n_neighbors); + + return 0; +} diff --git a/arborx/scripts/benchmark.py b/arborx/scripts/benchmark.py new file mode 100644 index 000000000..3c4dfc619 --- /dev/null +++ b/arborx/scripts/benchmark.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +import json +import re + +def load_json(filename): + with open(filename) as f: + try: + # Try loading a file using JSON reader + data = json.loads(f.read())['benchmarks'] + except ValueError: + # Try loading a file using ASCII reader + data = [] + f.seek(0) # reset the file as it was messed up above + try: + for line in f: + # Try to parse only the lines starting with "BM_" + if re.search('^BM_', line) != None: + for timer in ['manual_time', 'manual_time_mean', 'manual_time_median', 'manual_time_stddev']: + m = re.search('(^.*/' + timer + ') .*rate=([0-9.]*)(.*)$', line) + if m != None: + name = m.group(1) + rate = float(m.group(2)) + rate_scale = m.group(3) + if rate_scale == 'k/s': + rate = rate * 1000 + elif rate_scale == 'M/s': + rate = rate * 1000000 + else: + raise Exception('Unknown rate scaling: "' + rate_scale + '"') + data.append({'name' : name, 'rate' : rate}) + break + + except RuntimeError as e: + raise RuntimeError("Caught an error while parsing on line " + str(lineno) + ": " + e.args[0]) + + return data + +allowed_geometries = ['filled_box', 'hollow_box'] +allowed_algorithms = ['construction', 'radius_search', 'radius_callback_search', 'knn_search', 'knn_callback_search'] + +# Geometry is only checked for the source cloud +# We do not check target cloud geometry as they come in pairs: +# filled_box/filled_sphere or hollow_box/hollow_sphere +def parse_benchmark(json_data, algorithm, implementation, timer_aggregate, geometry, num_radius_passes = 2): + if geometry not in allowed_geometries: + raise Exception('Unknown geometry: "' + geometry + '"') + if algorithm not in allowed_algorithms: + raise Exception('Unknown algorithm: "' + algorithm + '"') + + geometries = { 'filled_box' : '0', 'hollow_box' : '1', 'filled_sphere' : '2', 'hollow_sphere' : '3' } + + # Escape () in implementation string + implementation = implementation.translate(str.maketrans({"(" : "\(", ")" : "\)", "%" : "\%" })) + + sorted = 1 + + # Templates: + # BM_construction>/_num_primitives_/_source_point_cloud_type_ + construction_template = algorithm + '<.*' + implementation + '[^/]*/([^/]*)/' + geometries[geometry] + '/' + # BM_knn_search>/_num_primitives_/_num_predicates_/_n_neighbors_/_sort_predicate_/_source_point_cloud_type_/_target_point_cloud_type_ + knn_template = algorithm + '<.*' + implementation + '[^/]*/([^/]*)/([^/]*)/[^/]*/' + str(sorted) + '/' + geometries[geometry] + '/' + # BM_knn_search>/_num_primitives_/_num_predicates_/_n_neighbors_/_sort_predicates_/_buffer_size_/_source_point_cloud_type_/_target_point_cloud_type_ + # we consider (2P) to be represented by buffer_size = 0 + radius_template_1P = algorithm + '<.*' + implementation + '[^/]*/([^/]*)/([^/]*)/[^/]*/' + str(sorted) + '/[^0][^/]+/' + geometries[geometry] + '/' + radius_template_2P = algorithm + '<.*' + implementation + '[^/]*/([^/]*)/([^/]*)/[^/]*/' + str(sorted) + '/0/' + geometries[geometry] + '/' + + num_primitives = [] + num_predicates = [] + rates = [] + + for benchmark in json_data: + name = benchmark['name'] + + # For Google Benchmark v1.5, one could do + # if benchmark['run_type'] != 'aggregate' or benchmark['aggregate_name'] != timer_aggregate: + # continue + if re.search(timer_aggregate, name) == None: + continue + + if algorithm == 'construction' and re.search(algorithm, name) != None: + m = re.search(construction_template, name) + if m != None: + num_primitives.append(int(m.group(1))) + rates.append(benchmark['rate']) + + elif (algorithm == 'knn_search' or algorithm == 'knn_callback_search') and re.search(algorithm, name) != None: + m = re.search(knn_template, name) + if m != None: + num_primitives.append(int(m.group(1))) + num_predicates.append(int(m.group(2))) + rates.append(benchmark['rate']) + + elif (algorithm == 'radius_search' or algorithm == 'radius_callback_search') and re.search(algorithm, name) != None: + m = re.search(radius_template_2P if num_radius_passes == 2 else radius_template_1P, name) + if m != None: + num_primitives.append(int(m.group(1))) + num_predicates.append(int(m.group(2))) + rates.append(benchmark['rate']) + + return num_primitives, num_predicates, rates diff --git a/arborx/scripts/benchmark_plot.py b/arborx/scripts/benchmark_plot.py new file mode 100755 index 000000000..3f3dd2140 --- /dev/null +++ b/arborx/scripts/benchmark_plot.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""benchmark_plot.py + +Usage: + benchmark_plot.py -a ALGORITHM [-b BACKENDS...] -g GEOMETRY -i INPUT... [-n] [-e] [-t TITLE] [-o OUTPUT_FILE] + benchmark_plot.py (-h | --help) + +Options: + -h --help Show this screen. + -a ALGORITHM --algo ALGORITHM Algorithm ['construction', 'radius_search', 'radius_callback_search', 'knn_search', 'knn_callback_search'] + -b BACKENDS --backends BACKENDS Backends to parse [default: all] + -g GEOMETRY --geometry GEOMETRY Geometry (source cloud) ['filled_box', 'hollow_box'] + -i FILE --input-files=FILE Input file(s) containing benchmark results in JSON format + -n --numbers Plot numbers [default: False] + -e --errors Plot error bars [default: False] + -o FILE --output-file=FILE Output file + -t TITLE --title=TITLE Plot title +""" +import matplotlib.pyplot as plt +import numpy as np +import math +import re +from docopt import docopt + +from benchmark import load_json, parse_benchmark, allowed_algorithms, allowed_geometries + +def find_available_backends(input_jsons): + arborx_backend_regex = "[^<]*]*)>.*" + + backends = set() + for input_json in input_jsons: + for benchmark in input_json: + search_result = re.search(arborx_backend_regex, benchmark['name']) + if search_result != None: + backends.add(search_result.group(1)) + backends = list(backends) + backends.sort() + + return backends + +class NotMatchingValuesAndPredicatesSizesException(Exception): + def __init__(self, message, i, backend): + super().__init__(message) + self.i = i + self.backend = backend + +def populate_data(input_jsons, backends): + all_num_primitives = [] + all_rates = [] + all_errors = [] + + for i in range(len(backends)): + # For every backend, find all (num_primitives, rate, error) in all input files + backend = backends[i] + + backend_num_primitives = [] + backend_rates = [] + backend_errors = [] + + implementation = 'ArborX::BVH<' + backend + '>' + for i in range(len(input_jsons)): + file_num_primitives, file_num_predicates, file_rates = parse_benchmark(input_jsons[i], algorithm, implementation, 'median', geometry) + _, _, file_errors = parse_benchmark(input_jsons[i], algorithm, implementation, 'stddev', geometry) + if algorithm != 'construction' and file_num_primitives != file_num_predicates: + raise NotMatchingValuesAndPredicatesSizesException("", i, backend) + + backend_num_primitives = backend_num_primitives + file_num_primitives + backend_rates = backend_rates + file_rates + backend_errors = backend_errors + file_errors + + all_num_primitives.append(backend_num_primitives) + all_rates.append(backend_rates) + all_errors.append(backend_errors) + + num_unique_primitives = [] + for i in range(len(all_num_primitives)): + num_unique_primitives = num_unique_primitives + all_num_primitives[i] + num_unique_primitives = list(set(num_unique_primitives)) + num_unique_primitives.sort() + + return num_unique_primitives, all_num_primitives, all_rates, all_errors, + +def backends_comparison_rate_figure(input_files, algorithm, geometry, backends, plot_numbers = False, plot_errors = True): + # Read in files to produce JSON + input_jsons = [] + for input_file in input_files: + input_jsons.append(load_json(input_file)) + + # Phase 0: parse backends + backends = backends + if backends[0] == "all": + # Figure out a list of available backends by populating a set by + # searching a regular expression in all input files + backends = find_available_backends(input_jsons) + + # Phase 1: data setup + try: + num_unique_primitives, all_num_primitives, all_rates, all_errors = populate_data(input_jsons, backends) + except NotMatchingValuesAndPredicatesSizesException as e: + raise Exception('The number of predicates does not match the number of primitives for "' + e.backend + '" in "' + input_files[e.i] + "'") + n_unique = len(num_unique_primitives) + + # If the same data point (algorithm, backend, num_sources) is present in + # multiple files, choose only the last one for plotting + y_scaling = 10**6 + rates = np.zeros([len(backends), n_unique]) + errors = np.zeros([len(backends), 2, n_unique]) + for i in range(0, len(backends)): + for j in range(0, len(all_num_primitives[i])): + index = num_unique_primitives.index(all_num_primitives[i][j]) + rates[i, index] = all_rates[i][j]/y_scaling + errors[i, 0, index] = 1 - all_errors[i][j]/all_rates[i][j] + errors[i, 1, index] = 1 + all_errors[i][j]/all_rates[i][j] + + # Phase 2: plot generation + plt.figure(figsize=(max(n_unique, 6), 5)) + + ax = plt.subplot(111) + patterns = ('/', 'x', '\\', '.', 'o', 'O') + + def autolabel(rects): + """Attach a text label above each bar in *rects*, displaying its height.""" + for rect in rects: + height = round(rect.get_height(),1) + ax.annotate('{}'.format(height), + xy=(rect.get_x() + rect.get_width() / 2, rect.get_height()), + xytext=(0, 3), # 3 points vertical offset + textcoords="offset points", + ha='center', va='bottom', + fontsize=18) + + # Create TeX-style label + def primitive_label(n): + e = math.floor(math.log(n + 1, 10)) + r = math.floor(n/10**e) + return (str(r) + '$\cdot $' if r > 1 else '') + '$10^' + str(e) + '$' + + width = 0.6 + scale = 1/len(backends) + for i in range(0, len(backends)): + barplot = ax.bar(np.arange(n_unique) + scale*(i-.5*(len(backends)-1)) * width, + width=scale*width*np.ones(n_unique), height=rates[i, :], + yerr=(errors[i, :] if plot_errors == True else None), + capsize=5, hatch=patterns[i % len(patterns)], + edgecolor='k', + error_kw=dict(ecolor='gray', lw=2, capsize=5, capthick=2), + label=backends[i]) + + if plot_numbers: + autolabel(barplot) + + ax.legend(loc='upper left', fontsize=20) + + ax.set_xlabel('Number of indexed objects', fontsize=20) + plt.xticks(fontsize=18) + ax.set_xticks(np.arange(n_unique)) + ax.set_xticklabels([primitive_label(size) for size in num_unique_primitives]) + ax.tick_params(axis='x', which='major', bottom='off', top='off') + ax.set_xlim([-0.5, n_unique - 0.5]) + + ax.set_ylabel('Rate (million queries/second)', fontsize=20) + plt.yticks(fontsize=18) + ax.yaxis.grid('on') + ax.set_ylim([0, 1.1*ax.get_ylim()[1]]) + +if __name__ == '__main__': + + # Process input + options = docopt(__doc__) + + algorithm = options['--algo'] + backends = options['--backends'] + geometry = options['--geometry'] + input_files = options['--input-files'] + output_file = options['--output-file'] + plot_numbers = options['--numbers'] + plot_errors = options['--errors'] + plot_title = options['--title'] + + # Check input + if geometry not in allowed_geometries: + raise Exception('Unknown geometry: "' + geometry + '". Allowed values are ' + str(allowed_geometries)) + + if algorithm not in allowed_algorithms: + raise Exception('Unknown algorithm: "' + algorithm + '". Allowed values are ' + str(allowed_algorithms)) + + backends_comparison_rate_figure(input_files, algorithm=algorithm, geometry=geometry, + backends=backends, plot_numbers=plot_numbers, plot_errors=plot_errors) + + if plot_title != None: + plt.title(plot_title, fontsize=18) + else: + plt.title(algorithm, fontsize=18) + + plt.tight_layout() + if output_file: + plt.savefig(output_file, bbox_inches='tight', dpi=600) + else: + plt.show() diff --git a/arborx/scripts/check_format_cpp.sh b/arborx/scripts/check_format_cpp.sh new file mode 100755 index 000000000..f96e8027d --- /dev/null +++ b/arborx/scripts/check_format_cpp.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +clang_format_executable=${CLANG_FORMAT_EXE:-clang-format} + +this_program=$(basename "$0") +usage="Usage: + $this_program [options] -- check format of the C++ source files + +Options: + -h --help Print help and exit + -q --quiet Quiet mode (do not print the diff) + -p --apply-patch Apply diff patch to the source files" + +verbose=1 +apply_patch=0 + +#echo "Arguments: $# $@" + +while [ $# -gt 0 ] +do + case $1 in + -p|--apply-patch) + apply_patch=1 + ;; + -q|--quiet) + verbose=0 + ;; + -h|--help) + echo "$usage" + exit 0 + ;; + *) + echo "$this_program: Unknown argument '$1'. See '$this_program --help'." + exit -1 + ;; + esac + + shift +done + +# stop right here if clang-format does not exist in $PATH +command -v $clang_format_executable >/dev/null 2>&1 || { echo >&2 "clang-format executable '$clang_format_executable' not found. Aborting."; exit 1; } + +# shamelessy redirecting everything to /dev/null in quiet mode +if [ $verbose -eq 0 ]; then + exec &>/dev/null +fi + +cpp_source_files=$(git ls-files | grep -E "\.hpp$|\.cpp$|\.h$|\.c$" | grep -v -f .clang-format-ignore) + +unformatted_files=() +for file in $cpp_source_files; do + diff -u \ + <(cat $file) \ + --label a/$file \ + <($clang_format_executable $file) \ + --label b/$file >&1 + if [ $? -eq 1 ]; then + unformatted_files+=($file) + fi +done + +n_unformatted_files=${#unformatted_files[@]} +if [ $n_unformatted_files -ne 0 ]; then + echo "${#unformatted_files[@]} file(s) not formatted properly:" + for file in ${unformatted_files[@]}; do + echo " $file" + if [ $apply_patch -eq 1 ]; then + $clang_format_executable -i $file + fi + done +else + echo "OK" +fi +exit $n_unformatted_files diff --git a/arborx/scripts/docker_cmake b/arborx/scripts/docker_cmake new file mode 100644 index 000000000..2c1c27604 --- /dev/null +++ b/arborx/scripts/docker_cmake @@ -0,0 +1,25 @@ +#!/bin/bash + +EXTRA_ARGS=("$@") + +rm -f CMakeCache.txt +rm -rf CMakeFiles/ + +ARGS=( + -D CMAKE_BUILD_TYPE=Debug + -D BUILD_SHARED_LIBS=ON + + ### TPLs + -D CMAKE_PREFIX_PATH="$KOKKOS_DIR;$BENCHMARK_DIR;$BOOST_DIR" + -D ARBORX_ENABLE_MPI=ON + + ### COMPILERS AND FLAGS ### + -D CMAKE_CXX_COMPILER_LAUNCHER=ccache + -D CMAKE_CXX_COMPILER="$KOKKOS_DIR/bin/nvcc_wrapper" + -D CMAKE_CXX_FLAGS="-Wall -Wextra -Wpedantic" + + ### MISC ### + -D MPIEXEC_PREFLAGS="--allow-run-as-root" +) + +cmake "${ARGS[@]}" "${EXTRA_ARGS[@]}" "${ARBORX_DIR}" diff --git a/arborx/scripts/requirements.txt b/arborx/scripts/requirements.txt new file mode 100644 index 000000000..c45223e74 --- /dev/null +++ b/arborx/scripts/requirements.txt @@ -0,0 +1,3 @@ +docopt +matplotlib +numpy diff --git a/arborx/src/ArborX.hpp b/arborx/src/ArborX.hpp new file mode 100644 index 000000000..05e7b22ba --- /dev/null +++ b/arborx/src/ArborX.hpp @@ -0,0 +1,28 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_HPP +#define ARBORX_HPP + +#include + +#include +#ifdef ARBORX_ENABLE_MPI +#include +#endif +#include +#include +#include +#include +#include +#include + +#endif diff --git a/arborx/src/ArborX_BruteForce.hpp b/arborx/src/ArborX_BruteForce.hpp new file mode 100644 index 000000000..5d595c4d2 --- /dev/null +++ b/arborx/src/ArborX_BruteForce.hpp @@ -0,0 +1,125 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_BRUTE_FORCE_HPP +#define ARBORX_BRUTE_FORCE_HPP + +#include +#include +#include +#include +#include + +#include + +namespace ArborX +{ + +template +class BruteForce +{ +public: + using memory_space = MemorySpace; + static_assert(Kokkos::is_memory_space::value, ""); + using size_type = typename MemorySpace::size_type; + using bounding_volume_type = Box; + + BruteForce() = default; + + template + BruteForce(ExecutionSpace const &space, Primitives const &primitives); + + KOKKOS_FUNCTION + size_type size() const noexcept { return _size; } + + KOKKOS_FUNCTION + bool empty() const noexcept { return size() == 0; } + + KOKKOS_FUNCTION + bounding_volume_type bounds() const noexcept { return _bounds; } + + template + void query(ExecutionSpace const &space, Predicates const &predicates, + Callback const &callback, Ignore = Ignore()) const; + + template + std::enable_if_t>{}> + query(ExecutionSpace const &space, Predicates const &predicates, + CallbackOrView &&callback_or_view, View &&view, Args &&... args) const + { + ArborX::query(*this, space, predicates, + std::forward(callback_or_view), + std::forward(view), std::forward(args)...); + } + +private: + size_type _size; + bounding_volume_type _bounds; + Kokkos::View _bounding_volumes; +}; + +template +template +BruteForce::BruteForce(ExecutionSpace const &space, + Primitives const &primitives) + : _size(AccessTraits::size(primitives)) + , _bounding_volumes( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "ArborX::BruteForce::bounding_volumes"), + _size) +{ + static_assert( + KokkosExt::is_accessible_from::value, ""); + Details::check_valid_access_traits(PrimitivesTag{}, primitives); + using Access = AccessTraits; + static_assert(KokkosExt::is_accessible_from::value, + "Primitives must be accessible from the execution space"); + + Kokkos::Profiling::pushRegion("ArborX::BruteForce::BruteForce"); + + Details::BruteForceImpl::initializeBoundingVolumesAndReduceBoundsOfTheScene( + space, primitives, _bounding_volumes, _bounds); + + Kokkos::Profiling::popRegion(); +} + +template +template +void BruteForce::query(ExecutionSpace const &space, + Predicates const &predicates, + Callback const &callback, Ignore) const +{ + static_assert( + KokkosExt::is_accessible_from::value, ""); + Details::check_valid_access_traits(PredicatesTag{}, predicates); + using Access = AccessTraits; + static_assert(KokkosExt::is_accessible_from::value, + "Predicates must be accessible from the execution space"); + using Tag = typename Details::AccessTraitsHelper::tag; + static_assert(std::is_same{}, + "nearest query not implemented yet"); + + Kokkos::Profiling::pushRegion("ArborX::BruteForce::query::spatial"); + + Details::BruteForceImpl::query(space, _bounding_volumes, predicates, + callback); + + Kokkos::Profiling::popRegion(); +} + +} // namespace ArborX + +#endif diff --git a/arborx/src/ArborX_Config.hpp.in b/arborx/src/ArborX_Config.hpp.in new file mode 100644 index 000000000..bdb1c0d79 --- /dev/null +++ b/arborx/src/ArborX_Config.hpp.in @@ -0,0 +1,20 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_CONFIG_HPP +#define ARBORX_CONFIG_HPP + +#cmakedefine ARBORX_ENABLE_ROCTHRUST +#cmakedefine ARBORX_ENABLE_ONEDPL +#cmakedefine ARBORX_ENABLE_MPI +#cmakedefine ARBORX_USE_CUDA_AWARE_MPI + +#endif diff --git a/arborx/src/ArborX_CrsGraphWrapper.hpp b/arborx/src/ArborX_CrsGraphWrapper.hpp new file mode 100644 index 000000000..b3dac1428 --- /dev/null +++ b/arborx/src/ArborX_CrsGraphWrapper.hpp @@ -0,0 +1,46 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_CRS_GRAPH_WRAPPER_HPP +#define ARBORX_CRS_GRAPH_WRAPPER_HPP + +#include "ArborX_DetailsCrsGraphWrapperImpl.hpp" + +namespace ArborX +{ + +template +inline void query(Tree const &tree, ExecutionSpace const &space, + Predicates const &predicates, + CallbackOrView &&callback_or_view, View &&view, + Args &&... args) +{ + Kokkos::Profiling::pushRegion("ArborX::query"); + + Details::CrsGraphWrapperImpl:: + check_valid_callback_if_first_argument_is_not_a_view(callback_or_view, + predicates, view); + + using Access = AccessTraits; + using Tag = typename Details::AccessTraitsHelper::tag; + + ArborX::Details::CrsGraphWrapperImpl::queryDispatch( + Tag{}, tree, space, predicates, + std::forward(callback_or_view), std::forward(view), + std::forward(args)...); + + Kokkos::Profiling::popRegion(); +} + +} // namespace ArborX + +#endif diff --git a/arborx/src/ArborX_DBSCAN.hpp b/arborx/src/ArborX_DBSCAN.hpp new file mode 100644 index 000000000..31a4269cd --- /dev/null +++ b/arborx/src/ArborX_DBSCAN.hpp @@ -0,0 +1,502 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_DBSCAN_HPP +#define ARBORX_DBSCAN_HPP + +#include +#include +#include +#include +#include + +#include + +namespace ArborX +{ + +namespace Details +{ + +// All points are marked as if they were core points minpts = 2 case. +// Obviously, this is not true. However, in the algorithms it is used only for +// pairs of points within the distance eps, in which case it is correct. +struct CCSCorePoints +{ + KOKKOS_FUNCTION bool operator()(int) const { return true; } +}; + +template +struct DBSCANCorePoints +{ + Kokkos::View _num_neigh; + int _core_min_size; + + KOKKOS_FUNCTION bool operator()(int const i) const + { + return _num_neigh(i) >= _core_min_size; + } +}; + +template +struct PrimitivesWithRadius +{ + Primitives _primitives; + double _r; +}; + +template +struct PrimitivesWithRadiusReorderedAndFiltered +{ + Primitives _primitives; + double _r; + PermuteFilter _filter; +}; + +// Mixed primitives consist of a set of boxes corresponding to dense cells, +// followed by boxes corresponding to points in non-dense cells. +template +struct MixedBoxPrimitives +{ + PointPrimitives _point_primitives; + Details::CartesianGrid _grid; + DenseCellOffsets _dense_cell_offsets; + int _num_points_in_dense_cells; // to avoid lastElement() in AccessTraits + CellIndices _sorted_cell_indices; + Permutation _permute; +}; + +} // namespace Details + +template +struct AccessTraits, PredicatesTag> +{ + using PrimitivesAccess = AccessTraits; + + using memory_space = typename PrimitivesAccess::memory_space; + using Predicates = Details::PrimitivesWithRadius; + + static size_t size(Predicates const &w) + { + return PrimitivesAccess::size(w._primitives); + } + static KOKKOS_FUNCTION auto get(Predicates const &w, size_t i) + { + return attach( + intersects(Sphere{PrimitivesAccess::get(w._primitives, i), w._r}), + (int)i); + } +}; + +template +struct AccessTraits, + PredicatesTag> +{ + using PrimitivesAccess = AccessTraits; + + using memory_space = typename PrimitivesAccess::memory_space; + using Predicates = + Details::PrimitivesWithRadiusReorderedAndFiltered; + + static size_t size(Predicates const &w) { return w._filter.extent(0); } + static KOKKOS_FUNCTION auto get(Predicates const &w, size_t i) + { + int index = w._filter(i); + return attach( + intersects(Sphere{PrimitivesAccess::get(w._primitives, index), w._r}), + (int)index); + } +}; + +template +struct AccessTraits, + ArborX::PrimitivesTag> +{ + using Primitives = Details::MixedBoxPrimitives; + static KOKKOS_FUNCTION std::size_t size(Primitives const &w) + { + auto const &dco = w._dense_cell_offsets; + + auto const n = w._permute.size(); + auto num_dense_primitives = dco.size() - 1; + auto num_sparse_primitives = n - w._num_points_in_dense_cells; + + return num_dense_primitives + num_sparse_primitives; + } + static KOKKOS_FUNCTION ArborX::Box get(Primitives const &w, std::size_t i) + { + auto const &dco = w._dense_cell_offsets; + + auto num_dense_primitives = dco.size() - 1; + if (i < num_dense_primitives) + { + // For a primitive corresponding to a dense cell, use that cell's box. It + // may not be tight around the points inside, but is cheap to compute. + auto cell_index = w._sorted_cell_indices(dco(i)); + return w._grid.cellBox(cell_index); + } + + // For a primitive corresponding to a point in a non-dense cell, use that + // point. But first, figure out its index, which requires some + // computations. + using Access = AccessTraits; + + i = (i - num_dense_primitives) + w._num_points_in_dense_cells; + Point const &point = Access::get(w._point_primitives, w._permute(i)); + return {point, point}; + } + using memory_space = typename MixedOffsets::memory_space; +}; + +namespace DBSCAN +{ + +enum class Implementation +{ + FDBSCAN, + FDBSCAN_DenseBox +}; + +struct Parameters +{ + // Print timers to standard output + bool _print_timers = false; + // Algorithm implementation (FDBSCAN or FDBSCAN-DenseBox) + Implementation _implementation = Implementation::FDBSCAN_DenseBox; + + Parameters &setPrintTimers(bool print_timers) + { + _print_timers = print_timers; + return *this; + } + Parameters &setImplementation(Implementation impl) + { + _implementation = impl; + return *this; + } +}; +} // namespace DBSCAN + +template +Kokkos::View::memory_space> +dbscan(ExecutionSpace const &exec_space, Primitives const &primitives, + float eps, int core_min_size, + DBSCAN::Parameters const ¶meters = DBSCAN::Parameters()) +{ + Kokkos::Profiling::pushRegion("ArborX::DBSCAN"); + + using Access = AccessTraits; + using MemorySpace = typename Access::memory_space; + + static_assert( + KokkosExt::is_accessible_from::value, + "Primitives must be accessible from the execution space"); + + ARBORX_ASSERT(eps > 0); + ARBORX_ASSERT(core_min_size >= 2); + + bool const is_special_case = (core_min_size == 2); + + Kokkos::Timer timer; + std::map elapsed; + + bool const verbose = parameters._print_timers; + auto timer_start = [&exec_space, verbose](Kokkos::Timer &timer) { + if (verbose) + exec_space.fence(); + timer.reset(); + }; + auto timer_seconds = [&exec_space, verbose](Kokkos::Timer const &timer) { + if (verbose) + exec_space.fence(); + return timer.seconds(); + }; + + int const n = Access::size(primitives); + + Kokkos::View num_neigh("ArborX::DBSCAN::num_neighbors", + 0); + + Kokkos::View labels( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "ArborX::DBSCAN::labels"), + n); + ArborX::iota(exec_space, labels); + + if (parameters._implementation == DBSCAN::Implementation::FDBSCAN) + { + // Build the tree + timer_start(timer); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::tree_construction"); + ArborX::BVH bvh(exec_space, primitives); + Kokkos::Profiling::popRegion(); + elapsed["construction"] = timer_seconds(timer); + + timer_start(timer); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters"); + auto const predicates = + Details::PrimitivesWithRadius{primitives, eps}; + if (is_special_case) + { + // Perform the queries and build clusters through callback + using CorePoints = Details::CCSCorePoints; + CorePoints core_points; + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters::query"); + bvh.query(exec_space, predicates, + Details::FDBSCANCallback{labels, + core_points}); + Kokkos::Profiling::popRegion(); + } + else + { + // Determine core points + Kokkos::Timer timer_local; + timer_start(timer_local); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters::num_neigh"); + Kokkos::resize(num_neigh, n); + bvh.query(exec_space, predicates, + Details::CountUpToN{num_neigh, core_min_size}); + Kokkos::Profiling::popRegion(); + elapsed["neigh"] = timer_seconds(timer_local); + + using CorePoints = Details::DBSCANCorePoints; + + // Perform the queries and build clusters through callback + timer_start(timer_local); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters:query"); + bvh.query(exec_space, predicates, + Details::FDBSCANCallback{ + labels, CorePoints{num_neigh, core_min_size}}); + Kokkos::Profiling::popRegion(); + elapsed["query"] = timer_seconds(timer_local); + } + } + else if (parameters._implementation == + DBSCAN::Implementation::FDBSCAN_DenseBox) + { + // Find dense boxes + timer_start(timer); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::dense_cells"); + Box bounds; + Details::TreeConstruction::calculateBoundingBoxOfTheScene( + exec_space, primitives, bounds); + + // The cell length is chosen to be eps/sqrt(dimension), so that any two + // points within the same cell are within eps distance of each other. + float const h = eps / std::sqrt(3); // 3D (for 2D change to std::sqrt(2)) + Details::CartesianGrid const grid(bounds, h); + + auto cell_indices = + Details::computeCellIndices(exec_space, primitives, grid); + + auto permute = Details::sortObjects(exec_space, cell_indices); + auto &sorted_cell_indices = cell_indices; // alias + + int num_nonempty_cells; + int num_points_in_dense_cells; + { + // Reorder indices and permutation so that the dense cells go first + auto cell_offsets = + Details::computeOffsetsInOrderedView(exec_space, sorted_cell_indices); + num_nonempty_cells = cell_offsets.size() - 1; + + num_points_in_dense_cells = Details::reorderDenseAndSparseCells( + exec_space, cell_offsets, core_min_size, sorted_cell_indices, + permute); + } + int num_points_in_sparse_cells = n - num_points_in_dense_cells; + + auto dense_sorted_cell_indices = Kokkos::subview( + sorted_cell_indices, Kokkos::make_pair(0, num_points_in_dense_cells)); + + auto dense_cell_offsets = Details::computeOffsetsInOrderedView( + exec_space, dense_sorted_cell_indices); + int num_dense_cells = dense_cell_offsets.size() - 1; + if (verbose) + { + printf("h = %e, nx = %zu, ny = %zu, nz = %zu\n", h, grid._nx, grid._ny, + grid._nz); + printf("#nonempty cells : %10d\n", num_nonempty_cells); + printf("#dense cells : %10d [%.2f%%]\n", num_dense_cells, + (100.f * num_dense_cells) / num_nonempty_cells); + printf("#dense cell points : %10d [%.2f%%]\n", num_points_in_dense_cells, + (100.f * num_points_in_dense_cells) / n); + printf("#mixed primitives : %10d\n", + num_dense_cells + num_points_in_sparse_cells); + } + + Details::unionFindWithinEachDenseCell(exec_space, dense_sorted_cell_indices, + permute, labels); + + Kokkos::Profiling::popRegion(); + elapsed["dense_cells"] = timer_seconds(timer); + + // Build the tree + timer_start(timer); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::tree_construction"); + BVH bvh( + exec_space, + Details::MixedBoxPrimitives{ + primitives, grid, dense_cell_offsets, num_points_in_dense_cells, + sorted_cell_indices, permute}); + + Kokkos::Profiling::popRegion(); + elapsed["construction"] = timer_seconds(timer); + + timer_start(timer); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters"); + + if (is_special_case) + { + // Perform the queries and build clusters through callback + using CorePoints = Details::CCSCorePoints; + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters::query"); + auto const predicates = + Details::PrimitivesWithRadius{primitives, eps}; + bvh.query( + exec_space, predicates, + Details::FDBSCANDenseBoxCallback{ + labels, CorePoints{}, primitives, dense_cell_offsets, permute, + eps}); + Kokkos::Profiling::popRegion(); + } + else + { + // Determine core points + Kokkos::Timer timer_local; + timer_start(timer_local); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters::num_neigh"); + Kokkos::resize(num_neigh, n); + // Set num neighbors for points in dense cells to max, so that they are + // automatically core points + Kokkos::parallel_for( + "ArborX::DBSCAN::mark_dense_cells_core_points", + Kokkos::RangePolicy(exec_space, 0, + num_points_in_dense_cells), + KOKKOS_LAMBDA(int i) { num_neigh(permute(i)) = INT_MAX; }); + // Count neighbors for points in sparse cells + auto sparse_permute = Kokkos::subview( + permute, Kokkos::make_pair(num_points_in_dense_cells, n)); + + auto const sparse_predicates = + Details::PrimitivesWithRadiusReorderedAndFiltered< + Primitives, decltype(sparse_permute)>{primitives, eps, + sparse_permute}; + bvh.query(exec_space, sparse_predicates, + Details::CountUpToN_DenseBox( + num_neigh, primitives, dense_cell_offsets, permute, + core_min_size, eps, core_min_size)); + Kokkos::Profiling::popRegion(); + elapsed["neigh"] = timer_seconds(timer_local); + + using CorePoints = Details::DBSCANCorePoints; + + // Perform the queries and build clusters through callback + timer_start(timer_local); + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::clusters:query"); + auto const predicates = + Details::PrimitivesWithRadius{primitives, eps}; + bvh.query( + exec_space, predicates, + Details::FDBSCANDenseBoxCallback{ + labels, CorePoints{num_neigh, core_min_size}, primitives, + dense_cell_offsets, permute, eps}); + Kokkos::Profiling::popRegion(); + elapsed["query"] = timer_seconds(timer_local); + } + } + + // Per [1]: + // + // ``` + // The finalization kernel will, ultimately, make all parents + // point directly to the representative. + // ``` + Kokkos::View cluster_sizes( + "ArborX::DBSCAN::cluster_sizes", n); + Kokkos::parallel_for("ArborX::DBSCAN::finalize_labels", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int const i) { + // ##### ECL license (see LICENSE.ECL) ##### + int next; + int vstat = labels(i); + int const old = vstat; + while (vstat > (next = labels(vstat))) + { + vstat = next; + } + if (vstat != old) + labels(i) = vstat; + + Kokkos::atomic_fetch_add(&cluster_sizes(labels(i)), 1); + }); + if (is_special_case) + { + // Ideally, this kernel would have had the exactly same form as in the + // else() clause. But there's no available valid is_core() for use here: + // - CCSCorePoints cannot be used as it always returns true, which is OK + // inside the callback, but not here + // - DBSCANCorePoints cannot be used either as num_neigh is not initialized + // in the special case. + Kokkos::parallel_for("ArborX::DBSCAN::mark_noise", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int const i) { + if (cluster_sizes(labels(i)) == 1) + labels(i) = -1; + }); + } + else + { + Details::DBSCANCorePoints is_core{num_neigh, core_min_size}; + Kokkos::parallel_for("ArborX::DBSCAN::mark_noise", + Kokkos::RangePolicy(exec_space, 0, n), + KOKKOS_LAMBDA(int const i) { + if (cluster_sizes(labels(i)) == 1 && !is_core(i)) + labels(i) = -1; + }); + } + Kokkos::Profiling::popRegion(); + elapsed["query+cluster"] = timer_seconds(timer); + + if (verbose) + { + if (parameters._implementation == DBSCAN::Implementation::FDBSCAN_DenseBox) + printf("-- dense cells : %10.3f\n", elapsed["dense_cells"]); + printf("-- construction : %10.3f\n", elapsed["construction"]); + printf("-- query+cluster : %10.3f\n", elapsed["query+cluster"]); + if (!is_special_case) + { + printf("---- neigh : %10.3f\n", elapsed["neigh"]); + printf("---- query : %10.3f\n", elapsed["query"]); + } + } + + Kokkos::Profiling::popRegion(); + + return labels; +} + +} // namespace ArborX + +#endif diff --git a/arborx/src/ArborX_DistributedSearchTree.hpp b/arborx/src/ArborX_DistributedSearchTree.hpp new file mode 100644 index 000000000..d68fa9c30 --- /dev/null +++ b/arborx/src/ArborX_DistributedSearchTree.hpp @@ -0,0 +1,25 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_DISTRIBUTED_SEARCH_TREE_HPP +#define ARBORX_DISTRIBUTED_SEARCH_TREE_HPP + +#include + +#include + +// clang-format off + +_Pragma("message(\"This file is deprecated. Use ArborX_DistributedTree.hpp instead!\")") + +// clang-format on + +#endif diff --git a/arborx/src/ArborX_DistributedTree.hpp b/arborx/src/ArborX_DistributedTree.hpp new file mode 100644 index 000000000..77d4851da --- /dev/null +++ b/arborx/src/ArborX_DistributedTree.hpp @@ -0,0 +1,225 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_DISTRIBUTED_TREE_HPP +#define ARBORX_DISTRIBUTED_TREE_HPP + +#include +#include +#include // accumulate +#include + +#include + +#include + +#include + +namespace ArborX +{ + +/** \brief Distributed search tree + * + * \note query() must be called as collective over all processes in the + * communicator passed to the constructor. + */ +template +class DistributedTree +{ +public: + using memory_space = MemorySpace; + static_assert(Kokkos::is_memory_space::value, ""); + using size_type = typename BVH::size_type; + using bounding_volume_type = typename BVH::bounding_volume_type; + + template + DistributedTree(MPI_Comm comm, ExecutionSpace const &space, + Primitives const &primitives); + + /** Returns the smallest axis-aligned box able to contain all the objects + * stored in the tree or an invalid box if the tree is empty. + */ + bounding_volume_type bounds() const noexcept { return _top_tree.bounds(); } + + /** Returns the global number of objects stored in the tree. + */ + size_type size() const noexcept { return _top_tree_size; } + + /** Indicates whether the tree is empty on all processes. + */ + bool empty() const noexcept { return size() == 0; } + + /** \brief Finds object satisfying the passed predicates (e.g. nearest to + * some point or intersecting with some box) + * + * This query function performs a batch of spatial or k-nearest neighbors + * searches. The results give indices of the objects that satisfy + * predicates (as given to the constructor). They are organized in a + * distributed compressed row storage format. + * + * \c indices stores the indices of the objects that satisfy the + * predicates. \c offset stores the locations in the \c indices view that + * start a predicate, that is, \c queries(q) is satisfied by \c indices(o) + * for objects(q) <= o < objects(q+1) that live on processes + * \c ranks(o) respectively. Following the usual convention, + * offset(n) = nnz, where \c n is the number of queries that + * were performed and \c nnz is the total number of collisions. + * + * \note The views \c indices, \c offset, and \c ranks are passed by + * reference because \c Kokkos::realloc() calls the assignment operator. + * + * \param[in] predicates Collection of predicates of the same type. These + * may be spatial predicates or nearest predicates. + * \param[out] args + * - \c indices Object local indices that satisfy the predicates. + * - \c offset Array of predicate offsets for one-dimensional + * storage. + * - \c ranks Process ranks that own objects. + * - \c distances Computed distances (optional and only for nearest + * predicates). + */ + template + void query(ExecutionSpace const &space, Predicates const &predicates, + Args &&... args) const + { + static_assert(Kokkos::is_execution_space::value, ""); + using Access = AccessTraits; + using Tag = typename Details::AccessTraitsHelper::tag; + using DeviceType = Kokkos::Device; + Details::DistributedTreeImpl::queryDispatch( + Tag{}, *this, space, predicates, std::forward(args)...); + } + +private: + template + friend struct Details::DistributedTreeImpl; + MPI_Comm getComm() const { return *_comm_ptr; } + std::shared_ptr _comm_ptr; + BVH _top_tree; // replicated + BVH _bottom_tree; // local + size_type _top_tree_size; + Kokkos::View _bottom_tree_sizes; +}; + +template +template +DistributedTree::DistributedTree( + MPI_Comm comm, ExecutionSpace const &space, Primitives const &primitives) +{ + Kokkos::Profiling::pushRegion("ArborX::DistributedTree::DistributedTree"); + + static_assert(Kokkos::is_execution_space::value, ""); + + // Create new context for the library to isolate library's communication from + // user's + _comm_ptr.reset( + // duplicate the communicator and store it in a std::shared_ptr so that + // all copies of the distributed tree point to the same object + [comm]() { + auto p = std::make_unique(); + MPI_Comm_dup(comm, p.get()); + return p.release(); + }(), + // custom deleter to mark the communicator object for deallocation + [](MPI_Comm *p) { + MPI_Comm_free(p); + delete p; + }); + + Kokkos::Profiling::pushRegion("ArborX::DistributedTree::DistributedTree::" + "bottom_tree_construction"); + + _bottom_tree = BVH(space, primitives); + + Kokkos::Profiling::popRegion(); + Kokkos::Profiling::pushRegion("ArborX::DistributedTree::DistributedTree::" + "top_tree_construction"); + + int comm_rank; + MPI_Comm_rank(getComm(), &comm_rank); + int comm_size; + MPI_Comm_size(getComm(), &comm_size); + + Kokkos::View boxes( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "ArborX::DistributedTree::DistributedTree::" + "rank_bounding_boxes"), + comm_size); + // FIXME when we move to MPI with CUDA-aware support, we will not need to + // copy from the device to the host + auto boxes_host = Kokkos::create_mirror_view(boxes); + boxes_host(comm_rank) = _bottom_tree.bounds(); + MPI_Allgather(MPI_IN_PLACE, 0, MPI_DATATYPE_NULL, + static_cast(boxes_host.data()), sizeof(Box), MPI_BYTE, + getComm()); + Kokkos::deep_copy(space, boxes, boxes_host); + + _top_tree = BVH{space, boxes}; + + Kokkos::Profiling::popRegion(); + Kokkos::Profiling::pushRegion("ArborX::DistributedTree::DistributedTree::" + "size_calculation"); + + _bottom_tree_sizes = Kokkos::View( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "ArborX::DistributedTree::" + "leave_count_in_local_trees"), + comm_size); + auto bottom_tree_sizes_host = Kokkos::create_mirror_view(_bottom_tree_sizes); + bottom_tree_sizes_host(comm_rank) = _bottom_tree.size(); + MPI_Allgather(MPI_IN_PLACE, 0, MPI_DATATYPE_NULL, + static_cast(bottom_tree_sizes_host.data()), + sizeof(size_type), MPI_BYTE, getComm()); + Kokkos::deep_copy(space, _bottom_tree_sizes, bottom_tree_sizes_host); + + _top_tree_size = accumulate(space, _bottom_tree_sizes, 0); + + Kokkos::Profiling::popRegion(); + Kokkos::Profiling::popRegion(); +} + +template +class DistributedTree::value>> + : public DistributedTree +{ +public: + using device_type = DeviceType; + + // clang-format off + template + [[deprecated("ArborX::DistributedTree templated on a device type is " + "deprecated, use it templated on a memory space instead.")]] + DistributedTree(MPI_Comm comm, Primitives const &primitives) + : DistributedTree( + comm, typename DeviceType::execution_space{}, primitives) + { + } + // clang-format on + template + void query(Args &&... args) const + { + DistributedTree::query( + typename DeviceType::execution_space{}, std::forward(args)...); + } +}; + +// clang-format off + +template +using DistributedSearchTree [[deprecated("Use DistributedTree instead.")]] = + DistributedTree; + +// clang-format-on + +} // namespace ArborX + +#endif diff --git a/arborx/src/ArborX_LinearBVH.hpp b/arborx/src/ArborX_LinearBVH.hpp new file mode 100644 index 000000000..5b214927d --- /dev/null +++ b/arborx/src/ArborX_LinearBVH.hpp @@ -0,0 +1,329 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_LINEAR_BVH_HPP +#define ARBORX_LINEAR_BVH_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ArborX +{ + +namespace Details +{ +struct HappyTreeFriends; +} // namespace Details + +template +class BasicBoundingVolumeHierarchy +{ +public: + using memory_space = MemorySpace; + static_assert(Kokkos::is_memory_space::value, ""); + using size_type = typename MemorySpace::size_type; + using bounding_volume_type = BoundingVolume; + + BasicBoundingVolumeHierarchy() = default; // build an empty tree + + template + BasicBoundingVolumeHierarchy(ExecutionSpace const &space, + Primitives const &primitives); + + KOKKOS_FUNCTION + size_type size() const noexcept { return _size; } + + KOKKOS_FUNCTION + bool empty() const noexcept { return size() == 0; } + + KOKKOS_FUNCTION + bounding_volume_type bounds() const noexcept { return _bounds; } + + template + void query(ExecutionSpace const &space, Predicates const &predicates, + Callback const &callback, + Experimental::TraversalPolicy const &policy = + Experimental::TraversalPolicy()) const; + + template + std::enable_if_t>{}> + query(ExecutionSpace const &space, Predicates const &predicates, + CallbackOrView &&callback_or_view, View &&view, Args &&... args) const + { + ArborX::query(*this, space, predicates, + std::forward(callback_or_view), + std::forward(view), std::forward(args)...); + } + +private: + friend struct Details::HappyTreeFriends; + +#if defined(KOKKOS_ENABLE_CUDA) || defined(KOKKOS_ENABLE_HIP) + // Ropes based traversal is only used for CUDA, as it was found to be slower + // than regular one for Power9 on Summit. It is also used with HIP. + using node_type = std::conditional_t< + std::is_same{}, + Details::NodeWithLeftChildAndRope, + Details::NodeWithTwoChildren>; +#else + using node_type = Details::NodeWithTwoChildren; +#endif + + Kokkos::View getInternalNodes() + { + assert(!empty()); + return Kokkos::subview(_internal_and_leaf_nodes, + std::make_pair(size_type{0}, size() - 1)); + } + + Kokkos::View getLeafNodes() + { + assert(!empty()); + return Kokkos::subview(_internal_and_leaf_nodes, + std::make_pair(size() - 1, 2 * size() - 1)); + } + Kokkos::View getLeafNodes() const + { + assert(!empty()); + return Kokkos::subview(_internal_and_leaf_nodes, + std::make_pair(size() - 1, 2 * size() - 1)); + } + + KOKKOS_FUNCTION + node_type const *getRoot() const { return _internal_and_leaf_nodes.data(); } + + KOKKOS_FUNCTION + node_type *getRoot() { return _internal_and_leaf_nodes.data(); } + + KOKKOS_FUNCTION + node_type const *getNodePtr(int i) const + { + return &_internal_and_leaf_nodes(i); + } + + KOKKOS_FUNCTION + bounding_volume_type const &getBoundingVolume(node_type const *node) const + { + return node->bounding_volume; + } + + KOKKOS_FUNCTION + bounding_volume_type &getBoundingVolume(node_type *node) + { + return node->bounding_volume; + } + + size_t _size; + bounding_volume_type _bounds; + Kokkos::View _internal_and_leaf_nodes; +}; + +template +class BasicBoundingVolumeHierarchy< + DeviceType, std::enable_if_t::value>> + : public BasicBoundingVolumeHierarchy +{ + using base_type = + BasicBoundingVolumeHierarchy; + +public: + using device_type = DeviceType; + + // clang-format off + [[deprecated("ArborX::BoundingVolumeHierarchy templated on a device type " + "is deprecated, use it templated on a memory space instead.")]] + BasicBoundingVolumeHierarchy() = default; + template + [[deprecated("ArborX::BoundingVolumeHierarchy templated on a device type " + "is deprecated, use it templated on a memory space instead.")]] + BasicBoundingVolumeHierarchy(Primitives const &primitives) + : base_type( + typename DeviceType::execution_space{}, primitives) + { + } + // clang-format on + template + std::enable_if_t::value> + query(FirstArgumentType &&arg1, Args &&... args) const + { + base_type::query(typename DeviceType::execution_space{}, + std::forward(arg1), + std::forward(args)...); + } + +private: + template + friend void ArborX::query(Tree const &tree, ExecutionSpace const &space, + Predicates const &predicates, + CallbackOrView &&callback_or_view, View &&view, + Args &&... args); + + template + std::enable_if_t::value> + query(FirstArgumentType const &space, Args &&... args) const + { + base_type::query(space, std::forward(args)...); + } +}; + +template +using BoundingVolumeHierarchy = BasicBoundingVolumeHierarchy; + +template +using BVH = BoundingVolumeHierarchy; + +template +template +BasicBoundingVolumeHierarchy:: + BasicBoundingVolumeHierarchy(ExecutionSpace const &space, + Primitives const &primitives) + : _size(AccessTraits::size(primitives)) + , _internal_and_leaf_nodes( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "ArborX::BVH::internal_and_leaf_nodes"), + _size > 0 ? 2 * _size - 1 : 0) +{ + KokkosExt::ScopedProfileRegion guard("ArborX::BVH::BVH"); + + Details::check_valid_access_traits(PrimitivesTag{}, primitives); + using Access = AccessTraits; + static_assert(KokkosExt::is_accessible_from::value, + "Primitives must be accessible from the execution space"); + + if (empty()) + { + return; + } + + Kokkos::Profiling::pushRegion( + "ArborX::BVH::BVH::calculate_scene_bounding_box"); + + // determine the bounding box of the scene + Box bbox{}; + Details::TreeConstruction::calculateBoundingBoxOfTheScene(space, primitives, + bbox); + + Kokkos::Profiling::popRegion(); + + if (size() == 1) + { + Details::TreeConstruction::initializeSingleLeafNode( + space, primitives, _internal_and_leaf_nodes); + Kokkos::deep_copy( + space, + Kokkos::View(&_bounds), + Kokkos::View( + &getBoundingVolume(getRoot()))); + return; + } + + Kokkos::Profiling::pushRegion("ArborX::BVH::BVH::assign_morton_codes"); + + // calculate Morton codes of all objects + Kokkos::View morton_indices( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "ArborX::BVH::BVH::morton"), + size()); + Details::TreeConstruction::assignMortonCodes(space, primitives, + morton_indices, bbox); + + Kokkos::Profiling::popRegion(); + Kokkos::Profiling::pushRegion("ArborX::BVH::BVH::sort_morton_codes"); + + // compute the ordering of primitives along Z-order space-filling curve + auto permutation_indices = Details::sortObjects(space, morton_indices); + + Kokkos::Profiling::popRegion(); + Kokkos::Profiling::pushRegion("ArborX::BVH::BVH::generate_hierarchy"); + + // generate bounding volume hierarchy + Details::TreeConstruction::generateHierarchy( + space, primitives, permutation_indices, morton_indices, getLeafNodes(), + getInternalNodes()); + + Kokkos::deep_copy( + space, + Kokkos::View( + &_bounds), + Kokkos::View( + &getBoundingVolume(getRoot()))); + + Kokkos::Profiling::popRegion(); +} + +template +template +void BasicBoundingVolumeHierarchy::query( + ExecutionSpace const &space, Predicates const &predicates, + Callback const &callback, Experimental::TraversalPolicy const &policy) const +{ + Details::check_valid_access_traits(PredicatesTag{}, predicates); + + using Access = AccessTraits; + using Tag = typename Details::AccessTraitsHelper::tag; + + auto profiling_prefix = + std::string("ArborX::BVH::query::") + + (std::is_same{} ? "spatial" + : "nearest"); + + Kokkos::Profiling::pushRegion(profiling_prefix); + + if (policy._sort_predicates) + { + Kokkos::Profiling::pushRegion(profiling_prefix + "::compute_permutation"); + using DeviceType = Kokkos::Device; + auto permute = + Details::BatchedQueries::sortQueriesAlongZOrderCurve( + space, static_cast(bounds()), predicates); + Kokkos::Profiling::popRegion(); + + using PermutedPredicates = + Details::PermutedData; + Details::traverse(space, *this, PermutedPredicates{predicates, permute}, + callback); + } + else + { + Details::traverse(space, *this, predicates, callback); + } + + Kokkos::Profiling::popRegion(); +} + +} // namespace ArborX + +#endif diff --git a/arborx/src/ArborX_Version.hpp.in b/arborx/src/ArborX_Version.hpp.in new file mode 100644 index 000000000..b37578c81 --- /dev/null +++ b/arborx/src/ArborX_Version.hpp.in @@ -0,0 +1,28 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_VERSION_HPP +#define ARBORX_VERSION_HPP + +#include + +#include + +namespace ArborX +{ + +inline std::string version() { return "@ARBORX_VERSION_STRING@"; } + +inline std::string gitCommitHash() { return "@ARBORX_GIT_COMMIT_HASH@"; } + +} // namespace ArborX + +#endif diff --git a/arborx/src/details/ArborX_AccessTraits.hpp b/arborx/src/details/ArborX_AccessTraits.hpp new file mode 100644 index 000000000..d5dcedfd0 --- /dev/null +++ b/arborx/src/details/ArborX_AccessTraits.hpp @@ -0,0 +1,192 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_ACCESS_TRAITS_HPP +#define ARBORX_ACCESS_TRAITS_HPP + +#include +#include +#include +#include + +#include + +namespace ArborX +{ + +struct PrimitivesTag +{ +}; + +struct PredicatesTag +{ +}; + +template +struct AccessTraits +{ + using not_specialized = void; // tag to detect existence of a specialization +}; + +template +using AccessTraitsNotSpecializedArchetypeAlias = + typename Traits::not_specialized; + +template +struct AccessTraits< + View, Tag, std::enable_if_t{} && View::rank == 1>> +{ + // Returns a const reference + KOKKOS_FUNCTION static typename View::const_value_type &get(View const &v, + int i) + { + return v(i); + } + + KOKKOS_FUNCTION + static typename View::size_type size(View const &v) { return v.extent(0); } + + using memory_space = typename View::memory_space; +}; + +template +struct AccessTraits< + View, Tag, std::enable_if_t{} && View::rank == 2>> +{ + // Returns by value + KOKKOS_FUNCTION static Point get(View const &v, int i) + { + return {{v(i, 0), v(i, 1), v(i, 2)}}; + } + + KOKKOS_FUNCTION + static typename View::size_type size(View const &v) { return v.extent(0); } + + using memory_space = typename View::memory_space; +}; + +namespace Details +{ + +// archetypal alias for a 'memory_space' type member in access traits +template +using AccessTraitsMemorySpaceArchetypeAlias = typename Traits::memory_space; + +// archetypal expression for 'size()' static member function in access traits +template +using AccessTraitsSizeArchetypeExpression = decltype( + Traits::size(std::declval const &>())); + +// archetypal expression for 'get()' static member function in access traits +template +using AccessTraitsGetArchetypeExpression = decltype( + Traits::get(std::declval const &>(), 0)); + +template +struct AccessTraitsHelper +{ + // Deduce return type of get() + using type = + std::decay_t>; + using tag = typename Tag::type; +}; + +template +void check_valid_access_traits(PredicatesTag, Predicates const &) +{ + using Access = AccessTraits; + static_assert( + !is_detected{}, + "Must specialize 'AccessTraits'"); + + static_assert(is_detected{}, + "AccessTraits must define " + "'memory_space' member type"); + static_assert( + Kokkos::is_memory_space< + detected_t>{}, + "'memory_space' member type must be a valid Kokkos memory space"); + + static_assert(is_detected{}, + "AccessTraits must define " + "'size()' static member function"); + static_assert( + std::is_integral< + detected_t>{}, + "size() static member function return type is not an integral type"); + + static_assert(is_detected{}, + "AccessTraits must define " + "'get()' static member function"); + + using Tag = typename AccessTraitsHelper::tag; + static_assert(std::is_same{} || + std::is_same{}, + "Invalid tag for the predicates"); +} + +template +void check_valid_access_traits(PrimitivesTag, Primitives const &) +{ + using Access = AccessTraits; + static_assert( + !is_detected{}, + "Must specialize 'AccessTraits'"); + + static_assert(is_detected{}, + "AccessTraits must define " + "'memory_space' member type"); + static_assert( + Kokkos::is_memory_space< + detected_t>{}, + "'memory_space' member type must be a valid Kokkos memory space"); + + static_assert(is_detected{}, + "AccessTraits must define " + "'size()' static member function"); + static_assert( + std::is_integral< + detected_t>{}, + "size() static member function return type is not an integral type"); + + static_assert(is_detected{}, + "AccessTraits must define " + "'get()' static member function"); + using T = + std::decay_t>; + static_assert(std::is_same{} || std::is_same{}, + "AccessTraits::get() return type " + "must decay to Point or to Box"); +} + +} // namespace Details + +namespace Traits +{ +using ::ArborX::PredicatesTag; +using ::ArborX::PrimitivesTag; +template +struct Access +{ + using not_specialized = void; +}; +} // namespace Traits +template +struct AccessTraits< + T, Tag, + std::enable_if_t>{}>> + : Traits::Access +{ +}; +} // namespace ArborX + +#endif diff --git a/arborx/src/details/ArborX_Box.hpp b/arborx/src/details/ArborX_Box.hpp new file mode 100644 index 000000000..9c2008822 --- /dev/null +++ b/arborx/src/details/ArborX_Box.hpp @@ -0,0 +1,120 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_BOX_HPP +#define ARBORX_BOX_HPP + +#include +#include +#include + +#include + +namespace ArborX +{ +/** + * Axis-Aligned Bounding Box. This is just a thin wrapper around an array of + * size 2x spatial dimension with a default constructor to initialize + * properly an "empty" box. + */ +struct Box +{ + KOKKOS_DEFAULTED_FUNCTION + constexpr Box() = default; + + KOKKOS_INLINE_FUNCTION + constexpr Box(Point const &min_corner, Point const &max_corner) + : _min_corner(min_corner) + , _max_corner(max_corner) + { + } + + KOKKOS_INLINE_FUNCTION + constexpr Point &minCorner() { return _min_corner; } + + KOKKOS_INLINE_FUNCTION + constexpr Point const &minCorner() const { return _min_corner; } + + KOKKOS_INLINE_FUNCTION + Point volatile &minCorner() volatile { return _min_corner; } + + KOKKOS_INLINE_FUNCTION + Point volatile const &minCorner() volatile const { return _min_corner; } + + KOKKOS_INLINE_FUNCTION + constexpr Point &maxCorner() { return _max_corner; } + + KOKKOS_INLINE_FUNCTION + constexpr Point const &maxCorner() const { return _max_corner; } + + KOKKOS_INLINE_FUNCTION + Point volatile &maxCorner() volatile { return _max_corner; } + + KOKKOS_INLINE_FUNCTION + Point volatile const &maxCorner() volatile const { return _max_corner; } + + Point _min_corner = {{KokkosExt::ArithmeticTraits::max::value, + KokkosExt::ArithmeticTraits::max::value, + KokkosExt::ArithmeticTraits::max::value}}; + Point _max_corner = {{-KokkosExt::ArithmeticTraits::max::value, + -KokkosExt::ArithmeticTraits::max::value, + -KokkosExt::ArithmeticTraits::max::value}}; + + KOKKOS_FUNCTION Box &operator+=(Box const &other) + { + using KokkosExt::max; + using KokkosExt::min; + + for (int d = 0; d < 3; ++d) + { + minCorner()[d] = min(minCorner()[d], other.minCorner()[d]); + maxCorner()[d] = max(maxCorner()[d], other.maxCorner()[d]); + } + return *this; + } + + KOKKOS_FUNCTION void operator+=(Box const volatile &other) volatile + { + using KokkosExt::max; + using KokkosExt::min; + + for (int d = 0; d < 3; ++d) + { + minCorner()[d] = min(minCorner()[d], other.minCorner()[d]); + maxCorner()[d] = max(maxCorner()[d], other.maxCorner()[d]); + } + } + + KOKKOS_FUNCTION Box &operator+=(Point const &point) + { + using KokkosExt::max; + using KokkosExt::min; + + for (int d = 0; d < 3; ++d) + { + minCorner()[d] = min(minCorner()[d], point[d]); + maxCorner()[d] = max(maxCorner()[d], point[d]); + } + return *this; + } + +// FIXME Temporary workaround until we clarify requirements on the Kokkos side. +#if defined(KOKKOS_ENABLE_OPENMPTARGET) || defined(KOKKOS_ENABLE_SYCL) +private: + friend KOKKOS_FUNCTION Box operator+(Box box, Box const &other) + { + return box += other; + } +#endif +}; +} // namespace ArborX + +#endif diff --git a/arborx/src/details/ArborX_Callbacks.hpp b/arborx/src/details/ArborX_Callbacks.hpp new file mode 100644 index 000000000..fae346c34 --- /dev/null +++ b/arborx/src/details/ArborX_Callbacks.hpp @@ -0,0 +1,212 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ +#ifndef ARBORX_CALLBACKS_HPP +#define ARBORX_CALLBACKS_HPP + +#include +#include + +#include + +#include // declval + +namespace ArborX +{ + +enum class CallbackTreeTraversalControl +{ + early_exit, + normal_continuation +}; + +namespace Details +{ + +struct InlineCallbackTag +{ +}; + +struct PostCallbackTag +{ +}; + +struct DefaultCallback +{ + using tag = InlineCallbackTag; + template + KOKKOS_FUNCTION void operator()(Query const &, int index, + OutputFunctor const &output) const + { + output(index); + } +}; + +// archetypal expression for user callbacks +template +using InlineCallbackArchetypeExpression = + decltype(std::declval()(std::declval(), + 0, std::declval())); + +// legacy nearest predicate archetypal expression for user callbacks +template +using Legacy_NearestPredicateInlineCallbackArchetypeExpression = decltype( + std::declval()(std::declval(), 0, 0.f, + std::declval())); + +// archetypal alias for a 'tag' type member in user callbacks +template +using CallbackTagArchetypeAlias = typename Callback::tag; + +template +struct is_tagged_post_callback + : std::is_same, + PostCallbackTag>::type +{ +}; + +// output functor to pass to the callback during detection +template +struct Sink +{ + void operator()(T const &) const {} +}; + +template +using OutputFunctorHelper = Sink; + +template +void check_generic_lambda_support(Callback const &) +{ +#ifdef __NVCC__ + // Without it would get a segmentation fault and no diagnostic whatsoever + static_assert( + !__nv_is_extended_host_device_lambda_closure_type(Callback), + "__host__ __device__ extended lambdas cannot be generic lambdas"); +#endif +} + +template +void check_valid_callback(Callback const &callback, Predicates const &, + OutputView const &) +{ + check_generic_lambda_support(callback); + + using Access = AccessTraits; + using PredicateTag = typename AccessTraitsHelper::tag; + using Predicate = typename AccessTraitsHelper::type; + + static_assert( + !(std::is_same{} && + is_detected>{}), + R"error(Callback signature has changed for nearest predicates. +See https://github.com/arborx/ArborX/pull/366 for more details. +Sorry!)error"); + + static_assert((std::is_same{} || + std::is_same{}) && + is_detected>{}, + "Callback 'operator()' does not have the correct signature"); + + static_assert( + std::is_void>>{}, + "Callback 'operator()' return type must be void"); +} + +// EXPERIMENTAL archetypal expression for user callbacks +template +using Experimental_CallbackArchetypeExpression = + decltype(std::declval()( + std::declval(), std::declval())); + +// Determine whether the callback returns a hint to exit the tree traversal +// early. +template +struct invoke_callback_and_check_early_exit_helper + : std::is_same>::type +{ +}; + +// Invoke a callback that may return a hint to interrupt the tree traversal and +// return true for early exit, or false for normal continuation. +template +KOKKOS_INLINE_FUNCTION + std::enable_if_t, std::decay_t, + std::decay_t>::value, + bool> + invoke_callback_and_check_early_exit(Callback &&callback, + Predicate &&predicate, + Primitive &&primitive) +{ + return ((Callback &&) callback)((Predicate &&) predicate, + (Primitive &&) primitive) == + CallbackTreeTraversalControl::early_exit; +} + +// Invoke a callback that does not return a hint. Always return false to +// signify that the tree traversal should continue normally. +template +KOKKOS_INLINE_FUNCTION + std::enable_if_t, std::decay_t, + std::decay_t>::value, + bool> + invoke_callback_and_check_early_exit(Callback &&callback, + Predicate &&predicate, + Primitive &&primitive) +{ + ((Callback &&) callback)((Predicate &&) predicate, (Primitive &&) primitive); + return false; +} + +template +void check_valid_callback(Callback const &callback, Predicates const &) +{ + check_generic_lambda_support(callback); + + using Access = AccessTraits; + using PredicateTag = typename AccessTraitsHelper::tag; + using Predicate = typename AccessTraitsHelper::type; + + static_assert((std::is_same{} || + std::is_same{}) && + is_detected{}, + "Callback 'operator()' does not have the correct signature"); + + static_assert( + (std::is_same{} && + (std::is_same>{} || + std::is_void>{})) || + std::is_same{}, + "Callback 'operator()' return type must be void or " + "ArborX::CallbackTreeTraversalControl"); + + static_assert( + std::is_same{} || + (std::is_same{} && + std::is_void>{}), + "Callback 'operator()' return type must be void"); +} + +} // namespace Details +} // namespace ArborX + +#endif diff --git a/arborx/src/details/ArborX_DetailsAlgorithms.hpp b/arborx/src/details/ArborX_DetailsAlgorithms.hpp new file mode 100644 index 000000000..1290dbed6 --- /dev/null +++ b/arborx/src/details/ArborX_DetailsAlgorithms.hpp @@ -0,0 +1,259 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ +#ifndef ARBORX_DETAILS_ALGORITHMS_HPP +#define ARBORX_DETAILS_ALGORITHMS_HPP + +#include +#include +#include // isFinite +#include +#include + +#include + +namespace ArborX +{ +namespace Details +{ + +KOKKOS_INLINE_FUNCTION +constexpr bool equals(Point const &l, Point const &r) +{ + for (int d = 0; d < 3; ++d) + if (l[d] != r[d]) + return false; + return true; +} + +KOKKOS_INLINE_FUNCTION +constexpr bool equals(Box const &l, Box const &r) +{ + return equals(l.minCorner(), r.minCorner()) && + equals(l.maxCorner(), r.maxCorner()); +} + +KOKKOS_INLINE_FUNCTION +constexpr bool equals(Sphere const &l, Sphere const &r) +{ + return equals(l.centroid(), r.centroid()) && l.radius() == r.radius(); +} + +KOKKOS_INLINE_FUNCTION +bool isValid(Point const &p) +{ + using KokkosExt::isFinite; + for (int d = 0; d < 3; ++d) + if (!isFinite(p[d])) + return false; + return true; +} + +KOKKOS_INLINE_FUNCTION +bool isValid(Box const &b) +{ + using KokkosExt::isFinite; + for (int d = 0; d < 3; ++d) + { + auto const r_d = b.maxCorner()[d] - b.minCorner()[d]; + if (r_d <= 0 || !isFinite(r_d)) + return false; + } + return true; +} + +KOKKOS_INLINE_FUNCTION +bool isValid(Sphere const &s) +{ + using KokkosExt::isFinite; + return isValid(s.centroid()) && isFinite(s.radius()) && (s.radius() >= 0.); +} + +// distance point-point +KOKKOS_INLINE_FUNCTION +float distance(Point const &a, Point const &b) +{ + float distance_squared = 0.0; + for (int d = 0; d < 3; ++d) + { + float tmp = b[d] - a[d]; + distance_squared += tmp * tmp; + } + return std::sqrt(distance_squared); +} + +// distance point-box +KOKKOS_INLINE_FUNCTION +float distance(Point const &point, Box const &box) +{ + Point projected_point; + for (int d = 0; d < 3; ++d) + { + if (point[d] < box.minCorner()[d]) + projected_point[d] = box.minCorner()[d]; + else if (point[d] > box.maxCorner()[d]) + projected_point[d] = box.maxCorner()[d]; + else + projected_point[d] = point[d]; + } + return distance(point, projected_point); +} + +// distance point-sphere +KOKKOS_INLINE_FUNCTION +float distance(Point const &point, Sphere const &sphere) +{ + using KokkosExt::max; + return max(distance(point, sphere.centroid()) - sphere.radius(), 0.f); +} + +// distance box-box +KOKKOS_INLINE_FUNCTION +float distance(Box const &box_a, Box const &box_b) +{ + float distance_squared = 0.; + for (int d = 0; d < 3; ++d) + { + auto const a_min = box_a.minCorner()[d]; + auto const a_max = box_a.maxCorner()[d]; + auto const b_min = box_b.minCorner()[d]; + auto const b_max = box_b.maxCorner()[d]; + if (a_min > b_max) + { + float const delta = a_min - b_max; + distance_squared += delta * delta; + } + else if (b_min > a_max) + { + float const delta = b_min - a_max; + distance_squared += delta * delta; + } + else + { + // The boxes overlap on this axis: distance along this axis is zero. + } + } + return std::sqrt(distance_squared); +} + +// distance box-sphere +KOKKOS_INLINE_FUNCTION +float distance(Sphere const &sphere, Box const &box) +{ + using KokkosExt::max; + + float distance_center_box = distance(sphere.centroid(), box); + return max(distance_center_box - sphere.radius(), 0.f); +} + +// expand an axis-aligned bounding box to include a point +KOKKOS_INLINE_FUNCTION +void expand(Box &box, Point const &point) { box += point; } + +// expand an axis-aligned bounding box to include another box +// NOTE: Box type is templated here to be able to use expand(box, box) in a +// Kokkos::parallel_reduce() in which case the arguments must be declared +// volatile. +template ::type, Box>::value>::type> +KOKKOS_INLINE_FUNCTION void expand(BOX &box, BOX const &other) +{ + box += other; +} + +// expand an axis-aligned bounding box to include a sphere +KOKKOS_INLINE_FUNCTION +void expand(Box &box, Sphere const &sphere) +{ + using KokkosExt::max; + using KokkosExt::min; + for (int d = 0; d < 3; ++d) + { + box.minCorner()[d] = + min(box.minCorner()[d], sphere.centroid()[d] - sphere.radius()); + box.maxCorner()[d] = + max(box.maxCorner()[d], sphere.centroid()[d] + sphere.radius()); + } +} + +// check if two axis-aligned bounding boxes intersect +KOKKOS_INLINE_FUNCTION +constexpr bool intersects(Box const &box, Box const &other) +{ + for (int d = 0; d < 3; ++d) + if (box.minCorner()[d] > other.maxCorner()[d] || + box.maxCorner()[d] < other.minCorner()[d]) + return false; + return true; +} + +KOKKOS_INLINE_FUNCTION +constexpr bool intersects(Point const &point, Box const &other) +{ + for (int d = 0; d < 3; ++d) + if (point[d] > other.maxCorner()[d] || point[d] < other.minCorner()[d]) + return false; + return true; +} + +// check if a sphere intersects with an axis-aligned bounding box +KOKKOS_INLINE_FUNCTION +bool intersects(Sphere const &sphere, Box const &box) +{ + return distance(sphere.centroid(), box) <= sphere.radius(); +} + +// calculate the centroid of a box +KOKKOS_INLINE_FUNCTION +void centroid(Box const &box, Point &c) +{ + for (int d = 0; d < 3; ++d) + c[d] = (box.minCorner()[d] + box.maxCorner()[d]) / 2; +} + +KOKKOS_INLINE_FUNCTION +void centroid(Point const &point, Point &c) { c = point; } + +KOKKOS_INLINE_FUNCTION +void centroid(Sphere const &sphere, Point &c) { c = sphere.centroid(); } + +KOKKOS_INLINE_FUNCTION +Point returnCentroid(Point const &point) { return point; } + +KOKKOS_INLINE_FUNCTION +Point returnCentroid(Box const &box) +{ + Point c; + for (int d = 0; d < 3; ++d) + c[d] = (box.minCorner()[d] + box.maxCorner()[d]) / 2; + return c; +} + +KOKKOS_INLINE_FUNCTION +Point returnCentroid(Sphere const &sphere) { return sphere.centroid(); } + +// transformation that maps the unit cube into a new axis-aligned box +// NOTE safe to perform in-place +KOKKOS_INLINE_FUNCTION +void translateAndScale(Point const &in, Point &out, Box const &ref) +{ + for (int d = 0; d < 3; ++d) + { + auto const a = ref.minCorner()[d]; + auto const b = ref.maxCorner()[d]; + out[d] = (a != b ? (in[d] - a) / (b - a) : 0); + } +} + +} // namespace Details +} // namespace ArborX + +#endif diff --git a/arborx/src/details/ArborX_DetailsBatchedQueries.hpp b/arborx/src/details/ArborX_DetailsBatchedQueries.hpp new file mode 100644 index 000000000..1080dd9f5 --- /dev/null +++ b/arborx/src/details/ArborX_DetailsBatchedQueries.hpp @@ -0,0 +1,188 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_DETAILS_BATCHED_QUERIES_HPP +#define ARBORX_DETAILS_BATCHED_QUERIES_HPP + +#include +#include +#include // returnCentroid, translateAndScale +#include // morton3D +#include // sortObjects +#include // exclusivePrefixSum, lastElement + +#include + +#include + +namespace ArborX +{ + +namespace Details +{ +template +struct BatchedQueries +{ +public: + // BatchedQueries defines functions for sorting queries along the Z-order + // space-filling curve in order to minimize data divergence. The goal is + // to increase correlation between traversal decisions made by nearby + // threads and thereby increase performance. + // + // NOTE: sortQueriesAlongZOrderCurve() does not actually apply the sorting + // order, it returns the permutation indices. applyPermutation() was added + // in that purpose. reversePermutation() is able to restore the initial + // order on the results that are in "compressed row storage" format. You + // may notice it is not used any more in the code that performs the batched + // queries. We found that it was slighly more performant to add a level of + // indirection when recording results rather than using that function at + // the end. We decided to keep reversePermutation around for now. + + template + static Kokkos::View + sortQueriesAlongZOrderCurve(ExecutionSpace const &space, + Box const &scene_bounding_box, + Predicates const &predicates) + { + using Access = AccessTraits; + auto const n_queries = Access::size(predicates); + + Kokkos::View morton_codes( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "ArborX::BVH::query::morton"), + n_queries); + Kokkos::parallel_for( + "ArborX::BatchedQueries::assign_morton_codes_to_queries", + Kokkos::RangePolicy(space, 0, n_queries), + KOKKOS_LAMBDA(int i) { + using Details::returnCentroid; + Point xyz = returnCentroid(getGeometry(Access::get(predicates, i))); + translateAndScale(xyz, xyz, scene_bounding_box); + morton_codes(i) = morton3D(xyz[0], xyz[1], xyz[2]); + }); + + return sortObjects(space, morton_codes); + } + + // NOTE trailing return type seems required :( + // error: The enclosing parent function ("applyPermutation") for an extended + // __host__ __device__ lambda must not have deduced return type + template + static auto + applyPermutation(ExecutionSpace const &space, + Kokkos::View permute, + Predicates const &v) + -> Kokkos::View>::type *, + DeviceType> + { + using Access = AccessTraits; + auto const n = Access::size(v); + ARBORX_ASSERT(permute.extent(0) == n); + + using T = std::decay_t(), std::declval()))>; + Kokkos::View w( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "predicates"), n); + Kokkos::parallel_for( + "ArborX::BatchedQueries::permute_entries", + Kokkos::RangePolicy(space, 0, n), + KOKKOS_LAMBDA(int i) { w(i) = Access::get(v, permute(i)); }); + + return w; + } + + template + static typename Offset::non_const_type + permuteOffset(ExecutionSpace const &space, Permute const &permute, + Offset const &offset) + { + auto const n = permute.extent(0); + ARBORX_ASSERT(offset.extent(0) == n + 1); + + auto tmp_offset = cloneWithoutInitializingNorCopying(offset); + Kokkos::parallel_for( + "ArborX::BatchedQueries::adjacent_difference_and_permutation", + Kokkos::RangePolicy(space, 0, n), KOKKOS_LAMBDA(int i) { + tmp_offset(permute(i)) = offset(i + 1) - offset(i); + }); + + exclusivePrefixSum(space, tmp_offset); + + return tmp_offset; + } + + template + static typename Values::non_const_type + permuteIndices(ExecutionSpace const &space, Permute const &permute, + Values const &indices, Offset const &offset, + Offset2 const &tmp_offset) + { + auto const n = permute.extent(0); + + ARBORX_ASSERT(offset.extent(0) == n + 1); + ARBORX_ASSERT(tmp_offset.extent(0) == n + 1); + ARBORX_ASSERT(lastElement(offset) == indices.extent_int(0)); + ARBORX_ASSERT(lastElement(tmp_offset) == indices.extent_int(0)); + + auto tmp_indices = cloneWithoutInitializingNorCopying(indices); + Kokkos::parallel_for( + "ArborX::BatchedQueries::permute_indices", + Kokkos::RangePolicy(space, 0, n), KOKKOS_LAMBDA(int q) { + for (int i = 0; i < offset(q + 1) - offset(q); ++i) + { + tmp_indices(tmp_offset(permute(q)) + i) = indices(offset(q) + i); + } + }); + return tmp_indices; + } + + template + static std::tuple, Kokkos::View> + reversePermutation(ExecutionSpace const &space, + Kokkos::View permute, + Kokkos::View offset, + Kokkos::View out) + { + auto const tmp_offset = permuteOffset(space, permute, offset); + + auto const tmp_out = + permuteIndices(space, permute, out, offset, tmp_offset); + return std::make_tuple(tmp_offset, tmp_out); + } + + template + static std::tuple, + Kokkos::View, + Kokkos::View> + reversePermutation(ExecutionSpace const &space, + Kokkos::View permute, + Kokkos::View offset, + Kokkos::View indices, + Kokkos::View distances) + { + auto const tmp_offset = permuteOffset(permute, offset); + + auto const tmp_indices = + permuteIndices(space, permute, indices, offset, tmp_offset); + + auto const tmp_distances = + permuteIndices(space, permute, distances, offset, tmp_offset); + + return std::make_tuple(tmp_offset, tmp_indices, tmp_distances); + } +}; + +} // namespace Details +} // namespace ArborX + +#endif diff --git a/arborx/src/details/ArborX_DetailsBruteForceImpl.hpp b/arborx/src/details/ArborX_DetailsBruteForceImpl.hpp new file mode 100644 index 000000000..40371e7f1 --- /dev/null +++ b/arborx/src/details/ArborX_DetailsBruteForceImpl.hpp @@ -0,0 +1,152 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_DETAILS_BRUTE_FORCE_IMPL_HPP +#define ARBORX_DETAILS_BRUTE_FORCE_IMPL_HPP + +#include +#include // expand +#include // Kokkos::reduction_identity + +#include + +namespace ArborX +{ +namespace Details +{ +struct BruteForceImpl +{ + template + static void initializeBoundingVolumesAndReduceBoundsOfTheScene( + ExecutionSpace const &space, Primitives const &primitives, + BoundingVolumes const &bounding_volumes, Bounds &bounds) + { + using Access = AccessTraits; + + int const n = Access::size(primitives); + + Kokkos::parallel_reduce("ArborX::BruteForce::BruteForce::" + "initialize_bounding_volumes_and_reduce_bounds", + Kokkos::RangePolicy(space, 0, n), + KOKKOS_LAMBDA(int i, Bounds &update) { + using Details::expand; + Bounds bounding_volume{}; + expand(bounding_volume, + Access::get(primitives, i)); + bounding_volumes(i) = bounding_volume; + update += bounding_volume; + }, + bounds); + } + + template + static void query(ExecutionSpace const &space, Primitives const &primitives, + Predicates const &predicates, Callback const &callback) + { + using TeamPolicy = Kokkos::TeamPolicy; + using AccessPrimitives = AccessTraits; + using AccessPredicates = AccessTraits; + using PredicateType = typename AccessTraitsHelper::type; + using PrimitiveType = typename AccessTraitsHelper::type; + + int const n_primitives = AccessPrimitives::size(primitives); + int const n_predicates = AccessPredicates::size(predicates); + int max_scratch_size = TeamPolicy::scratch_size_max(0); + // half of the scratch memory used by predicates and half for primitives + int const predicates_per_team = + max_scratch_size / 2 / sizeof(PredicateType); + int const primitives_per_team = + max_scratch_size / 2 / sizeof(PrimitiveType); + ARBORX_ASSERT(predicates_per_team > 0); + ARBORX_ASSERT(primitives_per_team > 0); + + int const n_primitive_tiles = + std::ceil((float)n_primitives / primitives_per_team); + int const n_predicate_tiles = + std::ceil((float)n_predicates / predicates_per_team); + int const n_teams = n_primitive_tiles * n_predicate_tiles; + + using ScratchPredicateType = + Kokkos::View>; + using ScratchPrimitiveType = + Kokkos::View>; + int scratch_size = ScratchPredicateType::shmem_size(predicates_per_team) + + ScratchPrimitiveType::shmem_size(primitives_per_team); + + Kokkos::parallel_for( + "ArborX::BruteForce::query::spatial::" + "check_all_predicates_against_all_primitives", + TeamPolicy(space, n_teams, Kokkos::AUTO, 1) + .set_scratch_size(0, Kokkos::PerTeam(scratch_size)), + KOKKOS_LAMBDA(typename TeamPolicy::member_type const &teamMember) { + // select the tiles of predicates/primitives checked by each team + int predicate_start = predicates_per_team * + (teamMember.league_rank() / n_primitive_tiles); + int primitive_start = primitives_per_team * + (teamMember.league_rank() % n_primitive_tiles); + + int predicates_in_this_team = KokkosExt::min( + predicates_per_team, n_predicates - predicate_start); + int primitives_in_this_team = KokkosExt::min( + primitives_per_team, n_primitives - primitive_start); + + ScratchPredicateType scratch_predicates(teamMember.team_scratch(0), + predicates_per_team); + ScratchPrimitiveType scratch_primitives(teamMember.team_scratch(0), + primitives_per_team); + // rank 0 in each team fills the scratch space with the + // predicates / primitives in the tile + if (teamMember.team_rank() == 0) + { + Kokkos::parallel_for( + Kokkos::ThreadVectorRange(teamMember, predicates_in_this_team), + [&](const int q) { + scratch_predicates(q) = + AccessPredicates::get(predicates, predicate_start + q); + }); + Kokkos::parallel_for( + Kokkos::ThreadVectorRange(teamMember, primitives_in_this_team), + [&](const int j) { + scratch_primitives(j) = + AccessPrimitives::get(primitives, primitive_start + j); + }); + } + teamMember.team_barrier(); + + // start threads for every predicate / primitive combination + Kokkos::parallel_for( + Kokkos::TeamThreadRange(teamMember, primitives_in_this_team), + [&](int j) { + Kokkos::parallel_for( + Kokkos::ThreadVectorRange(teamMember, + predicates_in_this_team), + [&](const int q) { + auto const &predicate = scratch_predicates(q); + auto const &primitive = scratch_primitives(j); + if (predicate(primitive)) + { + callback(predicate, j + primitive_start); + } + }); + }); + }); + } +}; +} // namespace Details +} // namespace ArborX + +#endif diff --git a/arborx/src/details/ArborX_DetailsConcepts.hpp b/arborx/src/details/ArborX_DetailsConcepts.hpp new file mode 100644 index 000000000..9f294ac8c --- /dev/null +++ b/arborx/src/details/ArborX_DetailsConcepts.hpp @@ -0,0 +1,77 @@ +/**************************************************************************** + * Copyright (c) 2017-2021 by the ArborX authors * + * All rights reserved. * + * * + * This file is part of the ArborX library. ArborX is * + * distributed under a BSD 3-clause license. For the licensing terms see * + * the LICENSE file in the top-level directory. * + * * + * SPDX-License-Identifier: BSD-3-Clause * + ****************************************************************************/ + +#ifndef ARBORX_DETAILS_CONCEPTS_HPP +#define ARBORX_DETAILS_CONCEPTS_HPP + +#include + +#if !defined(__cpp_lib_void_t) +namespace std +{ +template +using void_t = void; +} +#endif + +#if !defined(DOXYGEN_SHOULD_SKIP_THIS) +namespace ArborX +{ +namespace Details +{ + +struct not_a_type +{ + not_a_type() = delete; + ~not_a_type() = delete; + not_a_type(not_a_type const &) = delete; + void operator=(not_a_type const &) = delete; +}; + +// primary template handles all types not supporting the archetypal Op +template class Op, class... Args> +struct is_detected_impl : std::false_type +{ + using type = not_a_type; +}; + +// specialization recognizes and handles only types supporting Op +template