diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 37c4edb53..7b2155e89 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -30,7 +30,7 @@ else() ENDIF() add_subdirectory(brute_force_vs_bvh) -add_subdirectory(dbscan) +add_subdirectory(cluster) add_subdirectory(execution_space_instances) if(NOT WIN32) # FIXME: for now, skip the benchmarks using Google benchmark diff --git a/benchmarks/dbscan/ArborX_DBSCANVerification.hpp b/benchmarks/cluster/ArborX_DBSCANVerification.hpp similarity index 100% rename from benchmarks/dbscan/ArborX_DBSCANVerification.hpp rename to benchmarks/cluster/ArborX_DBSCANVerification.hpp diff --git a/benchmarks/cluster/CMakeLists.txt b/benchmarks/cluster/CMakeLists.txt new file mode 100644 index 000000000..f5291b3cf --- /dev/null +++ b/benchmarks/cluster/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(cluster_benchmark_helpers + data.cpp + print_timers.cpp +) +target_link_libraries(cluster_benchmark_helpers PRIVATE ArborX::ArborX) + +set(input_file "input.txt") +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${input_file} ${CMAKE_CURRENT_BINARY_DIR}/${input_file} COPYONLY) + +add_executable(ArborX_Benchmark_DBSCAN.exe dbscan.cpp) +target_include_directories(ArborX_Benchmark_DBSCAN.exe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(ArborX_Benchmark_DBSCAN.exe ArborX::ArborX Boost::program_options cluster_benchmark_helpers) +add_test(NAME ArborX_Benchmark_DBSCAN COMMAND ArborX_Benchmark_DBSCAN.exe --filename=${input_file} --eps=1.4 --verify) + +add_executable(ArborX_Benchmark_MST.exe mst.cpp) +target_include_directories(ArborX_Benchmark_MST.exe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(ArborX_Benchmark_MST.exe ArborX::ArborX Boost::program_options cluster_benchmark_helpers) +add_test(NAME ArborX_Benchmark_HDBSCAN COMMAND ArborX_Benchmark_HDBSCAN.exe --filename=${input_file}) + +add_executable(ArborX_Benchmark_HDBSCAN.exe hdbscan.cpp) +target_include_directories(ArborX_Benchmark_HDBSCAN.exe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(ArborX_Benchmark_HDBSCAN.exe ArborX::ArborX Boost::program_options cluster_benchmark_helpers) +add_test(NAME ArborX_Benchmark_MST COMMAND ArborX_Benchmark_MST.exe --filename=${input_file}) diff --git a/benchmarks/dbscan/README.md b/benchmarks/cluster/README.md similarity index 100% rename from benchmarks/dbscan/README.md rename to benchmarks/cluster/README.md diff --git a/benchmarks/cluster/data.cpp b/benchmarks/cluster/data.cpp new file mode 100644 index 000000000..24ccaf915 --- /dev/null +++ b/benchmarks/cluster/data.cpp @@ -0,0 +1,62 @@ +/**************************************************************************** + * Copyright (c) 2025, 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 "data.hpp" + +#include + +#include +#include + +#include "data_timpl.hpp" + +namespace ArborXBenchmark +{ + +// Explicit instantiations +using MemorySpace = typename Kokkos::DefaultExecutionSpace::memory_space; +#define INSTANTIATE_LOADER(DIM) \ + template Kokkos::View *, MemorySpace> \ + loadData(ArborXBenchmark::Parameters const &); +INSTANTIATE_LOADER(2) +INSTANTIATE_LOADER(3) +INSTANTIATE_LOADER(4) +INSTANTIATE_LOADER(5) +INSTANTIATE_LOADER(6) +#undef INSTANTIATE_LOADER + +int getDataDimension(std::string const &filename, bool binary) +{ + std::ifstream input; + if (!binary) + input.open(filename); + else + input.open(filename, std::ifstream::binary); + if (!input.good()) + throw std::runtime_error("Error reading file \"" + filename + "\""); + + int num_points; + int dim; + if (!binary) + { + input >> num_points; + input >> dim; + } + else + { + input.read(reinterpret_cast(&num_points), sizeof(int)); + input.read(reinterpret_cast(&dim), sizeof(int)); + } + input.close(); + + return dim; +} + +} // namespace ArborXBenchmark diff --git a/benchmarks/cluster/data.hpp b/benchmarks/cluster/data.hpp new file mode 100644 index 000000000..6c137cfd0 --- /dev/null +++ b/benchmarks/cluster/data.hpp @@ -0,0 +1,31 @@ +/**************************************************************************** + * Copyright (c) 2025, 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_BENCHMARK_DATA_HPP +#define ARBORX_BENCHMARK_DATA_HPP + +#include + +#include + +#include "parameters.hpp" + +namespace ArborXBenchmark +{ + +int getDataDimension(std::string const &filename, bool binary); + +template +Kokkos::View *, MemorySpace> +loadData(ArborXBenchmark::Parameters const ¶ms); + +} // namespace ArborXBenchmark + +#endif diff --git a/benchmarks/dbscan/data.hpp b/benchmarks/cluster/data_timpl.hpp similarity index 84% rename from benchmarks/dbscan/data.hpp rename to benchmarks/cluster/data_timpl.hpp index 78c3f33dc..89c46c258 100644 --- a/benchmarks/dbscan/data.hpp +++ b/benchmarks/cluster/data_timpl.hpp @@ -9,16 +9,27 @@ * SPDX-License-Identifier: BSD-3-Clause * ****************************************************************************/ -#ifndef DATA_HPP -#define DATA_HPP +#ifndef ARBORX_BENCHMARK_DATA_TIMPL_HPP +#define ARBORX_BENCHMARK_DATA_TIMPL_HPP #include #include +#include + +#include +#include #include +#include #include #include +#include "data.hpp" +#include "parameters.hpp" + +namespace ArborXBenchmark +{ + using ArborX::Point; template @@ -298,4 +309,42 @@ std::vector> GanTao(int n, bool variable_density = false, return points; } +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 +Kokkos::View *, MemorySpace> +loadData(ArborXBenchmark::Parameters const ¶ms) +{ + if (!params.filename.empty()) + { + // Read in data + printf("filename : %s [%s, max_pts = %d]\n", + params.filename.c_str(), (params.binary ? "binary" : "text"), + params.max_num_points); + printf("samples : %d\n", params.num_samples); + return vec2view(loadData(params.filename, params.binary, + params.max_num_points, + params.num_samples), + "Benchmark::primitives"); + } + + // Generate data + int dim = params.dim; + printf("generator : n = %d, dim = %d, density = %s\n", params.n, dim, + (params.variable_density ? "variable" : "constant")); + return vec2view(GanTao(params.n, params.variable_density), + "Benchmark::primitives"); +} + +} // namespace ArborXBenchmark + #endif diff --git a/benchmarks/cluster/dbscan.cpp b/benchmarks/cluster/dbscan.cpp new file mode 100644 index 000000000..f8cf5a33b --- /dev/null +++ b/benchmarks/cluster/dbscan.cpp @@ -0,0 +1,349 @@ +/**************************************************************************** + * Copyright (c) 2025, 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 "ArborX_DBSCANVerification.hpp" +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include "data.hpp" +#include "parameters.hpp" +#include "print_timers.hpp" + +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 +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"); + + namespace KokkosExt = ArborX::Details::KokkosExt; + + 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_inc(&cluster_sizes(labels(i))); + }); + + // 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; + constexpr int IGNORED_CLUSTER = -1; + int num_clusters; + KokkosExt::reallocWithoutInitializing(exec_space, 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); + KokkosExt::exclusive_scan(exec_space, cluster_offset, cluster_offset, 0); + + auto cluster_starts = KokkosExt::clone(exec_space, cluster_offset); + KokkosExt::reallocWithoutInitializing( + exec_space, cluster_indices, + KokkosExt::lastElement(exec_space, 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 +bool run_dbscan(ExecutionSpace const &exec_space, Primitives const &primitives, + ArborXBenchmark::Parameters const ¶ms) +{ + using MemorySpace = typename Primitives::memory_space; + + if (params.verbose) + { + Kokkos::Profiling::Experimental::set_push_region_callback( + ArborXBenchmark::push_region); + Kokkos::Profiling::Experimental::set_pop_region_callback( + ArborXBenchmark::pop_region); + } + + Kokkos::View labels("Example::labels", 0); + using ArborX::DBSCAN::Implementation; + Implementation implementation = Implementation::FDBSCAN; + if (params.implementation == "fdbscan-densebox") + implementation = Implementation::FDBSCAN_DenseBox; + + ArborX::DBSCAN::Parameters dbscan_params; + dbscan_params.setVerbosity(params.verbose).setImplementation(implementation); + + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::total"); + + labels = ArborX::dbscan(exec_space, primitives, params.eps, + params.core_min_size, dbscan_params); + + Kokkos::Profiling::pushRegion("ArborX::DBSCAN::postprocess"); + Kokkos::View cluster_indices("Testing::cluster_indices", + 0); + Kokkos::View cluster_offset("Testing::cluster_offset", 0); + sortAndFilterClusters(exec_space, labels, cluster_indices, cluster_offset, + params.cluster_min_size); + Kokkos::Profiling::popRegion(); + + Kokkos::Profiling::popRegion(); + + if (params.verbose) + { + bool const is_special_case = (params.core_min_size == 2); + + if (implementation == ArborX::DBSCAN::Implementation::FDBSCAN_DenseBox) + printf("-- dense cells : %10.3f\n", + ArborXBenchmark::get_time("ArborX::DBSCAN::dense_cells")); + printf("-- construction : %10.3f\n", + ArborXBenchmark::get_time("ArborX::DBSCAN::tree_construction")); + printf("-- query+cluster : %10.3f\n", + ArborXBenchmark::get_time("ArborX::DBSCAN::clusters")); + if (!is_special_case) + { + printf("---- neigh : %10.3f\n", + ArborXBenchmark::get_time("ArborX::DBSCAN::clusters::num_neigh")); + printf("---- query : %10.3f\n", + ArborXBenchmark::get_time("ArborX::DBSCAN::clusters::query")); + } + printf("-- postprocess : %10.3f\n", + ArborXBenchmark::get_time("ArborX::DBSCAN::postprocess")); + printf("total time : %10.3f\n", + ArborXBenchmark::get_time("ArborX::DBSCAN::total")); + } + + int num_points = primitives.extent_int(0); + 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 / num_points)); + int num_noise_points = num_points - num_cluster_points; + printf("#noise points : %d [%.2f%%]\n", num_noise_points, + (100.f * num_noise_points / num_points)); + + bool success = true; + if (params.verify) + { + success = ArborX::Details::verifyDBSCAN(exec_space, primitives, params.eps, + params.core_min_size, labels); + printf("Verification %s\n", (success ? "passed" : "failed")); + } + + if (success && !params.filename_labels.empty()) + writeLabelsData(params.filename_labels, labels); + + return success; +} + +template +std::string vec2string(std::vector const &s, std::string const &delim = ", ") +{ + assert(s.size() > 1); + + std::ostringstream ss; + std::copy(s.begin(), s.end(), + std::ostream_iterator{ss, delim.c_str()}); + auto delimited_items = ss.str().erase(ss.str().length() - delim.size()); + return "(" + delimited_items + ")"; +} + +int main(int argc, char *argv[]) +{ + Kokkos::ScopeGuard guard(argc, argv); + + using ExecutionSpace = Kokkos::DefaultExecutionSpace; + using MemorySpace = ExecutionSpace::memory_space; + + std::cout << "ArborX version : " << ArborX::version() << std::endl; + std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; + std::cout << "Kokkos version : " << ArborX::Details::KokkosExt::version() + << std::endl; + + namespace bpo = boost::program_options; + using namespace ArborXBenchmark; + + Parameters params; + + std::vector allowed_impls = {"fdbscan", "fdbscan-densebox"}; + + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "help message" ) + ( "binary", bpo::bool_switch(¶ms.binary), "binary file indicator") + ( "cluster-min-size", bpo::value(¶ms.cluster_min_size)->default_value(1), "minimum cluster size") + ( "core-min-size", bpo::value(¶ms.core_min_size)->default_value(2), "DBSCAN min_pts") + ( "dimension", bpo::value(¶ms.dim)->default_value(-1), "dimension of points to generate" ) + ( "eps", bpo::value(¶ms.eps), "DBSCAN eps" ) + ( "filename", bpo::value(¶ms.filename), "filename containing data" ) + ( "impl", bpo::value(¶ms.implementation)->default_value("fdbscan"), ("implementation " + vec2string(allowed_impls, " | ")).c_str() ) + ( "labels", bpo::value(¶ms.filename_labels)->default_value(""), "clutering results output" ) + ( "max-num-points", bpo::value(¶ms.max_num_points)->default_value(-1), "max number of points to read in") + ( "n", bpo::value(¶ms.n)->default_value(10), "number of points to generate" ) + ( "samples", bpo::value(¶ms.num_samples)->default_value(-1), "number of samples" ) + ( "variable-density", bpo::bool_switch(¶ms.variable_density), "type of cluster density to generate" ) + ( "verbose", bpo::bool_switch(¶ms.verbose), "verbose") + ( "verify", bpo::bool_switch(¶ms.verify), "verify connected components") + ; + // 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'; + std::cout << "[Generator Help]\n" + "If using generator, the recommended DBSCAN parameters are:\n" + "- core-min-size = 10\n" + "- eps = 60 (2D constant), 100 (2D variable), 200 (3D " + "constant), 400 (3D variable)" + << std::endl; + return 1; + } + + auto found = [](auto const &v, auto x) { + return std::find(v.begin(), v.end(), x) != v.end(); + }; + + if (!found(allowed_impls, params.implementation)) + { + std::cerr << "Implementation must be one of " << vec2string(allowed_impls) + << "\n"; + return 2; + } + + // Print out the runtime parameters + std::stringstream ss; + ss << params.implementation; + printf("eps : %f\n", params.eps); + printf("minpts : %d\n", params.core_min_size); + printf("cluster min size : %d\n", params.cluster_min_size); + if (!params.filename_labels.empty()) + printf("filename [labels] : %s [binary]\n", params.filename_labels.c_str()); + printf("implementation : %s\n", ss.str().c_str()); + printf("verify : %s\n", (params.verify ? "true" : "false")); + printf("verbose : %s\n", (params.verbose ? "true" : "false")); + + ExecutionSpace exec_space; + + int dim = + (params.filename.empty() + ? params.dim + : ArborXBenchmark::getDataDimension(params.filename, params.binary)); +#define SWITCH_DIM(DIM) \ + case DIM: \ + success = run_dbscan(exec_space, \ + ArborXBenchmark::loadData(params), \ + params); \ + break; + bool success = true; + switch (dim) + { + SWITCH_DIM(2) + SWITCH_DIM(3) + SWITCH_DIM(4) + SWITCH_DIM(5) + SWITCH_DIM(6) + default: + std::cerr << "Error: dimension " << dim << " not allowed\n" << std::endl; + } +#undef SWITCH_DIM + + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/benchmarks/dbscan/dbscan.cpp b/benchmarks/cluster/hdbscan.cpp similarity index 50% rename from benchmarks/dbscan/dbscan.cpp rename to benchmarks/cluster/hdbscan.cpp index 23df0eddc..c40d77de7 100644 --- a/benchmarks/dbscan/dbscan.cpp +++ b/benchmarks/cluster/hdbscan.cpp @@ -8,49 +8,81 @@ * * * SPDX-License-Identifier: BSD-3-Clause * ****************************************************************************/ - -#include "dbscan.hpp" - +#include #include #include #include -#include -#include #include #include #include -// FIXME: ideally, this function would be next to `loadData` in -// dbscan_timpl.hpp. However, that file is used for explicit instantiation, -// which would result in multiple duplicate symbols. So it is kept here. -int getDataDimension(std::string const &filename, bool binary) +#include "data.hpp" +#include "parameters.hpp" +#include "print_timers.hpp" + +template +void run_hdbscan(ExecutionSpace const &exec_space, Primitives const &primitives, + ArborXBenchmark::Parameters const ¶ms) { - std::ifstream input; - if (!binary) - input.open(filename); + if (params.verbose) + { + Kokkos::Profiling::Experimental::set_push_region_callback( + ArborXBenchmark::push_region); + Kokkos::Profiling::Experimental::set_pop_region_callback( + ArborXBenchmark::pop_region); + } + + using ArborX::Experimental::DendrogramImplementation; + DendrogramImplementation dendrogram_impl; + if (params.dendrogram == "union-find") + dendrogram_impl = DendrogramImplementation::UNION_FIND; + else if (params.dendrogram == "boruvka") + dendrogram_impl = DendrogramImplementation::BORUVKA; else - input.open(filename, std::ifstream::binary); - if (!input.good()) - throw std::runtime_error("Error reading file \"" + filename + "\""); + { + auto error_string = "Unknown dendogram: \"" + params.dendrogram + "\""; + Kokkos::abort(error_string.c_str()); + return; + } + + Kokkos::Profiling::pushRegion("ArborX::HDBSCAN::total"); + auto dendrogram = ArborX::Experimental::hdbscan( + exec_space, primitives, params.core_min_size, dendrogram_impl); + Kokkos::Profiling::popRegion(); - int num_points; - int dim; - if (!binary) + if (!params.verbose) + return; + + if (params.dendrogram == "boruvka") { - input >> num_points; - input >> dim; + printf("-- construction : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::construction")); + if (params.core_min_size > 1) + printf("-- core distances : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::compute_core_distances")); + printf("-- boruvka : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::boruvka")); + printf("---- sided parents : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::update_sided_parents")); + printf("---- vertex parents : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::compute_vertex_parents")); + printf("-- edge parents : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::compute_edge_parents")); } else { - input.read(reinterpret_cast(&num_points), sizeof(int)); - input.read(reinterpret_cast(&dim), sizeof(int)); + printf("-- mst : %10.3f\n", + ArborXBenchmark::get_time("ArborX::HDBSCAN::mst")); + printf("-- dendrogram : %10.3f\n", + ArborXBenchmark::get_time("ArborX::HDBSCAN::dendrogram")); + printf("---- edge sort : %10.3f\n", + ArborXBenchmark::get_time("ArborX::Dendrogram::sort_edges")); } - input.close(); - - return dim; + printf("total time : %10.3f\n", + ArborXBenchmark::get_time("ArborX::HDBSCAN::total")); } template @@ -69,40 +101,34 @@ int main(int argc, char *argv[]) { Kokkos::ScopeGuard guard(argc, argv); + using ExecutionSpace = Kokkos::DefaultExecutionSpace; + using MemorySpace = ExecutionSpace::memory_space; + std::cout << "ArborX version : " << ArborX::version() << std::endl; std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; std::cout << "Kokkos version : " << ArborX::Details::KokkosExt::version() << std::endl; namespace bpo = boost::program_options; + using namespace ArborXBenchmark; - ArborXBenchmark::Parameters params; - int dim; + Parameters params; - std::vector allowed_algorithms = {"dbscan", "hdbscan", "mst"}; std::vector allowed_dendrograms = {"boruvka", "union-find"}; - std::vector allowed_impls = {"fdbscan", "fdbscan-densebox"}; bpo::options_description desc("Allowed options"); // clang-format off desc.add_options() ( "help", "help message" ) - ( "algorithm", bpo::value(¶ms.algorithm)->default_value("dbscan"), ("algorithm " + vec2string(allowed_algorithms, " | ")).c_str() ) ( "binary", bpo::bool_switch(¶ms.binary), "binary file indicator") - ( "cluster-min-size", bpo::value(¶ms.cluster_min_size)->default_value(1), "minimum cluster size") ( "core-min-size", bpo::value(¶ms.core_min_size)->default_value(2), "DBSCAN min_pts") ( "dendrogram", bpo::value(¶ms.dendrogram)->default_value("boruvka"), ("dendrogram " + vec2string(allowed_dendrograms, " | ")).c_str() ) - ( "dimension", bpo::value(&dim)->default_value(3), "dimension of points to generate" ) - ( "eps", bpo::value(¶ms.eps), "DBSCAN eps" ) + ( "dimension", bpo::value(¶ms.dim)->default_value(-1), "dimension of points to generate" ) ( "filename", bpo::value(¶ms.filename), "filename containing data" ) - ( "impl", bpo::value(¶ms.implementation)->default_value("fdbscan"), ("implementation " + vec2string(allowed_impls, " | ")).c_str() ) - ( "labels", bpo::value(¶ms.filename_labels)->default_value(""), "clutering results output" ) ( "max-num-points", bpo::value(¶ms.max_num_points)->default_value(-1), "max number of points to read in") - ( "n", bpo::value(¶ms.n)->default_value(10), "number of points to generate" ) ( "samples", bpo::value(¶ms.num_samples)->default_value(-1), "number of samples" ) ( "variable-density", bpo::bool_switch(¶ms.variable_density), "type of cluster density to generate" ) ( "verbose", bpo::bool_switch(¶ms.verbose), "verbose") - ( "verify", bpo::bool_switch(¶ms.verify), "verify connected components") ; // clang-format on bpo::variables_map vm; @@ -125,18 +151,6 @@ int main(int argc, char *argv[]) return std::find(v.begin(), v.end(), x) != v.end(); }; - if (!found(allowed_impls, params.implementation)) - { - std::cerr << "Implementation must be one of " << vec2string(allowed_impls) - << "\n"; - return 2; - } - if (!found(allowed_algorithms, params.algorithm)) - { - std::cerr << "Algorithm must be one of " << vec2string(allowed_algorithms) - << "\n"; - return 3; - } if (!found(allowed_dendrograms, params.dendrogram)) { std::cerr << "Dendrogram must be one of " << vec2string(allowed_dendrograms) @@ -144,68 +158,33 @@ int main(int argc, char *argv[]) return 4; } - std::stringstream ss; - ss << params.implementation; - // Print out the runtime parameters - printf("algorithm : %s\n", params.algorithm.c_str()); - if (params.algorithm == "dbscan") - { - printf("eps : %f\n", params.eps); - printf("cluster min size : %d\n", params.cluster_min_size); - printf("implementation : %s\n", ss.str().c_str()); - printf("verify : %s\n", (params.verify ? "true" : "false")); - } - if (params.algorithm == "hdbscan") - { - printf("dendrogram : %s\n", params.dendrogram.c_str()); - } + printf("dendrogram : %s\n", params.dendrogram.c_str()); printf("minpts : %d\n", params.core_min_size); - if (!params.filename.empty()) - { - // Data is read in - printf("filename : %s [%s, max_pts = %d]\n", - params.filename.c_str(), (params.binary ? "binary" : "text"), - params.max_num_points); - printf("samples : %d\n", params.num_samples); - } - else - { - // Data is generated - printf("generator : n = %d, dim = %d, density = %s\n", params.n, - dim, (params.variable_density ? "variable" : "constant")); - } - if (!params.filename_labels.empty()) - printf("filename [labels] : %s [binary]\n", params.filename_labels.c_str()); printf("verbose : %s\n", (params.verbose ? "true" : "false")); - if (!params.filename.empty()) - dim = getDataDimension(params.filename, params.binary); - - using ArborXBenchmark::run; + ExecutionSpace exec_space; - bool success; + int dim = + (params.filename.empty() + ? params.dim + : ArborXBenchmark::getDataDimension(params.filename, params.binary)); +#define SWITCH_DIM(DIM) \ + case DIM: \ + run_hdbscan(exec_space, \ + ArborXBenchmark::loadData(params), params); \ + break; switch (dim) { - case 2: - success = run<2>(params); - break; - case 3: - success = run<3>(params); - break; - case 4: - success = run<4>(params); - break; - case 5: - success = run<5>(params); - break; - case 6: - success = run<6>(params); - break; + SWITCH_DIM(2) + SWITCH_DIM(3) + SWITCH_DIM(4) + SWITCH_DIM(5) + SWITCH_DIM(6) default: std::cerr << "Error: dimension " << dim << " not allowed\n" << std::endl; - success = false; } +#undef SWITCH_DIM - return success ? EXIT_SUCCESS : EXIT_FAILURE; + return 0; } diff --git a/benchmarks/dbscan/input.txt b/benchmarks/cluster/input.txt similarity index 100% rename from benchmarks/dbscan/input.txt rename to benchmarks/cluster/input.txt diff --git a/benchmarks/cluster/mst.cpp b/benchmarks/cluster/mst.cpp new file mode 100644 index 000000000..39ac6f064 --- /dev/null +++ b/benchmarks/cluster/mst.cpp @@ -0,0 +1,137 @@ +/**************************************************************************** + * Copyright (c) 2025, 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 "data.hpp" +#include "parameters.hpp" +#include "print_timers.hpp" + +template +void run_mst(ExecutionSpace const &exec_space, Primitives const &primitives, + ArborXBenchmark::Parameters const ¶ms) +{ + using MemorySpace = typename Primitives::memory_space; + + if (params.verbose) + { + Kokkos::Profiling::Experimental::set_push_region_callback( + ArborXBenchmark::push_region); + Kokkos::Profiling::Experimental::set_pop_region_callback( + ArborXBenchmark::pop_region); + } + + Kokkos::Profiling::pushRegion("ArborX::MST::total"); + ArborX::Experimental::MinimumSpanningTree mst( + exec_space, primitives, params.core_min_size); + Kokkos::Profiling::popRegion(); + + if (!params.verbose) + return; + + printf("-- construction : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::construction")); + if (params.core_min_size > 1) + printf("-- core distances : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::compute_core_distances")); + printf("-- boruvka : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::boruvka")); + printf("total time : %10.3f\n", + ArborXBenchmark::get_time("ArborX::MST::total")); +} + +int main(int argc, char *argv[]) +{ + Kokkos::ScopeGuard guard(argc, argv); + + using ExecutionSpace = Kokkos::DefaultExecutionSpace; + using MemorySpace = ExecutionSpace::memory_space; + + std::cout << "ArborX version : " << ArborX::version() << std::endl; + std::cout << "ArborX hash : " << ArborX::gitCommitHash() << std::endl; + std::cout << "Kokkos version : " << ArborX::Details::KokkosExt::version() + << std::endl; + + namespace bpo = boost::program_options; + using namespace ArborXBenchmark; + + Parameters params; + + std::vector allowed_dendrograms = {"boruvka", "union-find"}; + + bpo::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ( "help", "help message" ) + ( "binary", bpo::bool_switch(¶ms.binary), "binary file indicator") + ( "core-min-size", bpo::value(¶ms.core_min_size)->default_value(2), "DBSCAN min_pts") + ( "dimension", bpo::value(¶ms.dim)->default_value(-1), "dimension of points to generate" ) + ( "filename", bpo::value(¶ms.filename), "filename containing data" ) + ( "max-num-points", bpo::value(¶ms.max_num_points)->default_value(-1), "max number of points to read in") + ( "n", bpo::value(¶ms.n)->default_value(10), "number of points to generate" ) + ( "samples", bpo::value(¶ms.num_samples)->default_value(-1), "number of samples" ) + ( "variable-density", bpo::bool_switch(¶ms.variable_density), "type of cluster density to generate" ) + ( "verbose", bpo::bool_switch(¶ms.verbose), "verbose") + ; + // 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'; + std::cout << "[Generator Help]\n" + "If using generator, the recommended DBSCAN parameters are:\n" + "- core-min-size = 10\n" + "- eps = 60 (2D constant), 100 (2D variable), 200 (3D " + "constant), 400 (3D variable)" + << std::endl; + return 1; + } + + // Print out the runtime parameters + printf("minpts : %d\n", params.core_min_size); + printf("verbose : %s\n", (params.verbose ? "true" : "false")); + + ExecutionSpace exec_space; + + int dim = + (params.filename.empty() + ? params.dim + : ArborXBenchmark::getDataDimension(params.filename, params.binary)); +#define SWITCH_DIM(DIM) \ + case DIM: \ + run_mst(exec_space, ArborXBenchmark::loadData(params), \ + params); \ + break; + switch (dim) + { + SWITCH_DIM(2) + SWITCH_DIM(3) + SWITCH_DIM(4) + SWITCH_DIM(5) + SWITCH_DIM(6) + default: + std::cerr << "Error: dimension " << dim << " not allowed\n" << std::endl; + } +#undef SWITCH_DIM + + return 0; +} diff --git a/benchmarks/dbscan/dbscan.hpp b/benchmarks/cluster/parameters.hpp similarity index 94% rename from benchmarks/dbscan/dbscan.hpp rename to benchmarks/cluster/parameters.hpp index b5b04d186..689e54883 100644 --- a/benchmarks/dbscan/dbscan.hpp +++ b/benchmarks/cluster/parameters.hpp @@ -8,6 +8,8 @@ * * * SPDX-License-Identifier: BSD-3-Clause * ****************************************************************************/ +#ifndef PARAMETERS_HPP +#define PARAMETERS_HPP #include @@ -21,6 +23,7 @@ struct Parameters int cluster_min_size; int core_min_size; std::string dendrogram; + int dim; float eps; std::string filename; std::string filename_labels; @@ -33,7 +36,6 @@ struct Parameters bool verify; }; -template -bool run(Parameters const ¶ms); - } // namespace ArborXBenchmark + +#endif diff --git a/benchmarks/dbscan/print_timers.cpp b/benchmarks/cluster/print_timers.cpp similarity index 91% rename from benchmarks/dbscan/print_timers.cpp rename to benchmarks/cluster/print_timers.cpp index 0ef215db3..890b05141 100644 --- a/benchmarks/dbscan/print_timers.cpp +++ b/benchmarks/cluster/print_timers.cpp @@ -33,14 +33,14 @@ std::vector done_timers; } // namespace -void ArborX_Benchmark::push_region(char const *label) +void ArborXBenchmark::push_region(char const *label) { Kokkos::fence(); auto now = Timer::clock_type::now(); current_timers.push({label, now, {}}); } -void ArborX_Benchmark::pop_region() +void ArborXBenchmark::pop_region() { Kokkos::fence(); auto now = Timer::clock_type::now(); @@ -50,7 +50,7 @@ void ArborX_Benchmark::pop_region() done_timers.push_back(timer); } -double ArborX_Benchmark::get_time(std::string const &label) +double ArborXBenchmark::get_time(std::string const &label) { for (auto const &timer : done_timers) if (timer.label == label) diff --git a/benchmarks/dbscan/print_timers.hpp b/benchmarks/cluster/print_timers.hpp similarity index 93% rename from benchmarks/dbscan/print_timers.hpp rename to benchmarks/cluster/print_timers.hpp index b20ef827d..005646563 100644 --- a/benchmarks/dbscan/print_timers.hpp +++ b/benchmarks/cluster/print_timers.hpp @@ -11,9 +11,9 @@ #include -namespace ArborX_Benchmark +namespace ArborXBenchmark { void push_region(char const *); void pop_region(); double get_time(std::string const &label); -} // namespace ArborX_Benchmark +} // namespace ArborXBenchmark diff --git a/benchmarks/dbscan/CMakeLists.txt b/benchmarks/dbscan/CMakeLists.txt deleted file mode 100644 index 8423bae41..000000000 --- a/benchmarks/dbscan/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -set(EXPLICIT_INSTANTIATION_SOURCE_FILES) -set(TEMPLATE_PARAMETERS 2 3 4 5 6) -foreach(DIM ${TEMPLATE_PARAMETERS}) - set(filename ${CMAKE_CURRENT_BINARY_DIR}/dbscan_${DIM}.cpp) - file(WRITE ${filename} - "#include \"${CMAKE_CURRENT_SOURCE_DIR}/dbscan_timpl.hpp\"\n" - "template bool ArborXBenchmark::run<${DIM}>(ArborXBenchmark::Parameters const&);\n" - ) - list(APPEND EXPLICIT_INSTANTIATION_SOURCE_FILES ${filename}) -endforeach() - -add_executable(ArborX_Benchmark_DBSCAN.exe - ${EXPLICIT_INSTANTIATION_SOURCE_FILES} - print_timers.cpp - dbscan.cpp -) -target_include_directories(ArborX_Benchmark_DBSCAN.exe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(ArborX_Benchmark_DBSCAN.exe ArborX::ArborX Boost::program_options) - -add_executable(ArborX_DataConverter.exe converter.cpp) -target_compile_features(ArborX_DataConverter.exe PRIVATE cxx_std_17) -target_link_libraries(ArborX_DataConverter.exe Boost::program_options) - -set(input_file "input.txt") -add_test(NAME ArborX_Benchmark_DBSCAN COMMAND ArborX_Benchmark_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/benchmarks/dbscan/converter.cpp b/benchmarks/dbscan/converter.cpp deleted file mode 100644 index 13c9f30d6..000000000 --- a/benchmarks/dbscan/converter.cpp +++ /dev/null @@ -1,478 +0,0 @@ -/**************************************************************************** - * Copyright (c) 2025, 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 - -#ifdef _MSC_VER -#define ARBORX_STRTOK_R strtok_s -#else -#define ARBORX_STRTOK_R strtok_r -#endif - -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 = ARBORX_STRTOK_R(line, "\"[", &end_str); - while (pch != nullptr) - { - if (wordNo > 0) - { - char *pch2; - char *end_str2; - - pch2 = ARBORX_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 = ARBORX_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 = ARBORX_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/benchmarks/dbscan/dbscan_timpl.hpp b/benchmarks/dbscan/dbscan_timpl.hpp deleted file mode 100644 index 58921c37b..000000000 --- a/benchmarks/dbscan/dbscan_timpl.hpp +++ /dev/null @@ -1,343 +0,0 @@ -/**************************************************************************** - * Copyright (c) 2025, 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 "data.hpp" -#include "dbscan.hpp" -#include "print_timers.hpp" - -using ArborX::Point; - -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 -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"); - - namespace KokkosExt = ArborX::Details::KokkosExt; - - 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_inc(&cluster_sizes(labels(i))); - }); - - // 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; - constexpr int IGNORED_CLUSTER = -1; - int num_clusters; - KokkosExt::reallocWithoutInitializing(exec_space, 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); - KokkosExt::exclusive_scan(exec_space, cluster_offset, cluster_offset, 0); - - auto cluster_starts = KokkosExt::clone(exec_space, cluster_offset); - KokkosExt::reallocWithoutInitializing( - exec_space, cluster_indices, - KokkosExt::lastElement(exec_space, 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 -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 -bool ArborXBenchmark::run(ArborXBenchmark::Parameters const ¶ms) -{ - using ExecutionSpace = Kokkos::DefaultExecutionSpace; - using MemorySpace = typename ExecutionSpace::memory_space; - - if (params.verbose) - { - Kokkos::Profiling::Experimental::set_push_region_callback( - ArborX_Benchmark::push_region); - Kokkos::Profiling::Experimental::set_pop_region_callback( - ArborX_Benchmark::pop_region); - } - - ExecutionSpace exec_space; - - std::vector> data; - if (!params.filename.empty()) - { - // Read in data - data = loadData(params.filename, params.binary, params.max_num_points, - params.num_samples); - } - else - { - // Generate data - data = GanTao(params.n, params.variable_density); - } - - auto const primitives = vec2view(data, "Benchmark::primitives"); - - using Primitives = decltype(primitives); - - Kokkos::View labels("Example::labels", 0); - bool success = true; - if (params.algorithm == "dbscan") - { - using ArborX::DBSCAN::Implementation; - Implementation implementation = Implementation::FDBSCAN; - if (params.implementation == "fdbscan-densebox") - implementation = Implementation::FDBSCAN_DenseBox; - - ArborX::DBSCAN::Parameters dbscan_params; - dbscan_params.setVerbosity(params.verbose) - .setImplementation(implementation); - - Kokkos::Profiling::pushRegion("ArborX::DBSCAN::total"); - - labels = ArborX::dbscan( - exec_space, primitives, params.eps, params.core_min_size, - dbscan_params); - - Kokkos::Profiling::pushRegion("ArborX::DBSCAN::postprocess"); - Kokkos::View cluster_indices("Testing::cluster_indices", - 0); - Kokkos::View cluster_offset("Testing::cluster_offset", - 0); - sortAndFilterClusters(exec_space, labels, cluster_indices, cluster_offset, - params.cluster_min_size); - Kokkos::Profiling::popRegion(); - - Kokkos::Profiling::popRegion(); - - if (params.verbose) - { - bool const is_special_case = (params.core_min_size == 2); - - if (implementation == ArborX::DBSCAN::Implementation::FDBSCAN_DenseBox) - printf("-- dense cells : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::DBSCAN::dense_cells")); - printf("-- construction : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::DBSCAN::tree_construction")); - printf("-- query+cluster : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::DBSCAN::clusters")); - if (!is_special_case) - { - printf( - "---- neigh : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::DBSCAN::clusters::num_neigh")); - printf("---- query : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::DBSCAN::clusters::query")); - } - printf("-- postprocess : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::DBSCAN::postprocess")); - printf("total time : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::DBSCAN::total")); - } - - int num_points = primitives.extent_int(0); - 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 / num_points)); - int num_noise_points = num_points - num_cluster_points; - printf("#noise points : %d [%.2f%%]\n", num_noise_points, - (100.f * num_noise_points / num_points)); - - if (params.verify) - { - success = ArborX::Details::verifyDBSCAN( - exec_space, primitives, params.eps, params.core_min_size, labels); - printf("Verification %s\n", (success ? "passed" : "failed")); - } - } - else if (params.algorithm == "hdbscan") - { - using ArborX::Experimental::DendrogramImplementation; - DendrogramImplementation dendrogram_impl; - if (params.dendrogram == "union-find") - dendrogram_impl = DendrogramImplementation::UNION_FIND; - else if (params.dendrogram == "boruvka") - dendrogram_impl = DendrogramImplementation::BORUVKA; - else - { - auto error_string = "Unknown dendogram: \"" + params.dendrogram + "\""; - Kokkos::abort(error_string.c_str()); - return false; - } - - Kokkos::Profiling::pushRegion("ArborX::HDBSCAN::total"); - auto dendrogram = ArborX::Experimental::hdbscan( - exec_space, primitives, params.core_min_size, dendrogram_impl); - Kokkos::Profiling::popRegion(); - - if (params.verbose) - { - if (params.dendrogram == "boruvka") - { - printf("-- construction : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::construction")); - if (params.core_min_size > 1) - printf("-- core distances : %10.3f\n", - ArborX_Benchmark::get_time( - "ArborX::MST::compute_core_distances")); - printf("-- boruvka : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::boruvka")); - printf("---- sided parents : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::update_sided_parents")); - printf( - "---- vertex parents : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::compute_vertex_parents")); - printf("-- edge parents : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::compute_edge_parents")); - } - else - { - printf("-- mst : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::HDBSCAN::mst")); - printf("-- dendrogram : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::HDBSCAN::dendrogram")); - printf("---- edge sort : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::Dendrogram::sort_edges")); - } - printf("total time : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::HDBSCAN::total")); - } - } - else if (params.algorithm == "mst") - { - - Kokkos::Profiling::pushRegion("ArborX::MST::total"); - ArborX::Experimental::MinimumSpanningTree mst( - exec_space, primitives, params.core_min_size); - Kokkos::Profiling::popRegion(); - - if (params.verbose) - { - printf("-- construction : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::construction")); - if (params.core_min_size > 1) - printf( - "-- core distances : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::compute_core_distances")); - printf("-- boruvka : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::boruvka")); - printf("total time : %10.3f\n", - ArborX_Benchmark::get_time("ArborX::MST::total")); - } - } - - if (success && !params.filename_labels.empty()) - writeLabelsData(params.filename_labels, labels); - - return success; -} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d2ee370ac..15ab93886 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -201,7 +201,7 @@ add_executable(ArborX_Test_Clustering.exe utf_main.cpp ) target_link_libraries(ArborX_Test_Clustering.exe PRIVATE ArborX Boost::unit_test_framework Boost::dynamic_linking) -target_include_directories(ArborX_Test_Clustering.exe PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/benchmarks/dbscan) +target_include_directories(ArborX_Test_Clustering.exe PRIVATE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/benchmarks/cluster) add_test(NAME ArborX_Test_Clustering COMMAND ArborX_Test_Clustering.exe) # compare results with a dataset of 1000 points from mlpack