-
Notifications
You must be signed in to change notification settings - Fork 113
distinct_count_estimator aka HyperLogLog++ #429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 87 commits
Commits
Show all changes
90 commits
Select commit
Hold shift + click to select a range
836e77a
First draft
sleeepyjack d3a1e2f
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack 6718560
Code style
sleeepyjack c59744e
Resolve merge conflicts
sleeepyjack b7533a0
Initialize shmem atomics through placement new
sleeepyjack f4bdac2
Improve naming
sleeepyjack cea2afb
Move some functionality to storage class
sleeepyjack 0f0bd3f
Add inline docs for public APIs
sleeepyjack b21dcd1
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack 1c780c2
Add benchmark
sleeepyjack a83a3f3
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack b478e01
Remove scope ctor parameter for now
sleeepyjack e3d401a
Update benchmark
sleeepyjack 56520a6
Select cg reduce impl based on nvcc version
sleeepyjack 3673772
Re-format tuning header
sleeepyjack 799284e
Implement HLL++ bias correction step
sleeepyjack 758977c
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack abbeffa
Extend examples and fix some bugs along the way
sleeepyjack 86d4618
Refactor thresholds
sleeepyjack d6a9a4e
Initialize shmem storage using placement new
sleeepyjack 891d606
Add unit test
sleeepyjack 3544195
Remove experimental cg async reduce since it is buggy
sleeepyjack 3506ecb
Fix bit-shifting bug that lead to high error rates
sleeepyjack 919d0ab
Storage cleanups
sleeepyjack 12301df
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack 52f6e09
Update readme
sleeepyjack 0a0119d
Fix typo
sleeepyjack ab50bed
Fix typo
sleeepyjack 68d2df0
Apply suggestions from code review
sleeepyjack 4567169
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack 93f68a2
Use CUDART_VERSION instead of (__CUDACC_VER_MAJOR__
sleeepyjack 03a8572
Apply suggestions from code review
sleeepyjack 33f7baf
Enable Precision>18; fix some bugs, extend tests.
sleeepyjack 6e3683f
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack b1253bf
Remove storage class and move host implementations to ref class
sleeepyjack 64a5b70
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack 22c083d
Remove storage class
sleeepyjack 56cdc6b
Add vectorized add kernel
sleeepyjack b8dc849
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack 9b4b612
Add missing kernel config
sleeepyjack 30bd79d
Make tuning arrs accessible in non-constexpr context
sleeepyjack e93c248
Allow wider vector sizes
sleeepyjack 204b8e2
Fix processing of remaining items
sleeepyjack fe1cf5a
Guard invoke_one with macro
sleeepyjack ae9e77c
Specify sketch size/precision at runtime
sleeepyjack 65ff70a
Pre-compute register mask
sleeepyjack 8068799
Fix unit test
sleeepyjack 04c303d
Add sketch_size_kb strong type and fix stupid bug where I called a st…
sleeepyjack a7036ae
Fix benchmark
sleeepyjack 3e25da7
More robust error estimation in benchmark
sleeepyjack e5d5112
Benchmark gmem fallback kernel
sleeepyjack 99c0dee
Rename max_sketch_size_kb -> sketch_size_kb
sleeepyjack aeaecf4
Improve error handling and docs
sleeepyjack 55fa312
Cleanup finalizer
sleeepyjack 2229c68
Use double reduction
sleeepyjack 156a843
Use .estimate() in device ref example
sleeepyjack 80dde95
Add device ref test
sleeepyjack 730bf73
Restructure to reduce fp error
sleeepyjack c50e795
Rename parameter for other estimator ref
sleeepyjack d5595da
Update benchmark
sleeepyjack 16ad77a
Rebind allocator to register_type to ensure proper alignment
sleeepyjack b501a32
Use cudaMemcpyDefault
sleeepyjack 0bf0a88
Mention alignment requirements in device_ref_example
sleeepyjack d03120c
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack a361360
Pass T instead of Estimator to benchmark
sleeepyjack 66870a7
Fix typo in benchmark script
sleeepyjack b990dca
Rename hash function
sleeepyjack c87309e
Use placement new to initialize sketch
sleeepyjack 03d4b41
Remove custom_deleter member
sleeepyjack 7de06fb
Rename sketch_size.hpp -> sktech_size.cuh
sleeepyjack 185d3c4
Use std::abs
sleeepyjack 023d080
Use std::vector instead of thrust::host_vector>
sleeepyjack 53cdf37
Add note about shmem alignment
sleeepyjack 2a81714
Remove comment
sleeepyjack d859b39
Remove device-sided error handling since it hurts performance
sleeepyjack 43be0f0
Constexpr all the things!
sleeepyjack fbd6dab
Add constructor overload which takes the desired standard deviation
sleeepyjack dfe1a07
Remove stray include
sleeepyjack 2629adc
Bugfixes
sleeepyjack 1ad97e2
Fix merge
sleeepyjack 3b0da20
Add Spark parity tests
sleeepyjack f80509f
Fix error calculation
sleeepyjack bbb7258
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack b61a2db
Move include/cuco/sentinel.cuh -> include/cuco/types.cuh
sleeepyjack 9b0ee68
Move HLL-related strong types to types.cuh
sleeepyjack 75cd967
Apparently Doxygen has become even pickier...
sleeepyjack 9436931
Merge remote-tracking branch 'upstream/dev' into hll
sleeepyjack 6929f65
Clean up device ref example
sleeepyjack a496f9e
Update godbolt links
sleeepyjack 7fecd7b
Clean up unique sequence unit test
sleeepyjack File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| /* | ||
| * 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 <defaults.hpp> | ||
| #include <utils.hpp> | ||
|
|
||
| #include <cuco/distinct_count_estimator.cuh> | ||
| #include <cuco/static_set.cuh> | ||
| #include <cuco/utility/key_generator.cuh> | ||
|
|
||
| #include <nvbench/nvbench.cuh> | ||
|
|
||
| #include <thrust/device_vector.h> | ||
| #include <thrust/iterator/transform_iterator.h> | ||
|
|
||
| #include <cuda/functional> | ||
|
|
||
| #include <cmath> | ||
| #include <cstddef> | ||
|
|
||
| using namespace cuco::benchmark; | ||
| using namespace cuco::utility; | ||
|
|
||
| template <typename InputIt> | ||
| [[nodiscard]] std::size_t exact_distinct_count(InputIt first, std::size_t n) | ||
| { | ||
| // TODO static_set currently only supports types up-to 8-bytes in size. | ||
| // Casting is valid since the keys generated are representable in int64_t. | ||
| using T = std::int64_t; | ||
|
|
||
| auto cast_iter = thrust::make_transform_iterator( | ||
| first, cuda::proclaim_return_type<T>([] __device__(auto i) { return static_cast<T>(i); })); | ||
|
|
||
| auto set = cuco::static_set{n, 0.8, cuco::empty_key<T>{-1}}; | ||
| set.insert(cast_iter, cast_iter + n); | ||
| return set.size(); | ||
| } | ||
|
|
||
| template <class Estimator, class Dist> | ||
| [[nodiscard]] double relative_error(nvbench::state& state, std::size_t num_samples) | ||
| { | ||
| using T = typename Estimator::value_type; | ||
|
|
||
| auto const num_items = state.get_int64("NumInputs"); | ||
| auto const sketch_size_kb = state.get_int64("SketchSizeKB"); | ||
|
|
||
| thrust::device_vector<T> items(num_items); | ||
|
|
||
| key_generator gen; | ||
| Estimator estimator{cuco::sketch_size_kb(sketch_size_kb)}; | ||
| double error_sum = 0; | ||
| for (std::size_t i = 0; i < num_samples; ++i) { | ||
| gen.generate(dist_from_state<Dist>(state), items.begin(), items.end()); | ||
| estimator.add(items.begin(), items.end()); | ||
| double estimated_cardinality = estimator.estimate(); | ||
| double true_cardinality = exact_distinct_count(items.begin(), num_items); | ||
| error_sum += std::abs(estimated_cardinality / true_cardinality - 1.0); | ||
| estimator.clear(); | ||
| } | ||
|
|
||
| return error_sum / num_samples; | ||
| } | ||
|
|
||
| /** | ||
| * @brief A benchmark evaluating `cuco::distinct_count_estimator` end-to-end performance | ||
| */ | ||
| template <typename T, typename Dist> | ||
| void distinct_count_estimator_e2e(nvbench::state& state, nvbench::type_list<T, Dist>) | ||
| { | ||
| using estimator_type = cuco::distinct_count_estimator<T>; | ||
|
|
||
| auto const num_items = state.get_int64("NumInputs"); | ||
| auto const sketch_size_kb = state.get_int64("SketchSizeKB"); | ||
|
|
||
| state.add_element_count(num_items); | ||
| state.add_global_memory_reads<T>(num_items, "InputSize"); | ||
|
|
||
| auto const err_samples = (cuda::std::is_same_v<Dist, distribution::unique>) ? 1 : 5; | ||
| auto const err = relative_error<estimator_type, Dist>(state, err_samples); | ||
| auto& summ = state.add_summary("MeanRelativeError"); | ||
| summ.set_string("hint", "MRelErr"); | ||
| summ.set_string("short_name", "MeanRelativeError"); | ||
| summ.set_string("description", "Mean relatve approximation error."); | ||
| summ.set_float64("value", err); | ||
|
|
||
| thrust::device_vector<T> items(num_items); | ||
|
|
||
| key_generator gen; | ||
| gen.generate(dist_from_state<Dist>(state), items.begin(), items.end()); | ||
|
|
||
| estimator_type estimator{cuco::sketch_size_kb(sketch_size_kb)}; | ||
| std::size_t estimated_cardinality = 0; | ||
| state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, | ||
| [&](nvbench::launch& launch, auto& timer) { | ||
| timer.start(); | ||
| estimator.add_async(items.begin(), items.end(), {launch.get_stream()}); | ||
| estimated_cardinality = estimator.estimate({launch.get_stream()}); | ||
| timer.stop(); | ||
|
|
||
| estimator.clear_async({launch.get_stream()}); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * @brief A benchmark evaluating `cuco::distinct_count_estimator::add` performance | ||
| */ | ||
| template <typename T, typename Dist> | ||
| void distinct_count_estimator_add(nvbench::state& state, nvbench::type_list<T, Dist>) | ||
| { | ||
| using estimator_type = cuco::distinct_count_estimator<T>; | ||
|
|
||
| auto const num_items = state.get_int64("NumInputs"); | ||
| auto const sketch_size_kb = state.get_int64("SketchSizeKB"); | ||
|
|
||
| thrust::device_vector<T> items(num_items); | ||
|
|
||
| key_generator gen; | ||
| gen.generate(dist_from_state<Dist>(state), items.begin(), items.end()); | ||
|
|
||
| state.add_element_count(num_items); | ||
| state.add_global_memory_reads<T>(num_items, "InputSize"); | ||
|
|
||
| estimator_type estimator{cuco::sketch_size_kb(sketch_size_kb)}; | ||
| state.exec(nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { | ||
| timer.start(); | ||
| estimator.add_async(items.begin(), items.end(), {launch.get_stream()}); | ||
| timer.stop(); | ||
|
|
||
| estimator.clear_async({launch.get_stream()}); | ||
| }); | ||
| } | ||
|
|
||
| using TYPE_RANGE = nvbench::type_list<nvbench::int32_t, nvbench::int64_t, __int128_t>; | ||
|
|
||
| NVBENCH_BENCH_TYPES(distinct_count_estimator_e2e, | ||
| NVBENCH_TYPE_AXES(TYPE_RANGE, nvbench::type_list<distribution::uniform>)) | ||
| .set_name("distinct_count_estimator_e2e") | ||
| .set_type_axes_names({"T", "Distribution"}) | ||
| .add_int64_power_of_two_axis("NumInputs", {28, 29, 30}) | ||
| .add_int64_axis("SketchSizeKB", {8, 16, 32, 64, 128, 256}) // 256KB uses gmem fallback kernel | ||
| .add_int64_axis("Multiplicity", {1}) | ||
| .set_max_noise(defaults::MAX_NOISE); | ||
|
|
||
| NVBENCH_BENCH_TYPES(distinct_count_estimator_add, | ||
| NVBENCH_TYPE_AXES(TYPE_RANGE, nvbench::type_list<distribution::uniform>)) | ||
| .set_name("distinct_count_estimator::add_async") | ||
| .set_type_axes_names({"T", "Distribution"}) | ||
| .add_int64_power_of_two_axis("NumInputs", {28, 29, 30}) | ||
| .add_int64_axis("SketchSizeKB", {8, 16, 32, 64, 128, 256}) | ||
| .add_int64_axis("Multiplicity", {1}) | ||
| .set_max_noise(defaults::MAX_NOISE); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
examples/distinct_count_estimator/device_ref_example.cu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| /* | ||
| * 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 <cuco/distinct_count_estimator.cuh> | ||
|
|
||
| #include <thrust/device_vector.h> | ||
| #include <thrust/sequence.h> | ||
|
|
||
| #include <cstddef> | ||
| #include <iostream> | ||
|
|
||
| /** | ||
| * @file device_ref_example.cu | ||
| * @brief Demonstrates usage of `cuco::distinct_count_estimator` device-side APIs. | ||
| * | ||
| * This example demonstrates how the non-owning reference type `cuco::distinct_count_estimator_ref` | ||
| * can be used to implement a custom kernel that fuses the cardinality estimation step with any | ||
| * other workload that traverses the input data. | ||
| */ | ||
|
|
||
| template <class RefType, class InputIt> | ||
| __global__ void piggyback_kernel(RefType ref, InputIt first, std::size_t n) | ||
| { | ||
| // Transform the reference type (with device scope) to a reference type with block scope | ||
| using local_ref_type = typename RefType::with_scope<cuda::thread_scope_block>; | ||
|
|
||
| // Shared memory storage for the block-local estimator | ||
| extern __shared__ std::byte local_sketch[]; | ||
|
|
||
| // The following check is optional since the base address of dynamic shared memory is guaranteed | ||
| // to meet the alignment requirements | ||
| /* | ||
| auto const alignment = | ||
| 1ull << cuda::std::countr_zero(reinterpret_cast<std::uintptr_t>(local_sketch)); | ||
| assert(alignment >= local_ref_type::sketch_alignment()); | ||
| */ | ||
|
|
||
| auto const loop_stride = gridDim.x * blockDim.x; | ||
| auto idx = blockDim.x * blockIdx.x + threadIdx.x; | ||
| auto const block = cooperative_groups::this_thread_block(); | ||
|
|
||
| // Create the local estimator with the shared memory storage | ||
| local_ref_type local_ref(cuda::std::span{local_sketch, ref.sketch_bytes()}); | ||
|
|
||
| // Initialize the local estimator | ||
| local_ref.clear(block); | ||
| block.sync(); | ||
|
|
||
| while (idx < n) { | ||
| auto const& item = *(first + idx); | ||
|
|
||
| // Add each item to the local estimator | ||
| local_ref.add(item); | ||
|
|
||
| /* | ||
| Here we can add some custom workload that takes the input `item`. | ||
|
|
||
| The idea is that cardinality estimation can be fused/piggy-backed with any other workload that | ||
| traverses the data. Since `local_ref.add` can run close to the SOL of the DRAM bandwidth, we get | ||
| the estimate "for free" while performing other computations over the data. | ||
| */ | ||
|
|
||
| idx += loop_stride; | ||
| } | ||
| block.sync(); | ||
|
|
||
| // We can also compute the local estimate on the device | ||
| // auto const local_estimate = local_ref.estimate(block); | ||
| if (block.thread_rank() == 0) { | ||
| // The local estimate should approximately be `num_items`/`gridDim.x` | ||
| // printf("Estimate for block %d = %llu\n", blockIdx.x, local_estimate); | ||
| } | ||
|
|
||
| // In the end, we merge the shared memory estimator into the global estimator which gives us the | ||
| // final result | ||
| ref.merge(block, local_ref); | ||
| } | ||
|
|
||
| template <typename Ref, typename InputIt, typename OutputIt> | ||
| __global__ void device_estimate_kernel(cuco::sketch_size_kb sketch_size_kb, | ||
| InputIt in, | ||
| size_t n, | ||
| OutputIt out) | ||
| { | ||
| extern __shared__ std::byte local_sketch[]; | ||
|
|
||
| auto const block = cooperative_groups::this_thread_block(); | ||
|
|
||
| // only a single block computes the estimate | ||
| if (block.group_index().x == 0) { | ||
| Ref estimator(cuda::std::span(local_sketch, Ref::sketch_bytes(sketch_size_kb))); | ||
|
|
||
| estimator.clear(block); | ||
| block.sync(); | ||
|
|
||
| for (int i = block.thread_rank(); i < n; i += block.num_threads()) { | ||
| estimator.add(*(in + i)); | ||
| } | ||
| block.sync(); | ||
| // we can compute the final estimate on the device and return the result to the host | ||
| auto const estimate = estimator.estimate(block); | ||
|
|
||
| if (block.thread_rank() == 0) { *out = estimate; } | ||
| } | ||
| } | ||
|
|
||
| int main(void) | ||
| { | ||
| using T = int; | ||
| using estimator_type = cuco::distinct_count_estimator<T>; | ||
| constexpr std::size_t num_items = 1ull << 28; // 1GB | ||
| auto const sketch_size_kb = 32_KB; | ||
|
|
||
| thrust::device_vector<T> items(num_items); | ||
|
|
||
| // Generate `num_items` distinct items | ||
| thrust::sequence(items.begin(), items.end(), 0); | ||
|
|
||
| // Initialize the estimator | ||
| estimator_type estimator(sketch_size_kb); | ||
|
|
||
| // Add all items to the estimator | ||
| estimator.add(items.begin(), items.end()); | ||
|
|
||
| // Calculate the cardinality estimate from the bulk operation | ||
| std::size_t const estimated_cardinality_bulk = estimator.estimate(); | ||
|
|
||
| // Clear the estimator so it can be reused | ||
| estimator.clear(); | ||
|
|
||
| // Number of dynamic shared memory bytes required to store a CTA-local sketch | ||
| auto const sketch_bytes = estimator.sketch_bytes(); | ||
|
|
||
| // Call the custom kernel and pass a non-owning reference to the estimator to the GPU | ||
| piggyback_kernel<<<10, 512, sketch_bytes>>>(estimator.ref(), items.begin(), num_items); | ||
|
sleeepyjack marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Calculate the cardinality estimate from the custom kernel | ||
| std::size_t const estimated_cardinality_custom = estimator.estimate(); | ||
|
|
||
| thrust::device_vector<std::size_t> device_estimate(1); | ||
| device_estimate_kernel<typename estimator_type::ref_type<cuda::thread_scope_block>> | ||
| <<<1, 512, sketch_bytes>>>(sketch_size_kb, items.begin(), num_items, device_estimate.begin()); | ||
|
|
||
| std::size_t const estimated_cardinality_device = device_estimate[0]; | ||
|
|
||
| if (estimated_cardinality_custom == estimated_cardinality_bulk and | ||
| estimated_cardinality_device == estimated_cardinality_bulk) { | ||
| std::cout << "Success! Cardinality estimates are identical" << std::endl; | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to myself: Update these links once the PR is ready to be merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the note still valid?