diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index ab66caeec..b05030cef 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -290,9 +290,14 @@ target_compile_options( add_library( cuvs SHARED src/cluster/kmeans_balanced_fit_float.cu + src/cluster/kmeans_fit_mg_float.cu + src/cluster/kmeans_fit_mg_double.cu + src/cluster/kmeans_fit_double.cu src/cluster/kmeans_fit_float.cu src/cluster/kmeans_auto_find_k_float.cu + src/cluster/kmeans_fit_predict_double.cu src/cluster/kmeans_fit_predict_float.cu + src/cluster/kmeans_predict_double.cu src/cluster/kmeans_predict_float.cu src/cluster/kmeans_balanced_fit_float.cu src/cluster/kmeans_balanced_fit_predict_float.cu @@ -300,8 +305,10 @@ add_library( src/cluster/kmeans_balanced_fit_int8.cu src/cluster/kmeans_balanced_fit_predict_int8.cu src/cluster/kmeans_balanced_predict_int8.cu + src/cluster/kmeans_transform_double.cu src/cluster/kmeans_transform_float.cu src/cluster/single_linkage_float.cu + src/core/bitset.cu src/distance/detail/pairwise_matrix/dispatch_canberra_float_float_float_int.cu src/distance/detail/pairwise_matrix/dispatch_canberra_half_float_float_int.cu src/distance/detail/pairwise_matrix/dispatch_canberra_double_double_double_int.cu @@ -342,6 +349,8 @@ add_library( src/distance/detail/pairwise_matrix/dispatch_russel_rao_half_float_float_int.cu src/distance/detail/pairwise_matrix/dispatch_russel_rao_double_double_double_int.cu src/distance/detail/pairwise_matrix/dispatch_rbf.cu + src/distance/detail/pairwise_matrix/dispatch_l2_expanded_double_double_double_int64_t.cu + src/distance/detail/pairwise_matrix/dispatch_l2_expanded_float_float_float_int64_t.cu src/distance/detail/fused_distance_nn.cu src/distance/distance.cu src/distance/pairwise_distance.cu @@ -398,15 +407,12 @@ add_library( src/neighbors/ivf_pq/detail/ivf_pq_search_half_int64_t.cu src/neighbors/ivf_pq/detail/ivf_pq_search_int8_t_int64_t.cu src/neighbors/ivf_pq/detail/ivf_pq_search_uint8_t_int64_t.cu - src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_float_int64_t.cu - src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_half_int64_t.cu - src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_int8_t_int64_t.cu - src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_uint8_t_int64_t.cu src/neighbors/nn_descent.cu src/neighbors/nn_descent_float.cu src/neighbors/nn_descent_half.cu src/neighbors/nn_descent_int8.cu src/neighbors/nn_descent_uint8.cu + src/neighbors/reachability.cu src/neighbors/refine/detail/refine_device_float_float.cu src/neighbors/refine/detail/refine_device_half_float.cu src/neighbors/refine/detail/refine_device_int8_t_float.cu @@ -423,14 +429,13 @@ add_library( src/neighbors/vamana_serialize_uint8.cu src/neighbors/vamana_serialize_int8.cu src/selection/select_k_float_int64_t.cu + src/selection/select_k_float_int32_t.cu src/selection/select_k_float_uint32_t.cu src/selection/select_k_half_uint32_t.cu src/stats/silhouette_score.cu src/stats/trustworthiness_score.cu ) -target_compile_definitions(cuvs PRIVATE "CUVS_EXPLICIT_INSTANTIATE_ONLY") - target_compile_options( cuvs INTERFACE $<$:--expt-extended-lambda --expt-relaxed-constexpr> diff --git a/cpp/bench/ann/CMakeLists.txt b/cpp/bench/ann/CMakeLists.txt index 3224587e4..8cbf8c8b3 100644 --- a/cpp/bench/ann/CMakeLists.txt +++ b/cpp/bench/ann/CMakeLists.txt @@ -174,8 +174,6 @@ function(ConfigureAnnBench) ) endif() - target_compile_definitions(${BENCH_NAME} PRIVATE "CUVS_EXPLICIT_INSTANTIATE_ONLY") - target_include_directories( ${BENCH_NAME} PUBLIC "$" diff --git a/cpp/bench/ann/src/cuvs/cuvs_ann_bench_utils.h b/cpp/bench/ann/src/cuvs/cuvs_ann_bench_utils.h index 92274e263..11e0e4ad3 100644 --- a/cpp/bench/ann/src/cuvs/cuvs_ann_bench_utils.h +++ b/cpp/bench/ann/src/cuvs/cuvs_ann_bench_utils.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -75,13 +76,14 @@ inline auto rmm_oom_callback(std::size_t bytes, void*) -> bool */ class shared_raft_resources { public: - using pool_mr_type = rmm::mr::pool_memory_resource; - using mr_type = rmm::mr::failure_callback_resource_adaptor; + using pool_mr_type = rmm::mr::pool_memory_resource; + using mr_type = rmm::mr::failure_callback_resource_adaptor; + using large_mr_type = rmm::mr::managed_memory_resource; shared_raft_resources() try : orig_resource_{rmm::mr::get_current_device_resource()}, pool_resource_(orig_resource_, 1024 * 1024 * 1024ull), - resource_(&pool_resource_, rmm_oom_callback, nullptr) { + resource_(&pool_resource_, rmm_oom_callback, nullptr), large_mr_() { rmm::mr::set_current_device_resource(&resource_); } catch (const std::exception& e) { auto cuda_status = cudaGetLastError(); @@ -104,10 +106,16 @@ class shared_raft_resources { ~shared_raft_resources() noexcept { rmm::mr::set_current_device_resource(orig_resource_); } + auto get_large_memory_resource() noexcept + { + return static_cast(&large_mr_); + } + private: rmm::mr::device_memory_resource* orig_resource_; pool_mr_type pool_resource_; mr_type resource_; + large_mr_type large_mr_; }; /** @@ -130,6 +138,12 @@ class configured_raft_resources { res_{std::make_unique( rmm::cuda_stream_view(get_stream_from_global_pool()))} { + // set the large workspace resource to the raft handle, but without the deleter + // (this resource is managed by the shared_res). + raft::resource::set_large_workspace_resource( + *res_, + std::shared_ptr(shared_res_->get_large_memory_resource(), + raft::void_op{})); } /** Default constructor creates all resources anew. */ diff --git a/cpp/bench/ann/src/cuvs/cuvs_brute_force_knn.cu b/cpp/bench/ann/src/cuvs/cuvs_brute_force_knn.cu index 4c38b3420..55d5b8c70 100644 --- a/cpp/bench/ann/src/cuvs/cuvs_brute_force_knn.cu +++ b/cpp/bench/ann/src/cuvs/cuvs_brute_force_knn.cu @@ -134,7 +134,7 @@ class BruteForceKNNBenchmark { search_queries.data(), params_.num_queries, params_.dim), indices, distances, - std::nullopt); + cuvs::neighbors::filtering::none_sample_filter{}); flush_l2_cache(); raft::resource::sync_stream(handle_, stream_); } @@ -158,7 +158,7 @@ class BruteForceKNNBenchmark { search_queries.data(), params_.num_queries, params_.dim), indices, distances, - std::nullopt); + cuvs::neighbors::filtering::none_sample_filter{}); raft::resource::sync_stream(handle_, stream_); end = std::chrono::high_resolution_clock::now(); search_dur = end - start; @@ -178,7 +178,7 @@ class BruteForceKNNBenchmark { search_queries.data(), params_.num_queries, params_.dim), indices, distances, - std::nullopt); + cuvs::neighbors::filtering::none_sample_filter{}); flush_l2_cache(); raft::resource::sync_stream(handle_, stream_); } @@ -202,7 +202,7 @@ class BruteForceKNNBenchmark { search_queries.data(), params_.num_queries, params_.dim), indices, distances, - std::nullopt); + cuvs::neighbors::filtering::none_sample_filter{}); raft::resource::sync_stream(handle_, stream_); end = std::chrono::high_resolution_clock::now(); search_dur = end - start; diff --git a/cpp/bench/ann/src/cuvs/cuvs_wrapper.h b/cpp/bench/ann/src/cuvs/cuvs_wrapper.h index ea052533d..bf0fa5934 100644 --- a/cpp/bench/ann/src/cuvs/cuvs_wrapper.h +++ b/cpp/bench/ann/src/cuvs/cuvs_wrapper.h @@ -155,8 +155,12 @@ void cuvs_gpu::search( raft::make_device_matrix_view(neighbors, batch_size, k); auto distances_view = raft::make_device_matrix_view(distances, batch_size, k); - cuvs::neighbors::brute_force::search( - handle_, *index_, queries_view, neighbors_view, distances_view, std::nullopt); + cuvs::neighbors::brute_force::search(handle_, + *index_, + queries_view, + neighbors_view, + distances_view, + cuvs::neighbors::filtering::none_sample_filter{}); } template diff --git a/cpp/include/cuvs/cluster/kmeans.hpp b/cpp/include/cuvs/cluster/kmeans.hpp index 75205fa4f..89b3acc24 100644 --- a/cpp/include/cuvs/cluster/kmeans.hpp +++ b/cpp/include/cuvs/cluster/kmeans.hpp @@ -153,7 +153,7 @@ struct balanced_params : base_params { * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::params params; + * cuvs::cluster::kmeans::params params; * int n_features = 15, inertia, n_iter; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * @@ -203,7 +203,159 @@ void fit(raft::resources const& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::params params; + * cuvs::cluster::kmeans::params params; + * int64_t n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, + * n_features); + * + * kmeans::fit(handle, + * params, + * X, + * std::nullopt, + * centroids, + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must + * be in row-major format. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[inout] centroids [in] When init is InitMethod::Array, use + * centroids as the initial cluster centers. + * [out] The generated centroids from the + * kmeans algorithm are stored at the address + * pointed by 'centroids'. + * [dim = n_clusters x n_features] + * @param[out] inertia Sum of squared distances of samples to their + * closest cluster center. + * @param[out] n_iter Number of iterations run. + */ +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +/** + * @brief Find clusters with k-means algorithm. + * Initial centroids are chosen with k-means++ algorithm. Empty + * clusters are reinitialized by choosing new centroids with + * k-means++ algorithm. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; + * int n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); + * + * kmeans::fit(handle, + * params, + * X, + * std::nullopt, + * centroids, + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must + * be in row-major format. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[inout] centroids [in] When init is InitMethod::Array, use + * centroids as the initial cluster centers. + * [out] The generated centroids from the + * kmeans algorithm are stored at the address + * pointed by 'centroids'. + * [dim = n_clusters x n_features] + * @param[out] inertia Sum of squared distances of samples to their + * closest cluster center. + * @param[out] n_iter Number of iterations run. + */ +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +/** + * @brief Find clusters with k-means algorithm. + * Initial centroids are chosen with k-means++ algorithm. Empty + * clusters are reinitialized by choosing new centroids with + * k-means++ algorithm. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; + * int64_t n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, + * n_features); + * + * kmeans::fit(handle, + * params, + * X, + * std::nullopt, + * centroids, + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must + * be in row-major format. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[inout] centroids [in] When init is InitMethod::Array, use + * centroids as the initial cluster centers. + * [out] The generated centroids from the + * kmeans algorithm are stored at the address + * pointed by 'centroids'. + * [dim = n_clusters x n_features] + * @param[out] inertia Sum of squared distances of samples to their + * closest cluster center. + * @param[out] n_iter Number of iterations run. + */ +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +/** + * @brief Find clusters with k-means algorithm. + * Initial centroids are chosen with k-means++ algorithm. Empty + * clusters are reinitialized by choosing new centroids with + * k-means++ algorithm. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; * int n_features = 15, inertia, n_iter; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * @@ -250,7 +402,7 @@ void fit(raft::resources const& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::balanced_params params; + * cuvs::cluster::kmeans::balanced_params params; * int n_features = 15; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * @@ -284,7 +436,7 @@ void fit(const raft::resources& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::balanced_params params; + * cuvs::cluster::kmeans::balanced_params params; * int n_features = 15; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * @@ -308,7 +460,6 @@ void fit(const raft::resources& handle, cuvs::cluster::kmeans::balanced_params const& params, raft::device_matrix_view X, raft::device_matrix_view centroids); - /** * @brief Predict the closest cluster each sample in X belongs to. * @@ -318,7 +469,7 @@ void fit(const raft::resources& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::params params; + * cuvs::cluster::kmeans::params params; * int n_features = 15, inertia, n_iter; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * @@ -363,7 +514,7 @@ void predict(raft::resources const& handle, raft::device_matrix_view X, std::optional> sample_weight, raft::device_matrix_view centroids, - raft::device_vector_view labels, + raft::device_vector_view labels, bool normalize_weight, raft::host_scalar_view inertia); @@ -376,7 +527,7 @@ void predict(raft::resources const& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::params params; + * cuvs::cluster::kmeans::params params; * int n_features = 15, inertia, n_iter; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * @@ -388,7 +539,7 @@ void predict(raft::resources const& handle, * raft::make_scalar_view(&inertia), * raft::make_scalar_view(&n_iter)); * ... - * auto labels = raft::make_device_vector(handle, X.extent(0)); + * auto labels = raft::make_device_vector(handle, X.extent(0)); * * kmeans::predict(handle, * params, @@ -404,18 +555,26 @@ void predict(raft::resources const& handle, * @param[in] params Parameters for KMeans model. * @param[in] X New data to predict. * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] * @param[in] centroids Cluster centroids. The data must be in * row-major format. * [dim = n_clusters x n_features] + * @param[in] normalize_weight True if the weights should be normalized * @param[out] labels Index of the cluster each sample in X * belongs to. * [len = n_samples] + * @param[out] inertia Sum of squared distances of samples to + * their closest cluster center. */ -void predict(const raft::resources& handle, - cuvs::cluster::kmeans::balanced_params const& params, - raft::device_matrix_view X, +void predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, raft::device_matrix_view centroids, - raft::device_vector_view labels); + raft::device_vector_view labels, + bool normalize_weight, + raft::host_scalar_view inertia); /** * @brief Predict the closest cluster each sample in X belongs to. @@ -426,22 +585,144 @@ void predict(const raft::resources& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::balanced_params params; - * int n_features = 15; + * cuvs::cluster::kmeans::params params; + * int n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); + * + * kmeans::fit(handle, + * params, + * X, + * std::nullopt, + * centroids.view(), + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * ... + * auto labels = raft::make_device_vector(handle, X.extent(0)); + * + * kmeans::predict(handle, + * params, + * X, + * std::nullopt, + * centroids.view(), + * false, + * labels.view(), + * raft::make_scalar_view(&ineratia)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X New data to predict. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[in] centroids Cluster centroids. The data must be in + * row-major format. + * [dim = n_clusters x n_features] + * @param[in] normalize_weight True if the weights should be normalized + * @param[out] labels Index of the cluster each sample in X + * belongs to. + * [len = n_samples] + * @param[out] inertia Sum of squared distances of samples to + * their closest cluster center. + */ +void predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::device_vector_view labels, + bool normalize_weight, + raft::host_scalar_view inertia); + +/** + * @brief Predict the closest cluster each sample in X belongs to. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; + * int n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); + * + * kmeans::fit(handle, + * params, + * X, + * std::nullopt, + * centroids.view(), + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * ... + * auto labels = raft::make_device_vector(handle, X.extent(0)); + * + * kmeans::predict(handle, + * params, + * X, + * std::nullopt, + * centroids.view(), + * false, + * labels.view(), + * raft::make_scalar_view(&ineratia)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X New data to predict. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[in] centroids Cluster centroids. The data must be in + * row-major format. + * [dim = n_clusters x n_features] + * @param[in] normalize_weight True if the weights should be normalized + * @param[out] labels Index of the cluster each sample in X + * belongs to. + * [len = n_samples] + * @param[out] inertia Sum of squared distances of samples to + * their closest cluster center. + */ +void predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::device_vector_view labels, + bool normalize_weight, + raft::host_scalar_view inertia); + +/** + * @brief Predict the closest cluster each sample in X belongs to. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; + * int n_features = 15, inertia, n_iter; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * * kmeans::fit(handle, * params, * X, - * centroids.view()); + * std::nullopt, + * centroids.view(), + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); * ... * auto labels = raft::make_device_vector(handle, X.extent(0)); * * kmeans::predict(handle, * params, * X, + * std::nullopt, * centroids.view(), - * labels.view()); + * false, + * labels.view(), + * raft::make_scalar_view(&ineratia)); * @endcode * * @param[in] handle The raft handle. @@ -457,7 +738,7 @@ void predict(const raft::resources& handle, */ void predict(const raft::resources& handle, cuvs::cluster::kmeans::balanced_params const& params, - raft::device_matrix_view X, + raft::device_matrix_view X, raft::device_matrix_view centroids, raft::device_vector_view labels); @@ -471,7 +752,7 @@ void predict(const raft::resources& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::params params; + * cuvs::cluster::kmeans::params params; * int n_features = 15, inertia, n_iter; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * auto labels = raft::make_device_vector(handle, X.extent(0)); @@ -516,6 +797,171 @@ void fit_predict(raft::resources const& handle, raft::host_scalar_view inertia, raft::host_scalar_view n_iter); +/** + * @brief Compute k-means clustering and predicts cluster index for each sample + * in the input. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; + * int64_t n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, + * n_features); auto labels = raft::make_device_vector(handle, X.extent(0)); + * + * kmeans::fit_predict(handle, + * params, + * X, + * std::nullopt, + * centroids.view(), + * labels.view(), + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must be + * in row-major format. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[inout] centroids Optional + * [in] When init is InitMethod::Array, use + * centroids as the initial cluster centers + * [out] The generated centroids from the + * kmeans algorithm are stored at the address + * pointed by 'centroids'. + * [dim = n_clusters x n_features] + * @param[out] labels Index of the cluster each sample in X belongs + * to. + * [len = n_samples] + * @param[out] inertia Sum of squared distances of samples to their + * closest cluster center. + * @param[out] n_iter Number of iterations run. + */ +void fit_predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + std::optional> centroids, + raft::device_vector_view labels, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +/** + * @brief Compute k-means clustering and predicts cluster index for each sample + * in the input. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; + * int n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); + * auto labels = raft::make_device_vector(handle, X.extent(0)); + * + * kmeans::fit_predict(handle, + * params, + * X, + * std::nullopt, + * centroids.view(), + * labels.view(), + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must be + * in row-major format. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[inout] centroids Optional + * [in] When init is InitMethod::Array, use + * centroids as the initial cluster centers + * [out] The generated centroids from the + * kmeans algorithm are stored at the address + * pointed by 'centroids'. + * [dim = n_clusters x n_features] + * @param[out] labels Index of the cluster each sample in X belongs + * to. + * [len = n_samples] + * @param[out] inertia Sum of squared distances of samples to their + * closest cluster center. + * @param[out] n_iter Number of iterations run. + */ +void fit_predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + std::optional> centroids, + raft::device_vector_view labels, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +/** + * @brief Compute k-means clustering and predicts cluster index for each sample + * in the input. + * + * @code{.cpp} + * #include + * #include + * using namespace cuvs::cluster; + * ... + * raft::resources handle; + * cuvs::cluster::kmeans::params params; + * int64_t n_features = 15, inertia, n_iter; + * auto centroids = raft::make_device_matrix(handle, params.n_clusters, + * n_features); auto labels = raft::make_device_vector(handle, X.extent(0)); + * + * kmeans::fit_predict(handle, + * params, + * X, + * std::nullopt, + * centroids.view(), + * labels.view(), + * raft::make_scalar_view(&inertia), + * raft::make_scalar_view(&n_iter)); + * @endcode + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must be + * in row-major format. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[inout] centroids Optional + * [in] When init is InitMethod::Array, use + * centroids as the initial cluster centers + * [out] The generated centroids from the + * kmeans algorithm are stored at the address + * pointed by 'centroids'. + * [dim = n_clusters x n_features] + * @param[out] labels Index of the cluster each sample in X belongs + * to. + * [len = n_samples] + * @param[out] inertia Sum of squared distances of samples to their + * closest cluster center. + * @param[out] n_iter Number of iterations run. + */ +void fit_predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + std::optional> centroids, + raft::device_vector_view labels, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + /** * @brief Compute balanced k-means clustering and predicts cluster index for each sample * in the input. @@ -526,7 +972,7 @@ void fit_predict(raft::resources const& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::balanced_params params; + * cuvs::cluster::kmeans::balanced_params params; * int n_features = 15; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * auto labels = raft::make_device_vector(handle, X.extent(0)); @@ -570,7 +1016,7 @@ void fit_predict(const raft::resources& handle, * using namespace cuvs::cluster; * ... * raft::resources handle; - * cuvs::cluster::kmeans::balanced_params params; + * cuvs::cluster::kmeans::balanced_params params; * int n_features = 15; * auto centroids = raft::make_device_matrix(handle, params.n_clusters, n_features); * auto labels = raft::make_device_vector(handle, X.extent(0)); @@ -623,6 +1069,24 @@ void transform(raft::resources const& handle, raft::device_matrix_view centroids, raft::device_matrix_view X_new); +/** + * @brief Transform X to a cluster-distance space. + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must + * be in row-major format + * [dim = n_samples x n_features] + * @param[in] centroids Cluster centroids. The data must be in row-major format. + * [dim = n_clusters x n_features] + * @param[out] X_new X transformed in the new space. + * [dim = n_samples x n_features] + */ +void transform(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + raft::device_matrix_view centroids, + raft::device_matrix_view X_new); /** * @} */ diff --git a/cpp/include/cuvs/core/bitset.hpp b/cpp/include/cuvs/core/bitset.hpp index 99942e21c..8236bbf07 100644 --- a/cpp/include/cuvs/core/bitset.hpp +++ b/cpp/include/cuvs/core/bitset.hpp @@ -18,6 +18,12 @@ #include +extern template struct raft::core::bitset; +extern template struct raft::core::bitset; +extern template struct raft::core::bitset; +extern template struct raft::core::bitset; +extern template struct raft::core::bitset; + namespace cuvs::core { /* To use bitset functions containing CUDA code, include */ diff --git a/cpp/include/cuvs/neighbors/brute_force.hpp b/cpp/include/cuvs/neighbors/brute_force.hpp index 5408eb1a0..428fa592a 100644 --- a/cpp/include/cuvs/neighbors/brute_force.hpp +++ b/cpp/include/cuvs/neighbors/brute_force.hpp @@ -291,7 +291,8 @@ void search(raft::resources const& handle, raft::device_matrix_view queries, raft::device_matrix_view neighbors, raft::device_matrix_view distances, - std::optional> sample_filter); + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -326,7 +327,8 @@ void search(raft::resources const& handle, raft::device_matrix_view queries, raft::device_matrix_view neighbors, raft::device_matrix_view distances, - std::optional> sample_filter); + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. * @@ -346,7 +348,8 @@ void search(raft::resources const& handle, raft::device_matrix_view queries, raft::device_matrix_view neighbors, raft::device_matrix_view distances, - std::optional> sample_filter); + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. * @@ -366,7 +369,8 @@ void search(raft::resources const& handle, raft::device_matrix_view queries, raft::device_matrix_view neighbors, raft::device_matrix_view distances, - std::optional> sample_filter); + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @} */ diff --git a/cpp/include/cuvs/neighbors/cagra.hpp b/cpp/include/cuvs/neighbors/cagra.hpp index 20db7e8b7..e48050756 100644 --- a/cpp/include/cuvs/neighbors/cagra.hpp +++ b/cpp/include/cuvs/neighbors/cagra.hpp @@ -1055,6 +1055,8 @@ void extend( * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& res, @@ -1062,7 +1064,9 @@ void search(raft::resources const& res, const cuvs::neighbors::cagra::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1077,13 +1081,17 @@ void search(raft::resources const& res, * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& res, cuvs::neighbors::cagra::search_params const& params, const cuvs::neighbors::cagra::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1098,13 +1106,17 @@ void search(raft::resources const& res, * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& res, cuvs::neighbors::cagra::search_params const& params, const cuvs::neighbors::cagra::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1119,13 +1131,18 @@ void search(raft::resources const& res, * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& res, cuvs::neighbors::cagra::search_params const& params, const cuvs::neighbors::cagra::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); + /** * @} */ diff --git a/cpp/include/cuvs/neighbors/common.h b/cpp/include/cuvs/neighbors/common.h index 02cbeea96..d7ca878b9 100644 --- a/cpp/include/cuvs/neighbors/common.h +++ b/cpp/include/cuvs/neighbors/common.h @@ -44,7 +44,7 @@ enum cuvsFilterType { }; /** - * @brief Struct to hold address of cuvs::neighbor::prefilter and its type + * @brief Struct to hold address of cuvs::neighbors::prefilter and its type * */ typedef struct { diff --git a/cpp/include/cuvs/neighbors/common.hpp b/cpp/include/cuvs/neighbors/common.hpp index 8218b5f52..73ce80b41 100644 --- a/cpp/include/cuvs/neighbors/common.hpp +++ b/cpp/include/cuvs/neighbors/common.hpp @@ -383,8 +383,12 @@ inline constexpr bool is_vpq_dataset_v = is_vpq_dataset::value; namespace filtering { +struct base_filter { + virtual ~base_filter() = default; +}; + /* A filter that filters nothing. This is the default behavior. */ -struct none_ivf_sample_filter { +struct none_sample_filter : public base_filter { inline _RAFT_HOST_DEVICE bool operator()( // query index const uint32_t query_ix, @@ -392,10 +396,7 @@ struct none_ivf_sample_filter { const uint32_t cluster_ix, // the index of the current sample inside the current inverted list const uint32_t sample_ix) const; -}; -/* A filter that filters nothing. This is the default behavior. */ -struct none_cagra_sample_filter { inline _RAFT_HOST_DEVICE bool operator()( // query index const uint32_t query_ix, @@ -431,13 +432,33 @@ struct ivf_to_sample_filter { const uint32_t sample_ix) const; }; +/** + * @brief Filter an index with a bitmap + * + * @tparam bitmap_t Data type of the bitmap + * @tparam index_t Indexing type + */ +template +struct bitmap_filter : public base_filter { + // View of the bitset to use as a filter + const cuvs::core::bitmap_view bitmap_view_; + + bitmap_filter(const cuvs::core::bitmap_view bitmap_for_filtering); + inline _RAFT_HOST_DEVICE bool operator()( + // query index + const uint32_t query_ix, + // the index of the current sample + const uint32_t sample_ix) const; +}; + /** * @brief Filter an index with a bitset * + * @tparam bitset_t Data type of the bitset * @tparam index_t Indexing type */ template -struct bitset_filter { +struct bitset_filter : public base_filter { // View of the bitset to use as a filter const cuvs::core::bitset_view bitset_view_; diff --git a/cpp/include/cuvs/neighbors/ivf_flat.hpp b/cpp/include/cuvs/neighbors/ivf_flat.hpp index 44502f942..67d1b46c0 100644 --- a/cpp/include/cuvs/neighbors/ivf_flat.hpp +++ b/cpp/include/cuvs/neighbors/ivf_flat.hpp @@ -1163,13 +1163,17 @@ void extend(raft::resources const& handle, * dataset [n_queries, k] * @param[out] distances raft::device_matrix_view to the distances to the selected neighbors * [n_queries, k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& handle, const cuvs::neighbors::ivf_flat::search_params& params, cuvs::neighbors::ivf_flat::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1200,13 +1204,17 @@ void search(raft::resources const& handle, * dataset [n_queries, k] * @param[out] distances raft::device_matrix_view to the distances to the selected neighbors * [n_queries, k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& handle, const cuvs::neighbors::ivf_flat::search_params& params, cuvs::neighbors::ivf_flat::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1237,112 +1245,18 @@ void search(raft::resources const& handle, * dataset [n_queries, k] * @param[out] distances raft::device_matrix_view to the distances to the selected neighbors * [n_queries, k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& handle, const cuvs::neighbors::ivf_flat::search_params& params, cuvs::neighbors::ivf_flat::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); -/** - * @brief Search ANN using the constructed index with the given filter. - * - * See the [ivf_flat::build](#ivf_flat::build) documentation for a usage example. - * - * Note, this function requires a temporary buffer to store intermediate results between cuda kernel - * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can - * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or - * eliminate entirely allocations happening within `search`. - * The exact size of the temporary buffer depends on multiple factors and is an implementation - * detail. However, you can safely specify a small initial size for the memory pool, so that only a - * few allocations happen to grow it during the first invocations of the `search`. - * - * @param[in] handle - * @param[in] params configure the search - * @param[in] idx ivf-flat constructed index - * @param[in] queries a device matrix view to a row-major matrix [n_queries, index->dim()] - * @param[out] neighbors a device matrix view to the indices of the neighbors in the source dataset - * [n_queries, k] - * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, - * k] - * @param[in] sample_filter a device bitset filter function that greenlights samples for a given - * query. - */ -void search_with_filtering( - raft::resources const& handle, - const search_params& params, - index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances, - cuvs::neighbors::filtering::bitset_filter sample_filter); - -/** - * @brief Search ANN using the constructed index with the given filter. - * - * See the [ivf_flat::build](#ivf_flat::build) documentation for a usage example. - * - * Note, this function requires a temporary buffer to store intermediate results between cuda kernel - * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can - * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or - * eliminate entirely allocations happening within `search`. - * The exact size of the temporary buffer depends on multiple factors and is an implementation - * detail. However, you can safely specify a small initial size for the memory pool, so that only a - * few allocations happen to grow it during the first invocations of the `search`. - * - * @param[in] handle - * @param[in] params configure the search - * @param[in] idx ivf-flat constructed index - * @param[in] queries a device matrix view to a row-major matrix [n_queries, index->dim()] - * @param[out] neighbors a device matrix view to the indices of the neighbors in the source dataset - * [n_queries, k] - * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, - * k] - * @param[in] sample_filter a device bitset filter function that greenlights samples for a given - * query. - */ -void search_with_filtering( - raft::resources const& handle, - const search_params& params, - index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances, - cuvs::neighbors::filtering::bitset_filter sample_filter); - -/** - * @brief Search ANN using the constructed index with the given filter. - * - * See the [ivf_flat::build](#ivf_flat::build) documentation for a usage example. - * - * Note, this function requires a temporary buffer to store intermediate results between cuda kernel - * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can - * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or - * eliminate entirely allocations happening within `search`. - * The exact size of the temporary buffer depends on multiple factors and is an implementation - * detail. However, you can safely specify a small initial size for the memory pool, so that only a - * few allocations happen to grow it during the first invocations of the `search`. - * - * @param[in] handle - * @param[in] params configure the search - * @param[in] idx ivf-flat constructed index - * @param[in] queries a device matrix view to a row-major matrix [n_queries, index->dim()] - * @param[out] neighbors a device matrix view to the indices of the neighbors in the source dataset - * [n_queries, k] - * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, - * k] - * @param[in] sample_filter a device bitset filter function that greenlights samples for a given - * query. - */ -void search_with_filtering( - raft::resources const& handle, - const search_params& params, - index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances, - cuvs::neighbors::filtering::bitset_filter sample_filter); /** * @} */ @@ -2039,18 +1953,18 @@ void reset_index(const raft::resources& res, index* index); * using namespace cuvs::neighbors; * raft::resources res; * // use default index parameters - * ivf_pq::index_params index_params; + * ivf_flat::index_params index_params; * // initialize an empty index - * ivf_pq::index index(res, index_params, D); - * ivf_pq::helpers::reset_index(res, &index); + * ivf_flat::index index(res, index_params, D); + * ivf_flat::helpers::reset_index(res, &index); * // resize the first IVF list to hold 5 records - * auto spec = list_spec{ - * index->pq_bits(), index->pq_dim(), index->conservative_memory_allocation()}; + * auto spec = list_spec{ + * index->dim(), index->conservative_memory_allocation()}; * uint32_t new_size = 5; * ivf::resize_list(res, list, spec, new_size, 0); * raft::update_device(index.list_sizes(), &new_size, 1, stream); * // recompute the internal state of the index - * ivf_pq::helpers::recompute_internal_state(res, index); + * ivf_flat::helpers::recompute_internal_state(res, index); * @endcode * * @param[in] res raft resource @@ -2067,18 +1981,18 @@ void recompute_internal_state(const raft::resources& res, index* * using namespace cuvs::neighbors; * raft::resources res; * // use default index parameters - * ivf_pq::index_params index_params; + * ivf_flat::index_params index_params; * // initialize an empty index - * ivf_pq::index index(res, index_params, D); - * ivf_pq::helpers::reset_index(res, &index); + * ivf_flat::index index(res, index_params, D); + * ivf_flat::helpers::reset_index(res, &index); * // resize the first IVF list to hold 5 records - * auto spec = list_spec{ - * index->pq_bits(), index->pq_dim(), index->conservative_memory_allocation()}; + * auto spec = list_spec{ + * index->dim(), index->conservative_memory_allocation()}; * uint32_t new_size = 5; * ivf::resize_list(res, list, spec, new_size, 0); * raft::update_device(index.list_sizes(), &new_size, 1, stream); * // recompute the internal state of the index - * ivf_pq::helpers::recompute_internal_state(res, index); + * ivf_flat::helpers::recompute_internal_state(res, index); * @endcode * * @param[in] res raft resource @@ -2095,18 +2009,18 @@ void recompute_internal_state(const raft::resources& res, index * using namespace cuvs::neighbors; * raft::resources res; * // use default index parameters - * ivf_pq::index_params index_params; + * ivf_flat::index_params index_params; * // initialize an empty index - * ivf_pq::index index(res, index_params, D); - * ivf_pq::helpers::reset_index(res, &index); + * ivf_flat::index index(res, index_params, D); + * ivf_flat::helpers::reset_index(res, &index); * // resize the first IVF list to hold 5 records - * auto spec = list_spec{ - * index->pq_bits(), index->pq_dim(), index->conservative_memory_allocation()}; + * auto spec = list_spec{ + * index->dim(), index->conservative_memory_allocation()}; * uint32_t new_size = 5; * ivf::resize_list(res, list, spec, new_size, 0); * raft::update_device(index.list_sizes(), &new_size, 1, stream); * // recompute the internal state of the index - * ivf_pq::helpers::recompute_internal_state(res, index); + * ivf_flat::helpers::recompute_internal_state(res, index); * @endcode * * @param[in] res raft resource diff --git a/cpp/include/cuvs/neighbors/ivf_pq.hpp b/cpp/include/cuvs/neighbors/ivf_pq.hpp index 8c378b1f0..3ce5f382f 100644 --- a/cpp/include/cuvs/neighbors/ivf_pq.hpp +++ b/cpp/include/cuvs/neighbors/ivf_pq.hpp @@ -1400,13 +1400,17 @@ void extend(raft::resources const& handle, * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& handle, const cuvs::neighbors::ivf_pq::search_params& search_params, cuvs::neighbors::ivf_pq::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1441,13 +1445,17 @@ void search(raft::resources const& handle, * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& handle, const cuvs::neighbors::ivf_pq::search_params& search_params, cuvs::neighbors::ivf_pq::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1482,13 +1490,17 @@ void search(raft::resources const& handle, * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& handle, const cuvs::neighbors::ivf_pq::search_params& search_params, cuvs::neighbors::ivf_pq::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); /** * @brief Search ANN using the constructed index. @@ -1523,145 +1535,18 @@ void search(raft::resources const& handle, * [n_queries, k] * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, * k] + * @param[in] sample_filter an optional device filter function object that greenlights samples + * for a given query. (none_sample_filter for no filtering) */ void search(raft::resources const& handle, const cuvs::neighbors::ivf_pq::search_params& search_params, cuvs::neighbors::ivf_pq::index& index, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances); + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter = + cuvs::neighbors::filtering::none_sample_filter{}); -/** - * @brief Search ANN using the constructed index with the given filter. - * - * See the [ivf_pq::build](#ivf_pq::build) documentation for a usage example. - * - * Note, this function requires a temporary buffer to store intermediate results between cuda kernel - * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can - * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or - * eliminate entirely allocations happening within `search`. - * The exact size of the temporary buffer depends on multiple factors and is an implementation - * detail. However, you can safely specify a small initial size for the memory pool, so that only a - * few allocations happen to grow it during the first invocations of the `search`. - * - * @param[in] handle - * @param[in] params configure the search - * @param[in] idx ivf-pq constructed index - * @param[in] queries a device matrix view to a row-major matrix [n_queries, index->dim()] - * @param[out] neighbors a device matrix view to the indices of the neighbors in the source dataset - * [n_queries, k] - * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, - * k] - * @param[in] sample_filter a device bitset filter function that greenlights samples for a given - * query. - */ -void search_with_filtering( - raft::resources const& handle, - const search_params& params, - index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances, - cuvs::neighbors::filtering::bitset_filter sample_filter); - -/** - * @brief Search ANN using the constructed index with the given filter. - * - * See the [ivf_pq::build](#ivf_pq::build) documentation for a usage example. - * - * Note, this function requires a temporary buffer to store intermediate results between cuda kernel - * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can - * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or - * eliminate entirely allocations happening within `search`. - * The exact size of the temporary buffer depends on multiple factors and is an implementation - * detail. However, you can safely specify a small initial size for the memory pool, so that only a - * few allocations happen to grow it during the first invocations of the `search`. - * - * @param[in] handle - * @param[in] params configure the search - * @param[in] idx ivf-pq constructed index - * @param[in] queries a device matrix view to a row-major matrix [n_queries, index->dim()] - * @param[out] neighbors a device matrix view to the indices of the neighbors in the source dataset - * [n_queries, k] - * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, - * k] - * @param[in] sample_filter a device bitset filter function that greenlights samples for a given - * query. - */ -void search_with_filtering( - raft::resources const& handle, - const search_params& params, - index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances, - cuvs::neighbors::filtering::bitset_filter sample_filter); - -/** - * @brief Search ANN using the constructed index with the given filter. - * - * See the [ivf_pq::build](#ivf_pq::build) documentation for a usage example. - * - * Note, this function requires a temporary buffer to store intermediate results between cuda kernel - * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can - * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or - * eliminate entirely allocations happening within `search`. - * The exact size of the temporary buffer depends on multiple factors and is an implementation - * detail. However, you can safely specify a small initial size for the memory pool, so that only a - * few allocations happen to grow it during the first invocations of the `search`. - * - * @param[in] handle - * @param[in] params configure the search - * @param[in] idx ivf-pq constructed index - * @param[in] queries a device matrix view to a row-major matrix [n_queries, index->dim()] - * @param[out] neighbors a device matrix view to the indices of the neighbors in the source dataset - * [n_queries, k] - * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, - * k] - * @param[in] sample_filter a device bitset filter function that greenlights samples for a given - * query. - */ -void search_with_filtering( - raft::resources const& handle, - const search_params& params, - index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances, - cuvs::neighbors::filtering::bitset_filter sample_filter); - -/** - * @brief Search ANN using the constructed index with the given filter. - * - * See the [ivf_pq::build](#ivf_pq::build) documentation for a usage example. - * - * Note, this function requires a temporary buffer to store intermediate results between cuda kernel - * calls, which may lead to undesirable allocations and slowdown. To alleviate the problem, you can - * pass a pool memory resource or a large enough pre-allocated memory resource to reduce or - * eliminate entirely allocations happening within `search`. - * The exact size of the temporary buffer depends on multiple factors and is an implementation - * detail. However, you can safely specify a small initial size for the memory pool, so that only a - * few allocations happen to grow it during the first invocations of the `search`. - * - * @param[in] handle - * @param[in] params configure the search - * @param[in] idx ivf-pq constructed index - * @param[in] queries a device matrix view to a row-major matrix [n_queries, index->dim()] - * @param[out] neighbors a device matrix view to the indices of the neighbors in the source dataset - * [n_queries, k] - * @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries, - * k] - * @param[in] sample_filter a device bitset filter function that greenlights samples for a given - * query. - */ -void search_with_filtering( - raft::resources const& handle, - const search_params& params, - index& idx, - raft::device_matrix_view queries, - raft::device_matrix_view neighbors, - raft::device_matrix_view distances, - cuvs::neighbors::filtering::bitset_filter sample_filter); /** * @} */ diff --git a/cpp/include/cuvs/neighbors/reachability.hpp b/cpp/include/cuvs/neighbors/reachability.hpp new file mode 100644 index 000000000..9746ac856 --- /dev/null +++ b/cpp/include/cuvs/neighbors/reachability.hpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace cuvs::neighbors::reachability { + +/** + * @defgroup reachability_cpp Mutual Reachability + * @{ + */ +/** + * Constructs a mutual reachability graph, which is a k-nearest neighbors + * graph projected into mutual reachability space using the following + * function for each data point, where core_distance is the distance + * to the kth neighbor: max(core_distance(a), core_distance(b), d(a, b)) + * + * Unfortunately, points in the tails of the pdf (e.g. in sparse regions + * of the space) can have very large neighborhoods, which will impact + * nearby neighborhoods. Because of this, it's possible that the + * radius for points in the main mass, which might have a very small + * radius initially, to expand very large. As a result, the initial + * knn which was used to compute the core distances may no longer + * capture the actual neighborhoods after projection into mutual + * reachability space. + * + * For the experimental version, we execute the knn twice- once + * to compute the radii (core distances) and again to capture + * the final neighborhoods. Future iterations of this algorithm + * will work improve upon this "exact" version, by using + * more specialized data structures, such as space-partitioning + * structures. It has also been shown that approximate nearest + * neighbors can yield reasonable neighborhoods as the + * data sizes increase. + * + * @param[in] handle raft handle for resource reuse + * @param[in] X input data points (size m * n) + * @param[in] min_samples this neighborhood will be selected for core distances + * @param[out] indptr CSR indptr of output knn graph (size m + 1) + * @param[out] core_dists output core distances array (size m) + * @param[out] out COO object, uninitialized on entry, on exit it stores the + * (symmetrized) maximum reachability distance for the k nearest + * neighbors. + * @param[in] metric distance metric to use, default Euclidean + * @param[in] alpha weight applied when internal distance is chosen for + * mutual reachability (value of 1.0 disables the weighting) + */ +void mutual_reachability_graph( + const raft::resources& handle, + raft::device_matrix_view X, + int min_samples, + raft::device_vector_view indptr, + raft::device_vector_view core_dists, + raft::sparse::COO& out, + cuvs::distance::DistanceType metric = cuvs::distance::DistanceType::L2SqrtExpanded, + float alpha = 1.0); +/** + * @} + */ +} // namespace cuvs::neighbors::reachability diff --git a/cpp/include/cuvs/selection/select_k.hpp b/cpp/include/cuvs/selection/select_k.hpp index dc34caf41..e4dfdb12c 100644 --- a/cpp/include/cuvs/selection/select_k.hpp +++ b/cpp/include/cuvs/selection/select_k.hpp @@ -87,6 +87,16 @@ void select_k( SelectAlgo algo = SelectAlgo::kAuto, std::optional> len_i = std::nullopt); +void select_k(raft::resources const& handle, + raft::device_matrix_view in_val, + std::optional> in_idx, + raft::device_matrix_view out_val, + raft::device_matrix_view out_idx, + bool select_min, + bool sorted = false, + SelectAlgo algo = SelectAlgo::kAuto, + std::optional> len_i = std::nullopt); + /** * Select k smallest or largest key/values from each row in the input data. * diff --git a/cpp/src/cluster/detail/connectivities.cuh b/cpp/src/cluster/detail/connectivities.cuh index ada424192..e61c9166f 100644 --- a/cpp/src/cluster/detail/connectivities.cuh +++ b/cpp/src/cluster/detail/connectivities.cuh @@ -16,7 +16,7 @@ #pragma once -#include "../../distance/distance.cuh" +#include "./kmeans_common.cuh" #include #include #include @@ -153,7 +153,11 @@ void pairwise_distances(const raft::resources& handle, // TODO: It would ultimately be nice if the MST could accept // dense inputs directly so we don't need to double the memory // usage to hand it a sparse array here. - distance::pairwise_distance(handle, X, X, data, m, m, n, metric); + auto X_view = raft::make_device_matrix_view(X, m, n); + + cuvs::cluster::kmeans::detail::pairwise_distance_kmeans( + handle, X_view, X_view, raft::make_device_matrix_view(data, m, m), metric); + // self-loops get max distance auto transform_in = thrust::make_zip_iterator(thrust::make_tuple(thrust::make_counting_iterator(0), data)); diff --git a/cpp/src/cluster/detail/kmeans.cuh b/cpp/src/cluster/detail/kmeans.cuh index e7d4bdf76..9b673bca3 100644 --- a/cpp/src/cluster/detail/kmeans.cuh +++ b/cpp/src/cluster/detail/kmeans.cuh @@ -198,7 +198,7 @@ void kmeansPlusPlus(raft::resources const& handle, // Output - pwd [n_trials x n_samples] auto pwd = distBuffer.view(); cuvs::cluster::kmeans::detail::pairwise_distance_kmeans( - handle, centroidCandidates.view(), X, pwd, workspace, metric); + handle, centroidCandidates.view(), X, pwd, metric); // Update nearest cluster distance for each centroid candidate // Note pwd and minDistBuf points to same buffer which currently holds pairwise distance values. @@ -1247,7 +1247,7 @@ void kmeans_transform(raft::resources const& handle, // calculate pairwise distance between cluster centroids and current batch // of input dataset pairwise_distance_kmeans( - handle, datasetView, centroids, pairwiseDistanceView, workspace, metric); + handle, datasetView, centroids, pairwiseDistanceView, metric); } } diff --git a/cpp/src/cluster/detail/kmeans_common.cuh b/cpp/src/cluster/detail/kmeans_common.cuh index 04c1a6802..eec71b5d2 100644 --- a/cpp/src/cluster/detail/kmeans_common.cuh +++ b/cpp/src/cluster/detail/kmeans_common.cuh @@ -293,7 +293,6 @@ void pairwise_distance_kmeans(raft::resources const& handle, raft::device_matrix_view X, raft::device_matrix_view centroids, raft::device_matrix_view pairwiseDistance, - rmm::device_uvector& workspace, cuvs::distance::DistanceType metric) { auto n_samples = X.extent(0); @@ -303,15 +302,23 @@ void pairwise_distance_kmeans(raft::resources const& handle, ASSERT(X.extent(1) == centroids.extent(1), "# features in dataset and centroids are different (must be same)"); - cuvs::distance::pairwise_distance(handle, - X.data_handle(), - centroids.data_handle(), - pairwiseDistance.data_handle(), - n_samples, - n_clusters, - n_features, - workspace, - metric); + if (metric == cuvs::distance::DistanceType::L2Expanded) { + cuvs::distance::distance(handle, X, centroids, pairwiseDistance); + } else if (metric == cuvs::distance::DistanceType::L2SqrtExpanded) { + cuvs::distance::distance(handle, X, centroids, pairwiseDistance); + } else { + RAFT_FAIL("kmeans requires L2Expanded or L2SqrtExpanded distance, have %i", metric); + } } // shuffle and randomly select 'n_samples_to_gather' from input 'in' and stores @@ -461,7 +468,7 @@ void minClusterAndDistanceCompute( // calculate pairwise distance between current tile of cluster centroids // and input dataset pairwise_distance_kmeans( - handle, datasetView, centroidsView, pairwiseDistanceView, workspace, metric); + handle, datasetView, centroidsView, pairwiseDistanceView, metric); // argmin reduction returning pair // calculates the closest centroid and the distance to the closest @@ -591,7 +598,7 @@ void minClusterDistanceCompute(raft::resources const& handle, // calculate pairwise distance between current tile of cluster centroids // and input dataset pairwise_distance_kmeans( - handle, datasetView, centroidsView, pairwiseDistanceView, workspace, metric); + handle, datasetView, centroidsView, pairwiseDistanceView, metric); raft::linalg::coalescedReduction(minClusterDistanceView.data_handle(), pairwiseDistanceView.data_handle(), diff --git a/cpp/src/cluster/detail/kmeans_mg.cuh b/cpp/src/cluster/detail/kmeans_mg.cuh new file mode 100644 index 000000000..b0f435502 --- /dev/null +++ b/cpp/src/cluster/detail/kmeans_mg.cuh @@ -0,0 +1,781 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../kmeans.cuh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace cuvs::cluster::kmeans::mg::detail { + +#define CUVS_LOG_KMEANS(handle, fmt, ...) \ + do { \ + bool isRoot = true; \ + if (raft::resource::comms_initialized(handle)) { \ + const auto& comm = raft::resource::get_comms(handle); \ + const int my_rank = comm.get_rank(); \ + isRoot = my_rank == 0; \ + } \ + if (isRoot) { RAFT_LOG_DEBUG(fmt, ##__VA_ARGS__); } \ + } while (0) + +template +struct KeyValueIndexOp { + __host__ __device__ __forceinline__ IndexT + operator()(const raft::KeyValuePair& a) const + { + return a.key; + } +}; + +#define KMEANS_COMM_ROOT 0 + +static cuvs::cluster::kmeans::params default_params; + +// Selects 'n_clusters' samples randomly from X +template +void initRandom(const raft::resources& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + raft::device_matrix_view centroids) +{ + const auto& comm = raft::resource::get_comms(handle); + cudaStream_t stream = raft::resource::get_cuda_stream(handle); + auto n_local_samples = X.extent(0); + auto n_features = X.extent(1); + auto n_clusters = params.n_clusters; + + const int my_rank = comm.get_rank(); + const int n_ranks = comm.get_size(); + + std::vector nCentroidsSampledByRank(n_ranks, 0); + std::vector nCentroidsElementsToReceiveFromRank(n_ranks, 0); + + const int nranks_reqd = std::min(n_ranks, n_clusters); + ASSERT(KMEANS_COMM_ROOT < nranks_reqd, "KMEANS_COMM_ROOT must be in [0, %d)\n", nranks_reqd); + + for (int rank = 0; rank < nranks_reqd; ++rank) { + int nCentroidsSampledInRank = n_clusters / nranks_reqd; + if (rank == KMEANS_COMM_ROOT) { + nCentroidsSampledInRank += n_clusters - nCentroidsSampledInRank * nranks_reqd; + } + nCentroidsSampledByRank[rank] = nCentroidsSampledInRank; + nCentroidsElementsToReceiveFromRank[rank] = nCentroidsSampledInRank * n_features; + } + + auto nCentroidsSampledInRank = nCentroidsSampledByRank[my_rank]; + ASSERT((IndexT)nCentroidsSampledInRank <= (IndexT)n_local_samples, + "# random samples requested from rank-%d is larger than the available " + "samples at the rank (requested is %lu, available is %lu)", + my_rank, + (size_t)nCentroidsSampledInRank, + (size_t)n_local_samples); + + auto centroidsSampledInRank = + raft::make_device_matrix(handle, nCentroidsSampledInRank, n_features); + + cuvs::cluster::kmeans::shuffle_and_gather( + handle, X, centroidsSampledInRank.view(), nCentroidsSampledInRank, params.rng_state.seed); + + std::vector displs(n_ranks); + thrust::exclusive_scan(thrust::host, + nCentroidsElementsToReceiveFromRank.begin(), + nCentroidsElementsToReceiveFromRank.end(), + displs.begin()); + + // gather centroids from all ranks + comm.allgatherv(centroidsSampledInRank.data_handle(), // sendbuff + centroids.data_handle(), // recvbuff + nCentroidsElementsToReceiveFromRank.data(), // recvcount + displs.data(), + stream); +} + +/* + * @brief Selects 'n_clusters' samples from X using scalable kmeans++ algorithm + * Scalable kmeans++ pseudocode + * 1: C = sample a point uniformly at random from X + * 2: psi = phi_X (C) + * 3: for O( log(psi) ) times do + * 4: C' = sample each point x in X independently with probability + * p_x = l * ( d^2(x, C) / phi_X (C) ) + * 5: C = C U C' + * 6: end for + * 7: For x in C, set w_x to be the number of points in X closer to x than any + * other point in C + * 8: Recluster the weighted points in C into k clusters + */ +template +void initKMeansPlusPlus(const raft::resources& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + raft::device_matrix_view centroidsRawData, + rmm::device_uvector& workspace) +{ + const auto& comm = raft::resource::get_comms(handle); + cudaStream_t stream = raft::resource::get_cuda_stream(handle); + const int my_rank = comm.get_rank(); + const int n_rank = comm.get_size(); + + auto n_samples = X.extent(0); + auto n_features = X.extent(1); + auto n_clusters = params.n_clusters; + auto metric = params.metric; + + raft::random::RngState rng(params.rng_state.seed, raft::random::GeneratorType::GenPhilox); + + // <<<< Step-1 >>> : C <- sample a point uniformly at random from X + // 1.1 - Select a rank r' at random from the available n_rank ranks with a + // probability of 1/n_rank [Note - with same seed all rank selects + // the same r' which avoids a call to comm] + // 1.2 - Rank r' samples a point uniformly at random from the local dataset + // X which will be used as the initial centroid for kmeans++ + // 1.3 - Communicate the initial centroid chosen by rank-r' to all other + // ranks + std::mt19937 gen(params.rng_state.seed); + std::uniform_int_distribution<> dis(0, n_rank - 1); + int rp = dis(gen); + + // buffer to flag the sample that is chosen as initial centroids + std::vector h_isSampleCentroid(n_samples); + std::fill(h_isSampleCentroid.begin(), h_isSampleCentroid.end(), 0); + + auto initialCentroid = raft::make_device_matrix(handle, 1, n_features); + CUVS_LOG_KMEANS( + handle, "@Rank-%d : KMeans|| : initial centroid is sampled at rank-%d\n", my_rank, rp); + + // 1.2 - Rank r' samples a point uniformly at random from the local dataset + // X which will be used as the initial centroid for kmeans++ + if (my_rank == rp) { + std::mt19937 gen(params.rng_state.seed); + std::uniform_int_distribution<> dis(0, n_samples - 1); + + int cIdx = dis(gen); + auto centroidsView = raft::make_device_matrix_view( + X.data_handle() + cIdx * n_features, 1, n_features); + + raft::copy( + initialCentroid.data_handle(), centroidsView.data_handle(), centroidsView.size(), stream); + + h_isSampleCentroid[cIdx] = 1; + } + + // 1.3 - Communicate the initial centroid chosen by rank-r' to all other ranks + comm.bcast(initialCentroid.data_handle(), initialCentroid.size(), rp, stream); + + // device buffer to flag the sample that is chosen as initial centroid + auto isSampleCentroid = raft::make_device_vector(handle, n_samples); + + raft::copy( + isSampleCentroid.data_handle(), h_isSampleCentroid.data(), isSampleCentroid.size(), stream); + + rmm::device_uvector centroidsBuf(0, stream); + + // reset buffer to store the chosen centroid + centroidsBuf.resize(initialCentroid.size(), stream); + raft::copy(centroidsBuf.begin(), initialCentroid.data_handle(), initialCentroid.size(), stream); + + auto potentialCentroids = raft::make_device_matrix_view( + centroidsBuf.data(), initialCentroid.extent(0), initialCentroid.extent(1)); + // <<< End of Step-1 >>> + + rmm::device_uvector L2NormBuf_OR_DistBuf(0, stream); + + // L2 norm of X: ||x||^2 + auto L2NormX = raft::make_device_vector(handle, n_samples); + if (metric == cuvs::distance::DistanceType::L2Expanded || + metric == cuvs::distance::DistanceType::L2SqrtExpanded) { + raft::linalg::rowNorm(L2NormX.data_handle(), + X.data_handle(), + X.extent(1), + X.extent(0), + raft::linalg::L2Norm, + true, + stream); + } + + auto minClusterDistance = raft::make_device_vector(handle, n_samples); + auto uniformRands = raft::make_device_vector(handle, n_samples); + + // <<< Step-2 >>>: psi <- phi_X (C) + auto clusterCost = raft::make_device_scalar(handle, 0); + + cuvs::cluster::kmeans::min_cluster_distance(handle, + X, + potentialCentroids, + minClusterDistance.view(), + L2NormX.view(), + L2NormBuf_OR_DistBuf, + params.metric, + params.batch_samples, + params.batch_centroids, + workspace); + + // compute partial cluster cost from the samples in rank + cuvs::cluster::kmeans::cluster_cost( + handle, + minClusterDistance.view(), + workspace, + clusterCost.view(), + cuda::proclaim_return_type( + [] __device__(const DataT& a, const DataT& b) { return a + b; })); + + // compute total cluster cost by accumulating the partial cost from all the + // ranks + comm.allreduce( + clusterCost.data_handle(), clusterCost.data_handle(), 1, raft::comms::op_t::SUM, stream); + + DataT psi = 0; + raft::copy(&psi, clusterCost.data_handle(), 1, stream); + + // <<< End of Step-2 >>> + + ASSERT(comm.sync_stream(stream) == raft::comms::status_t::SUCCESS, + "An error occurred in the distributed operation. This can result from " + "a failed rank"); + + // Scalable kmeans++ paper claims 8 rounds is sufficient + int niter = std::min(8, (int)ceil(log(psi))); + CUVS_LOG_KMEANS(handle, + "@Rank-%d:KMeans|| :phi - %f, max # of iterations for kmeans++ loop - " + "%d\n", + my_rank, + psi, + niter); + + // <<<< Step-3 >>> : for O( log(psi) ) times do + for (int iter = 0; iter < niter; ++iter) { + CUVS_LOG_KMEANS(handle, + "@Rank-%d:KMeans|| - Iteration %d: # potential centroids sampled - " + "%d\n", + my_rank, + iter, + potentialCentroids.extent(0)); + + cuvs::cluster::kmeans::min_cluster_distance(handle, + X, + potentialCentroids, + minClusterDistance.view(), + L2NormX.view(), + L2NormBuf_OR_DistBuf, + params.metric, + params.batch_samples, + params.batch_centroids, + workspace); + + cuvs::cluster::kmeans::cluster_cost( + handle, + minClusterDistance.view(), + workspace, + clusterCost.view(), + cuda::proclaim_return_type( + [] __device__(const DataT& a, const DataT& b) { return a + b; })); + comm.allreduce( + clusterCost.data_handle(), clusterCost.data_handle(), 1, raft::comms::op_t::SUM, stream); + raft::copy(&psi, clusterCost.data_handle(), 1, stream); + ASSERT(comm.sync_stream(stream) == raft::comms::status_t::SUCCESS, + "An error occurred in the distributed operation. This can result " + "from a failed rank"); + + // <<<< Step-4 >>> : Sample each point x in X independently and identify new + // potentialCentroids + raft::random::uniform( + handle, rng, uniformRands.data_handle(), uniformRands.extent(0), (DataT)0, (DataT)1); + cuvs::cluster::kmeans::SamplingOp select_op(psi, + params.oversampling_factor, + n_clusters, + uniformRands.data_handle(), + isSampleCentroid.data_handle()); + + rmm::device_uvector inRankCp(0, stream); + cuvs::cluster::kmeans::sample_centroids(handle, + X, + minClusterDistance.view(), + isSampleCentroid.view(), + select_op, + inRankCp, + workspace); + /// <<<< End of Step-4 >>>> + + int* nPtsSampledByRank; + RAFT_CUDA_TRY(cudaMallocHost(&nPtsSampledByRank, n_rank * sizeof(int))); + + /// <<<< Step-5 >>> : C = C U C' + // append the data in Cp from all ranks to the buffer holding the + // potentialCentroids + // RAFT_CUDA_TRY(cudaMemsetAsync(nPtsSampledByRank, 0, n_rank * sizeof(int), stream)); + std::fill(nPtsSampledByRank, nPtsSampledByRank + n_rank, 0); + nPtsSampledByRank[my_rank] = inRankCp.size() / n_features; + comm.allgather(&(nPtsSampledByRank[my_rank]), nPtsSampledByRank, 1, stream); + ASSERT(comm.sync_stream(stream) == raft::comms::status_t::SUCCESS, + "An error occurred in the distributed operation. This can result " + "from a failed rank"); + + auto nPtsSampled = + thrust::reduce(thrust::host, nPtsSampledByRank, nPtsSampledByRank + n_rank, 0); + + // gather centroids from all ranks + std::vector sizes(n_rank); + thrust::transform( + thrust::host, nPtsSampledByRank, nPtsSampledByRank + n_rank, sizes.begin(), [&](int val) { + return val * n_features; + }); + + RAFT_CUDA_TRY_NO_THROW(cudaFreeHost(nPtsSampledByRank)); + + std::vector displs(n_rank); + thrust::exclusive_scan(thrust::host, sizes.begin(), sizes.end(), displs.begin()); + + centroidsBuf.resize(centroidsBuf.size() + nPtsSampled * n_features, stream); + comm.allgatherv(inRankCp.data(), + centroidsBuf.end() - nPtsSampled * n_features, + sizes.data(), + displs.data(), + stream); + + auto tot_centroids = potentialCentroids.extent(0) + nPtsSampled; + potentialCentroids = + raft::make_device_matrix_view(centroidsBuf.data(), tot_centroids, n_features); + /// <<<< End of Step-5 >>> + } /// <<<< Step-6 >>> + + CUVS_LOG_KMEANS(handle, + "@Rank-%d:KMeans||: # potential centroids sampled - %d\n", + my_rank, + potentialCentroids.extent(0)); + + if ((IndexT)potentialCentroids.extent(0) > (IndexT)n_clusters) { + // <<< Step-7 >>>: For x in C, set w_x to be the number of pts closest to X + // temporary buffer to store the sample count per cluster, destructor + // releases the resource + + auto weight = raft::make_device_vector(handle, potentialCentroids.extent(0)); + + cuvs::cluster::kmeans::count_samples_in_cluster( + handle, params, X, L2NormX.view(), potentialCentroids, workspace, weight.view()); + + // merge the local histogram from all ranks + comm.allreduce(weight.data_handle(), // sendbuff + weight.data_handle(), // recvbuff + weight.size(), // count + raft::comms::op_t::SUM, + stream); + + // <<< end of Step-7 >>> + + // Step-8: Recluster the weighted points in C into k clusters + // Note - reclustering step is duplicated across all ranks and with the same + // seed they should generate the same potentialCentroids + auto const_centroids = raft::make_device_matrix_view( + potentialCentroids.data_handle(), potentialCentroids.extent(0), potentialCentroids.extent(1)); + cuvs::cluster::kmeans::init_plus_plus( + handle, params, const_centroids, centroidsRawData, workspace); + + auto inertia = raft::make_host_scalar(0); + auto n_iter = raft::make_host_scalar(0); + auto weight_view = + raft::make_device_vector_view(weight.data_handle(), weight.extent(0)); + cuvs::cluster::kmeans::params params_copy = params; + params_copy.rng_state = default_params.rng_state; + + cuvs::cluster::kmeans::fit_main(handle, + params_copy, + const_centroids, + weight_view, + centroidsRawData, + inertia.view(), + n_iter.view(), + workspace); + + } else if ((IndexT)potentialCentroids.extent(0) < (IndexT)n_clusters) { + // supplement with random + auto n_random_clusters = n_clusters - potentialCentroids.extent(0); + CUVS_LOG_KMEANS(handle, + "[Warning!] KMeans||: found fewer than %d centroids during " + "initialization (found %d centroids, remaining %d centroids will be " + "chosen randomly from input samples)\n", + n_clusters, + potentialCentroids.extent(0), + n_random_clusters); + + // generate `n_random_clusters` centroids + cuvs::cluster::kmeans::params rand_params = params; + rand_params.rng_state = default_params.rng_state; + rand_params.init = cuvs::cluster::kmeans::params::InitMethod::Random; + rand_params.n_clusters = n_random_clusters; + initRandom(handle, rand_params, X, centroidsRawData); + + // copy centroids generated during kmeans|| iteration to the buffer + raft::copy(centroidsRawData.data_handle() + n_random_clusters * n_features, + potentialCentroids.data_handle(), + potentialCentroids.size(), + stream); + + } else { + // found the required n_clusters + raft::copy(centroidsRawData.data_handle(), + potentialCentroids.data_handle(), + potentialCentroids.size(), + stream); + } +} + +template +void checkWeights(const raft::resources& handle, + rmm::device_uvector& workspace, + raft::device_vector_view weight) +{ + cudaStream_t stream = raft::resource::get_cuda_stream(handle); + rmm::device_scalar wt_aggr(stream); + + const auto& comm = raft::resource::get_comms(handle); + + auto n_samples = weight.extent(0); + size_t temp_storage_bytes = 0; + RAFT_CUDA_TRY(cub::DeviceReduce::Sum( + nullptr, temp_storage_bytes, weight.data_handle(), wt_aggr.data(), n_samples, stream)); + + workspace.resize(temp_storage_bytes, stream); + + RAFT_CUDA_TRY(cub::DeviceReduce::Sum( + workspace.data(), temp_storage_bytes, weight.data_handle(), wt_aggr.data(), n_samples, stream)); + + comm.allreduce(wt_aggr.data(), // sendbuff + wt_aggr.data(), // recvbuff + 1, // count + raft::comms::op_t::SUM, + stream); + DataT wt_sum = wt_aggr.value(stream); + raft::resource::sync_stream(handle, stream); + + if (wt_sum != n_samples) { + CUVS_LOG_KMEANS(handle, + "[Warning!] KMeans: normalizing the user provided sample weights to " + "sum up to %d samples", + n_samples); + + DataT scale = n_samples / wt_sum; + raft::linalg::unaryOp( + weight.data_handle(), + weight.data_handle(), + weight.size(), + cuda::proclaim_return_type([=] __device__(const DataT& wt) { return wt * scale; }), + stream); + } +} + +template +void fit(const raft::resources& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter, + rmm::device_uvector& workspace) +{ + const auto& comm = raft::resource::get_comms(handle); + cudaStream_t stream = raft::resource::get_cuda_stream(handle); + auto n_samples = X.extent(0); + auto n_features = X.extent(1); + auto n_clusters = params.n_clusters; + auto metric = params.metric; + + auto weight = raft::make_device_vector(handle, n_samples); + if (sample_weight) { + raft::copy(weight.data_handle(), sample_weight->data_handle(), n_samples, stream); + } else { + thrust::fill(raft::resource::get_thrust_policy(handle), + weight.data_handle(), + weight.data_handle() + weight.size(), + 1); + } + + // check if weights sum up to n_samples + checkWeights(handle, workspace, weight.view()); + + if (params.init == cuvs::cluster::kmeans::params::InitMethod::Random) { + // initializing with random samples from input dataset + CUVS_LOG_KMEANS(handle, + "KMeans.fit: initialize cluster centers by randomly choosing from the " + "input data.\n"); + initRandom(handle, params, X, centroids); + } else if (params.init == cuvs::cluster::kmeans::params::InitMethod::KMeansPlusPlus) { + // default method to initialize is kmeans++ + CUVS_LOG_KMEANS(handle, "KMeans.fit: initialize cluster centers using k-means++ algorithm.\n"); + initKMeansPlusPlus(handle, params, X, centroids, workspace); + } else if (params.init == cuvs::cluster::kmeans::params::InitMethod::Array) { + CUVS_LOG_KMEANS(handle, + "KMeans.fit: initialize cluster centers from the ndarray array input " + "passed to init argument.\n"); + + } else { + THROW("unknown initialization method to select initial centers"); + } + + // stores (key, value) pair corresponding to each sample where + // - key is the index of nearest cluster + // - value is the distance to the nearest cluster + auto minClusterAndDistance = + raft::make_device_vector, IndexT>(handle, n_samples); + + // temporary buffer to store L2 norm of centroids or distance matrix, + // destructor releases the resource + rmm::device_uvector L2NormBuf_OR_DistBuf(0, stream); + + // temporary buffer to store intermediate centroids, destructor releases the + // resource + auto newCentroids = raft::make_device_matrix(handle, n_clusters, n_features); + + // temporary buffer to store the weights per cluster, destructor releases + // the resource + auto wtInCluster = raft::make_device_vector(handle, n_clusters); + + // L2 norm of X: ||x||^2 + auto L2NormX = raft::make_device_vector(handle, n_samples); + if (metric == cuvs::distance::DistanceType::L2Expanded || + metric == cuvs::distance::DistanceType::L2SqrtExpanded) { + raft::linalg::rowNorm(L2NormX.data_handle(), + X.data_handle(), + X.extent(1), + X.extent(0), + raft::linalg::L2Norm, + true, + stream); + } + + DataT priorClusteringCost = 0; + for (n_iter[0] = 1; n_iter[0] <= params.max_iter; ++n_iter[0]) { + CUVS_LOG_KMEANS(handle, + "KMeans.fit: Iteration-%d: fitting the model using the initialize " + "cluster centers\n", + n_iter[0]); + + auto const_centroids = raft::make_device_matrix_view( + centroids.data_handle(), centroids.extent(0), centroids.extent(1)); + // computes minClusterAndDistance[0:n_samples) where + // minClusterAndDistance[i] is a pair where + // 'key' is index to an sample in 'centroids' (index of the nearest + // centroid) and 'value' is the distance between the sample 'X[i]' and the + // 'centroid[key]' + cuvs::cluster::kmeans::min_cluster_and_distance(handle, + X, + const_centroids, + minClusterAndDistance.view(), + L2NormX.view(), + L2NormBuf_OR_DistBuf, + params.metric, + params.batch_samples, + params.batch_centroids, + workspace); + + // Using TransformInputIteratorT to dereference an array of + // cub::KeyValuePair and converting them to just return the Key to be used + // in reduce_rows_by_key prims + KeyValueIndexOp conversion_op; + cub::TransformInputIterator, + raft::KeyValuePair*> + itr(minClusterAndDistance.data_handle(), conversion_op); + + workspace.resize(n_samples, stream); + + // Calculates weighted sum of all the samples assigned to cluster-i and + // store the result in newCentroids[i] + raft::linalg::reduce_rows_by_key((DataT*)X.data_handle(), + X.extent(1), + itr, + weight.data_handle(), + workspace.data(), + X.extent(0), + X.extent(1), + static_cast(n_clusters), + newCentroids.data_handle(), + stream); + + // Reduce weights by key to compute weight in each cluster + raft::linalg::reduce_cols_by_key(weight.data_handle(), + itr, + wtInCluster.data_handle(), + (IndexT)1, + (IndexT)weight.extent(0), + (IndexT)n_clusters, + stream); + + // merge the local histogram from all ranks + comm.allreduce(wtInCluster.data_handle(), // sendbuff + wtInCluster.data_handle(), // recvbuff + wtInCluster.size(), // count + raft::comms::op_t::SUM, + stream); + + // reduces newCentroids from all ranks + comm.allreduce(newCentroids.data_handle(), // sendbuff + newCentroids.data_handle(), // recvbuff + newCentroids.size(), // count + raft::comms::op_t::SUM, + stream); + + // Computes newCentroids[i] = newCentroids[i]/wtInCluster[i] where + // newCentroids[n_clusters x n_features] - 2D array, newCentroids[i] has + // sum of all the samples assigned to cluster-i + // wtInCluster[n_clusters] - 1D array, wtInCluster[i] contains # of + // samples in cluster-i. + // Note - when wtInCluster[i] is 0, newCentroid[i] is reset to 0 + + raft::linalg::matrixVectorOp( + newCentroids.data_handle(), + newCentroids.data_handle(), + wtInCluster.data_handle(), + newCentroids.extent(1), + newCentroids.extent(0), + true, + false, + cuda::proclaim_return_type([=] __device__(DataT mat, DataT vec) { + if (vec == 0) + return DataT(0); + else + return mat / vec; + }), + stream); + + // copy the centroids[i] to newCentroids[i] when wtInCluster[i] is 0 + cub::ArgIndexInputIterator itr_wt(wtInCluster.data_handle()); + raft::matrix::gather_if( + centroids.data_handle(), + centroids.extent(1), + centroids.extent(0), + itr_wt, + itr_wt, + wtInCluster.extent(0), + newCentroids.data_handle(), + cuda::proclaim_return_type( + [=] __device__(raft::KeyValuePair map) { // predicate + // copy when the # of samples in the cluster is 0 + if (map.value == 0) + return true; + else + return false; + }), + cuda::proclaim_return_type( + [=] __device__(raft::KeyValuePair map) { // map + return map.key; + }), + stream); + + // compute the squared norm between the newCentroids and the original + // centroids, destructor releases the resource + auto sqrdNorm = raft::make_device_scalar(handle, 1); + raft::linalg::mapThenSumReduce( + sqrdNorm.data_handle(), + newCentroids.size(), + cuda::proclaim_return_type([=] __device__(const DataT a, const DataT b) { + DataT diff = a - b; + return diff * diff; + }), + stream, + centroids.data_handle(), + newCentroids.data_handle()); + + DataT sqrdNormError = 0; + raft::copy(&sqrdNormError, sqrdNorm.data_handle(), sqrdNorm.size(), stream); + + raft::copy(centroids.data_handle(), newCentroids.data_handle(), newCentroids.size(), stream); + + bool done = false; + if (params.inertia_check) { + rmm::device_scalar> clusterCostD(stream); + + // calculate cluster cost phi_x(C) + cuvs::cluster::kmeans::cluster_cost( + handle, + minClusterAndDistance.view(), + workspace, + raft::make_device_scalar_view(clusterCostD.data()), + cuda::proclaim_return_type>( + [] __device__(const raft::KeyValuePair& a, + const raft::KeyValuePair& b) { + raft::KeyValuePair res; + res.key = 0; + res.value = a.value + b.value; + return res; + })); + + // Cluster cost phi_x(C) from all ranks + comm.allreduce(&(clusterCostD.data()->value), + &(clusterCostD.data()->value), + 1, + raft::comms::op_t::SUM, + stream); + + DataT curClusteringCost = 0; + raft::copy(&curClusteringCost, &(clusterCostD.data()->value), 1, stream); + + ASSERT(comm.sync_stream(stream) == raft::comms::status_t::SUCCESS, + "An error occurred in the distributed operation. This can result " + "from a failed rank"); + ASSERT(curClusteringCost != (DataT)0.0, + "Too few points and centroids being found is getting 0 cost from " + "centers\n"); + + if (n_iter[0] > 0) { + DataT delta = curClusteringCost / priorClusteringCost; + if (delta > 1 - params.tol) done = true; + } + priorClusteringCost = curClusteringCost; + } + + raft::resource::sync_stream(handle, stream); + if (sqrdNormError < params.tol) done = true; + + if (done) { + CUVS_LOG_KMEANS( + handle, "Threshold triggered after %d iterations. Terminating early.\n", n_iter[0]); + break; + } + } +} + +}; // namespace cuvs::cluster::kmeans::mg::detail diff --git a/cpp/src/cluster/kmeans.cuh b/cpp/src/cluster/kmeans.cuh index 1d12142da..5e6d756cc 100644 --- a/cpp/src/cluster/kmeans.cuh +++ b/cpp/src/cluster/kmeans.cuh @@ -17,10 +17,12 @@ #include "detail/kmeans.cuh" #include "detail/kmeans_auto_find_k.cuh" +#include "kmeans_mg.hpp" #include #include #include #include +#include #include #include @@ -94,8 +96,13 @@ void fit(raft::resources const& handle, raft::host_scalar_view inertia, raft::host_scalar_view n_iter) { - cuvs::cluster::kmeans::detail::kmeans_fit( - handle, params, X, sample_weight, centroids, inertia, n_iter); + // use the mnmg kmeans fit if we have comms initialize, single gpu otherwise + if (raft::resource::comms_initialized(handle)) { + cuvs::cluster::kmeans::mg::fit(handle, params, X, sample_weight, centroids, inertia, n_iter); + } else { + cuvs::cluster::kmeans::detail::kmeans_fit( + handle, params, X, sample_weight, centroids, inertia, n_iter); + } } /** diff --git a/cpp/src/cluster/kmeans_fit_double.cu b/cpp/src/cluster/kmeans_fit_double.cu new file mode 100644 index 000000000..4f193da09 --- /dev/null +++ b/cpp/src/cluster/kmeans_fit_double.cu @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kmeans.cuh" +#include + +namespace cuvs::cluster::kmeans { + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) +{ + cuvs::cluster::kmeans::fit( + handle, params, X, sample_weight, centroids, inertia, n_iter); +} + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) +{ + cuvs::cluster::kmeans::fit( + handle, params, X, sample_weight, centroids, inertia, n_iter); +} +} // namespace cuvs::cluster::kmeans diff --git a/cpp/src/cluster/kmeans_fit_float.cu b/cpp/src/cluster/kmeans_fit_float.cu index 89862a46c..3888ae492 100644 --- a/cpp/src/cluster/kmeans_fit_float.cu +++ b/cpp/src/cluster/kmeans_fit_float.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, NVIDIA CORPORATION. + * Copyright (c) 2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,4 +30,16 @@ void fit(raft::resources const& handle, cuvs::cluster::kmeans::fit( handle, params, X, sample_weight, centroids, inertia, n_iter); } + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) +{ + cuvs::cluster::kmeans::fit( + handle, params, X, sample_weight, centroids, inertia, n_iter); +} } // namespace cuvs::cluster::kmeans diff --git a/cpp/src/cluster/kmeans_fit_mg_double.cu b/cpp/src/cluster/kmeans_fit_mg_double.cu new file mode 100644 index 000000000..15081dfba --- /dev/null +++ b/cpp/src/cluster/kmeans_fit_mg_double.cu @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./detail/kmeans_mg.cuh" +#include "kmeans_mg.hpp" +#include + +namespace cuvs::cluster::kmeans::mg { + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) +{ + rmm::device_uvector workspace(0, raft::resource::get_cuda_stream(handle)); + + cuvs::cluster::kmeans::mg::detail::fit( + handle, params, X, sample_weight, centroids, inertia, n_iter, workspace); +} + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) +{ + rmm::device_uvector workspace(0, raft::resource::get_cuda_stream(handle)); + + cuvs::cluster::kmeans::mg::detail::fit( + handle, params, X, sample_weight, centroids, inertia, n_iter, workspace); +} +} // namespace cuvs::cluster::kmeans::mg diff --git a/cpp/src/cluster/kmeans_fit_mg_float.cu b/cpp/src/cluster/kmeans_fit_mg_float.cu new file mode 100644 index 000000000..54fbd6763 --- /dev/null +++ b/cpp/src/cluster/kmeans_fit_mg_float.cu @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./detail/kmeans_mg.cuh" +#include "kmeans_mg.hpp" +#include + +namespace cuvs::cluster::kmeans::mg { + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) +{ + rmm::device_uvector workspace(0, raft::resource::get_cuda_stream(handle)); + + cuvs::cluster::kmeans::mg::detail::fit( + handle, params, X, sample_weight, centroids, inertia, n_iter, workspace); +} + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) +{ + rmm::device_uvector workspace(0, raft::resource::get_cuda_stream(handle)); + + cuvs::cluster::kmeans::mg::detail::fit( + handle, params, X, sample_weight, centroids, inertia, n_iter, workspace); +} +} // namespace cuvs::cluster::kmeans::mg diff --git a/cpp/src/cluster/kmeans_fit_predict_double.cu b/cpp/src/cluster/kmeans_fit_predict_double.cu new file mode 100644 index 000000000..28a1d70c0 --- /dev/null +++ b/cpp/src/cluster/kmeans_fit_predict_double.cu @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kmeans.cuh" +#include + +namespace cuvs::cluster::kmeans { + +void fit_predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + std::optional> centroids, + raft::device_vector_view labels, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) + +{ + cuvs::cluster::kmeans::fit_predict( + handle, params, X, sample_weight, centroids, labels, inertia, n_iter); +} + +void fit_predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + std::optional> centroids, + raft::device_vector_view labels, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) + +{ + cuvs::cluster::kmeans::fit_predict( + handle, params, X, sample_weight, centroids, labels, inertia, n_iter); +} +} // namespace cuvs::cluster::kmeans diff --git a/cpp/src/cluster/kmeans_fit_predict_float.cu b/cpp/src/cluster/kmeans_fit_predict_float.cu index f043f7624..be3652db5 100644 --- a/cpp/src/cluster/kmeans_fit_predict_float.cu +++ b/cpp/src/cluster/kmeans_fit_predict_float.cu @@ -32,4 +32,18 @@ void fit_predict(raft::resources const& handle, cuvs::cluster::kmeans::fit_predict( handle, params, X, sample_weight, centroids, labels, inertia, n_iter); } + +void fit_predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + std::optional> centroids, + raft::device_vector_view labels, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter) + +{ + cuvs::cluster::kmeans::fit_predict( + handle, params, X, sample_weight, centroids, labels, inertia, n_iter); +} } // namespace cuvs::cluster::kmeans diff --git a/cpp/src/cluster/kmeans_mg.hpp b/cpp/src/cluster/kmeans_mg.hpp new file mode 100644 index 000000000..34f38314a --- /dev/null +++ b/cpp/src/cluster/kmeans_mg.hpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once +#include +#include +#include +#include + +namespace cuvs::cluster::kmeans::mg { + +/** + * @brief MNMG kmeans fit + * + * @param[in] handle The raft handle. + * @param[in] params Parameters for KMeans model. + * @param[in] X Training instances to cluster. The data must + * be in row-major format. + * [dim = n_samples x n_features] + * @param[in] sample_weight Optional weights for each observation in X. + * [len = n_samples] + * @param[inout] centroids [in] When init is InitMethod::Array, use + * centroids as the initial cluster centers. + * [out] The generated centroids from the + * kmeans algorithm are stored at the address + * pointed by 'centroids'. + * [dim = n_clusters x n_features] + * @param[out] inertia Sum of squared distances of samples to their + * closest cluster center. + * @param[out] n_iter Number of iterations run. + */ +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); + +void fit(raft::resources const& handle, + const cuvs::cluster::kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::host_scalar_view inertia, + raft::host_scalar_view n_iter); +} // namespace cuvs::cluster::kmeans::mg diff --git a/cpp/src/cluster/kmeans_predict_double.cu b/cpp/src/cluster/kmeans_predict_double.cu new file mode 100644 index 000000000..1fcc393ac --- /dev/null +++ b/cpp/src/cluster/kmeans_predict_double.cu @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kmeans.cuh" +#include + +namespace cuvs::cluster::kmeans { + +void predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::device_vector_view labels, + bool normalize_weight, + raft::host_scalar_view inertia) + +{ + cuvs::cluster::kmeans::predict( + handle, params, X, sample_weight, centroids, labels, normalize_weight, inertia); +} + +void predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::device_vector_view labels, + bool normalize_weight, + raft::host_scalar_view inertia) + +{ + cuvs::cluster::kmeans::predict( + handle, params, X, sample_weight, centroids, labels, normalize_weight, inertia); +} +} // namespace cuvs::cluster::kmeans diff --git a/cpp/src/cluster/kmeans_predict_float.cu b/cpp/src/cluster/kmeans_predict_float.cu index d092152f1..b5f9f9e51 100644 --- a/cpp/src/cluster/kmeans_predict_float.cu +++ b/cpp/src/cluster/kmeans_predict_float.cu @@ -32,4 +32,17 @@ void predict(raft::resources const& handle, cuvs::cluster::kmeans::predict( handle, params, X, sample_weight, centroids, labels, normalize_weight, inertia); } +void predict(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + std::optional> sample_weight, + raft::device_matrix_view centroids, + raft::device_vector_view labels, + bool normalize_weight, + raft::host_scalar_view inertia) + +{ + cuvs::cluster::kmeans::predict( + handle, params, X, sample_weight, centroids, labels, normalize_weight, inertia); +} } // namespace cuvs::cluster::kmeans diff --git a/cpp/src/cluster/kmeans_transform_double.cu b/cpp/src/cluster/kmeans_transform_double.cu new file mode 100644 index 000000000..4a026812e --- /dev/null +++ b/cpp/src/cluster/kmeans_transform_double.cu @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "kmeans.cuh" +#include + +namespace cuvs::cluster::kmeans { + +void transform(raft::resources const& handle, + const kmeans::params& params, + raft::device_matrix_view X, + raft::device_matrix_view centroids, + raft::device_matrix_view X_new) + +{ + cuvs::cluster::kmeans::transform(handle, params, X, centroids, X_new); +} +} // namespace cuvs::cluster::kmeans diff --git a/cpp/src/core/bitset.cu b/cpp/src/core/bitset.cu new file mode 100644 index 000000000..c791747a9 --- /dev/null +++ b/cpp/src/core/bitset.cu @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include + +template struct raft::core::bitset; +template struct raft::core::bitset; +template struct raft::core::bitset; +template struct raft::core::bitset; +template struct raft::core::bitset; diff --git a/cpp/src/distance/detail/pairwise_matrix/dispatch-ext.cuh b/cpp/src/distance/detail/pairwise_matrix/dispatch-ext.cuh index 3107f0fa4..edfd7cf5f 100644 --- a/cpp/src/distance/detail/pairwise_matrix/dispatch-ext.cuh +++ b/cpp/src/distance/detail/pairwise_matrix/dispatch-ext.cuh @@ -22,8 +22,6 @@ #include // raft::identity_op #include // RAFT_EXPLICIT -#ifdef CUVS_EXPLICIT_INSTANTIATE_ONLY - namespace cuvs::distance::detail { template python dispatch_00_generate.py + * + */ + +#include "../distance_ops/all_ops.cuh" // ops::* +#include "dispatch-inl.cuh" // dispatch +#include "dispatch_sm60.cuh" +#include "dispatch_sm80.cuh" +#include // raft::identity_op +#define instantiate_raft_distance_detail_pairwise_matrix_dispatch( \ + OpT, DataT, AccT, OutT, FinOpT, IdxT) \ + template void cuvs::distance::detail:: \ + pairwise_matrix_dispatch, DataT, AccT, OutT, FinOpT, IdxT>( \ + OpT distance_op, \ + IdxT m, \ + IdxT n, \ + IdxT k, \ + const DataT* x, \ + const DataT* y, \ + const OutT* x_norm, \ + const OutT* y_norm, \ + OutT* out, \ + FinOpT fin_op, \ + cudaStream_t stream, \ + bool is_row_major) + +instantiate_raft_distance_detail_pairwise_matrix_dispatch( + cuvs::distance::detail::ops::l2_exp_distance_op, + double, + double, + double, + raft::identity_op, + int64_t); + +#undef instantiate_raft_distance_detail_pairwise_matrix_dispatch diff --git a/cpp/src/distance/detail/pairwise_matrix/dispatch_l2_expanded_float_float_float_int64_t.cu b/cpp/src/distance/detail/pairwise_matrix/dispatch_l2_expanded_float_float_float_int64_t.cu new file mode 100644 index 000000000..94910875c --- /dev/null +++ b/cpp/src/distance/detail/pairwise_matrix/dispatch_l2_expanded_float_float_float_int64_t.cu @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * NOTE: this file is generated by dispatch_00_generate.py + * + * Make changes there and run in this directory: + * + * > python dispatch_00_generate.py + * + */ + +#include "../distance_ops/all_ops.cuh" // ops::* +#include "dispatch-inl.cuh" // dispatch +#include "dispatch_sm60.cuh" +#include "dispatch_sm80.cuh" +#include // raft::identity_op +#define instantiate_raft_distance_detail_pairwise_matrix_dispatch( \ + OpT, DataT, AccT, OutT, FinOpT, IdxT) \ + template void cuvs::distance::detail:: \ + pairwise_matrix_dispatch, DataT, AccT, OutT, FinOpT, IdxT>( \ + OpT distance_op, \ + IdxT m, \ + IdxT n, \ + IdxT k, \ + const DataT* x, \ + const DataT* y, \ + const OutT* x_norm, \ + const OutT* y_norm, \ + OutT* out, \ + FinOpT fin_op, \ + cudaStream_t stream, \ + bool is_row_major) + +instantiate_raft_distance_detail_pairwise_matrix_dispatch( + cuvs::distance::detail::ops::l2_exp_distance_op, float, float, float, raft::identity_op, int64_t); + +#undef instantiate_raft_distance_detail_pairwise_matrix_dispatch diff --git a/cpp/src/distance/detail/pairwise_matrix/dispatch_rbf.cu b/cpp/src/distance/detail/pairwise_matrix/dispatch_rbf.cu index 1cb0ed8ae..3c8f25109 100644 --- a/cpp/src/distance/detail/pairwise_matrix/dispatch_rbf.cu +++ b/cpp/src/distance/detail/pairwise_matrix/dispatch_rbf.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, NVIDIA CORPORATION. + * Copyright (c) 2024, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cpp/src/distance/distance-ext.cuh b/cpp/src/distance/distance-ext.cuh index e7fa30f03..e623f76ba 100644 --- a/cpp/src/distance/distance-ext.cuh +++ b/cpp/src/distance/distance-ext.cuh @@ -26,8 +26,6 @@ #include -#ifdef CUVS_EXPLICIT_INSTANTIATE_ONLY - namespace cuvs { namespace distance { @@ -149,8 +147,6 @@ void pairwise_distance(raft::resources const& handle, }; // namespace distance }; // namespace cuvs -#endif // CUVS_EXPLICIT_INSTANTIATE_ONLY - /* * Hierarchy of instantiations: * @@ -244,6 +240,15 @@ instantiate_cuvs_distance_distance_by_algo(cuvs::distance::DistanceType::Linf); instantiate_cuvs_distance_distance_by_algo(cuvs::distance::DistanceType::LpUnexpanded); instantiate_cuvs_distance_distance_by_algo(cuvs::distance::DistanceType::RusselRaoExpanded); +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2Expanded, float, float, float, int64_t); +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2Expanded, double, double, double, int64_t); +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2SqrtExpanded, float, float, float, int64_t); +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2SqrtExpanded, double, double, double, int64_t); + #undef instantiate_cuvs_distance_distance_by_algo #undef instantiate_cuvs_distance_distance diff --git a/cpp/src/distance/distance.cu b/cpp/src/distance/distance.cu index 72be93f10..c1d39f360 100644 --- a/cpp/src/distance/distance.cu +++ b/cpp/src/distance/distance.cu @@ -105,6 +105,16 @@ instantiate_cuvs_distance_distance_by_algo(cuvs::distance::DistanceType::Linf); instantiate_cuvs_distance_distance_by_algo(cuvs::distance::DistanceType::LpUnexpanded); instantiate_cuvs_distance_distance_by_algo(cuvs::distance::DistanceType::RusselRaoExpanded); +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2Expanded, float, float, float, int64_t); +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2Expanded, double, double, double, int64_t); + +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2SqrtExpanded, float, float, float, int64_t); +instantiate_cuvs_distance_distance( + cuvs::distance::DistanceType::L2SqrtExpanded, double, double, double, int64_t); + #undef instantiate_cuvs_distance_distance_by_algo #undef instantiate_cuvs_distance_distance diff --git a/cpp/src/distance/distance.cuh b/cpp/src/distance/distance.cuh index d1bfc8212..005cb212d 100644 --- a/cpp/src/distance/distance.cuh +++ b/cpp/src/distance/distance.cuh @@ -15,8 +15,4 @@ */ #pragma once -#ifndef CUVS_EXPLICIT_INSTANTIATE_ONLY -#include "distance-inl.cuh" -#endif - #include "distance-ext.cuh" diff --git a/cpp/src/neighbors/brute_force.cu b/cpp/src/neighbors/brute_force.cu index c76feb015..b0f87e9ac 100644 --- a/cpp/src/neighbors/brute_force.cu +++ b/cpp/src/neighbors/brute_force.cu @@ -145,54 +145,45 @@ void index::update_dataset( dataset_view_ = raft::make_const_mdspan(dataset_.view()); } -#define CUVS_INST_BFKNN(T, DistT) \ - auto build(raft::resources const& res, \ - raft::device_matrix_view dataset, \ - cuvs::distance::DistanceType metric, \ - DistT metric_arg) \ - ->cuvs::neighbors::brute_force::index \ - { \ - return detail::build(res, dataset, metric, metric_arg); \ - } \ - auto build(raft::resources const& res, \ - raft::device_matrix_view dataset, \ - cuvs::distance::DistanceType metric, \ - DistT metric_arg) \ - ->cuvs::neighbors::brute_force::index \ - { \ - return detail::build(res, dataset, metric, metric_arg); \ - } \ - \ - void search( \ - raft::resources const& res, \ - const cuvs::neighbors::brute_force::index& idx, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - std::optional> sample_filter = std::nullopt) \ - { \ - if (!sample_filter.has_value()) { \ - detail::brute_force_search(res, idx, queries, neighbors, distances); \ - } else { \ - detail::brute_force_search_filtered( \ - res, idx, queries, *sample_filter, neighbors, distances); \ - } \ - } \ - void search( \ - raft::resources const& res, \ - const cuvs::neighbors::brute_force::index& idx, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - std::optional> sample_filter = std::nullopt) \ - { \ - if (!sample_filter.has_value()) { \ - detail::brute_force_search(res, idx, queries, neighbors, distances); \ - } else { \ - RAFT_FAIL("filtered search isn't available with col_major queries yet"); \ - } \ - } \ - \ +#define CUVS_INST_BFKNN(T, DistT) \ + auto build(raft::resources const& res, \ + raft::device_matrix_view dataset, \ + cuvs::distance::DistanceType metric, \ + DistT metric_arg) \ + ->cuvs::neighbors::brute_force::index \ + { \ + return detail::build(res, dataset, metric, metric_arg); \ + } \ + auto build(raft::resources const& res, \ + raft::device_matrix_view dataset, \ + cuvs::distance::DistanceType metric, \ + DistT metric_arg) \ + ->cuvs::neighbors::brute_force::index \ + { \ + return detail::build(res, dataset, metric, metric_arg); \ + } \ + \ + void search(raft::resources const& res, \ + const cuvs::neighbors::brute_force::index& idx, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ + { \ + detail::search( \ + res, idx, queries, neighbors, distances, sample_filter); \ + } \ + void search(raft::resources const& res, \ + const cuvs::neighbors::brute_force::index& idx, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ + { \ + detail::search( \ + res, idx, queries, neighbors, distances, sample_filter); \ + } \ + \ template struct cuvs::neighbors::brute_force::index; CUVS_INST_BFKNN(float, float); @@ -200,4 +191,4 @@ CUVS_INST_BFKNN(half, float); #undef CUVS_INST_BFKNN -} // namespace cuvs::neighbors::brute_force +} // namespace cuvs::neighbors::brute_force \ No newline at end of file diff --git a/cpp/src/neighbors/brute_force_c.cpp b/cpp/src/neighbors/brute_force_c.cpp index f3ca2e730..eda79aa31 100644 --- a/cpp/src/neighbors/brute_force_c.cpp +++ b/cpp/src/neighbors/brute_force_c.cpp @@ -64,28 +64,31 @@ void _search(cuvsResources_t res, using neighbors_mdspan_type = raft::device_matrix_view; using distances_mdspan_type = raft::device_matrix_view; using prefilter_mds_type = raft::device_vector_view; - using prefilter_opt_type = cuvs::core::bitmap_view; + using prefilter_bmp_type = cuvs::core::bitmap_view; auto queries_mds = cuvs::core::from_dlpack(queries_tensor); auto neighbors_mds = cuvs::core::from_dlpack(neighbors_tensor); auto distances_mds = cuvs::core::from_dlpack(distances_tensor); - std::optional> filter_opt; - if (prefilter.type == NO_FILTER) { - filter_opt = std::nullopt; - } else { + cuvs::neighbors::brute_force::search(*res_ptr, + *index_ptr, + queries_mds, + neighbors_mds, + distances_mds, + cuvs::neighbors::filtering::none_sample_filter{}); + } else if (prefilter.type == BITMAP) { auto prefilter_ptr = reinterpret_cast(prefilter.addr); auto prefilter_mds = cuvs::core::from_dlpack(prefilter_ptr); - auto prefilter_view = prefilter_opt_type((const uint32_t*)prefilter_mds.data_handle(), - queries_mds.extent(0), - index_ptr->dataset().extent(0)); - - filter_opt = std::make_optional(prefilter_view); + auto prefilter_view = cuvs::neighbors::filtering::bitmap_filter( + prefilter_bmp_type((const uint32_t*)prefilter_mds.data_handle(), + queries_mds.extent(0), + index_ptr->dataset().extent(0))); + cuvs::neighbors::brute_force::search( + *res_ptr, *index_ptr, queries_mds, neighbors_mds, distances_mds, prefilter_view); + } else { + RAFT_FAIL("Unsupported prefilter type: BITSET"); } - - cuvs::neighbors::brute_force::search( - *res_ptr, *index_ptr, queries_mds, neighbors_mds, distances_mds, filter_opt); } } // namespace diff --git a/cpp/src/neighbors/cagra.cuh b/cpp/src/neighbors/cagra.cuh index 033f080e2..dacfd6f63 100644 --- a/cpp/src/neighbors/cagra.cuh +++ b/cpp/src/neighbors/cagra.cuh @@ -332,11 +332,29 @@ void search(raft::resources const& res, const index& idx, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances) + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) { - using none_filter_type = cuvs::neighbors::filtering::none_cagra_sample_filter; - return cagra::search_with_filtering( - res, params, idx, queries, neighbors, distances, none_filter_type{}); + try { + using none_filter_type = cuvs::neighbors::filtering::none_sample_filter; + auto& sample_filter = dynamic_cast(sample_filter_ref); + auto sample_filter_copy = sample_filter; + return search_with_filtering( + res, params, idx, queries, neighbors, distances, sample_filter_copy); + return; + } catch (const std::bad_cast&) { + } + + try { + auto& sample_filter = + dynamic_cast&>( + sample_filter_ref); + auto sample_filter_copy = sample_filter; + return search_with_filtering( + res, params, idx, queries, neighbors, distances, sample_filter_copy); + } catch (const std::bad_cast&) { + RAFT_FAIL("Unsupported sample filter type"); + } } template diff --git a/cpp/src/neighbors/cagra_search_float.cu b/cpp/src/neighbors/cagra_search_float.cu index e981d9127..3aca84f74 100644 --- a/cpp/src/neighbors/cagra_search_float.cu +++ b/cpp/src/neighbors/cagra_search_float.cu @@ -19,15 +19,17 @@ namespace cuvs::neighbors::cagra { -#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - cuvs::neighbors::cagra::search_params const& params, \ - const cuvs::neighbors::cagra::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::cagra::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + cuvs::neighbors::cagra::search_params const& params, \ + const cuvs::neighbors::cagra::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ + { \ + cuvs::neighbors::cagra::search( \ + handle, params, index, queries, neighbors, distances, sample_filter); \ } CUVS_INST_CAGRA_SEARCH(float, uint32_t); diff --git a/cpp/src/neighbors/cagra_search_half.cu b/cpp/src/neighbors/cagra_search_half.cu index d80f2bc00..02be12731 100644 --- a/cpp/src/neighbors/cagra_search_half.cu +++ b/cpp/src/neighbors/cagra_search_half.cu @@ -19,15 +19,17 @@ namespace cuvs::neighbors::cagra { -#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - cuvs::neighbors::cagra::search_params const& params, \ - const cuvs::neighbors::cagra::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::cagra::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + cuvs::neighbors::cagra::search_params const& params, \ + const cuvs::neighbors::cagra::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ + { \ + cuvs::neighbors::cagra::search( \ + handle, params, index, queries, neighbors, distances, sample_filter); \ } CUVS_INST_CAGRA_SEARCH(half, uint32_t); diff --git a/cpp/src/neighbors/cagra_search_int8.cu b/cpp/src/neighbors/cagra_search_int8.cu index b44a7507d..3442ef55f 100644 --- a/cpp/src/neighbors/cagra_search_int8.cu +++ b/cpp/src/neighbors/cagra_search_int8.cu @@ -18,15 +18,17 @@ #include namespace cuvs::neighbors::cagra { -#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - cuvs::neighbors::cagra::search_params const& params, \ - const cuvs::neighbors::cagra::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::cagra::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + cuvs::neighbors::cagra::search_params const& params, \ + const cuvs::neighbors::cagra::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ + { \ + cuvs::neighbors::cagra::search( \ + handle, params, index, queries, neighbors, distances, sample_filter); \ } CUVS_INST_CAGRA_SEARCH(int8_t, uint32_t); diff --git a/cpp/src/neighbors/cagra_search_uint8.cu b/cpp/src/neighbors/cagra_search_uint8.cu index cbb7d6652..08fe1861b 100644 --- a/cpp/src/neighbors/cagra_search_uint8.cu +++ b/cpp/src/neighbors/cagra_search_uint8.cu @@ -19,15 +19,17 @@ namespace cuvs::neighbors::cagra { -#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - cuvs::neighbors::cagra::search_params const& params, \ - const cuvs::neighbors::cagra::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::cagra::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_CAGRA_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + cuvs::neighbors::cagra::search_params const& params, \ + const cuvs::neighbors::cagra::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ + { \ + cuvs::neighbors::cagra::search( \ + handle, params, index, queries, neighbors, distances, sample_filter); \ } CUVS_INST_CAGRA_SEARCH(uint8_t, uint32_t); diff --git a/cpp/src/neighbors/detail/cagra/cagra_search.cuh b/cpp/src/neighbors/detail/cagra/cagra_search.cuh index 6dc601f32..4c15b8e14 100644 --- a/cpp/src/neighbors/detail/cagra/cagra_search.cuh +++ b/cpp/src/neighbors/detail/cagra/cagra_search.cuh @@ -17,6 +17,7 @@ #pragma once #include "factory.cuh" +#include "sample_filter_utils.cuh" #include "search_plan.cuh" #include "search_single_cta_inst.cuh" @@ -42,48 +43,6 @@ namespace cuvs::neighbors::cagra::detail { -template -struct CagraSampleFilterWithQueryIdOffset { - const uint32_t offset; - CagraSampleFilterT filter; - - CagraSampleFilterWithQueryIdOffset(const uint32_t offset, const CagraSampleFilterT filter) - : offset(offset), filter(filter) - { - } - - _RAFT_DEVICE auto operator()(const uint32_t query_id, const uint32_t sample_id) - { - return filter(query_id + offset, sample_id); - } -}; - -template -struct CagraSampleFilterT_Selector { - using type = CagraSampleFilterWithQueryIdOffset; -}; -template <> -struct CagraSampleFilterT_Selector { - using type = cuvs::neighbors::filtering::none_cagra_sample_filter; -}; - -// A helper function to set a query id offset -template -inline typename CagraSampleFilterT_Selector::type set_offset( - CagraSampleFilterT filter, const uint32_t offset) -{ - typename CagraSampleFilterT_Selector::type new_filter(offset, filter); - return new_filter; -} -template <> -inline - typename CagraSampleFilterT_Selector::type - set_offset( - cuvs::neighbors::filtering::none_cagra_sample_filter filter, const uint32_t) -{ - return filter; -} - template void search_main_core(raft::resources const& res, search_params params, diff --git a/cpp/src/neighbors/detail/cagra/factory.cuh b/cpp/src/neighbors/detail/cagra/factory.cuh index 2f201de3b..abc907da5 100644 --- a/cpp/src/neighbors/detail/cagra/factory.cuh +++ b/cpp/src/neighbors/detail/cagra/factory.cuh @@ -29,7 +29,7 @@ namespace cuvs::neighbors::cagra::detail { template + typename CagraSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> class factory { public: /** diff --git a/cpp/src/neighbors/detail/cagra/sample_filter_utils.cuh b/cpp/src/neighbors/detail/cagra/sample_filter_utils.cuh new file mode 100644 index 000000000..cd77b9b6b --- /dev/null +++ b/cpp/src/neighbors/detail/cagra/sample_filter_utils.cuh @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../../sample_filter.cuh" + +#include + +namespace cuvs::neighbors::cagra::detail { + +template +struct CagraSampleFilterWithQueryIdOffset { + const uint32_t offset; + CagraSampleFilterT filter; + + CagraSampleFilterWithQueryIdOffset(const uint32_t offset, const CagraSampleFilterT filter) + : offset(offset), filter(filter) + { + } + + _RAFT_DEVICE auto operator()(const uint32_t query_id, const uint32_t sample_id) + { + return filter(query_id + offset, sample_id); + } +}; + +template +struct CagraSampleFilterT_Selector { + using type = CagraSampleFilterWithQueryIdOffset; +}; +template <> +struct CagraSampleFilterT_Selector { + using type = cuvs::neighbors::filtering::none_sample_filter; +}; + +// A helper function to set a query id offset +template +inline typename CagraSampleFilterT_Selector::type set_offset( + CagraSampleFilterT filter, const uint32_t offset) +{ + typename CagraSampleFilterT_Selector::type new_filter(offset, filter); + return new_filter; +} +template <> +inline typename CagraSampleFilterT_Selector::type +set_offset( + cuvs::neighbors::filtering::none_sample_filter filter, const uint32_t) +{ + return filter; +} +} // namespace cuvs::neighbors::cagra::detail diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py index 4e3983e3f..b05afd2c9 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_00_generate.py @@ -38,6 +38,9 @@ */ #include "search_multi_cta_inst.cuh" +#include "sample_filter_utils.cuh" + +#define COMMA , namespace cuvs::neighbors::cagra::detail::multi_cta_search { """ @@ -65,7 +68,10 @@ with open(path, "w") as f: f.write(header) f.write( - f"instantiate_kernel_selection(\n {data_t}, {idx_t}, {distance_t}, cuvs::neighbors::filtering::none_cagra_sample_filter);\n" + f"instantiate_kernel_selection(\n {data_t}, {idx_t}, {distance_t}, cuvs::neighbors::filtering::none_sample_filter);\n" + ) + f.write( + f"instantiate_kernel_selection(\n {data_t}, {idx_t}, {distance_t}, CagraSampleFilterWithQueryIdOffset>);\n" ) f.write(trailer) # For pasting into CMakeLists.txt diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32.cu index fae5a9387..0ee0fa082 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_float_uint32.cu @@ -23,12 +23,20 @@ * */ +#include "sample_filter_utils.cuh" #include "search_multi_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::multi_cta_search { instantiate_kernel_selection(float, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + cuvs::neighbors::filtering::none_sample_filter); +instantiate_kernel_selection(float, + uint32_t, + float, + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_half_uint32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_half_uint32.cu index 9606d510f..3bd4df172 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_half_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_half_uint32.cu @@ -23,12 +23,17 @@ * */ +#include "sample_filter_utils.cuh" #include "search_multi_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::multi_cta_search { +instantiate_kernel_selection(half, uint32_t, float, cuvs::neighbors::filtering::none_sample_filter); instantiate_kernel_selection(half, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32.cu index a3322c435..4e7389b4b 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_int8_uint32.cu @@ -23,12 +23,20 @@ * */ +#include "sample_filter_utils.cuh" #include "search_multi_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::multi_cta_search { instantiate_kernel_selection(int8_t, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + cuvs::neighbors::filtering::none_sample_filter); +instantiate_kernel_selection(int8_t, + uint32_t, + float, + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh b/cpp/src/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh index 4dfc46256..9fa9d5894 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_kernel-inl.cuh @@ -282,7 +282,7 @@ RAFT_KERNEL __launch_bounds__(1024, 1) search_kernel( // Filtering if constexpr (!std::is_same::value) { + cuvs::neighbors::filtering::none_sample_filter>::value) { constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; const INDEX_T invalid_index = utils::get_max_value(); @@ -305,7 +305,7 @@ RAFT_KERNEL __launch_bounds__(1024, 1) search_kernel( // Post process for filtering if constexpr (!std::is_same::value) { + cuvs::neighbors::filtering::none_sample_filter>::value) { constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; const INDEX_T invalid_index = utils::get_max_value(); diff --git a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32.cu b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32.cu index 51fc6526f..ed0e0387c 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_multi_cta_uint8_uint32.cu @@ -23,12 +23,20 @@ * */ +#include "sample_filter_utils.cuh" #include "search_multi_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::multi_cta_search { instantiate_kernel_selection(uint8_t, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + cuvs::neighbors::filtering::none_sample_filter); +instantiate_kernel_selection(uint8_t, + uint32_t, + float, + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::multi_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_multi_kernel.cuh b/cpp/src/neighbors/detail/cagra/search_multi_kernel.cuh index 0daae17b3..9c22134a6 100644 --- a/cpp/src/neighbors/detail/cagra/search_multi_kernel.cuh +++ b/cpp/src/neighbors/detail/cagra/search_multi_kernel.cuh @@ -365,7 +365,7 @@ RAFT_KERNEL compute_distance_to_child_nodes_kernel( } if constexpr (!std::is_same::value) { + cuvs::neighbors::filtering::none_sample_filter>::value) { if (!sample_filter(query_id, parent_index)) { parent_candidates_ptr[parent_list_index + (lds * query_id)] = utils::get_max_value(); parent_distance_ptr[parent_list_index + (lds * query_id)] = @@ -779,7 +779,7 @@ struct search : search_plan_impl { // Topk hint can not be used when applying a filter uint32_t* const top_hint_ptr = - std::is_same::value + std::is_same::value ? topk_hint.data() : nullptr; // Init topk_hint @@ -878,7 +878,7 @@ struct search : search_plan_impl { auto result_distances_ptr = result_distances.data() + (iter & 0x1) * result_buffer_size; if constexpr (!std::is_same::value) { + cuvs::neighbors::filtering::none_sample_filter>::value) { // Remove parent bit in search results remove_parent_bit(num_queries, result_buffer_size, diff --git a/cpp/src/neighbors/detail/cagra/search_plan.cuh b/cpp/src/neighbors/detail/cagra/search_plan.cuh index 6ecbbc2e8..f23b96631 100644 --- a/cpp/src/neighbors/detail/cagra/search_plan.cuh +++ b/cpp/src/neighbors/detail/cagra/search_plan.cuh @@ -361,7 +361,7 @@ struct search_plan_impl : public search_plan_impl_base { std::to_string(hashmap_max_fill_rate) + " has been given."; } if constexpr (!std::is_same::value) { + cuvs::neighbors::filtering::none_sample_filter>::value) { if (hashmap_mode == hash_mode::SMALL) { error_message += "`SMALL` hash is not available when filtering"; } else { diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py index 4693cd54d..d59201061 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_00_generate.py @@ -37,8 +37,11 @@ * */ +#include "sample_filter_utils.cuh" #include "search_single_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::single_cta_search { """ @@ -68,7 +71,10 @@ with open(path, "w") as f: f.write(header) f.write( - f"instantiate_kernel_selection(\n {data_t}, {idx_t}, {distance_t}, cuvs::neighbors::filtering::none_cagra_sample_filter);\n" + f"instantiate_kernel_selection(\n {data_t}, {idx_t}, {distance_t}, cuvs::neighbors::filtering::none_sample_filter);\n" + ) + f.write( + f"instantiate_kernel_selection(\n {data_t}, {idx_t}, {distance_t}, CagraSampleFilterWithQueryIdOffset>);\n" ) f.write(trailer) diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32.cu index f8495bc01..7de479e97 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_float_uint32.cu @@ -23,12 +23,20 @@ * */ +#include "sample_filter_utils.cuh" #include "search_single_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::single_cta_search { instantiate_kernel_selection(float, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + cuvs::neighbors::filtering::none_sample_filter); +instantiate_kernel_selection(float, + uint32_t, + float, + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_half_uint32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_half_uint32.cu index c21e6d1f4..10abe1b24 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_half_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_half_uint32.cu @@ -23,12 +23,17 @@ * */ +#include "sample_filter_utils.cuh" #include "search_single_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::single_cta_search { +instantiate_kernel_selection(half, uint32_t, float, cuvs::neighbors::filtering::none_sample_filter); instantiate_kernel_selection(half, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32.cu index 56a0d8ba9..ec0ea974c 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_int8_uint32.cu @@ -23,12 +23,20 @@ * */ +#include "sample_filter_utils.cuh" #include "search_single_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::single_cta_search { instantiate_kernel_selection(int8_t, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + cuvs::neighbors::filtering::none_sample_filter); +instantiate_kernel_selection(int8_t, + uint32_t, + float, + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh b/cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh index 21a0f6bb2..79cb6bc10 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh @@ -627,8 +627,7 @@ __device__ void search_core( // topk with bitonic sort _CLK_START(); - if (std::is_same::value || + if (std::is_same::value || *filter_flag == 0) { topk_by_bitonic_sort(result_distances_buffer, result_indices_buffer, @@ -716,7 +715,7 @@ __device__ void search_core( // Filtering if constexpr (!std::is_same::value) { + cuvs::neighbors::filtering::none_sample_filter>::value) { if (threadIdx.x == 0) { *filter_flag = 0; } __syncthreads(); @@ -742,7 +741,7 @@ __device__ void search_core( // Post process for filtering if constexpr (!std::is_same::value) { + cuvs::neighbors::filtering::none_sample_filter>::value) { constexpr INDEX_T index_msb_1_mask = utils::gen_index_msb_1_mask::value; const INDEX_T invalid_index = utils::get_max_value(); diff --git a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32.cu b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32.cu index ee6427170..9df50513c 100644 --- a/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32.cu +++ b/cpp/src/neighbors/detail/cagra/search_single_cta_uint8_uint32.cu @@ -23,12 +23,20 @@ * */ +#include "sample_filter_utils.cuh" #include "search_single_cta_inst.cuh" +#define COMMA , + namespace cuvs::neighbors::cagra::detail::single_cta_search { instantiate_kernel_selection(uint8_t, uint32_t, float, - cuvs::neighbors::filtering::none_cagra_sample_filter); + cuvs::neighbors::filtering::none_sample_filter); +instantiate_kernel_selection(uint8_t, + uint32_t, + float, + CagraSampleFilterWithQueryIdOffset< + cuvs::neighbors::filtering::bitset_filter>); } // namespace cuvs::neighbors::cagra::detail::single_cta_search diff --git a/cpp/src/neighbors/detail/knn_brute_force.cuh b/cpp/src/neighbors/detail/knn_brute_force.cuh index cf27bcde7..e5eeecbc9 100644 --- a/cpp/src/neighbors/detail/knn_brute_force.cuh +++ b/cpp/src/neighbors/detail/knn_brute_force.cuh @@ -62,7 +62,10 @@ namespace cuvs::neighbors::detail { * Calculates brute force knn, using a fixed memory budget * by tiling over both the rows and columns of pairwise_distances */ -template +template void tiled_brute_force_knn(const raft::resources& handle, const ElementType* search, // size (m ,d) const ElementType* index, // size (n ,d) @@ -78,7 +81,8 @@ void tiled_brute_force_knn(const raft::resources& handle, size_t max_col_tile_size = 0, const DistanceT* precomputed_index_norms = nullptr, const DistanceT* precomputed_search_norms = nullptr, - const uint32_t* filter_bitmap = nullptr) + const uint32_t* filter_bitmap = nullptr, + DistanceEpilogue distance_epilogue = raft::identity_op()) { // Figure out the number of rows/cols to tile for size_t tile_rows = 0; @@ -207,7 +211,8 @@ void tiled_brute_force_knn(const raft::resources& handle, IndexType col = j + (idx % current_centroid_size); cuvs::distance::detail::ops::l2_exp_cutlass_op l2_op(sqrt); - return l2_op(row_norms[row], col_norms[col], dist[idx]); + auto val = l2_op(row_norms[row], col_norms[col], dist[idx]); + return distance_epilogue(val, row, col); }); } else if (metric == cuvs::distance::DistanceType::CosineExpanded) { auto row_norms = precomputed_search_norms ? precomputed_search_norms : search_norms.data(); @@ -221,8 +226,22 @@ void tiled_brute_force_knn(const raft::resources& handle, IndexType row = i + (idx / current_centroid_size); IndexType col = j + (idx % current_centroid_size); auto val = DistanceT(1.0) - dist[idx] / DistanceT(row_norms[row] * col_norms[col]); - return val; + return distance_epilogue(val, row, col); }); + } else { + // if we're not l2 distance, and we have a distance epilogue - run it now + if constexpr (!std::is_same_v) { + auto distances_ptr = temp_distances.data(); + raft::linalg::map_offset( + handle, + raft::make_device_vector_view(temp_distances.data(), + current_query_size * current_centroid_size), + [=] __device__(size_t idx) { + IndexType row = i + (idx / current_centroid_size); + IndexType col = j + (idx % current_centroid_size); + return distance_epilogue(distances_ptr[idx], row, col); + }); + } } if (filter_bitmap != nullptr) { @@ -699,6 +718,38 @@ void brute_force_search_filtered( return; } +template +void search(raft::resources const& res, + const cuvs::neighbors::brute_force::index& idx, + raft::device_matrix_view queries, + raft::device_matrix_view neighbors, + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) +{ + try { + auto& sample_filter = + dynamic_cast(sample_filter_ref); + return brute_force_search(res, idx, queries, neighbors, distances); + } catch (const std::bad_cast&) { + } + + try { + auto& sample_filter = + dynamic_cast&>( + sample_filter_ref); + if constexpr (std::is_same_v) { + RAFT_FAIL("filtered search isn't available with col_major queries yet"); + } else { + cuvs::core::bitmap_view sample_filter_view = + sample_filter.bitmap_view_; + return brute_force_search_filtered( + res, idx, queries, sample_filter_view, neighbors, distances); + } + } catch (const std::bad_cast&) { + RAFT_FAIL("Unsupported sample filter type"); + } +} + template cuvs::neighbors::brute_force::index build( raft::resources const& res, diff --git a/cpp/src/neighbors/detail/reachability.cuh b/cpp/src/neighbors/detail/reachability.cuh new file mode 100644 index 000000000..903c6f1da --- /dev/null +++ b/cpp/src/neighbors/detail/reachability.cuh @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include "./knn_brute_force.cuh" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace cuvs::neighbors::detail::reachability { + +/** + * Extract core distances from KNN graph. This is essentially + * performing a knn_dists[:,min_pts] + * @tparam value_idx data type for integrals + * @tparam value_t data type for distance + * @tparam tpb block size for kernel + * @param[in] knn_dists knn distance array (size n * k) + * @param[in] min_samples this neighbor will be selected for core distances + * @param[in] n_neighbors the number of neighbors of each point in the knn graph + * @param[in] n number of samples + * @param[out] out output array (size n) + * @param[in] stream stream for which to order cuda operations + */ +template +void core_distances( + value_t* knn_dists, int min_samples, int n_neighbors, size_t n, value_t* out, cudaStream_t stream) +{ + ASSERT(n_neighbors >= min_samples, + "the size of the neighborhood should be greater than or equal to min_samples"); + + auto exec_policy = rmm::exec_policy(stream); + + auto indices = thrust::make_counting_iterator(0); + + thrust::transform(exec_policy, indices, indices + n, out, [=] __device__(value_idx row) { + return knn_dists[row * n_neighbors + (min_samples - 1)]; + }); +} + +/** + * Wraps the brute force knn API, to be used for both training and prediction + * @tparam value_idx data type for integrals + * @tparam value_t data type for distance + * @param[in] handle raft handle for resource reuse + * @param[in] X input data points (size m * n) + * @param[out] inds nearest neighbor indices (size n_search_items * k) + * @param[out] dists nearest neighbor distances (size n_search_items * k) + * @param[in] m number of rows in X + * @param[in] n number of columns in X + * @param[in] search_items array of items to search of dimensionality D (size n_search_items * n) + * @param[in] n_search_items number of rows in search_items + * @param[in] k number of nearest neighbors + * @param[in] metric distance metric to use + */ +template +void compute_knn(const raft::resources& handle, + const value_t* X, + value_idx* inds, + value_t* dists, + size_t m, + size_t n, + const value_t* search_items, + size_t n_search_items, + int k, + cuvs::distance::DistanceType metric) +{ + // perform knn + tiled_brute_force_knn(handle, X, search_items, m, n_search_items, n, k, dists, inds, metric); +} + +/* + @brief Internal function for CPU->GPU interop + to compute core_dists +*/ +template +void _compute_core_dists(const raft::resources& handle, + const value_t* X, + value_t* core_dists, + size_t m, + size_t n, + cuvs::distance::DistanceType metric, + int min_samples) +{ + RAFT_EXPECTS(metric == cuvs::distance::DistanceType::L2SqrtExpanded, + "Currently only L2 expanded distance is supported"); + + auto stream = raft::resource::get_cuda_stream(handle); + + rmm::device_uvector inds(min_samples * m, stream); + rmm::device_uvector dists(min_samples * m, stream); + + // perform knn + compute_knn(handle, X, inds.data(), dists.data(), m, n, X, m, min_samples, metric); + + // Slice core distances (distances to kth nearest neighbor) + core_distances(dists.data(), min_samples, min_samples, m, core_dists, stream); +} + +// Functor to post-process distances into reachability space +template +struct ReachabilityPostProcess { + DI value_t operator()(value_t value, value_idx row, value_idx col) const + { + return max(core_dists[col], max(core_dists[row], alpha * value)); + } + + const value_t* core_dists; + value_t alpha; +}; + +/** + * Given core distances, Fuses computations of L2 distances between all + * points, projection into mutual reachability space, and k-selection. + * @tparam value_idx + * @tparam value_t + * @param[in] handle raft handle for resource reuse + * @param[out] out_inds output indices array (size m * k) + * @param[out] out_dists output distances array (size m * k) + * @param[in] X input data points (size m * n) + * @param[in] m number of rows in X + * @param[in] n number of columns in X + * @param[in] k neighborhood size (includes self-loop) + * @param[in] core_dists array of core distances (size m) + */ +template +void mutual_reachability_knn_l2(const raft::resources& handle, + value_idx* out_inds, + value_t* out_dists, + const value_t* X, + size_t m, + size_t n, + int k, + value_t* core_dists, + value_t alpha) +{ + // Create a functor to postprocess distances into mutual reachability space + // Note that we can't use a lambda for this here, since we get errors like: + // `A type local to a function cannot be used in the template argument of the + // enclosing parent function (and any parent classes) of an extended __device__ + // or __host__ __device__ lambda` + auto epilogue = ReachabilityPostProcess{core_dists, alpha}; + + cuvs::neighbors::detail:: + tiled_brute_force_knn>( + handle, + X, + X, + m, + m, + n, + k, + out_dists, + out_inds, + cuvs::distance::DistanceType::L2SqrtExpanded, + 2.0, + 0, + 0, + nullptr, + nullptr, + nullptr, + epilogue); +} + +template +void mutual_reachability_graph(const raft::resources& handle, + const value_t* X, + size_t m, + size_t n, + cuvs::distance::DistanceType metric, + int min_samples, + value_t alpha, + value_idx* indptr, + value_t* core_dists, + raft::sparse::COO& out) +{ + RAFT_EXPECTS(metric == cuvs::distance::DistanceType::L2SqrtExpanded, + "Currently only L2 expanded distance is supported"); + + auto stream = raft::resource::get_cuda_stream(handle); + auto exec_policy = raft::resource::get_thrust_policy(handle); + + rmm::device_uvector coo_rows(min_samples * m, stream); + rmm::device_uvector inds(min_samples * m, stream); + rmm::device_uvector dists(min_samples * m, stream); + + // perform knn + compute_knn(handle, X, inds.data(), dists.data(), m, n, X, m, min_samples, metric); + + // Slice core distances (distances to kth nearest neighbor) + core_distances(dists.data(), min_samples, min_samples, m, core_dists, stream); + + /** + * Compute L2 norm + */ + mutual_reachability_knn_l2( + handle, inds.data(), dists.data(), X, m, n, min_samples, core_dists, (value_t)1.0 / alpha); + + // self-loops get max distance + auto coo_rows_counting_itr = thrust::make_counting_iterator(0); + thrust::transform(exec_policy, + coo_rows_counting_itr, + coo_rows_counting_itr + (m * min_samples), + coo_rows.data(), + [min_samples] __device__(value_idx c) -> value_idx { return c / min_samples; }); + + raft::sparse::linalg::symmetrize( + handle, coo_rows.data(), inds.data(), dists.data(), m, m, min_samples * m, out); + + raft::sparse::convert::sorted_coo_to_csr(out.rows(), out.nnz, indptr, m + 1, stream); + + // self-loops get max distance + auto transform_in = + thrust::make_zip_iterator(thrust::make_tuple(out.rows(), out.cols(), out.vals())); + + thrust::transform(exec_policy, + transform_in, + transform_in + out.nnz, + out.vals(), + [=] __device__(const thrust::tuple& tup) { + return thrust::get<0>(tup) == thrust::get<1>(tup) + ? std::numeric_limits::max() + : thrust::get<2>(tup); + }); +} + +} // namespace cuvs::neighbors::detail::reachability diff --git a/cpp/src/neighbors/ivf_flat/generate_ivf_flat.py b/cpp/src/neighbors/ivf_flat/generate_ivf_flat.py index e739bddd4..1fabcca8c 100644 --- a/cpp/src/neighbors/ivf_flat/generate_ivf_flat.py +++ b/cpp/src/neighbors/ivf_flat/generate_ivf_flat.py @@ -140,28 +140,18 @@ """ search_macro = """ -#define CUVS_INST_IVF_FLAT_SEARCH(T, IdxT) \\ - void search(raft::resources const& handle, \\ - const cuvs::neighbors::ivf_flat::search_params& params, \\ - cuvs::neighbors::ivf_flat::index& index, \\ - raft::device_matrix_view queries, \\ - raft::device_matrix_view neighbors, \\ - raft::device_matrix_view distances) \\ - { \\ - cuvs::neighbors::ivf_flat::detail::search( \\ - handle, params, index, queries, neighbors, distances); \\ - } \\ - void search_with_filtering( \\ - raft::resources const& handle, \\ - const search_params& params, \\ - index& idx, \\ - raft::device_matrix_view queries, \\ - raft::device_matrix_view neighbors, \\ - raft::device_matrix_view distances, \\ - cuvs::neighbors::filtering::bitset_filter sample_filter) \\ - { \\ - cuvs::neighbors::ivf_flat::detail::search_with_filtering( \\ - handle, params, idx, queries, neighbors, distances, sample_filter); \\ +#define CUVS_INST_IVF_FLAT_SEARCH(T, IdxT) \\ + void search( \\ + raft::resources const& handle, \\ + const cuvs::neighbors::ivf_flat::search_params& params, \\ + cuvs::neighbors::ivf_flat::index& index, \\ + raft::device_matrix_view queries, \\ + raft::device_matrix_view neighbors, \\ + raft::device_matrix_view distances, \\ + const cuvs::neighbors::filtering::base_filter& sample_filter) \\ + { \\ + cuvs::neighbors::ivf_flat::detail::search( \\ + handle, params, index, queries, neighbors, distances, sample_filter); \\ } """ diff --git a/cpp/src/neighbors/ivf_flat/ivf_flat_interleaved_scan.cuh b/cpp/src/neighbors/ivf_flat/ivf_flat_interleaved_scan.cuh index a4f769741..9626b2ce5 100644 --- a/cpp/src/neighbors/ivf_flat/ivf_flat_interleaved_scan.cuh +++ b/cpp/src/neighbors/ivf_flat/ivf_flat_interleaved_scan.cuh @@ -1304,7 +1304,7 @@ struct select_interleaved_scan_kernel { * (one block processes one or more probes, hence: 1 <= grid_dim_x <= n_probes) * @param stream * @param sample_filter - * A filter that selects samples for a given query. Use an instance of none_ivf_sample_filter to + * A filter that selects samples for a given query. Use an instance of none_sample_filter to * provide a green light for every sample. */ template diff --git a/cpp/src/neighbors/ivf_flat/ivf_flat_search.cuh b/cpp/src/neighbors/ivf_flat/ivf_flat_search.cuh index b7dac3ef8..032b6a8ff 100644 --- a/cpp/src/neighbors/ivf_flat/ivf_flat_search.cuh +++ b/cpp/src/neighbors/ivf_flat/ivf_flat_search.cuh @@ -20,13 +20,14 @@ #include "../detail/ann_utils.cuh" #include "../ivf_common.cuh" // cuvs::neighbors::detail::ivf #include "ivf_flat_interleaved_scan.cuh" // interleaved_scan -#include // none_ivf_sample_filter +#include // none_sample_filter #include // raft::neighbors::ivf_flat::index #include "../detail/ann_utils.cuh" // utils::mapping #include // is_min_close, DistanceType #include // cuvs::selection::select_k -#include // RAFT_LOG_TRACE +#include +#include // RAFT_LOG_TRACE #include #include // raft::resources #include // raft::linalg::gemm @@ -307,7 +308,7 @@ void search_impl(raft::resources const& handle, /** See raft::neighbors::ivf_flat::search docs */ template + typename IvfSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> inline void search_with_filtering(raft::resources const& handle, const search_params& params, const index& index, @@ -402,15 +403,24 @@ void search(raft::resources const& handle, const index& idx, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances) + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) { - search_with_filtering(handle, - params, - idx, - queries, - neighbors, - distances, - cuvs::neighbors::filtering::none_ivf_sample_filter()); + try { + auto& sample_filter = + dynamic_cast(sample_filter_ref); + return search_with_filtering(handle, params, idx, queries, neighbors, distances, sample_filter); + } catch (const std::bad_cast&) { + } + + try { + auto& sample_filter = + dynamic_cast&>( + sample_filter_ref); + return search_with_filtering(handle, params, idx, queries, neighbors, distances, sample_filter); + } catch (const std::bad_cast&) { + RAFT_FAIL("Unsupported sample filter type"); + } } } // namespace cuvs::neighbors::ivf_flat::detail diff --git a/cpp/src/neighbors/ivf_flat/ivf_flat_search_float_int64_t.cu b/cpp/src/neighbors/ivf_flat/ivf_flat_search_float_int64_t.cu index 93e46cbef..3f262d612 100644 --- a/cpp/src/neighbors/ivf_flat/ivf_flat_search_float_int64_t.cu +++ b/cpp/src/neighbors/ivf_flat/ivf_flat_search_float_int64_t.cu @@ -35,22 +35,11 @@ namespace cuvs::neighbors::ivf_flat { cuvs::neighbors::ivf_flat::index& index, \ raft::device_matrix_view queries, \ raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ { \ cuvs::neighbors::ivf_flat::detail::search( \ - handle, params, index, queries, neighbors, distances); \ - } \ - void search_with_filtering( \ - raft::resources const& handle, \ - const search_params& params, \ - index& idx, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - cuvs::neighbors::filtering::bitset_filter sample_filter) \ - { \ - cuvs::neighbors::ivf_flat::detail::search_with_filtering( \ - handle, params, idx, queries, neighbors, distances, sample_filter); \ + handle, params, index, queries, neighbors, distances, sample_filter); \ } CUVS_INST_IVF_FLAT_SEARCH(float, int64_t); diff --git a/cpp/src/neighbors/ivf_flat/ivf_flat_search_int8_t_int64_t.cu b/cpp/src/neighbors/ivf_flat/ivf_flat_search_int8_t_int64_t.cu index 5f75d3d48..4357afb0a 100644 --- a/cpp/src/neighbors/ivf_flat/ivf_flat_search_int8_t_int64_t.cu +++ b/cpp/src/neighbors/ivf_flat/ivf_flat_search_int8_t_int64_t.cu @@ -35,22 +35,11 @@ namespace cuvs::neighbors::ivf_flat { cuvs::neighbors::ivf_flat::index& index, \ raft::device_matrix_view queries, \ raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ { \ cuvs::neighbors::ivf_flat::detail::search( \ - handle, params, index, queries, neighbors, distances); \ - } \ - void search_with_filtering( \ - raft::resources const& handle, \ - const search_params& params, \ - index& idx, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - cuvs::neighbors::filtering::bitset_filter sample_filter) \ - { \ - cuvs::neighbors::ivf_flat::detail::search_with_filtering( \ - handle, params, idx, queries, neighbors, distances, sample_filter); \ + handle, params, index, queries, neighbors, distances, sample_filter); \ } CUVS_INST_IVF_FLAT_SEARCH(int8_t, int64_t); diff --git a/cpp/src/neighbors/ivf_flat/ivf_flat_search_uint8_t_int64_t.cu b/cpp/src/neighbors/ivf_flat/ivf_flat_search_uint8_t_int64_t.cu index a2696dc84..8265a3e17 100644 --- a/cpp/src/neighbors/ivf_flat/ivf_flat_search_uint8_t_int64_t.cu +++ b/cpp/src/neighbors/ivf_flat/ivf_flat_search_uint8_t_int64_t.cu @@ -35,22 +35,11 @@ namespace cuvs::neighbors::ivf_flat { cuvs::neighbors::ivf_flat::index& index, \ raft::device_matrix_view queries, \ raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter) \ { \ cuvs::neighbors::ivf_flat::detail::search( \ - handle, params, index, queries, neighbors, distances); \ - } \ - void search_with_filtering( \ - raft::resources const& handle, \ - const search_params& params, \ - index& idx, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - cuvs::neighbors::filtering::bitset_filter sample_filter) \ - { \ - cuvs::neighbors::ivf_flat::detail::search_with_filtering( \ - handle, params, idx, queries, neighbors, distances, sample_filter); \ + handle, params, index, queries, neighbors, distances, sample_filter); \ } CUVS_INST_IVF_FLAT_SEARCH(uint8_t, int64_t); diff --git a/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq.py b/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq.py index 9b3083c3b..a5a829967 100644 --- a/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq.py +++ b/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq.py @@ -67,29 +67,15 @@ search_macro = """ #define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \\ void search(raft::resources const& handle, \\ - const cuvs::neighbors::ivf_pq::search_params& params, \\ - cuvs::neighbors::ivf_pq::index& index, \\ - raft::device_matrix_view queries, \\ - raft::device_matrix_view neighbors, \\ - raft::device_matrix_view distances) \\ - { \\ - cuvs::neighbors::ivf_pq::detail::search( \\ - handle, params, index, queries, neighbors, distances); \\ - } -""" -search_with_filter_macro = """ -#define CUVS_INST_IVF_PQ_SEARCH_FILTER(T, IdxT) \\ - void search_with_filtering(raft::resources const& handle, \\ const cuvs::neighbors::ivf_pq::search_params& params, \\ cuvs::neighbors::ivf_pq::index& index, \\ raft::device_matrix_view queries, \\ raft::device_matrix_view neighbors, \\ raft::device_matrix_view distances, \\ - cuvs::neighbors::filtering::bitset_filter< \\ - uint32_t, IdxT> sample_filter) \\ + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) \\ { \\ - cuvs::neighbors::ivf_pq::detail::search_with_filtering( \\ - handle, params, index, queries, neighbors, distances, sample_filter); \\ + cuvs::neighbors::ivf_pq::detail::search( \\ + handle, params, index, queries, neighbors, distances, sample_filter_ref); \\ } """ @@ -104,11 +90,6 @@ definition=search_macro, name="CUVS_INST_IVF_PQ_SEARCH", ), - search_with_filter=dict( - include=search_include_macro, - definition=search_with_filter_macro, - name="CUVS_INST_IVF_PQ_SEARCH_FILTER", - ), ) for type_path, (T, IdxT) in types.items(): diff --git a/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq_compute_similarity.py b/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq_compute_similarity.py index 4c35b2836..75373e746 100644 --- a/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq_compute_similarity.py +++ b/cpp/src/neighbors/ivf_pq/detail/generate_ivf_pq_compute_similarity.py @@ -86,7 +86,7 @@ """ none_filter_int64 = "cuvs::neighbors::filtering::ivf_to_sample_filter" \ - "" + "" bitset_filter64 = "cuvs::neighbors::filtering::ivf_to_sample_filter" \ ">" diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_float.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_float.cu index 26312a4ae..bc73ff5a3 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_float.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_float.cu @@ -71,4 +71,4 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, float, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_false.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_false.cu index f08f1700c..2aa0bacf4 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_false.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_false.cu @@ -71,4 +71,4 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_true.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_true.cu index 588c89604..d4e3fdf5c 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_true.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_fp8_true.cu @@ -71,4 +71,4 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_half.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_half.cu index 6c2f77412..02e118158 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_half.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_float_half.cu @@ -71,4 +71,4 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, half, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_false.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_false.cu index 7170e49db..cde961c72 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_false.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_false.cu @@ -71,4 +71,4 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( half, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_true.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_true.cu index c552065ab..f1efe79f9 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_true.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_fp8_true.cu @@ -71,4 +71,4 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( half, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_half.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_half.cu index 8d9399da3..bb56fd08d 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_half.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_compute_similarity_half_half.cu @@ -71,4 +71,4 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( half, half, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_float_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_float_int64_t.cu index 0f54eede7..07ee110bc 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_float_int64_t.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_float_int64_t.cu @@ -29,15 +29,17 @@ namespace cuvs::neighbors::ivf_pq { -#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::ivf_pq::detail::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + const cuvs::neighbors::ivf_pq::search_params& params, \ + cuvs::neighbors::ivf_pq::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) \ + { \ + cuvs::neighbors::ivf_pq::detail::search( \ + handle, params, index, queries, neighbors, distances, sample_filter_ref); \ } CUVS_INST_IVF_PQ_SEARCH(float, int64_t); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_half_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_half_int64_t.cu index e5556e593..cf387cb67 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_half_int64_t.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_half_int64_t.cu @@ -29,15 +29,17 @@ namespace cuvs::neighbors::ivf_pq { -#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::ivf_pq::detail::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + const cuvs::neighbors::ivf_pq::search_params& params, \ + cuvs::neighbors::ivf_pq::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) \ + { \ + cuvs::neighbors::ivf_pq::detail::search( \ + handle, params, index, queries, neighbors, distances, sample_filter_ref); \ } CUVS_INST_IVF_PQ_SEARCH(half, int64_t); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_int8_t_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_int8_t_int64_t.cu index 297e615d2..5ec9093df 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_int8_t_int64_t.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_int8_t_int64_t.cu @@ -29,15 +29,17 @@ namespace cuvs::neighbors::ivf_pq { -#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::ivf_pq::detail::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + const cuvs::neighbors::ivf_pq::search_params& params, \ + cuvs::neighbors::ivf_pq::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) \ + { \ + cuvs::neighbors::ivf_pq::detail::search( \ + handle, params, index, queries, neighbors, distances, sample_filter_ref); \ } CUVS_INST_IVF_PQ_SEARCH(int8_t, int64_t); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_uint8_t_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_uint8_t_int64_t.cu index 3cf8bfaff..d2e2f3b00 100644 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_uint8_t_int64_t.cu +++ b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_uint8_t_int64_t.cu @@ -29,15 +29,17 @@ namespace cuvs::neighbors::ivf_pq { -#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ - void search(raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances) \ - { \ - cuvs::neighbors::ivf_pq::detail::search(handle, params, index, queries, neighbors, distances); \ +#define CUVS_INST_IVF_PQ_SEARCH(T, IdxT) \ + void search(raft::resources const& handle, \ + const cuvs::neighbors::ivf_pq::search_params& params, \ + cuvs::neighbors::ivf_pq::index& index, \ + raft::device_matrix_view queries, \ + raft::device_matrix_view neighbors, \ + raft::device_matrix_view distances, \ + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) \ + { \ + cuvs::neighbors::ivf_pq::detail::search( \ + handle, params, index, queries, neighbors, distances, sample_filter_ref); \ } CUVS_INST_IVF_PQ_SEARCH(uint8_t, int64_t); diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_float_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_float_int64_t.cu deleted file mode 100644 index 4e7541882..000000000 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_float_int64_t.cu +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2024, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * NOTE: this file is generated by generate_ivf_pq.py - * - * Make changes there and run in this directory: - * - * > python generate_ivf_pq.py - * - */ - -#include - -#include "../ivf_pq_search.cuh" - -namespace cuvs::neighbors::ivf_pq { - -#define CUVS_INST_IVF_PQ_SEARCH_FILTER(T, IdxT) \ - void search_with_filtering( \ - raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - cuvs::neighbors::filtering::bitset_filter sample_filter) \ - { \ - cuvs::neighbors::ivf_pq::detail::search_with_filtering( \ - handle, params, index, queries, neighbors, distances, sample_filter); \ - } -CUVS_INST_IVF_PQ_SEARCH_FILTER(float, int64_t); - -#undef CUVS_INST_IVF_PQ_SEARCH_FILTER - -} // namespace cuvs::neighbors::ivf_pq diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_half_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_half_int64_t.cu deleted file mode 100644 index 5874fba6c..000000000 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_half_int64_t.cu +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2024, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * NOTE: this file is generated by generate_ivf_pq.py - * - * Make changes there and run in this directory: - * - * > python generate_ivf_pq.py - * - */ - -#include - -#include "../ivf_pq_search.cuh" - -namespace cuvs::neighbors::ivf_pq { - -#define CUVS_INST_IVF_PQ_SEARCH_FILTER(T, IdxT) \ - void search_with_filtering( \ - raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - cuvs::neighbors::filtering::bitset_filter sample_filter) \ - { \ - cuvs::neighbors::ivf_pq::detail::search_with_filtering( \ - handle, params, index, queries, neighbors, distances, sample_filter); \ - } -CUVS_INST_IVF_PQ_SEARCH_FILTER(half, int64_t); - -#undef CUVS_INST_IVF_PQ_SEARCH_FILTER - -} // namespace cuvs::neighbors::ivf_pq diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_int8_t_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_int8_t_int64_t.cu deleted file mode 100644 index 52b1c68e7..000000000 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_int8_t_int64_t.cu +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2024, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * NOTE: this file is generated by generate_ivf_pq.py - * - * Make changes there and run in this directory: - * - * > python generate_ivf_pq.py - * - */ - -#include - -#include "../ivf_pq_search.cuh" - -namespace cuvs::neighbors::ivf_pq { - -#define CUVS_INST_IVF_PQ_SEARCH_FILTER(T, IdxT) \ - void search_with_filtering( \ - raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - cuvs::neighbors::filtering::bitset_filter sample_filter) \ - { \ - cuvs::neighbors::ivf_pq::detail::search_with_filtering( \ - handle, params, index, queries, neighbors, distances, sample_filter); \ - } -CUVS_INST_IVF_PQ_SEARCH_FILTER(int8_t, int64_t); - -#undef CUVS_INST_IVF_PQ_SEARCH_FILTER - -} // namespace cuvs::neighbors::ivf_pq diff --git a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_uint8_t_int64_t.cu b/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_uint8_t_int64_t.cu deleted file mode 100644 index e3d936155..000000000 --- a/cpp/src/neighbors/ivf_pq/detail/ivf_pq_search_with_filter_uint8_t_int64_t.cu +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2024, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * NOTE: this file is generated by generate_ivf_pq.py - * - * Make changes there and run in this directory: - * - * > python generate_ivf_pq.py - * - */ - -#include - -#include "../ivf_pq_search.cuh" - -namespace cuvs::neighbors::ivf_pq { - -#define CUVS_INST_IVF_PQ_SEARCH_FILTER(T, IdxT) \ - void search_with_filtering( \ - raft::resources const& handle, \ - const cuvs::neighbors::ivf_pq::search_params& params, \ - cuvs::neighbors::ivf_pq::index& index, \ - raft::device_matrix_view queries, \ - raft::device_matrix_view neighbors, \ - raft::device_matrix_view distances, \ - cuvs::neighbors::filtering::bitset_filter sample_filter) \ - { \ - cuvs::neighbors::ivf_pq::detail::search_with_filtering( \ - handle, params, index, queries, neighbors, distances, sample_filter); \ - } -CUVS_INST_IVF_PQ_SEARCH_FILTER(uint8_t, int64_t); - -#undef CUVS_INST_IVF_PQ_SEARCH_FILTER - -} // namespace cuvs::neighbors::ivf_pq diff --git a/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity.cuh b/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity.cuh index 48e2bf222..37612402c 100644 --- a/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity.cuh +++ b/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity.cuh @@ -16,7 +16,7 @@ #pragma once -#include "../sample_filter.cuh" // none_ivf_sample_filter +#include "../sample_filter.cuh" // none_sample_filter #include "ivf_pq_fp_8bit.cuh" // cuvs::neighbors::ivf_pq::detail::fp_8bit #include // cuvs::distance::DistanceType @@ -177,37 +177,37 @@ instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( half, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( half, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( half, half, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, half, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, float, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( float, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA true>, cuvs::neighbors::filtering::ivf_to_sample_filter< - int64_t COMMA cuvs::neighbors::filtering::none_ivf_sample_filter>); + int64_t COMMA cuvs::neighbors::filtering::none_sample_filter>); instantiate_cuvs_neighbors_ivf_pq_detail_compute_similarity_select( half, cuvs::neighbors::ivf_pq::detail::fp_8bit<5u COMMA false>, diff --git a/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity_impl.cuh b/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity_impl.cuh index 5fccbb385..8404ca1f9 100644 --- a/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity_impl.cuh +++ b/cpp/src/neighbors/ivf_pq/ivf_pq_compute_similarity_impl.cuh @@ -17,7 +17,7 @@ #pragma once #include "../ivf_common.cuh" // dummy_block_sort_t -#include "../sample_filter.cuh" // none_ivf_sample_filter +#include "../sample_filter.cuh" // none_sample_filter #include // cuvs::distance::DistanceType #include // codebook_gen #include // matrix::detail::select::warpsort::warp_sort_distributed @@ -247,7 +247,7 @@ __device__ auto ivfpq_compute_score(uint32_t pq_dim, * query_kths keep the current state of the filtering - atomically updated distances to the * k-th closest neighbors for each query [n_queries]. * @param sample_filter - * A filter that selects samples for a given query. Use an instance of none_ivf_sample_filter to + * A filter that selects samples for a given query. Use an instance of none_sample_filter to * provide a green light for every sample. * @param lut_scores * The device pointer for storing the lookup table globally [gridDim.x, pq_dim << PqBits]. @@ -513,7 +513,7 @@ RAFT_KERNEL compute_similarity_kernel(uint32_t dim, // The signature of the kernel defined by a minimal set of template parameters template + typename IvfSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> using compute_similarity_kernel_t = decltype(&compute_similarity_kernel); @@ -522,7 +522,7 @@ template + typename IvfSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> struct compute_similarity_kernel_config { public: static auto get(uint32_t pq_bits, uint32_t k_max) @@ -572,7 +572,7 @@ template + typename IvfSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> auto get_compute_similarity_kernel(uint32_t pq_bits, uint32_t k_max) -> compute_similarity_kernel_t { @@ -617,7 +617,7 @@ struct selected { template + typename IvfSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> void compute_similarity_run(selected s, rmm::cuda_stream_view stream, uint32_t dim, @@ -682,7 +682,7 @@ void compute_similarity_run(selected s, */ template + typename IvfSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> auto compute_similarity_select(const cudaDeviceProp& dev_props, bool manage_local_topk, int locality_hint, diff --git a/cpp/src/neighbors/ivf_pq/ivf_pq_search.cuh b/cpp/src/neighbors/ivf_pq/ivf_pq_search.cuh index 5f812dc4f..e185f18dc 100644 --- a/cpp/src/neighbors/ivf_pq/ivf_pq_search.cuh +++ b/cpp/src/neighbors/ivf_pq/ivf_pq_search.cuh @@ -19,7 +19,7 @@ #include "../../core/nvtx.hpp" #include "../detail/ann_utils.cuh" #include "../ivf_common.cuh" -#include "../sample_filter.cuh" // none_ivf_sample_filter +#include "../sample_filter.cuh" // none_sample_filter #include "ivf_pq_compute_similarity.cuh" #include "ivf_pq_fp_8bit.cuh" @@ -592,7 +592,7 @@ constexpr uint32_t kMaxQueries = 4096; /** See raft::spatial::knn::ivf_pq::search docs */ template + typename IvfSampleFilterT = cuvs::neighbors::filtering::none_sample_filter> inline void search(raft::resources const& handle, const search_params& params, const index& index, @@ -789,14 +789,23 @@ void search(raft::resources const& handle, const index& idx, raft::device_matrix_view queries, raft::device_matrix_view neighbors, - raft::device_matrix_view distances) + raft::device_matrix_view distances, + const cuvs::neighbors::filtering::base_filter& sample_filter_ref) { - search_with_filtering(handle, - params, - idx, - queries, - neighbors, - distances, - cuvs::neighbors::filtering::none_ivf_sample_filter{}); + try { + auto& sample_filter = + dynamic_cast(sample_filter_ref); + return search_with_filtering(handle, params, idx, queries, neighbors, distances, sample_filter); + } catch (const std::bad_cast&) { + } + + try { + auto& sample_filter = + dynamic_cast&>( + sample_filter_ref); + return search_with_filtering(handle, params, idx, queries, neighbors, distances, sample_filter); + } catch (const std::bad_cast&) { + RAFT_FAIL("Unsupported sample filter type"); + } } } // namespace cuvs::neighbors::ivf_pq::detail diff --git a/cpp/src/neighbors/reachability.cu b/cpp/src/neighbors/reachability.cu new file mode 100644 index 000000000..2e366106c --- /dev/null +++ b/cpp/src/neighbors/reachability.cu @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "./detail/reachability.cuh" + +namespace cuvs::neighbors::reachability { + +void mutual_reachability_graph(const raft::resources& handle, + raft::device_matrix_view X, + int min_samples, + raft::device_vector_view indptr, + raft::device_vector_view core_dists, + raft::sparse::COO& out, + cuvs::distance::DistanceType metric, + float alpha) +{ + RAFT_EXPECTS(core_dists.extent(0) == static_cast(X.extent(0)), + "core_dists doesn't have expected size"); + RAFT_EXPECTS(indptr.extent(0) == static_cast(X.extent(0) + 1), + "indptr doesn't have expected size"); + + cuvs::neighbors::detail::reachability::mutual_reachability_graph( + handle, + X.data_handle(), + X.extent(0), + X.extent(1), + metric, + min_samples, + alpha, + indptr.data_handle(), + core_dists.data_handle(), + out); +} +} // namespace cuvs::neighbors::reachability diff --git a/cpp/src/neighbors/refine/refine_device.cuh b/cpp/src/neighbors/refine/refine_device.cuh index 5bf315ae5..6184e540b 100644 --- a/cpp/src/neighbors/refine/refine_device.cuh +++ b/cpp/src/neighbors/refine/refine_device.cuh @@ -126,7 +126,7 @@ void refine_device( 0, chunk_index.data(), cuvs::distance::is_min_close(cuvs::distance::DistanceType(metric)), - cuvs::neighbors::filtering::none_ivf_sample_filter(), + cuvs::neighbors::filtering::none_sample_filter(), neighbors_uint32, distances.data_handle(), grid_dim_x, diff --git a/cpp/src/neighbors/sample_filter.cu b/cpp/src/neighbors/sample_filter.cu index 32a0d3bfb..2da4bea4e 100644 --- a/cpp/src/neighbors/sample_filter.cu +++ b/cpp/src/neighbors/sample_filter.cu @@ -18,6 +18,11 @@ namespace cuvs::neighbors::filtering { +template struct bitmap_filter; +template struct bitmap_filter; +template struct bitmap_filter; +template struct bitmap_filter; + template struct bitset_filter; template struct bitset_filter; template struct bitset_filter; diff --git a/cpp/src/neighbors/sample_filter.cuh b/cpp/src/neighbors/sample_filter.cuh index e49d54920..258116ed3 100644 --- a/cpp/src/neighbors/sample_filter.cuh +++ b/cpp/src/neighbors/sample_filter.cuh @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include @@ -26,7 +27,7 @@ namespace cuvs::neighbors::filtering { /* A filter that filters nothing. This is the default behavior. */ -inline _RAFT_HOST_DEVICE bool none_ivf_sample_filter::operator()( +inline _RAFT_HOST_DEVICE bool none_sample_filter::operator()( // query index const uint32_t query_ix, // the current inverted list index @@ -38,7 +39,7 @@ inline _RAFT_HOST_DEVICE bool none_ivf_sample_filter::operator()( } /* A filter that filters nothing. This is the default behavior. */ -inline _RAFT_HOST_DEVICE bool none_cagra_sample_filter::operator()( +inline _RAFT_HOST_DEVICE bool none_sample_filter::operator()( // query index const uint32_t query_ix, // the index of the current sample @@ -107,4 +108,20 @@ inline _RAFT_HOST_DEVICE bool bitset_filter::operator()( return bitset_view_.test(sample_ix); } +template +bitmap_filter::bitmap_filter( + const cuvs::core::bitmap_view bitmap_for_filtering) + : bitmap_view_{bitmap_for_filtering} +{ +} + +template +inline _RAFT_HOST_DEVICE bool bitmap_filter::operator()( + // query index + const uint32_t query_ix, + // the index of the current sample + const uint32_t sample_ix) const +{ + return bitmap_view_.test(query_ix, sample_ix); +} } // namespace cuvs::neighbors::filtering diff --git a/cpp/src/selection/select_k_float_int32_t.cu b/cpp/src/selection/select_k_float_int32_t.cu new file mode 100644 index 000000000..66672a642 --- /dev/null +++ b/cpp/src/selection/select_k_float_int32_t.cu @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./select_k.cuh" + +instantiate_cuvs_selection_select_k(float, int); diff --git a/cpp/src/stats/detail/trustworthiness_score.cuh b/cpp/src/stats/detail/trustworthiness_score.cuh index f4725a2e8..4d9c3af75 100644 --- a/cpp/src/stats/detail/trustworthiness_score.cuh +++ b/cpp/src/stats/detail/trustworthiness_score.cuh @@ -108,7 +108,7 @@ void run_knn(const raft::resources& h, input_view, raft::make_device_matrix_view(indices, n, n_neighbors), raft::make_device_matrix_view(distances, n, n_neighbors), - std::nullopt); + cuvs::neighbors::filtering::none_sample_filter{}); } /** diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 24489b1bf..58cfc3862 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -71,8 +71,6 @@ function(ConfigureTest) "$<$:${CUVS_CUDA_FLAGS}>" ) - target_compile_definitions(${TEST_NAME} PRIVATE "CUVS_EXPLICIT_INSTANTIATE_ONLY") - if(_CUVS_TEST_NOCUDA) target_compile_definitions(${TEST_NAME} PRIVATE "CUVS_DISABLE_CUDA") endif() diff --git a/cpp/test/cluster/kmeans_mg.cu b/cpp/test/cluster/kmeans_mg.cu new file mode 100644 index 000000000..b9e06b2f1 --- /dev/null +++ b/cpp/test/cluster/kmeans_mg.cu @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +#include + +#define NCCLCHECK(cmd) \ + do { \ + ncclResult_t res = cmd; \ + if (res != ncclSuccess) { \ + printf("Failed, NCCL error %s:%d '%s'\n", __FILE__, __LINE__, ncclGetErrorString(res)); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +namespace cuvs { + +template +struct KmeansInputs { + int n_row; + int n_col; + int n_clusters; + T tol; + bool weighted; +}; + +template +class KmeansTest : public ::testing::TestWithParam> { + protected: + KmeansTest() + : stream(handle.get_stream()), + d_labels(0, stream), + d_labels_ref(0, stream), + d_centroids(0, stream), + d_sample_weight(0, stream) + { + } + + void basicTest() + { + testparams = ::testing::TestWithParam>::GetParam(); + ncclComm_t nccl_comm; + NCCLCHECK(ncclCommInitAll(&nccl_comm, 1, {0})); + raft::comms::build_comms_nccl_only(&handle, nccl_comm, 1, 0); + + int n_samples = testparams.n_row; + int n_features = testparams.n_col; + params.n_clusters = testparams.n_clusters; + params.tol = testparams.tol; + params.n_init = 5; + params.rng_state.seed = 1; + params.oversampling_factor = 1; + + auto stream = handle.get_stream(); + rmm::device_uvector X(n_samples * n_features, stream); + rmm::device_uvector labels(n_samples, stream); + + raft::random::make_blobs(handle, + X.data(), + labels.data(), + n_samples, + n_features, + params.n_clusters, + true, + nullptr, + nullptr, + 1.0, + false, + -10.0f, + 10.0f, + 1234ULL); + + d_labels.resize(n_samples, stream); + d_labels_ref.resize(n_samples, stream); + d_centroids.resize(params.n_clusters * n_features, stream); + + std::optional> d_sw = std::nullopt; + if (testparams.weighted) { + d_sample_weight.resize(n_samples, stream); + thrust::fill(thrust::cuda::par.on(stream), + d_sample_weight.data(), + d_sample_weight.data() + n_samples, + 1); + d_sw = raft::make_device_vector_view(d_sample_weight.data(), n_samples); + } + raft::copy(d_labels_ref.data(), labels.data(), n_samples, stream); + + handle.sync_stream(stream); + + T inertia = 0; + int n_iter = 0; + + auto X_view = raft::make_device_matrix_view(X.data(), n_samples, n_features); + auto centroids_view = + raft::make_device_matrix_view(d_centroids.data(), params.n_clusters, n_features); + + cuvs::cluster::kmeans::fit(handle, + params, + X_view, + d_sw, + centroids_view, + raft::make_host_scalar_view(&inertia), + raft::make_host_scalar_view(&n_iter)); + + cuvs::cluster::kmeans::predict( + handle, + params, + X_view, + d_sw, + d_centroids.data(), + raft::make_device_vector_view(d_labels.data(), n_samples), + true, + raft::make_host_scalar_view(&inertia)); + score = raft::stats::adjusted_rand_index( + d_labels_ref.data(), d_labels.data(), n_samples, raft::resource::get_cuda_stream(handle)); + handle.sync_stream(stream); + + if (score < 0.99) { + std::cout << "Expected: " << raft::arr2Str(d_labels_ref.data(), 25, "d_labels_ref", stream) + << std::endl; + std::cout << "Actual: " << raft::arr2Str(d_labels.data(), 25, "d_labels", stream) + << std::endl; + std::cout << "score = " << score << std::endl; + } + ncclCommDestroy(nccl_comm); + } + + void SetUp() override { basicTest(); } + + protected: + raft::handle_t handle; + cudaStream_t stream; + KmeansInputs testparams; + rmm::device_uvector d_labels; + rmm::device_uvector d_labels_ref; + rmm::device_uvector d_centroids; + rmm::device_uvector d_sample_weight; + double score; + cuvs::cluster::kmeans::params params; +}; + +const std::vector> inputsf2 = {{1000, 32, 5, 0.0001, true}, + {1000, 32, 5, 0.0001, false}, + {1000, 100, 20, 0.0001, true}, + {1000, 100, 20, 0.0001, false}, + {10000, 32, 10, 0.0001, true}, + {10000, 32, 10, 0.0001, false}, + {10000, 100, 50, 0.0001, true}, + {10000, 100, 50, 0.0001, false}}; + +const std::vector> inputsd2 = {{1000, 32, 5, 0.0001, true}, + {1000, 32, 5, 0.0001, false}, + {1000, 100, 20, 0.0001, true}, + {1000, 100, 20, 0.0001, false}, + {10000, 32, 10, 0.0001, true}, + {10000, 32, 10, 0.0001, false}, + {10000, 100, 50, 0.0001, true}, + {10000, 100, 50, 0.0001, false}}; + +typedef KmeansTest KmeansTestF; +TEST_P(KmeansTestF, Result) { ASSERT_TRUE(score >= 0.99); } + +typedef KmeansTest KmeansTestD; +TEST_P(KmeansTestD, Result) { ASSERT_TRUE(score >= 0.99); } + +INSTANTIATE_TEST_CASE_P(KmeansTests, KmeansTestF, ::testing::ValuesIn(inputsf2)); + +INSTANTIATE_TEST_CASE_P(KmeansTests, KmeansTestD, ::testing::ValuesIn(inputsd2)); + +} // end namespace cuvs diff --git a/cpp/test/cluster/linkage.cu b/cpp/test/cluster/linkage.cu index 0f2461fa7..c9f9a50e7 100644 --- a/cpp/test/cluster/linkage.cu +++ b/cpp/test/cluster/linkage.cu @@ -14,15 +14,6 @@ * limitations under the License. */ -// XXX: We allow the instantiation of masked_l2_nn here: -// raft::linkage::FixConnectivitiesRedOp red_op(params.n_row); -// raft::linkage::cross_component_nn( -// handle, out_edges, data.data(), colors.data(), params.n_row, params.n_col, red_op); -// -// TODO: consider adding this to libraft.so or creating an instance in a -// separate translation unit for this test. -#undef CUVS_EXPLICIT_INSTANTIATE_ONLY - #include "../test_utils.cuh" #include diff --git a/cpp/test/neighbors/ann_brute_force.cuh b/cpp/test/neighbors/ann_brute_force.cuh index 461a202f2..c2afa4e8b 100644 --- a/cpp/test/neighbors/ann_brute_force.cuh +++ b/cpp/test/neighbors/ann_brute_force.cuh @@ -96,8 +96,12 @@ class AnnBruteForceTest : public ::testing::TestWithParam( distances_bruteforce_dev.data(), ps.num_queries, ps.k); - brute_force::search( - handle_, idx, search_queries_view, indices_out_view, dists_out_view, std::nullopt); + brute_force::search(handle_, + idx, + search_queries_view, + indices_out_view, + dists_out_view, + cuvs::neighbors::filtering::none_sample_filter{}); raft::resource::sync_stream(handle_); @@ -110,8 +114,12 @@ class AnnBruteForceTest : public ::testing::TestWithParam= offset; + } +}; + /** Xorshift rondem number generator. * * See https://en.wikipedia.org/wiki/Xorshift#xorshift for reference. @@ -663,6 +675,203 @@ class AnnCagraAddNodesTest : public ::testing::TestWithParam { rmm::device_uvector search_queries; }; +template +class AnnCagraFilterTest : public ::testing::TestWithParam { + public: + AnnCagraFilterTest() + : stream_(raft::resource::get_cuda_stream(handle_)), + ps(::testing::TestWithParam::GetParam()), + database(0, stream_), + search_queries(0, stream_) + { + } + + protected: + void testCagra() + { + if (ps.metric == cuvs::distance::DistanceType::InnerProduct && + ps.build_algo == graph_build_algo::NN_DESCENT) + GTEST_SKIP(); + + size_t queries_size = ps.n_queries * ps.k; + std::vector indices_Cagra(queries_size); + std::vector indices_naive(queries_size); + std::vector distances_Cagra(queries_size); + std::vector distances_naive(queries_size); + + { + rmm::device_uvector distances_naive_dev(queries_size, stream_); + rmm::device_uvector indices_naive_dev(queries_size, stream_); + auto* database_filtered_ptr = database.data() + test_cagra_sample_filter::offset * ps.dim; + cuvs::neighbors::naive_knn( + handle_, + distances_naive_dev.data(), + indices_naive_dev.data(), + search_queries.data(), + database_filtered_ptr, + ps.n_queries, + ps.n_rows - test_cagra_sample_filter::offset, + ps.dim, + ps.k, + ps.metric); + raft::linalg::addScalar(indices_naive_dev.data(), + indices_naive_dev.data(), + IdxT(test_cagra_sample_filter::offset), + queries_size, + stream_); + raft::update_host(distances_naive.data(), distances_naive_dev.data(), queries_size, stream_); + raft::update_host(indices_naive.data(), indices_naive_dev.data(), queries_size, stream_); + raft::resource::sync_stream(handle_); + } + + { + rmm::device_uvector distances_dev(queries_size, stream_); + rmm::device_uvector indices_dev(queries_size, stream_); + + { + cagra::index_params index_params; + index_params.metric = ps.metric; // Note: currently ony the cagra::index_params metric is + // not used for knn_graph building. + + switch (ps.build_algo) { + case graph_build_algo::IVF_PQ: + index_params.graph_build_params = + graph_build_params::ivf_pq_params(raft::matrix_extent(ps.n_rows, ps.dim)); + if (ps.ivf_pq_search_refine_ratio) { + std::get( + index_params.graph_build_params) + .refinement_rate = *ps.ivf_pq_search_refine_ratio; + } + break; + case graph_build_algo::NN_DESCENT: { + index_params.graph_build_params = + graph_build_params::nn_descent_params(index_params.intermediate_graph_degree); + break; + } + case graph_build_algo::AUTO: + // do nothing + break; + }; + + index_params.compression = ps.compression; + cagra::search_params search_params; + search_params.algo = ps.algo; + search_params.max_queries = ps.max_queries; + search_params.team_size = ps.team_size; + + // TODO: setting search_params.itopk_size here breaks the filter tests, but is required for + // k>1024 skip these tests until fixed + if (ps.k >= 1024) { GTEST_SKIP(); } + // search_params.itopk_size = ps.itopk_size; + + auto database_view = raft::make_device_matrix_view( + (const DataT*)database.data(), ps.n_rows, ps.dim); + + cagra::index index(handle_); + if (ps.host_dataset) { + auto database_host = raft::make_host_matrix(ps.n_rows, ps.dim); + raft::copy(database_host.data_handle(), database.data(), database.size(), stream_); + auto database_host_view = raft::make_host_matrix_view( + (const DataT*)database_host.data_handle(), ps.n_rows, ps.dim); + index = cagra::build(handle_, index_params, database_host_view); + } else { + index = cagra::build(handle_, index_params, database_view); + } + + if (!ps.include_serialized_dataset) { index.update_dataset(handle_, database_view); } + + auto search_queries_view = raft::make_device_matrix_view( + search_queries.data(), ps.n_queries, ps.dim); + auto indices_out_view = + raft::make_device_matrix_view(indices_dev.data(), ps.n_queries, ps.k); + auto dists_out_view = raft::make_device_matrix_view( + distances_dev.data(), ps.n_queries, ps.k); + auto removed_indices = + raft::make_device_vector(handle_, test_cagra_sample_filter::offset); + thrust::sequence( + raft::resource::get_thrust_policy(handle_), + thrust::device_pointer_cast(removed_indices.data_handle()), + thrust::device_pointer_cast(removed_indices.data_handle() + removed_indices.extent(0))); + raft::resource::sync_stream(handle_); + cuvs::core::bitset removed_indices_bitset( + handle_, removed_indices.view(), ps.n_rows); + auto bitset_filter_obj = + cuvs::neighbors::filtering::bitset_filter(removed_indices_bitset.view()); + cagra::search(handle_, + search_params, + index, + search_queries_view, + indices_out_view, + dists_out_view, + bitset_filter_obj); + raft::update_host(distances_Cagra.data(), distances_dev.data(), queries_size, stream_); + raft::update_host(indices_Cagra.data(), indices_dev.data(), queries_size, stream_); + raft::resource::sync_stream(handle_); + } + + // Test search results for nodes marked as filtered + bool unacceptable_node = false; + for (int q = 0; q < ps.n_queries; q++) { + for (int i = 0; i < ps.k; i++) { + const auto n = indices_Cagra[q * ps.k + i]; + unacceptable_node = unacceptable_node | !test_cagra_sample_filter()(q, n); + } + } + EXPECT_FALSE(unacceptable_node); + + double min_recall = ps.min_recall; + // TODO(mfoerster): re-enable uniquenes test + EXPECT_TRUE(eval_neighbours(indices_naive, + indices_Cagra, + distances_naive, + distances_Cagra, + ps.n_queries, + ps.k, + 0.003, + min_recall, + false)); + if (!ps.compression.has_value()) { + // Don't evaluate distances for CAGRA-Q for now as the error can be somewhat large + EXPECT_TRUE(eval_distances(handle_, + database.data(), + search_queries.data(), + indices_dev.data(), + distances_dev.data(), + ps.n_rows, + ps.dim, + ps.n_queries, + ps.k, + ps.metric, + 1.0e-4)); + } + } + } + + void SetUp() override + { + database.resize(((size_t)ps.n_rows) * ps.dim, stream_); + search_queries.resize(ps.n_queries * ps.dim, stream_); + raft::random::RngState r(1234ULL); + InitDataset(handle_, database.data(), ps.n_rows, ps.dim, ps.metric, r); + InitDataset(handle_, search_queries.data(), ps.n_queries, ps.dim, ps.metric, r); + raft::resource::sync_stream(handle_); + } + + void TearDown() override + { + raft::resource::sync_stream(handle_); + database.resize(0, stream_); + search_queries.resize(0, stream_); + } + + private: + raft::resources handle_; + rmm::cuda_stream_view stream_; + AnnCagraInputs ps; + rmm::device_uvector database; + rmm::device_uvector search_queries; +}; + inline std::vector generate_inputs() { // TODO(tfeher): test MULTI_CTA kernel with search_width > 1 to allow multiple CTA per queries diff --git a/cpp/test/neighbors/ann_cagra/test_float_uint32_t.cu b/cpp/test/neighbors/ann_cagra/test_float_uint32_t.cu index d4e634719..ca188d132 100644 --- a/cpp/test/neighbors/ann_cagra/test_float_uint32_t.cu +++ b/cpp/test/neighbors/ann_cagra/test_float_uint32_t.cu @@ -26,9 +26,13 @@ TEST_P(AnnCagraTestF_U32, AnnCagra) { this->testCagra(); } typedef AnnCagraAddNodesTest AnnCagraAddNodesTestF_U32; TEST_P(AnnCagraAddNodesTestF_U32, AnnCagraAddNodes) { this->testCagra(); } +typedef AnnCagraFilterTest AnnCagraFilterTestF_U32; +TEST_P(AnnCagraFilterTestF_U32, AnnCagra) { this->testCagra(); } + INSTANTIATE_TEST_CASE_P(AnnCagraTest, AnnCagraTestF_U32, ::testing::ValuesIn(inputs)); INSTANTIATE_TEST_CASE_P(AnnCagraAddNodesTest, AnnCagraAddNodesTestF_U32, ::testing::ValuesIn(inputs)); +INSTANTIATE_TEST_CASE_P(AnnCagraFilterTest, AnnCagraFilterTestF_U32, ::testing::ValuesIn(inputs)); } // namespace cuvs::neighbors::cagra diff --git a/cpp/test/neighbors/ann_cagra/test_int8_t_uint32_t.cu b/cpp/test/neighbors/ann_cagra/test_int8_t_uint32_t.cu index 72bdee428..4aa03afd5 100644 --- a/cpp/test/neighbors/ann_cagra/test_int8_t_uint32_t.cu +++ b/cpp/test/neighbors/ann_cagra/test_int8_t_uint32_t.cu @@ -24,10 +24,13 @@ typedef AnnCagraTest AnnCagraTestI8_U32; TEST_P(AnnCagraTestI8_U32, AnnCagra) { this->testCagra(); } typedef AnnCagraAddNodesTest AnnCagraAddNodesTestI8_U32; TEST_P(AnnCagraAddNodesTestI8_U32, AnnCagra) { this->testCagra(); } +typedef AnnCagraFilterTest AnnCagraFilterTestI8_U32; +TEST_P(AnnCagraFilterTestI8_U32, AnnCagra) { this->testCagra(); } INSTANTIATE_TEST_CASE_P(AnnCagraTest, AnnCagraTestI8_U32, ::testing::ValuesIn(inputs)); INSTANTIATE_TEST_CASE_P(AnnCagraAddNodesTest, AnnCagraAddNodesTestI8_U32, ::testing::ValuesIn(inputs)); +INSTANTIATE_TEST_CASE_P(AnnCagraFilterTest, AnnCagraFilterTestI8_U32, ::testing::ValuesIn(inputs)); } // namespace cuvs::neighbors::cagra diff --git a/cpp/test/neighbors/ann_cagra/test_uint8_t_uint32_t.cu b/cpp/test/neighbors/ann_cagra/test_uint8_t_uint32_t.cu index b68bfa574..b8e2a6b77 100644 --- a/cpp/test/neighbors/ann_cagra/test_uint8_t_uint32_t.cu +++ b/cpp/test/neighbors/ann_cagra/test_uint8_t_uint32_t.cu @@ -24,10 +24,13 @@ typedef AnnCagraTest AnnCagraTestU8_U32; TEST_P(AnnCagraTestU8_U32, AnnCagra) { this->testCagra(); } typedef AnnCagraAddNodesTest AnnCagraAddNodesTestU8_U32; TEST_P(AnnCagraAddNodesTestU8_U32, AnnCagra) { this->testCagra(); } +typedef AnnCagraFilterTest AnnCagraFilterTestU8_U32; +TEST_P(AnnCagraFilterTestU8_U32, AnnCagra) { this->testCagra(); } INSTANTIATE_TEST_CASE_P(AnnCagraTest, AnnCagraTestU8_U32, ::testing::ValuesIn(inputs)); INSTANTIATE_TEST_CASE_P(AnnCagraAddNodesTest, AnnCagraAddNodesTestU8_U32, ::testing::ValuesIn(inputs)); +INSTANTIATE_TEST_CASE_P(AnnCagraFilterTest, AnnCagraFilterTestU8_U32, ::testing::ValuesIn(inputs)); } // namespace cuvs::neighbors::cagra diff --git a/cpp/test/neighbors/ann_ivf_flat.cuh b/cpp/test/neighbors/ann_ivf_flat.cuh index 17ec84097..8cc46b2f7 100644 --- a/cpp/test/neighbors/ann_ivf_flat.cuh +++ b/cpp/test/neighbors/ann_ivf_flat.cuh @@ -304,7 +304,7 @@ class AnnIVFFlatTest : public ::testing::TestWithParam> { ivf::resize_list(handle_, lists[label], list_device_spec, list_size, 0); } - idx.recompute_internal_state(handle_); + ivf_flat::helpers::recompute_internal_state(handle_, &idx); using interleaved_group = raft::Pow2; @@ -466,18 +466,19 @@ class AnnIVFFlatTest : public ::testing::TestWithParam> { cuvs::core::bitset removed_indices_bitset( handle_, removed_indices.view(), ps.num_db_vecs); + auto bitset_filter_obj = + cuvs::neighbors::filtering::bitset_filter(removed_indices_bitset.view()); // Search with the filter auto search_queries_view = raft::make_device_matrix_view( search_queries.data(), ps.num_queries, ps.dim); - ivf_flat::search_with_filtering( - handle_, - search_params, - index, - search_queries_view, - indices_ivfflat_dev.view(), - distances_ivfflat_dev.view(), - cuvs::neighbors::filtering::bitset_filter(removed_indices_bitset.view())); + ivf_flat::search(handle_, + search_params, + index, + search_queries_view, + indices_ivfflat_dev.view(), + distances_ivfflat_dev.view(), + bitset_filter_obj); raft::update_host( distances_ivfflat.data(), distances_ivfflat_dev.data_handle(), queries_size, stream_); diff --git a/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu b/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu index 0ce168f5e..6a4a34516 100644 --- a/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu +++ b/cpp/test/neighbors/ann_ivf_flat/test_float_int64_t.cu @@ -21,7 +21,12 @@ namespace cuvs::neighbors::ivf_flat { typedef AnnIVFFlatTest AnnIVFFlatTestF_float; -TEST_P(AnnIVFFlatTestF_float, AnnIVFFlat) { this->testIVFFlat(); } +TEST_P(AnnIVFFlatTestF_float, AnnIVFFlat) +{ + this->testIVFFlat(); + this->testPacker(); + this->testFilter(); +} INSTANTIATE_TEST_CASE_P(AnnIVFFlatTest, AnnIVFFlatTestF_float, ::testing::ValuesIn(inputs)); diff --git a/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu b/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu index 15935fd88..5335b1656 100644 --- a/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu +++ b/cpp/test/neighbors/ann_ivf_flat/test_int8_t_int64_t.cu @@ -21,7 +21,12 @@ namespace cuvs::neighbors::ivf_flat { typedef AnnIVFFlatTest AnnIVFFlatTestF_int8; -TEST_P(AnnIVFFlatTestF_int8, AnnIVFFlat) { this->testIVFFlat(); } +TEST_P(AnnIVFFlatTestF_int8, AnnIVFFlat) +{ + this->testIVFFlat(); + this->testPacker(); + this->testFilter(); +} INSTANTIATE_TEST_CASE_P(AnnIVFFlatTest, AnnIVFFlatTestF_int8, ::testing::ValuesIn(inputs)); diff --git a/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu b/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu index 42a8dab2e..e5573bcbc 100644 --- a/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu +++ b/cpp/test/neighbors/ann_ivf_flat/test_uint8_t_int64_t.cu @@ -21,7 +21,12 @@ namespace cuvs::neighbors::ivf_flat { typedef AnnIVFFlatTest AnnIVFFlatTestF_uint8; -TEST_P(AnnIVFFlatTestF_uint8, AnnIVFFlat) { this->testIVFFlat(); } +TEST_P(AnnIVFFlatTestF_uint8, AnnIVFFlat) +{ + this->testIVFFlat(); + this->testPacker(); + this->testFilter(); +} INSTANTIATE_TEST_CASE_P(AnnIVFFlatTest, AnnIVFFlatTestF_uint8, ::testing::ValuesIn(inputs)); diff --git a/cpp/test/neighbors/ann_ivf_pq.cuh b/cpp/test/neighbors/ann_ivf_pq.cuh index e6d8efc93..f02568b74 100644 --- a/cpp/test/neighbors/ann_ivf_pq.cuh +++ b/cpp/test/neighbors/ann_ivf_pq.cuh @@ -18,10 +18,10 @@ #include "../test_utils.cuh" #include "ann_utils.cuh" #include "naive_knn.cuh" +#include #include #include -#include #include #include #include @@ -629,14 +629,10 @@ class ivf_pq_filter_test : public ::testing::TestWithParam { cuvs::core::bitset removed_indices_bitset( handle_, removed_indices.view(), ps.num_db_vecs); - cuvs::neighbors::ivf_pq::search_with_filtering( - handle_, - ps.search_params, - index, - query_view, - inds_view, - dists_view, - cuvs::neighbors::filtering::bitset_filter(removed_indices_bitset.view())); + auto bitset_filter_obj = + cuvs::neighbors::filtering::bitset_filter(removed_indices_bitset.view()); + cuvs::neighbors::ivf_pq::search( + handle_, ps.search_params, index, query_view, inds_view, dists_view, bitset_filter_obj); raft::update_host(distances_ivf_pq.data(), distances_ivf_pq_dev.data(), queries_size, stream_); raft::update_host(indices_ivf_pq.data(), indices_ivf_pq_dev.data(), queries_size, stream_); diff --git a/cpp/test/neighbors/brute_force.cu b/cpp/test/neighbors/brute_force.cu index f1a05e045..8c354baa9 100644 --- a/cpp/test/neighbors/brute_force.cu +++ b/cpp/test/neighbors/brute_force.cu @@ -93,7 +93,8 @@ class KNNTest : public ::testing::TestWithParam> { auto metric = cuvs::distance::DistanceType::L2Unexpanded; auto idx = cuvs::neighbors::brute_force::build(handle, index, metric); - cuvs::neighbors::brute_force::search(handle, idx, search, indices, distances, std::nullopt); + cuvs::neighbors::brute_force::search( + handle, idx, search, indices, distances, cuvs::neighbors::filtering::none_sample_filter{}); build_actual_output<<>>( actual_labels_.data(), rows_, k_, search_labels_.data(), indices_.data()); @@ -401,7 +402,7 @@ class RandomBruteForceKNNTest : public ::testing::TestWithParam search_queries.data(), params_.num_queries, params_.dim), indices, distances, - std::nullopt); + cuvs::neighbors::filtering::none_sample_filter{}); } else { auto idx = cuvs::neighbors::brute_force::build( handle_, @@ -417,7 +418,7 @@ class RandomBruteForceKNNTest : public ::testing::TestWithParam search_queries.data(), params_.num_queries, params_.dim), indices, distances, - std::nullopt); + cuvs::neighbors::filtering::none_sample_filter{}); } ASSERT_TRUE(cuvs::neighbors::devArrMatchKnnPair(ref_indices_.data(), diff --git a/cpp/test/neighbors/brute_force_prefiltered.cu b/cpp/test/neighbors/brute_force_prefiltered.cu index ae9111ea1..12b1c529e 100644 --- a/cpp/test/neighbors/brute_force_prefiltered.cu +++ b/cpp/test/neighbors/brute_force_prefiltered.cu @@ -502,7 +502,12 @@ class PrefilteredBruteForceTest auto out_idx = raft::make_device_matrix_view( out_idx_d.data(), params.n_queries, params.top_k); - brute_force::search(handle, dataset, queries, out_idx, out_val, std::make_optional(filter)); + brute_force::search(handle, + dataset, + queries, + out_idx, + out_val, + cuvs::neighbors::filtering::bitmap_filter(filter)); std::vector out_val_h(params.n_queries * params.top_k, std::numeric_limits::infinity()); diff --git a/docs/source/developer_guide.md b/docs/source/developer_guide.md index 516819b1c..e54336852 100644 --- a/docs/source/developer_guide.md +++ b/docs/source/developer_guide.md @@ -292,97 +292,6 @@ Sometimes, we need to temporarily change the log pattern (eg: for reporting deci 4. Before creating a new primitive, check to see if one exists already. If one exists but the API isn't flexible enough to include your use-case, consider first refactoring the existing primitive. If that is not possible without an extreme number of changes, consider how the public API could be made more flexible. If the new primitive is different enough from all existing primitives, consider whether an existing public API could invoke the new primitive as an option or argument. If the new primitive is different enough from what exists already, add a header for the new public API function to the appropriate subdirectory and namespace. -## Header organization of expensive function templates - -RAFT is a heavily templated library. Several core functions are expensive to compile and we want to prevent duplicate compilation of this functionality. To limit build time, RAFT provides a precompiled library (libraft.so) where expensive function templates are instantiated for the most commonly used template parameters. To prevent (1) accidental instantiation of these templates and (2) unnecessary dependency on the internals of these templates, we use a split header structure and define macros to control template instantiation. This section describes the macros and header structure. - -**Macros.** We define the macros `RAFT_COMPILED` and `RAFT_EXPLICIT_INSTANTIATE_ONLY`. The `RAFT_COMPILED` macro is defined by `CMake` when compiling code that (1) is part of `libraft.so` or (2) is linked with `libraft.so`. It indicates that a precompiled `libraft.so` is present at runtime. - -The `RAFT_EXPLICIT_INSTANTIATE_ONLY` macro is defined by `CMake` during compilation of `libraft.so` itself. When defined, it indicates that implicit instantiations of expensive function templates are forbidden (they result in a compiler error). In the RAFT project, we additionally define this macro during compilation of the tests and benchmarks. - -Below, we summarize which combinations of `RAFT_COMPILED` and `RAFT_EXPLICIT_INSTANTIATE_ONLY` are used in practice and what the effect of the combination is. - -| RAFT_COMPILED | RAFT_EXPLICIT_INSTANTIATE_ONLY | Which targets | -|---------------|--------------------------------|------------------------------------------------------------------------------------------------------| -| defined | defined | `raft::compiled`, RAFT tests, RAFT benchmarks | -| defined | | Downstream libraries depending on `libraft` like cuML, cuGraph. | -| | | Downstream libraries depending on `libraft-headers` like cugraph-ops. | - - -| RAFT_COMPILED | RAFT_EXPLICIT_INSTANTIATE_ONLY | Effect | -|---------------|--------------------------------|-------------------------------------------------------------------------------------------------------| -| defined | defined | Templates are precompiled. Compiler error on accidental instantiation of expensive function template. | -| defined | | Templates are precompiled. Implicit instantiation allowed. | -| | | Nothing precompiled. Implicit instantiation allowed. | -| | defined | Avoid this: nothing precompiled. Compiler error on any instantiation of expensive function template. | - - - -**Header organization.** Any header file that defines an expensive function template (say `expensive.cuh`) should be split in three parts: `expensive.cuh`, `expensive-inl.cuh`, and `expensive-ext.cuh`. The file `expensive-inl.cuh` ("inl" for "inline") contains the template definitions, i.e., the actual code. The file `expensive.cuh` includes one or both of the other two files, depending on the values of the `RAFT_COMPILED` and `RAFT_EXPLICIT_INSTANTIATE_ONLY` macros. The file `expensive-ext.cuh` contains `extern template` instantiations. In addition, if `RAFT_EXPLICIT_INSTANTIATE_ONLY` is set, it contains template definitions to ensure that a compiler error is raised in case of accidental instantiation. - -The dispatching by `expensive.cuh` is performed as follows: -``` c++ -#ifndef RAFT_EXPLICIT_INSTANTIATE_ONLY -// If implicit instantiation is allowed, include template definitions. -#include "expensive-inl.cuh" -#endif - -#ifdef RAFT_COMPILED -// Include extern template instantiations when RAFT is compiled. -#include "expensive-ext.cuh" -#endif -``` - -The file `expensive-inl.cuh` is unchanged: -``` c++ -namespace raft { -template -void expensive(T arg) { - // .. function body -} -} // namespace raft -``` - -The file `expensive-ext.cuh` contains the following: -``` c++ -#include // RAFT_EXPLICIT - -#ifdef RAFT_EXPLICIT_INSTANTIATE_ONLY -namespace raft { -// (1) define templates to raise an error in case of accidental instantiation -template void expensive(T arg) RAFT_EXPLICIT; -} // namespace raft -#endif //RAFT_EXPLICIT_INSTANTIATE_ONLY - -// (2) Provide extern template instantiations. -extern template void raft::expensive(int); -extern template void raft::expensive(float); -``` - -This header has two responsibilities: (1) define templates to raise an error in case of accidental instantiation and (2) provide `extern template` instantiations. -First, if `RAFT_EXPLICIT_INSTANTIATE_ONLY` is set, `expensive` is defined. This is done for two reasons: (1) to give a definition, because the definition in `expensive-inl.cuh` was skipped and (2) to indicate that the template should be explicitly instantiated by taging it with the `RAFT_EXPLICIT` macro. This macro defines the function body, and it ensures that an informative error message is generated when an implicit instantiation erroneously occurs. Finally, the `extern template` instantiations are listed. - -To actually generate the code for the template instances, the file `src/expensive.cu` contains the following. Note that the only difference between the extern template instantiations in `expensive-ext.cuh` and these lines are the removal of the word `extern`: - -``` c++ -#include - -template void raft::expensive(int); -template void raft::expensive(float); -``` - -**Design considerations**: - -1. In the `-ext.cuh` header, do not include implementation headers. Only include function parameter types and types that are used to instantiate the templates. If a primitive takes custom parameter types, define them in a separate header called `_types.hpp`. (see [Common Design Considerations](https://github.com/rapidsai/raft/blob/7b065aff81a0b1976e2a9e2f3de6690361a1111b/docs/source/developer_guide.md#common-design-considerations)). - -2. Keep docstrings in the `-inl.cuh` header, as it is closer to the code. Remove docstrings from template definitions in the `-ext.cuh` header. Make sure to explicitly include public APIs in the RAFT API docs. That is, add `#include ` to the docs in `docs/source/cpp_api/expensive.rst` (instead of `#include `). - -3. The order of inclusion in `expensive.cuh` is extremely important. If `RAFT_EXPLICIT_INSTANTIATE_ONLY` is not defined, but `RAFT_COMPILED` is defined, then we must include the template definitions before the `extern template` instantiations. - -4. If a header file defines multiple expensive templates, it can be that one of them is not instantiated. In this case, **do define** the template with `RAFT_EXPLICIT` in the `-ext` header. This way, when the template is instantiated, the developer gets a helpful error message instead of a confusing "function not found". - -This header structure was proposed in [issue #1416](https://github.com/rapidsai/raft/issues/1416), which contains more background on the motivation of this structure and the mechanics of C++ template instantiation. - ## Testing It's important for RAFT to maintain a high test coverage of the public APIs in order to minimize the potential for downstream projects to encounter unexpected build or runtime behavior as a result of changes. diff --git a/notebooks/cuvs_hpo_example.ipynb b/notebooks/cuvs_hpo_example.ipynb new file mode 100644 index 000000000..d8b11a82c --- /dev/null +++ b/notebooks/cuvs_hpo_example.ipynb @@ -0,0 +1,7181 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6c47d37e-fd75-4604-a787-ccccf392e9d3", + "metadata": {}, + "source": [ + "## Background \n", + "This notebook showcases how to leverage Optuna for hyperparameter tuning, specifically for the n_lists and n_probes parameters. We will demonstrate how to optimize these parameters using Optuna's Bayesian optimization capabilities. \n", + "\n", + "Note: This notebook has been tested on Sagemaker Studio with an instance type of ml.g5.12xlarge." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d35868b-93ad-43b4-ae15-59e75aa89e3c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "#Install Required Packages\n", + "%mamba install -c conda-forge -c nvidia -c rapidsai-nightly cuvs optuna -y\n", + "%pip install cupy" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "9ebbb352-260c-4078-8589-e0538338a275", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import cupy as cp\n", + "import numpy as np\n", + "from cuvs.neighbors import ivf_flat\n", + "import urllib.request\n", + "import numpy as np\n", + "import time\n", + "import optuna\n", + "from utils import calc_recall\n", + "from optuna.visualization import plot_optimization_history\n", + "import math\n", + "import os\n" + ] + }, + { + "cell_type": "markdown", + "id": "933b9ab7-4c81-4124-9890-dbda75eb9fd9", + "metadata": {}, + "source": [ + "## Download wiki-all dataset\n" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "4f819371-019b-4378-8a47-389de1689c05", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import tarfile\n", + "home_dir = os.path.expanduser(\"~/\")\n", + "#wiki-all datasets are in tar format\n", + "def download_files(url, file):\n", + " if os.path.exists(home_dir + \"/\" + file):\n", + " print(\"tar file is already downloaded\")\n", + " else:\n", + " urllib.request.urlretrieve(url, home_dir + \"/\" + file)\n", + " # Open the .tar file\n", + " with tarfile.open(home_dir + \"/\" + file, 'r') as tar:\n", + " filename = file.split(\".\")[0]\n", + " if os.path.exists(home_dir + \"/\" + filename + \"/\"):\n", + " print(\"Files already extracted\")\n", + " return home_dir + \"/\" + filename + \"/\"\n", + " # Extract all contents into the specified directory\n", + " extract_path=home_dir + \"/\" +file.split(\".\")[0]\n", + " tar.extractall(extract_path)\n", + " return extract_path" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "26197a6e-f5c6-443f-9c1d-95105cc7038d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tar file is already downloaded\n", + "Files already extracted\n" + ] + } + ], + "source": [ + "extracted_path=download_files('https://data.rapids.ai/raft/datasets/wiki_all_1M/wiki_all_1M.tar', 'wiki_all_1M.tar')" + ] + }, + { + "cell_type": "markdown", + "id": "1b0ead9f-417b-4305-8277-92ca34352ed1", + "metadata": {}, + "source": [ + "## Dataset Preparation: Load fbin, ibin files \n", + "This example utilizes the Wiki-1M dataset, a collection of four binary files containing: \n", + "\n", + "Database vectors: Used for index building and searching.\n", + "Query vectors: Used for index building and searching.\n", + "Ground truth neighbors: Associated with a particular distance, used for evaluation.\n", + "Distances: Associated with a particular distance, used for evaluation.\n", + "The file suffixes denote the data type of vectors stored in the file: \n", + "\n", + ".fbin: float32\n", + ".ibin: int\n", + "For more information on the Wiki-1M dataset, please refer to the [RAPIDS documentation](https://docs.rapids.ai/api/raft/nightly/ann_benchmarks_dataset)\n", + "." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "2c6a0772-bb7c-43b7-aecc-8864140b0353", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def read_data(file_path, dtype):\n", + " with open(file_path, \"rb\") as f:\n", + " rows,cols = np.fromfile(f, count=2, dtype= np.int32)\n", + " d = np.fromfile(f,count=rows*cols,dtype=dtype).reshape(rows, cols)\n", + " return cp.asarray(d)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "b46775d5-37c2-4be2-8c89-019ee5474f6b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "vectors= read_data(extracted_path + \"/base.1M.fbin\",np.float32)\n", + "queries = read_data(extracted_path + \"/queries.fbin\",np.float32)\n", + "gt_neighbors = read_data(extracted_path + \"/groundtruth.1M.neighbors.ibin\",np.int32)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "fae0ab44-a9da-4443-817c-325364ff0ee7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "#Get the dataset size of database vectors\n", + "dataset_size = vectors.shape[0]\n", + "dim = vectors.shape[1]" + ] + }, + { + "cell_type": "markdown", + "id": "d35b88c4-a005-4bef-90bd-da33fee63f8b", + "metadata": {}, + "source": [ + "## Visualization\n", + "\n", + "Generates and displays Pareto front plots for a given Optuna study object." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "d321bd84-b6f7-4bdb-889f-578a8512cd8c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def visualization(study_obj):\n", + " \"\"\"\n", + " This function creates two Pareto front plots to visualize trade-offs between different \n", + " optimization objectives. The plots help in understanding the balance between competing \n", + " objectives in the optimization process.\n", + "\n", + " Args:\n", + " study_obj (optuna.Study): The Optuna study object containing the optimization results.\n", + "\n", + " The function produces the following plots:\n", + " 1. **Figure 1**: A Pareto front plot showing the trade-off between `build_time_in_secs` \n", + " and `recall`. It visualizes how the optimization process balances the build time \n", + " and recall score.\n", + " 2. **Figure 2**: A Pareto front plot showing the trade-off between `latency_in_ms` \n", + " and `recall`. This plot illustrates the relationship between latency and recall score.\n", + " \n", + " \"\"\"\n", + " \n", + " fig1 = optuna.visualization.plot_pareto_front(\n", + " study_obj,\n", + " targets=lambda t: (t.values[0], t.values[2]),\n", + " target_names=[\"build_time_in_secs\", \"recall\"],\n", + " )\n", + " fig1.show()\n", + "\n", + " fig2 = optuna.visualization.plot_pareto_front(\n", + " study_obj,\n", + " targets=lambda t: (t.values[1], t.values[2]),\n", + " target_names=[\"latency_in_ms\", \"recall\"],\n", + " )\n", + " fig2.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "449d2a28-4286-451f-89bc-10ce535aa134", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def print_target_instance_summary(target_instance):\n", + " print(f\"\\tnumber: {target_instance.number}\")\n", + " print(f\"\\tparams: {target_instance.params}\")\n", + " print(f\"\\tvalues: {target_instance.values}\")\n", + " \n", + "def print_best_trial_values(optuna_study):\n", + " \"\"\"\n", + " Prints details about the trials on the Pareto front of an Optuna study.\n", + "\n", + " This function analyzes the best trials from an Optuna study, which are typically \n", + " those with the most favorable trade-offs among multiple objectives. It prints \n", + " information on three specific metrics:\n", + "\n", + " 1. The number of trials on the Pareto front.\n", + " 2. The trial with the highest accuracy among the best trials.\n", + " 3. The trial with the lowest build time among the best trials.\n", + " 4. The trial with the lowest latency among the best trials.\n", + "\n", + " Parameters:\n", + " optuna_study (optuna.study.Study): An Optuna study object that contains information\n", + " about the trials and their respective metrics.\n", + "\n", + " The function assumes that each trial has three metrics recorded in the `values` list:\n", + " - `values[0]`: Build time\n", + " - `values[1]`: latency\n", + " - `values[2]`: Accuracy\n", + " \n", + " \"\"\"\n", + " print(f\"Number of trials on the Pareto front: {len(optuna_study.best_trials)}\")\n", + "\n", + " trial_with_lowest_build_time = min(optuna_study.best_trials, key=lambda t: t.values[0])\n", + " print(f\"Trial with lowest build time in secs: \")\n", + " print_target_instance_summary(trial_with_lowest_build_time)\n", + "\n", + " trial_with_lowest_latency = min(optuna_study.best_trials, key=lambda t: t.values[1])\n", + " print(f\"Trial with lowest latency in ms: \")\n", + " print_target_instance_summary(trial_with_lowest_latency)\n", + " \n", + " trial_with_highest_accuracy = max(optuna_study.best_trials, key=lambda t: t.values[2])\n", + " print(f\"Trial with highest accuracy: \")\n", + " print_target_instance_summary(trial_with_highest_accuracy)" + ] + }, + { + "cell_type": "markdown", + "id": "f08a88d8-b805-4ed3-8521-f9cb0c2bd28b", + "metadata": {}, + "source": [ + "## Hyperparameter Optimization (HPO) for CUVS Libraries\n", + "\n", + "An Optuna trial object used to suggest values for the hyperparameters of various CUVS libraries (such as ivf_flat, ivf_pq, and cagra).\n", + "\n", + "The multi-objective function returns a tuple of three float values, each rounded to four decimal places:\n", + "\n", + "build_time_in_secs: Time taken to build the index, measured in seconds.\n", + "latency_in_ms: Average search latency, measured in milliseconds. Calculated as the total search time divided by the number of queries.\n", + "recall: Recall metric, indicating the proportion of relevant neighbors retrieved.\n" + ] + }, + { + "cell_type": "markdown", + "id": "47c43a77-f3d0-4920-a45d-9c4566d5b01f", + "metadata": {}, + "source": [ + "## ivf_flat HPO example" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "id": "bfdde156-f624-495d-aa52-bc03e9113120", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def multi_objective_ivf_flat(trial):\n", + " \"\"\"\n", + " Optimizes the parameters for an Inverted File Index (IVF) Flat index in a multi-objective setting.\n", + "\n", + " \"\"\"\n", + " # Suggest an integer for the number of lists\n", + " n_lists = trial.suggest_int(\"n_lists\", 10, dataset_size*0.1)\n", + " # Suggest an integer for the number of probes\n", + " n_probes = trial.suggest_int(\"n_probes\",n_lists*0.01 , n_lists*0.1)\n", + " build_params = ivf_flat.IndexParams(\n", + " n_lists=n_lists,\n", + " )\n", + " start_build_time = time.time()\n", + " index = ivf_flat.build(build_params, vectors)\n", + " build_time_in_secs = time.time() - start_build_time\n", + "\n", + " # Configure search parameters\n", + " search_params = ivf_flat.SearchParams(n_probes=n_probes)\n", + " # Perform the search\n", + " start_search_time = time.time()\n", + " distances, indices = ivf_flat.search(search_params, index, queries, k=10)\n", + " search_time = time.time() - start_search_time\n", + " \n", + " latency_in_ms = (search_time * 1000)/queries.shape[0]\n", + " \n", + " found_distances, found_indices = cp.asnumpy(distances), cp.asnumpy(indices)\n", + " recall = calc_recall(found_indices, gt_neighbors)\n", + " return round(build_time_in_secs,4), round(latency_in_ms,4), round(recall,4)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "23944214-d72f-486a-bb9c-22a6c8b9516e", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:24:19,149] A new study created in memory with name: no-name-ab0e88bd-1516-4eb2-a60d-01ae30ad08c4\n", + "[I 2024-08-19 16:24:51,762] Trial 0 finished with values: [23.1406, 0.3441, 0.9994] and parameters: {'n_lists': 48570, 'n_probes': 2511}. \n", + "[I 2024-08-19 16:25:28,379] Trial 1 finished with values: [24.232, 0.6336, 0.9999] and parameters: {'n_lists': 51021, 'n_probes': 4945}. \n", + "[I 2024-08-19 16:25:58,230] Trial 2 finished with values: [22.675, 0.1172, 0.9949] and parameters: {'n_lists': 47692, 'n_probes': 760}. \n", + "[I 2024-08-19 16:26:54,339] Trial 3 finished with values: [42.3469, 0.7272, 0.9997] and parameters: {'n_lists': 91882, 'n_probes': 6687}. \n", + "[I 2024-08-19 16:27:29,832] Trial 4 finished with values: [27.2722, 0.2141, 0.9985] and parameters: {'n_lists': 57879, 'n_probes': 1657}. \n", + "[I 2024-08-19 16:28:22,050] Trial 5 finished with values: [42.7912, 0.2936, 0.999] and parameters: {'n_lists': 92899, 'n_probes': 2804}. \n", + "[I 2024-08-19 16:28:54,048] Trial 6 finished with values: [24.9842, 0.0949, 0.9935] and parameters: {'n_lists': 52962, 'n_probes': 668}. \n", + "[I 2024-08-19 16:29:32,878] Trial 7 finished with values: [26.306, 0.6545, 0.9999] and parameters: {'n_lists': 55683, 'n_probes': 5439}. \n", + "[I 2024-08-19 16:30:02,789] Trial 8 finished with values: [20.7545, 0.3232, 0.9992] and parameters: {'n_lists': 43405, 'n_probes': 2093}. \n", + "[I 2024-08-19 16:30:31,208] Trial 9 finished with values: [17.2627, 0.5324, 0.9998] and parameters: {'n_lists': 35266, 'n_probes': 3028}. \n" + ] + } + ], + "source": [ + "ivf_flat_study = optuna.create_study(directions=['minimize', 'minimize', 'maximize'])\n", + "ivf_flat_study.optimize(multi_objective_ivf_flat, n_trials=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "id": "3c6c30d1-13e1-4f5f-99c2-6f2c6c1d389b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of trials on the Pareto front: 8\n", + "Trial with lowest build time in secs: \n", + "\tnumber: 9\n", + "\tparams: {'n_lists': 35266, 'n_probes': 3028}\n", + "\tvalues: [17.2627, 0.5324, 0.9998]\n", + "Trial with lowest latency in ms: \n", + "\tnumber: 6\n", + "\tparams: {'n_lists': 52962, 'n_probes': 668}\n", + "\tvalues: [24.9842, 0.0949, 0.9935]\n", + "Trial with highest accuracy: \n", + "\tnumber: 1\n", + "\tparams: {'n_lists': 51021, 'n_probes': 4945}\n", + "\tvalues: [24.232, 0.6336, 0.9999]\n" + ] + } + ], + "source": [ + "print_best_trial_values(ivf_flat_study)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "1ed2ec62-0b0b-43e5-abbc-9bd7d06e5ebd", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{text}Trial", + "marker": { + "color": [ + 3, + 7 + ], + "colorbar": { + "title": { + "text": "Trial" + } + }, + "colorscale": [ + [ + 0, + "rgb(247,251,255)" + ], + [ + 0.125, + "rgb(222,235,247)" + ], + [ + 0.25, + "rgb(198,219,239)" + ], + [ + 0.375, + "rgb(158,202,225)" + ], + [ + 0.5, + "rgb(107,174,214)" + ], + [ + 0.625, + "rgb(66,146,198)" + ], + [ + 0.75, + "rgb(33,113,181)" + ], + [ + 0.875, + "rgb(8,81,156)" + ], + [ + 1, + "rgb(8,48,107)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 3,
\"values\": [
42.3469,
0.7272,
0.9997
],
\"params\": {
\"n_lists\": 91882,
\"n_probes\": 6687
}
}", + "{
\"number\": 7,
\"values\": [
26.306,
0.6545,
0.9999
],
\"params\": {
\"n_lists\": 55683,
\"n_probes\": 5439
}
}" + ], + "type": "scatter", + "x": [ + 42.3469, + 26.306 + ], + "y": [ + 0.9997, + 0.9999 + ] + }, + { + "hovertemplate": "%{text}Best Trial", + "marker": { + "color": [ + 0, + 1, + 2, + 4, + 5, + 6, + 8, + 9 + ], + "colorbar": { + "title": { + "text": "Best Trial" + }, + "x": 1.1, + "xpad": 40, + "y": 0.5 + }, + "colorscale": [ + [ + 0, + "rgb(255,245,240)" + ], + [ + 0.125, + "rgb(254,224,210)" + ], + [ + 0.25, + "rgb(252,187,161)" + ], + [ + 0.375, + "rgb(252,146,114)" + ], + [ + 0.5, + "rgb(251,106,74)" + ], + [ + 0.625, + "rgb(239,59,44)" + ], + [ + 0.75, + "rgb(203,24,29)" + ], + [ + 0.875, + "rgb(165,15,21)" + ], + [ + 1, + "rgb(103,0,13)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 0,
\"values\": [
23.1406,
0.3441,
0.9994
],
\"params\": {
\"n_lists\": 48570,
\"n_probes\": 2511
}
}", + "{
\"number\": 1,
\"values\": [
24.232,
0.6336,
0.9999
],
\"params\": {
\"n_lists\": 51021,
\"n_probes\": 4945
}
}", + "{
\"number\": 2,
\"values\": [
22.675,
0.1172,
0.9949
],
\"params\": {
\"n_lists\": 47692,
\"n_probes\": 760
}
}", + "{
\"number\": 4,
\"values\": [
27.2722,
0.2141,
0.9985
],
\"params\": {
\"n_lists\": 57879,
\"n_probes\": 1657
}
}", + "{
\"number\": 5,
\"values\": [
42.7912,
0.2936,
0.999
],
\"params\": {
\"n_lists\": 92899,
\"n_probes\": 2804
}
}", + "{
\"number\": 6,
\"values\": [
24.9842,
0.0949,
0.9935
],
\"params\": {
\"n_lists\": 52962,
\"n_probes\": 668
}
}", + "{
\"number\": 8,
\"values\": [
20.7545,
0.3232,
0.9992
],
\"params\": {
\"n_lists\": 43405,
\"n_probes\": 2093
}
}", + "{
\"number\": 9,
\"values\": [
17.2627,
0.5324,
0.9998
],
\"params\": {
\"n_lists\": 35266,
\"n_probes\": 3028
}
}" + ], + "type": "scatter", + "x": [ + 23.1406, + 24.232, + 22.675, + 27.2722, + 42.7912, + 24.9842, + 20.7545, + 17.2627 + ], + "y": [ + 0.9994, + 0.9999, + 0.9949, + 0.9985, + 0.999, + 0.9935, + 0.9992, + 0.9998 + ] + } + ], + "layout": { + "autosize": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Pareto-front Plot" + }, + "xaxis": { + "autorange": true, + "range": [ + 15.745768536042302, + 44.3081314639577 + ], + "title": { + "text": "build_time_in_secs" + }, + "type": "linear" + }, + "yaxis": { + "autorange": true, + "range": [ + 0.9929718446601943, + 1.0004281553398058 + ], + "title": { + "text": "recall" + }, + "type": "linear" + } + } + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA40AAAFoCAYAAADzQh4hAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQmcTtX/xz9jzJjFmBkzw9jGlrVkKdkjkjXSQvbSQrSRIlr4CckWRUKUiFASJYpItojsIfs+YxtjVrP8/+fOPM/s5t659547d+ZzX69e033uOed77vt7H7znnHuOS1JSUhJ4kAAJkAAJkAAJkAAJkAAJkAAJkEAWBFwojXwuSIAESIAESIAESIAESIAESIAEsiNAaeSzQQIkQAIkQAIkQAIkQAIkQAIkkC0BSiMfDhIgARIgARIgARIgARIgARIgAUojnwESIAESIAESIAESIAESIAESIAHtBDjSqJ0Za5AACZAACZAACZAACZAACZBAgSFAaSwwqeaNkgAJkAAJkAAJkAAJkAAJkIB2ApRG7cxYgwRIgARIgARIgARIgARIgAQKDAFKY4FJNW+UBEiABEiABEiABEiABEiABLQToDRqZ8YaJEACJEACJEACJEACJEACJFBgCFAaC0yqeaMkQAIkQAIkQAIkQAIkQAIkoJ0ApVE7M9YgARIgARIgARIgARIgARIggQJDgNJYYFLNGyUBEiABEiABEiABEiABEiAB7QQojdqZsQYJkAAJkAAJkAAJkAAJkAAJFBgClMYCk2reKAmQAAmQAAmQAAmQAAmQAAloJ0Bp1M6MNUiABEiABEiABEiABEiABEigwBCgNBaYVPNGSYAESIAESIAESIAESIAESEA7AUqjdmasQQIkQAIkQAIkQAIkQAIkQAIFhgClscCkmjdKAiRAAiRAAiRAAiRAAiRAAtoJUBq1M2MNEiABEiABEiABEiABEiABEigwBCiNBSbVvFESIAESIAESIAESIAESIAES0E6A0qidGWuQAAmQAAmQAAmQAAmQAAmQQIEhQGksMKnmjZIACZAACZAACZAACZAACZCAdgKURu3MWIMESIAESIAESIAESIAESIAECgwBSmOBSTVvlARIgARIgARIgARIgARIgAS0E6A0amfGGiRAAiRAAiRAAiRAAiRAAiRQYAhQGgtMqnmjJEACJEACJEACJEACJEACJKCdAKVROzPWIAESIAESIAESIAESIAESIIECQ4DSWGBSzRslARIgARIgARIgARIgARIgAe0EKI3ambEGCZAACZAACZAACZAACZAA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Ux1bZHsvQhpzypmonHF6qvhMLb875UA1ZBYkARIgARIgARIgARIgARJQCNhaGtXmUEwhPXshVJG40iUDFUnUc4h9HkOvXIe/rw8C/Isp0zozHmIk72LoVWXRHSFnaQ8hNaI/Af6+ynuEMg/RrzPnL6Nw4cIQwipWmNV7CKEW0ljU21NhYvahlt+dcmB2H9k+CZAACZAACZAACZAACeQXAgVCGvNLsngfJEACJEACJEACJEACJEACJCCbAKVRNnHGIwESIAESIAESIAESIAESIAEbEaA02ihZ7CoJkAAJkAAJkAAJkAAJkAAJyCZAaZRNnPFIgARIgARIgARIgARIgARIwEYEKI02Sha7SgIkQAIkQAIkQAIkQAIkQAKyCVAaZRNnPBIgARIgARIgARIgARIgARKwEQFKo42Sxa6SAAmQAAmQAAmQAAmQAAmQgGwClEbZxBmPBEiABEiABEiABEiABEiABGxEgNJoo2SxqyRAAiRAAiRAAiRAAiRAAiQgmwClUTZxxiMBEiABEiABEiABEiABEiABGxGgNNooWewqCZAACZAACZAACZAACZAACcgmQGmUTZzxSIAESIAESIAESIAESIAESMBGBCiNNkoWu0oCJEACJEACJEACJEACJEACsglQGmUTZzwSIAESIAESIAESIAESIAESsBEBSqONksWukgAJkAAJkAAJkAAJkAAJkIBsApRG2cQZjwRIgARIgARIgARIgARIgARsRIDSaKNksaskQAIkQAIkQAIkQAIkQAIkIJsApVE2ccYjARIgARIgARIgARIgARIgARsRoDTaKFnsKgmQAAmQAAmQAAmQAAmQAAnIJkBplE2c8UiABEiABEiABEiABEiABEjARgQojTZKFrtKAiRAAiRAAiRAAiRAAiRAArIJUBplE2c8EiABEiABEiABEiABEiABErARAUqjjZLFrpIACZAACZAACZAACZAACZCAbAKURtnEGY8ESIAESIAESIAESIAESIAEbESA0mijZLGrJEACJEACJEACJEACJEACJCCbAKVRNnHGIwESIAESIAESIAESIAESIAEbEaA02ihZ7CoJkAAJkAAJkAAJkAAJkAAJyCZAaZRNnPFIgARIgARIgARIgARIgARIwEYEKI02Sha7SgIkQAIkQAIkQAIkQAIkQAKyCVAaZRNnPBIgARIgARIgARIgARIgARKwEQFKo42Sxa6SAAmQAAmQAAmQAAmQAAmQgGwClEbZxBmPBEiABEiABEiABEiABEiABGxEgNJoo2SxqyRAAiRAAiRAAiRAAiRAAiQgmwClUTZxxiMBEiABEiABEiABEiABEiABGxGgNNooWewqCZAACZAACZAACZAACZAACcgmQGmUTZzxSIAESIAESIAESIAESIAESMBGBCiNNkoWu0oCJEACJEACJEACJEACJEACsglQGmUTZzwSIAESIAESIAESIAESIAESsBEBSqONksWukgAJkAAJkAAJkAAJkAAJkIBsApRG2cQZjwRIgARIgARIgARIgARIgARsRIDSaKNksaskQAIkQAIkQAIkQAIkQAIkIJsApVE2ccYjARIgARIgARIgARIgARIgARsRoDTaKFnsKgmQAAmQAAmQAAmQAAmQAAnIJkBplE2c8UiABEiABEiABEiABEiABEjARgQojTZKFrtKAiRAAiRAAiRAAiRAAiRAArIJUBplE2c8EiABEiABEiABEiABEiABErARAUqjjZLFrpIACZAACZAACZAACZAACZCAbAKURtnEGY8ESIAESIAESIAESIAESIAEbESA0mijZLGrJEACJEACJEACJEACJEACJCCbAKVRJ/ELV6N1tsDqagm4AAgO8MRFMleLzLJyQX4euBERi9sJSZb1gYFzJuDr7Yb4hCRExsTnXJglLCPg6e4KjyKuuB4RZ1kfGDhnAoUKuaCEbxFcuh6Tc2GWMJRA6QBPQ9tjYyRAApkJUBp1PhWURp0ANVSnNGqAZXFRSqPFCVAZntKoEpTFxSiNFidAZXhKo0pQJhSjNJoAlU2SQAYClEadjwSlUSdADdUpjRpgWVyU0mhxAlSGpzSqBGVxMUqjxQlQGZ7SqBKUCcUojSZAZZMkQGk09hmgNBrL806tURrlsdYbidKol6Cc+pRGOZz1RqE06iUopz6lUQ7nrKJQGq1jz8gFhwBHGnXmmtKoE6CG6pRGDbAsLkpptDgBKsNTGlWCsrgYpdHiBKgMT2lUCcqEYpRGE6CySRLIQIDSqPKRSEpKQkJiIgq7uqarQWlUCdCAYpRGAyBKaoLSKAm0zjCURp0AJVWnNEoCrTMMpVEnQB3VKY064LEqCagkQGlUCWrVuq2YOmcZNiybSmlUyczoYpRGo4ma1x6l0Ty2RrZMaTSS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SqPOgFk9CZAACeRGICEx2SIcX5/0dy4acSUmJcv3GuZ1idlX0zsolfRRzPwlp6TmGSrqs3ZWV0kfGEMCJEACJEACJKCOAKVRHS9GkwAJkAAJkAAJkAAJkAAJkIBLEaA0ulS6OVgSIAESIAESIAESIAESIAESUEeA0qiOF6NJgARIgARIgARIgARIgARIwKUIUBpdKt0cLAmQAAmQAAmQAAmQAAmQAAmoI0BpVMeL0SRAAiRAAiRAAiRAAiRAAiTgUgQojS6Vbg6WBEiABEiABEiABEiABEiABNQRoDSq48VoEiABEiABEiABEiABEiABEnApApRGl0o3B0sCJEACJEACJEACJEACJEAC6ghQGtXxYjQJkAAJkAAJkAAJkAAJkAAJuBQBSqNLpZuDJQESIAESIAESIAESIAESIAF1BCiN6ngxmgRIgARIgARIgARIgARIgARcigCl0aXSzcGSAAmQAAmQAAmQAAmQAAmQgDoClEZ1vBhNAiRAAiRAAiRAAiRAAiRAAi5FgNLoUunmYEmABEiABEiABEiABEiABEhAHQFKozpejCYBEiABEiABEiABEiABEiABlyJAaXSpdHOwJEACJEACJEACJEACJEACJKCOAKVRHS9GkwAJkAAJkAAJkAAJkAAJkIBLEaA0ulS6OVgSIAESIAESIAESIAESIAESUEeA0qiOF6NJgARIgARIgARIgARIgARIwKUIUBpdKt0cLAmQAAmQAAmQAAmQAAmQAAmoI0BpVMeL0SRAAiRAAiRAAiRAAiRAAiTgUgQojS6Vbg6WBEiABEiABEiABEiABEiABNQRoDSq48VoEiABEiABEiABEiABEiABEnApApRGl0o3B0sCJEACJEACJEACJEACJEAC6ghQGtXxYjQJkAAJkAAJkAAJkAAJkAAJuBQBSqNLpZuDJQESIAESIAESIAESIAESIAF1BCiN6ngxmgRIgARIgARIgARIgARIgARcigCl0aXSzcGSAAmQAAmQAAmQAAmQAAmQgDoClEZ1vBhNAiRAAiRAAiRAAiRAAiRAAi5FgNLoUunmYEmABEiABEiABEiABEiABEhAHQFKozpejCYBEiABEiABEiABEiABEiABlyJAaXSpdHOwJEACJEACJEACJEACJEACJKCOAKVRHS9GkwAJkAAJkAAJkAAJkAAJkIBLEaA0ulS6OVgSIAESIAESIAESIAESIAESUEeA0qiOF6NJgARIgARIgARIgARIgARIwKUIUBpdKt0cLAmQAAmQAAmQAAmQAAmQAAmoI0BpVMeL0SRAAiRAAiRAAiRAAiRAAiTgUgT+H4huzZfMCUNAAAAAAElFTkSuQmCC", + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{text}Trial", + "marker": { + "color": [ + 3, + 7 + ], + "colorbar": { + "title": { + "text": "Trial" + } + }, + "colorscale": [ + [ + 0, + "rgb(247,251,255)" + ], + [ + 0.125, + "rgb(222,235,247)" + ], + [ + 0.25, + "rgb(198,219,239)" + ], + [ + 0.375, + "rgb(158,202,225)" + ], + [ + 0.5, + "rgb(107,174,214)" + ], + [ + 0.625, + "rgb(66,146,198)" + ], + [ + 0.75, + "rgb(33,113,181)" + ], + [ + 0.875, + "rgb(8,81,156)" + ], + [ + 1, + "rgb(8,48,107)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 3,
\"values\": [
42.3469,
0.7272,
0.9997
],
\"params\": {
\"n_lists\": 91882,
\"n_probes\": 6687
}
}", + "{
\"number\": 7,
\"values\": [
26.306,
0.6545,
0.9999
],
\"params\": {
\"n_lists\": 55683,
\"n_probes\": 5439
}
}" + ], + "type": "scatter", + "x": [ + 0.7272, + 0.6545 + ], + "y": [ + 0.9997, + 0.9999 + ] + }, + { + "hovertemplate": "%{text}Best Trial", + "marker": { + "color": [ + 0, + 1, + 2, + 4, + 5, + 6, + 8, + 9 + ], + "colorbar": { + "title": { + "text": "Best Trial" + }, + "x": 1.1, + "xpad": 40, + "y": 0.5 + }, + "colorscale": [ + [ + 0, + "rgb(255,245,240)" + ], + [ + 0.125, + "rgb(254,224,210)" + ], + [ + 0.25, + "rgb(252,187,161)" + ], + [ + 0.375, + "rgb(252,146,114)" + ], + [ + 0.5, + "rgb(251,106,74)" + ], + [ + 0.625, + "rgb(239,59,44)" + ], + [ + 0.75, + "rgb(203,24,29)" + ], + [ + 0.875, + "rgb(165,15,21)" + ], + [ + 1, + "rgb(103,0,13)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 0,
\"values\": [
23.1406,
0.3441,
0.9994
],
\"params\": {
\"n_lists\": 48570,
\"n_probes\": 2511
}
}", + "{
\"number\": 1,
\"values\": [
24.232,
0.6336,
0.9999
],
\"params\": {
\"n_lists\": 51021,
\"n_probes\": 4945
}
}", + "{
\"number\": 2,
\"values\": [
22.675,
0.1172,
0.9949
],
\"params\": {
\"n_lists\": 47692,
\"n_probes\": 760
}
}", + "{
\"number\": 4,
\"values\": [
27.2722,
0.2141,
0.9985
],
\"params\": {
\"n_lists\": 57879,
\"n_probes\": 1657
}
}", + "{
\"number\": 5,
\"values\": [
42.7912,
0.2936,
0.999
],
\"params\": {
\"n_lists\": 92899,
\"n_probes\": 2804
}
}", + "{
\"number\": 6,
\"values\": [
24.9842,
0.0949,
0.9935
],
\"params\": {
\"n_lists\": 52962,
\"n_probes\": 668
}
}", + "{
\"number\": 8,
\"values\": [
20.7545,
0.3232,
0.9992
],
\"params\": {
\"n_lists\": 43405,
\"n_probes\": 2093
}
}", + "{
\"number\": 9,
\"values\": [
17.2627,
0.5324,
0.9998
],
\"params\": {
\"n_lists\": 35266,
\"n_probes\": 3028
}
}" + ], + "type": "scatter", + "x": [ + 0.3441, + 0.6336, + 0.1172, + 0.2141, + 0.2936, + 0.0949, + 0.3232, + 0.5324 + ], + "y": [ + 0.9994, + 0.9999, + 0.9949, + 0.9985, + 0.999, + 0.9935, + 0.9992, + 0.9998 + ] + } + ], + "layout": { + "autosize": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Pareto-front Plot" + }, + "xaxis": { + "autorange": true, + "range": [ + 0.057328040634567215, + 0.7647719593654327 + ], + "title": { + "text": "latency_in_ms" + }, + "type": "linear" + }, + "yaxis": { + "autorange": true, + "range": [ + 0.9929718446601943, + 1.0004281553398058 + ], + "title": { + "text": "recall" + }, + "type": "linear" + } + } + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA40AAAFoCAYAAADzQh4hAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQd4FUXbhp+TAqmkh9BC7yKCUhVBkI4gFpBmQREEUUEUFFH4EBBpgoIoSBMEqSIognSkCVKkSe8tJCEhIb38/2yyJ52ck905ySbPXpdX2LM7szP3u4m5804xJScnJ4MHCZAACZAACZAACZAACZAACZAACWRDwERp5HtBAiRAAiRAAiRAAiRAAiRAAiSQEwFKI98NEiABEiABEiABEiABEiABEiCBHAlQGvlykAAJkAAJkAAJkAAJkAAJkAAJUBr5DpAACZAACZAACZAACZAACZAACVhPgJlG65mxBAmQAAmQAAmQAAmQAAmQAAkUGQKUxiITanaUBEiABEiABEiABEiABEiABKwnQGm0nhlLkAAJkAAJkAAJkAAJkAAJkECRIUBpLDKhZkdJgARIgARIgARIgARIgARIwHoClEbrmbEECZAACZAACZAACZAACZAACRQZApTGIhNqdpQESIAESIAESIAESIAESIAErCdAabSeGUuQAAmQAAmQAAmQAAmQAAmQQJEhQGksMqFmR0mABEiABEiABEiABEiABEjAegKURuuZsQQJkAAJkAAJkAAJkAAJkAAJFBkClMYiE2p2lARIgARIgARIgARIgARIgASsJ0BptJ4ZS5AACZAACZAACZAACZAACZBAkSFAaSwyoWZHSYAESIAESIAESIAESIAESMB6ApRG65mxBAmQAAmQAAmQAAmQAAmQAAkUGQKUxiITanaUBEiABEiABEiABEiABEiABKwnQGm0nhlLkAAJkAAJkAAJkAAJkAAJkECRIUBpLDKhZkdJgARIgARIgARIgARIgARIwHoClEbrmbEECZAACZAACZAACZAACZAACRQZApTGIhNqdpQESIAESIAESIAESIAESIAErCdAabSeGUuQAAmQAAmQAAmQAAmQAAmQQJEhQGksMqFmR0mABEiABEiABEiABEiABEjAegKURuuZsQQJkAAJkAAJkAAJkAAJkAAJFBkClMYiE2p2lARIgARIgARIgARIgARIgASsJ0BptJ4ZS5AACZAACZAACZAACZAACZBAkSFAaSwyoWZHSYAESIAESIAESIAESIAESMB6ApRG65mxBAmQAAmQAAmQAAmQAAmQAAkU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ucVMFM48PFV8Zim/B8XAYsi8kQRIgARIgARIgARIgARIQCFgaGm0NIZiCOnVG0GKxJUu6atIopZD7PMYFHwXXh7u8PEqoQzrzHyITN7NoBBl0R0hZ+kPITWiPT5eHso8Qlseol1Xrt+Gg4MDhLCKFWa1HkKohTS6uTorTGQflvJ7UAxkt5H1kwAJkAAJkAAJkAAJkEBhIVAkpLGwBIv9IAESIAESIAESIAESIAESIAFbE6A02po4n0cCJEACJEACJEACJEACJEACBiJAaTRQsNhUEiABEiABEiABEiABEiABErA1AUqjrYnzeSRAAiRAAiRAAiRAAiRAAiRgIAKURgMFi00lARIgARIgARIgARIgARIgAVsToDTamjifRwIkQAIkQAIkQAIkQAIkQAIGIkBpNFCw2FQSIAESIAESIAESIAESIAESsDUBSqOtifN5JEACJEACJEACJEACJEACJGAgApRGAwWLTSUBEiABEiABEiABEiABEiABWxOgNNqaOJ9HAiRAAiRAAiRAAiRAAiRAAgYiQGk0ULDYVBIgARIgARIgARIgARIgARKwNQFKo62J83kkQAIkQAIkQAIkQAIkQAIkYCAClEYDBYtNJQESIAESIAESIAESIAESIAFbE6A02po4n0cCJEACJEACJEACJEACJEACBiJAaTRQsNhUEiABEiABEiABEiABEiABErA1AUqjrYnzeSRAAiRAAiRAAiRAAiRAAiRgIAKURgMFi00lARIgARIgARIgARIgARIgAVsToDTamjifRwIkQAIkQAIkQAIkQAIkQAIGIkBpNFCw2FQSIAESIAESIAESIAESIAESsDUBSqOtifN5JEACJEACJEACJEACJEACJGAgApRGAwWLTSUBEiABEiABEiABEiABEiABWxOgNNqaOJ9HAiRAAiRAAiRAAiRAAiRAAgYiQGk0ULDYVBIgARIgARIgARIgARIgARKwNQFKo62J83kkQAIkQAIkQAIkQAIkQAIkYCAClEYDBYtNJQESIAESIAESIAESIAESIAFbE6A02po4n0cCJEACJEACJEACJEACJEACBiJAaTRQsNhUEiABEiABEiABEiABEiABErA1AUqjrYnzeSRAAiRAAiRAAiRAAiRAAiRgIAKURgMFi00lARIgARIgARIgARIgARIgAVsToDTamjifRwIkQAIkQAIk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", + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualization(ivf_flat_study)" + ] + }, + { + "cell_type": "markdown", + "id": "8cca41e4-b697-4a0e-98f8-f3b682bf552f", + "metadata": {}, + "source": [ + "## ivf_pq HPO example" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "dc4f0458-52e8-4663-82d7-afa33d68f242", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from cuvs.neighbors import ivf_pq,refine" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "id": "81c092df-fd11-472c-aa90-85b31f66d494", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def multi_objective_ivf_pq(trial):\n", + " \"\"\"\n", + " Optimizes hyperparameters for Inverted File Product Quantization (IVF-PQ) in a multi-objective setting..\n", + "\n", + " \"\"\"\n", + " # Suggest values for build parameters\n", + " pq_dim = trial.suggest_int(\"pq_dim\", dim*0.25, dim, step=2)\n", + " n_lists = 1000\n", + "\n", + " # Suggest an integer for the number of probes\n", + " n_probes = trial.suggest_int(\"n_probes\",n_lists*0.01 , n_lists*0.1)\n", + "\n", + " build_params = ivf_pq.IndexParams(\n", + " n_lists=n_lists,\n", + " pq_dim=pq_dim,\n", + " )\n", + "\n", + " start_build_time = time.time()\n", + " index = ivf_pq.build(build_params, vectors)\n", + " build_time_in_secs = time.time() - start_build_time\n", + "\n", + " # Configure search parameters\n", + " search_params = ivf_pq.SearchParams(n_probes=n_probes)\n", + "\n", + " # perform search and refine to increase recall/accuracy\n", + " start_search_time = time.time()\n", + " distances, indices = ivf_pq.search(search_params, index, queries, k=10)\n", + " search_time = time.time() - start_search_time\n", + "\n", + " latency_in_ms = (search_time * 1000)/queries.shape[0]\n", + "\n", + " found_distances, found_indices = cp.asnumpy(distances), cp.asnumpy(indices)\n", + " recall = calc_recall(found_indices, gt_neighbors)\n", + "\n", + " return round(build_time_in_secs,4), round(latency_in_ms, 4), round(recall,4)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "6d805aee-d93c-4423-84f7-0c0a38f036fe", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:41:17,476] A new study created in memory with name: no-name-f823ce34-eccc-4c5a-a90d-1963f50a3a89\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 662\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:41:39,686] Trial 0 finished with values: [9.7626, 0.6995, 0.9562] and parameters: {'pq_dim': 662, 'n_probes': 79}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 210\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:41:51,544] Trial 1 finished with values: [4.9705, 0.1443, 0.8414] and parameters: {'pq_dim': 210, 'n_probes': 57}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 678\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:42:15,891] Trial 2 finished with values: [10.0147, 0.8856, 0.9581] and parameters: {'pq_dim': 678, 'n_probes': 98}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 432\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:42:34,553] Trial 3 finished with values: [9.3991, 0.3831, 0.9497] and parameters: {'pq_dim': 432, 'n_probes': 70}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 410\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:42:52,606] Trial 4 finished with values: [9.018, 0.3598, 0.9471] and parameters: {'pq_dim': 410, 'n_probes': 69}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 242\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:43:04,224] Trial 5 finished with values: [4.2757, 0.1919, 0.8495] and parameters: {'pq_dim': 242, 'n_probes': 66}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 688\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:43:24,543] Trial 6 finished with values: [10.1002, 0.4773, 0.9518] and parameters: {'pq_dim': 688, 'n_probes': 50}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 388\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:43:42,703] Trial 7 finished with values: [8.6237, 0.411, 0.9475] and parameters: {'pq_dim': 388, 'n_probes': 85}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 286\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:43:57,057] Trial 8 finished with values: [6.0929, 0.2856, 0.8991] and parameters: {'pq_dim': 286, 'n_probes': 79}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 284\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:44:09,228] Trial 9 finished with values: [6.0429, 0.0726, 0.8775] and parameters: {'pq_dim': 284, 'n_probes': 19}. \n" + ] + } + ], + "source": [ + "ivf_pq_study = optuna.create_study(directions=['minimize', 'minimize', 'maximize'])\n", + "ivf_pq_study.optimize(multi_objective_ivf_pq, n_trials=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "id": "b7479455-9859-4e17-94df-19834360a0e5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of trials on the Pareto front: 10\n", + "Trial with lowest build time in secs: \n", + "\tnumber: 5\n", + "\tparams: {'pq_dim': 242, 'n_probes': 66}\n", + "\tvalues: [4.2757, 0.1919, 0.8495]\n", + "Trial with lowest latency in ms: \n", + "\tnumber: 9\n", + "\tparams: {'pq_dim': 284, 'n_probes': 19}\n", + "\tvalues: [6.0429, 0.0726, 0.8775]\n", + "Trial with highest accuracy: \n", + "\tnumber: 2\n", + "\tparams: {'pq_dim': 678, 'n_probes': 98}\n", + "\tvalues: [10.0147, 0.8856, 0.9581]\n" + ] + } + ], + "source": [ + "print_best_trial_values(ivf_pq_study)" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "15ce945b-0c46-4f55-bcbb-3b836c80db2c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{text}Trial", + "marker": { + "color": [], + "colorbar": { + "title": { + "text": "Trial" + } + }, + "colorscale": [ + [ + 0, + "rgb(247,251,255)" + ], + [ + 0.125, + "rgb(222,235,247)" + ], + [ + 0.25, + "rgb(198,219,239)" + ], + [ + 0.375, + "rgb(158,202,225)" + ], + [ + 0.5, + "rgb(107,174,214)" + ], + [ + 0.625, + "rgb(66,146,198)" + ], + [ + 0.75, + "rgb(33,113,181)" + ], + [ + 0.875, + "rgb(8,81,156)" + ], + [ + 1, + "rgb(8,48,107)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [], + "type": "scatter", + "x": [], + "y": [] + }, + { + "hovertemplate": "%{text}Best Trial", + "marker": { + "color": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "colorbar": { + "title": { + "text": "Best Trial" + }, + "x": 1.1, + "xpad": 40, + "y": 0.5 + }, + "colorscale": [ + [ + 0, + "rgb(255,245,240)" + ], + [ + 0.125, + "rgb(254,224,210)" + ], + [ + 0.25, + "rgb(252,187,161)" + ], + [ + 0.375, + "rgb(252,146,114)" + ], + [ + 0.5, + "rgb(251,106,74)" + ], + [ + 0.625, + "rgb(239,59,44)" + ], + [ + 0.75, + "rgb(203,24,29)" + ], + [ + 0.875, + "rgb(165,15,21)" + ], + [ + 1, + "rgb(103,0,13)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 0,
\"values\": [
9.7626,
0.6995,
0.9562
],
\"params\": {
\"pq_dim\": 662,
\"n_probes\": 79
}
}", + "{
\"number\": 1,
\"values\": [
4.9705,
0.1443,
0.8414
],
\"params\": {
\"pq_dim\": 210,
\"n_probes\": 57
}
}", + "{
\"number\": 2,
\"values\": [
10.0147,
0.8856,
0.9581
],
\"params\": {
\"pq_dim\": 678,
\"n_probes\": 98
}
}", + "{
\"number\": 3,
\"values\": [
9.3991,
0.3831,
0.9497
],
\"params\": {
\"pq_dim\": 432,
\"n_probes\": 70
}
}", + "{
\"number\": 4,
\"values\": [
9.018,
0.3598,
0.9471
],
\"params\": {
\"pq_dim\": 410,
\"n_probes\": 69
}
}", + "{
\"number\": 5,
\"values\": [
4.2757,
0.1919,
0.8495
],
\"params\": {
\"pq_dim\": 242,
\"n_probes\": 66
}
}", + "{
\"number\": 6,
\"values\": [
10.1002,
0.4773,
0.9518
],
\"params\": {
\"pq_dim\": 688,
\"n_probes\": 50
}
}", + "{
\"number\": 7,
\"values\": [
8.6237,
0.411,
0.9475
],
\"params\": {
\"pq_dim\": 388,
\"n_probes\": 85
}
}", + "{
\"number\": 8,
\"values\": [
6.0929,
0.2856,
0.8991
],
\"params\": {
\"pq_dim\": 286,
\"n_probes\": 79
}
}", + "{
\"number\": 9,
\"values\": [
6.0429,
0.0726,
0.8775
],
\"params\": {
\"pq_dim\": 284,
\"n_probes\": 19
}
}" + ], + "type": "scatter", + "x": [ + 9.7626, + 4.9705, + 10.0147, + 9.3991, + 9.018, + 4.2757, + 10.1002, + 8.6237, + 6.0929, + 6.0429 + ], + "y": [ + 0.9562, + 0.8414, + 0.9581, + 0.9497, + 0.9471, + 0.8495, + 0.9518, + 0.9475, + 0.8991, + 0.8775 + ] + } + ], + "layout": { + "autosize": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Pareto-front Plot" + }, + "xaxis": { + "autorange": true, + "range": [ + 3.929601822989145, + 10.446298177010853 + ], + "title": { + "text": "build_time_in_secs" + }, + "type": "linear" + }, + "yaxis": { + "autorange": true, + "range": [ + 0.8317694174757282, + 0.9677305825242718 + ], + "title": { + "text": "recall" + }, + "type": "linear" + } + } + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA40AAAFoCAYAAADzQh4hAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQmcjdX/xz939pUZxr7vSkKl0ioqKSV+Rdb2kqKIlFKklD2KlFZGSkqiRJESkiKEhGTf1xkzY9b//5xxr5kxY+6d556ve5/5PK9Xr+nO85zzfc77ezDvOZsjKysrC7xIgARIgARIgARIgARIgARIgARIIB8CDkoj+wUJkAAJkAAJkAAJkAAJkAAJkEBBBCiN7BskQAIkQAIkQAIkQAIkQAIkQAIFEqA0snOQAAmQAAmQAAmQAAmQAAmQAAlQGtkHSIAESIAESIAESIAESIAESIAEPCfAkUbPmbEECZAACZAACZAACZAACZAACRQbApTGYpNqNpQESIAESIAESIAESIAESIAEPCdAafScGUuQAAmQAAmQAAmQAAmQAAmQQLEhQGksNqlmQ0mABEiABEiABEiABEiABEjAcwKURs+ZsQQJkAAJkAAJkAAJkAAJkAAJFBsClMZik2o2lARIgARIgARIgARIgARIgAQ8J0Bp9JwZS5AACZAACZAACZAACZAACZBAsSFAaSw2qWZDSYAESIAESIAESIAESIAESMBzApRGz5mxBAmQAAmQAAmQAAmQAAmQAAkUGwKUxmKTajaUBEiABEiABEiABEiABEiABDwnQGn0nBlLkAAJkAAJkAAJkAAJkAAJkECxIUBpLDapZkNJgARIgARIgARIgARIgARIwHMClEbPmbEECZAACZAACZAACZAACZAACRQbApTGYpNqNpQESIAESIAESIAESIAESIAEPCdAafScGUuQAAmQAAmQAAmQAAmQAAmQQLEhQGksNqlmQ0mABEiABEiABEiABEiABEjAcwKURs+ZsQQJkAAJkAAJkAAJkAAJkAAJFBsClMZik2o2lARIgARIgARIgARIgARIgAQ8J0Bp9JwZS5AACZAACZAACZAACZAACZBAsSFAaSw2qWZDSYAESIAESIAESIAESIAESMBzApRGz5mxBAmQAAmQAAmQAAmQAAmQAAkUGwKUxmKTajaUBEiABEiABEiABEiABEiABDwnQGn0nBlLkAAJkAAJkAAJkAAJkAAJkECxIUBpLDapZkNJgARIgARIgARIgARIgARIwHMClEbPmbEECZAACZAACZAACZAACZAACRQb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p7MXaQFW1+B+9+HuNs0LbIuSxsJypgrnnZ6qvucuv3PlwG3IfJAESIAESIAESIAESIAESEAT8GtpdDeHagrpzj0HtMRVLBenJdHKpc55PHDoKGJLRqN0bAk9rTPvpUby9h44rDfdUXKW81JSo96ndGxJvY5Q8lLvtWP3fgQFBUEJq9ph1uqlhFpJY1RkuGZi+nKX37lyYPodWT8JkAAJkAAJkAAJkAAJ2IVAsZBGuySL7SABEiABEiABEiABEiABEiABaQKURmnijEcCJEACJEACJEACJEACJEACfkSA0uhHyeKrkgAJkAAJkAAJkAAJkAAJkIA0AUqjNHHGIwESIAESIAESIAESIAESIAE/IkBp9KNk8VVJgARIgARIgARIgARIgARIQJoApVGaOOORAAmQAAmQAAmQAAmQAAmQgB8RoDT6UbL4qiRAAiRAAiRAAiRAAiRAAiQgTYDSKE2c8UiABEiABEiABEiABEiABEjAjwhQGv0oWXxVEiABEiABEiABEiABEiABEpAmQGmUJs54JEACJEACJEACJEACJEACJOBHBCiNfpQsvioJkAAJkAAJkAAJkAAJkAAJSBOgNEoTZzwSIAESIAESIAESIAESIAES8CMClEY/ShZflQRIgARIgARIgARIgARIgASkCVAapYkzHgmQAAmQAAmQAAmQAAmQAAn4EQFKox8li69KAiRAAiRAAiRAAiRAAiRAAtIEKI3SxBmPBEiABEiABEiABEiABEiABPyIAKXRj5LFVyUBEiABEiABEiABEiABEiABaQKURmnijEcCJEACJEACJEACJEACJEACfkSA0uhHyeKrkgAJkAAJkAAJkAAJkAAJkIA0AUqjNHHGIwESIAESIAESIAESIAESIAE/IkBp9KNk8VVJgARIgARIgARIgARIgARIQJoApVGaOOORAAmQAAmQAAmQAAmQAAmQgB8RoDT6UbL4qiRAAiRAAiRAAiRAAiRAAiQgTYDSKE2c8UiABEiABEiABEiABEiABEjAjwhQGv0oWXxVEiABEiABEiABEiABEiABEpAmQGmUJs54JEACJEACJEACJEACJEACJOBHBCiNfpQsvioJkAAJkAAJkAAJkAAJkAAJSBOgNEoTZzwSIAESIAESIAESIAESIAES8CMClEY/ShZflQRIgARIgARIgARIgARIgASkCVAapYkzHgmQAAmQAAmQAAmQAAmQAAn4EQFKox8li69KAiRAAiRAAiRAAiRAAiRAAtIEKI3SxBmPBEiABEiABEiABEiABEiABPyIAKXRj5LFVyUBEiABEiABEiABEiABEiABaQKURmnijEcCJEACJEACJEACJEACJEACfkSA0uhHyeKrkgAJkAAJkAAJkAAJkAAJkIA0AUqjNHHGIwESIAESIAESIAESIAESIAE/IkBp9KNk8VVJgARIgARIgARIgARIgARIQJoApVGaOOORAAmQAAmQAAmQAAmQAAmQgB8RoDT6UbL4qiRAAiRAAiRAAiRAAiRAAiQgTYDSKE2c8UiABEiABEiABEiABEiABEjAjwhQGv0oWXxVEiABEiABEiABEiABEiABEpAmQGmUJs54JEACJEACJEACJEACJEACJOBHBCiNfpQsvioJkAAJkAAJkAAJkAAJkAAJSBOgNEoTZzwSIAESIAESIAESIAESIAES8CMClEY/ShZflQRIgARIgARIgARIgARIgASkCVAapYkzHgmQAAmQAAmQAAmQAAmQAAn4EQFKox8li69KAiRAAiRAAiRAAiRAAiRAAtIEKI3SxBmPBEiABEiABEiABEiABEiABPyIAKXRj5LFVyUBEiABEiABEiABEiABEiABaQKURovE9xxOPmcN5WLDcOj4KWRkZlmMxOLuEogrEYoTyWlITct0twifs0ggNioEKWkZSD6VYbEmFneXQHR4EOBwICEpzd0ifM4igYjQQIQEB+JYYqrFmljcXQIhQQEoERms/x3lJUMgMMCBuJKh2H80RSagF6JULB3uhVpYBQmQwLkIUBot9g9Ko0WABopTGg1ALaRKSqM8c0qjPHNKozxzSqM8c0qjPHNGJAF/IEBptJglSqNFgAaKUxoNQKU0ykMtJCKlUT4llEZ55pRGeeaURnnmjEgC/kCA0mgxS5RGiwANFKc0GoBKaZSHSmn0OeaURvmUUBrlmVMa5ZkzIgn4AwFKo8UsURotAjRQnNJoACqlUR4qpdHnmFMa5VNCaZRnTmmUZ86IJOAPBCiNFrNEabQI0EBxSqMBqJRGeaiURp9jTmmUTwmlUZ45pVGeOSOSgD8QoDRazBKl0SJAA8UpjQagUhrloVIafY45pVE+JZRGeeaURnnmjEgC/kCA0mgxS5RGiwANFKc0GoBKaZSHSmn0OeaU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", + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{text}Trial", + "marker": { + "color": [], + "colorbar": { + "title": { + "text": "Trial" + } + }, + "colorscale": [ + [ + 0, + "rgb(247,251,255)" + ], + [ + 0.125, + "rgb(222,235,247)" + ], + [ + 0.25, + "rgb(198,219,239)" + ], + [ + 0.375, + "rgb(158,202,225)" + ], + [ + 0.5, + "rgb(107,174,214)" + ], + [ + 0.625, + "rgb(66,146,198)" + ], + [ + 0.75, + "rgb(33,113,181)" + ], + [ + 0.875, + "rgb(8,81,156)" + ], + [ + 1, + "rgb(8,48,107)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [], + "type": "scatter", + "x": [], + "y": [] + }, + { + "hovertemplate": "%{text}Best Trial", + "marker": { + "color": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "colorbar": { + "title": { + "text": "Best Trial" + }, + "x": 1.1, + "xpad": 40, + "y": 0.5 + }, + "colorscale": [ + [ + 0, + "rgb(255,245,240)" + ], + [ + 0.125, + "rgb(254,224,210)" + ], + [ + 0.25, + "rgb(252,187,161)" + ], + [ + 0.375, + "rgb(252,146,114)" + ], + [ + 0.5, + "rgb(251,106,74)" + ], + [ + 0.625, + "rgb(239,59,44)" + ], + [ + 0.75, + "rgb(203,24,29)" + ], + [ + 0.875, + "rgb(165,15,21)" + ], + [ + 1, + "rgb(103,0,13)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 0,
\"values\": [
9.7626,
0.6995,
0.9562
],
\"params\": {
\"pq_dim\": 662,
\"n_probes\": 79
}
}", + "{
\"number\": 1,
\"values\": [
4.9705,
0.1443,
0.8414
],
\"params\": {
\"pq_dim\": 210,
\"n_probes\": 57
}
}", + "{
\"number\": 2,
\"values\": [
10.0147,
0.8856,
0.9581
],
\"params\": {
\"pq_dim\": 678,
\"n_probes\": 98
}
}", + "{
\"number\": 3,
\"values\": [
9.3991,
0.3831,
0.9497
],
\"params\": {
\"pq_dim\": 432,
\"n_probes\": 70
}
}", + "{
\"number\": 4,
\"values\": [
9.018,
0.3598,
0.9471
],
\"params\": {
\"pq_dim\": 410,
\"n_probes\": 69
}
}", + "{
\"number\": 5,
\"values\": [
4.2757,
0.1919,
0.8495
],
\"params\": {
\"pq_dim\": 242,
\"n_probes\": 66
}
}", + "{
\"number\": 6,
\"values\": [
10.1002,
0.4773,
0.9518
],
\"params\": {
\"pq_dim\": 688,
\"n_probes\": 50
}
}", + "{
\"number\": 7,
\"values\": [
8.6237,
0.411,
0.9475
],
\"params\": {
\"pq_dim\": 388,
\"n_probes\": 85
}
}", + "{
\"number\": 8,
\"values\": [
6.0929,
0.2856,
0.8991
],
\"params\": {
\"pq_dim\": 286,
\"n_probes\": 79
}
}", + "{
\"number\": 9,
\"values\": [
6.0429,
0.0726,
0.8775
],
\"params\": {
\"pq_dim\": 284,
\"n_probes\": 19
}
}" + ], + "type": "scatter", + "x": [ + 0.6995, + 0.1443, + 0.8856, + 0.3831, + 0.3598, + 0.1919, + 0.4773, + 0.411, + 0.2856, + 0.0726 + ], + "y": [ + 0.9562, + 0.8414, + 0.9581, + 0.9497, + 0.9471, + 0.8495, + 0.9518, + 0.9475, + 0.8991, + 0.8775 + ] + } + ], + "layout": { + "autosize": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Pareto-front Plot" + }, + "xaxis": { + "autorange": true, + "range": [ + 0.02429064848316169, + 0.9339093515168384 + ], + "title": { + "text": "latency_in_ms" + }, + "type": "linear" + }, + "yaxis": { + "autorange": true, + "range": [ + 0.8317694174757282, + 0.9677305825242718 + ], + "title": { + "text": "recall" + }, + "type": "linear" + } + } + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA40AAAFoCAYAAADzQh4hAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQmcTeX/xz9jVmPGzDDGviV7Qr9UKiWSlBIVYWiPVIpIkVBR9pAltDESiWyJspRsKRJCSPZlLDOM2Zf//zkz95oZM86dc+59znPv/ZzX6/ea353nPOf7Pe/P9ft5e87ik5WVlQVuJEACJEACJEACJEACJEACJEACJFAAAR9KI78XJEACJEACJEACJEACJEACJEAChRGgNPK7QQIkQAIkQAIkQAIkQAIkQAIkUCgBSiO/HCRAAiRAAiRAAiRAAiRAAiRAApRGfgdIgARIgARIgARIgARIgARIgASKToArjUVnxhkkQAIkQAIkQAIkQAIkQAIk4DUEKI1eEzVPlARIgARIgARIgARIgARIgASKToDSWHRmnEECJEACJEACJEACJEACJEACXkOA0ug1UfNESYAESIAESIAESIAESIAESKDoBCiNRWfGGSRAAiRAAiRAAiRAAiRAAiTgNQQojV4TNU+UBEiABEiABEiABEiABEiABIpOgNJYdGacQQIkQAIkQAIkQAIkQAIkQAJeQ4DS6DVR80RJgARIgARIgARIgARIgARIoOgEKI1FZ8YZJEACJEACJEACJEACJEACJOA1BCiNXhM1T5QESIAESIAESIAESIAESIAEik6A0lh0ZpxBAiRAAiRAAiRAAiRAAiRAAl5DgNLoNVHzREmABEiABEiABEiABEiABEig6AQojUVnxhkkQAIkQAIkQAIkQAIkQAIk4DUEKI1eEzVPlARIgARIgARIgARIgARIgASKToDSWHRmnEECJEACJEACJEACJEACJEACXkOA0ug1UfNESYAESIAESIAESIAESIAESKDoBCiNRWfGGSRAAiRAAiRAAiRAAiRAAiTgNQQojV4TNU+UBEiABEiABEiABEiABEiABIpOgNJYdGacQQIkQAIkQAIkQAIkQAIkQAJeQ4DS6DVR80RJgARIgARIgARIgARIgARIoOgEKI1FZ8YZJEACJEACJEACJEACJEACJOA1BCiNXhM1T5QESIAESIAESIAESIAESIAEik6A0lh0ZpxBAiRAAiRAAiRAAiRAAiRAAl5DgNLoNVHzREmABEiABEiABEiABEiABEig6AQojUVnxhkkQAIkQAIkQAIk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abG2zXZPo/idGLNtn3+9Al8vXqMJsNiG9nsKj7dtXui5CGnUy0xMzn95qvido/yulYHDkLkjCZAACZAACZAACZAACZCARsCtpdHRDMUlpEdPnNEkrkLZSE0SzWziPY9nzl5ARFgoSkeU1C7rzL+JlbyTZ85pD90RcpZ7E1Ij+ikdEabdRyhzE30dOX4afn5+EMIqnjBrdhNCLaQxpERxjYmrN0f5XSsDV/fI45MACZAACZAACZAACZCApxDwCmn0lLB4HiRAAiRAAiRAAiRAAiRAAiQgmwClUTZx1iMBEiABEiABEiABEiABEiABNyJAaXSjsNgqCZAACZAACZAACZAACZAACcgmQGmUTZz1SIAESIAESIAESIAESIAESMCNCFAa3SgstkoCJEACJEACJEACJEACJEACsglQGmUTZz0SIAESIAESIAESIAESIAEScCMClEY3CoutkgAJkAAJkAAJkAAJkAAJkIBsApRG2cRZjwRIgARIgARIgARIgARIgATciACl0Y3CYqskQAIkQAIkQAIkQAIkQAIkIJsApVE2cdYjARIgARIgARIgARIgARIgATciQGl0o7DYKgmQAAmQAAmQAAmQAAmQAAnIJkBplE2c9UiABEiABEiABEiABEiABEjAjQhQGt0oLLZKAiRAAiRAAiRAAiRAAiRAArIJUBplE2c9EiABEiABEiABEiABEiABEnAjApRGNwqLrZIACZAACZAACZAACZAACZCAbAKURtnEWY8ESIAESIAESIAESIAESIAE3IgApdGNwmKrJEACJEACJEACJEACJEACJCCbAKVRNnHWIwESIAESIAESIAESIAESIAE3IkBpdKOw2CoJkAAJkAAJkAAJkAAJkAAJyCZAaZRNnPVIgARIgARIgARIgARIgARIwI0IUBrdKCy2SgIkQAIkQAIkQAIkQAIkQAKyCVAaZRNnPRIgARIgARIgARIgARIgARJwIwKURjcKi62SAAmQAAmQAAmQAAmQAAmQgGwClEbZxFmPBEiABEiABEiABEiABEiABNyIAKXRjcJiqyRAAiRAAiRAAiRAAiRAAiQgmwClUTZx1iMBEiABEiABEiABEiABEiABNyJAaXSjsNgqCZAACZAACZAACZAACZAACcgmQGmUTZz1SIAESIAESIAESIAESIAESMCNCFAa3SgstkoCJEACJEAC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", + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualization(ivf_pq_study)" + ] + }, + { + "cell_type": "markdown", + "id": "7d5494bb-b254-4dfd-9029-f546365be894", + "metadata": { + "tags": [] + }, + "source": [ + "## cagra HPO example" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "98828ce8-fec0-4096-8a5a-9be3e181a0ce", + "metadata": {}, + "outputs": [], + "source": [ + "from cuvs.neighbors import cagra\n" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "id": "c644ded3-644b-4f51-81f6-15b612123678", + "metadata": {}, + "outputs": [], + "source": [ + "def multi_objective_cagra(trial):\n", + " \"\"\"\n", + " Optimizes the parameters for the cagra index using a multi-objective approach.\n", + "\n", + " \"\"\"\n", + " # Suggest values for build parameters\n", + " intermediate_graph_degree = trial.suggest_int(\"intermediate_graph_degree\", 64, 128, step=2 )\n", + "\n", + " # Suggest an integer for the number of probes\n", + " itopk_size = trial.suggest_int(\"itopk_size\", 64, 128, step=2)\n", + "\n", + " build_params = cagra.IndexParams(\n", + " intermediate_graph_degree=intermediate_graph_degree\n", + " )\n", + "\n", + " start_build_time = time.time()\n", + " cagra_index = cagra.build(build_params, vectors)\n", + " build_time_in_secs = time.time() - start_build_time\n", + "\n", + " # Configure search parameters\n", + " search_params = cagra.SearchParams(itopk_size=itopk_size)\n", + "\n", + " # perform search and refine to increase recall/accuracy\n", + " start_search_time = time.time()\n", + " distances, indices = cagra.search(search_params, cagra_index, queries, k=10)\n", + " search_time = time.time() - start_search_time\n", + "\n", + " latency_in_ms = (search_time * 1000)/queries.shape[0]\n", + "\n", + " found_distances, found_indices = cp.asnumpy(distances), cp.asnumpy(indices)\n", + " recall = calc_recall(found_indices, gt_neighbors)\n", + "\n", + " return round(build_time_in_secs,4), round(latency_in_ms,4), round(recall,4)" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "516b9b78-ba13-490c-ba47-d6786fb33606", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:53:39,324] A new study created in memory with name: no-name-b457b87f-2a54-4e19-944d-5902bf10ea8e\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 192\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:56:22,063] Trial 0 finished with values: [157.0238, 0.0412, 0.9903] and parameters: {'intermediate_graph_degree': 76, 'itopk_size': 124}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[I] [16:56:15.120128] optimizing graph\n", + "[I] [16:56:16.300838] Graph optimized, creating index\n", + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 192\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 16:59:03,390] Trial 1 finished with values: [155.633, 0.0364, 0.9884] and parameters: {'intermediate_graph_degree': 72, 'itopk_size': 106}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[I] [16:58:56.534874] optimizing graph\n", + "[I] [16:58:57.649931] Graph optimized, creating index\n", + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 192\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 17:01:51,648] Trial 2 finished with values: [162.741, 0.0231, 0.9801] and parameters: {'intermediate_graph_degree': 90, 'itopk_size': 66}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[I] [17:01:44.581010] optimizing graph\n", + "[I] [17:01:46.084702] Graph optimized, creating index\n", + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 192\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 17:04:46,930] Trial 3 finished with values: [169.5533, 0.0396, 0.9915] and parameters: {'intermediate_graph_degree': 110, 'itopk_size': 114}. \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[I] [17:04:39.128583] optimizing graph\n", + "[I] [17:04:41.155907] Graph optimized, creating index\n", + "using ivf_pq::index_params nrows 1000000, dim 768, n_lits 1000, pq_dim 192\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2024-08-19 17:07:31,215] Trial 4 finished with values: [158.4761, 0.0414, 0.9905] and parameters: {'intermediate_graph_degree': 80, 'itopk_size': 126}. \n" + ] + } + ], + "source": [ + "cagra_study = optuna.create_study(directions=['minimize', 'minimize', 'maximize'])\n", + "cagra_study.optimize(multi_objective_cagra, n_trials=5)" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "id": "293050f3-09ad-4333-b2e7-5887f2a25465", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of trials on the Pareto front: 5\n", + "Trial with lowest build time in secs: \n", + "\tnumber: 1\n", + "\tparams: {'intermediate_graph_degree': 72, 'itopk_size': 106}\n", + "\tvalues: [155.633, 0.0364, 0.9884]\n", + "Trial with lowest latency in ms: \n", + "\tnumber: 2\n", + "\tparams: {'intermediate_graph_degree': 90, 'itopk_size': 66}\n", + "\tvalues: [162.741, 0.0231, 0.9801]\n", + "Trial with highest accuracy: \n", + "\tnumber: 3\n", + "\tparams: {'intermediate_graph_degree': 110, 'itopk_size': 114}\n", + "\tvalues: [169.5533, 0.0396, 0.9915]\n" + ] + } + ], + "source": [ + "print_best_trial_values(cagra_study)" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "0b431ad6-fd07-40b4-b751-39a3ee5169d7", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{text}Trial", + "marker": { + "color": [], + "colorbar": { + "title": { + "text": "Trial" + } + }, + "colorscale": [ + [ + 0, + "rgb(247,251,255)" + ], + [ + 0.125, + "rgb(222,235,247)" + ], + [ + 0.25, + "rgb(198,219,239)" + ], + [ + 0.375, + "rgb(158,202,225)" + ], + [ + 0.5, + "rgb(107,174,214)" + ], + [ + 0.625, + "rgb(66,146,198)" + ], + [ + 0.75, + "rgb(33,113,181)" + ], + [ + 0.875, + "rgb(8,81,156)" + ], + [ + 1, + "rgb(8,48,107)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [], + "type": "scatter", + "x": [], + "y": [] + }, + { + "hovertemplate": "%{text}Best Trial", + "marker": { + "color": [ + 0, + 1, + 2, + 3, + 4 + ], + "colorbar": { + "title": { + "text": "Best Trial" + }, + "x": 1.1, + "xpad": 40, + "y": 0.5 + }, + "colorscale": [ + [ + 0, + "rgb(255,245,240)" + ], + [ + 0.125, + "rgb(254,224,210)" + ], + [ + 0.25, + "rgb(252,187,161)" + ], + [ + 0.375, + "rgb(252,146,114)" + ], + [ + 0.5, + "rgb(251,106,74)" + ], + [ + 0.625, + "rgb(239,59,44)" + ], + [ + 0.75, + "rgb(203,24,29)" + ], + [ + 0.875, + "rgb(165,15,21)" + ], + [ + 1, + "rgb(103,0,13)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 0,
\"values\": [
157.0238,
0.0412,
0.9903
],
\"params\": {
\"intermediate_graph_degree\": 76,
\"itopk_size\": 124
}
}", + "{
\"number\": 1,
\"values\": [
155.633,
0.0364,
0.9884
],
\"params\": {
\"intermediate_graph_degree\": 72,
\"itopk_size\": 106
}
}", + "{
\"number\": 2,
\"values\": [
162.741,
0.0231,
0.9801
],
\"params\": {
\"intermediate_graph_degree\": 90,
\"itopk_size\": 66
}
}", + "{
\"number\": 3,
\"values\": [
169.5533,
0.0396,
0.9915
],
\"params\": {
\"intermediate_graph_degree\": 110,
\"itopk_size\": 114
}
}", + "{
\"number\": 4,
\"values\": [
158.4761,
0.0414,
0.9905
],
\"params\": {
\"intermediate_graph_degree\": 80,
\"itopk_size\": 126
}
}" + ], + "type": "scatter", + "x": [ + 157.0238, + 155.633, + 162.741, + 169.5533, + 158.4761 + ], + "y": [ + 0.9903, + 0.9884, + 0.9801, + 0.9915, + 0.9905 + ] + } + ], + "layout": { + "autosize": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Pareto-front Plot" + }, + "xaxis": { + "autorange": true, + "range": [ + 154.8058405093237, + 170.38045949067632 + ], + "title": { + "text": "build_time_in_secs" + }, + "type": "linear" + }, + "yaxis": { + "autorange": true, + "range": [ + 0.9791592233009708, + 0.9924407766990292 + ], + "title": { + "text": "recall" + }, + "type": "linear" + } + } + }, + "image/png": "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", + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{text}Trial", + "marker": { + "color": [], + "colorbar": { + "title": { + "text": "Trial" + } + }, + "colorscale": [ + [ + 0, + "rgb(247,251,255)" + ], + [ + 0.125, + "rgb(222,235,247)" + ], + [ + 0.25, + "rgb(198,219,239)" + ], + [ + 0.375, + "rgb(158,202,225)" + ], + [ + 0.5, + "rgb(107,174,214)" + ], + [ + 0.625, + "rgb(66,146,198)" + ], + [ + 0.75, + "rgb(33,113,181)" + ], + [ + 0.875, + "rgb(8,81,156)" + ], + [ + 1, + "rgb(8,48,107)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [], + "type": "scatter", + "x": [], + "y": [] + }, + { + "hovertemplate": "%{text}Best Trial", + "marker": { + "color": [ + 0, + 1, + 2, + 3, + 4 + ], + "colorbar": { + "title": { + "text": "Best Trial" + }, + "x": 1.1, + "xpad": 40, + "y": 0.5 + }, + "colorscale": [ + [ + 0, + "rgb(255,245,240)" + ], + [ + 0.125, + "rgb(254,224,210)" + ], + [ + 0.25, + "rgb(252,187,161)" + ], + [ + 0.375, + "rgb(252,146,114)" + ], + [ + 0.5, + "rgb(251,106,74)" + ], + [ + 0.625, + "rgb(239,59,44)" + ], + [ + 0.75, + "rgb(203,24,29)" + ], + [ + 0.875, + "rgb(165,15,21)" + ], + [ + 1, + "rgb(103,0,13)" + ] + ], + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "text": [ + "{
\"number\": 0,
\"values\": [
157.0238,
0.0412,
0.9903
],
\"params\": {
\"intermediate_graph_degree\": 76,
\"itopk_size\": 124
}
}", + "{
\"number\": 1,
\"values\": [
155.633,
0.0364,
0.9884
],
\"params\": {
\"intermediate_graph_degree\": 72,
\"itopk_size\": 106
}
}", + "{
\"number\": 2,
\"values\": [
162.741,
0.0231,
0.9801
],
\"params\": {
\"intermediate_graph_degree\": 90,
\"itopk_size\": 66
}
}", + "{
\"number\": 3,
\"values\": [
169.5533,
0.0396,
0.9915
],
\"params\": {
\"intermediate_graph_degree\": 110,
\"itopk_size\": 114
}
}", + "{
\"number\": 4,
\"values\": [
158.4761,
0.0414,
0.9905
],
\"params\": {
\"intermediate_graph_degree\": 80,
\"itopk_size\": 126
}
}" + ], + "type": "scatter", + "x": [ + 0.0412, + 0.0364, + 0.0231, + 0.0396, + 0.0414 + ], + "y": [ + 0.9903, + 0.9884, + 0.9801, + 0.9915, + 0.9905 + ] + } + ], + "layout": { + "autosize": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Pareto-front Plot" + }, + "xaxis": { + "autorange": true, + "range": [ + 0.02201259393264681, + 0.042487406067353184 + ], + "title": { + "text": "latency_in_ms" + }, + "type": "linear" + }, + "yaxis": { + "autorange": true, + "range": [ + 0.9791592233009708, + 0.9924407766990292 + ], + "title": { + "text": "recall" + }, + "type": "linear" + } + } + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA40AAAFoCAYAAADzQh4hAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQmcTeX/xz/XMGbMjH0nSwopiVIoEZVIxC+UrT1SFBFRIVGyhUhRiZEskaVEkVKWFFmSlKSsY2cYY4z5/59n3GtmzIzvmXPPY+49n/N69ZrunO/3POd5fw/mPc9yPElJSUngQQIkQAIkQAIkQAIkQAIkQAIkQALpEPBQGvlckAAJkAAJkAAJkAAJkAAJkAAJZESA0shngwRIgARIgARIgARIgARIgARIIEMClEY+HCRAAiRAAiRAAiRAAiRAAiRAApRGPgMkQAIkQAIkQAIkQAIkQAIkQALWCXCk0TozZpAACZAACZAACZAACZAACZCAawhQGl1TanaUBEiABEiABEiABEiABEiABKwToDRaZ8YMEiABEiABEiABEiABEiABEnANAUqja0rNjpIACZAACZAACZAACZAACZCAdQKURuvMmEECJEACJEACJEACJEACJEACriFAaXRNqdlREiABEiABEiABEiABEiABErBOgNJonRkzSIAESIAESIAESIAESIAESMA1BCiNrik1O0oCJEACJEACJEACJEACJEAC1glQGq0zYwYJkAAJkAAJkAAJkAAJkAAJuIYApdE1pWZHSYAESIAESIAESIAESIAESMA6AUqjdWbMIAESIAESIAESIAESIAESIAHXEKA0uqbU7CgJkAAJkAAJkAAJkAAJkAAJWCdAabTOjBkkQAIkQAIkQAIkQAIkQAIk4BoClEbXlJodJQESIAESIAESIAESIAESIAHrBCiN1pkxgwRIgARIgARIgARIgARIgARcQ4DS6JpSs6MkQAIkQAIkQAIkQAIkQAIkYJ0ApdE6M2aQAAmQAAmQAAmQAAmQAAmQgGsIUBpdU2p2lARIgARIgARIgARIgARIgASsE6A0WmfGDBIgARIgARIgARIgARIgARJwDQFKo2tKzY6SAAmQAAmQAAmQAAmQAAmQgHUClEbrzJhBAiRAAiRAAiRAAiRAAiRAAq4hQGl0TanZURIgARIgARIgARIgARIgARKwToDSaJ0ZM0iABEiABEiABEiABEiABEjANQQoja4pNTtKAiRAAiRAAiRAAiRAAiRAAtYJUBqtM2MGCZAACZAACZAACZAACZAACbiGAKXRNaVm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mUpOOz1VfU/KL7MaiCEzkARIgARIgARIgARIgARIQBMIaGmU1lBNIf1vT4yWuJLFCmtJtHOo9zzGHDyCAvmiUKhAXj2tM+2hRvL2xhzSm+4oOUt5KKlR91OoQD69jtDkoe7r3937kTNnTihhVTvM2j2UUCtpjIwI10ycPqT8MquB0/fI65MACZAACZAACZAACZBAsBBwhTQGS7HYDxIgARIgARIgARIgARIgARIwTYDSaJo42yMBEiABEiABEiABEiABEiCBACJAaQygYvFWSYAESIAESIAESIAESIAESMA0AUqjaeJsjwRIgARIgARIgARIgARIgAQCiAClMYCKxVslARIgARIgARIgARIgARIgAdMEKI2mibM9EiABEiABEiABEiABEiABEgggApTGACoWb5UESIAESIAESIAESIAESIAETBOgNJomzvZIgARIgARIgARIgARIgARIIIAIUBoDqFi8VRIgARIgARIgARIgARIgARIwTYDSaJo42yMBEiABEiABEiABEiABEiCBACJAaQygYvFWSYAESIAESIAESIAESIAESMA0AUqjaeJsjwRIgARIgARIgARIgARIgAQCiAClMYCKxVslARIgARIgARIgARIgARIgAdMEKI2mibM9EiABEiABEiABEiABEiABEgggApTGACoWb5UESIAESIAESIAESIAESIAETBOgNJomzvZIgARIgARIgARIgARIgARIIIAIUBoDqFi8VRIgARIgARIgARIgARIgARIwTYDSaJo42yMBEiABEiABEiABEiABEiCBACJAaQygYvFWSYAESIAESIAESIAESIAESMA0AUqjaeJsjwRIgARIgARIgARIgARIgAQCiAClMYCKxVslARIgARIgARIgARIgARIgAdMEKI2mibM9EiABEiABEiABEiABEiABEgggApTGACoWb5UESIAESIAESIAESIAESIAETBOgNJomzvZIgARIgARIgARIgARIgARIIIAIUBoDqFi8VRIgARIgARIgARIgARIgARIwTYDSaJo42yMBEiABEiABEiABEiABEiCBACJAaQygYvFWSYAESIAESIAESIAESIAESMA0AUqjaeJsjwRIgARIgARIgARIgARIgAQCiAClMYCKxVslARIgARIgARIgARIgARIgAdMEKI2mibM9EiABEiABEiABEiABEiABEgggApTGACoWb5UESIAESIAESIAESIAESIAETBOgNJomzvZIgARIgARIgARIgARIgARIIIAIUBoDqFi8VRIgARIgARIgARIgARIgARIwTYDSaJo42yMBEiABEiABEiABEiABEiCBACJAaQygYvFWSYAESIAESIAESIAESIAESMA0AUqjaeJsjwRIgARIgARIgARIgARIgAQCiAClMYCKxVslARIgARIgARIgARIgARIgAdMEKI2mibM9EiABEiABEiABEiABEiABEgggApTGACoWb5UESIAESIAESIAESIAESIAETBOgNJomzvZIgARIgARIgARIgARIgARIIIAIUBoDqFi8VRIgARIgARIgARIgARIgARIwTYDSaJo42yMBEiABEiABEiABEiABEiCBACJAaQygYvFWSYAESIAESIAESIAESIAESMA0AUqjaeJsjwRIgARIgARIgARIgARIgAQCiAClMYCKxVslARIgARIgARIgARIgARIgAdMEKI2mibM9EiABEiABEiABEiABEiABEgggApTGACoWb5UESIAESIAESIAESIAESIAETBOgNJomzvZIgARIgARIgARIgARIgARIIIAIUBoDqFi8VRIgARIgARIgARIgARIgARIwTYDSaJP4nkNxNq/AdAmBYgXCcOBYPM6dS5KEM4YEMiVQMCoUp06fxemEcyRFArYJRIXnBDwenDiVYPtavAAJhIWGIE/uEBw+cYYwhARKFgoXRjKMBEggqwQojVkldz6P0mgToDCd0igExTARAUqjCBODhAQojUJQDBMRoDSKMKUKojRaZ8YMErBKgNJolViaeEqjTYDCdEqjEBTDRAQojSJMDBISoDQKQTFMRIDSKMJEabSOiRkkYIsApdEWPoDSaBOgMJ3SKATFMBEBSqMIE4OEBCiNQlAMExGgNIowURqtY2IGCdgiQGm0hY/SaBOfOJ3SKEbFQAEBSqMAEkPEBCiNYlQMFBCgNAogpQnh9FTrzJhBAlYJUBqtEksTz5FGmwCF6ZRGISiGiQhQGkWYGCQkQGkUgmKYiAClUYQpVRCl0TozZpCAVQKURqvEKI02iWUtndKYNW7MSp8ApZFPhj8JUBr9SZPXojRafwYojdaZMYMErBKgNFolRmm0SSxr6ZTGrHFjFqWRz4DzBCiNzjN2UwuURuvVpjRaZ8YMErBKgNJo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", + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "visualization(cagra_study)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "conda_python3", + "language": "python", + "name": "conda_python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}